Skip to content

Phase 54: Era-Specific & Domain Sub-Engine Wiring

Status: COMPLETE Tests: 53 new tests (all passing) Block: 6 (Final Tightening)

Overview

Phase 54 wires 12 era-specific engines into the simulation loop. These engines were instantiated in _create_era_engines() but never called from battle/campaign/engine ticks. Phase 43 wired VolleyFireEngine, ArcheryEngine, MeleeEngine, and FormationNapoleonicEngine — Phase 54 connects the remaining 12.

Changes by Substep

54a: WW2 Era Engines

  • campaign.py: ConvoyEngine.update_convoy() called per strategic tick for registered convoys (WW2 era only)
  • campaign.py: StrategicBombingEngine.apply_target_regeneration() called per strategic tick (WW2 era only)
  • 8 tests

54b: WW1 Era Engines

  • engine.py: BarrageEngine.update(dt, trench_engine) called per tick in _update_environment (WW1 era only)
  • battle.py: Barrage zone suppression check on defender before WW1 engagement routing
  • battle.py: TrenchSystemEngine.movement_factor_at() reduces effective speed inside trenches
  • 9 tests

54c: Napoleonic Era Engines

  • battle.py: CavalryEngine.initiate_charge()/update_charge() for cavalry units in melee range (Napoleonic era only). Conservative string matching: cavalry/hussar/dragoon/lancer/cuirassier
  • engine.py: CourierEngine.update(sim_time) called per tick for pending message delivery
  • campaign.py: ForagingEngine.update_recovery(dt_days) called per strategic tick
  • 8 tests

54d: Ancient/Medieval Era Engines

  • engine.py: AncientFormationEngine.update(dt) called per tick for formation transitions
  • engine.py: NavalOarEngine.update(dt) called per tick for crew fatigue
  • engine.py: VisualSignalEngine.update(dt, sim_time) called per tick for signal delivery
  • campaign.py: SiegeEngine.advance_day()/check_starvation() called per strategic tick for active sieges
  • battle.py: Formation modifiers applied to Ancient engagement routing:
  • archery_vulnerability scales archery casualties
  • melee_power scales attacker effective strength
  • defense_mod scales defender effective strength
  • 10 tests

54e: Space Sub-Engine Verification & Scenario Coverage

  • constellations.py: Added get_gps_cep() public convenience method
  • Scenario YAML: Added space_config to taiwan_strait, korean_peninsula. Added commander_config to korean_peninsula, suwalki_gap
  • Verified SpaceEngine.update() delegates to all 5 sub-engines (GPS, ISR, Early Warning, SATCOM, ASAT)
  • 10 tests

54f: Dead YAML Fields & Context Cleanup

  • battle.py: Weapon traverse arc constraint — weapons with traverse_deg between 0 and 360 exclusive block targets outside arc. traverse_deg=0 (platform-aimed) or traverse_deg=360 (full rotation) = no constraint
  • battle.py: Weapon elevation constraint — only applies for weapons with non-default elevation values (not -5/85 defaults)
  • battle.py: terminal_maneuver=True on ammo gives 5% crew_skill bonus (1.05x)
  • scenario.py: Annotated dead context fields (seasons_engine, obscurants_engine) with TODO comments
  • 8 tests

Key Design Decisions

Wiring Levels

  • Full wiring: AncientFormationEngine (modifiers affect combat outcomes), NavalOarEngine, BarrageEngine, CavalryEngine
  • Structural wiring: ConvoyEngine, StrategicBombingEngine, GasWarfareEngine, SiegeEngine, CourierEngine, ForagingEngine, VisualSignalEngine
  • Already wired: NavalGunneryEngine (Phase 43)

traverse_deg=0 Semantics

Initially caused regressions: aircraft weapons (AIM-120, R-77, Sidewinder, Vulcan M61) all have traverse_deg=0 meaning "platform-aimed, no mount constraint." The check must use 0 < traverse < 360 to only constrain weapons with explicit limited arcs.

Elevation Constraint Safety

Default elevation values (-5 to 85 degrees) are meant for ground-based weapon mounts but are too restrictive for aircraft shooting down at targets. The constraint only fires when non-default values are specified.

terminal_maneuver Modifier

Set at 1.05x (5%) rather than plan's 1.2x (20%). Even 5% compounds across many guided missile engagements; 20% shifted 3 scenario outcomes. The is True guard prevents MagicMock truthiness issues in tests.

Deficits Resolved

Deficit Resolution
12 era-specific engines never called All wired into battle/campaign/engine loop with era gating
Space sub-engine delegation unverified Verified via 5 tests + get_gps_cep() public API
0 scenarios with space_config 2 scenarios (taiwan_strait, korean_peninsula)
0 scenarios with commander_config 2 scenarios (korean_peninsula, suwalki_gap)
traverse_deg/elevation dead YAML fields Wired as weapon engagement arc constraints
terminal_maneuver dead YAML field Wired as 5% Pk modifier
Dead context fields (seasons_engine, obscurants_engine) Annotated with TODO

Files Modified

File Changes
simulation/battle.py Barrage suppression, cavalry charge, formation modifiers, weapon arc constraints, terminal maneuver, trench movement
simulation/engine.py Barrage update, courier update, formation update, oar update, visual signal update
simulation/campaign.py WW2 convoy/bombing, Napoleonic foraging, Ancient siege
simulation/scenario.py Dead context field annotations
space/constellations.py get_gps_cep() public method
4 scenario YAML files space_config, commander_config
tests/unit/test_phase54_era_wiring.py 53 new tests

Lessons Learned

  • traverse_deg=0 means "platform-aimed": Aircraft weapons, VLS launchers, and missiles use 0 to indicate the weapon is aimed by the platform, not by its own mount. Must be excluded from traverse arc filtering.
  • MagicMock comparisons are dangerous: getattr(mock, "field", 360.0) returns a MagicMock, not 360.0. Always use isinstance() guards before numeric comparisons with fields from weapon/ammo definitions.
  • Small Pk modifiers compound: A 5% hit bonus (1.05x) applied to every guided missile engagement has measurable but acceptable impact. 20% (1.2x) shifts 3 scenario outcomes.
  • Elevation constraints must be opt-in: Default elevation values (-5/85) designed for ground mounts break air-to-ground at steep angles. Only constrain weapons with explicitly set (non-default) elevation arcs.
  • Pydantic models reject unknown fields: SpaceConfig and CommanderConfig reject extra YAML keys. Always check model schema before adding scenario config.

Postmortem

1. Delivered vs Planned

Scope: On target with minor descopes.

Planned Item Status Notes
12 era-specific engines wired Delivered All 12 wired (11 with call sites, GasWarfareEngine structural — instantiated only)
Space sub-engine verification Delivered 5 delegation tests + get_gps_cep() public API
2 scenarios with space_config Delivered taiwan_strait, korean_peninsula
2 scenarios with commander_config Delivered korean_peninsula, suwalki_gap
2 scenarios with cbrn_config Partial Only korean_peninsula (1 of 2)
2 scenarios with school_config Partial Only suwalki_gap (1 of 2)
traverse_deg/elevation wired Delivered Weapon arc constraints with MagicMock safety
terminal_maneuver wired Delivered 1.05x crew_skill bonus (reduced from planned 1.2x)
seeker_fov_deg wired Descoped Not wired — engagement cone constraint deferred
Data-only field annotations Descoped propulsion/unit_cost_factor/weight_kg/data_link_range docstrings not added
Dead context fields cleaned Delivered seasons_engine/obscurants_engine annotated with TODO
CBRNEngine public API Already existed get_mopp_level() was already public at line 257
SIGINTEngine verification Already wired Phase 52d already wired SIGINT fusion
ECCMEngine verification Already wired Compute-on-demand, no update() needed
~54 tests Delivered 53 tests (within 2% of target)

Unplanned items: bekaa_valley_1982 commander_config was added then removed after shifting scenario outcomes.

2. Integration Audit

Module/Feature Wired? Notes
ConvoyEngine Yes campaign.py update_convoy() per strategic tick
StrategicBombingEngine Yes campaign.py apply_target_regeneration()
BarrageEngine Yes engine.py update() + battle.py suppression check
GasWarfareEngine No Instantiated in scenario.py but zero call sites in battle/engine/campaign
TrenchSystemEngine Yes battle.py movement_factor_at()
CavalryEngine Yes battle.py initiate_charge/update_charge
CourierEngine Yes engine.py update(sim_time)
ForagingEngine Yes campaign.py update_recovery(dt_days)
AncientFormationEngine Yes engine.py update() + battle.py modifiers
NavalOarEngine Yes engine.py update()
VisualSignalEngine Yes engine.py update(dt, sim_time)
SiegeEngine Yes campaign.py advance_day/check_starvation
get_gps_cep() Yes Public method on SpaceEngine
Weapon traverse arc Yes battle.py with 0 < traverse < 360 guard
Weapon elevation arc Yes battle.py with non-default guard
terminal_maneuver Yes battle.py crew_skill *= 1.05

Red flag: GasWarfareEngine is listed as "structural wiring" in the devlog but has zero call sites. Other "structural" engines (ConvoyEngine, CourierEngine, ForagingEngine, VisualSignalEngine) all have actual update() calls. GasWarfareEngine is effectively unwired.

3. Test Quality Review

  • Integration tests: 27/53 tests call real CampaignManager/SimulationEngine/BattleManager methods with mocked contexts — good integration coverage.
  • Mock-only tests: 13/53 tests verify MagicMock API behavior or pure math (traverse arc geometry, elevation angles). These test the logic but not the wiring.
  • Edge cases covered: None engines (9 tests), exception handling (3 tests), boundary values (traverse=0/360, elevation defaults).
  • Gap: No integration test exercises the full battle loop with a real era-specific engine producing actual combat outcome changes. Tests verify calls happen but not outcomes change.

4. API Surface Check

  • get_gps_cep() has proper type hints and docstring. ✓
  • No unintended public APIs introduced. ✓
  • All engine calls use getattr(ctx, "engine_name", None) pattern consistently. ✓

5. Deficit Discovery

New limitations introduced by Phase 54:

Deficit Severity Assigned To
GasWarfareEngine not wired (instantiated but zero call sites) Medium Phase 55
seeker_fov_deg dead YAML field (planned but not implemented) Low Phase 55
Data-only field annotations not added (propulsion, unit_cost_factor, weight_kg) Low Deferred
cbrn_config only in 1 scenario (planned 2) Low Phase 55
school_config only in 1 scenario (planned 2) Low Phase 55

6. Documentation Freshness

  • CLAUDE.md: Phase 54 row added, test count updated. ✓
  • README.md: Badge updated, phase table row added. ✓
  • devlog/index.md: Phase 54 row added. ✓
  • development-phases-block6.md: Status COMPLETE, deficit map updated. ✓
  • MEMORY.md: Updated with Phase 54 status and lessons. ✓
  • project-structure.md: Status line updated. ✓
  • docs/index.md: Badge and test count updated (was stale at Phase 53). Fixed in postmortem.
  • mkdocs.yml: Phase 54 devlog entry added. Fixed in postmortem.

7. Performance Sanity

53 new tests run in 0.33s — negligible impact. No performance concerns.

8. Summary

  • Scope: On target (minor descopes: seeker_fov_deg, data-only annotations, 2nd cbrn/school scenario)
  • Quality: High — robust MagicMock guards, era gating, exception handling
  • Integration: Mostly wired — GasWarfareEngine is the one gap (instantiated but not called)
  • Deficits: 5 new items (1 medium, 4 low)
  • Action items: docs/index.md and mkdocs.yml fixed during postmortem. Remaining items deferred to Phase 55+.