feat(cli): add --transcript option

This commit is contained in:
2026-09-04 23:34:12 +02:00
parent cedff5b932
commit 7591614460
4 changed files with 61 additions and 2 deletions
+2
View File
@@ -54,6 +54,8 @@ allowed-confusables = ["σ"]
"src/pf1e_simulator/cli.py" = ["T201"]
# combat.py: CombatEngine.__init__ needs 6 params (rng, grid, states, round_cap, policy, spell_registry)
"src/pf1e_simulator/combat.py" = ["PLR0913"]
# runner.py: run_encounter needs 6 params (spec, runs, seed, policy, spell_registry, capture_first_transcript)
"src/pf1e_simulator/runner.py" = ["PLR0913"]
# ── Basedpyright ──────────────────────────────────────────────────────────────
[tool.basedpyright]
+16 -1
View File
@@ -100,6 +100,11 @@ def main(argv: list[str] | None = None) -> int:
default=100,
help="max rounds per battle (default: 100)",
)
parser.add_argument(
"--transcript",
action="store_true",
help="print the turn-by-turn log of the first battle before the report",
)
args = parser.parse_args(argv)
if not args.side:
parser.error("at least one --side is required")
@@ -113,9 +118,19 @@ def main(argv: list[str] | None = None) -> int:
return 2
try:
registry = load_spell_registry(SPELLS_DIR)
report = run_encounter(spec, runs=args.runs, seed=args.seed, spell_registry=registry)
report = run_encounter(
spec,
runs=args.runs,
seed=args.seed,
spell_registry=registry,
capture_first_transcript=args.transcript,
)
except ValueError as exc:
print(f"pf1e-sim: {exc}", file=sys.stderr)
return 2
if args.transcript and report.transcript is not None:
print("=== Déroulé du premier combat ===")
print("\n".join(report.transcript))
print()
print(format_report(report, spec))
return 0
+15 -1
View File
@@ -57,7 +57,12 @@ class CombatantAttrition:
@dataclass(frozen=True)
class EncounterReport:
"""Aggregated results of a Monte Carlo encounter."""
"""Aggregated results of a Monte Carlo encounter.
`transcript` holds the turn-by-turn log of the first battle when
`run_encounter` is called with ``capture_first_transcript=True``; it is
None otherwise (the default keeps the report cheap for pure Monte Carlo).
"""
runs: int
seed: int
@@ -65,6 +70,7 @@ class EncounterReport:
draws: int
avg_rounds: float
attrition: dict[str, CombatantAttrition]
transcript: tuple[str, ...] | None = None
def win_rate(self, side: str) -> float:
"""Fraction of runs won by `side` (closed form, see metrics)."""
@@ -126,11 +132,15 @@ def run_encounter(
*,
policy: Policy | None = None,
spell_registry: dict[str, SpellSpec] | None = None,
capture_first_transcript: bool = False,
) -> EncounterReport:
"""Run `runs` battles (one SeededRng per run) and aggregate balance metrics.
The spell registry is loaded once (from ``data/spells/`` unless overridden)
and passed into every ``CombatEngine`` so caster PCs actually cast.
When ``capture_first_transcript`` is True, the turn-by-turn log of the
first battle (``seed + 0``) is stored in ``report.transcript``.
"""
if runs <= 0:
msg = "runs must be positive"
@@ -144,6 +154,7 @@ def run_encounter(
draws = 0
total_rounds = 0
totals = {cid: CombatantStats() for cid in ids}
transcript: tuple[str, ...] | None = None
for index in range(runs):
if index > 0:
states = build_states(spec)
@@ -156,6 +167,8 @@ def run_encounter(
policy=policy,
spell_registry=spell_registry,
).run()
if capture_first_transcript and index == 0:
transcript = result.transcript
if result.winner is None:
draws += 1
else:
@@ -185,4 +198,5 @@ def run_encounter(
draws=draws,
avg_rounds=total_rounds / runs,
attrition=attrition,
transcript=transcript,
)
+28
View File
@@ -57,3 +57,31 @@ def test_cli_rejects_missing_deployment_side(capsys: pytest.CaptureFixture[str])
rc = main(["--map", str(MAP), "--side", "players", str(GOBLIN), "--runs", "10"])
assert rc == 2
assert "monsters" in capsys.readouterr().err
def test_cli_transcript_prints_first_battle_log(capsys: pytest.CaptureFixture[str]) -> None:
rc = main(
[
"--map",
str(MAP),
"--side",
"players",
str(GOBLIN),
"--side",
"monsters",
str(ORC),
"--runs",
"10",
"--seed",
"1",
"--transcript",
]
)
out = capsys.readouterr().out
assert rc == 0
assert "Déroulé du premier combat" in out
assert "initiative:" in out
assert "round 1" in out
# The balance report must still be printed after the transcript.
assert "Rapport d'équilibrage" in out
assert out.index("Déroulé du premier combat") < out.index("Rapport d'équilibrage")