Combat Simulator — Design
Date: 2026-09-05 · Status: implemented as v1 in this repo
Purpose
A deterministic implementation of the rulebook (docs/rulebook.md) that can (a) fight speculative assembled mechs against each other thousands of times per second for tuning, and (b) show a single fight die by die. It is the source of truth for the rules as we move toward the real game: any engine that ships later must reproduce this engine's results on the same seeds and the same part data.
Stack and why
TypeScript, single pnpm package, matching the battlecon browser-prototype pattern.
engine/pure, side-effect free, seeded RNG, zero dependencies. One language for CLI and UI means one implementation of the rules.data/*.jsonthe part, pilot and kit catalog. JSON so a future engine in any language consumes the same catalog unchanged.cli/Node entry (pnpm sim) for batches, single verbose games, catalog and build inspection.web/Vite + React bench: assemble rosters, step through a game, run batches in a Web Worker with charts.
Engine architecture
| Module | Rules | Responsibility |
|---|---|---|
rng.ts |
A1 | mulberry32 seeded RNG, scriptedRng for tests, dice notation 2d6+2 |
modifiers.ts |
F2 | [+]/[−] cancel, roll remainder as d6, take highest |
hex.ts |
H | axial hex math (distance, line, odd-r offset, step toward) |
board.ts |
H2–H7 | hand-authored MapDef (offset coords, ordered features), terrain and elevation, movement cost and Dijkstra reachability, line of sight with interpolated sight height, hard/soft cover, high ground, zones |
maps.ts |
H1 | map registry: flat (open field) and quarry (data/maps/quarry.json) |
assembly.ts |
B, C7, F4 | resolve builds, derived stats, largest-remainder hit bands, team validation |
combat.ts |
F, G | mech runtime state, resolveFire (hit → location → pen → damage → crit → heat), stress |
battle.ts |
E, I | GameState: deployment, initiative, alternating activations, scoring, event log, snapshots |
ai.ts |
— | positional policy: scores every reachable hex by modifier-aware expected damage, incoming threat, cover, high ground and zones, then Barrage/Fire/Aimed/Lock On/Vent/Brace/Stabilize |
sim.ts |
— | runBatch: seeds seed..seed+n-1, aggregates weapon/part/stress/build stats |
Dice order inside resolveFire is fixed and documented because the UI replays it and the worked example (rulebook J) is a unit test: modifier d6s once, then per shot hit d20, location d20 (unless aimed), pen d6, damage dice, crit d6, on-hit heat dice; weapon heat after the last shot.
Every game emits typed events (GameEvent). activation_end carries a snapshot of every mech so viewers can scrub through a fight without re-simulating.
Scope
In (v1, 2026-09-05): full combat pipeline, per-part armor/durability/destruction, crit tables (crit at Armor + 4), heat and stress, pilots with stats, two-axis ability activation (D3) and five of the six example abilities, bonds (D4 proximity bonus), alternating activation with initiative, round cap, scoring by kills plus zone points with Hull tiebreak, roster validation. Terrain, elevation, movement costs, line of sight, cover and high ground from geometry (H2–H6), zone control (H7), hand-authored maps, deployment columns. On the open field, static per-side cover and elevation toggles stand in for terrain. The web bench draws the board, each activation's path, and each shot's line.
Out (tracked in liste): reactions (Overwatch, Backup Fire, Marrow's riposte), Guard, smoke, Indirect fire, Overcharge, melee system (F11), sideboarding and best-of-three match flow, an AI aggression knob and a second policy, falls (the AI never takes one), geometric partial cover, multi-hex footprints.
Determinism contract
- Same scenario + seed ⇒ identical events. Tests enforce it for
runGameandrunBatch. - All randomness flows through
GameState.rng. Sorting uses explicit tie-breakers. - Batch game i uses
seed + i, so any game in a batch can be replayed withpnpm sim play --seed <seed+i>.
Testing
Vitest, 84 tests. TDD throughout: tests were written first and watched to fail. Coverage of note: worked example J reproduced die for die with scriptedRng; penetration thresholds; overflow and transfer; cover; range bands; stress table; hit-band rounding (including the float tie that integer remainders fixed); roster rules; determinism.
React components are not unit-tested (no DOM test environment installed); the model behind them (web/src/model.ts) is.
Interfaces
CLI:
pnpm sim catalog
pnpm sim build courier bastion
pnpm sim play --a reaver --b bastion --seed 9 --distance 4
pnpm sim batch --a courier,foreman,reaver --b bastion,foreman,courier -n 5000 --json out.json
Team specs accept kit ids, inline chassis:head:rightArm:leftArm:legs:back:pilot, or a .json file of builds.
Web (pnpm dev): Bench (assemble, weight, stats, hit tape), Battle (seeded game, stepper, dice log), Batch (worker-backed, outcome bar, histograms, weapon and build tables, JSON export). Both interfaces print the equivalent CLI command so a finding in the browser is reproducible in a shell.
Performance
The charging AI ran about 10k games/second. The positional AI evaluates sight from every reachable hex to every enemy and back, and runs at 80–130 games/second (1000 Quarry games in 13s). Enough for thousand-game batches; the planned sweep tool will want per-activation sight caching.