feat(runner): Monte Carlo runner, closed-form metrics and balance CLI

Adds the S6 layer on top of the deterministic combat engine:
- metrics.py: closed-form binomial statistics (win rate, standard error,
  3-sigma confidence band) with clamped degenerate proportions.
- runner.py: EncounterSpec/Side definitions, per-run SeededRng streams,
  global duplicate-id disambiguation, attrition averages per combatant.
- cli.py: pf1e-sim entry point producing a French balance report with
  win rates, 3-sigma bands, draws, rounds, and attrition.
- Movement fix: greedy straight-line heuristic could oscillate at walls;
  _step_toward now follows the true shortest path via an unbounded
  Dijkstra cost field from the target (Grid.reachable budget=None).
- Regression tests: melee unit routes around a wall and engages; grid
  unbounded-budget coverage.
This commit is contained in:
2026-08-17 22:49:50 +02:00
parent 577aec33c4
commit a21e234b41
10 changed files with 625 additions and 7 deletions
+59
View File
@@ -0,0 +1,59 @@
"""Tests for the pf1e-sim command-line interface."""
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import pytest
from pf1e_simulator.cli import main
ROOT = Path(__file__).resolve().parents[1]
MAP = ROOT / "data" / "maps" / "sample_arena.yaml"
GOBLIN = ROOT / "data" / "monsters" / "goblin.json"
ORC = ROOT / "data" / "monsters" / "orc.json"
def test_cli_prints_balance_report(capsys: pytest.CaptureFixture[str]) -> None:
rc = main(
[
"--map",
str(MAP),
"--side",
"players",
str(GOBLIN),
str(GOBLIN),
str(GOBLIN),
"--side",
"monsters",
str(ORC),
str(ORC),
"--runs",
"50",
"--seed",
"1",
]
)
out = capsys.readouterr().out
assert rc == 0
assert "Rapport d'équilibrage" in out
assert "Victoires players" in out
assert "Victoires monsters" in out
assert "bande 3σ" in out
assert "Nuls" in out
assert "Rounds moyens" in out
assert "Attrition moyenne" in out
def test_cli_rejects_side_not_in_deployment(capsys: pytest.CaptureFixture[str]) -> None:
rc = main(["--map", str(MAP), "--side", "bogus", str(GOBLIN), "--runs", "10"])
assert rc == 2
assert "deployment" in capsys.readouterr().err
def test_cli_rejects_missing_deployment_side(capsys: pytest.CaptureFixture[str]) -> None:
rc = main(["--map", str(MAP), "--side", "players", str(GOBLIN), "--runs", "10"])
assert rc == 2
assert "monsters" in capsys.readouterr().err