Skip to content

Phase 66: Unconventional, Naval, & Cleanup

Status: Complete Block: 7 (Final Engine Hardening) Tests: 50 new (8 infra + 19 unconventional + 8 mines + 9 cleanup + 6 structural)

Summary

Phase 66 wired three categories of dormant engines into the simulation loop:

  1. UnconventionalWarfareEngine — IED encounters during ground movement, guerrilla disengage evaluation, human shield Pk reduction. All gated by enable_unconventional_warfare=False.
  2. MineWarfareEngine completion — mine persistence (battery decay per tick), mine sweeping by minesweeper units. Gated by enable_mine_persistence=False.
  3. P2 engine cleanup — siege assault/sally wiring in campaign loop, propulsion drag reduction in ballistics, data link range gate for UAVs, ConditionsEngine facade instantiation.

What Was Built

CalibrationSchema (Step 0)

  • 4 new fields: enable_unconventional_warfare, enable_mine_persistence, guerrilla_disengage_threshold, human_shield_pk_reduction
  • All with safe defaults (flags=False, thresholds matching existing engine defaults)

IED Encounters (Step 66a-1)

  • Inserted in battle.py movement section (after existing mine encounter block)
  • Ground units moving within 2× blast radius trigger detection/detonation cycle
  • EW jamming check for remote IEDs, speed-based detection roll, engineer bonus
  • IED marked inactive after detonation (no double-trigger)
  • Naval units skip IED check (handled by mine warfare)
  • Gated by enable_unconventional_warfare

Guerrilla Routing & Human Shields (Step 66a-2)

  • Human shield: Queries population_engine.get_density_at(), reduces crew_skill via evaluate_human_shield() × human_shield_pk_reduction
  • Guerrilla disengage: Post-engagement pass scanning insurgent/militia/guerrilla units, computing casualty fraction, calling evaluate_guerrilla_disengage()
  • guerrilla_disengage_threshold CalibrationSchema field wired to override engine default

Mine Warfare Completion (Step 66a-3)

  • update_mine_persistence(dt_hours) called in engine.py _update_environment(), converts dt seconds → hours
  • Mine sweeping for minesweeper units (keyword match on unit_type), CONTACT type, 2000m radius
  • Both gated by enable_mine_persistence

P2 Engines & Cleanup (Step 66b)

  • Siege assault/sally: campaign.py siege block enhanced — attempt_assault() during BREACH phase, sally_sortie() each day
  • Propulsion drag: ballistics.py compute_trajectory() applies drag reduction factor before RK4 kernel — rocket=0.3×, turbojet=0.2×, ramjet=0.15×
  • Data link range: battle.py checks data_link_range on attacker; if beyond range of parent unit, skip engagement
  • ConditionsEngine facade: scenario.py instantiates ConditionsEngine(weather, time_of_day, seasons, obscurants, sea_state, acoustics, em), registered as conditions_facade on SimulationContext

Files Modified (6 source)

File Changes
simulation/calibration.py 4 new CalibrationSchema fields
simulation/battle.py IED encounters, data link range gate, human shield Pk modifier, guerrilla disengage evaluation
simulation/engine.py Mine persistence update, mine sweeping for minesweeper units
simulation/campaign.py Siege attempt_assault() and sally_sortie() wiring
combat/ballistics.py Propulsion drag reduction modifier
simulation/scenario.py ConditionsEngine facade instantiation, conditions_facade field on SimulationContext

New Test Files (5)

File Tests Coverage
test_phase_66_infra.py 8 CalibrationSchema field defaults, flag acceptance, backward compat
test_phase_66a_unconventional.py 19 IED (7), guerrilla (7), human shield (3), flag gating (2)
test_phase_66a_mines.py 8 Persistence (3), sweeping (2), laying (2), checkpoint (1)
test_phase_66b_cleanup.py 9 Propulsion (3), data link (2), siege (2), facade (2)
test_phase_66_structural.py 6 Source-level string assertions

Design Decisions

  1. IEDs as terrain hazards: Checked in movement section of battle.py (near mine encounters), not engagement routing. Matches real-world IED mechanics.
  2. Guerrilla disengage as post-engagement pass: Not a separate routing path — standard engagement resolution with guerrilla-specific modifiers applied afterward.
  3. Human shield via crew_skill: Reduces effective Pk through the existing crew_skill modifier rather than adding a new Pk pipeline. Simpler, same effect.
  4. Propulsion drag as pre-RK4 modifier: Applied before _rk4_trajectory_kernel, not inside it. Cleaner separation.
  5. ConditionsEngine alongside EMEnvironment: New conditions_facade field added rather than replacing conditions_engine (which holds EMEnvironment since Phase 61). Migration deferred.

Deviations from Plan

Planned Actual Reason
~30 tests 50 tests Consistent with prior phases — thorough unit + structural coverage
AmphibiousAssaultEngine wiring Deferred (D5) Requires scenario setup (beach coords, craft allocation, tide windows)
P4 dead code removal Deferred (D7) Shadow_azimuth, solar/lunar decomposition, deep_channel_depth — 6-line methods with zero maintenance burden. Safer to allowlist than remove in hardening block.
SimulationContext TODO cleanup Deferred (D10) Low-priority cosmetic cleanup
Scenario YAML minefields Not implemented Deferred — existing obstacles mechanism covers pre-placed IEDs; naval mine emplacement via API

Known Limitations & Deferrals

ID Item Reason
D1 Guerrilla retreat movement Disengage evaluated but unit doesn't physically move away — needs movement system integration
D2 Population center spatial lookup evaluate_human_shield uses population_engine.get_density_at() if available, falls back to 0.0. Full spatial density integration deferred.
D3 IED auto-emplacement by insurgent AI Only pre-placed IEDs (from scenario) and emplace_ied() API. AI-driven emplacement deferred.
D4 Mine sweeping all types Sweeping hardcoded to CONTACT type. Full multi-type sweeping deferred.
D5 AmphibiousAssaultEngine Full beach assault state machine requires scenario setup. Deferred entirely.
D6 ConditionsEngine replacing EMEnvironment conditions_engine field holds EMEnvironment. New conditions_facade added alongside. Migration deferred.
D7 P4 dead code removal shadow_azimuth, solar/lunar decomposition, deep_channel_depth — added to allowlist instead of removed.
D8 Data link range degradation Binary gate (beyond range = no engagement). Gradual C2 degradation deferred.
D9 Propulsion altitude performance cruise_altitude_m on AmmoDefinition not wired to altitude-dependent Pk. Only drag reduction implemented.
D10 SimulationContext TODO cleanup Low-priority cosmetic cleanup. Deferred to Phase 67 docs pass.

Issues & Fixes

  1. EventBus has no subscribe_all: Test initially used bus.subscribe_all(). Fixed to bus.subscribe(IEDDetonationEvent, ...).
  2. DamageEngine requires (event_bus, rng): Test had DamageEngine(rng). Fixed to DamageEngine(bus, rng).
  3. WeaponDefinition uses display_name: Tests used name=. Fixed to display_name=.
  4. AmmoDefinition requires ammo_type: Missing required field in test constructors. Fixed by adding ammo_type="HE".
  5. guerrilla_disengage_threshold unconsumed: CalibrationSchema field was defined but never referenced via cal.get() in battle.py. Fixed by wiring it to override the engine's default threshold.

Lessons Learned

  • Calibration coverage test catches unconsumed fields: The test_all_fields_have_consumers test (from Phase 62) immediately flagged guerrilla_disengage_threshold as unused. Essential regression safety net.
  • Post-engagement passes are clean: Scanning all units after engagement resolution (guerrilla disengage) is architecturally cleaner than mid-resolution checks.
  • Propulsion drag as multiplier is simple and correct: rocket=0.3× means 70% drag reduction. The RK4 kernel doesn't need modification — just pass the adjusted coefficient.
  • ConditionsEngine facade is the right pattern: Rather than breaking the conditions_engine field (EMEnvironment), adding conditions_facade alongside preserves backward compat while enabling unified queries.

Postmortem

1. Delivered vs Planned

Scope: Over-delivered (50 tests vs planned ~30).

All 66a items delivered: IED encounters, guerrilla disengage, human shields, mine persistence, mine sweeping. All 66b items delivered except 3 deferrals: AmphibiousAssaultEngine (D5), P4 dead code removal (D7), SimulationContext TODO cleanup (D10). These are reasonable deferrals — AmphibiousAssaultEngine needs extensive scenario infrastructure, and dead code removal/TODO cleanup are cosmetic in a hardening block.

Unplanned additions: CalibrationSchema guerrilla_disengage_threshold and human_shield_pk_reduction fields for fine-tuning. These were in the implementation plan but not the roadmap.

2. Integration Audit

Component Instantiated Called Gated Verdict
UnconventionalWarfareEngine scenario.py:1747 battle.py (4 sites) enable_unconventional_warfare Fully wired
MineWarfareEngine.update_mine_persistence scenario.py:1082 engine.py:814 enable_mine_persistence Fully wired
MineWarfareEngine.sweep_mines scenario.py:1082 engine.py:829 enable_mine_persistence Fully wired
SiegeEngine.attempt_assault scenario.py (existing) campaign.py:214 SiegePhase.BREACH Fully wired
SiegeEngine.sally_sortie scenario.py (existing) campaign.py:215 Always (per day) Fully wired
Propulsion drag ammunition.py:210 ballistics.py:412 propulsion != "none" Fully wired
Data link range aerial.py:69 battle.py:2640 enable_unconventional_warfare Fully wired
ConditionsEngine facade scenario.py:1319 SimulationContext.conditions_facade Try/except Instantiated

All CalibrationSchema fields have consumers (verified by test_all_fields_have_consumers). No dead code introduced. No orphaned event types.

3. Test Quality Review

  • Unit tests with realistic APIs: Tests call actual engine methods (emplace_ied, detonate_ied, compute_trajectory, etc.) — not mocks of engine internals.
  • Edge cases covered: Zero density (human shield), zero time (mine persistence), max speed (IED detection=0), clamped values (shield_val=1.0).
  • Flag gating verified: Multiple tests confirm enable_unconventional_warfare=False and enable_mine_persistence=False skip the relevant blocks.
  • Structural tests: 6 source-level string assertions catch regressions 100x faster than full scenario runs.
  • No integration tests exercising full battle loop: Tests verify individual engine calls and CalibrationSchema behavior, but don't run a full scenario with unconventional warfare enabled. This is acceptable — Phase 67 will do full scenario evaluation.
  • No @pytest.mark.slow needed: All 50 tests complete in 1.2s.

4. API Surface Check

  • All new CalibrationSchema fields have type hints (pydantic enforced).
  • All battle.py/engine.py/campaign.py insertions use private-prefixed locals (_uw_eng, _ied_id, _mine_eng).
  • No new public functions introduced — all changes are wiring within existing methods.
  • get_logger(__name__) already present in all modified files.
  • Propulsion drag in ballistics.py uses getattr(ammo, "propulsion", "none") — defensive against missing field.

5. Deficit Discovery

10 deferrals documented (D1-D10). Key new deficits: - D1 (Guerrilla retreat movement): Disengage is evaluated but units don't physically relocate. Needs movement integration. - D4 (Mine sweeping all types): Only CONTACT type swept. MAGNETIC/ACOUSTIC need countermeasure-type-specific sweeping. - D5 (AmphibiousAssaultEngine): Entire engine unexercised. Needs beach scenario infrastructure. - D8 (Data link degradation): Binary gate only. Gradual C2 quality loss over distance would be more realistic.

All deferrals are assigned and documented in devlog. None are regressions.

6. Documentation Freshness

  • Phase 66 devlog: Created with this postmortem.
  • CLAUDE.md: Needs Phase 66 entry in Block 7 table.
  • devlog/index.md: Needs Phase 66 row + deferral entries.
  • development-phases-block7.md: Needs Phase 66 status → Complete.
  • README.md: Test count badge needs update.
  • mkdocs.yml: Missing Phase 65 AND Phase 66 devlog nav entries.
  • project-structure.md: Status line needs update.
  • MEMORY.md: Needs Phase 66 current status update.

7. Performance Sanity

  • Phase 66 tests: 50 tests in 1.2s (no heavy tests).
  • Full suite timing: pending (running in background).
  • No performance-critical code introduced — all new blocks are gated by if checks with enable_*=False defaults. Zero cost when disabled.

8. Summary

  • Scope: Over-delivered (50 vs ~30 tests, all core items plus CalibrationSchema tuning params)
  • Quality: High — defensive coding, proper gating, edge case coverage
  • Integration: Fully wired — all 6 modified source files, all CalibrationSchema fields consumed
  • Deficits: 10 documented (D1-D10), all reasonable deferrals
  • Action items: Update lockstep documentation (CLAUDE.md, devlog/index.md, development-phases-block7.md, README.md, mkdocs.yml, project-structure.md, MEMORY.md)