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>
197 lines
6.4 KiB
Python
197 lines
6.4 KiB
Python
"""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
|