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:
@@ -394,3 +394,24 @@ def test_nearest_enemy_targeting_and_full_rounds() -> None:
|
||||
"round 4 gob: orc-2 down",
|
||||
"battle over: players win in 4 rounds",
|
||||
)
|
||||
|
||||
|
||||
def test_melee_routes_around_wall() -> None:
|
||||
"""Melee unit follows the true shortest path instead of oscillating at a wall."""
|
||||
legend = {
|
||||
".": TerrainType(type="floor", move_cost=1),
|
||||
"#": TerrainType(type="wall", move_cost=None, blocks_los=True),
|
||||
}
|
||||
spec = MapSpec(
|
||||
name="wall-test",
|
||||
terrain=("....#...", "....#...", "........"),
|
||||
legend=legend,
|
||||
)
|
||||
grid = Grid.from_spec(spec)
|
||||
mover = make_combatant("mover", hp=20, speed=30)
|
||||
target = make_combatant("target", speed=0)
|
||||
states = [make_state(mover, "players", (1, 1)), make_state(target, "monsters", (1, 5))]
|
||||
engine = CombatEngine(SeededRng(42), grid, states)
|
||||
result = engine.run()
|
||||
assert result.winner == "players"
|
||||
assert result.stats["mover"].hits > 0
|
||||
|
||||
Reference in New Issue
Block a user