Initial commit, with rules 0.5 and basic bots

This commit is contained in:
2026-09-26 16:01:46 +02:00
commit 0aaf113fc3
32 changed files with 4206 additions and 0 deletions
+670
View File
@@ -0,0 +1,670 @@
"""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,
SentToStart,
Source,
TrackChanged,
TurnSkipped,
)
from dojo.rules import VARIANT_A, VARIANT_B, Card
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(VARIANT_A, 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)
@pytest.mark.parametrize(("config", "gain"), [(VARIANT_A, 0), (VARIANT_B, 1)])
def test_higher_belt_beating_lower_gains_the_variant_amount(config, gain: int) -> None:
game, agents, _ = make_game(config=config, 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 + gain
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_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)
# 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
turn(game, agents, 1)
assert events_of(game, TurnSkipped) == [TurnSkipped(round=1, 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
@pytest.mark.parametrize(("before", "after"), [(14, 14), (13, 14), (15, 16), (17, 17)])
def test_variant_b_squares_cannot_make_a_player_brown(before: int, after: int) -> None:
game, agents, _ = make_game(config=VARIANT_B, dice=[1, 1])
put(game, 0, Pos(0, 3), track=before)
turn(game, agents, 0, Pos(0, 5))
assert game.players[0].track == after
def test_variant_a_squares_can_make_a_player_brown() -> None:
game, agents, _ = make_game(config=VARIANT_A, 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_variant_b_kumite_can_make_a_player_brown() -> None:
game, agents, _ = make_game(config=VARIANT_B, dice=[1, 1, 3, 3])
put(game, 0, Pos(4, 3), track=14)
put(game, 1, Pos(4, 5), track=12)
turn(game, agents, 0, Pos(4, 5))
assert game.players[0].track == 17
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, **tracks: int) -> tuple[Game, list]:
game, agents, _ = make_game(players=players, 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
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
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(VARIANT_A, 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