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