79cbbccbb2
Bots now pick their start square and break path ties in their own corner's frame; the absolute tie-break made corners 1 and 2 win 25-28%. With that fixed, seat order is within target at 4 players, and a +1 stripe first-player bonus (new switch) gains only ~1 point, so no compensation. RULES.md v0.9 lists the tested alternatives; the plan has the 6.8 consolidation run and the seat-order study. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
867 lines
26 KiB
Python
867 lines
26 KiB
Python
"""Rules engine behaviour, one RULES.md rule per test where possible.
|
|
|
|
Positions refer to the fixture board (tests/fixtures/quadrant_valid.txt). In quadrant 0:
|
|
Allenamento (0,5); Nebbia (1,4); tunnel (1,6)<->(3,4); Fatica (3,6); Passo Falso (4,1);
|
|
Sfida (5,2); Dojo entrance (5,7)->(6,7). Player 1's Safe Room starts at (0,13).
|
|
"""
|
|
|
|
import pytest
|
|
from scripted import make_game, put, turn
|
|
|
|
from dojo.board import Pos
|
|
from dojo.engine import Game, RandomSource, seat_corners
|
|
from dojo.events import (
|
|
CardDrawn,
|
|
ExamTaken,
|
|
FightKind,
|
|
GameOver,
|
|
KumiteFought,
|
|
Moved,
|
|
MoveOptions,
|
|
SentToStart,
|
|
Source,
|
|
TrackChanged,
|
|
TurnSkipped,
|
|
)
|
|
from dojo.rules import BLACK, DEFAULT_RULES, Card, RulesConfig
|
|
|
|
START_0 = Pos(0, 0)
|
|
START_1 = Pos(0, 13)
|
|
NEBBIA = Pos(1, 4)
|
|
|
|
|
|
def events_of[T](game: Game, kind: type[T]) -> list[T]:
|
|
return [e for e in game.events if isinstance(e, kind)]
|
|
|
|
|
|
def only_fight(game: Game) -> KumiteFought:
|
|
fights = events_of(game, KumiteFought)
|
|
assert len(fights) == 1
|
|
return fights[0]
|
|
|
|
|
|
# Setup ------------------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize("players", range(2, 9))
|
|
def test_corner_mates_never_play_consecutively(players: int) -> None:
|
|
corners = seat_corners(players)
|
|
|
|
assert len(corners) == players
|
|
assert all(corners[i] != corners[(i + 1) % players] for i in range(players))
|
|
assert sorted(set(corners)) == list(range(min(players, 4)))
|
|
|
|
|
|
def test_seats_up_to_four_take_corners_clockwise() -> None:
|
|
assert seat_corners(4) == (0, 1, 2, 3)
|
|
|
|
|
|
def test_players_start_in_their_own_safe_room() -> None:
|
|
from dataclasses import replace
|
|
|
|
game, _, _ = make_game(players=5, config=replace(DEFAULT_RULES, start_position=3))
|
|
|
|
for player in game.players:
|
|
assert player.position in game.board.safe_room(player.corner)
|
|
assert player.track == 3
|
|
|
|
|
|
def test_illegal_agent_choice_is_rejected() -> None:
|
|
game, agents, _ = make_game(dice=[1, 1])
|
|
agents[0].ends.append(Pos(9, 9))
|
|
|
|
with pytest.raises(AssertionError, match="no offered path"):
|
|
game.play_turn()
|
|
|
|
|
|
# Kumite -----------------------------------------------------------------------------------
|
|
|
|
|
|
def test_senshu_tie_goes_to_the_attacker_and_equal_belts_swap_a_belt() -> None:
|
|
game, agents, dice = make_game(dice=[1, 1, 3, 3])
|
|
put(game, 0, Pos(4, 3), track=3)
|
|
put(game, 1, Pos(4, 5), track=3)
|
|
|
|
turn(game, agents, 0, Pos(4, 5))
|
|
|
|
fight = only_fight(game)
|
|
assert fight.attacker_won and fight.kind is FightKind.EQUAL
|
|
assert (game.players[0].track, game.players[1].track) == (6, 0)
|
|
assert game.players[0].position == Pos(4, 5)
|
|
assert game.players[1].position == START_1
|
|
assert not dice.rolls
|
|
|
|
|
|
def test_upset_swaps_a_belt() -> None:
|
|
game, agents, _ = make_game(dice=[1, 1, 6, 3])
|
|
put(game, 0, Pos(4, 3), track=0)
|
|
put(game, 1, Pos(4, 5), track=6)
|
|
|
|
turn(game, agents, 0, Pos(4, 5))
|
|
|
|
fight = only_fight(game)
|
|
assert fight.kind is FightKind.UPSET
|
|
assert (fight.attacker_total, fight.defender_total) == (6, 5)
|
|
assert (game.players[0].track, game.players[1].track) == (3, 3)
|
|
|
|
|
|
def test_higher_belt_beating_lower_gains_nothing() -> None:
|
|
game, agents, _ = make_game(dice=[1, 1, 1, 2])
|
|
put(game, 0, Pos(4, 3), track=6)
|
|
put(game, 1, Pos(4, 5), track=0)
|
|
|
|
turn(game, agents, 0, Pos(4, 5))
|
|
|
|
assert only_fight(game).kind is FightKind.EXPECTED
|
|
assert game.players[0].track == 6
|
|
assert game.players[1].track == 0
|
|
assert game.players[1].position == START_1
|
|
|
|
|
|
def test_belt_bonus_is_capped_at_two() -> None:
|
|
game, agents, _ = make_game(dice=[1, 1, 1, 4])
|
|
put(game, 0, Pos(4, 3), track=15)
|
|
put(game, 1, Pos(4, 5), track=0)
|
|
|
|
turn(game, agents, 0, Pos(4, 5))
|
|
|
|
fight = only_fight(game)
|
|
assert (fight.attacker_total, fight.defender_total) == (3, 4)
|
|
assert not fight.attacker_won and fight.kind is FightKind.UPSET
|
|
assert (game.players[0].track, game.players[1].track) == (12, 3)
|
|
assert game.players[0].position == START_0
|
|
|
|
|
|
def test_losing_attacker_goes_to_start_and_skips_the_square_effect() -> None:
|
|
game, agents, _ = make_game(dice=[1, 1, 1, 2])
|
|
put(game, 0, Pos(0, 3), track=3)
|
|
put(game, 1, Pos(0, 5), track=3)
|
|
|
|
turn(game, agents, 0, Pos(0, 5))
|
|
|
|
assert game.players[0].position == START_0
|
|
assert (game.players[0].track, game.players[1].track) == (0, 6)
|
|
assert game.players[1].position == Pos(0, 5)
|
|
assert not [e for e in events_of(game, TrackChanged) if e.source is Source.SQUARE]
|
|
|
|
|
|
def test_winning_attacker_continues_its_move() -> 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))
|
|
|
|
assert game.players[0].position == Pos(4, 5)
|
|
assert game.players[1].position == START_1
|
|
assert [e.path for e in events_of(game, Moved)] == [
|
|
(Pos(4, 3),),
|
|
(Pos(4, 4), Pos(4, 5)),
|
|
]
|
|
|
|
|
|
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)
|
|
put(game, 1, Pos(4, 3), track=3)
|
|
|
|
turn(game, agents, 0, Pos(4, 5))
|
|
|
|
assert game.players[0].position == START_0
|
|
assert game.players[1].position == Pos(4, 3)
|
|
assert not dice.rolls
|
|
|
|
|
|
def test_no_kumite_inside_a_safe_room() -> None:
|
|
game, agents, _ = make_game(dice=[1, 1])
|
|
put(game, 1, Pos(1, 1))
|
|
put(game, 0, Pos(1, 3))
|
|
|
|
turn(game, agents, 0, Pos(1, 1))
|
|
|
|
assert not events_of(game, KumiteFought)
|
|
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_player_bonus_raises_only_the_first_seat() -> None:
|
|
game, _, _ = make_game(players=3, config=RulesConfig(start_position=3, first_player_bonus=1))
|
|
|
|
assert [p.track for p in game.players] == [4, 3, 3]
|
|
|
|
|
|
def test_first_player_bonus_cannot_be_negative() -> None:
|
|
with pytest.raises(ValueError, match="first_player_bonus"):
|
|
RulesConfig(first_player_bonus=-1)
|
|
|
|
|
|
def test_first_kumite_round_must_be_positive() -> None:
|
|
with pytest.raises(ValueError, match="first_kumite_round"):
|
|
RulesConfig(first_kumite_round=0)
|
|
|
|
|
|
# Tecniche ---------------------------------------------------------------------------------
|
|
|
|
|
|
def test_kiai_adds_to_the_total() -> None:
|
|
game, agents, _ = make_game(dice=[1, 1, 2, 3])
|
|
put(game, 0, Pos(4, 3), track=3)
|
|
put(game, 1, Pos(4, 5), track=3)
|
|
game.players[0].tecnica = Card.KIAI
|
|
agents[0].plays = frozenset({Card.KIAI})
|
|
|
|
turn(game, agents, 0, Pos(4, 5))
|
|
|
|
fight = only_fight(game)
|
|
assert (fight.attacker_total, fight.defender_total) == (4, 3)
|
|
assert fight.cards == ((0, Card.KIAI),)
|
|
assert agents[0].fight_views[0].attacker_total == 2
|
|
assert game.players[0].tecnica is None
|
|
|
|
|
|
def test_kaeshi_rerolls_the_die() -> None:
|
|
game, agents, dice = make_game(dice=[1, 1, 1, 4, 5])
|
|
put(game, 0, Pos(4, 3), track=3)
|
|
put(game, 1, Pos(4, 5), track=3)
|
|
game.players[0].tecnica = Card.KAESHI
|
|
agents[0].plays = frozenset({Card.KAESHI})
|
|
|
|
turn(game, agents, 0, Pos(4, 5))
|
|
|
|
assert only_fight(game).attacker_total == 5
|
|
assert only_fight(game).attacker_won
|
|
assert not dice.rolls
|
|
|
|
|
|
def test_defender_sees_the_attackers_card_before_deciding() -> None:
|
|
game, agents, _ = make_game(dice=[1, 1, 3, 3])
|
|
put(game, 0, Pos(4, 3), track=3)
|
|
put(game, 1, Pos(4, 5), track=3)
|
|
game.players[0].tecnica = Card.KIAI
|
|
game.players[1].tecnica = Card.KIAI
|
|
agents[0].plays = agents[1].plays = frozenset({Card.KIAI})
|
|
|
|
turn(game, agents, 0, Pos(4, 5))
|
|
|
|
assert agents[1].fight_views[0].played == ((0, Card.KIAI),)
|
|
assert agents[1].fight_views[0].attacker_total == 5
|
|
assert only_fight(game).cards == ((0, Card.KIAI), (1, Card.KIAI))
|
|
|
|
|
|
def test_uke_losing_defender_stays_and_the_attacker_steps_on() -> None:
|
|
game, agents, _ = make_game(dice=[1, 1, 5, 1])
|
|
put(game, 0, Pos(4, 3), track=3)
|
|
put(game, 1, Pos(4, 5), track=3)
|
|
game.players[1].tecnica = Card.UKE
|
|
agents[1].plays = frozenset({Card.UKE})
|
|
|
|
turn(game, agents, 0, Pos(4, 5), Pos(4, 6))
|
|
|
|
assert game.players[1].position == Pos(4, 5)
|
|
assert game.players[1].track == 0
|
|
assert game.players[0].position == Pos(4, 6)
|
|
assert not events_of(game, SentToStart)
|
|
|
|
|
|
def test_uke_losing_attacker_returns_to_the_previous_square() -> None:
|
|
game, agents, _ = make_game(dice=[1, 1, 1, 6])
|
|
put(game, 0, Pos(4, 3), track=3)
|
|
put(game, 1, Pos(4, 5), track=3)
|
|
game.players[0].tecnica = Card.UKE
|
|
agents[0].plays = frozenset({Card.UKE})
|
|
|
|
turn(game, agents, 0, Pos(4, 5))
|
|
|
|
assert game.players[0].position == Pos(4, 4)
|
|
assert (game.players[0].track, game.players[1].track) == (0, 6)
|
|
|
|
|
|
# Meditazione ------------------------------------------------------------------------------
|
|
|
|
|
|
def test_meditating_pawn_is_passed_through_without_a_fight() -> None:
|
|
game, agents, _ = make_game(dice=[1, 1])
|
|
put(game, 0, Pos(4, 3))
|
|
put(game, 1, Pos(4, 4))
|
|
game.players[1].meditating = True
|
|
|
|
turn(game, agents, 0, Pos(4, 5))
|
|
|
|
assert not events_of(game, KumiteFought)
|
|
assert game.players[0].position == Pos(4, 5)
|
|
|
|
|
|
def test_ending_on_a_meditating_pawn_takes_one_extra_step() -> None:
|
|
game, agents, _ = make_game(dice=[1, 1])
|
|
put(game, 0, Pos(4, 2))
|
|
put(game, 1, Pos(4, 4))
|
|
game.players[1].meditating = True
|
|
|
|
turn(game, agents, 0, Pos(4, 4), Pos(4, 5))
|
|
|
|
assert game.players[0].position == Pos(4, 5)
|
|
extra_step_options = {path[-1] for path in agents[0].offered_paths[1]}
|
|
assert extra_step_options == {Pos(3, 4), Pos(4, 5), Pos(5, 4)}
|
|
|
|
|
|
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=start, player=1)]
|
|
assert not game.players[1].meditating
|
|
|
|
|
|
# Squares ----------------------------------------------------------------------------------
|
|
|
|
|
|
def test_allenamento_and_fatica() -> None:
|
|
game, agents, _ = make_game(dice=[1, 1, 1, 1])
|
|
put(game, 0, Pos(0, 3), track=3)
|
|
turn(game, agents, 0, Pos(0, 5))
|
|
assert game.players[0].track == 4
|
|
|
|
put(game, 0, Pos(4, 5))
|
|
turn(game, agents, 0, Pos(3, 6))
|
|
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)
|
|
|
|
turn(game, agents, 0, Pos(0, 5))
|
|
|
|
assert game.players[0].track == 15
|
|
|
|
|
|
def test_passo_falso_returns_to_the_move_start_and_fights_stand() -> None:
|
|
game, agents, _ = make_game(dice=[1, 1, 6, 1])
|
|
put(game, 0, Pos(4, 3), track=3)
|
|
put(game, 1, Pos(4, 2), track=3)
|
|
|
|
turn(game, agents, 0, Pos(4, 1), Pos(4, 1))
|
|
|
|
assert game.players[0].position == Pos(4, 3)
|
|
assert game.players[0].track == 6
|
|
assert game.players[1].position == START_1
|
|
|
|
|
|
def test_sfida_square_fights_a_chosen_opponent_and_clamps_at_zero() -> None:
|
|
game, agents, _ = make_game(dice=[1, 1, 4, 4])
|
|
put(game, 0, Pos(5, 4), track=0)
|
|
put(game, 1, Pos(10, 10), track=0)
|
|
agents[0].sfida_target = 1
|
|
|
|
turn(game, agents, 0, Pos(5, 2))
|
|
|
|
assert only_fight(game).attacker_won
|
|
assert (game.players[0].track, game.players[1].track) == (3, 0)
|
|
assert game.players[0].position == Pos(5, 2)
|
|
assert game.players[1].position == START_1
|
|
|
|
|
|
@pytest.mark.parametrize("meditating", [False, True])
|
|
def test_sfida_cannot_target_safe_rooms_or_meditating_players(meditating: bool) -> None:
|
|
game, agents, _ = make_game(dice=[1, 1])
|
|
put(game, 0, Pos(5, 4))
|
|
if meditating:
|
|
put(game, 1, Pos(10, 10))
|
|
game.players[1].meditating = True
|
|
agents[0].sfida_target = 1
|
|
|
|
turn(game, agents, 0, Pos(5, 2))
|
|
|
|
assert not events_of(game, KumiteFought)
|
|
|
|
|
|
def test_tunnel_is_optional_and_ends_the_move() -> None:
|
|
game, agents, _ = make_game(dice=[1, 1, 1, 1])
|
|
put(game, 0, NEBBIA)
|
|
turn(game, agents, 0, Pos(1, 6))
|
|
assert game.players[0].position == Pos(1, 6)
|
|
|
|
put(game, 0, NEBBIA)
|
|
agents[0].use_tunnels = True
|
|
turn(game, agents, 0, Pos(1, 6))
|
|
assert game.players[0].position == Pos(3, 4)
|
|
|
|
|
|
def test_occupied_tunnel_exit_starts_a_kumite() -> None:
|
|
game, agents, _ = make_game(dice=[1, 1, 3, 3])
|
|
put(game, 0, NEBBIA, track=3)
|
|
put(game, 1, Pos(3, 4), track=3)
|
|
agents[0].use_tunnels = True
|
|
|
|
turn(game, agents, 0, Pos(1, 6))
|
|
|
|
assert only_fight(game).attacker_won
|
|
assert game.players[0].position == Pos(3, 4)
|
|
assert game.players[1].position == START_1
|
|
|
|
|
|
def test_tunnel_exit_with_a_meditating_pawn_cannot_be_used() -> None:
|
|
game, agents, _ = make_game(dice=[1, 1])
|
|
put(game, 0, NEBBIA)
|
|
put(game, 1, Pos(3, 4))
|
|
game.players[1].meditating = True
|
|
agents[0].use_tunnels = True
|
|
|
|
turn(game, agents, 0, Pos(1, 6))
|
|
|
|
assert game.players[0].position == Pos(1, 6)
|
|
|
|
|
|
# Nebbia del Dojo --------------------------------------------------------------------------
|
|
|
|
|
|
def draw_on_nebbia(
|
|
card: Card, players: int = 2, dice: tuple[int, ...] = (1, 1), **tracks: int
|
|
) -> tuple[Game, list]:
|
|
game, agents, _ = make_game(players=players, dice=dice, deck=((card, 1),))
|
|
put(game, 0, Pos(0, 3))
|
|
for name, track in tracks.items():
|
|
game.players[int(name[1:])].track = track
|
|
return game, agents
|
|
|
|
|
|
def test_card_is_drawn_when_stopping_on_nebbia() -> None:
|
|
game, agents = draw_on_nebbia(Card.ALLENAMENTO_INTENSIVO)
|
|
|
|
turn(game, agents, 0, NEBBIA)
|
|
|
|
assert events_of(game, CardDrawn) == [CardDrawn(0, Card.ALLENAMENTO_INTENSIVO)]
|
|
assert game.players[0].track == 1
|
|
|
|
|
|
def test_discard_pile_is_reshuffled_when_the_deck_runs_out() -> None:
|
|
game, agents = draw_on_nebbia(Card.ALLENAMENTO_INTENSIVO, dice=(1, 1, 1, 1))
|
|
turn(game, agents, 0, NEBBIA)
|
|
put(game, 0, Pos(0, 3))
|
|
|
|
turn(game, agents, 0, NEBBIA)
|
|
|
|
assert game.players[0].track == 2
|
|
|
|
|
|
def test_colpo_proibito() -> None:
|
|
game, agents = draw_on_nebbia(Card.COLPO_PROIBITO, p0=3)
|
|
|
|
turn(game, agents, 0, NEBBIA)
|
|
|
|
assert game.players[0].track == 2
|
|
|
|
|
|
def test_sensei_osserva_helps_every_lowest_player() -> None:
|
|
game, agents = draw_on_nebbia(Card.SENSEI_OSSERVA, players=3, p0=3, p1=0, p2=0)
|
|
|
|
turn(game, agents, 0, NEBBIA)
|
|
|
|
assert [p.track for p in game.players] == [3, 2, 2]
|
|
|
|
|
|
def test_peso_della_cintura_hits_every_highest_player() -> None:
|
|
game, agents = draw_on_nebbia(Card.PESO_DELLA_CINTURA, players=3, p0=3, p1=6, p2=6)
|
|
|
|
turn(game, agents, 0, NEBBIA)
|
|
|
|
assert [p.track for p in game.players] == [3, 5, 5]
|
|
|
|
|
|
def test_meditazione_card() -> None:
|
|
game, agents = draw_on_nebbia(Card.MEDITAZIONE)
|
|
|
|
turn(game, agents, 0, NEBBIA)
|
|
|
|
assert game.players[0].meditating
|
|
|
|
|
|
def test_passo_falso_card() -> None:
|
|
game, agents = draw_on_nebbia(Card.PASSO_FALSO)
|
|
|
|
turn(game, agents, 0, NEBBIA)
|
|
|
|
assert game.players[0].position == Pos(0, 3)
|
|
|
|
|
|
def test_irimi_swaps_places_without_a_fight() -> None:
|
|
game, agents = draw_on_nebbia(Card.IRIMI)
|
|
put(game, 1, Pos(10, 10))
|
|
agents[0].irimi_target = 1
|
|
|
|
turn(game, agents, 0, NEBBIA)
|
|
|
|
assert game.players[0].position == Pos(10, 10)
|
|
assert game.players[1].position == NEBBIA
|
|
assert not events_of(game, KumiteFought)
|
|
|
|
|
|
@pytest.mark.parametrize("card", [Card.SFIDA, Card.IRIMI])
|
|
def test_sfida_and_irimi_cards_are_mandatory(card: Card) -> None:
|
|
game, agents = draw_on_nebbia(card, dice=(1, 1, 3, 3))
|
|
put(game, 1, Pos(10, 10))
|
|
|
|
turn(game, agents, 0, NEBBIA)
|
|
|
|
if card is Card.SFIDA:
|
|
assert only_fight(game).defender == 1
|
|
else:
|
|
assert game.players[0].position == Pos(10, 10)
|
|
|
|
|
|
def test_tunnel_segreto_teleports_to_a_chosen_tunnel() -> None:
|
|
game, agents = draw_on_nebbia(Card.TUNNEL_SEGRETO)
|
|
agents[0].tunnel_destination = Pos(3, 4)
|
|
|
|
turn(game, agents, 0, NEBBIA)
|
|
|
|
assert game.players[0].position == Pos(3, 4)
|
|
|
|
|
|
def test_tsugi_ashi_makes_another_full_move() -> None:
|
|
game, agents = draw_on_nebbia(Card.TSUGI_ASHI, dice=(1, 1, 1, 1))
|
|
|
|
turn(game, agents, 0, NEBBIA, Pos(2, 5))
|
|
|
|
assert game.players[0].position == Pos(2, 5)
|
|
|
|
|
|
@pytest.mark.parametrize(("keep_drawn", "kept"), [(True, Card.UKE), (False, Card.KIAI)])
|
|
def test_only_one_tecnica_can_be_held(keep_drawn: bool, kept: Card) -> None:
|
|
game, agents = draw_on_nebbia(Card.UKE)
|
|
game.players[0].tecnica = Card.KIAI
|
|
agents[0].keep_drawn = keep_drawn
|
|
|
|
turn(game, agents, 0, NEBBIA)
|
|
|
|
assert game.players[0].tecnica is kept
|
|
|
|
|
|
# Central Dojo and Final Exam --------------------------------------------------------------
|
|
|
|
|
|
def dojo_game(
|
|
dice: list[int], players: int = 3, config: RulesConfig = DEFAULT_RULES, **tracks: int
|
|
) -> tuple[Game, list]:
|
|
game, agents, _ = make_game(players=players, config=config, dice=dice)
|
|
put(game, 0, Pos(4, 7), track=15)
|
|
for name, track in tracks.items():
|
|
game.players[int(name[1:])].track = track
|
|
return game, agents
|
|
|
|
|
|
def test_only_brown_players_may_enter_the_dojo() -> None:
|
|
game, agents = dojo_game([1, 1])
|
|
game.players[0].track = 14
|
|
|
|
turn(game, agents, 0)
|
|
|
|
offered = agents[0].offered_paths[0]
|
|
assert not any(game.board.dojo & set(path) for path in offered)
|
|
|
|
|
|
def test_passing_the_exam_wins_the_game() -> None:
|
|
game, agents = dojo_game([1, 1, 4, 1], p1=9, p2=12)
|
|
|
|
turn(game, agents, 0, Pos(6, 7))
|
|
|
|
assert events_of(game, ExamTaken) == [ExamTaken(challenger=0, defender=2, passed=True)]
|
|
assert game.winner == 0
|
|
assert game.finished
|
|
assert isinstance(game.events[-1], GameOver)
|
|
|
|
|
|
def test_failing_the_exam_costs_a_belt_and_the_defender_gains_nothing() -> None:
|
|
game, agents = dojo_game([1, 1, 1, 6], p1=9, p2=12)
|
|
|
|
turn(game, agents, 0, Pos(6, 7))
|
|
|
|
assert game.winner is None
|
|
assert game.players[0].track == 12
|
|
assert game.players[0].position == START_0
|
|
assert game.players[2].track == 12
|
|
|
|
|
|
@pytest.mark.parametrize(("loss", "track"), [(0, 15), (1, 14)])
|
|
def test_failed_exam_loss_is_configurable(loss: int, track: int) -> None:
|
|
config = RulesConfig(exam_failure_loss=loss)
|
|
game, agents = dojo_game([1, 1, 1, 6], config=config, p1=9, p2=12)
|
|
|
|
turn(game, agents, 0, Pos(6, 7))
|
|
|
|
assert game.players[0].track == track
|
|
assert game.players[0].position == START_0
|
|
|
|
|
|
def test_failed_exam_loss_cannot_be_negative() -> None:
|
|
with pytest.raises(ValueError, match="exam_failure_loss"):
|
|
RulesConfig(exam_failure_loss=-1)
|
|
|
|
|
|
def test_uke_in_the_exam_returns_to_the_entrance() -> None:
|
|
game, agents = dojo_game([1, 1, 1, 6], p1=9, p2=12)
|
|
game.players[0].tecnica = Card.UKE
|
|
agents[0].plays = frozenset({Card.UKE})
|
|
|
|
turn(game, agents, 0, Pos(6, 7))
|
|
|
|
assert game.players[0].position == Pos(5, 7)
|
|
assert game.players[0].track == 12
|
|
|
|
|
|
def test_challenger_picks_among_tied_defenders() -> None:
|
|
game, agents = dojo_game([1, 1, 4, 1], p1=12, p2=12)
|
|
agents[0].exam_defender = 1
|
|
|
|
turn(game, agents, 0, Pos(6, 7))
|
|
|
|
assert events_of(game, ExamTaken)[0].defender == 1
|
|
|
|
|
|
def test_meditating_players_cannot_defend() -> None:
|
|
game, agents = dojo_game([1, 1, 4, 1], p1=9, p2=12)
|
|
game.players[2].meditating = True
|
|
|
|
turn(game, agents, 0, Pos(6, 7))
|
|
|
|
assert events_of(game, ExamTaken)[0].defender == 1
|
|
|
|
|
|
@pytest.mark.parametrize(("ties_to_defender", "passed"), [(False, True), (True, False)])
|
|
def test_exam_ties_can_go_to_the_defender(ties_to_defender: bool, passed: bool) -> None:
|
|
config = RulesConfig(exam_ties_to_defender=ties_to_defender)
|
|
# Black belt defender: +1, so 4 vs 3 is a tie.
|
|
game, agents = dojo_game([1, 1, 4, 3], config=config, p1=9, p2=15)
|
|
|
|
turn(game, agents, 0, Pos(6, 7))
|
|
|
|
assert events_of(game, ExamTaken)[0].passed is passed
|
|
|
|
|
|
def test_exam_ties_go_to_the_defender_only_in_the_exam() -> None:
|
|
game, agents, _ = make_game(config=RulesConfig(exam_ties_to_defender=True), dice=[1, 1, 3, 3])
|
|
put(game, 0, Pos(4, 3), track=3)
|
|
put(game, 1, Pos(4, 5), track=3)
|
|
|
|
turn(game, agents, 0, Pos(4, 5))
|
|
|
|
assert only_fight(game).attacker_won
|
|
|
|
|
|
@pytest.mark.parametrize(("black", "passed"), [(False, True), (True, False)])
|
|
def test_exam_defender_may_fight_as_a_black_belt(black: bool, passed: bool) -> None:
|
|
config = RulesConfig(exam_defender_black=black)
|
|
game, agents = dojo_game([1, 1, 4, 4], config=config, p1=9, p2=12)
|
|
|
|
turn(game, agents, 0, Pos(6, 7))
|
|
|
|
fight = only_fight(game)
|
|
assert fight.defender_belt == (BLACK if black else 4)
|
|
assert (fight.attacker_total, fight.defender_total) == ((4, 5) if black else (5, 4))
|
|
assert events_of(game, ExamTaken)[0].passed is passed
|
|
|
|
|
|
def test_best_of_three_exam_is_passed_on_two_rounds() -> None:
|
|
game, agents = dojo_game(
|
|
[1, 1, 6, 1, 1, 6, 6, 1], config=RulesConfig(exam_rounds=3), p1=9, p2=15
|
|
)
|
|
|
|
turn(game, agents, 0, Pos(6, 7))
|
|
|
|
rounds = events_of(game, KumiteFought)
|
|
assert [r.attacker_won for r in rounds] == [True, False, True]
|
|
assert all(r.exam for r in rounds)
|
|
assert events_of(game, ExamTaken) == [ExamTaken(challenger=0, defender=2, passed=True)]
|
|
assert game.winner == 0
|
|
|
|
|
|
def test_best_of_three_exam_stops_once_decided() -> None:
|
|
game, agents = dojo_game([1, 1, 1, 6, 1, 6], config=RulesConfig(exam_rounds=3), p1=9, p2=15)
|
|
|
|
turn(game, agents, 0, Pos(6, 7))
|
|
|
|
assert len(events_of(game, KumiteFought)) == 2
|
|
assert game.winner is None
|
|
assert game.players[0].track == 12
|
|
assert game.players[0].position == START_0
|
|
|
|
|
|
def test_uke_played_in_any_exam_round_saves_the_challenger() -> None:
|
|
game, agents = dojo_game([1, 1, 1, 6, 1, 6], config=RulesConfig(exam_rounds=3), p1=9, p2=15)
|
|
game.players[0].tecnica = Card.UKE
|
|
agents[0].plays = frozenset({Card.UKE})
|
|
|
|
turn(game, agents, 0, Pos(6, 7))
|
|
|
|
assert events_of(game, KumiteFought)[0].cards == ((0, Card.UKE),)
|
|
assert game.players[0].position == Pos(5, 7)
|
|
assert game.players[0].track == 12
|
|
|
|
|
|
@pytest.mark.parametrize("rounds", [0, 2])
|
|
def test_exam_rounds_must_be_odd(rounds: int) -> None:
|
|
with pytest.raises(ValueError, match="odd"):
|
|
RulesConfig(exam_rounds=rounds)
|
|
|
|
|
|
def test_kumite_won_mid_move_can_open_the_dojo() -> None:
|
|
game, agents, dice = make_game(dice=[1, 1, 3, 3, 6, 1])
|
|
put(game, 0, Pos(4, 7), track=14)
|
|
put(game, 1, Pos(5, 7), track=12)
|
|
|
|
turn(game, agents, 0, (Pos(5, 7), Pos(5, 6)), Pos(6, 7))
|
|
|
|
assert game.players[0].track == 17
|
|
assert events_of(game, ExamTaken) == [ExamTaken(challenger=0, defender=1, passed=True)]
|
|
assert game.winner == 0
|
|
assert not dice.rolls
|
|
|
|
|
|
# Whole game -------------------------------------------------------------------------------
|
|
|
|
|
|
def test_a_game_runs_to_completion() -> None:
|
|
from dataclasses import replace
|
|
|
|
from scripted import ScriptedAgent, fixture_board
|
|
|
|
config = replace(DEFAULT_RULES, max_rounds=30)
|
|
game = Game(fixture_board(), config, [ScriptedAgent() for _ in range(4)], RandomSource(7))
|
|
|
|
game.run()
|
|
|
|
assert game.finished
|
|
assert isinstance(game.events[-1], GameOver)
|
|
with pytest.raises(RuntimeError):
|
|
game.play_turn()
|
|
|
|
|
|
def test_views_hide_which_tecnica_an_opponent_holds() -> None:
|
|
game, _, _ = make_game()
|
|
game.players[0].tecnica = Card.KIAI
|
|
|
|
theirs, mine = game.view(1), game.view(0)
|
|
|
|
assert theirs.tecnica is None
|
|
assert theirs.players[0].holds_tecnica
|
|
assert not theirs.players[1].holds_tecnica
|
|
assert mine.tecnica is Card.KIAI
|
|
|
|
|
|
def test_agents_decide_on_their_own_current_view() -> None:
|
|
game, agents, _ = make_game(dice=[1, 1])
|
|
put(game, 1, Pos(4, 4), track=7)
|
|
|
|
turn(game, agents, 1)
|
|
|
|
view = agents[1].path_views[0]
|
|
assert view.me == 1
|
|
assert view.players[1].position == Pos(4, 4)
|
|
assert view.belt(1) == 2
|