"""Build the printable Italian playtest edition: `uv run python edition/build.py`. Writes `edition/it/playtest.html` (A4, print from the browser) from `RULES.it.md`, the quick reference `edition/it/riepilogo.md`, the card texts `edition/it/carte.toml`, the board (`board/quadrant.txt`, `board/legend.toml`) and the engine's deck. Never edit the HTML by hand. """ import html import tomllib from collections.abc import Iterator, Mapping from pathlib import Path import markdown from dojo.board import BOARD_SIZE, QUADRANT_SIZE, Board, Pos, Square, load_board from dojo.grid import Grid from dojo.rules import BELTS, DEFAULT_RULES ROOT = Path(__file__).resolve().parent.parent EDITION = ROOT / "edition" / "it" OUTPUT = EDITION / "playtest.html" TITLE = "Il Labirinto del Dojo" EDITION_LABEL = "Bozza per playtest v0.9" TILE_CELL_MM = 23.0 OVERVIEW_CELL_MM = 11.5 CARDS_PER_PAGE = 9 BELT_NAMES = ("Bianca", "Gialla", "Arancione", "Verde", "Blu", "Marrone") BELT_COLOURS = ("#ffffff", "#fde047", "#fb923c", "#4ade80", "#60a5fa", "#a16207") TILE_NAMES = { (0, 0): "in alto a sinistra", (0, 1): "in alto a destra", (1, 1): "in basso a destra", (1, 0): "in basso a sinistra", } # Square code -> (fill, name, symbol). Tunnels are added per pair, see tunnel_labels(). SQUARE_STYLE: Mapping[str, tuple[str, str, str]] = { Square.SAFE: ("#dbeafe", "", ""), Square.DOJO: ("#44403c", "", ""), Square.TATAMI: ("#fbf7ee", "", ""), Square.ALLENAMENTO: ("#bbf7d0", "Allenamento", "+1"), Square.FATICA: ("#fecaca", "Fatica", "\u22121"), Square.NEBBIA: ("#ddd6fe", "Nebbia", "?"), Square.PASSO_FALSO: ("#fed7aa", "Passo Falso", "↩"), Square.SFIDA: ("#fde68a", "Sfida", "VS"), } TUNNEL_FILL = "#bae6fd" ENTRANCE_COLOUR = "#eab308" def tunnel_labels(board: Board) -> dict[Pos, int]: """A number per Tunnel pair, unique on the whole board (the source repeats its digits).""" labels: dict[Pos, int] = {} for square in sorted(board.tunnels): if square not in labels: number = len(set(labels.values())) + 1 labels[square] = labels[board.tunnels[square]] = number return labels def board_svg(board: Board, rows: range, cols: range, cell: float) -> str: grid = board.to_grid() tunnels = tunnel_labels(board) width, height = len(cols) * cell, len(rows) * cell parts = [ f'' ] for r in rows: for c in cols: x, y = (c - cols.start) * cell, (r - rows.start) * cell parts.append(_cell_svg(board.code(Pos(r, c)), tunnels.get(Pos(r, c)), x, y, cell)) parts.extend(_label_svg(board, rows, cols, cell)) parts.extend(_walls_svg(grid, rows, cols, cell)) parts.extend(_entrances_svg(board, rows, cols, cell)) parts.append("") return "".join(parts) def _cell_svg(code: str, tunnel: int | None, x: float, y: float, cell: float) -> str: if tunnel is not None: fill, name, symbol = TUNNEL_FILL, "Tunnel", str(tunnel) else: fill, name, symbol = SQUARE_STYLE[code] parts = [ f'' ] if symbol: parts.append( f'{html.escape(symbol)}' ) if name: parts.append( f'{html.escape(name)}' ) return "".join(parts) def _label_svg(board: Board, rows: range, cols: range, cell: float) -> Iterator[str]: """ "Stanza Sicura" and "Dojo" written across their rooms, where the room lies in view.""" for code, text, colour in ( (Square.SAFE, "Stanza Sicura", "#1e3a8a"), (Square.DOJO, "DOJO", "#fafaf9"), ): squares = [p for p in board.cells_with(code) if p.row in rows and p.col in cols] for group in _rooms(squares): row = sum(p.row for p in group) / len(group) - rows.start + 0.5 col = sum(p.col for p in group) / len(group) - cols.start + 0.5 size = cell * (0.3 if code is Square.DOJO else 0.2) yield ( f'{text}' ) def _rooms(squares: list[Pos]) -> list[list[Pos]]: """Orthogonally connected groups of ``squares``.""" left, groups = set(squares), [] while left: stack, group = [left.pop()], [] while stack: p = stack.pop() group.append(p) for q in ( Pos(p.row + 1, p.col), Pos(p.row - 1, p.col), Pos(p.row, p.col + 1), Pos(p.row, p.col - 1), ): if q in left: left.remove(q) stack.append(q) groups.append(group) return groups def _walls_svg(grid: Grid, rows: range, cols: range, cell: float) -> Iterator[str]: stroke = cell * 0.09 style = f'stroke="#1c1917" stroke-width="{stroke}" stroke-linecap="square"' for r in range(rows.start, rows.stop + 1): for c in cols: if grid.h_walls[r][c]: x, y = (c - cols.start) * cell, (r - rows.start) * cell yield f'' for r in rows: for c in range(cols.start, cols.stop + 1): if grid.v_walls[r][c]: x, y = (c - cols.start) * cell, (r - rows.start) * cell yield f'' def _entrances_svg(board: Board, rows: range, cols: range, cell: float) -> Iterator[str]: """A gold bar on every open edge into the Dojo, so the entrances stand out.""" style = f'stroke="{ENTRANCE_COLOUR}" stroke-width="{cell * 0.12}"' for square in sorted(board.dojo): if square.row not in rows or square.col not in cols: continue for outside in board.neighbours(square): if outside in board.dojo: continue x = (max(square.col, outside.col) - cols.start) * cell y = (max(square.row, outside.row) - rows.start) * cell if square.row == outside.row: x1, y1, x2, y2 = x, y + cell * 0.2, x, y + cell * 0.8 else: x1, y1, x2, y2 = x + cell * 0.2, y, x + cell * 0.8, y yield f'' def legend_html() -> str: items = [ (fill, name, symbol) for code, (fill, name, symbol) in SQUARE_STYLE.items() if code not in (Square.SAFE, Square.DOJO, Square.TATAMI) ] items.append((TUNNEL_FILL, "Tunnel (stesso numero)", "3")) items.append((ENTRANCE_COLOUR, "Ingresso del Dojo", "")) cells = "".join( f'' f"{html.escape(symbol)}{html.escape(name)}" for fill, name, symbol in items ) return f'

{cells}

' def track_html() -> str: per_belt = DEFAULT_RULES.stripes_per_belt rows = [] for belt in range(len(BELTS)): cells = "".join( f'{belt * per_belt + stripe}' for stripe in range(per_belt) ) rows.append(f"{BELT_NAMES[belt]}{cells}") rows.append( 'NeraEsame Finale superato: vittoria' ) return ( '

Scala delle cinture

' "

Un segnalino per giocatore. Una tacca = una casella; una cintura = una riga. " "Dal 15 (Marrone) il Dojo Centrale è aperto.

" f'{"".join(reversed(rows))}
' ) def cards_html(texts: Mapping[str, Mapping[str, str]]) -> str: missing = {card.value for card, _ in DEFAULT_RULES.deck} ^ set(texts) if missing: raise SystemExit(f"carte.toml and the engine's deck disagree on: {sorted(missing)}") cards = [texts[card.value] for card, copies in DEFAULT_RULES.deck for _ in range(copies)] pages = [] for start in range(0, len(cards), CARDS_PER_PAGE): faces = "".join(_card_html(card) for card in cards[start : start + CARDS_PER_PAGE]) pages.append(f'
{faces}
') return "".join(pages) def _card_html(card: Mapping[str, str]) -> str: kind = card["tipo"] note = '

Tienila coperta. Al massimo una.

' if kind == "Tecnica" else "" return ( f'

{html.escape(kind)}

' f'

{html.escape(card["nome"])}

{html.escape(card["testo"])}

' f'{note}

Nebbia del Dojo

' ) def board_pages(board: Board) -> str: whole = range(BOARD_SIZE) pages = [ '

Tabellone intero

' f"{board_svg(board, whole, whole, OVERVIEW_CELL_MM)}{legend_html()}" "

Muri: linee spesse. Barre gialle: ingressi del Dojo (solo per chi è Marrone). " "I Tunnel con lo stesso numero sono collegati.

" ] for (qr, qc), name in TILE_NAMES.items(): rows = range(qr * QUADRANT_SIZE, (qr + 1) * QUADRANT_SIZE) cols = range(qc * QUADRANT_SIZE, (qc + 1) * QUADRANT_SIZE) pages.append( f'

Tabellone da gioco, foglio ' f"{len(pages)} di 4: {name}. Ritaglia lungo il bordo esterno e accosta i fogli." f"

{board_svg(board, rows, cols, TILE_CELL_MM)}
" ) return "".join(pages) def rules_html(path: Path) -> str: return markdown.markdown(path.read_text(encoding="utf-8"), extensions=["tables", "toc"]) def build() -> str: board = load_board(ROOT / "board" / "quadrant.txt", ROOT / "board" / "legend.toml") with (EDITION / "carte.toml").open("rb") as handle: texts = tomllib.load(handle) style = (EDITION / "style.css").read_text(encoding="utf-8") return ( '' f"{TITLE}" f'

{TITLE}

{EDITION_LABEL}

' f"{rules_html(EDITION / 'riepilogo.md')}
" f'
{rules_html(ROOT / "RULES.it.md")}
' f"{track_html()}{board_pages(board)}{cards_html(texts)}" "" ) if __name__ == "__main__": OUTPUT.write_text(build(), encoding="utf-8") print(f"wrote {OUTPUT.relative_to(ROOT)}")