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
+112 -1
View File
@@ -17,6 +17,7 @@ from dojo.events import (
GameOver,
KumiteFought,
Moved,
MoveOptions,
SentToStart,
Source,
TrackChanged,
@@ -159,6 +160,21 @@ def test_winning_attacker_continues_its_move() -> None:
]
def test_every_path_choice_records_the_squares_it_could_end_on() -> None:
game, agents, _ = make_game(dice=[1, 2, 6, 1])
put(game, 0, Pos(4, 2), track=3)
put(game, 1, Pos(4, 3), track=3)
turn(game, agents, 0, Pos(4, 5), Pos(4, 5))
offered = agents[0].offered_paths
assert len(offered) == 2
assert [e.ends for e in events_of(game, MoveOptions)] == [
tuple(game.board.code(end) for end in sorted({path[-1] for path in paths}))
for paths in offered
]
def test_losing_attacker_stops_even_with_steps_left() -> None:
game, agents, dice = make_game(dice=[1, 2, 1, 6])
put(game, 0, Pos(4, 2), track=3)
@@ -182,6 +198,59 @@ def test_no_kumite_inside_a_safe_room() -> None:
assert game.players[0].position == game.players[1].position == Pos(1, 1)
def test_no_kumite_in_round_one_opponents_are_passed_through() -> None:
game, agents, _ = make_game(dice=[1, 2])
game.round = 1
put(game, 0, Pos(4, 2), track=3)
put(game, 1, Pos(4, 3), track=3)
turn(game, agents, 0, Pos(4, 5))
assert not events_of(game, KumiteFought)
assert (game.players[0].position, game.players[1].position) == (Pos(4, 5), Pos(4, 3))
def test_no_kumite_in_round_one_a_move_cannot_end_on_an_opponent() -> None:
game, agents, _ = make_game(dice=[1, 1])
game.round = 1
put(game, 0, Pos(4, 2))
put(game, 1, Pos(4, 4))
turn(game, agents, 0, Pos(4, 4))
assert not events_of(game, KumiteFought)
assert game.players[0].position != Pos(4, 4)
assert len(agents[0].offered_paths) == 2 # The move, then the Meditazione extra step.
def test_no_kumite_in_round_one_sfida_has_no_target() -> None:
game, agents, _ = make_game(dice=[1, 1])
game.round = 1
put(game, 0, Pos(5, 0))
put(game, 1, Pos(4, 4))
turn(game, agents, 0, Pos(5, 2))
assert not events_of(game, KumiteFought)
def test_no_kumite_in_round_one_an_occupied_tunnel_exit_cannot_be_used() -> None:
game, agents, _ = make_game(dice=[1, 1])
game.round = 1
put(game, 0, NEBBIA)
put(game, 1, Pos(3, 4))
agents[0].use_tunnels = True
turn(game, agents, 0, Pos(1, 6))
assert game.players[0].position == Pos(1, 6)
def test_first_kumite_round_must_be_positive() -> None:
with pytest.raises(ValueError, match="first_kumite_round"):
RulesConfig(first_kumite_round=0)
# Tecniche ---------------------------------------------------------------------------------
@@ -289,10 +358,11 @@ def test_ending_on_a_meditating_pawn_takes_one_extra_step() -> None:
def test_meditating_players_turn_is_skipped_once() -> None:
game, agents, _ = make_game()
game.players[1].meditating = True
start = game.round
turn(game, agents, 1)
assert events_of(game, TurnSkipped) == [TurnSkipped(round=1, player=1)]
assert events_of(game, TurnSkipped) == [TurnSkipped(round=start, player=1)]
assert not game.players[1].meditating
@@ -310,6 +380,47 @@ def test_allenamento_and_fatica() -> None:
assert game.players[0].track == 3
def test_fatica_costs_a_stripe_when_walked_over() -> None:
game, agents, _ = make_game(dice=[1, 1])
put(game, 0, Pos(3, 5), track=3)
turn(game, agents, 0, (Pos(3, 6), Pos(3, 7)))
assert game.players[0].track == 2
def test_fatica_walked_over_is_free_when_only_stopping_costs() -> None:
game, agents, _ = make_game(dice=[1, 1], config=RulesConfig(fatica_on_crossing=False))
put(game, 0, Pos(3, 5), track=3)
turn(game, agents, 0, (Pos(3, 6), Pos(3, 7)))
assert game.players[0].track == 3
def test_routes_to_one_square_are_offered_apart_when_one_crosses_fatica() -> None:
game, agents, _ = make_game(dice=[1, 1])
put(game, 0, Pos(2, 6))
turn(game, agents, 0, (Pos(2, 7), Pos(3, 7)))
to_corner = [path for path in agents[0].offered_paths[0] if path[-1] == Pos(3, 7)]
assert sorted(to_corner) == [(Pos(2, 7), Pos(3, 7)), (Pos(3, 6), Pos(3, 7))]
assert game.players[0].track == 0
def test_fatica_that_ends_brown_closes_the_dojo_for_that_route() -> None:
game, agents, _ = make_game(dice=[2, 3])
put(game, 0, Pos(3, 5), track=15)
agents[0].ends.append(Pos(3, 4))
game.play_turn()
into_dojo = [path for path in agents[0].offered_paths[0] if path[-1] == Pos(6, 7)]
assert into_dojo
assert all(Pos(3, 6) not in path for path in into_dojo)
def test_squares_can_make_a_player_brown() -> None:
game, agents, _ = make_game(dice=[1, 1])
put(game, 0, Pos(0, 3), track=14)