Rules v0.8: Fatica on crossing, no Kumite in round 1, board draft 2

- Fatica costs a stripe for every Fatica walked onto; a route whose Fatica
  drops a Brown player below 15 no longer enters the Dojo
- No Kumite in the first round: opponents are treated like meditating pawns
- Board draft 2: two Fatica tolls per quadrant, Allenamento moved to the ring
- Casual bot, --summary-csv, per-seat breakdown and move-options metric
- Out of contention measured at 3 belts
- Plan: Phase 6 baselines, decisions D34-D43

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-27 15:59:45 +02:00
parent 4998931cb0
commit bbc95b6722
18 changed files with 566 additions and 80 deletions
+16 -4
View File
@@ -10,6 +10,7 @@ from dojo.events import (
GameOver,
KumiteFought,
Moved,
MoveOptions,
Source,
TrackChanged,
TurnSkipped,
@@ -37,9 +38,11 @@ def test_measure_counts_fights_income_and_exams() -> None:
metrics = run(
[
TurnStarted(1, 0, 7),
MoveOptions(0, ("A", "F", ".")),
Moved(0, (HOME_0,)),
TrackChanged(0, 0, 1, Source.SQUARE),
TurnStarted(1, 1, 5),
MoveOptions(1, ("A", "?")),
Moved(1, (HOME_0,)),
TrackChanged(1, 0, 1, Source.SQUARE),
fought(1, 0, FightKind.EQUAL),
@@ -65,6 +68,10 @@ def test_measure_counts_fights_income_and_exams() -> None:
assert (metrics.card_gain, metrics.card_loss) == (5, -1)
assert (metrics.square_income_home, metrics.square_income_away) == (1, 1)
assert (metrics.winner_seat, metrics.winner_bot, metrics.rounds) == (0, "balanced", 2)
assert (metrics.move_choices, metrics.end_squares) == (2, 5)
assert (metrics.allenamento_reachable, metrics.fatica_reachable) == (2, 1)
assert metrics.final_track == (4, 4)
assert (metrics.exam_attempts_by_seat, metrics.exam_defences_by_seat) == ((1, 0), (0, 1))
# Both Exam rounds count as fights for the time estimate.
assert metrics.minutes == pytest.approx(round((3 * TURN_SECONDS + 5 * KUMITE_SECONDS) / 60, 1))
@@ -94,9 +101,9 @@ def test_standings_are_sampled_at_turn_ends() -> None:
# Halfway is the end of turn 2, led by player 1.
assert metrics.halfway_leader_won is False
assert metrics.contenders_at_end == 0
# Out (more than 6 behind): players 1 and 2 after turn 1, player 2 after turn 2.
assert metrics.out_of_contention_share == pytest.approx(3 / 12)
assert metrics.worst_out_of_contention_share == pytest.approx(0.5)
# Out (more than 9 behind): player 2 after turn 2 only.
assert metrics.out_of_contention_share == round(1 / 12, 3)
assert metrics.worst_out_of_contention_share == pytest.approx(1 / 4)
# 0>1 becomes 1>0; 0>2 becomes 2>0; 1>2 (ties skipped) becomes 2>1.
assert metrics.rank_swaps == 3
assert metrics.winner_turns_to_brown == 1
@@ -127,13 +134,18 @@ def test_summarize_splits_wins_by_seat_and_bot() -> None:
replace(base, lineup="balanced,random", winner_seat=0, winner_bot="balanced"),
replace(base, lineup="random,balanced", winner_seat=1, winner_bot="balanced"),
replace(base, lineup="random,balanced", winner_seat=0, winner_bot="random"),
base,
replace(
base, final_track=(2, 9), exam_attempts_by_seat=(0, 3), exam_defences_by_seat=(2, 0)
),
]
summary = summarize("test", rows)
assert summary.finished == 0.75
assert summary.win_rate_by_seat == (0.5, 0.25)
assert summary.seats[0].final_track == pytest.approx(0.5)
assert summary.seats[1].exam_attempts_per_game == pytest.approx(0.75)
assert (summary.seats[0].exam_defended, summary.seats[1].exam_defended) == (0.25, 0.0)
assert summary.win_rate_by_bot == {"balanced": 0.5, "random": 0.25}
assert summary.rounds.median == 10