Spaces:
Running
Running
| from __future__ import annotations | |
| from dataclasses import dataclass, field | |
| from typing import Literal, List, Set, Dict, Optional | |
| from datetime import datetime | |
| import uuid | |
| Direction = Literal["H", "V"] | |
| class Coord: | |
| x: int # row, 0-based | |
| y: int # col, 0-based | |
| def in_bounds(self, size: int) -> bool: | |
| return 0 <= self.x < size and 0 <= self.y < size | |
| class Word: | |
| text: str | |
| start: Coord | |
| direction: Direction | |
| cells: List[Coord] = field(default_factory=list) | |
| def __post_init__(self): | |
| self.text = self.text.upper() | |
| if self.direction not in ("H", "V"): | |
| raise ValueError("direction must be 'H' or 'V'") | |
| if not self.text.isalpha(): | |
| raise ValueError("word must be alphabetic A–Z only") | |
| # compute cells based on start and direction | |
| length = len(self.text) | |
| cells: List[Coord] = [] | |
| for i in range(length): | |
| if self.direction == "H": | |
| cells.append(Coord(self.start.x, self.start.y + i)) | |
| else: | |
| cells.append(Coord(self.start.x + i, self.start.y)) | |
| object.__setattr__(self, "cells", cells) | |
| def length(self) -> int: | |
| return len(self.text) | |
| def last_cell(self) -> Coord: | |
| return self.cells[-1] | |
| class Puzzle: | |
| """Puzzle configuration and metadata. | |
| Fields | |
| - words: The list of placed words. | |
| - radar: Points used to render the UI radar (defaults to each word's last cell). | |
| - may_overlap: If True, words may overlap on shared letters (e.g., a crossword-style junction). | |
| - spacer: (2 to -3) Controls proximity and overlap rules between distinct words: | |
| * spacer = 0 -> words may be directly adjacent (touching next to each other). | |
| * spacer = 1 -> at least 1 blank cell must separate words (no immediate adjacency). | |
| * spacer > 1 -> enforce that many blank cells of separation. | |
| * spacer < 0 -> allow overlaps on a common letter; abs(spacer) is the maximum | |
| number of trailing letters each word may extend beyond the | |
| shared letter (e.g., -3 allows up to 3 letters past the overlap). | |
| Note: These are configuration hints for the generator/logic. Enforcement is not implemented here. | |
| """ | |
| words: List[Word] | |
| radar: List[Coord] = field(default_factory=list) | |
| may_overlap: bool = False | |
| spacer: int = 1 | |
| # Unique identifier for this puzzle instance (used for deterministic regen and per-session assets) | |
| uid: str = field(default_factory=lambda: uuid.uuid4().hex) | |
| # Cached set of all word cells (computed once, used by is_game_over check) | |
| _all_word_cells: Set[Coord] = field(default_factory=set, repr=False, init=False) | |
| def __post_init__(self): | |
| pulses = [w.last_cell for w in self.words] | |
| self.radar = pulses | |
| # Pre-compute all word cells once for faster is_game_over() checks | |
| all_cells: Set[Coord] = set() | |
| for w in self.words: | |
| all_cells.update(w.cells) | |
| object.__setattr__(self, '_all_word_cells', all_cells) | |
| class GameState: | |
| grid_size: int | |
| puzzle: Puzzle | |
| revealed: Set[Coord] | |
| guessed: Set[str] | |
| score: int | |
| last_action: str | |
| can_guess: bool | |
| game_mode: Literal["classic", "too easy"] = "classic" | |
| points_by_word: Dict[str, int] = field(default_factory=dict) | |
| start_time: Optional[datetime] = None | |
| end_time: Optional[datetime] = None |