"""funnies crossword construction. Per functional design (BR1.2 / Q1=B): AI-assisted — an injectable clue-drafter (typically the cloud model) supplies across/down clues; funnies lays them out onto a 10x10 grid (Q2=B). The clue-drafter is injectable so tests stub it (no real network in CI). The grid layout is deterministic/dependency-free. """ from __future__ import annotations from typing import Protocol from .model import Crossword class ClueDrafter(Protocol): """Interface for the AI-assisted clue drafting (the sanctioned cloud call).""" def draft_clues( self, theme_words: list[str], context: str, grid_size: int ) -> tuple[list[str], list[str]]: ... class StubClueDrafter: """Deterministic stub used in tests / offline: simple auto-clues.""" def draft_clues(self, theme_words, context, grid_size=10): across = [f"Word {i + 1}" for i in range(len(theme_words))] down = ["Crossing clue"] * min(4, len(theme_words)) return across, down GRID_SIZE = 10 def _blank_grid() -> list[list[str]]: return [[" " for _ in range(GRID_SIZE)] for _ in range(GRID_SIZE)] def _lay_out_across(words: list[str], base: list[list[str]]) -> None: """Place theme words horizontally on row r for the grid.""" for r, word in enumerate(words[:GRID_SIZE]): if r >= GRID_SIZE: break w = word[:GRID_SIZE] base[r][: len(w)] = list(w.upper()) def build_crossword( theme_words: list[str], context: str, clue_drafter: ClueDrafter | None = None, ) -> Crossword: """Build a 10x10 crossword with AI-drafted clues (BR1.2, Q2=B).""" drafter = clue_drafter or StubClueDrafter() across, down = drafter.draft_clues(theme_words, context, GRID_SIZE) grid = _blank_grid() _lay_out_across(theme_words, grid) solution = [row[:] for row in grid] return Crossword( crossword_id="xw-1", grid=grid, clues_across=across, clues_down=down, solution=solution, )