"""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