"""Bot decisions on the fixture board (positions as in test_engine.py).""" import random import pytest from scripted import fixture_board, make_game, put from dojo.board import Board, Pos, rotate from dojo.bots import ( AGGRESSIVE, BALANCED, FARMER, PROFILES, CasualAgent, HeuristicAgent, Profile, RandomAgent, own_frame, series_probability, start_square, win_probability, ) from dojo.engine import Agent, FightView, Game, RandomSource from dojo.rules import DEFAULT_RULES, Card, RulesConfig ALLENAMENTO = Pos(0, 5) FATICA = Pos(3, 6) TATAMI = Pos(4, 4) DOJO_ENTRANCE = Pos(5, 7) DOJO = Pos(6, 7) def test_win_probability_counts_senshu_and_bonuses() -> None: assert win_probability(0, 0) == pytest.approx(21 / 36) assert win_probability(2, 0) == pytest.approx(30 / 36) assert win_probability(0, 2) == pytest.approx(10 / 36) assert win_probability(0, 0, senshu=False) == pytest.approx(15 / 36) def test_series_probability_is_a_best_of_n() -> None: assert series_probability(0.3, 1) == pytest.approx(0.3) assert series_probability(0.5, 3) == pytest.approx(0.5) assert series_probability(0.6, 3) == pytest.approx(0.6**2 * (3 - 2 * 0.6)) def test_kiai_that_only_ties_the_exam_is_kept_when_ties_go_to_the_defender() -> None: game, _, _ = make_game(config=RulesConfig(exam_ties_to_defender=True)) game.players[0].tecnica = Card.KIAI exam = FightView(0, 1, True, 3, 5, played=()) assert not HeuristicAgent().play_tecnica(game.view(0), exam) assert HeuristicAgent().play_tecnica(game.view(0), fight(3, 5)) def test_heuristic_prefers_allenamento_to_fatica() -> None: game, _, _ = make_game() choice = HeuristicAgent().choose_path(game.view(0), [(FATICA,), (ALLENAMENTO,)]) assert choice == (ALLENAMENTO,) def test_heuristic_walks_around_fatica() -> None: game, _, _ = make_game() put(game, 0, Pos(2, 6), track=3) paths = [(FATICA, Pos(3, 7)), (Pos(2, 7), Pos(3, 7))] assert HeuristicAgent().choose_path(game.view(0), paths) == paths[1] def test_heuristic_sees_no_opponent_to_fight_in_round_one() -> None: game, _, _ = make_game() game.round = 1 put(game, 0, Pos(4, 3), track=3) put(game, 1, TATAMI, track=3) # In round 2 the aggressive bot picks the fight (test_profiles_differ_on_an_even_fight). # Two plain Tatami tie; the tie goes to the lower square in the bot's frame, (4, 2). choice = HeuristicAgent(AGGRESSIVE).choose_path(game.view(0), [(TATAMI,), (Pos(4, 2),)]) assert choice == (Pos(4, 2),) def test_bots_start_from_the_same_square_of_every_safe_room() -> None: board = fixture_board() assert len({own_frame(start_square(board, corner), corner) for corner in range(4)}) == 1 def test_tied_paths_are_chosen_alike_from_every_corner() -> None: game, _, _ = make_game(players=4) agent = HeuristicAgent(BALANCED) chosen = set() for seat, player in enumerate(game.players): player.position = rotate(Pos(4, 3), player.corner) tatami = [(rotate(square, player.corner),) for square in (Pos(5, 3), Pos(3, 3), Pos(4, 2))] choice = agent.choose_path(game.view(seat), tatami) chosen.add(own_frame(choice[0], player.corner)) assert chosen == {Pos(3, 3)} def test_brown_heuristic_takes_the_exam_when_it_can() -> None: game, _, _ = make_game() put(game, 0, DOJO_ENTRANCE, track=15) choice = HeuristicAgent().choose_path(game.view(0), [(ALLENAMENTO,), (DOJO,)]) assert choice == (DOJO,) @pytest.mark.parametrize(("profile", "fights"), [(FARMER, False), (AGGRESSIVE, True)]) def test_profiles_differ_on_an_even_fight(profile: Profile, fights: bool) -> None: game, _, _ = make_game() put(game, 0, Pos(4, 3), track=3) put(game, 1, TATAMI, track=3) choice = HeuristicAgent(profile).choose_path(game.view(0), [(Pos(5, 3),), (TATAMI,)]) assert (choice == (TATAMI,)) is fights def fight(attacker_total: int, defender_total: int) -> FightView: return FightView(0, 1, False, attacker_total, defender_total, played=()) @pytest.mark.parametrize( ("me", "card", "totals", "plays"), [ (0, Card.KIAI, (3, 5), True), # 5 ties 5: Senshu gives it to the attacker. (0, Card.KIAI, (2, 5), False), # Kiai cannot turn it: keep it. (0, Card.KIAI, (5, 5), False), # Already winning. (1, Card.KIAI, (5, 4), True), # The defender needs to beat, not tie. (1, Card.KIAI, (6, 4), False), (0, Card.KAESHI, (1, 6), True), # Losing: a reroll can only help. (1, Card.UKE, (4, 4), True), ], ) def test_heuristic_plays_a_tecnica_only_when_losing( me: int, card: Card, totals: tuple[int, int], plays: bool ) -> None: game, _, _ = make_game() game.players[me].tecnica = card assert HeuristicAgent().play_tecnica(game.view(me), fight(*totals)) is plays def test_heuristic_keeps_the_tecnica_its_profile_ranks_first() -> None: game, _, _ = make_game() assert HeuristicAgent(BALANCED).keep_tecnica(game.view(0), Card.UKE, Card.KIAI) is Card.KIAI assert HeuristicAgent(FARMER).keep_tecnica(game.view(0), Card.UKE, Card.KIAI) is Card.UKE def test_casual_bot_compares_every_move_when_offered_few() -> None: game, _, _ = make_game() choice = CasualAgent(random.Random(0)).choose_path(game.view(0), [(FATICA,), (ALLENAMENTO,)]) assert choice == (ALLENAMENTO,) def test_casual_bot_compares_only_a_sample_of_many_moves() -> None: game, _, _ = make_game() paths = [(Pos(4, col),) for col in range(8)] choice = CasualAgent(random.Random(3), sample=1).choose_path(game.view(0), paths) assert choice == random.Random(3).sample(paths, 1)[0] def test_casual_bot_needs_a_sample() -> None: with pytest.raises(ValueError, match="sample size 0"): CasualAgent(random.Random(0), sample=0) def play(board: Board, config: RulesConfig, seed: int) -> Game: rng = random.Random(seed) agents: list[Agent] = [HeuristicAgent(p) for p in PROFILES.values()] agents.append(RandomAgent(rng)) game = Game(board, config, agents, RandomSource(seed)) game.run() return game @pytest.mark.parametrize( "config", [ DEFAULT_RULES, RulesConfig(exam_ties_to_defender=True, exam_defender_black=True, exam_rounds=3), ], ) def test_bots_play_full_games_reproducibly(board: Board, config: RulesConfig) -> None: first, again = play(board, config, seed=7), play(board, config, seed=7) assert first.winner is not None assert first.events == again.events