Files
newspaper_wedding/newspaper/funnies/crossword.py
T
2026-09-14 11:57:22 +10:00

67 lines
2.0 KiB
Python

"""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,
)