Skip to content

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.pyNAPOLEONIC_ERA_CONFIG: disables ew, space, cbrn, gps, thermal_sights, data_links, pgm. VISUAL-only sensors. c2_delay_multiplier=8.0.
  • simulation/scenario.py — 6 new SimulationContext fields: 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

  1. CBRN fully disabled — Unlike WW1 (which kept CBRN enabled for gas warfare), Napoleonic era has no chemical weapons. All 7 modern modules disabled.
  2. 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.
  3. 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).
  4. 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.
  5. Courier pool per HQ — Max 4 couriers at a time. Drum/bugle instantaneous but 300m range. Mounted ADC ~33 min for 10km.
  6. French Old Guard replaces "grenadier company" — Plan said "grenadier company" but Old Guard is the iconic Napoleonic elite unit. More recognizable, same function.
  7. 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

  1. 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.
  2. Cavalry charge ignores terrain effects — Charge speed is not modified by terrain slope or obstacles. Would need heightmap query integration.
  3. No frontage/depth in melee — Simplified to force ratio × formation modifier. Historical frontage constraints (e.g., narrow passes) not modeled.
  4. Foraging ambush casualty rate hardcoded — 10% of foraging party. Should be configurable in ForagingConfig.
  5. 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.