Files
kdp_generator/models.py
T

132 lines
3.0 KiB
Python
Raw Normal View History

2026-03-24 19:36:04 +02:00
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
class Direction(Enum):
ACROSS = "across"
DOWN = "down"
@dataclass
class WordClue:
"""A word-clue pair parsed from the input CSV."""
word: str
clue: str
difficulty: Optional[str] = None
category: Optional[str] = None
@dataclass
class Cell:
"""A single cell in the crossword grid."""
letter: Optional[str] = None
number: Optional[int] = None
@property
def is_empty(self) -> bool:
return self.letter is None
@property
def is_numbered(self) -> bool:
return self.number is not None
@dataclass
class PlacedWord:
"""A word placed on the crossword grid with its position and direction."""
word: str
clue: str
row: int
col: int
direction: Direction
number: Optional[int] = None
@property
def length(self) -> int:
return len(self.word)
def cells(self) -> list[tuple[int, int]]:
"""Return the (row, col) positions this word occupies."""
if self.direction == Direction.ACROSS:
return [(self.row, self.col + i) for i in range(self.length)]
return [(self.row + i, self.col) for i in range(self.length)]
def end_row(self) -> int:
if self.direction == Direction.DOWN:
return self.row + self.length - 1
return self.row
def end_col(self) -> int:
if self.direction == Direction.ACROSS:
return self.col + self.length - 1
return self.col
@dataclass
class NumberedClue:
"""A clue with its assigned number, ready for display."""
number: int
clue: str
word: str
direction: Direction
@dataclass
class Crossword:
"""A complete crossword puzzle: grid, placed words, and numbered clues."""
grid: list[list[Cell]]
placed_words: list[PlacedWord]
across_clues: list[NumberedClue] = field(default_factory=list)
down_clues: list[NumberedClue] = field(default_factory=list)
width: int = 0
height: int = 0
@property
def word_count(self) -> int:
return len(self.placed_words)
@property
def density(self) -> float:
"""Fraction of grid cells that contain letters."""
total = self.width * self.height
if total == 0:
return 0.0
filled = sum(1 for row in self.grid for cell in row if not cell.is_empty)
return filled / total
@dataclass
class TrimSize:
"""KDP trim size specification in inches."""
width: float
height: float
label: str
def __str__(self) -> str:
return self.label
TRIM_SIZES: dict[str, TrimSize] = {
"8.5x11": TrimSize(8.5, 11.0, "8.5\" x 11\""),
"6x9": TrimSize(6.0, 9.0, "6\" x 9\""),
"8x10": TrimSize(8.0, 10.0, "8\" x 10\""),
}
@dataclass
class PuzzleBook:
"""A collection of crossword puzzles forming a book."""
title: str
author: str
puzzles: list[Crossword] = field(default_factory=list)
trim_size: TrimSize = field(default_factory=lambda: TRIM_SIZES["8.5x11"])
solutions_at_end: bool = False