Drop variant B, adopt Black-belt Exam defender, add one-sidedness metrics
- Remove variant B (Brown cap, +1 for higher beating lower) from rules, engine, bots and simulator; a higher belt beating a lower one gains 0. - Final Exam: the defender fights as a Black belt (RULES.md v0.7). Ties to the defender and best-of-3 stay as simulator switches (`--exam`). - Report stripe income per source as gains and losses. - Add ten one-sidedness measures sampled at turn ends (lead gap, winner margin and lead share, halfway leader, last lead change, contention, rank swaps, leader Kumite losses, Exam challengers, winner pace). - Plan: decisions D28-D33 and the detailed Phase 6. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+2
-2
@@ -9,7 +9,7 @@ from dojo.board import Board, Pos, build_board
|
||||
from dojo.engine import FightView, Game, GameView
|
||||
from dojo.grid import parse_grid
|
||||
from dojo.movement import Path as Walk
|
||||
from dojo.rules import VARIANT_A, Card, RulesConfig
|
||||
from dojo.rules import DEFAULT_RULES, Card, RulesConfig
|
||||
|
||||
FIXTURE = Path(__file__).parent / "fixtures" / "quadrant_valid.txt"
|
||||
NO_CARDS: tuple[tuple[Card, int], ...] = ()
|
||||
@@ -91,7 +91,7 @@ def fixture_board() -> Board:
|
||||
|
||||
def make_game(
|
||||
players: int = 2,
|
||||
config: RulesConfig = VARIANT_A,
|
||||
config: RulesConfig = DEFAULT_RULES,
|
||||
dice: Iterable[int] = (),
|
||||
deck: tuple[tuple[Card, int], ...] = NO_CARDS,
|
||||
board: Board | None = None,
|
||||
|
||||
+25
-2
@@ -14,10 +14,11 @@ from dojo.bots import (
|
||||
HeuristicAgent,
|
||||
Profile,
|
||||
RandomAgent,
|
||||
series_probability,
|
||||
win_probability,
|
||||
)
|
||||
from dojo.engine import Agent, FightView, Game, RandomSource
|
||||
from dojo.rules import VARIANT_A, VARIANT_B, Card, RulesConfig
|
||||
from dojo.rules import DEFAULT_RULES, Card, RulesConfig
|
||||
|
||||
ALLENAMENTO = Pos(0, 5)
|
||||
FATICA = Pos(3, 6)
|
||||
@@ -30,6 +31,22 @@ 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:
|
||||
@@ -100,7 +117,13 @@ def play(board: Board, config: RulesConfig, seed: int) -> Game:
|
||||
return game
|
||||
|
||||
|
||||
@pytest.mark.parametrize("config", [VARIANT_A, VARIANT_B])
|
||||
@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)
|
||||
|
||||
|
||||
+6
-4
@@ -62,19 +62,21 @@ def test_sim_prints_a_summary_per_scenario_and_writes_games(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
games = tmp_path / "games.csv"
|
||||
argv = ["sim", "--games", "2", "--variant", "A", "B", "--players", "3", "--jobs", "1"]
|
||||
argv = ["sim", "--games", "2", "--exam", "c", "none", "--players", "3", "--jobs", "1"]
|
||||
|
||||
assert main([*argv, "--csv", str(games)]) == EXIT_OK
|
||||
|
||||
output = capsys.readouterr().out
|
||||
assert "== A p3 kiai+2 start0 balanced,farmer,aggressive (2 games" in output
|
||||
assert "== B p3 kiai+2 start0" in output
|
||||
assert "== exam:none p3 kiai+2 start0 balanced,farmer,aggressive (2 games" in output
|
||||
assert "== exam:c p3 kiai+2 start0" in output
|
||||
rows = games.read_text(encoding="utf-8").splitlines()
|
||||
assert rows[0].startswith("scenario,seed,lineup,winner_seat")
|
||||
assert len(rows) == 1 + 4
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", [["--lineup", "balanced,cheater"], ["--games", "0"]])
|
||||
@pytest.mark.parametrize(
|
||||
"bad", [["--lineup", "balanced,cheater"], ["--games", "0"], ["--exam", "b"]]
|
||||
)
|
||||
def test_sim_rejects_bad_arguments(bad: list[str], capsys: pytest.CaptureFixture[str]) -> None:
|
||||
with pytest.raises(SystemExit) as exit_info:
|
||||
main(["sim", *bad])
|
||||
|
||||
+89
-31
@@ -22,7 +22,7 @@ from dojo.events import (
|
||||
TrackChanged,
|
||||
TurnSkipped,
|
||||
)
|
||||
from dojo.rules import VARIANT_A, VARIANT_B, Card
|
||||
from dojo.rules import BLACK, DEFAULT_RULES, Card, RulesConfig
|
||||
|
||||
START_0 = Pos(0, 0)
|
||||
START_1 = Pos(0, 13)
|
||||
@@ -58,7 +58,7 @@ def test_seats_up_to_four_take_corners_clockwise() -> None:
|
||||
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))
|
||||
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)
|
||||
@@ -104,16 +104,15 @@ def test_upset_swaps_a_belt() -> None:
|
||||
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])
|
||||
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 + gain
|
||||
assert game.players[0].track == 6
|
||||
assert game.players[1].track == 0
|
||||
assert game.players[1].position == START_1
|
||||
|
||||
@@ -311,18 +310,8 @@ def test_allenamento_and_fatica() -> None:
|
||||
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])
|
||||
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))
|
||||
@@ -330,16 +319,6 @@ def test_variant_a_squares_can_make_a_player_brown() -> None:
|
||||
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)
|
||||
@@ -545,8 +524,10 @@ def test_only_one_tecnica_can_be_held(keep_drawn: bool, kept: Card) -> None:
|
||||
# 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)
|
||||
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
|
||||
@@ -614,6 +595,83 @@ def test_meditating_players_cannot_defend() -> None:
|
||||
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)
|
||||
@@ -635,7 +693,7 @@ def test_a_game_runs_to_completion() -> None:
|
||||
|
||||
from scripted import ScriptedAgent, fixture_board
|
||||
|
||||
config = replace(VARIANT_A, max_rounds=30)
|
||||
config = replace(DEFAULT_RULES, max_rounds=30)
|
||||
game = Game(fixture_board(), config, [ScriptedAgent() for _ in range(4)], RandomSource(7))
|
||||
|
||||
game.run()
|
||||
|
||||
+64
-29
@@ -12,26 +12,28 @@ from dojo.events import (
|
||||
Moved,
|
||||
Source,
|
||||
TrackChanged,
|
||||
TurnSkipped,
|
||||
TurnStarted,
|
||||
)
|
||||
from dojo.metrics import KUMITE_SECONDS, TURN_SECONDS, GameMetrics, measure, summarize
|
||||
from dojo.rules import VARIANT_A
|
||||
from dojo.rules import DEFAULT_RULES
|
||||
|
||||
CORNERS = (0, 1)
|
||||
BOTS = ("balanced", "random")
|
||||
HOME_0 = Pos(0, 1) # quadrant 0
|
||||
HOME_1 = Pos(0, 14) # quadrant 1
|
||||
|
||||
|
||||
def fought(attacker: int, defender: int, kind: FightKind, *, exam: bool = False) -> KumiteFought:
|
||||
return KumiteFought(attacker, defender, 0, 0, 4, 3, True, kind, exam, cards=())
|
||||
def fought(
|
||||
attacker: int, defender: int, kind: FightKind, *, won: bool = True, exam: bool = False
|
||||
) -> KumiteFought:
|
||||
return KumiteFought(attacker, defender, 0, 0, 4, 3, won, kind, exam, cards=())
|
||||
|
||||
|
||||
def run(events: list[Event]) -> GameMetrics:
|
||||
return measure(events, VARIANT_A, CORNERS, BOTS, scenario="test", seed=3)
|
||||
def run(events: list[Event], players: int = 2) -> GameMetrics:
|
||||
corners = tuple(range(players))
|
||||
bots = ("balanced", "random", "farmer")[:players]
|
||||
return measure(events, DEFAULT_RULES, corners, bots, scenario="test", seed=3)
|
||||
|
||||
|
||||
def test_measure_counts_fights_income_and_leads() -> None:
|
||||
def test_measure_counts_fights_income_and_exams() -> None:
|
||||
metrics = run(
|
||||
[
|
||||
TurnStarted(1, 0, 7),
|
||||
@@ -47,6 +49,8 @@ def test_measure_counts_fights_income_and_leads() -> None:
|
||||
TurnStarted(2, 0, 6),
|
||||
fought(1, 0, FightKind.EXPECTED), # Player 1 defends: not part of a chain.
|
||||
TrackChanged(0, 0, 5, Source.CARD),
|
||||
TrackChanged(0, 5, 4, Source.CARD),
|
||||
fought(0, 1, FightKind.EQUAL, won=False, exam=True),
|
||||
fought(0, 1, FightKind.EQUAL, exam=True),
|
||||
ExamTaken(0, 1, passed=True),
|
||||
GameOver(0, 2),
|
||||
@@ -55,41 +59,72 @@ def test_measure_counts_fights_income_and_leads() -> None:
|
||||
|
||||
assert (metrics.kumite, metrics.upsets, metrics.expected_wins) == (3, 1, 1)
|
||||
assert metrics.max_chain == 2
|
||||
assert (metrics.exam_attempts, metrics.exam_passes) == (1, 1)
|
||||
assert (metrics.income_square, metrics.income_kumite, metrics.income_card) == (2, 2, 5)
|
||||
assert (metrics.exam_attempts, metrics.exam_passes, metrics.exam_challengers) == (1, 1, 1)
|
||||
assert (metrics.square_gain, metrics.square_loss) == (2, 0)
|
||||
assert (metrics.kumite_gain, metrics.kumite_loss) == (3, -1)
|
||||
assert (metrics.card_gain, metrics.card_loss) == (5, -1)
|
||||
assert (metrics.square_income_home, metrics.square_income_away) == (1, 1)
|
||||
# Leader: 0 (1-0), tie (1-1) keeps it, 1 (0-4), 0 (5-4).
|
||||
assert metrics.lead_changes == 2
|
||||
assert metrics.final_spread == 1
|
||||
assert (metrics.winner_seat, metrics.winner_bot, metrics.rounds) == (0, "balanced", 2)
|
||||
assert metrics.minutes == pytest.approx(round((3 * TURN_SECONDS + 4 * KUMITE_SECONDS) / 60, 1))
|
||||
# Both Exam rounds count as fights for the time estimate.
|
||||
assert metrics.minutes == pytest.approx(round((3 * TURN_SECONDS + 5 * KUMITE_SECONDS) / 60, 1))
|
||||
|
||||
|
||||
def test_measure_tracks_turns_started_on_14() -> None:
|
||||
def test_standings_are_sampled_at_turn_ends() -> None:
|
||||
metrics = run(
|
||||
[
|
||||
TrackChanged(0, 0, 14, Source.CARD),
|
||||
TurnStarted(1, 0, 7),
|
||||
TrackChanged(0, 0, 9, Source.CARD), # end of turn 1: (9, 0, 0), leader 0
|
||||
TurnStarted(1, 1, 7),
|
||||
TurnStarted(2, 0, 7),
|
||||
TrackChanged(0, 14, 15, Source.KUMITE),
|
||||
TurnStarted(2, 1, 7),
|
||||
TurnStarted(3, 0, 7),
|
||||
TrackChanged(0, 15, 14, Source.SQUARE),
|
||||
TurnStarted(4, 0, 7),
|
||||
GameOver(None, 4),
|
||||
TrackChanged(1, 0, 3, Source.SQUARE),
|
||||
TrackChanged(1, 3, 10, Source.CARD), # end of turn 2: (9, 10, 0), leader 1
|
||||
TurnStarted(1, 2, 7),
|
||||
TrackChanged(2, 0, 10, Source.CARD), # end of turn 3: (9, 10, 10), no strict leader
|
||||
TurnSkipped(2, 0),
|
||||
TrackChanged(2, 10, 15, Source.KUMITE), # end of turn 4: (9, 10, 15), leader 2
|
||||
GameOver(2, 2),
|
||||
],
|
||||
players=3,
|
||||
)
|
||||
|
||||
assert metrics.lead_changes == 2
|
||||
assert metrics.last_lead_change == pytest.approx(4 / 4)
|
||||
assert metrics.lead_gap_mean == pytest.approx((9 + 1 + 0 + 5) / 4)
|
||||
assert metrics.winner_margin == 5
|
||||
assert metrics.winner_lead_share == pytest.approx(1 / 4)
|
||||
# Halfway is the end of turn 2, led by player 1.
|
||||
assert metrics.halfway_leader_won is False
|
||||
assert metrics.contenders_at_end == 0
|
||||
# Out (more than 6 behind): players 1 and 2 after turn 1, player 2 after turn 2.
|
||||
assert metrics.out_of_contention_share == pytest.approx(3 / 12)
|
||||
assert metrics.worst_out_of_contention_share == pytest.approx(0.5)
|
||||
# 0>1 becomes 1>0; 0>2 becomes 2>0; 1>2 (ties skipped) becomes 2>1.
|
||||
assert metrics.rank_swaps == 3
|
||||
assert metrics.winner_turns_to_brown == 1
|
||||
assert metrics.final_spread == 6
|
||||
|
||||
|
||||
def test_leader_losing_a_kumite_is_counted() -> None:
|
||||
metrics = run(
|
||||
[
|
||||
TurnStarted(1, 0, 7),
|
||||
TrackChanged(0, 0, 3, Source.CARD),
|
||||
TurnStarted(1, 1, 7),
|
||||
fought(1, 0, FightKind.EQUAL), # Leader 0 loses.
|
||||
fought(1, 0, FightKind.EQUAL, won=False), # The attacker loses: 0 wins.
|
||||
GameOver(None, 1),
|
||||
]
|
||||
)
|
||||
|
||||
assert metrics.turns_on_14 == 3
|
||||
assert metrics.longest_14_streak == 2
|
||||
assert metrics.winner_seat is None
|
||||
assert metrics.leader_kumite_losses == 1
|
||||
assert metrics.winner_margin is None
|
||||
assert metrics.halfway_leader_won is None
|
||||
assert metrics.last_lead_change is None
|
||||
|
||||
|
||||
def test_summarize_splits_wins_by_seat_and_bot() -> None:
|
||||
base = run([GameOver(None, 10)])
|
||||
base = run([TurnStarted(1, 0, 7), GameOver(None, 10)])
|
||||
rows = [
|
||||
replace(base, winner_seat=0, winner_bot="balanced"),
|
||||
replace(base, lineup="balanced,random", winner_seat=0, winner_bot="balanced"),
|
||||
replace(base, lineup="random,balanced", winner_seat=1, winner_bot="balanced"),
|
||||
replace(base, lineup="random,balanced", winner_seat=0, winner_bot="random"),
|
||||
base,
|
||||
|
||||
+35
-13
@@ -2,6 +2,7 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from dojo.rules import RulesConfig
|
||||
from dojo.sim import BoardFiles, Scenario, matrix, run
|
||||
|
||||
BOARD = Path(__file__).parent.parent / "board"
|
||||
@@ -10,16 +11,35 @@ LINEUP = ("balanced", "random")
|
||||
|
||||
|
||||
def test_matrix_crosses_every_axis() -> None:
|
||||
scenarios = matrix(["A", "B"], [3, 4], [2, 1], [0], [LINEUP])
|
||||
scenarios = matrix(["none", "acd"], [3, 4], [2, 1], [0], [LINEUP])
|
||||
|
||||
assert len(scenarios) == 8
|
||||
assert scenarios[0].name == "A p3 kiai+2 start0 balanced,random"
|
||||
assert scenarios[-1].config.kiai_bonus == 1
|
||||
assert scenarios[-1].config.brown_requires_kumite
|
||||
assert scenarios[0].name == "exam:none p3 kiai+2 start0 balanced,random"
|
||||
assert scenarios[0].config == RulesConfig(exam_defender_black=False)
|
||||
assert scenarios[-1].config == RulesConfig(
|
||||
exam_ties_to_defender=True, exam_defender_black=True, exam_rounds=3, kiai_bonus=1
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exam", "ties", "black", "rounds"),
|
||||
[
|
||||
("none", False, False, 1),
|
||||
("a", True, False, 1),
|
||||
("c", False, True, 1),
|
||||
("d", False, False, 3),
|
||||
("cd", False, True, 3),
|
||||
],
|
||||
)
|
||||
def test_exam_options_map_to_rules(exam: str, ties: bool, black: bool, rounds: int) -> None:
|
||||
config = Scenario(exam, 4, 2, 0, LINEUP).config
|
||||
|
||||
assert (config.exam_ties_to_defender, config.exam_defender_black) == (ties, black)
|
||||
assert config.exam_rounds == rounds
|
||||
|
||||
|
||||
def test_lineup_fills_the_seats_and_rotates_each_game() -> None:
|
||||
scenario = Scenario("A", 3, 2, 0, LINEUP)
|
||||
scenario = Scenario("none", 3, 2, 0, LINEUP)
|
||||
|
||||
assert scenario.seating(0) == ("balanced", "random", "balanced")
|
||||
assert scenario.seating(1) == ("random", "balanced", "balanced")
|
||||
@@ -27,23 +47,25 @@ def test_lineup_fills_the_seats_and_rotates_each_game() -> None:
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("variant", "players", "lineup", "message"),
|
||||
("exam", "players", "lineup", "message"),
|
||||
[
|
||||
("C", 4, LINEUP, "unknown variant"),
|
||||
("A", 4, ("balanced", "cheater"), "unknown bots"),
|
||||
("A", 4, (), "unknown bots"),
|
||||
("A", 9, LINEUP, "supported range"),
|
||||
("b", 4, LINEUP, "unknown Exam options"),
|
||||
("aa", 4, LINEUP, "unknown Exam options"),
|
||||
("", 4, LINEUP, "unknown Exam options"),
|
||||
("none", 4, ("balanced", "cheater"), "unknown bots"),
|
||||
("none", 4, (), "unknown bots"),
|
||||
("none", 9, LINEUP, "supported range"),
|
||||
],
|
||||
)
|
||||
def test_invalid_scenarios_are_rejected(
|
||||
variant: str, players: int, lineup: tuple[str, ...], message: str
|
||||
exam: str, players: int, lineup: tuple[str, ...], message: str
|
||||
) -> None:
|
||||
with pytest.raises(ValueError, match=message):
|
||||
Scenario(variant, players, 2, 0, lineup)
|
||||
Scenario(exam, players, 2, 0, lineup)
|
||||
|
||||
|
||||
def test_runs_are_reproducible_and_independent_of_worker_count() -> None:
|
||||
scenarios = matrix(["A", "B"], [3], [2], [0], [LINEUP])
|
||||
scenarios = matrix(["none", "acd"], [3], [2], [0], [LINEUP])
|
||||
|
||||
serial = run(FILES, scenarios, games=2, seed=5, jobs=1)
|
||||
parallel = run(FILES, scenarios, games=2, seed=5, jobs=2)
|
||||
|
||||
Reference in New Issue
Block a user