Rules v0.8: Fatica on crossing, no Kumite in round 1, board draft 2
- Fatica costs a stripe for every Fatica walked onto; a route whose Fatica drops a Brown player below 15 no longer enters the Dojo - No Kumite in the first round: opponents are treated like meditating pawns - Board draft 2: two Fatica tolls per quadrant, Allenamento moved to the ring - Casual bot, --summary-csv, per-seat breakdown and move-options metric - Out of contention measured at 3 belts - Plan: Phase 6 baselines, decisions D34-D43 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+57
-6
@@ -1,15 +1,15 @@
|
||||
"""Simulation output: a plain-text summary per scenario and one CSV row per game."""
|
||||
"""Simulation output: a plain-text summary per scenario, and CSV files with one row per game
|
||||
or one row per scenario."""
|
||||
|
||||
import csv
|
||||
import dataclasses
|
||||
from collections.abc import Iterable
|
||||
from pathlib import Path
|
||||
|
||||
from dojo.metrics import GameMetrics, Spread, Summary
|
||||
from dojo.metrics import CONTENTION_BELTS, GameMetrics, SeatSummary, Spread, Summary
|
||||
|
||||
|
||||
def format_summary(summary: Summary) -> str:
|
||||
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())
|
||||
income = ", ".join(
|
||||
f"{source} +{gain:.2f}/{loss:.2f}"
|
||||
@@ -35,12 +35,14 @@ def format_summary(summary: Summary) -> str:
|
||||
f"Winner led {summary.winner_lead_share:.0%} of turns; halfway leader won "
|
||||
f"{summary.halfway_leader_wins:.0%} of {summary.halfway_games} games",
|
||||
f"Contention {summary.contenders_at_end:.2f} rivals within one belt at the end; "
|
||||
f"{summary.out_of_contention:.0%} of player-turns out (>2 belts behind)",
|
||||
f"{summary.out_of_contention:.0%} of player-turns out (>{CONTENTION_BELTS} belts behind)",
|
||||
f"Worst player {_shares(summary.worst_out_of_contention)} (share of turns out)",
|
||||
f"Rank swaps {_spread(summary.rank_swaps)}",
|
||||
f"Leader beaten {summary.leader_kumite_losses:.2f} Kumite lost by the leader per game",
|
||||
f"Final spread {_spread(summary.final_spread)}",
|
||||
f"Wins by seat {seats}",
|
||||
f"Move options {summary.end_squares_per_move:.1f} end squares/move; can end on "
|
||||
f"Allenamento {summary.allenamento_reachable:.0%}, Fatica {summary.fatica_reachable:.0%}",
|
||||
*(f"Seat {seat:<11}{_seat(stats)}" for seat, stats in enumerate(summary.seats)),
|
||||
f"Wins by bot {bots}",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
@@ -52,7 +54,56 @@ def write_games_csv(path: Path, games: Iterable[GameMetrics]) -> None:
|
||||
writer = csv.DictWriter(file, fieldnames=fields)
|
||||
writer.writeheader()
|
||||
for game in games:
|
||||
writer.writerow(dataclasses.asdict(game))
|
||||
writer.writerow(
|
||||
{
|
||||
name: ",".join(map(str, value)) if isinstance(value, tuple) else value
|
||||
for name, value in dataclasses.asdict(game).items()
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def write_summary_csv(path: Path, summaries: Iterable[Summary]) -> None:
|
||||
"""One row per scenario; a column missing from a scenario (fewer seats, other bots) is empty."""
|
||||
rows = [_summary_row(summary) for summary in summaries]
|
||||
fields = list(dict.fromkeys(name for row in rows for name in row))
|
||||
with path.open("w", encoding="utf-8", newline="") as file:
|
||||
writer = csv.DictWriter(file, fieldnames=fields, restval="")
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
|
||||
|
||||
def _summary_row(summary: Summary) -> dict[str, object]:
|
||||
row: dict[str, object] = {}
|
||||
for field in dataclasses.fields(Summary):
|
||||
name, value = field.name, getattr(summary, field.name)
|
||||
match value:
|
||||
case Spread():
|
||||
row |= {f"{name}_{stat}": round(v, 3) for stat, v in _numbers(value).items()}
|
||||
case float():
|
||||
row[name] = round(value, 4)
|
||||
case str() | int():
|
||||
row[name] = value
|
||||
case _:
|
||||
pass # Income, seats and bots are flattened below.
|
||||
for source, (gain, loss) in summary.income_per_turn.items():
|
||||
row[f"{source}_gain_per_turn"] = round(gain, 4)
|
||||
row[f"{source}_loss_per_turn"] = round(loss, 4)
|
||||
for seat, stats in enumerate(summary.seats):
|
||||
row |= {f"seat{seat}_{stat}": round(v, 4) for stat, v in _numbers(stats).items()}
|
||||
row |= {f"win_rate_{bot}": round(v, 4) for bot, v in summary.win_rate_by_bot.items()}
|
||||
return row
|
||||
|
||||
|
||||
def _numbers(record: Spread | SeatSummary) -> dict[str, float]:
|
||||
return dataclasses.asdict(record)
|
||||
|
||||
|
||||
def _seat(stats: SeatSummary) -> str:
|
||||
return (
|
||||
f"won {stats.win_rate:.0%}, final track {stats.final_track:.1f}, "
|
||||
f"Exam {stats.exam_attempts_per_game:.2f} attempts/game, "
|
||||
f"defended in {stats.exam_defended:.0%} of games"
|
||||
)
|
||||
|
||||
|
||||
def _spread(spread: Spread) -> str:
|
||||
|
||||
Reference in New Issue
Block a user