from dataclasses import replace import pytest from dojo.board import Pos from dojo.events import ( Event, ExamTaken, FightKind, GameOver, KumiteFought, Moved, Source, TrackChanged, TurnSkipped, TurnStarted, ) from dojo.metrics import KUMITE_SECONDS, TURN_SECONDS, GameMetrics, measure, summarize from dojo.rules import DEFAULT_RULES HOME_0 = Pos(0, 1) # quadrant 0 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], 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_exams() -> None: metrics = run( [ TurnStarted(1, 0, 7), Moved(0, (HOME_0,)), TrackChanged(0, 0, 1, Source.SQUARE), TurnStarted(1, 1, 5), Moved(1, (HOME_0,)), TrackChanged(1, 0, 1, Source.SQUARE), fought(1, 0, FightKind.EQUAL), TrackChanged(1, 1, 4, Source.KUMITE), TrackChanged(0, 1, 0, Source.KUMITE), fought(1, 0, FightKind.UPSET), 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), ] ) assert (metrics.kumite, metrics.upsets, metrics.expected_wins) == (3, 1, 1) assert metrics.max_chain == 2 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) assert (metrics.winner_seat, metrics.winner_bot, metrics.rounds) == (0, "balanced", 2) # 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_standings_are_sampled_at_turn_ends() -> None: metrics = run( [ TurnStarted(1, 0, 7), TrackChanged(0, 0, 9, Source.CARD), # end of turn 1: (9, 0, 0), leader 0 TurnStarted(1, 1, 7), 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.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([TurnStarted(1, 0, 7), GameOver(None, 10)]) rows = [ 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, ] summary = summarize("test", rows) assert summary.finished == 0.75 assert summary.win_rate_by_seat == (0.5, 0.25) assert summary.win_rate_by_bot == {"balanced": 0.5, "random": 0.25} assert summary.rounds.median == 10 def test_summarize_needs_games() -> None: with pytest.raises(ValueError, match="no games"): summarize("test", [])