Add the Italian playtest edition (7.2.0, D53)

RULES.it.md translates RULES.md v0.9. edition/build.py renders a
printable A4 HTML (edition/it/playtest.html): quick reference, rules,
belt track, board overview and four board tiles drawn from the board
model (Tunnel pairs numbered, Dojo entrances marked), and the 22 Nebbia
cards. Adds markdown as a dev dependency.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-27 19:06:09 +02:00
parent fea6792f25
commit 84673c573d
11 changed files with 1567 additions and 3 deletions
+15
View File
@@ -6,6 +6,7 @@ balance it.
| Document | What it is | | Document | What it is |
|---|---| |---|---|
| [`RULES.md`](RULES.md) | The rulebook (English working draft). | | [`RULES.md`](RULES.md) | The rulebook (English working draft). |
| [`RULES.it.md`](RULES.it.md) | Italian rulebook (playtest edition). |
| [`board/FORMAT.md`](board/FORMAT.md) | How the board is written as text. | | [`board/FORMAT.md`](board/FORMAT.md) | How the board is written as text. |
| [`agents/plans/`](agents/plans/) | Active work plan: decisions, phases, open questions. | | [`agents/plans/`](agents/plans/) | Active work plan: decisions, phases, open questions. |
@@ -53,6 +54,20 @@ uv run dojo sim --seed 42 --jobs 1 # other dice; single process
Minutes are estimated as 45 s per turn plus 30 s per Kumite. Minutes are estimated as 45 s per turn plus 30 s per Kumite.
## Italian playtest edition
`edition/build.py` builds `edition/it/playtest.html`: quick reference, full rules, belt track,
the board (one A4 overview and four A4 tiles to cut and join), and the 22 Nebbia cards to cut
out. Open it in a browser and print on A4 at 100% scale (no "fit to page"), with background
graphics on.
```sh
uv run python edition/build.py
```
Sources: `RULES.it.md`, `edition/it/riepilogo.md` (quick reference), `edition/it/carte.toml`
(card texts), `edition/it/style.css`, and the board files. Never edit the HTML by hand.
## Development ## Development
```sh ```sh
+219
View File
@@ -0,0 +1,219 @@
# Il Labirinto del Dojo — Regolamento
*Bozza per playtest v0.9, tradotta dal regolamento inglese (RULES.md) v0.9.*
## Scopo del gioco
Si parte cintura Bianca, si sale fino alla cintura Marrone, si raggiunge il Dojo Centrale e
si supera l'Esame Finale. Il primo giocatore che indossa la Cintura Nera vince.
## Componenti
- Tabellone: griglia 16×16 con muri, stanze e tunnel; 4 Stanze Sicure negli angoli; il Dojo
Centrale (4×4, al centro).
- Fino a 8 pedine.
- La scala delle cinture, con un segnalino per giocatore.
- 2 dadi a sei facce (d6).
- Mazzo Nebbia del Dojo (22 carte).
- Carta riepilogo con gli effetti delle caselle.
## La scala delle cinture
Ogni giocatore ha una posizione da **0 a 17**. La tua cintura è il tratto in cui ti trovi:
| Posizione | Cintura |
|---|---|
| 0–2 | Bianca |
| 3–5 | Gialla |
| 6–8 | Arancione |
| 9–11 | Verde |
| 12–14 | Blu |
| 15–17 | Marrone |
- Una **tacca** = 1 passo sulla scala. Una **cintura** = 3 tacche.
- La posizione non scende mai sotto 0 e non sale mai oltre 17.
- Chi è sotto 15 non è Marrone, qualunque cintura avesse prima.
## Preparazione
- 3–4 giocatori: una pedina per Stanza Sicura d'angolo.
- 5–8 giocatori: due pedine per Stanza Sicura. Chi divide la stessa stanza non gioca mai
subito dopo l'altro.
- Tutti partono dalla posizione 0 (Bianca). Con 6 o più giocatori, tutti partono invece
da 3 (Gialla).
- *Partita più breve:* con qualunque numero di giocatori potete partire tutti da 3 (Gialla).
Si risparmia circa un quinto del tempo di gioco.
- Sorteggiate il primo giocatore; si gioca in senso orario.
## Il turno
1. Tira **2d6**.
2. Muovi **esattamente** quel numero di passi:
- solo in orizzontale o in verticale;
- mai attraverso un muro;
- mai rientrando in una casella già visitata durante questo movimento.
Se non esiste un movimento del numero intero, muovi il più possibile. (Il tabellone è
disegnato senza vicoli ciechi, quindi non dovrebbe mai succedere: è solo una garanzia.)
3. **Entrare in una casella occupata da un avversario avvia un Kumite** (tu sei
l'attaccante). Vale anche per l'ultimo passo. Uscire da una casella non avvia mai un
Kumite.
- Se vinci, il difensore torna alla Partenza e tu continui con i passi che ti restano.
Puoi affrontare più avversari nello stesso movimento (catena).
- Se perdi, torni alla Partenza e il tuo turno finisce.
4. **Ogni casella Fatica in cui entri costa 1 tacca**, compreso l'ultimo passo, prima di un
eventuale Kumite su quella casella. Fermarti lì non costa altro.
5. Quando il movimento finisce, applica l'effetto della casella su cui ti sei fermato.
## Zone sicure
**Stanze Sicure d'angolo** (3×3, due uscite su lati diversi):
- Dentro non si combatte nessun Kumite.
- Nessun effetto di casella (sono tutte Tatami).
- Più pedine possono stare sulla stessa casella.
- Le pedine dentro non possono essere bersaglio di carte o di effetti delle caselle.
- Ogni giocatore può entrare in qualunque Stanza Sicura.
- "Tornare alla Partenza" significa sempre tornare nella **tua** Stanza Sicura. Metti la
pedina sulla casella della stanza che preferisci, sia all'inizio sia quando ci torni.
**Dojo Centrale**: nessuna pedina vi resta mai dentro (vedi l'Esame Finale).
## Kumite
**Niente Kumite nel primo giro.** Finché tutti i giocatori non hanno fatto il loro primo
turno, gli avversari si trattano come pedine in meditazione (vedi
[Meditazione](#meditazione)): li attraversi, un movimento che finirebbe su uno di loro fa un
passo in più, non puoi usare un Tunnel se la sua uscita è occupata da loro, e la Sfida non
ha bersagli. Irimi (Scambio) funziona comunque: non è un Kumite.
1. Entrambi i giocatori tirano **1d6**.
2. La cintura più alta aggiunge **+1 per ogni cintura di differenza, al massimo +2**.
3. Dopo aver tirato entrambi i dadi, l'attaccante può giocare una carta Tecnica, poi il
difensore può giocarne una. Ciascuno decide vedendo entrambi i dadi; il difensore vede
anche la carta dell'attaccante. Una Tecnica si gioca solo in questo momento (anche Uke:
la giochi prima di conoscere il risultato).
4. Vince il totale più alto.
5. **Regola del Senshu**: in caso di pareggio vince l'attaccante.
### Conseguenze
| Risultato | Vincitore | Sconfitto |
|---|---|---|
| Cinture uguali | +3 | −3, torna alla Partenza |
| Vince la cintura più bassa | +3 | −3, torna alla Partenza |
| Vince la cintura più alta | niente | torna alla Partenza |
## Le caselle
| Casella | Effetto quando ti fermi |
|---|---|
| Tatami | Niente. |
| Allenamento | +1 tacca. |
| Fatica | −1 tacca per **ogni** Fatica in cui entri camminando, che tu ti fermi o no (vedi [Il turno](#il-turno)); fermarti non costa altro. Essere spostato su una Fatica senza camminare (Tunnel, Passo Falso, Irimi, ritirata con Uke, Partenza) non costa niente. |
| Nebbia del Dojo | Pesca una carta. La casella resta attiva. Quando il mazzo finisce, rimescola gli scarti. |
| Tunnel | **Puoi** teletrasportarti al Tunnel con lo stesso numero. Il movimento è già finito; la casella d'uscita non ha effetto. Se l'uscita è occupata, inizia un Kumite (tu sei l'attaccante). Non puoi usare un'uscita occupata da una pedina in meditazione. |
| Passo Falso | Torna alla casella da cui è partito questo movimento. I Kumite già combattuti durante il movimento restano validi. |
| Sfida | **Devi** combattere un Kumite contro un avversario a tua scelta fuori da una zona sicura (tu sei l'attaccante). Se non ce n'è nessuno, non succede niente. |
## Meditazione
Una pedina in meditazione non può essere affrontata né presa di mira. Le altre pedine la
attraversano liberamente. Se il tuo movimento finisce esattamente sulla sua casella, fai un
passo in più seguendo le normali regole di movimento (può avviare un Kumite); si applica
l'effetto della casella su cui ti fermi alla fine. Se nessun passo in più è possibile,
resti dove sei.
Lo stesso passo in più si applica ogni volta che il tuo movimento finirebbe su una casella
(fuori dalle Stanze Sicure) con una pedina che non puoi affrontare: per esempio un
difensore che ha perso ma è stato salvato da Uke.
## Il mazzo Nebbia del Dojo
Le carte Nebbia sono imprevisti, non regali: **ogni carta va applicata**, anche quando ti
danneggia. Se una carta richiede un bersaglio e non ce n'è nessuno, non succede niente.
**Le carte Tecnica sono segrete:**
- Quando peschi una Tecnica, non mostrarla: tienila coperta davanti a te.
- Tutti possono vedere *che* ne hai una, non *quale*.
- Ne tieni **al massimo una**. Quando ne peschi una seconda, tienine una e scarta l'altra
**coperta**, così nessuno sa quale hai tenuto.
- Una Tecnica si rivela solo quando la giochi; poi va negli scarti.
Tutte le altre carte si mostrano e si applicano subito.
| Tipo | Carta | Effetto | Copie |
|---|---|---|---|
| Tecnica | Kiai | +2 al tuo totale nel Kumite. | 3 |
| Tecnica | Uke (Parata) | Se perdi, non torni alla Partenza (le tacche cambiano comunque). Un difensore sconfitto resta sulla sua casella; un attaccante sconfitto torna alla casella da cui veniva, e il suo turno finisce senza effetto di casella. | 2 |
| Tecnica | Kaeshi (Contrattacco) | Ritira il tuo dado. | 2 |
| Movimento | Tsugi-ashi | Tira 2d6 e fai un altro movimento completo. Quel movimento ha una sua casella di partenza (per il Passo Falso). | 2 |
| Movimento | Tunnel Segreto | Teletrasportati su un Tunnel a tua scelta (uscita occupata: Kumite, tu sei l'attaccante). Non puoi scegliere un Tunnel occupato da una pedina in meditazione. | 1 |
| Movimento | Irimi (Scambio) | Scambia il posto con un avversario a tua scelta fuori da una zona sicura. Nessun Kumite, nessun effetto di casella. | 2 |
| Interazione | Sfida | Combatti un Kumite contro un avversario a tua scelta fuori da una zona sicura. Tu sei l'attaccante. | 2 |
| Evento | Il Sensei Osserva | Il giocatore (o i giocatori) più in basso sulla scala guadagna +2. | 1 |
| Evento | Il Peso della Cintura | Il giocatore (o i giocatori) più in alto sulla scala perde 1. | 1 |
| Evento | Allenamento Intensivo | +1 tacca. | 2 |
| Evento | Colpo Proibito | −1 tacca. | 1 |
| Evento | Passo Falso | Torna alla casella da cui è partito questo movimento. | 2 |
| Evento | Meditazione | Salti il prossimo turno. Fino alla fine del turno saltato non puoi essere affrontato né preso di mira. | 1 |
## Il Dojo Centrale e l'Esame Finale
- Solo un giocatore Marrone (posizione 15 o più) può entrare. Per tutti gli altri il Dojo è
un muro.
- Entrare costa un passo e non serve il numero esatto: raggiungi una casella accanto a un
ingresso con almeno 1 passo rimasto ed entra. Il movimento finisce subito.
- Puoi entrare nello stesso movimento in cui sei diventato Marrone vincendo un Kumite.
- La cintura cambia subito anche al contrario: se una Fatica attraversata durante il
movimento ti porta sotto 15, il Dojo è chiuso per il resto di quel movimento.
- L'Esame Finale inizia subito. Difende **l'avversario più in alto sulla scala** (in caso
di parità, lo sfidante sceglie tra loro). La pedina del difensore non si muove. Chi è in
meditazione non può difendere; se nessuno può, torni alla casella da cui sei entrato e in
questo turno non c'è Esame.
- L'Esame è un Kumite con lo sfidante come attaccante; valgono il Senshu e le carte Tecnica.
- **Il difensore combatte come Cintura Nera**, qualunque sia la sua cintura: lo sfidante
Marrone è una cintura sotto, quindi il difensore aggiunge +1.
- **Vince lo sfidante**: indossa la Cintura Nera e vince la partita.
- **Perde lo sfidante**: −3 e torna alla Partenza. Il difensore non guadagna niente.
- Se lo sfidante gioca Uke e perde, torna alla casella da cui è entrato invece che alla
Partenza.
## Domande frequenti
**Quali effetti posso rifiutare?**
Solo la casella Tunnel ("puoi teletrasportarti"). Tutto il resto, caselle e carte Nebbia,
va applicato, comprese Sfida, Irimi e Tunnel Segreto.
**Sono stato spostato senza camminare (Passo Falso, Irimi, ritirata con Uke). Questo avvia
un Kumite o l'effetto della casella in cui arrivo?**
No in entrambi i casi (una Fatica costa una tacca solo quando ci entri camminando). Solo
arrivare con un Tunnel o con il Tunnel Segreto avvia un Kumite su una casella occupata, e
anche in quel caso la casella d'uscita non ha effetto.
**Sono diventato Marrone vincendo un Kumite a metà movimento. Posso entrare nel Dojo con i
passi che mi restano?**
Sì. La cintura cambia subito, quindi il Dojo si apre per il resto di quel movimento.
**Sono a 15 e la mia strada verso il Dojo passa su una Fatica. Posso prenderla?**
No: la Fatica ti porta a 14 prima dell'ingresso, e il Dojo si chiude. Prendi una strada
senza Fatica, oppure guadagna prima una tacca.
**Il mio movimento finirebbe su una pedina che non posso affrontare. Che faccio?**
Fai un passo in più (vedi [Meditazione](#meditazione)). Se non è possibile, resti dove sei.
**Sono bloccato e non posso muovere il numero intero.**
Muovi il più possibile. Su un tabellone ben disegnato non dovrebbe mai succedere.
**Uke mi ha salvato da attaccante. Applico l'effetto della casella in cui torno?**
No. Perdere un Kumite fa sempre finire il turno.
**Ho pescato Tsugi-ashi e poi sono finito su un Passo Falso nel movimento in più. Dove
torno?**
Alla casella da cui è partito il movimento in più, non a quella da cui è iniziato il turno.
**Gli avversari possono sapere quale Tecnica ho?**
No. Sanno solo se ne hai una. Vedi [Il mazzo Nebbia del Dojo](#il-mazzo-nebbia-del-dojo).
+1 -1
View File
@@ -84,7 +84,7 @@ Each player has a position from **0 to 17**. Your belt is the segment you are in
**No Kumite in the first round.** Until every player has had a turn, opponents are treated **No Kumite in the first round.** Until every player has had a turn, opponents are treated
like meditating pawns (see [Meditazione](#meditazione)): you pass through them, a move that like meditating pawns (see [Meditazione](#meditazione)): you pass through them, a move that
would end on one takes an extra step, their Tunnel exit cannot be used, and Sfida has no would end on one takes an extra step, their Tunnel exit cannot be used, and Sfida has no
target. Irimi still works: it is not a Kumite. target. Irimi (scambio) still works: it is not a Kumite.
1. Both players roll **1d6**. 1. Both players roll **1d6**.
2. The higher belt adds **+1 per belt of difference, maximum +2**. 2. The higher belt adds **+1 per belt of difference, maximum +2**.
@@ -69,6 +69,7 @@ Deliverables:
| D50 | **Length accepted as is; a failed Exam still costs 3 stripes.** L1 (Brown floor against Fatica) rejected; L2 (failed Exam −1 / 0) saves only 3–5 min on the mean and weakens comebacks (6.11.4). `exam_failure_loss` stays a simulator switch. At 4 players mixed and balanced are inside the target; casual (91 / 122) and all-aggressive (90 / 127) stay slightly outside, within the "1–2 h" promise; the shorter-game start on Yellow (D48) is the players' lever | Developer, 6.11 results. | | D50 | **Length accepted as is; a failed Exam still costs 3 stripes.** L1 (Brown floor against Fatica) rejected; L2 (failed Exam −1 / 0) saves only 3–5 min on the mean and weakens comebacks (6.11.4). `exam_failure_loss` stays a simulator switch. At 4 players mixed and balanced are inside the target; casual (91 / 122) and all-aggressive (90 / 127) stay slightly outside, within the "1–2 h" promise; the shorter-game start on Yellow (D48) is the players' lever | Developer, 6.11 results. |
| D51 | **Bots decide alike in every corner** (start square and tied paths broken in the player's own frame), overriding D40 for this bias: the old tie-break by absolute board order made corners 1 and 2 win 25–28% and corners 0 and 3 23% | Developer, 6.2.1 diagnosis. | | D51 | **Bots decide alike in every corner** (start square and tied paths broken in the player's own frame), overriding D40 for this bias: the old tie-break by absolute board order made corners 1 and 2 win 25–28% and corners 0 and 3 23% | Developer, 6.2.1 diagnosis. |
| D52 | **No first-player compensation**: the seat order stays as it is. At 4 players every seat is inside 25% ± 3 pp; +1 stripe for the first player gains about 1 point (6.2.4), not worth an extra rule. `first_player_bonus` stays a simulator switch (no CLI axis) | Developer, 6.2.4 results. | | D52 | **No first-player compensation**: the seat order stays as it is. At 4 players every seat is inside 25% ± 3 pp; +1 stripe for the first player gains about 1 point (6.2.4), not worth an extra rule. `first_player_bonus` stays a simulator switch (no CLI axis) | Developer, 6.2.4 results. |
| D53 | **Italian playtest edition format**: source `RULES.it.md` (full rules, without the tested alternatives) plus `edition/it/riepilogo.md` and `edition/it/carte.toml`; `edition/build.py` renders one local printable A4 HTML (`edition/it/playtest.html`): quick reference, rules, belt track, board overview plus four A4 tiles, 22 cut-out cards. Board drawn from the board model; card copies from the engine's deck. Japanese names keep an Italian gloss ("Uke (Parata)"). Glossary: stripe = tacca, track = scala delle cinture, Safe Room = Stanza Sicura, Start = Partenza, round = giro, attacker / defender = attaccante / difensore | Developer (format, printable board, gloss, local HTML only). |
## Phases ## Phases
@@ -179,7 +180,7 @@ Deliverables:
- One change at a time. When a change is adopted, re-run the 6.1 baseline so later steps start from the current rules, and log the decision (D-number) here and in `RULES.md`. - One change at a time. When a change is adopted, re-run the 6.1 baseline so later steps start from the current rules, and log the decision (D-number) here and in `RULES.md`.
- Results go in this file as a table per step (command line, scenario, key measures). - Results go in this file as a table per step (command line, scenario, key measures).
**Next action:** Phase 6 is complete; Phase 7 (7.1 freeze the rules) when the developer says so. 6.9 skipped (D44), 6.4.1 parked (D49). Everything up to D52 is committed; the 6.8 final run is not; commit only when the developer asks. **Next action:** the developer's playtest with the Italian edition (7.2.0); then 7.2.1 and 7.1. 6.9 skipped (D44), 6.4.1 parked (D49). Everything up to D52 is committed; 7.2.0 and D53 are not; commit only when the developer asks.
**Targets** (confirmed by the developer as a starting point, D32): **Targets** (confirmed by the developer as a starting point, D32):
@@ -477,8 +478,10 @@ Deliverables:
- At 6 players the two seats alone in a Safe Room win 17–20% vs 13–17% for the four sharing (6.2.3); accepted with D52. - At 6 players the two seats alone in a Safe Room win 17–20% vs 13–17% for the four sharing (6.2.3); accepted with D52.
- [ ] 7.1 Freeze rules. - [ ] 7.1 Freeze rules.
- [ ] 7.2 Italian edition. - [ ] 7.2 Italian edition.
- [x] 7.2.0 Playtest edition (D53): `RULES.it.md` translated from RULES.md v0.9; quick reference; `edition/build.py` (dev dependency `markdown`) writes `edition/it/playtest.html`, 15 A4 pages when printed (checked by printing to PDF with headless Chromium): quick reference, rules (5 pages), belt track, board overview, 4 board tiles (23 mm squares, Tunnel pairs numbered 1–8 across the board, Dojo entrances marked in gold), 3 card sheets (63 × 88 mm). No card backs.
- [ ] 7.2.1 Playtest feedback folded into `RULES.md` and `RULES.it.md`.
- [ ] 7.3 Physical tracking method and components; printable board. - [ ] 7.3 Physical tracking method and components; printable board.
- [ ] Tunnel pairs need unique labels on the printed board (the text format repeats each digit in all 4 quadrants). - [x] Tunnel pairs need unique labels on the printed board (the text format repeats each digit in all 4 quadrants): numbered 1–8 by `edition/build.py`.
## Questions ## Questions
+274
View File
@@ -0,0 +1,274 @@
"""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'<svg class="board" xmlns="http://www.w3.org/2000/svg" width="{width}mm" '
f'height="{height}mm" viewBox="0 0 {width} {height}">'
]
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("</svg>")
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'<rect x="{x}" y="{y}" width="{cell}" height="{cell}" fill="{fill}" '
f'stroke="#a8a29e" stroke-width="{cell * 0.01}"/>'
]
if symbol:
parts.append(
f'<text x="{x + cell / 2}" y="{y + cell * 0.62}" font-size="{cell * 0.38}" '
f'text-anchor="middle" font-weight="700">{html.escape(symbol)}</text>'
)
if name:
parts.append(
f'<text x="{x + cell / 2}" y="{y + cell * 0.9}" font-size="{cell * 0.14}" '
f'text-anchor="middle">{html.escape(name)}</text>'
)
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 x="{col * cell}" y="{row * cell + size / 3}" font-size="{size}" '
f'text-anchor="middle" font-weight="700" fill="{colour}">{text}</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'<line x1="{x}" y1="{y}" x2="{x + cell}" y2="{y}" {style}/>'
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'<line x1="{x}" y1="{y}" x2="{x}" y2="{y + cell}" {style}/>'
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'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" {style}/>'
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'<span class="key"><span class="swatch" style="background:{fill}">'
f"{html.escape(symbol)}</span>{html.escape(name)}</span>"
for fill, name, symbol in items
)
return f'<p class="legend">{cells}</p>'
def track_html() -> str:
per_belt = DEFAULT_RULES.stripes_per_belt
rows = []
for belt in range(len(BELTS)):
cells = "".join(
f'<td style="background:{BELT_COLOURS[belt]}">{belt * per_belt + stripe}</td>'
for stripe in range(per_belt)
)
rows.append(f"<tr><th>{BELT_NAMES[belt]}</th>{cells}</tr>")
rows.append(
'<tr class="black"><th>Nera</th><td colspan="3">Esame Finale superato: vittoria</td></tr>'
)
return (
'<section class="page"><h2>Scala delle cinture</h2>'
"<p>Un segnalino per giocatore. Una tacca = una casella; una cintura = una riga. "
"Dal 15 (Marrone) il Dojo Centrale è aperto.</p>"
f'<table class="track">{"".join(reversed(rows))}</table></section>'
)
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'<section class="page cards">{faces}</section>')
return "".join(pages)
def _card_html(card: Mapping[str, str]) -> str:
kind = card["tipo"]
note = '<p class="note">Tienila coperta. Al massimo una.</p>' if kind == "Tecnica" else ""
return (
f'<div class="card {html.escape(kind.lower())}"><p class="kind">{html.escape(kind)}</p>'
f'<h3>{html.escape(card["nome"])}</h3><p class="text">{html.escape(card["testo"])}</p>'
f'{note}<p class="deck">Nebbia del Dojo</p></div>'
)
def board_pages(board: Board) -> str:
whole = range(BOARD_SIZE)
pages = [
'<section class="page"><h2>Tabellone intero</h2>'
f"{board_svg(board, whole, whole, OVERVIEW_CELL_MM)}{legend_html()}"
"<p>Muri: linee spesse. Barre gialle: ingressi del Dojo (solo per chi è Marrone). "
"I Tunnel con lo stesso numero sono collegati.</p></section>"
]
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'<section class="page tile"><p class="tile-name">Tabellone da gioco, foglio '
f"{len(pages)} di 4: {name}. Ritaglia lungo il bordo esterno e accosta i fogli."
f"</p>{board_svg(board, rows, cols, TILE_CELL_MM)}</section>"
)
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 (
'<!doctype html><html lang="it"><head><meta charset="utf-8">'
f"<title>{TITLE}</title><style>{style}</style></head><body>"
f'<section class="page cover"><h1>{TITLE}</h1><p class="edition">{EDITION_LABEL}</p>'
f"{rules_html(EDITION / 'riepilogo.md')}</section>"
f'<section class="page rules">{rules_html(ROOT / "RULES.it.md")}</section>'
f"{track_html()}{board_pages(board)}{cards_html(texts)}"
"</body></html>"
)
if __name__ == "__main__":
OUTPUT.write_text(build(), encoding="utf-8")
print(f"wrote {OUTPUT.relative_to(ROOT)}")
+67
View File
@@ -0,0 +1,67 @@
# Testo delle carte Nebbia del Dojo, edizione italiana. Le chiavi sono i nomi delle carte
# nel motore (dojo.rules.Card); le copie vengono dal mazzo del motore (DEFAULT_DECK).
["Kiai"]
tipo = "Tecnica"
nome = "Kiai"
testo = "Gioca dopo i dadi di un Kumite: +2 al tuo totale."
["Uke"]
tipo = "Tecnica"
nome = "Uke (Parata)"
testo = "Gioca dopo i dadi di un Kumite. Se perdi, non torni alla Partenza (le tacche cambiano comunque). Da difensore resti sulla tua casella; da attaccante torni alla casella da cui venivi e il turno finisce."
["Kaeshi"]
tipo = "Tecnica"
nome = "Kaeshi (Contrattacco)"
testo = "Gioca dopo i dadi di un Kumite: ritira il tuo dado."
["Tsugi-ashi"]
tipo = "Movimento"
nome = "Tsugi-ashi"
testo = "Tira 2d6 e fai un altro movimento completo."
["Tunnel Segreto"]
tipo = "Movimento"
nome = "Tunnel Segreto"
testo = "Teletrasportati su un Tunnel a tua scelta. Uscita occupata: Kumite, tu sei l'attaccante."
["Irimi"]
tipo = "Movimento"
nome = "Irimi (Scambio)"
testo = "Scambia il posto con un avversario a tua scelta fuori da una zona sicura. Nessun Kumite, nessun effetto di casella."
["Sfida"]
tipo = "Interazione"
nome = "Sfida"
testo = "Combatti un Kumite contro un avversario a tua scelta fuori da una zona sicura. Tu sei l'attaccante."
["Il Sensei Osserva"]
tipo = "Evento"
nome = "Il Sensei Osserva"
testo = "Il giocatore (o i giocatori) più in basso sulla scala guadagna +2."
["Il Peso della Cintura"]
tipo = "Evento"
nome = "Il Peso della Cintura"
testo = "Il giocatore (o i giocatori) più in alto sulla scala perde 1."
["Allenamento Intensivo"]
tipo = "Evento"
nome = "Allenamento Intensivo"
testo = "+1 tacca."
["Colpo Proibito"]
tipo = "Evento"
nome = "Colpo Proibito"
testo = "−1 tacca."
["Passo Falso"]
tipo = "Evento"
nome = "Passo Falso"
testo = "Torna alla casella da cui è partito questo movimento."
["Meditazione"]
tipo = "Evento"
nome = "Meditazione"
testo = "Salti il prossimo turno. Fino alla fine del turno saltato non puoi essere affrontato né preso di mira."
File diff suppressed because one or more lines are too long
+47
View File
@@ -0,0 +1,47 @@
## Riepilogo
**Scopo:** sali da Bianca a Marrone (15), entra nel Dojo Centrale e supera l'Esame Finale.
### Il turno
1. Tira **2d6** e muovi **esattamente** quei passi: in orizzontale o verticale, mai
attraverso un muro, mai due volte sulla stessa casella nello stesso movimento.
2. **Entri su un avversario → Kumite** (sei l'attaccante). Vinci: lui torna alla Partenza,
tu continui. Perdi: torni alla Partenza e il turno finisce.
3. **Ogni Fatica in cui entri: −1 tacca**, anche se non ti fermi.
4. Fine movimento: applica la casella su cui ti fermi.
**Primo giro:** niente Kumite. Gli avversari si attraversano; se ti fermeresti su uno di
loro, fai un passo in più.
### Kumite
- Entrambi tirano **1d6**. La cintura più alta aggiunge **+1 per cintura di differenza**
(massimo +2).
- Visti i dadi: l'attaccante può giocare una Tecnica, poi il difensore.
- Totale più alto vince. **Pareggio: vince l'attaccante** (Senshu).
| Risultato | Vincitore | Sconfitto |
|---|---|---|
| Cinture uguali | +3 | −3, Partenza |
| Vince la più bassa | +3 | −3, Partenza |
| Vince la più alta | niente | Partenza |
### Caselle
| Casella | Effetto |
|---|---|
| Allenamento | +1 tacca |
| Fatica | −1 tacca per ogni Fatica attraversata o raggiunta camminando |
| Nebbia | pesca una carta (Tecniche coperte, massimo una) |
| Tunnel | puoi spostarti al Tunnel con lo stesso numero (uscita occupata: Kumite) |
| Passo Falso | torna alla casella di partenza del movimento |
| Sfida | Kumite obbligato contro un avversario a scelta |
### Esame Finale
- Solo da **Marrone (15+)**. Entri nel Dojo con almeno 1 passo rimasto; il movimento finisce.
- Difende l'avversario **più alto sulla scala**, e combatte come **Cintura Nera** (+1).
- Vinci: **Cintura Nera, hai vinto**. Perdi: −3 e torni alla Partenza.
**Stanze Sicure:** niente Kumite, niente effetti. La Partenza è sempre la tua stanza.
+245
View File
@@ -0,0 +1,245 @@
@page {
size: A4;
margin: 10mm;
}
:root {
--ink: #1c1917;
--muted: #57534e;
--line: #d6d3d1;
--paper: #ffffff;
--desk: #e7e5e4;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--desk);
color: var(--ink);
font: 10.5pt/1.4 Georgia, "Times New Roman", serif;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.page {
width: 210mm;
min-height: 297mm;
margin: 8mm auto;
padding: 10mm;
background: var(--paper);
box-shadow: 0 1px 4px rgb(0 0 0 / 0.2);
}
h1 {
margin: 0;
font-size: 24pt;
}
h2 {
margin: 1.2em 0 0.4em;
font-size: 15pt;
border-bottom: 1px solid var(--line);
}
h3 {
margin: 1em 0 0.3em;
font-size: 12pt;
}
.edition {
margin: 0 0 4mm;
color: var(--muted);
font-style: italic;
}
table {
border-collapse: collapse;
margin: 2mm 0 3mm;
width: 100%;
}
th,
td {
border: 1px solid var(--line);
padding: 1mm 2mm;
text-align: left;
vertical-align: top;
}
th {
background: #f5f5f4;
}
ol,
ul {
margin: 1mm 0 2mm;
padding-left: 6mm;
}
.cover {
font-size: 10pt;
}
.cover h2 {
margin-top: 3mm;
}
.board {
display: block;
margin: 0 auto;
font-family: Helvetica, Arial, sans-serif;
}
.legend {
display: flex;
flex-wrap: wrap;
gap: 2mm 5mm;
margin: 4mm 0 2mm;
font-family: Helvetica, Arial, sans-serif;
font-size: 9pt;
}
.key {
display: inline-flex;
align-items: center;
gap: 1.5mm;
}
.swatch {
display: inline-block;
width: 7mm;
height: 7mm;
border: 1px solid #a8a29e;
font-weight: 700;
line-height: 7mm;
text-align: center;
}
.tile-name {
margin: 0 0 4mm;
color: var(--muted);
font-size: 9pt;
}
.track {
width: auto;
margin: 6mm auto;
font-family: Helvetica, Arial, sans-serif;
}
.track th {
width: 32mm;
font-size: 13pt;
vertical-align: middle;
}
.track td {
width: 32mm;
height: 30mm;
border: 2px solid var(--ink);
font-size: 20pt;
font-weight: 700;
text-align: center;
vertical-align: middle;
}
.track .black th,
.track .black td {
background: var(--ink);
color: var(--paper);
font-size: 12pt;
}
.cards {
display: grid;
grid-template-columns: repeat(3, 63mm);
grid-auto-rows: 88mm;
gap: 0;
align-content: start;
justify-content: center;
padding-top: 8mm;
}
.card {
display: flex;
flex-direction: column;
padding: 4mm;
border: 0.3mm dashed #78716c;
font-family: Helvetica, Arial, sans-serif;
}
.card .kind {
margin: 0;
color: var(--muted);
font-size: 8pt;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.card h3 {
margin: 1mm 0 3mm;
font-size: 13pt;
}
.card .text {
flex: 1;
margin: 0;
font-size: 10pt;
}
.card .note {
margin: 0 0 2mm;
font-size: 8pt;
font-style: italic;
}
.card .deck {
margin: 0;
padding-top: 1.5mm;
border-top: 1px solid var(--line);
color: var(--muted);
font-size: 7.5pt;
text-align: center;
}
.card.tecnica {
background: #eef2ff;
}
.card.movimento {
background: #ecfeff;
}
.card.interazione {
background: #fff7ed;
}
.card.evento {
background: #f7fee7;
}
@media print {
body {
background: none;
}
.page {
width: auto;
min-height: 0;
margin: 0;
padding: 0;
box-shadow: none;
break-after: page;
}
.page:last-child {
break-after: auto;
}
.cards {
padding-top: 4mm;
}
}
+1
View File
@@ -17,6 +17,7 @@ build-backend = "uv_build"
[dependency-groups] [dependency-groups]
dev = [ dev = [
"markdown>=3.11",
"pytest>=9.1.1", "pytest>=9.1.1",
"ruff>=0.16.9", "ruff>=0.16.9",
] ]
Generated
+11
View File
@@ -18,6 +18,7 @@ source = { editable = "." }
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "markdown" },
{ name = "pytest" }, { name = "pytest" },
{ name = "ruff" }, { name = "ruff" },
] ]
@@ -26,6 +27,7 @@ dev = [
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "markdown", specifier = ">=3.11" },
{ name = "pytest", specifier = ">=9.1.1" }, { name = "pytest", specifier = ">=9.1.1" },
{ name = "ruff", specifier = ">=0.16.9" }, { name = "ruff", specifier = ">=0.16.9" },
] ]
@@ -39,6 +41,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
] ]
[[package]]
name = "markdown"
version = "3.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f8/4f/700155c8c20d9e655dd0732b5fc3c7614f291b9148da271d7388e50bf774/markdown-3.11.tar.gz", hash = "sha256:180224db6aed87ba9ce1f2781ebcd5826253de8ff637112090e24b84502bbf9f", size = 485623, upload-time = "2026-09-25T13:46:23.473Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/1e/32971905a7ab47f8b66866ed949fa48b104ba1c4a6fa57794c4f2c4b2cb8/markdown-3.11-py3-none-any.whl", hash = "sha256:cd6c89e7eb308c8b332ed673215a52d208a43f8bacc030b1419376129408719e", size = 111296, upload-time = "2026-09-25T13:46:22.163Z" },
]
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "26.3" version = "26.3"