a21e234b41
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.
33 lines
1.0 KiB
Python
33 lines
1.0 KiB
Python
"""Closed-form statistics for Monte Carlo balance reports.
|
|
|
|
Proportions follow the binomial model: p = wins/n with standard error
|
|
sqrt(p(1-p)/n). The 3-sigma band is the default confidence interval used in
|
|
the CLI report; clamp keeps degenerate proportions (0 or 1) exact.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
|
|
|
|
def win_rate(wins: int, runs: int) -> float:
|
|
"""Fraction of runs won: wins / runs."""
|
|
if runs <= 0:
|
|
msg = "runs must be positive"
|
|
raise ValueError(msg)
|
|
return wins / runs
|
|
|
|
|
|
def win_rate_sigma(p: float, runs: int) -> float:
|
|
"""Standard error of a proportion: sqrt(p(1-p)/n)."""
|
|
if runs <= 0:
|
|
msg = "runs must be positive"
|
|
raise ValueError(msg)
|
|
return math.sqrt(p * (1 - p) / runs)
|
|
|
|
|
|
def win_rate_band(p: float, runs: int, *, sigma_count: float = 3.0) -> tuple[float, float]:
|
|
"""p +/- sigma_count standard errors, clamped to [0, 1]."""
|
|
sigma = win_rate_sigma(p, runs)
|
|
return (max(0.0, p - sigma_count * sigma), min(1.0, p + sigma_count * sigma))
|