feat(cli): add --transcript option
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user