Skip to content

Phase 51: Naval Combat Completeness

Summary

Phase 51 is the third phase of Block 6. Completed naval engagement routing (DEPTH_CHARGE, ASROC, shore bombardment guard, VLS ammo tracking), naval posture system (NavalPosture enum with speed/engagement effects), DEW disable path (threshold-based destroy/disable), and mine/blockade engine wiring.

  • 7 files modified, 1 test file created
  • 37 new tests, 0 regressions (7840 passed, 21 skipped)
  • 6 deficits resolved: D2, D6, D16, Phase 43 shore bombardment, Phase 6 blockade, Phase 6 VLS

What Was Built

51a: Naval Engagement Routing

  • DEPTH_CHARGE routing: _route_naval_engagement() now routes DEPTH_CHARGE category weapons to naval_subsurface_engine.depth_charge_attack(). Damage >= 0.6 → DESTROYED, else DISABLED.
  • ASROC routing: MISSILE_LAUNCHER targeting Domain.SUBMARINE routes to naval_subsurface_engine.asroc_engagement() before the generic MISSILE_LAUNCHER salvo path. Hit with damage >= 0.6 → DESTROYED, else DISABLED.
  • Shore bombardment guard: Added attacker domain check — only Domain.NAVAL or Domain.SUBMARINE attackers route to naval gunfire support. Prevents GROUND attacker with CANNON from erroneously entering the shore bombardment path.
  • VLS ammo tracking: BattleManager._vls_launches: dict[str, int] tracks missiles launched per unit. When weapon.definition.magazine_capacity > 0, checks exhaustion before firing and increments count after. Safe int() conversion handles MagicMock'd weapon definitions in existing tests.

51b: Naval Posture

  • NavalPosture IntEnum: ANCHORED (0), UNDERWAY (1), TRANSIT (2), BATTLE_STATIONS (3). Added to naval.py before NavalUnitType.
  • naval_posture field on NavalUnit with default UNDERWAY. State round-trips via get_state()/set_state().
  • Spawn default: loader.py sets naval_posture=NavalPosture.UNDERWAY for all naval/submarine/amphibious units.
  • Speed multiplier: _NAVAL_POSTURE_SPEED_MULT — ANCHORED=0.0x, UNDERWAY=1.0x, TRANSIT=1.2x, BATTLE_STATIONS=0.9x. Applied in _execute_movement().
  • Engagement gate: ANCHORED units skip offensive engagement (parallel to air posture GROUNDED gate).
  • Auto-assignment: In tick loop, modern/WW1/WW2 era naval units auto-transition to BATTLE_STATIONS when enemies within 2x engagement range, back to UNDERWAY when threat clears. Era-gated: ancient/napoleonic eras excluded (oar-powered ships don't have modern battle stations concept).

51c: DEW Disable Path

  • dew_disable_threshold: float = 0.5 added to CalibrationSchema. Configurable per-scenario.
  • Threshold-based damage: DEW hit with p_hit >= threshold → DESTROYED, below → DISABLED. Replaces unconditional DESTROYED.
  • Wavelength-dependent transmittance: compute_atmospheric_transmittance() now accepts wavelength_nm parameter. Rayleigh+Mie composite scattering correction: shorter wavelengths (532nm green) scatter more than 1064nm (Nd:YAG IR). execute_laser_engagement() reads beam_wavelength_nm from weapon definition.

51d: Mine and Blockade Wiring

  • Mine encounter check: After movement phase, naval/submarine/amphibious units moving (speed >= 0.1) are checked against all armed mines in mine_warfare_engine._mines. Trigger radius per mine type. Damage assessed against destruction/disable thresholds.
  • DisruptionEngine instantiation: Created in ScenarioLoader._create_engines(), attached to SimulationContext.disruption_engine. State persistence in get_state/set_state engine lists.
  • Blockade query: campaign.py._update_supply_network() queries disruption_engine.active_blockades() and logs effectiveness per zone. Replaces pass stub.

Design Decisions

  1. ASROC before generic MISSILE_LAUNCHER: Target domain check (Domain.SUBMARINE) differentiates ASROC from anti-ship missile. Order matters — ASROC block placed before generic MISSILE_LAUNCHER to prevent fallthrough to salvo model.

  2. VLS safe int conversion: getattr(wpn_inst.definition, "magazine_capacity", 0) returns MagicMock when definition is mocked. Added try/except (TypeError, ValueError) conversion to int to maintain backward compat with existing Phase 43 tests.

  3. Era-gated naval posture auto-assignment: Ancient/napoleonic naval units (triremes, galleys, ships of the line) don't have modern battle stations mechanics. Without the era gate, Salamis scenario regressed (Greek ships slowed to 0.9x by BATTLE_STATIONS auto-assignment, shifted outcome to Persian victory).

  4. Blockade query in campaign.py, not supply_network.py: Plan specified modifying supply_network.py. Actual: blockade effectiveness queried and logged in campaign.py._update_supply_network(). Full throughput reduction deferred — requires more complex route cost modification that's better suited to Phase 56.

  5. Mine check O(units x mines) with guard: Only runs when _mines list is non-empty. No current scenarios lay mines, so zero overhead in practice. STRtree spatial indexing deferred to Phase 56.

Deviations from Plan

Planned Actual Reason
Modify supply_network.py for blockade throughput Blockade query in campaign.py only (log, no throughput reduction) Complexity deferral — full throughput reduction needs route cost integration
ANCHORED +50% detection cross-section Not implemented Detection modifier requires detection engine integration; speed + engagement gate sufficient for Phase 51
TRANSIT engagement delay Not implemented Delay mechanic requires cooldown tracking; simplified to speed-only effect
BATTLE_STATIONS +20% detection Not implemented Detection modifier deferred with ANCHORED detection
~36 tests 37 tests On target
Era-gated naval posture auto-assignment Unplanned — required to prevent Salamis regression
Safe magazine_capacity int conversion Unplanned — required for MagicMock compat in Phase 43 tests

Issues & Fixes

  1. MagicMock magazine_capacity: Phase 43 tests mock wpn_inst.definition as MagicMock, causing magazine_capacity > 0 to fail with TypeError. Fixed with try/except int conversion.

  2. Salamis regression: Naval posture auto-assignment set triremes to BATTLE_STATIONS (0.9x speed), slowing Greek advance. Greeks lost. Fixed by gating auto-assignment to modern/WW1/WW2 eras only.

  3. Unit constructor: Test helper initially used display_name= (nonexistent field). Fixed to name=.

Known Limitations

  • Naval posture detection modifiers not implemented (ANCHORED cross-section, TRANSIT reduced emissions, BATTLE_STATIONS active sensors) — deferred to Phase 52 or later
  • TRANSIT engagement delay not implemented — deferred
  • Blockade effectiveness queries logged but not integrated into supply throughput reduction — deferred to Phase 56
  • No scenario exercises magazine_capacity > 0 on naval weapons — VLS tracking is structurally wired but untested end-to-end in scenarios
  • No scenario lays mines programmatically — mine encounter check is structurally wired but untested end-to-end
  • Mine check is O(units x mines) — STRtree spatial indexing deferred to Phase 56

Postmortem

Scope: On target

All 4 substeps delivered. 37 tests (plan: ~36). 6 deficits resolved. Two planned detection modifiers deferred (reasonable — detection integration is more complex than speed/engagement gates).

Quality: High

  • 37 tests across 7 test classes covering all routing paths, posture values, DEW thresholds, mine encounters, and disruption wiring
  • Historical accuracy regression caught and fixed (Salamis era-gating)
  • Safe MagicMock handling prevents fragile test failures
  • No TODOs/FIXMEs in new code

Integration: Fully wired (with one deferral)

  • All new routing paths reachable from main engagement loop
  • NavalPosture used in loader, battle movement, and engagement gate
  • DisruptionEngine instantiated, on context, queried in campaign
  • Mine encounter check runs in movement phase
  • One deferral: supply_network.py blockade throughput integration

Deficits: 3 new items

  1. Naval posture detection modifiers (ANCHORED/TRANSIT/BATTLE_STATIONS) — Phase 52+
  2. Blockade throughput reduction in supply_network.py — Phase 56
  3. No scenarios exercise VLS capacity or mine encounters end-to-end — Phase 55+

Test time: ~1222s (within normal range for historical validation tests)

Lessons Learned

  • Era-gating is essential for posture systems: Modern naval concepts (battle stations speed penalty) don't apply to ancient oar-powered ships. Auto-assignment mechanics must check era before applying.
  • MagicMock auto-creates attributes: getattr(mock, "field", 0) returns a MagicMock, not 0. Safe int conversion needed when existing tests use broad mocks.
  • Blockade wiring is structural first: Instantiating DisruptionEngine and querying blockades (even if only logging) is the right first step. Full throughput integration requires understanding route cost mechanics.