Phase 22: Napoleonic Era¶
Summary¶
Napoleonic era data package (~53 YAML files) + 6 engine extensions (volley fire, melee combat, cavalry charges, Napoleonic formations, courier C2, foraging logistics) + 2 validation scenarios (Austerlitz, Waterloo). 233 new tests, 5,659 total passing.
Follows the Phase 20-21 era framework pattern: era config + data package + targeted engine extensions. No new dependencies.
What Was Built¶
22a: Era Config + Data (102 tests)¶
core/era.py—NAPOLEONIC_ERA_CONFIG: disables ew, space, cbrn, gps, thermal_sights, data_links, pgm. VISUAL-only sensors.c2_delay_multiplier=8.0.simulation/scenario.py— 6 newSimulationContextfields:volley_fire_engine,melee_engine,cavalry_engine,formation_napoleonic_engine,courier_engine,foraging_engine. All in state persistence.- ~53 YAML data files:
- 10 units: french_line_infantry, french_light_infantry, french_old_guard, british_line_infantry, british_rifle_company, cuirassier_squadron, hussar_squadron, lancer_squadron, horse_artillery_battery, foot_artillery_battery
- 9 weapons: brown_bess, charleville_1777, baker_rifle, 6pdr_cannon, 12pdr_cannon, howitzer_napoleonic, cavalry_saber, lance, bayonet
- 9 ammo: musket_ball_75, musket_ball_69, rifle_ball, roundshot_6pdr, roundshot_12pdr, canister_6pdr, canister_12pdr, howitzer_shell_nap, howitzer_canister_nap
- 3 sensors: telescope_napoleonic, cavalry_scout, observation_post_napoleonic
- 10 signatures: one per unit (zeroed thermal/radar/EM)
- 3 doctrines: french_grande_armee, british_thin_red_line, coalition_linear
- 3 commanders: napoleon_grande_armee, wellington_defense, blucher_offensive
- 2 comms: mounted_courier, drum_bugle_signals
- 2 scenarios: austerlitz, waterloo
22b: Engine Extensions (98 tests)¶
| Module | Lines | Purpose |
|---|---|---|
combat/volley_fire.py |
~230 | Massed musket fire aggregate model. Binomial casualties from range table interpolation × formation × smoke × volley type. Canister sub-model. |
combat/melee.py |
~210 | Contact combat. Pre-contact morale check (cavalry shock lowers defender threshold). Force ratio × base rate × formation modifier. Pursuit casualties. |
movement/cavalry.py |
~250 | Charge state machine: WALK→TROT→GALLOP→CHARGE→IMPACT→PURSUIT→RALLY. Distance-driven phase transitions. Fatigue accumulation. Max gallop duration. |
movement/formation_napoleonic.py |
~220 | LINE/COLUMN/SQUARE/SKIRMISH. Firepower fraction, speed, cavalry/artillery vulnerability per formation. Worst-of-both during transitions. 30-120s transition times. |
c2/courier.py |
~230 | Physical messenger dispatch. Terrain-dependent speed. Interception risk per km. Drum/bugle range limit. Courier pool per HQ. |
logistics/foraging.py |
~200 | Zone-based terrain productivity × seasonal modifier × remaining fraction. Depletion/recovery. Ambush risk per foraging mission. |
22c: Validation Scenarios (33 tests)¶
- Austerlitz (Dec 2, 1805): 12km×8km, hilly_defense. French (4 line + 1 light + 1 Old Guard + 2 cuirassier + 2 artillery) vs Coalition (5 line + 2 hussar + 2 artillery). Historical: ~27% coalition / ~10% French casualties.
- Waterloo (Jun 18, 1815): 6km×4km, hilly_defense. French (4 line + 1 Old Guard + 2 cuirassier + 1 lancer + 3 artillery) vs British (4 line + 1 rifle + 2 hussar + 2 artillery). Historical: ~40% French casualties, cavalry repulsed by squares.
Design Decisions¶
- CBRN fully disabled — Unlike WW1 (which kept CBRN enabled for gas warfare), Napoleonic era has no chemical weapons. All 7 modern modules disabled.
- Volley fire as aggregate model — Like WW1 barrage, not individual engagement. Binomial(n_effective, Phit) where Phit from smoothbore range table. Smoke accumulates per volley, decays with wind.
- Pre-contact morale check — Most Napoleonic charges were decided before contact. Cavalry shock multiplier × formation vulnerability lowers defender threshold. Square is ~immune (vuln 0.1).
- Worst-of-both during formation transition — Transitioning is dangerous. Vulnerability uses the higher (worse) value; speed/firepower uses the lower (worse) value of both formations.
- Courier pool per HQ — Max 4 couriers at a time. Drum/bugle instantaneous but 300m range. Mounted ADC ~33 min for 10km.
- French Old Guard replaces "grenadier company" — Plan said "grenadier company" but Old Guard is the iconic Napoleonic elite unit. More recognizable, same function.
- Faction-specific infantry, generic cavalry/artillery — French and British use different muskets. Cavalry and artillery are generic with scenario differentiation.
Deviations from Plan¶
| Planned | Delivered | Reason |
|---|---|---|
| "grenadier company" unit | french_old_guard |
More historically significant; same role |
| 9 units listed | 10 units delivered | Added french_light_infantry for tactical variety |
| "Frontage and depth matter" in melee | Force ratio model | Frontage/depth adds complexity without proportional fidelity gain at battalion scale |
| ~51 YAML planned | ~53 YAML delivered | Slightly more ammo and signature files |
Exit Criteria Verification¶
| Criterion | Result | Status |
|---|---|---|
| Musket volley 2-5% casualty at 100m | avg ~25 from 500 muskets (5%) | ✅ |
| Cavalry breaks infantry not in square | LINE breaks at lower morale; SQUARE holds | ✅ |
| Square stops cavalry | cavalry_vulnerability = 0.1 | ✅ |
| Square vulnerable to artillery | artillery_vulnerability = 2.0 (max) | ✅ |
| Courier C2 hour-scale delays | 10km MOUNTED_ADC → ~2000s (~33 min) | ✅ |
| Formation changes take minutes | 30-120s depending on from/to | ✅ |
| Deterministic replay | Same seed → identical results (3 tests) | ✅ |
Known Limitations¶
- ScenarioLoader doesn't auto-wire Napoleonic engines — Extends existing era-wiring gap from Phases 16-21. Engines exist standalone, wired via SimulationContext
None-check fields. - Cavalry charge ignores terrain effects — Charge speed is not modified by terrain slope or obstacles. Would need heightmap query integration.
- No frontage/depth in melee — Simplified to force ratio × formation modifier. Historical frontage constraints (e.g., narrow passes) not modeled.
- Foraging ambush casualty rate hardcoded — 10% of foraging party. Should be configurable in ForagingConfig.
- Fallback RNG seed 42 — Volley, melee, cavalry, courier, foraging engines use
np.random.default_rng(42)when no RNG injected. Same pattern as WW1 engines.
Postmortem¶
Scope¶
On target. 233 tests vs planned ~215. All 6 engine extensions delivered. Both validation scenarios delivered. One unit substitution (grenadier → Old Guard) and one addition (french_light_infantry). All exit criteria met.
Quality¶
High. All 6 modules follow Config + Engine + get_state/set_state pattern. Type hints on all public functions. get_logger used throughout. No TODOs/FIXMEs. RNG discipline maintained (DI pattern). One minor style fix applied (foraging.py local math import → module level).
Integration¶
Consistent with era pattern. Engines are standalone with SimulationContext fields + state persistence — same deferred-wiring pattern as WW1/WW2/EW/Space/CBRN. Cross-engine integration tested (formation→volley, formation→melee, cavalry→melee, smoke→volley). No dead modules.
Test Quality¶
- 233 tests across 3 files with good mix of unit/integration
- Statistical tests use 20-100 run samples (sufficient for Binomial)
- Edge cases covered: zero inputs, out-of-range, pool exhaustion, unknown zones
- Deterministic replay verified for volley, melee, foraging
- Backward compatibility verified for modern/ww2/ww1 eras
Deficits¶
5 new items (see Known Limitations above). All are minor and match existing patterns. The ScenarioLoader wiring gap is the only structural deficit and it's shared across Phases 16-22.
Performance¶
- Full regression: 5,659 tests in 108s (comparable to Phase 21)
- Phase 22 tests alone: ~4.4s for 233 tests
- No performance regression
Action Items¶
- Fix foraging.py local math import
- Create phase-22.md devlog ← this file
- Update lockstep docs (CLAUDE.md, devlog/index.md, development-phases-post-mvp.md, README.md, MEMORY.md, project-structure.md)
Lessons Learned¶
- Aggregate models continue to work well for historical eras: Volley fire (Binomial), like WW1 barrage, captures key dynamics without individual-shot simulation.
- Formation as combat multiplier: The rock-paper-scissors of LINE/COLUMN/SQUARE/SKIRMISH vs infantry/cavalry/artillery is the core Napoleonic mechanic. Simple modifier tables produce rich emergent behavior.
- Pre-contact morale is the key cavalry mechanic: Most charges decided before impact. The threshold × shock × vulnerability formula captures this elegantly.
- Worst-of-both during transitions creates realistic risk: Ordering a formation change under fire is dangerous, as it should be.
- Era framework scales well: Adding a 4th era (after Modern, WW2, WW1) required zero changes to the framework — just a new config + data + engine extensions.