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
+7
View File
@@ -7,6 +7,9 @@ dependencies = [
"pyyaml",
]
[project.scripts]
pf1e-sim = "pf1e_simulator.cli:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
@@ -38,6 +41,8 @@ ignore = [
# CPY001: missing copyright notice — we don't use copyright headers
"CPY001",
]
# RUF001: sigma in the CLI balance report is intentional, not a confusable
allowed-confusables = ["σ"]
[tool.ruff.lint.per-file-ignores]
# Tests: assert is expected, magic numbers are fine, annotations are noisy,
@@ -45,6 +50,8 @@ ignore = [
"tests/**" = ["S101", "PLR2004", "ANN", "PLR0913", "PLR0917"]
# rng.py: `random.Random` is used for reproducible Monte Carlo streams, not cryptography
"src/pf1e_simulator/rng.py" = ["S311"]
# cli.py: stdout printing is the point of a CLI
"src/pf1e_simulator/cli.py" = ["T201"]
# ── Basedpyright ──────────────────────────────────────────────────────────────
[tool.basedpyright]