Stochastic Warfare -- Block 6 Development Phases (49--57)¶
Philosophy¶
Block 6 is the final tightening block. No new subsystems, no new UI features. The engine has 25+ instantiated-but-never-called engines, 10 dead YAML data fields, 3 fully implemented subsystems that were never even instantiated, a free-form calibration dict that silently swallows mistyped keys, and config fields that zero scenarios exercise. All were built in Phases 1--48 and tested in isolation, but they contribute nothing to simulation outcomes because they're disconnected.
This block wires every orphaned engine, hardens every schema, exercises every calibration parameter, and validates every scenario against historical outcomes. The exit criterion is zero unresolved deficits and zero dead code paths in the simulation core.
Cross-document alignment: This document must stay synchronized with brainstorm-block6.md (design thinking, deficit inventory, dead engine audit), devlog/index.md (deficit inventory), and specs/project-structure.md (module definitions). Run /cross-doc-audit after any structural change.
Engine changes are wiring, not building: Block 6 modifies battle.py, engine.py, scenario.py, and campaign.py extensively but creates minimal new source files. The work is connecting existing tested systems, not designing new ones.
Phase 49: Calibration Schema Hardening -- COMPLETE¶
Goal: Replace the free-form calibration_overrides: dict[str, Any] with a typed pydantic CalibrationSchema validated at parse time. Clean up dead calibration data. Exercise all untested calibration paths in test scenarios.
Dependencies: Block 5 complete (Phases 40--48).
49a: Define CalibrationSchema Pydantic Model¶
Create a typed calibration schema that replaces the free-form dict.
stochastic_warfare/simulation/calibration.py(new) --CalibrationSchemapydantic model with ~100 known keys organized by subsystem:CombatCalibration:hit_probability_modifier,force_ratio_modifier,fire_on_move_penalty_mult,target_value_weights(dict[str, float] with defaults HQ=2.0, AD=1.8, etc.),blast_radius_to_fill_c(dict[str, float] per munition category)MovementCalibration:advance_speed_mps(replaces deadadvance_speed),dig_in_ticks,formation_spacing_m(per-side prefixed)EngagementCalibration:engagement_range_m,min_engagement_range_m,thermal_contrast,wave_interval_s,target_selection_mode(enum: THREAT_SCORED, NEAREST, RANDOM)VictoryCalibration:victory_weights(morale/casualty/territory),force_destroyed_threshold,morale_collapsed_thresholdMoraleCalibration:cohesion,leadership,suppression,transition_cooldown,rout_cascade_radius_m,rout_friendly_count_threshold,rout_morale_penaltyEnvironmentCalibration:visibility_km,weather_modifier,night_detection_modifierNavalCalibration:torpedo_pk,attacker_pk,defender_pd_pk,engagement_range_nmEraCalibration: era-specific fields gated by era (volley fire params, archery volleys, melee modifiers, etc.)-
All fields have defaults matching current hardcoded values -- zero behavioral change on migration
-
stochastic_warfare/simulation/scenario.py(modified) -- Replacecalibration_overrides: dict[str, Any] | None = Nonewithcalibration: CalibrationSchema = CalibrationSchema(). ScenarioLoader validates at parse time. Invalid keys cause pydanticValidationErrorat load, not silent pass-through.
Tests (~15):
- Schema loads with all defaults -- matches current hardcoded values
- Unknown key in YAML raises ValidationError
- Per-side prefixed fields (e.g., blue_hit_probability_modifier) resolve correctly
- Schema round-trips through YAML dump/load
- Era-specific fields ignored when era doesn't match
49b: Migrate All Scenario YAMLs¶
Migrate all ~37 scenario YAMLs from calibration_overrides: {key: val} to calibration: structured fields.
- All scenario YAML files (modified) -- Replace
calibration_overrides:blocks withcalibration:structured blocks. Automated migration script validates before/after equivalence. - Remove
advance_speedfrom 10 scenario YAMLs -- dead data. Wireadvance_speed_mpsinto movement engine if meaningful, or remove entirely. - Fix calibration audit test (
tests/unit/test_phase48_deficit_fixes.py) -- Replace_EXTERNAL_KEYSstring-list approach with schema-based validation. Remove false-positiveadvance_speedentry.
Tests (~10): - All 37+ scenarios load without ValidationError - Scenario output unchanged before/after migration (deterministic diff) - Previously-dead keys either wired or removed
49c: Exercise Untested Calibration Paths¶
Create test scenarios that exercise every calibration parameter that currently has zero coverage.
data/scenarios/calibration_test/(new) -- Synthetic test scenarios designed to exercise:dig_in_ticks: scenario with defensive units that should dig in after N tickswave_interval_s: scenario with wave attack timingtarget_selection_mode: scenario testing NEAREST vs THREAT_SCORED selectionvictory_weights: scenario with composite victory scoring (morale + casualty weights)morale_configweights: scenario with per-scenario morale tuning (cohesion, leadership, suppression)roe_level: expand to 5+ additional scenarios (COIN, peacekeeping, hybrid gray zone)
Tests (~20): - dig_in_ticks: units transition to DUG_IN after configured ticks - wave_interval_s: engagements occur in waves with configured interval - target_selection_mode NEAREST: units engage closest target regardless of threat - target_selection_mode THREAT_SCORED: units engage highest-threat target - victory_weights: morale-weighted victory differs from casualty-weighted - morale_config: custom cohesion/leadership values change morale transition rates - roe_level: WEAPONS_TIGHT prevents engagement below confidence threshold
Resolves deficits: E1, E2, E3, E4, E5, E6, E7, E10.
Exit Criteria¶
- CalibrationSchema pydantic model validates all scenario YAMLs at parse time
- Zero
dict[str, Any]calibration access in simulation code - All 10
advance_speeddead data entries removed or wired - All 7 previously-untested calibration paths exercised in at least one test scenario
- Morale config weights tuned in at least 3 representative scenarios
- ROE set in 7+ scenarios (up from 2)
- All existing tests pass unchanged
- ~45 new tests
Phase 50: Combat Fidelity Polish -- COMPLETE¶
Goal: Wire posture → movement speed, air posture, continuous concealment, training level data, WW1 barrage penalty fix, configurable target value weights, and melee weapon range verification.
Dependencies: Phase 49 (calibration schema for new config fields).
50a: Posture Affects Movement Speed¶
DUG_IN and FORTIFIED units should not be able to move at full speed.
stochastic_warfare/simulation/battle.py(modified) -- In movement phase, apply posture-based speed multiplier:MOVING: 1.0x (no change)HALTED: 1.0x (can resume)HASTY_DEFENSE: 0.5xDUG_IN: 0.0x (must un-dig to move; transition takes 1 tick)FORTIFIED: 0.0x (permanent position)stochastic_warfare/movement/engine.py(modified) -- Acceptposture_speed_multparameter incompute_movement().
Tests (~8): - DUG_IN unit ordered to move: speed = 0 until posture transitions to MOVING - HASTY_DEFENSE unit moves at 50% speed - MOVING unit unaffected - Transition from DUG_IN → MOVING takes 1 tick delay
Resolves deficit: D1.
50b: Air Unit Posture¶
Add air posture states that affect fuel, detection, and weapons availability.
stochastic_warfare/entities/unit_classes/aerial.py(modified) -- Addair_postureenum:GROUNDED,INGRESSING,ON_STATION,RETURNING. Auto-assign based on unit state (distance to objective, fuel level, engagement status).stochastic_warfare/simulation/battle.py(modified) -- Air posture affects:GROUNDED: cannot engage, not detectable by radar (only ground sensors)INGRESSING: high speed, reduced detection cross-section (aspect angle), limited weapons (no bombs while ingressing)ON_STATION: normal engagement, full weapons availabilityRETURNING: no engagement, fuel-critical, increased detection (defensive flight profile)
Tests (~8): - Aircraft transitions GROUNDED → INGRESSING → ON_STATION → RETURNING based on mission state - GROUNDED aircraft not eligible for air engagement - INGRESSING aircraft has reduced detection cross-section - ON_STATION aircraft can use all assigned weapons - RETURNING aircraft does not engage
Resolves deficit: D3.
50c: Continuous Concealment¶
Replace binary hidden/revealed with a concealment score that degrades with sustained observation.
stochastic_warfare/detection/detection.py(modified) -- Addconcealment_score(0.0--1.0) per target track:- Initial concealment based on terrain type and unit posture (forest=0.9, urban=0.8, open=0.2)
- Each tick of sustained detection reduces concealment by
observation_decay_rate(default 0.05/tick) - Concealment modifies detection SNR:
effective_snr = snr - concealment_dbwhereconcealment_db = concealment_score * max_concealment_db - Engagement authorization requires
concealment_score < engagement_concealment_threshold(default 0.5) stochastic_warfare/detection/detection.py(modified) --DetectionConfiggetsobservation_decay_rate,max_concealment_db,engagement_concealment_thresholdfields.
Tests (~10): - Unit in forest starts at 0.9 concealment - Sustained observation reduces concealment by 0.05/tick - Concealment below threshold allows engagement - Concealment above threshold blocks engagement - Moving unit resets concealment to terrain baseline - Thermal/radar sensors have reduced concealment effect (multiply by 0.3)
Resolves deficit: D4.
50d: Training Level Data Population¶
Add training_level to unit YAML definitions. The engine code already reads it (battle.py:1822).
- All unit YAML files (~60 modified) -- Add
training_levelfield: - Elite units (M1A2, F-22, SAS): 0.9
- Veteran units (M1A1, F-15, Paratroopers): 0.8
- Regular units (Bradley, Riflemen): 0.7
- Conscript/militia (Insurgent, T-55): 0.5
- Historical: Per-era defaults (Roman legionary=0.8, medieval levy=0.4, Napoleonic line=0.6)
Tests (~6): - Unit with training_level=0.9 has higher effective_skill than 0.5 - Default training_level (0.5 if missing) matches current behavior - Historical units have era-appropriate training levels
Resolves deficit: D14.
50e: WW1 Barrage Penalty Fix, Target Weights, Melee Range¶
Fix WW1 barrage accuracy, make target value weights configurable, verify melee weapon ranges.
stochastic_warfare/simulation/battle.py(modified) -- Skip fire-on-move penalty forBARRAGEengagement type. Barrage accuracy is pre-planned fire, not aimed fire.stochastic_warfare/simulation/battle.py(modified) --_score_target()readstarget_value_weightsfromCalibrationSchemainstead of hardcoded dict. Default values match current: HQ=2.0, AD=1.8, ARTILLERY=1.5, ARMOR=1.2, INFANTRY=1.0.data/eras/*/weapons/(modified) -- Verify all melee weapons havemax_range_m: 0or appropriate close-range value. Fix any that would be filtered out by range check.
Tests (~8): - WW1 barrage engagement has no fire-on-move penalty applied - Modern fire-on-move penalty still applies for non-barrage engagements - Custom target_value_weights from calibration change target prioritization - Default target_value_weights match current behavior - All melee weapons pass range filtering for close combat
Resolves deficits: D7, Phase 41 target weights, Phase 43 melee range.
Exit Criteria¶
- Posture affects movement speed (DUG_IN = 0.0x, HASTY = 0.5x)
- Air units have 4 posture states affecting engagement eligibility
- Concealment degrades continuously with observation duration
- All unit YAMLs have training_level set
- WW1 barrage exempt from fire-on-move penalty
- Target value weights configurable via calibration schema
- All melee weapons have correct max_range_m
- ~40 new tests
Phase 51: Naval Combat Completeness -- COMPLETE¶
Goal: Wire existing naval engine methods into battle.py routing, implement naval posture, add DEW disable path, wire MineWarfareEngine and DisruptionEngine (blockade).
Status: Complete. 37 new tests, 0 regressions. 7 files modified + 1 test file created. 6 deficits resolved (D2, D6, D16, Phase 43 shore bombardment, Phase 6 blockade, Phase 6 VLS).
Dependencies: Phase 50 (posture system for naval posture).
51a: Wire Naval Engine Methods into Battle Routing¶
Connect the existing NavalSubsurfaceEngine and NavalSurfaceEngine methods to _route_naval_engagement().
stochastic_warfare/simulation/battle.py(modified) --_route_naval_engagement():- Torpedo engagements →
ctx.naval_subsurface_engine.torpedo_engagement() - Depth charge engagements →
ctx.naval_subsurface_engine.depth_charge_attack() - Anti-ship missile engagements →
ctx.naval_surface_enginesalvo model - Naval gunnery →
ctx.naval_gunnery_engine(WW2+ era) orctx.naval_surface_enginegun method - Shore bombardment → verify platform is NAVAL domain before routing (fix Phase 43 bug)
stochastic_warfare/simulation/scenario.py(modified) -- VerifyNavalSubsurfaceEngine,NavalSurfaceEngine,NavalGunfireSupportEngineare set as named attributes on context (not just stored in a list).
Tests (~12): - Torpedo weapon routes to torpedo_engagement() - Depth charge routes to depth_charge_attack() - Anti-ship missile routes to salvo model - Shore bombardment from naval platform → naval gunfire support - Shore bombardment from land artillery → does NOT route to naval gunfire support - Naval weapon miss does not cascade to direct-fire fallback - Backward compat: non-naval engagement unchanged
51b: Naval Posture¶
Implement naval posture affecting vulnerability, detection, and weapons readiness.
stochastic_warfare/entities/unit_classes/naval.py(modified) -- Addnaval_postureenum:ANCHORED,UNDERWAY,TRANSIT,BATTLE_STATIONS.stochastic_warfare/simulation/battle.py(modified) -- Naval posture effects:ANCHORED: 0.0x speed, +50% detection cross-section, weapons limited to self-defenseUNDERWAY: normal speed, normal detection, normal weaponsTRANSIT: 1.2x speed, -20% detection (reduced emissions), weapons on standby (delay to engage)BATTLE_STATIONS: 0.9x speed, full weapons readiness, +20% detection (all sensors active)- Auto-assign based on unit state: near enemy → BATTLE_STATIONS, distant transit → TRANSIT, in port → ANCHORED.
Tests (~8): - ANCHORED ship has zero movement speed - BATTLE_STATIONS ship has full weapons readiness - TRANSIT ship has engagement delay - Posture auto-transitions based on enemy proximity
51c: DEW Disable Path¶
Add partial damage for DEW engagements instead of always destroying.
stochastic_warfare/simulation/battle.py(modified) -- In DEW engagement resolution:- Below
dew_config.disable_threshold(default 0.5): target → DISABLED (combat-ineffective but not destroyed) - Above threshold: target → DESTROYED (current behavior)
beam_wavelength_nmfrom weapon YAML feeds into Beer-Lambert atmospheric transmittance calculation (currently dead YAML field)
Tests (~6): - DEW engagement below threshold → DISABLED - DEW engagement above threshold → DESTROYED - beam_wavelength_nm affects transmittance calculation - DISABLED unit does not engage but counts for force ratio
Resolves deficit: D16.
51d: MineWarfareEngine and DisruptionEngine Wiring¶
Wire mine engagements and blockade mechanics.
stochastic_warfare/simulation/battle.py(modified) -- Route mine encounters throughMineWarfareEngine: when a naval unit enters a cell with mines, trigger mine warfare resolution.stochastic_warfare/simulation/scenario.py(modified) -- InstantiateDisruptionEngine, attach to context.stochastic_warfare/simulation/campaign.py(modified) -- CallDisruptionEngine.check_blockade()when computing naval supply routes.stochastic_warfare/logistics/supply_network.py(modified) -- When computing route cost for naval supply links, query blockade effectiveness for the zone and reduce throughput.
Tests (~10): - Unit entering mined zone triggers mine engagement - Mine trigger probability depends on unit signature match - Blockade reduces supply throughput for blockaded zone - No blockade → full throughput (backward compat) - DisruptionEngine state persists through get_state/set_state
Exit Criteria¶
- Naval engagements route through specialized engines (torpedo, depth charge, ASM, gunnery)
- Naval units have 4 posture states affecting combat
- DEW has disable/destroy threshold
- Mine encounters trigger mine warfare engine
- Blockade mechanics affect supply network
- beam_wavelength_nm YAML field wired into DEW calculations
- ~36 new tests
Resolves deficits: D2, D6, D16, Phase 43 shore bombardment, Phase 6 blockade, Phase 6 VLS.
Phase 52: Environmental Continuity -- COMPLETE¶
Goal: Replace binary environmental gates with continuous functions. Night gradation, weather → ballistics, terrain-based comms LOS, space/EW SIGINT fusion.
Dependencies: Phase 49 (calibration schema for environmental params).
52a: Night Gradation¶
Replace binary day/night detection modifier with continuous function of solar elevation.
stochastic_warfare/simulation/battle.py(modified) -- Replace binary night gate:# BEFORE: binary if is_night: detection_mod *= 0.5 # AFTER: continuous from AstronomyEngine solar_elevation_deg if solar_elev > 0: mod = 1.0 # day elif solar_elev > -6: mod = 0.8 # civil twilight elif solar_elev > -12: mod = 0.5 # nautical twilight elif solar_elev > -18: mod = 0.3 # astronomical twilight else: mod = 0.2 # full night # Thermal sensors: mod = max(0.8, mod) -- thermal barely affected
Tests (~8): - Solar elevation +10 deg → modifier 1.0 - Solar elevation -3 deg (civil twilight) → modifier 0.8 - Solar elevation -15 deg (astronomical) → modifier 0.3 - Thermal sensor at full night → modifier 0.8 (not 0.2) - Backward compat: scenarios with no astronomy engine unchanged
Resolves deficit: D8.
52b: Weather Effects on Ballistics and Sensors¶
Wire wind drift into ballistic trajectories and precipitation into sensor attenuation.
stochastic_warfare/combat/ballistics.py(modified) -- In RK4 trajectory computation, add cross-wind drift term:dx_wind = wind_speed * sin(wind_dir - heading) * dt. Wind data fromWeatherEngine.get_conditions().stochastic_warfare/detection/detection.py(modified) -- Precipitation attenuation on radar SNR:rain_atten_db = k * R^alpha * range_kmwhere k/alpha from ITU-R P.838 lookup by frequency band (X-band k=0.01, Ka-band k=0.1, per mm/hr).stochastic_warfare/movement/formations.py(modified) -- Sea state → naval formation spacing: higher sea state increases minimum formation spacing.
Tests (~10): - Cross-wind drift deflects trajectory proportional to wind speed - Zero wind → no drift (backward compat) - Heavy rain (10 mm/hr) at X-band → ~0.1 dB/km additional attenuation - Clear weather → no rain attenuation - Sea state 4+ increases naval formation minimum spacing
Resolves deficit: D9.
52c: Terrain-Based Comms LOS¶
Radio communications check terrain LOS between transmitter and receiver.
stochastic_warfare/c2/communications.py(modified) -- When computing comms reliability:- If
los_engineavailable on context, callcheck_los(tx_pos, rx_pos) - If terrain blocks LOS: apply diffraction loss (~6 dB per obstruction for UHF/VHF)
- HF skywave comms: exempt from terrain LOS (ionospheric propagation)
- Satellite comms: exempt from terrain LOS
- Courier/messenger: already has terrain speed from CourierEngine (no change)
stochastic_warfare/c2/communications.py(modified) -- Accept optionallos_engineparameter. Usegetattr(ctx, "los_engine", None)pattern.
Tests (~8): - Radio comms through clear LOS → no attenuation - Radio comms through hill → 6 dB loss - Radio comms through mountain ridge → 12 dB loss (2 obstructions) - HF skywave → no terrain attenuation regardless - SATCOM → no terrain attenuation - Missing los_engine → no terrain check (backward compat)
52d: Space SIGINT + EW SIGINT Fusion¶
Fuse space-based SIGINT detections with ground-based EW SIGINT into unified target tracks.
stochastic_warfare/simulation/engine.py(modified) -- After space engine update and EW engine update, if both SIGINT sources have detections of the same target, fuse into a single track with improved accuracy (weighted average of positions, combined confidence).stochastic_warfare/detection/intel_fusion.py(modified) -- Addfuse_sigint_tracks()method accepting space SIGINT and EW SIGINT detection lists.
Tests (~6): - Two SIGINT detections of same target fuse into one track - Fused track has better position accuracy than either individual - No space engine → no fusion (backward compat) - No EW engine → no fusion (backward compat)
Exit Criteria¶
- Night detection is continuous function of solar elevation (5 levels)
- Wind drift affects ballistic trajectories
- Precipitation attenuates radar detection
- Radio comms check terrain LOS (with diffraction model)
- Space + EW SIGINT fuse when both available
- ~32 new tests
Phase 53: C2 & AI Completeness -- COMPLETE¶
Goal: Wire FogOfWarManager (critical), PlanningProcessEngine, OrderPropagationEngine, StratagemEngine, ATOPlanningEngine. Compute C2 effectiveness from comms state. Wire school_id auto-assignment and SEAD/IADS parameters. Wire escalation sub-engines.
Dependencies: Phase 52 (comms LOS for C2 effectiveness computation).
53a: FogOfWarManager Wiring¶
The most impactful single wiring target in Block 6.
stochastic_warfare/simulation/battle.py(modified) -- Each tick, callctx.fog_of_war.update_detections(side, detected_units)with per-side detection results. Queryctx.fog_of_war.get_known_units(side)when building assessment.stochastic_warfare/simulation/engine.py(modified) -- After detection phase, callfog_of_war.update()to maintain per-side detection pictures.stochastic_warfare/c2/ai/assessment.py(modified) --build_assessment()accepts optionalknown_unitsparameter. When provided, enemy force estimate uses detected units only, not ground truth. Add uncertainty bounds based on last-seen time.- Configuration:
enable_fog_of_war: bool = Falsein scenario config. Disabled by default for backward compat. Enable per scenario.
Tests (~15): - With fog_of_war enabled: commander only sees detected enemies - With fog_of_war disabled: commander sees all enemies (current behavior) - Stale detections (not seen for N ticks) degrade in confidence - Force estimate uncertainty increases with fewer detections - Assessment differs between sides (each sees different picture) - Backward compat: existing scenarios unchanged with fog_of_war=False
Resolves deficit: D12 (per-commander assessment).
53b: C2 Effectiveness Computation¶
Replace hardcoded 1.0 C2 effectiveness with computed value.
stochastic_warfare/c2/communications.py(modified) -- Addcompute_c2_effectiveness(unit_id, hq_id)method: Wherenum_hopsfrom multi-hop C2 (Phase 12),signal_qualityfrom comms reliability,range_factor= 1.0 within range, degraded beyond.stochastic_warfare/simulation/battle.py(modified) -- Replacec2_effectiveness = 1.0withc2_effectiveness = comms_engine.compute_c2_effectiveness(unit_id, hq_id). C2 effectiveness modifies OODA cycle speed and fire coordination.
Tests (~8): - Unit in comms range of HQ → C2 effectiveness ~1.0 - Unit at max range → C2 effectiveness ~0.7 - Unit out of range → C2 effectiveness ~0.3 (degraded autonomous ops) - Multi-hop relay → penalty per hop - No comms engine → 1.0 (backward compat)
53c: StratagemEngine and School Wiring¶
Wire the complete 417-line StratagemEngine and school_id auto-assignment.
stochastic_warfare/simulation/scenario.py(modified) -- InstantiateStratagemEnginein_create_engines(), attach to context.stochastic_warfare/simulation/battle.py(modified) -- In_process_ooda_completions()DECIDE phase:- Call
stratagem_engine.evaluate_opportunities(commander, assessment)to identify applicable stratagems - Weight by
school.get_stratagem_affinity()for commander's doctrinal school - Activate highest-scoring stratagem via
stratagem_engine.activate_stratagem() stochastic_warfare/c2/ai/commander.py(modified) -- In commander initialization, ifschool_idis set, look up school fromSchoolRegistryand assign. Replace deadschool_idfield with live wiring.
Tests (~10): - StratagemEngine instantiated from scenario loader - DECIDE phase evaluates stratagem opportunities - Sun Tzu school prefers DECEPTION stratagem (affinity weight) - Clausewitz school prefers CONCENTRATION stratagem - school_id in YAML auto-assigns school to commander - No school_config → no stratagems evaluated (backward compat)
Resolves deficits: Phase 19 school_id, Phase 19 get_stratagem_affinity, Phase 25 stratagem affinity wiring.
53d: ATOPlanningEngine, OrderPropagation, PlanningProcess¶
Wire the three remaining dead C2 engines.
stochastic_warfare/simulation/scenario.py(modified) -- InstantiateATOPlanningEngine,OrderPropagationEnginein_create_engines().stochastic_warfare/simulation/campaign.py(modified) -- Callato_engine.generate_ato()each campaign tick to assign air units to CAS/interdiction/SEAD sorties based on campaign objectives.stochastic_warfare/simulation/battle.py(modified) -- Callorder_propagation_engine.propagate_orders()after DECIDE phase to push orders through chain of command with delay based on C2 effectiveness.stochastic_warfare/simulation/engine.py(modified) -- Callplanning_engine.update()during campaign tick for MDMP/COA generation.
Tests (~10): - ATO generates air sorties from campaign objectives - Order propagation introduces delay based on hop count - Planning engine produces COA candidates - All three engines have get_state/set_state round-trip - No campaign context → engines not called (backward compat)
53e: SEAD/IADS Parameters and Escalation Sub-Engines¶
Wire the 4 SEAD/IADS parameters and 3 escalation sub-engines.
stochastic_warfare/combat/iads.py(modified) -- Readsead_effectivenessfrom escalation config, apply as suppression modifier on IADS nodes after SEAD strike.iads_degradation_rateas health decay per destroyed node.sead_arm_effectivenessas Pk modifier for ARM missiles.stochastic_warfare/escalation/ladder.py(modified) -- Readdrone_provocation_probfrom config, use in escalation trigger evaluation for unmanned platform encounters.stochastic_warfare/simulation/engine.py(modified) -- Add update() calls for:PoliticalPressureEngine.update()-- evaluate international/domestic pressure effectsUnconventionalWarfareEngine.update()-- process IED encounters, guerrilla actionsUXOEngine.update()-- process unexploded ordnance fields
Tests (~8): - SEAD strike reduces IADS node effectiveness by sead_effectiveness factor - ARM missile Pk modified by sead_arm_effectiveness - Destroyed IADS node degrades sector health by iads_degradation_rate - drone_provocation_prob triggers escalation evaluation - Political pressure accumulates over campaign ticks - UXO fields processed from submunition failures
Resolves deficit: E8 (4 SEAD/IADS params).
Exit Criteria¶
- FogOfWarManager queried per tick; per-side detection pictures maintained
- C2 effectiveness computed from comms state (not hardcoded 1.0)
- StratagemEngine evaluates opportunities during DECIDE phase
- school_id auto-assigns doctrinal school to commanders
- ATO generates air sorties from campaign objectives
- Order propagation introduces chain-of-command delay
- All 4 SEAD/IADS parameters consumed from config
- 3 escalation sub-engines called in engine step loop
- ~51 new tests
Phase 54: Era-Specific & Domain Sub-Engine Wiring -- COMPLETE¶
Goal: Wire the 12 dead era-specific engines into battle/campaign routing. Verify space sub-engine delegation. Create scenarios for dormant config fields. Wire or remove dead YAML data fields. Clean up dead context fields.
Status: Complete. 53 new tests, 0 regressions. 6 files modified + 4 scenario YAMLs + 1 test file created. 7 deficits resolved (12 era engines dead, space sub-engines dead, 0 scenarios with space_config, 0 scenarios with commander_config, traverse_deg/elevation dead YAML, terminal_maneuver dead YAML, dead context fields).
Dependencies: Phase 53 (C2 engines for courier/order propagation, ATO for strategic bombing).
54a: WW2 Era Engines¶
Wire ConvoyEngine and StrategicBombingEngine into campaign loop.
stochastic_warfare/simulation/campaign.py(modified) -- If WW2 era active:- Call
convoy_engine.update()each campaign tick for convoy escort resolution - Call
strategic_bombing_engine.update()for strategic bombing campaign target sets stochastic_warfare/simulation/battle.py(modified) -- Convoy engagements route through ConvoyEngine for wolf-pack / escort interaction.
Tests (~8): - Convoy engine resolves escort effectiveness per campaign tick - Strategic bombing engine processes target sets with CEP - Non-WW2 scenario → engines not called
54b: WW1 Era Engines¶
Wire BarrageEngine, GasWarfareEngine, TrenchSystemEngine into battle routing.
stochastic_warfare/simulation/battle.py(modified) -- If WW1 era active:- Barrage engagement type routes to
barrage_engine(zone-based fire density) instead of IndirectFireEngine - Chemical weapon engagement routes to
gas_warfare_engine(wraps CBRN pipeline) - Trench cover/concealment queries
trench_engine.query_trench_at(position)for units inside trench lines stochastic_warfare/simulation/engine.py(modified) -- Calltrench_engine.update()if present.
Tests (~10): - Barrage engagement uses fire density model (rounds/hectare) - Chemical weapon routes through gas warfare → CBRN pipeline - Unit inside trench line gets trench cover bonus - Unit outside trench line gets no bonus - Non-WW1 scenario → engines not called
54c: Napoleonic Era Engines¶
Wire CavalryEngine, CourierEngine, ForagingEngine into battle/campaign routing.
stochastic_warfare/simulation/battle.py(modified) -- If Napoleonic era active:- Cavalry melee engagements route through
cavalry_enginecharge state machine - Verify CourierEngine wired into order propagation for Napoleonic C2
stochastic_warfare/simulation/campaign.py(modified) -- Callforaging_engine.update()for supply when Napoleonic era active and supply lines cut.
Tests (~8): - Cavalry charge state machine: approach → charge → melee → pursuit - Courier C2 dispatch with terrain-dependent speed - Foraging engine activates when supply lines cut - Non-Napoleonic scenario → engines not called
54d: Ancient/Medieval Era Engines¶
Wire SiegeEngine, AncientFormationEngine, NavalOarEngine, VisualSignalEngine.
stochastic_warfare/simulation/campaign.py(modified) -- If Ancient era active:- Call
siege_engine.update()for daily siege state machine (assault/breach/starve) stochastic_warfare/simulation/battle.py(modified) -- If Ancient era active:- Formation effects from
ancient_formation_enginemodify combat (phalanx, testudo, shield wall) - Naval movement through
naval_oar_enginefor oar-powered speed/endurance stochastic_warfare/c2/communications.py(modified) -- Route Ancient-era C2 throughvisual_signal_enginefor LOS-dependent signal propagation.
Tests (~10): - Siege state machine progresses through daily phases - Phalanx formation grants frontal protection bonus - Oar-powered ships have speed/endurance from naval_oar_engine - Visual signals require LOS between units - Non-Ancient scenario → engines not called
54e: Space Sub-Engine Verification and Scenario Coverage¶
Verify SpaceEngine delegates to sub-engines. Create scenarios for dormant configs.
stochastic_warfare/space/constellations.py(modified) -- VerifySpaceEngine.update()delegates to GPS, ISR, Early Warning, SATCOM, ASAT sub-engines. Add explicit delegation calls if missing.stochastic_warfare/ew/sigint.py(modified) -- VerifySIGINTEngineis called from engine.py EW step. Add explicit call if missing.stochastic_warfare/ew/eccm.py(modified) -- VerifyECCMEngineis called when ECCM counters are active. Add explicit call if missing.- New scenarios -- Create at least 2 scenarios with
space_config(e.g., GPS denial scenario, satellite ISR scenario). Create at least 2 withcommander_config. Addcbrn_configto 2 more scenarios. Addschool_configto 2 more scenarios. - Add public API methods:
CBRNEngine.get_mopp_level(unit_id)replacing private_mopp_levelsaccess.SpaceEngine.get_gps_cep()replacing nestedspace_engine.gps_engineaccess.
Tests (~10): - SpaceEngine.update() calls GPS, ISR, Early Warning sub-engines - SIGINTEngine called from engine.py when ew_config present - ECCMEngine called when ECCM counters active - GPS denial scenario reduces accuracy - Public MOPP level API matches private dict values
54f: Dead YAML Fields and Context Cleanup¶
Wire useful dead YAML fields. Remove unused ones and dead context fields.
- Weapon fields to wire:
traverse_deg,elevation_min_deg,elevation_max_deg→ weapon engagement arc constraint in target selection (can't engage target outside traverse/elevation arc)beam_wavelength_nm→ already wired in 51c (DEW Beer-Lambert)- Ammo fields to wire:
terminal_maneuver→ hit probability modifier for terminal phase maneuvering munitionsseeker_fov_deg→ engagement cone constraint (seeker can't acquire target outside FOV)- Fields to remove or document as data-only:
propulsion,unit_cost_factor,weight_kg,data_link_range→ mark as data-only (used for scenario design reference, not simulation behavior) with docstring annotation- Dead context fields: Remove
SeasonsEngine,ConditionsEngine,ObscurantsEnginedeclarations from SimulationContext if truly unused. If seasons should affect weather, add a TODO for future work.
Tests (~8): - Weapon outside traverse arc cannot engage target at that bearing - Weapon below elevation_min cannot engage airborne target - Terminal maneuver munition has higher hit probability in terminal phase - Seeker FOV constrains engagement cone - Removed context fields don't break any tests
Exit Criteria¶
- All 12 era-specific engines wired into battle/campaign loop with era gating
- Space sub-engines verified to receive delegation from parent
- SIGINT and ECCM engines called from engine.py
- 6+ new scenarios exercise space_config, commander_config, expanded cbrn_config, school_config
- Dead YAML fields either wired (traverse, elevation, terminal_maneuver, seeker_fov) or documented as data-only
- Dead context fields cleaned up
- Fragile private API access replaced with public methods
- ~54 new tests
Phase 55: Resolution & Scenario Migration — COMPLETE (43 tests)¶
Goal: Fix resolution switching time_expired issue, migrate 8 legacy scenarios to campaign format, expand ROE coverage, fix proxy units and data gaps, fix Falklands mechanism.
Dependencies: Phases 50--54 (all combat/environmental/C2 changes must be wired before recalibrating).
55a: Resolution Switching Fix¶
Fix the structural issue where long-range battles resolve via time_expired.
stochastic_warfare/simulation/engine.py(modified) -- Modify resolution switching logic:- Keep tactical resolution while any pair of opposing units is within
2 * max_engagement_rangeAND at least one engagement has occurred in the last N ticks - Allow
force_destroyedandmorale_collapsedvictory evaluation during strategic ticks (not just tactical) - Add
resolution_switching_engagement_range_multconfig (default 2.0) for the range multiplier stochastic_warfare/simulation/battle.py(modified) -- PublishEngagementOccurredEventthat the resolution switcher can track.
Tests (~8): - Units starting >50km apart: tactical resolution maintained while closing - Resolution switches to strategic only after disengagement - force_destroyed evaluates during strategic ticks - Existing short-range scenarios unaffected
Resolves deficits: E9, D15.
55b: Legacy Scenario Migration¶
Migrate 8 pre-Phase-32 scenarios to campaign format.
- 8 scenario YAML files (modified) -- Restructure from flat format to campaign format:
- Add
campaign:wrapper withsides,objectives,victory_conditions - Map existing unit lists to
sides[].forces[].units[] - Add appropriate victory conditions based on scenario description
- Verify all scenarios loadable through
POST /api/runsendpoint.
Tests (~8): - All 8 migrated scenarios load through ScenarioLoader - All 8 migrated scenarios load through API - Migrated scenario output matches pre-migration output (same RNG → same result)
55c: ROE Expansion, Data Gaps, Proxy Units¶
Expand ROE coverage, fix data gaps, replace proxy units.
- ROE expansion -- Add
roe_levelto 5+ additional scenarios: - Korean Peninsula: WEAPONS_TIGHT (ROE escalation scenario)
- Hybrid Gray Zone: WEAPONS_HOLD (initial, escalates)
- Bekaa Valley: WEAPONS_FREE (already combat)
- Falklands campaigns: WEAPONS_TIGHT (maritime ROE)
- COIN campaign: WEAPONS_TIGHT
- Data gaps:
- A-4 Skyhawk: add bomb weapon (Mk 82 500lb) and weapon_assignment
- Eastern Front WW2: add weapon_assignments for all units
- Roman equites: create proper unit definition (not Saracen cavalry proxy)
- Iraqi Republican Guard: create proper unit definition (not insurgent_squad proxy)
- Falklands campaign: Recalibrate so combat engagements drive outcome, not instant morale collapse. Adjust initial morale, engagement ranges, force_destroyed threshold.
- Rout cascade config: Add per-scenario rout configuration to calibration schema for scenarios that need non-default cascade behavior.
Tests (~10): - A-4 Skyhawk can deliver bombs in Falklands scenario - Roman equites has correct ground_type (CAVALRY) - Iraqi Republican Guard has appropriate training_level and equipment - Falklands runs >2 ticks with actual engagements - ROE WEAPONS_TIGHT blocks engagement below confidence threshold
Exit Criteria¶
- Resolution switching allows decisive outcomes for long-range battles
- All 8 legacy scenarios loadable through API
- ROE set in 7+ scenarios
- All proxy units replaced with proper definitions
- Falklands campaign resolves via combat, not instant morale collapse
- ~26 new tests
Phase 56: Performance & Logistics — COMPLETE¶
Goal: Rally spatial indexing, maintenance → readiness wiring, per-era medical/engineering data, Weibull per-subsystem, VLS exhaustion enforcement, naval posture detection, gas casualty calibration, blockade throughput.
Status: COMPLETE — 39 new tests. 8 deficits resolved.
56a: Rally STRtree Spatial Index — COMPLETE¶
battle.py: Replaced O(n^2) rally + rout cascade with STRtree spatial queries. Per-side trees built once at start of_execute_morale. Also fixed indentation bug that only checked last unit's distance.
56b: Maintenance → Readiness Wiring — COMPLETE¶
engine.py:complete_repairs()call + readiness=0 → DISABLED transition.campaign.py: Filled_run_maintenancestub with actual delegation.battle.py: Readiness-based movement speed penalty.
56c: Medical/Engineering Per-Era + Weibull Per-Subsystem — COMPLETE¶
era.py:physics_overridesfor treatment/repair times per era (WW2/WW1/Napoleonic/Ancient).scenario.py: Reads era overrides when constructing MedicalConfig/EngineeringConfig.maintenance.py: Per-subsystem Weibull shapes via prefix categorization.calibration.py:subsystem_weibull_shapes: dict[str, float]field.
56d: VLS Exhaustion Enforcement — COMPLETE¶
battle.py: Exhaustion logging +_vls_launchescheckpoint persistence. Port reload deferred.
56e: Naval Posture Detection Modifiers — COMPLETE¶
battle.py:_NAVAL_POSTURE_DETECT_MULTtable (ANCHORED=1.2x, UNDERWAY=1.0x, TRANSIT=0.85x, BATTLE_STATIONS=1.3x).
56f: Gas Casualty Calibration — COMPLETE¶
calibration.py:gas_casualty_floor,gas_protection_scalingfields.battle.py: Replaced hardcoded values withcal.get()lookups.
56g: Blockade Throughput Reduction — COMPLETE¶
campaign.py: Blockade effectiveness degrades SEA transport route conditions.
Deficits Resolved¶
- D5 (O(n^2) rally), D10 (maintenance→readiness), D11 (medical/eng sparse), D13 (Weibull global), Phase 6 VLS, Phase 51 naval posture detection, Phase 55 gas casualty hardcoded, Phase 51 blockade throughput.
Phase 57: Full Validation & Regression — COMPLETE¶
Goal: Validate ALL scenarios against historical outcomes. Exercise ALL calibration parameters. Zero-deficit audit. Full documentation sync.
Status: COMPLETE — 51 new tests. OPERATIONAL resolution deadlock fixed, checkpoint migrated to JSON, 14 deficits resolved, 34 accepted limitations dispositioned, 1 dormant capability. Zero unresolved deficits.
Dependencies: Phases 49--56 (all changes must be wired).
57a: Full Scenario Evaluation¶
Run every scenario and verify correct outcome.
- All 42+ scenarios -- Run through engine with 10-run MC per scenario:
- Verify correct historical winner at >80% rate for each historical scenario
- Verify modern scenarios reach decisive outcome (not time_expired) at >70% rate
- Record force ratios, engagement counts, victory conditions triggered
- Recalibrate as needed -- Adjust CalibrationSchema parameters where outcomes are wrong. Document calibration rationale in YAML comments.
- Previously-wrong scenarios (6 from Block 5 analysis) -- Run 100-run MC on Agincourt, Salamis, Trafalgar, Midway, Stalingrad, Golan. Confirm >80% correct winner rate.
Tests (~15): - Parametrized test: for each scenario, verify correct winner - Force ratio within historical bounds (±50% of documented ratio) - No scenario resolves via time_expired when decisive combat expected
57b: Calibration Parameter Coverage¶
Verify every calibration parameter is exercised.
tests/unit/test_calibration_coverage.py(new) -- For each field inCalibrationSchema:- At least one scenario sets a non-default value, OR
- At least one test scenario exercises the field
- Dead parameter audit -- If any CalibrationSchema field has zero consumers in Python code, flag as dead and remove.
Tests (~10): - Every CalibrationSchema field consumed by at least one Python code path - Every CalibrationSchema field set by at least one scenario or test
57c: Zero-Deficit Audit¶
Close every open item in devlog/index.md.
- Review every unresolved item -- Mark as:
- RESOLVED (with phase citation)
- WON'T FIX (with rationale)
- No items remain in "unresolved" state -- Everything has a disposition.
- Update deficit count in CLAUDE.md, MEMORY.md, README.md.
57d: OPERATIONAL Resolution Deadlock Fix¶
The Phase 55a closing range guard created a deadlock for forces between 15-30km apart: STRATEGIC switched to OPERATIONAL, but no battles were created, and update_strategic() only ran at STRATEGIC resolution. Forces were frozen.
stochastic_warfare/simulation/engine.py(modified) -- Runupdate_strategic()at OPERATIONAL resolution when no active battles exist, allowing forces to advance to TACTICAL engagement range.- 6 scenario recalibrations: bekaa_valley_1982, gulf_war_ew_1991, falklands_naval, korean_peninsula, 73_easting, taiwan_strait.
57e: Checkpoint Migration (Pickle to JSON)¶
Migrated checkpoint serialization from pickle to JSON for security and portability.
stochastic_warfare/core/checkpoint.py(modified) --NumpyEncoderfor JSON serialization (arrays, integer/float/bool subtypes). Legacy pickle fallback on restore.tests/unit/test_checkpoint.py-- 10 new tests (JSON round-trip, NumpyEncoder, legacy fallback, error handling).
57f: Phase 55 Test Update¶
- Korean Peninsula scenario ROE changed from
WEAPONS_TIGHTtoWEAPONS_FREEin Phase 55 resolution tests.
57g: Documentation Sync and Cross-Doc Audit¶
Synchronize all documentation.
- CLAUDE.md -- Update test counts, phase status, Block 6 summary
- README.md -- Update test badge, architecture summary
- docs/index.md -- Update badges, block status table
- docs/devlog/index.md -- Update all deficit dispositions
- docs/specs/project-structure.md -- Verify module list matches implementation
- mkdocs.yml -- Add Block 6 phase devlog entries
- MEMORY.md -- Update status, lessons learned
- Run
/cross-doc-audit-- All 19 checks must pass
Exit Criteria¶
- All 42+ scenarios produce correct historical winner at >80% MC rate
- All CalibrationSchema fields exercised
- Zero unresolved deficits in devlog/index.md
- Cross-doc audit passes all 19 checks
- OPERATIONAL resolution deadlock fixed
- Checkpoint serialization migrated to JSON
- ~51 new tests
- Block 6 total: ~8,655 tests (8,383 Python + 272 frontend)
Deficit Resolution Map¶
Phase 48 Postmortem Deficits (E1--E10)¶
| ID | Deficit | Resolved In |
|---|---|---|
| E1 | advance_speed dead data |
Phase 49b |
| E2 | dig_in_ticks untested |
Phase 49c |
| E3 | wave_interval_s untested |
Phase 49c |
| E4 | target_selection_mode untested |
Phase 49c |
| E5 | roe_level sparse coverage |
Phase 49c + 55c |
| E6 | Morale config weights unused | Phase 49c |
| E7 | victory_weights untested |
Phase 49c |
| E8 | 4 SEAD/IADS params unwired | Phase 53e (sead_effectiveness + iads_degradation_rate) + Phase 55c (sead_arm_effectiveness + drone_provocation_prob) — fully resolved |
| E9 | Resolution switching time_expired | Phase 55a |
| E10 | Calibration audit false pass | Phase 49b |
Formally Deferred Items (D1--D16)¶
| ID | Deficit | Resolved In |
|---|---|---|
| D1 | Posture → movement speed | Phase 50a |
| D2 | Naval posture undefined | Phase 51b |
| D3 | Air posture undefined | Phase 50b |
| D4 | Binary concealment | Phase 50c |
| D5 | O(n^2) rally cascade | Phase 56a |
| D6 | Phantom naval engines | Phase 51a |
| D7 | WW1 barrage fire-on-move | Phase 50e |
| D8 | Night/day binary | Phase 52a |
| D9 | Weather stops at visibility | Phase 52b |
| D10 | Maintenance registration | Phase 56b |
| D11 | Medical/engineering data sparse | Phase 56c |
| D13 | Weibull global | Phase 56c |
| D14 | Training data disconnected | Phase 50d |
| D15 | time_expired wins | Phase 55a |
| D16 | DEW always destroys | Phase 51c |
Persistent Known Limitations (Earlier Phases)¶
| Deficit | Resolved In |
|---|---|
| Messenger intercept risk | Phase 53 (low priority) |
| Phase 51d (wired), |
|
| Phase 56d (exhaustion enforced, port reload deferred) | |
| Stratagems not proactively planned | Phase 53c (opportunity-evaluated, not COA-planned) |
| Phase 52d (resolved) | |
| Phase 53c (resolved) | |
| Phase 53c (resolved) | |
| Phase 53b (resolved) | |
| Phase 53d (resolved) | |
| Phase 53c (resolved) | |
| Phase 53c (resolved) | |
| Proxy units in scenarios | Phase 55c |
| 8 legacy scenarios can't load API | Phase 55b |
| MineWarfareEngine dead | Phase 51d |
| Phase 53c (resolved) | |
| DisruptionEngine dead | Phase 51d |
| Phase 53d (resolved) | |
| Phase 53a (resolved) | |
| Phase 53d (resolved) | |
| Phase 53d (resolved) | |
| Phase 54e (resolved — verified delegation + get_gps_cep() API) | |
| Phase 54a--d (resolved — all 12 wired with era gating) | |
| Phase 53e (resolved — PoliticalPressureEngine wired) | |
| Phase 54f (resolved — traverse_deg/elevation/terminal_maneuver wired, data-only fields documented) | |
| Phase 54f (resolved — seasons_engine/obscurants_engine annotated TODO, conditions_engine kept) | |
| Phase 54e (resolved — get_gps_cep() public convenience method) | |
| Phase 56e | |
| Phase 55c-3 (resolved) | |
| Phase 55c-4 (resolved) | |
| Phase 55c-1 (resolved) | |
| Phase 55c-2 (resolved) |
Phase Summary¶
| Phase | Focus | Tests | Cumulative | Status |
|---|---|---|---|---|
| 49 | Calibration Schema Hardening | 51 | 8,053 | Complete |
| 50 | Combat Fidelity Polish | 40 | 8,093 | Complete |
| 51 | Naval Combat Completeness | 37 | 8,130 | Complete |
| 52 | Environmental Continuity | 32 | 8,162 | Complete |
| 53 | C2 & AI Completeness | 44 | 8,206 | Complete |
| 54 | Era & Domain Sub-Engine Wiring | 53 | 8,259 | Complete |
| 55 | Resolution & Scenario Migration | 43 | 8,302 | Complete |
| 56 | Performance & Logistics | 39 | 8,341 | Complete |
| 57 | Full Validation & Regression | 51 | 8,383 | Complete |
Block 6 total: 390 new tests across 9 phases. Cumulative: 8,383 Python tests + 272 frontend = 8,655 total.