"""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'")
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(
'
Nera
Esame 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 = '