Add Monte Carlo simulator, game metrics and sim CLI (Phase 5)
`dojo sim` plays seeded games over a scenario matrix (variant, players, Kiai bonus, start position, bot lineup) in parallel, prints a summary per scenario and optionally writes one CSV row per game. Metrics are replayed from the event log. Plan updated with the Phase 5 baseline. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
"""Simulation output: a plain-text summary per scenario and one CSV row per game."""
|
||||
|
||||
import csv
|
||||
import dataclasses
|
||||
from collections.abc import Iterable
|
||||
from pathlib import Path
|
||||
|
||||
from dojo.events import Source
|
||||
from dojo.metrics import GameMetrics, Spread, Summary
|
||||
|
||||
|
||||
def format_summary(summary: Summary) -> str:
|
||||
income = summary.income_per_turn
|
||||
seats = ", ".join(f"{seat} {rate:.0%}" for seat, rate in enumerate(summary.win_rate_by_seat))
|
||||
bots = ", ".join(f"{bot} {rate:.0%}" for bot, rate in summary.win_rate_by_bot.items())
|
||||
lines = [
|
||||
f"== {summary.scenario} ({summary.games} games, {summary.finished:.0%} finished)",
|
||||
f"Rounds {_spread(summary.rounds)}",
|
||||
f"Minutes {_spread(summary.minutes)}",
|
||||
f"Kumite {summary.kumite_per_round:.2f}/round; upsets {summary.upset_rate:.0%}, "
|
||||
f"higher beats lower {summary.expected_rate:.0%}",
|
||||
f"Longest chain {_spread(summary.max_chain)} (Kumite in one turn, per game)",
|
||||
f"Exam {summary.exam_attempts_per_game:.2f} attempts/game, "
|
||||
f"{summary.exam_success:.0%} passed",
|
||||
f"Stuck on 14 {summary.turns_on_14_per_game:.2f} turns/game, "
|
||||
f"longest streak {summary.longest_14_streak} turns",
|
||||
f"Income/turn square {income[Source.SQUARE]:+.2f} "
|
||||
f"(home {summary.square_income_home_per_turn:+.2f}, "
|
||||
f"away {summary.square_income_away_per_turn:+.2f}), "
|
||||
f"card {income[Source.CARD]:+.2f}, kumite {income[Source.KUMITE]:+.2f}, "
|
||||
f"exam {income[Source.EXAM]:+.2f}",
|
||||
f"Lead changes {_spread(summary.lead_changes)}",
|
||||
f"Final spread {_spread(summary.final_spread)}",
|
||||
f"Wins by seat {seats}",
|
||||
f"Wins by bot {bots}",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def write_games_csv(path: Path, games: Iterable[GameMetrics]) -> None:
|
||||
fields = [field.name for field in dataclasses.fields(GameMetrics)]
|
||||
with path.open("w", encoding="utf-8", newline="") as file:
|
||||
writer = csv.DictWriter(file, fieldnames=fields)
|
||||
writer.writeheader()
|
||||
for game in games:
|
||||
writer.writerow(dataclasses.asdict(game))
|
||||
|
||||
|
||||
def _spread(spread: Spread) -> str:
|
||||
return (
|
||||
f"mean {spread.mean:.1f} p10 {spread.p10:.1f} median {spread.median:.1f} "
|
||||
f"p90 {spread.p90:.1f}"
|
||||
)
|
||||
Reference in New Issue
Block a user