Stochastic Warfare -- Block 5 Development Phases (40--48)¶
Philosophy¶
Block 5 is the core combat fidelity block. No new subsystems, no new UI features. The engine has ~40 built-but-disconnected systems -- posture, suppression, terrain cover, ROE, morale multipliers, aggregate fire models, naval combat engines, detection pipeline, logistics engines, weather/night effects, population engines, and strategic/campaign engines. All were built in Phases 1--30 and tested in isolation, but none feed into the tactical engagement loop in battle.py.
This block wires everything together, hardens the mathematical models, sources all unsourced constants from literature, fixes scenario data errors, and recalibrates all 42 scenarios against historical outcomes. The result is a defensible simulation where every system interacts realistically, not a collection of isolated engines.
Cross-document alignment: This document must stay synchronized with brainstorm-block5.md (design thinking, scenario analysis, mathematical model 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 5 modifies stochastic_warfare/simulation/battle.py extensively but creates minimal new source files. The work is connecting existing systems, not designing new ones.
Phase 40: Battle Loop Foundation¶
Goal: Fix the is_tie victory bug and wire the five lowest-risk disconnected systems into _execute_engagements(): posture, fire-on-move, domain filtering, suppression, and morale multipliers. Instantiate terrain managers as prerequisites for Phase 41.
Status: Complete.
Dependencies: Block 4 complete (Phases 37--39).
40a: Victory Bug Fix¶
Fix the evaluate_force_advantage() is_tie bug that mislabels 15+ scenarios as "Draw".
stochastic_warfare/simulation/victory.py(modified) -- Inevaluate_force_advantage()(line ~600), replace the flawedis_tieinitialization logic:# BEFORE (broken): is_tie = True for side, units in units_by_side.items(): if survival > best_survival: if best_survival >= 0: is_tie = False best_survival = survival best_side = side elif survival == best_survival: is_tie = True # AFTER (fixed): sides_at_best = 0 for side, units in units_by_side.items(): if survival > best_survival: best_survival = survival best_side = side sides_at_best = 1 elif survival == best_survival: sides_at_best += 1 is_tie = sides_at_best != 1
Tests (~8): - Blue 100% vs red 0% → blue wins (not draw), regardless of dict iteration order - Both sides 50% → tie - Three-sided: one dominant → winner, two equal best → tie - Regression: run 73 Easting scenario, verify blue victory (not draw) - Regression: run Bekaa Valley scenario, verify blue victory (not draw)
40b: Wire Posture into Engagements¶
Connect the existing GroundUnit.posture attribute to the engagement engine. Currently _execute_engagements() never passes target_posture, defaulting every unit to MOVING.
stochastic_warfare/simulation/battle.py(modified) -- In_execute_engagements():- Extract target unit's posture (or
UnitStatusequivalent) - Pass as
target_postureparameter to engagement call - Auto-assign posture based on unit state:
MOVINGif unit moved this tick (distance > 0)HALTEDif unit did not move this tickDEFENSIVEif unit is on adefensive_sideslist and halted for >1 tickDUG_INif unit has been defensive longer thandig_in_time_s(from unit config, default 300s)FORTIFIEDreserved for scenario-placed fortifications and obstacle overlays
- Track per-unit
ticks_stationarycounter for dig-in progression
Tests (~10): - Moving unit has posture MOVING, passes to engagement - Halted unit (no movement) has posture HALTED - Defensive unit after threshold time transitions to DUG_IN - DUG_IN target has reduced hit probability (test via Phit output) - FORTIFIED target has maximal protection - Posture resets to MOVING when unit starts moving again - Backward compat: existing engagements still work when posture logic added
40c: Wire Fire-on-Move Penalty¶
Pass the shooter's current speed to the engagement engine. Currently shooter_speed_mps is always 0.0.
stochastic_warfare/simulation/battle.py(modified) -- In_execute_engagements():- Compute shooter's movement speed this tick (distance traveled / tick duration, or unit's current
speedattribute) - Pass as
shooter_speed_mpsto engagement call - The existing formula in
hit_probability.pyalready handles the math:shoot_pen = max(0.3, 1.0 - 0.25 * (shooter_speed_mps / 10.0)) stochastic_warfare/entities/unit_classes/ground.py(modified) -- Add optionalfire_on_move_penalty_multfield to unit config (default 1.0). Per-era/unit-type override:- Modern MBT with stabilization: 0.8 (reduced penalty)
- WW2 tank: 1.0 (standard penalty)
- Musketeer: 3.6 (90% penalty at walking speed ~2.5 m/s →
0.25 * 3.6 * 2.5/10 = 0.225, so ~23% penalty walking) - Artillery: set via separate "must deploy" gate (cannot fire while moving at all)
Tests (~6): - Stationary shooter has no penalty (speed=0 → multiplier=1.0) - Moving shooter at 10 m/s has 25% penalty - Moving shooter at 40 m/s has 70% penalty (capped at 0.3) - Fire-on-move multiplier from unit config applies - Artillery unit cannot fire while moving (100% penalty or gate)
40d: Domain Filtering in Target Selection¶
Prevent engagement mismatches like tank guns targeting fighter aircraft.
stochastic_warfare/simulation/battle.py(modified) -- In target selection within_execute_engagements():- Before selecting a target, check if the attacker has at least one weapon capable of engaging the target's domain
- Create weapon-domain compatibility check:
LANDweapons (tank guns, rifles, ATGMs): can targetLANDonly (exception: some AA-capable weapons)AIRweapons (SAMs, AAA): can targetAIRonly (exception: some can targetLAND)NAVALweapons (torpedoes, anti-ship missiles): can targetNAVAL/SUB- Multi-role weapons flagged in YAML (e.g., Vulcan cannon can be AA or ground)
- Skip incompatible targets; if no compatible targets exist, unit does not engage this tick
stochastic_warfare/combat/weapons.py(modified) -- Addtarget_domains: list[str]field to weapon definition (default: inferred fromWeaponCategory). Data-driven, not hardcoded if/else.- Data YAML (modified) -- Add
target_domainsto weapon definitions where needed (most can be auto-inferred from category)
Tests (~10): - Tank gun (LAND weapon) cannot target AIR unit - SAM (AIR weapon) targets AIR unit correctly - Multi-role weapon (Vulcan) can target both AIR and LAND - Torpedo targets NAVAL/SUB only - Unit with no compatible targets for nearest enemy skips engagement - Regression: existing single-domain scenarios unchanged
40e: Wire Suppression Engine¶
Replace hardcoded suppression_level=0.0 with actual suppression state.
stochastic_warfare/simulation/battle.py(modified) -- In_execute_engagements():- Query
ctx.suppression_engine(if available) for each unit's current suppression level - Pass actual
suppression_levelto engagement instead of hardcoded 0.0 - After each engagement hit, call
suppression_engine.update_suppression()for the target - Suppression decays per tick via
suppression_engine.decay()at start of engagement phase stochastic_warfare/simulation/scenario.py(modified) -- InstantiateSuppressionEngineand attach toSimulationContext(currently not instantiated)
Tests (~6): - Suppressed unit (level=0.8) has reduced accuracy in engagement - Sustained fire on target increases suppression level - Suppression decays when fire is lifted - Zero suppression matches current behavior (backward compat) - Suppression engine instantiated from scenario loader
40f: Wire Morale Multipliers into Engagement¶
Apply the already-computed morale accuracy multipliers during engagement resolution.
stochastic_warfare/simulation/battle.py(modified) -- In_execute_engagements():- Query
ctx.morale_statesfor attacker's morale state - Look up accuracy multiplier: STEADY=1.0, SHAKEN=0.7, BROKEN=0.3, ROUTED=0.1
- Apply as
morale_accuracy_modto hit probability (multiply Phit by morale multiplier) - ROUTED units should not engage at all (already partially handled via status check, but enforce explicitly)
- SURRENDERED units definitely do not engage
Tests (~6): - STEADY attacker fires at full accuracy - SHAKEN attacker has 0.7 multiplier on Phit - BROKEN attacker has 0.3 multiplier - ROUTED attacker does not engage - SURRENDERED attacker does not engage
40g: Terrain Manager Instantiation¶
Instantiate the 4 terrain managers that are prerequisites for Phase 41 terrain-combat wiring.
stochastic_warfare/simulation/scenario.py(modified) -- InScenarioLoader.load(), instantiate:InfrastructureManagerfromterrain/infrastructure.py(buildings, roads, bridges)ObstacleManagerfromterrain/obstacles.py(minefields, wire, fortifications via STRtree)HydrographyManagerfromterrain/hydrography.py(rivers, water bodies)PopulationManagerfromterrain/population.py(settlement density)- Attach to
SimulationContextasctx.infrastructure,ctx.obstacles,ctx.hydrography,ctx.terrain_population - Populate from scenario YAML data where available; empty/default otherwise (backward compat)
Tests (~6): - Scenario loader creates InfrastructureManager (even if empty) - ObstacleManager queryable after load - HydrographyManager queryable after load - Existing scenarios load without error with new managers - Managers attached to SimulationContext
Exit Criteria¶
evaluate_force_advantage()returns correct winner for 73 Easting (blue 100%, red 0% → blue victory, not draw)- Moving units have reduced hit probability vs stationary units
- DUG_IN units have ~40% hit reduction
- Tank guns do not engage fighter aircraft
- Suppressed units fire with reduced accuracy
- SHAKEN units fire with 70% accuracy
- Terrain managers instantiated and queryable
- All existing ~7,833 tests pass unchanged
Phase 41: Combat Depth¶
Goal: Wire terrain data into combat resolution, add force quality/training system, replace closest-enemy targeting with threat-based scoring, and instantiate the detection intelligence pipeline.
Status: Complete.
Dependencies: Phase 40 (posture wiring enables terrain→posture auto-assignment; domain filtering informs target scoring; terrain managers instantiated).
41a: Terrain-Combat Interaction¶
Wire the existing terrain classification system, trenches, buildings, obstacles, and heightmap into _execute_engagements(). The data exists and is queryable per-position -- it just needs to be called during engagement.
stochastic_warfare/simulation/battle.py(modified) -- In_execute_engagements(), before each engagement:- Query
ctx.terrain.classification.properties_at(target_pos)→cover,concealment - Query
ctx.trenches.query_at(target_pos)(if trench overlay exists) →cover_value - Query
ctx.infrastructure.buildings_at(target_pos)(if instantiated) → buildingcover_value - Query
ctx.obstacles.obstacles_at(target_pos)(if instantiated) → fortification cover - Compute elevation delta:
elevation_at(shooter_pos) - elevation_at(target_pos) - Query
ctx.environment.ground_stateat target/attacker positions - Compute composite terrain modifier:
- Pass
terrain_hit_mod * elevation_modas a modifier to engagement Phit - Use
concealment_modto reduce effective detection range for target acquisition stochastic_warfare/simulation/battle.py(modified) -- Soft ground / mud effects:- Query
GroundStateat attacker position - If
SATURATEDorMUD: reduce cavalry/vehicle charge effectiveness by 40-60% - Apply as movement speed cap and charge momentum penalty
- Affects Agincourt (French knights in mud) and similar scenarios
stochastic_warfare/simulation/battle.py(modified) -- Force channeling:- Add optional
max_engagers_per_sideto scenario config - When set, limit the number of units that can simultaneously engage per tick
- Affects Salamis (narrow strait), Thermopylae (pass), bridge crossings
Tests (~20): - Target in forest (cover=0.5) has 50% reduced Phit - Target in urban (cover=0.8) has 80% reduced Phit - Target in open terrain (cover=0.0) has no reduction - Target in trench (cover_value=0.85) has 85% reduction - Building cover_value used when target inside building polygon - Elevation advantage +10% per 33m - Elevation disadvantage (shooting uphill) has penalty - Concealment reduces detection range (target in forest harder to find) - Mud/saturated ground reduces cavalry charge effectiveness - Force channeling limits simultaneous engagers - Cover stacking uses max(), not additive - Terrain query handles missing managers gracefully (no crash)
41b: Force Quality & Training Level¶
Add a training/quality system that modifies engagement effectiveness beyond raw numbers.
stochastic_warfare/entities/base.pyorentities/unit_classes/ground.py(modified) -- Addtraining_level: floatfield to unit definition (default 0.5, range 0.0--1.0):- Elite: 0.9--1.0 (SAS, Israeli armor, Old Guard)
- Veteran: 0.7--0.9 (experienced regulars)
- Regular: 0.5--0.7 (standard forces)
- Green: 0.3--0.5 (militia, raw recruits)
- Untrained: 0.0--0.3 (civilian, mob)
stochastic_warfare/simulation/battle.py(modified) -- Apply quality multiplier in engagement:effective_skill = base_crew_skill * (0.5 + 0.5 * training_level)- Pass
effective_skillascrew_skillto engagement engine - Higher quality → faster reload, better accuracy, faster reaction
stochastic_warfare/simulation/victory.py(modified) -- Quality-weighted force ratios:- Replace raw unit count with
sum(training_level * combat_power)in force advantage calculation - Morale thresholds adjusted by quality (elite units break at higher casualty rates)
- Data YAML (modified) -- Add
training_levelto unit definitions for historical scenarios: - Israeli armor: 0.9, Syrian armor: 0.5 (Golan)
- British navy: 0.9, Franco-Spanish: 0.4 (Trafalgar)
- US forces: 0.85, Iraqi: 0.3 (73 Easting)
Tests (~10): - Elite unit (0.9) has higher effective skill than regular (0.5) - Quality-weighted force ratio: 10 elite ≈ 18 regular in combat power - Training level defaults to 0.5 for existing units (backward compat) - Quality affects victory evaluation (small elite force can beat large green force) - YAML training_level field loads correctly
41c: Threat-Based Target Selection¶
Replace closest-enemy Euclidean distance with weighted threat scoring.
stochastic_warfare/simulation/battle.py(modified) -- Replacenp.argmin(dists)in target selection:- Compute score for each potential target:
where:
threat_factor= target's ability to damage this unit (weapon range, damage potential vs our armor)pk_factor= our probability of hitting at this range (prefer high-Pk shots)value_factor= target's tactical value (commander units, artillery, SAM, carriers)distance_penalty= range normalized by weapon effective range
- Select target with highest score (not closest)
- Configurable via
target_selection_mode: "closest" | "threat_scored"(default: "threat_scored", "closest" for backward compat) stochastic_warfare/simulation/battle.py(modified) -- Commander intent integration:- OODA DECIDE phase can set priority target types per unit
- Priority targets get score bonus (2x multiplier)
Tests (~10): - Threat-scored selection prefers high-threat target over closer low-threat - Anti-tank weapon prioritizes tanks over infantry - AA weapon prioritizes aircraft over ground - Commander intent (priority target type) gets score bonus - "closest" mode preserves existing behavior for backward compat - Multiple units don't all target the same enemy (fire distribution)
41d: Detection Pipeline Wiring¶
Instantiate the 4 disconnected detection engines and wire detection quality into engagement.
stochastic_warfare/simulation/scenario.py(modified) -- Instantiate:IntelFusionEnginefromdetection/intel_fusion.pyDeceptionEnginefromdetection/deception.pySonarEnginefromdetection/sonar.py(naval scenarios)UnderwaterDetectionEnginefromdetection/underwater_detection.py(submarine scenarios)IdentificationEnginefromdetection/identification.py- Attach to
SimulationContext stochastic_warfare/simulation/battle.py(modified) -- Detection quality modulates engagement:- Query detection confidence/SNR for target contact
- Higher SNR → better Pk (precise tracking enables accurate fire)
- Low SNR (marginal detection) → reduced Pk penalty
- Target ID confidence stored for Phase 42 ROE integration
- Formula:
detection_quality_mod = min(1.0, max(0.3, snr_linear / snr_threshold))
Tests (~10): - IntelFusionEngine instantiated from scenario loader - SonarEngine instantiated for naval scenarios - High-SNR detection has no Pk penalty - Low-SNR (marginal) detection has reduced Pk - Undetected targets cannot be engaged - Detection pipeline gracefully handles missing sensors (no crash)
Exit Criteria¶
- Terrain cover reduces hit probability (forest ≈ 50%, urban ≈ 80%)
- Elevation advantage provides +10-30% hit bonus
- Mud/saturated ground penalizes cavalry charges
- Elite units outperform regular units beyond just numbers
- Target selection prioritizes threats over proximity
- Detection quality modulates engagement accuracy
- All existing tests pass unchanged
Phase 42: Tactical Behavior¶
Goal: Wire the ROE engine into the battle loop, implement hold-fire / effective range discipline, improve victory conditions with quality-weighting and morale-based victory, and complete morale-suppression feedback loops.
Status: Complete.
Dependencies: Phase 41 (target scoring informs ROE decisions, force quality feeds victory conditions).
42a: Hold-Fire & ROE Wiring¶
Wire the existing RoeEngine and add effective range discipline.
stochastic_warfare/combat/weapons.py(modified) -- Addeffective_range_mfield to weapon definitions (alongside existingmax_range_m). Default: 80% ofmax_range_m. Scenario/era-specific overrides:- Smoothbore musket: effective 100m, max 200m
- Longbow: effective 150m, max 250m
- Modern rifle: effective 400m, max 800m
- Tank gun: effective 2000m, max 3500m
stochastic_warfare/simulation/battle.py(modified) -- Engagement range discipline:- Defensive units prefer to hold fire until target crosses
effective_range_m - Configurable per doctrine:
hold_fire_until_effective_range: truein behavior rules - Outside effective range: fire with natural Phit degradation (existing dispersion model handles this)
- Inside effective range: fire normally
stochastic_warfare/simulation/scenario.py(modified) -- InstantiateRoeEngineand attach toSimulationContextstochastic_warfare/simulation/battle.py(modified) -- ROE gate before engagement:- Call
roe_engine.check_engagement_authorized(attacker, target, roe_level)before each engagement - Respect WEAPONS_HOLD (self-defense only), WEAPONS_TIGHT (positive ID required), WEAPONS_FREE (fire at will)
- Combine with target ID confidence from Phase 41d: WEAPONS_TIGHT requires identification_confidence > threshold
Tests (~12): - Defensive unit holds fire until target enters effective range - Offensive unit fires at max range (no hold-fire) - WEAPONS_HOLD: unit does not fire unless being fired upon - WEAPONS_TIGHT: unit fires only on positively identified targets - WEAPONS_FREE: unit fires at all detected targets - ROE blocks engagement when unauthorized - Effective range defaults to 80% of max range - Hold-fire behavior configurable per doctrine/side
42b: Victory Condition Improvements¶
Extend the victory evaluator with quality-weighted assessment and morale-based victory.
stochastic_warfare/simulation/victory.py(modified) -- Quality-weighted force advantage:- Replace raw
active_count / total_countwithsum(active_quality) / sum(total_quality)where quality =training_level * combat_power - 10 elite units surviving ≈ 18 regular units in force advantage
stochastic_warfare/simulation/victory.py(modified) -- Morale-based victory:- New victory condition type:
MORALE_COLLAPSE - Triggered when >60% of a side's units are ROUTED or SURRENDERED
- Morale cascade: adjacent units of a routed side get morale penalty
stochastic_warfare/simulation/victory.py(modified) -- Composite time_expired adjudication:- Replace simple "who has more active units" with composite score:
- Configurable weights in scenario YAML (different battles weight differently)
Tests (~10): - Quality-weighted: 10 elite (0.9) beats 15 regular (0.5) in force advantage - Morale collapse: >60% routed triggers defeat - Time_expired: composite score used instead of raw count - Composite weights configurable per scenario - Backward compat: default weights match current behavior for existing scenarios
42c: Morale-Suppression Feedback¶
Complete the morale cascade mechanic with suppression feeding into morale degradation.
stochastic_warfare/simulation/battle.py(modified) -- Suppression feeds morale:- After computing suppression levels (from 40e), pass suppression as a factor to morale engine
- High suppression → faster morale degradation
- This creates the feedback loop: fire → suppression → morale degradation → accuracy loss → rout
stochastic_warfare/morale/state.py(modified) -- Enforce morale effects in combat:- Verify SHAKEN (0.7 accuracy) and BROKEN (0.3) multipliers are applied via battle.py engagement modifier (wired in 40f)
- Add suppression_weight to morale degradation calculation (already exists as config field, verify it's being used with actual suppression values)
stochastic_warfare/simulation/battle.py(modified) -- Morale cascade:- When a unit routs, adjacent units of same side get morale penalty (cascade effect)
- Cascading rout enables morale-based defeat (42b)
Tests (~8): - Sustained suppression degrades morale faster than unsuppressed - Morale cascade: one rout triggers adjacent morale checks - Full feedback loop: fire → suppression → morale break → rout → cascade → defeat - Morale-based victory triggers when cascade reaches 60% threshold
Exit Criteria¶
- ROE engine gates engagements (WEAPONS_HOLD blocks fire except self-defense)
- Defensive units hold fire until effective range
- Victory evaluator uses quality-weighted force ratios
- Morale collapse is a first-class victory condition
- Suppression-morale feedback loop produces realistic rout cascades
- All existing tests pass unchanged
Phase 43: Domain-Specific Resolution¶
Goal: Route engagements to era-appropriate aggregate models (volley fire, massed archery, barrage) and domain-appropriate engines (indirect fire, 5 naval combat engines). Formation-based casualty assessment and simultaneous fire coordination.
Status: Complete.
Dependencies: Phase 42 (hold-fire discipline required for volley coordination; terrain affects aggregate fire).
43a: Era-Aware Engagement Routing¶
Route pre-modern engagements to their aggregate fire models.
stochastic_warfare/simulation/battle.py(modified) -- In_execute_engagements(), checkctx.era:Era.ANCIENT_MEDIEVAL:- Ranged weapons →
ctx.archery_engine.fire_volley()for massed archery - Melee range →
ctx.melee_engine.resolve_melee()
- Ranged weapons →
Era.NAPOLEONIC:- Musket/rifle →
ctx.volley_fire_engine.fire_volley()for volley fire - Cavalry charge →
ctx.cavalry_engine.execute_charge() - Artillery → indirect fire routing (43b)
- Musket/rifle →
Era.WW1:- Rifle →
ctx.volley_fire_engine.fire_volley()(modified for bolt-action rate) - MG → individual engagement (high rate, sustained suppression)
- Artillery →
ctx.barrage_engine.execute_barrage()for creeping barrage
- Rifle →
Era.WW2/Era.MODERN:- Continue using existing
EngagementEngine.execute_engagement()
- Continue using existing
- Aggregate casualty application:
- Aggregate models return total casualties per volley (e.g., 150 muskets × 5% = 7.5 expected casualties)
- Apply as fractional unit damage: each volley reduces target unit's effective strength
- Map fractional damage to individual unit destruction events (probabilistic)
- Simultaneous fire coordination:
- Multiple units of same type targeting same enemy fire in coordinated volley
- Combined Phit computation instead of sequential individual shots
Tests (~15): - Napoleonic musket engagement routes to volley fire engine - Ancient archery routes to archery engine - WW1 artillery routes to barrage engine - Modern engagement routes to standard engagement engine (unchanged) - Aggregate casualties: 100 archers at 100m produce realistic hit count (~12 per volley) - Volley fire: 500 muskets at 100m produce ~25 casualties per volley - Era routing configurable (can override per scenario) - Backward compat: modern scenarios unaffected
43b: Indirect Fire Routing¶
Route artillery and mortar engagements to the indirect fire engine.
stochastic_warfare/simulation/battle.py(modified) -- Weapon category routing:- If
weapon.category in (ARTILLERY, MORTAR): route toctx.indirect_fire_engine - Indirect fire uses CEP-based area effect, not direct-fire Pk
- Minimum engagement range enforced (mortars cannot fire at 10m)
- Fire mission timing: request → compute → flight time → impact (delay, not instant)
- Observer correction: accuracy improves over successive fire missions on same target
stochastic_warfare/simulation/scenario.py(modified) -- InstantiateIndirectFireEngineand attach to context
Tests (~8): - Artillery engagement routes to indirect fire engine - Indirect fire uses CEP (area effect, not point target Pk) - Minimum range enforced - Observer correction improves accuracy - Mortar routing works - Direct-fire weapons still use standard engine
43c: Naval Domain Routing¶
Route naval engagements to the 5 specialized naval engines.
stochastic_warfare/simulation/battle.py(modified) -- Naval engagement type routing:- Surface-to-surface →
NavalSurfaceEngine - Submarine torpedo →
NavalSubsurfaceEngine - WW1/WW2 gun duel →
NavalGunneryEngine(bracket convergence) - Shore bombardment →
NavalGunfireSupportEngine - Mine encounter →
MineWarfareEngine - Determine engagement type from unit domains and weapon categories
stochastic_warfare/simulation/scenario.py(modified) -- Instantiate naval engines for scenarios with naval units:NavalSurfaceEngine,NavalSubsurfaceEngine,NavalGunneryEngine,NavalGunfireSupportEngine,MineWarfareEngine- Attach to
SimulationContext - Only instantiate when scenario has naval units (no overhead for land-only scenarios)
Tests (~12): - Surface naval engagement routes to NavalSurfaceEngine - Torpedo engagement routes to NavalSubsurfaceEngine - WW2 gunnery routes to NavalGunneryEngine (bracket convergence) - Shore bombardment routes correctly - Land-only scenario does not instantiate naval engines - Naval engagement produces domain-appropriate results (bracket convergence, not rifle Pk)
Exit Criteria¶
- Napoleonic battles produce meaningful casualties via volley fire (not 3 total in 7200 ticks)
- Ancient archery volleys from 100 archers produce realistic attrition
- Artillery uses CEP-based area effect, not direct-fire Pk
- Naval battles use bracket convergence and torpedo mechanics, not generic Pk
- Era routing is data-driven and configurable
- All existing tests pass unchanged
Phase 44: Full Subsystem Integration¶
Goal: Wire ALL remaining disconnected subsystem engines into the simulation loop: CBRN, EW, Space/GPS, weather/night, logistics (8 engines), population (5 engines), strategic/campaign (4 engines), and command authority.
Status: Complete. 37 tests. Zero new source files — pure instantiation + query wiring.
Dependencies: Phase 43 (engagement routing framework enables subsystem modifier injection). Phase 41 (detection pipeline required for command picture).
44a: CBRN/EW/Space Integration¶
Wire the three specialized domain engines into the engagement loop.
stochastic_warfare/simulation/battle.py(modified) -- CBRN in engagement:- Query
ctx.cbrn_enginecontamination grid at unit position each tick - Apply chemical/radiation exposure as damage (use existing CBRN casualty model)
- Check MOPP level for protection factor
- Nuclear detonation: apply blast/thermal/radiation damage from
ctx.nuclear_engineoutput to all units in radius stochastic_warfare/simulation/battle.py(modified) -- EW in engagement:- Query
ctx.ew_enginefor J/S ratio at sensor locations - Apply jamming degradation to detection range in target acquisition
- GPS spoofing affects guided weapon Pk:
pk_mod *= gps_accuracy_factor stochastic_warfare/simulation/battle.py(modified) -- Space/GPS in engagement:- Query
ctx.gps_enginefor current DOP at unit location - Apply CEP degradation to guided weapon accuracy
- SATCOM disruption affects C2 order propagation delay
Tests (~15): - Chemical contamination causes casualties in battle loop - MOPP protection reduces chemical casualties - Nuclear blast applies damage to units in radius - EW jamming degrades detection range - GPS denial degrades guided weapon accuracy - SATCOM disruption increases C2 delay
44b: Weather & Night Effects¶
Wire detailed weather and astronomy into engagement resolution.
stochastic_warfare/simulation/battle.py(modified) -- Weather effects on combat:- Query weather engine for current conditions
- Rain: Pk penalty (light=5%, heavy=15%)
- Fog: Pk penalty 30%, detection range halved
- Storm: Pk penalty 40%
- Sea state: naval gunnery dispersion increases with wave height
- Wind vector: pass to ballistic trajectory (already in RK4 model)
stochastic_warfare/simulation/battle.py(modified) -- Night/day effects:- Query
ctx.astronomy_enginefor solar elevation and illumination - Night (sun below horizon): visual detection range reduced 50-80%
- Thermal/NVG sensors unaffected or enhanced at night
- Dawn/dusk: silhouette advantage (backlit targets easier to spot from west at dawn, east at dusk)
Tests (~10): - Rain reduces Pk by configured percentage - Fog halves detection range - Night reduces visual detection (not thermal) - NVG-equipped unit has advantage at night - Sea state increases naval gunnery dispersion - Clear day has no weather penalty
44c: Logistics Engine Wiring¶
Call the 8 logistics engines from the simulation loop.
stochastic_warfare/simulation/engine.py(modified) -- Per-tick logistics calls:MaintenanceEngine.check_breakdowns(): equipment can fail (Poisson), temporarily removing unitsMedicalEngine.process_casualties(): wounded units enter M/M/c evacuation queueEngineeringEngine.update(): obstacle creation/clearance progressTransportEngine.route_supplies(): supply deliveryDisruptionEngine.check_interdiction(): enemy action on supply routesNavalLogisticsEngine.update()/NavalBasingEngine.update(): naval supply opsPrisonerEngine.update(): prisoner handlingstochastic_warfare/simulation/battle.py(modified) -- Supply gates combat capability:- Low ammo (< 20%): reduced fire rate (50%)
- No ammo: cannot fire
- No fuel: cannot move (immobile)
- Low supply: morale penalty
Tests (~15): - Maintenance engine causes equipment breakdown - Broken equipment temporarily unavailable - Medical engine evacuates wounded - Low ammo reduces fire rate - No fuel immobilizes unit - Supply state affects morale
44d: Population & Strategic Engines¶
Wire population/civilian engines and strategic/campaign engines.
stochastic_warfare/simulation/engine.py(modified) -- Population engine calls:CivilianManager.update(): civilian population trackingCollateralEngine.assess(): collateral damage → feeds escalation engineDisplacementEngine.update(): refugee flowCivilianHumintEngine.collect(): civilian intelligenceInfluenceEngine.update(): hearts-and-mindsstochastic_warfare/simulation/campaign.py(modified) -- Strategic engine calls:AirCampaignEngine.update(): air campaign phase managementIadsEngine.update(): integrated air defenseStrategicBombingEngine.execute(): strategic target destructionStrategicTargetingEngine.plan(): deep strike planningstochastic_warfare/simulation/scenario.py(modified) -- InstantiateCommandEngine:- Replace
command_engine=Nonewith actual instantiation - Wire hierarchical authority and command range into OODA timing
- Units outside command range have degraded initiative (slower OODA cycle)
Tests (~15): - Collateral damage feeds escalation level - Civilian displacement occurs in COIN scenarios - Command authority loss degrades subordinate initiative - Strategic bombing engine called in campaign scenarios - IADS engine affects air defense in EW scenarios - Population engines handle empty population gracefully
Exit Criteria¶
- Chemical contamination causes tactical casualties
- Nuclear detonation damages units in blast radius
- EW jamming degrades detection and guided weapon accuracy
- Night combat favors thermal-sensor-equipped units
- Weather affects hit probability and detection range
- Equipment breaks down over campaign duration
- Low supply restricts combat capability
- Collateral damage affects escalation
- Command authority loss degrades unit initiative
- All existing tests pass unchanged
Phase 45: Mathematical Model Audit & Hardening¶
Goal: Replace simplified or physics-incorrect mathematical models with appropriate formulations, validate all unsourced constants against military/scientific literature, and move key tuning parameters to configurable pydantic models.
Status: Complete. 21 new tests (7,565 total Python passing). 11 modified source files + 1 modified test file + 1 new test file.
Dependencies: Phases 40--44 (all systems must be wired before formulas can be hardened -- changing a formula in an unwired system has no observable effect).
Deliverables: AssessmentConfig pydantic model for configurable thresholds, Hopkinson-Cranz overpressure blast model replacing Gaussian, Weibull maintenance distribution option, explosive_fill_kg field on ammo definitions, moderate_condition_floor for hit probability bounds, citation comments on 9 source files, exponential threat cost in pathfinding. Sub-phases completed: 45e (Constant Sourcing & Config), 45d (Hit Probability Review), 45b (Morale Constant Validation), 45c (Maintenance Model Review), 45a (Blast Damage Model).
45a: Blast Damage Model¶
Replace the Gaussian blast damage envelope with physics-based overpressure scaling.
stochastic_warfare/combat/damage.py(modified) -- ReplaceP_kill = exp(-d²/2σ²)with Hopkinson-Cranz overpressure:overpressure_psi = K * (scaled_distance)^(-a)wherescaled_distance = R / W^(1/3)- Regime-dependent exponent: strong shock (a≈2.65), weak shock (a≈1.4)
- Kill/injury/suppression thresholds from overpressure level (same approach as
cbrn/nuclear.py, scaled to conventional weapons) - Posture protection reduces effective overpressure exposure
- Source constants from Glasstone & Dolan or Army TM 60A-1-1-31
Tests (~6): - Overpressure decreases with distance (inverse power law, not Gaussian) - Kill radius matches expected values for common explosive weights - Posture protection reduces effective overpressure - Fragmentation still uses separate 1/r² model
45b: Morale Constant Validation¶
Source morale Markov chain constants from military research literature.
stochastic_warfare/morale/state.py(modified) -- Research and replace arbitrary constants:- Use
/research-militaryskill to find: Dupuy "Attrition" WEI/WUV methodology, S.L.A. Marshall "Men Against Fire", NATO STANAG on combat stress, Rowland "The Stress of Battle" - Replace
casualty_weight=2.0with literature-derived value - Replace
suppression_weight=1.5with literature-derived value - Replace
leadership_weight=0.3,cohesion_weight=0.4with literature-derived values - Add source citations as code comments
- Validate: Monte Carlo of morale transitions should match published combat participation rates (~15-25% of soldiers actively fire, per Marshall)
Tests (~4): - Updated constants produce morale transition rates consistent with literature - High-casualty scenario produces rout within expected timeframe - Morale recovery rates match rehabilitation timelines
45c: Maintenance Model Review¶
Evaluate Weibull distribution as replacement for exponential time-to-failure.
stochastic_warfare/logistics/maintenance.py(modified) -- Consider Weibull:- Weibull shape parameter k: k<1 = infant mortality, k=1 = exponential (current), k>1 = wear-out
- Military equipment typically k=1.2--1.8 (gradual wear-out)
- Source MTBF values from MIL-HDBK-217F reliability prediction
- If Weibull adoption warranted: replace
1 - exp(-dt/MTBF)with1 - exp(-(t/η)^k)where η = scale, k = shape - Make k and η configurable per unit type in YAML
Tests (~4): - Weibull with k=1 matches current exponential behavior - Weibull with k=1.5 produces increasing failure rate over time - MTBF from MIL-HDBK matches expected equipment reliability
45d: Hit Probability Review¶
Review multiplicative modifier chain for theoretical soundness.
stochastic_warfare/combat/hit_probability.py(modified) -- Audit modifier independence:- Current:
Phit = disp × skill × motion × vis × posture × unc × cond - Verify modifiers don't compound to unrealistic values in moderate conditions
- Consider bounding composite modifier:
max(min_phit, min(max_phit, Phit)) - Evaluate if conditional probability formulation is more appropriate for any modifier pair
- Document theoretical basis for each modifier in code comments
Tests (~4): - Moderate conditions don't produce unrealistically low Phit - Extreme conditions produce appropriately low Phit - Min/max bounds prevent degenerate values
45e: Constant Sourcing & Configuration¶
Source the 10 critical unsourced constant groups and make key parameters configurable.
stochastic_warfare/combat/damage.py(modified) -- Source armor effectiveness table from military engineering data. Add citations.stochastic_warfare/combat/damage.py(modified) -- Source posture blast/frag protection from military engineering manuals. Add citations.stochastic_warfare/combat/naval_subsurface.py(modified) -- Source torpedo evasion/CM probabilities from naval warfare literature. Add citations.stochastic_warfare/combat/naval_gunnery.py(modified) -- Source bracket convergence and base Pk from WW2 gunnery records. Add citations.stochastic_warfare/detection/sonar.py(modified) -- Source convergence zone depths from oceanographic data (replace hardcoded 55km/110km). Add citations.stochastic_warfare/c2/ai/assessment.py(modified) -- Move ~30 threshold constants toAssessmentConfigpydantic model. Source weights from FM 5-0/FM 6-0 if possible. Add citations.stochastic_warfare/morale/state.py(modified) -- Move morale weights toMoraleConfig(already partially there). Add source citations.stochastic_warfare/logistics/maintenance.py(modified) -- Source MTBF from MIL-HDBK-217F. Add citations.stochastic_warfare/escalation/ladder.py(modified) -- Source desperation index weights from escalation theory. Add citations.stochastic_warfare/movement/pathfinding.py(modified) -- Replace linear threat cost with exponential:cost = k * exp(α * (1 - d/radius))
Tests (~10): - Each sourced constant produces expected behavior in its domain - Configurable parameters load from YAML overrides - Default values match previous behavior (backward compat where appropriate) - Exponential threat avoidance produces steeper cost near threat center
Exit Criteria¶
- Blast damage uses overpressure physics, not Gaussian game mechanic
- Morale constants sourced from military research with citations
- All 10 critical constant groups have documented sources
- Key tuning parameters movable to YAML configuration
- All existing tests pass (or updated where formula changes produce different results)
Phase 46: Scenario Data Cleanup & Expansion¶
Goal: Fix faction/unit mismatches in scenario YAML files and create missing era/faction-appropriate unit definitions.
Status: Complete. 57 new tests (7,622 total Python passing). 21 new YAML data files + 9 modified scenario YAML + 1 new test file + 2 modified test files. Zero new Python source files.
Dependencies: Phase 45 (model hardening should precede data cleanup so calibration targets are stable).
Deliverables: 6 new unit types (SA-6 Gainful, A-4 Skyhawk, Carthaginian Infantry, Numidian Cavalry, Insurgent Squad, Civilian Noncombatant), 4 new weapons (sa6_3m9, mk12_20mm, ak47, rpg7), 4 new ammo types (3m9_sam, 20mm_mk100, 7_62x39_fmj, pg7_heat), 2 new sensors (1s91_straight_flush, apq94_radar), 5 new signatures (sa6_gainful, a4_skyhawk, carthaginian_infantry, numidian_cavalry, insurgent_squad, civilian_noncombatant). 9 scenarios corrected: Bekaa Valley, Gulf War EW, Falklands San Carlos, Cannae, Eastern Front 1943, COIN Campaign, Hybrid Gray Zone, Srebrenica, Halabja.
46a: Adversary Unit Corrections¶
Fixed 4 scenarios using wrong-faction equipment.
- 46a-1: SA-6 Gainful (5 new files) --
sa6_gainful.yaml(unit),sa6_3m9.yaml(weapon),3m9_sam.yaml(ammo),1s91_straight_flush.yaml(sensor),sa6_gainful.yaml(signature). Replaced US Patriot in Bekaa Valley 1982 and Gulf War EW 1991 scenarios. - 46a-2: A-4 Skyhawk (5 new files) --
a4_skyhawk.yaml(unit),mk12_20mm.yaml(weapon),20mm_mk100.yaml(ammo),apq94_radar.yaml(sensor),a4_skyhawk.yaml(signature). Replaced MiG-29A in Falklands San Carlos scenario. A-4 is attack aircraft (not fighter) -- historically correct for Argentine air raids. - 46a-3: Carthaginian Units (4 new files) --
carthaginian_infantry.yaml+numidian_cavalry.yaml(units),carthaginian_infantry.yaml+numidian_cavalry.yaml(signatures). Reused existing gladius/pilum weapons. Replaced roman_legionary_cohort (on Carthaginian side) and mongol_horse_archer at Cannae. Roman cavalry:norman_knight_conroireplaced withsaracen_cavalry(closer to pre-medieval light cavalry).
46b: Era/Faction Infantry¶
Replaced us_rifle_squad proxies in 5 scenarios plus fixed 1 era tag.
- 46b-1: Eastern Front 1943 (0 new files) -- Changed
era: modern→era: ww2. Replaced US units with existing WW2 units:soviet_rifle_squad+t34_85(blue/Soviet),wehrmacht_rifle_squad+panzer_iv_h+tiger_i(red/German). - 46b-2: Insurgent Squad (6 new files) --
insurgent_squad.yaml(unit),ak47.yaml(weapon),rpg7.yaml(weapon),7_62x39_fmj.yaml(ammo),pg7_heat.yaml(ammo),insurgent_squad.yaml(signature). Replacedus_rifle_squadin COIN Campaign (red), Hybrid Gray Zone (red), Srebrenica (both sides with display_name overrides). Also replacedm3a2_bradleywitht72min Srebrenica (Bosnian Serbs used Yugoslav army equipment). - 46b-3: Civilian Noncombatant (2 new files) --
civilian_noncombatant.yaml(unit, empty equipment list),civilian_noncombatant.yaml(signature). Replacedus_rifle_squadin Halabja 1988 (blue/Kurdish civilians). Red side usesinsurgent_squad+t72m(Iraqi Republican Guard).
Tests (57 in tests/unit/test_phase_46_data.py):
- Schema validation: all new units, weapons, ammo, sensors, signatures load via pydantic
- Scenario load: all 9 modified scenarios load as valid YAML with correct structure
- Unit property: SA-6 range, A-4 speed, insurgent weapons, civilian no-weapons, Carthaginian melee
- Faction validation: no wrong-faction units remain in modified scenarios
- Cross-reference: weapon→ammo refs resolve
- 2 existing tests updated (Cannae cavalry check, civilian empty equipment)
Exit Criteria¶
- No scenario uses wrong-faction equipment (no US Patriot as Soviet SAM) ✓
- No scenario uses
us_rifle_squadas a proxy for non-US infantry ✓ - All scenarios use era-appropriate units ✓
- All new unit YAML passes pydantic validation ✓
- All 7,622 tests pass ✓
Phase 47: Full Recalibration & Validation¶
Goal: Systematically calibrate all 42 scenarios against historical outcomes using the fully wired and hardened engine. Establish Monte Carlo confidence intervals and create regression tests.
Status: Complete.
Dependencies: Phases 40--46 (all engine improvements, model hardening, and data cleanup must be complete).
47a: Historical Scenario Calibration¶
Run each of the 16 historical scenarios through Monte Carlo and calibrate to match documented outcomes.
- For each historical scenario:
- Run N=100 Monte Carlo iterations
- Record: winner distribution, casualty ratios, battle duration, victory condition type
- Compare against documented historical outcome
- Adjust scenario YAML as needed: terrain features, force quality values, engagement rules, reinforcement timing
- Document calibration rationale in scenario YAML comments
- Target outcomes:
- Agincourt: English decisive victory (archers + mud dominate)
- Cannae: Carthaginian decisive (encirclement, Roman rout)
- Salamis: Greek decisive (strait channeling negates Persian numbers)
- Trafalgar: British decisive (gunnery superiority, 0 British ships sunk)
- Austerlitz: French decisive (maneuver, concentrated attack)
- Waterloo: British victory (defensive hold + Prussian arrival)
- Somme: German defensive victory (7:1 casualty ratio)
- Midway: USN decisive (intelligence, carrier vulnerability)
- Stalingrad: Soviet holds (urban defense, reinforcements)
- Golan: Israeli victory (hull-down, gunnery, 4.6:1 exchange ratio)
- 73 Easting: Blue decisive (training superiority)
- And remaining historical scenarios
47b: Contemporary Scenario Validation¶
Verify modern/contemporary scenarios produce plausible results.
- Run each contemporary scenario through Monte Carlo (N=50)
- Verify: no domain mismatch artifacts, reasonable casualty ratios, plausible victory conditions
- Verify: CBRN/EW/Space effects are observable in results
- Verify: escalation scenarios show escalation progression
47c: Regression Test Suite¶
Create a formal regression test for historical accuracy.
tests/validation/test_historical_accuracy.py(new) -- Parametrized test:- For each of 16 historical scenarios: run N=20, verify correct winner with p > 0.8
- Casualty ratio within 2x of historical for key battles
- This test is
@pytest.mark.slow(Monte Carlo)
Tests (~50): - 16 historical scenarios × correct winner (parametrized) - 13 contemporary scenarios × completion without mismatch artifacts - Casualty ratio validation for key battles (Golan, Somme, 73 Easting) - Regression: all scenarios complete without error
Exit Criteria¶
- All 16 historical scenarios produce correct winner in >80% of Monte Carlo runs
- Casualty ratios within 2x of historical for calibrated battles
- All 42 scenarios complete without error
- Regression test codified in
test_historical_accuracy.py - Calibration rationale documented per scenario
File Inventory¶
Phase 40 (~8 modified + ~1 new test file)¶
| Action | File | Sub-phase |
|---|---|---|
| MODIFY | stochastic_warfare/simulation/victory.py -- fix is_tie bug |
40a |
| MODIFY | stochastic_warfare/simulation/battle.py -- posture, fire-on-move, domain filter, suppression, morale |
40b/c/d/e/f |
| MODIFY | stochastic_warfare/entities/unit_classes/ground.py -- fire_on_move_penalty_mult |
40c |
| MODIFY | stochastic_warfare/combat/weapons.py -- target_domains field |
40d |
| MODIFY | stochastic_warfare/simulation/scenario.py -- instantiate suppression engine, terrain managers |
40e/g |
| NEW | tests/unit/test_phase_40_foundation.py |
40a-g |
Phase 41 (~6 modified + ~1 new test file)¶
| Action | File | Sub-phase |
|---|---|---|
| MODIFY | stochastic_warfare/simulation/battle.py -- terrain queries, quality mod, target scoring, detection quality |
41a/b/c/d |
| MODIFY | stochastic_warfare/entities/base.py -- training_level field |
41b |
| MODIFY | stochastic_warfare/simulation/victory.py -- quality-weighted force ratio |
41b |
| MODIFY | stochastic_warfare/simulation/scenario.py -- detection pipeline instantiation |
41d |
| MODIFY | Data YAML files -- training_level per unit | 41b |
| NEW | tests/unit/test_phase_41_depth.py |
41a-d |
Phase 42 (~6 modified + ~1 new test file)¶
| Action | File | Sub-phase |
|---|---|---|
| MODIFY | stochastic_warfare/combat/weapons.py -- effective_range_m field |
42a |
| MODIFY | stochastic_warfare/simulation/battle.py -- ROE gate, hold-fire, morale cascade |
42a/c |
| MODIFY | stochastic_warfare/simulation/scenario.py -- instantiate RoeEngine |
42a |
| MODIFY | stochastic_warfare/simulation/victory.py -- composite victory, morale collapse |
42b |
| MODIFY | stochastic_warfare/morale/state.py -- suppression integration |
42c |
| NEW | tests/unit/test_phase_42_behavior.py |
42a-c |
Phase 43 (~8 modified + ~1 new test file)¶
| Action | File | Sub-phase |
|---|---|---|
| MODIFY | stochastic_warfare/simulation/battle.py -- era routing, indirect fire routing, naval routing |
43a/b/c |
| MODIFY | stochastic_warfare/simulation/scenario.py -- instantiate aggregate/indirect/naval engines |
43a/b/c |
| MODIFY | stochastic_warfare/combat/volley_fire.py -- integration interface |
43a |
| MODIFY | stochastic_warfare/combat/archery.py -- integration interface |
43a |
| MODIFY | stochastic_warfare/combat/indirect_fire.py -- integration interface |
43b |
| MODIFY | stochastic_warfare/combat/naval_surface.py -- integration interface |
43c |
| MODIFY | stochastic_warfare/combat/naval_subsurface.py -- integration interface |
43c |
| NEW | tests/unit/test_phase_43_routing.py |
43a-c |
Phase 44 (~12 modified + ~1 new test file)¶
| Action | File | Sub-phase |
|---|---|---|
| MODIFY | stochastic_warfare/simulation/battle.py -- CBRN/EW/GPS/weather/night in engagement |
44a/b |
| MODIFY | stochastic_warfare/simulation/engine.py -- logistics tick calls, population calls |
44c/d |
| MODIFY | stochastic_warfare/simulation/campaign.py -- strategic engine calls |
44d |
| MODIFY | stochastic_warfare/simulation/scenario.py -- instantiate all remaining engines, CommandEngine |
44a-d |
| MODIFY | stochastic_warfare/logistics/maintenance.py -- integration interface |
44c |
| MODIFY | stochastic_warfare/logistics/medical.py -- integration interface |
44c |
| MODIFY | stochastic_warfare/logistics/engineering.py -- integration interface |
44c |
| MODIFY | stochastic_warfare/population/civilians.py -- integration interface |
44d |
| MODIFY | stochastic_warfare/population/collateral.py -- integration interface |
44d |
| MODIFY | stochastic_warfare/c2/command.py -- integration interface |
44d |
| MODIFY | stochastic_warfare/combat/air_campaign.py -- integration interface |
44d |
| NEW | tests/unit/test_phase_44_integration.py |
44a-d |
Phase 45 (11 modified source + 1 modified test + 1 new test file) -- COMPLETE¶
| Action | File | Sub-phase |
|---|---|---|
| MODIFY | stochastic_warfare/combat/damage.py -- Hopkinson-Cranz blast, explosive_fill_kg |
45a |
| MODIFY | stochastic_warfare/morale/state.py -- sourced constants, citation comments |
45b |
| MODIFY | stochastic_warfare/logistics/maintenance.py -- Weibull option |
45c |
| MODIFY | stochastic_warfare/combat/hit_probability.py -- moderate_condition_floor, modifier audit |
45d |
| MODIFY | stochastic_warfare/combat/naval_subsurface.py -- sourced constants, citations |
45e |
| MODIFY | stochastic_warfare/combat/naval_gunnery.py -- sourced constants, citations |
45e |
| MODIFY | stochastic_warfare/detection/sonar.py -- sourced convergence zones, citations |
45e |
| MODIFY | stochastic_warfare/c2/ai/assessment.py -- AssessmentConfig pydantic model |
45e |
| MODIFY | stochastic_warfare/escalation/ladder.py -- sourced weights, citations |
45e |
| MODIFY | stochastic_warfare/movement/pathfinding.py -- exponential threat cost |
45e |
| NEW | tests/unit/test_phase_45_models.py |
45a-e |
Phase 46 (21 new YAML + 9 modified scenario YAML + 1 new test + 2 modified test) -- COMPLETE¶
| Action | File | Sub-phase |
|---|---|---|
| NEW | data/units/air_defense/sa6_gainful.yaml |
46a |
| NEW | data/weapons/missiles/sa6_3m9.yaml |
46a |
| NEW | data/ammunition/missiles/3m9_sam.yaml |
46a |
| NEW | data/sensors/1s91_straight_flush.yaml |
46a |
| NEW | data/signatures/sa6_gainful.yaml |
46a |
| NEW | data/units/air_fixed_wing/a4_skyhawk.yaml |
46a |
| NEW | data/weapons/guns/mk12_20mm.yaml |
46a |
| NEW | data/ammunition/autocannon/20mm_mk100.yaml |
46a |
| NEW | data/sensors/apq94_radar.yaml |
46a |
| NEW | data/signatures/a4_skyhawk.yaml |
46a |
| NEW | data/eras/ancient_medieval/units/carthaginian_infantry.yaml |
46a |
| NEW | data/eras/ancient_medieval/units/numidian_cavalry.yaml |
46a |
| NEW | data/eras/ancient_medieval/signatures/carthaginian_infantry.yaml |
46a |
| NEW | data/eras/ancient_medieval/signatures/numidian_cavalry.yaml |
46a |
| NEW | data/units/infantry/insurgent_squad.yaml |
46b |
| NEW | data/weapons/rifles/ak47.yaml |
46b |
| NEW | data/weapons/rockets/rpg7.yaml |
46b |
| NEW | data/ammunition/small_arms/7_62x39_fmj.yaml |
46b |
| NEW | data/ammunition/rockets/pg7_heat.yaml |
46b |
| NEW | data/signatures/insurgent_squad.yaml |
46b |
| NEW | data/units/civilian_noncombatant.yaml |
46b |
| NEW | data/signatures/civilian_noncombatant.yaml |
46b |
| MODIFY | data/scenarios/bekaa_valley_1982/scenario.yaml -- SA-6 replaces Patriot |
46a |
| MODIFY | data/scenarios/gulf_war_ew_1991/scenario.yaml -- SA-6 replaces Patriot |
46a |
| MODIFY | data/scenarios/falklands_san_carlos/scenario.yaml -- A-4 replaces MiG-29 |
46a |
| MODIFY | data/eras/ancient_medieval/scenarios/cannae/scenario.yaml -- Carthaginian units |
46a |
| MODIFY | data/scenarios/eastern_front_1943/scenario.yaml -- era: ww2 + WW2 units |
46b |
| MODIFY | data/scenarios/coin_campaign/scenario.yaml -- insurgent_squad |
46b |
| MODIFY | data/scenarios/hybrid_gray_zone/scenario.yaml -- insurgent_squad |
46b |
| MODIFY | data/scenarios/srebrenica_1995/scenario.yaml -- insurgent_squad + t72m |
46b |
| MODIFY | data/scenarios/halabja_1988/scenario.yaml -- civilian_noncombatant + insurgent_squad |
46b |
| NEW | tests/unit/test_phase_46_data.py |
46a-b |
| MODIFY | tests/validation/test_phase_23c_ancient_validation.py -- numidian_cavalry |
46a |
| MODIFY | tests/integration/test_phase2_integration.py -- civilian no-equipment |
46b |
Phase 47 (~42 YAML modified + ~1 new test file)¶
| Action | File | Sub-phase |
|---|---|---|
| MODIFY | All 42 scenario YAML files -- calibration adjustments | 47a/b |
| NEW | tests/validation/test_historical_accuracy.py |
47c |
Test Targets¶
| Phase | Sub-phase | New Tests | Focus |
|---|---|---|---|
| 40a | Victory bug | ~8 | is_tie fix, regression on 73 Easting/Bekaa |
| 40b | Posture | ~10 | Auto-assignment, DUG_IN protection, dig-in progression |
| 40c | Fire-on-move | ~6 | Speed penalty, stabilization multiplier, artillery gate |
| 40d | Domain filter | ~10 | Weapon-domain compat, multi-role, skip incompatible |
| 40e | Suppression | ~6 | Actual levels, accumulation, decay, backward compat |
| 40f | Morale enforcement | ~6 | SHAKEN/BROKEN/ROUTED multipliers applied |
| 40g | Terrain managers | ~6 | Instantiation, queryable, backward compat |
| 41a | Terrain-combat | ~20 | Cover, concealment, elevation, mud, channeling |
| 41b | Force quality | ~10 | Training level, quality-weighted ratios |
| 41c | Target selection | ~10 | Threat scoring, commander intent, fire distribution |
| 41d | Detection pipeline | ~10 | Instantiation, SNR→Pk modulation |
| 42a | Hold-fire / ROE | ~12 | Effective range, WEAPONS_HOLD/TIGHT/FREE |
| 42b | Victory conditions | ~10 | Quality-weighted, morale collapse, composite |
| 42c | Morale-suppression | ~8 | Feedback loop, cascade, morale defeat |
| 43a | Era routing | ~15 | Volley fire, archery, aggregate casualties |
| 43b | Indirect fire | ~8 | CEP, observer correction, min range |
| 43c | Naval routing | ~12 | 5 engine types, domain detection |
| 44a | CBRN/EW/Space | ~15 | Chemical casualties, jamming, GPS CEP |
| 44b | Weather/night | ~10 | Rain/fog/night effects, thermal advantage |
| 44c | Logistics | ~15 | Breakdowns, evacuation, supply gates |
| 44d | Population/strategic | ~15 | Collateral, command authority, air campaign |
| 45a | Blast model | ~6 | Hopkinson-Cranz, posture protection |
| 45b | Morale constants | ~4 | Literature-sourced values, validation |
| 45c | Maintenance model | ~4 | Weibull, MTBF sourcing |
| 45d | Hit probability | ~4 | Modifier audit, bounds |
| 45e | Constant sourcing | ~10 | 10 groups sourced, configurable, citations |
| 46a-b | Data cleanup | ~30 | New units load, scenarios run, era-appropriate |
| 47a-c | Recalibration | ~50 | Historical accuracy, Monte Carlo, regression |
| Total | ~350 |
Implementation Order¶
Phase 40 ─── sequential sub-phases ────────────────────────────────────
40a (is_tie fix) ──> 40b (posture) ──> 40c (fire-on-move)
40d (domain filter, parallel with 40b/c)
40e (suppression, parallel with 40b/c)
40f (morale, after 40e)
40g (terrain managers, parallel)
Phase 41 ─── depends on 40 ────────────────────────────────────────────
41a (terrain-combat, depends on 40b+40g)
41b (force quality, independent)
41c (target selection, depends on 40d+41b)
41d (detection pipeline, independent)
Phase 42 ─── depends on 41 ────────────────────────────────────────────
42a (ROE + hold-fire, depends on 41c)
42b (victory, depends on 41b)
42c (morale-suppression, depends on 40e+40f)
Phase 43 ─── depends on 42 ────────────────────────────────────────────
43a (era routing, depends on 41a+42a)
43b (indirect fire, depends on 41a)
43c (naval routing, independent)
Phase 44 ─── depends on 43 ────────────────────────────────────────────
44a (CBRN/EW/Space, independent)
44b (weather/night, independent)
44c (logistics, independent)
44d (population/strategic/command, depends on 41d)
Phase 45 ─── depends on 44 ────────────────────────────────────────────
45a-e (all sequential — formula changes + sourcing)
Phase 46 ─── depends on 45 ────────────────────────────────────────────
46a-b (data cleanup, can partially parallel with 45)
Phase 47 ─── depends on 46 ────────────────────────────────────────────
47a-c (calibration, strictly last)
Deficit Resolution¶
| Deficit | Origin | Resolved In |
|---|---|---|
is_tie bug in evaluate_force_advantage() |
Block 5 analysis | 40a |
| Posture never passed to engagement engine | Phase 2 | 40b |
shooter_speed_mps always 0.0 |
Phase 4 | 40c |
| No domain filtering in target selection | Phase 2 | 40d |
| Suppression hardcoded to 0.0 | Phase 4 | 40e |
| Morale multipliers not enforced in engagements | Phase 4 | 40f |
| Terrain managers not instantiated | Phase 1 | 40g |
| Terrain cover/concealment not used in combat | Phase 1 | 41a |
| Numerical advantage dominates (no quality) | Phase 2 | 41b |
| Target selection always closest enemy | Phase 9 | 41c |
| Detection pipeline not instantiated | Phase 3 | 41d |
| Target ID confidence never assessed | Phase 3 | 41d |
| ROE engine never called from battle loop | Phase 5 | 42a |
| Engagement range = weapon max range | Phase 4 | 42a |
| Morale effects only used in AI assessment | Phase 4 | 42c |
| Aggregate models never called from battle loop | Phases 21--23 | 43a |
| Indirect fire engine not routed | Phase 4 | 43b |
| Naval engines never called from engagement loop | Phases 4/20 | 43c |
| CBRN effects don't influence tactical combat | Phases 18/25 | 44a |
| EW effects don't influence tactical combat | Phases 16/25 | 44a |
| Space/GPS effects don't influence tactical combat | Phases 17/25 | 44a |
| Weather effects stop at visibility_m | Phase 1 | 44b |
| Night/day cycle has no combat effect | Phase 1 | 44b |
| 7+ logistics engines never called | Phase 6 | 44c |
| Population/civilian engines never called | Phases 12/24 | 44d |
| Strategic/campaign engines never called | Phase 12 | 44d |
| Command authority engine set to None | Phase 5 | 44d |
| Blast damage uses Gaussian, not overpressure | Phase 4 | 45a |
| Morale constants have no published sources | Phase 4 | 45b |
| Maintenance uses exponential, not Weibull | Phase 6 | 45c |
| Armor effectiveness table unsourced | Phase 4 | 45e |
| 10 critical constant groups unsourced | Various | 45e |
| Wrong-faction units in scenarios | Phase 30 | 46a |
us_rifle_squad used as universal proxy |
Phase 30 | 46b |
| Morale collapsed ignores cond.params | Phase 47 | 48a | | Domain mapping incomplete (CANNON, AAA, NAVAL_GUN) | Phase 40 | 48a | | Indirect fire lethal radius hardcoded | Phase 43 | 48a | | Fire-on-move no accuracy penalty | Phase 40 | 48a | | Naval Pk values hardcoded | Phase 43 | 48a | | Rally radius hardcoded 500m | Phase 42 | 48b | | Elevation caps hardcoded | Phase 41 | 48b | | Target value weights hardcoded | Phase 41 | 48b | | No per-scenario ROE in YAML | Phase 42 | 48c | | No scenarios exercise DEW | Phase 28.5 | 48c | | A-4 Skyhawk missing bomb weapon | Phase 46 | 48c | | Falklands campaign 2-tick resolution | Phase 47 | 48c | | Iraqi Republican Guard as insurgent proxy | Phase 46 | 48c | | Saracen cavalry as Roman equites proxy | Phase 46 | 48c |
Total: ~47 deficits targeted for resolution across Block 5.
Phase 48: Block 5 Deficit Resolution¶
Goal: Resolve 14 tractable deficits, formally defer 16 as accepted limitations. Zero new features — only bug fixes, configurable constants, data gap fills, and deficit inventory cleanup.
Status: Complete.
Dependencies: Phase 47 complete.
48a: Engine Code Fixes (5 items)¶
- A1:
_check_morale_collapsed()readscond.params.threshold(matching_check_force_destroyedpattern) - A6: Domain mappings corrected — CANNON→{GROUND, AERIAL}, AAA→{AERIAL, GROUND}, NAVAL_GUN→{GROUND, NAVAL, AERIAL}
- A4:
_apply_indirect_fire_result()acceptslethal_radius_mandcasualty_per_hitparameters, call site passesammo_def.blast_radius_m - A5: Fire-on-move accuracy penalty — up to 50% crew_skill degradation proportional to speed fraction
- A3:
NavalEngagementConfig(BaseModel)replaces 6 hardcoded values, embedded inBattleConfig.naval_config
48b: Configurable Constants (3 items)¶
- B1: Rally radius uses
RoutConfig.cascade_radius_minstead of hardcoded 500m - B2: Elevation caps configurable via
BattleConfig.elevation_advantage_cap/elevation_disadvantage_floor - A7: Target value weights configurable via
BattleConfig.target_value_*fields
48c: Scenario Data Fixes (6 items)¶
- C3: A-4 Skyhawk gets bomb rack weapon (new
bomb_rack_generic.yaml+ equipment entry) - C1: Per-scenario ROE added to Srebrenica (blue) and Hybrid Gray Zone (both sides)
- C2: DEW config added to Taiwan Strait scenario
- C8: New
roman_equitesunit replaces anachronisticsaracen_cavalryin Cannae - C7: New
iraqi_republican_guardunit replacesinsurgent_squadproxy in Halabja - C5: Falklands campaign calibration moderated (morale_degrade_rate 3.0→1.5, red_cohesion 0.1→0.4)
48d: Calibration Parameter Wiring (6 items — unplanned)¶
Root cause: calibration_overrides is free-form dict[str, Any] — no schema validation means unused keys pass silently. Keys added in data phases were never consumed by wiring phases.
- force_ratio_modifier: Per-side
{side}_force_ratio_modifierwired intocrew_skill(Dupuy CEV). Cascades to aggregate paths and naval routing. - Per-side hit_probability_modifier:
hit_probability_modifier_{side_name}pattern with fallback to global. - jammer_coverage_mult: Scales EW SNR penalty in engagement loop.
- stealth_detection_penalty: Degrades detection_quality_mod for low-RCS targets.
- sigint_detection_bonus: Boosts ESM/SIGINT sensor detection quality.
- sam_suppression_modifier: Degrades SAM/AAA crew_skill (simulates SEAD suppression).
48e: Victory Condition Enhancements (unplanned)¶
- target_side:
force_destroyedacceptsparams.target_sideto restrict checking to one side. - count_disabled: DISABLED units count as out-of-action when
target_sideis set (opt-in, backward compat). - Trafalgar fixed: Changed from
time_expired → britishtoforce_destroyed → target_side: franco_spanish.
48f: Scenario Recalibrations (3 items — unplanned)¶
- Normandy:
german_force_ratio_modifier2.0 → 1.3 - Stalingrad:
german_force_ratio_modifier2.0 → 1.3,soviet_force_ratio_modifier1.0 → 1.2 - Trafalgar: Added
british_force_ratio_modifier: 2.5,franco_spanish_force_ratio_modifier: 0.6
48g: Calibration Key Audit Test¶
TestCalibrationKeyAudit validates every calibration_overrides key across all scenario YAMLs is consumed or explicitly deferred. Categorized key sets prevent future drift.
48h: Deferred Deficits (16 items)¶
Formally documented as accepted limitations: D1-D16 (posture-movement, naval/air posture, binary concealment, O(n^2) rally, phantom naval engines, WW1 barrage, binary night, weather Pk, maintenance registration, medical/engineering data, per-commander assessment, global Weibull, training YAML, time_expired wins, DEW destroy-only, DEW AD routing).
File Inventory¶
| File | Change |
|---|---|
stochastic_warfare/simulation/victory.py |
A1: morale threshold from params; 48e: target_side + count_disabled |
stochastic_warfare/simulation/battle.py |
A3-A5, A7, B1-B2: NavalEngagementConfig, indirect fire params, fire-on-move, target values, rally, elevation; 48d: force_ratio_modifier, EW params, per-side hit_prob |
stochastic_warfare/combat/ammunition.py |
A6: domain mappings |
data/weapons/bombs/bomb_rack_generic.yaml |
New: bomb delivery weapon |
data/eras/ancient_medieval/units/roman_equites.yaml |
New: Roman cavalry unit |
data/eras/ancient_medieval/signatures/roman_equites.yaml |
New: Roman cavalry signature |
data/units/infantry/iraqi_republican_guard.yaml |
New: Iraqi RG infantry unit |
data/signatures/iraqi_republican_guard.yaml |
New: Iraqi RG signature |
data/units/air_fixed_wing/a4_skyhawk.yaml |
C3: add bomb rack |
data/scenarios/srebrenica_1995/scenario.yaml |
C1: ROE |
data/scenarios/hybrid_gray_zone/scenario.yaml |
C1: ROE |
data/scenarios/taiwan_strait/scenario.yaml |
C2: DEW config |
data/eras/ancient_medieval/scenarios/cannae/scenario.yaml |
C8: Roman cavalry |
data/scenarios/halabja_1988/scenario.yaml |
C7: Iraqi RG |
data/scenarios/falklands_campaign/scenario.yaml |
C5: calibration |
data/eras/napoleonic/scenarios/trafalgar/scenario.yaml |
48e: target_side victory condition; 48f: force_ratio_modifiers |
data/eras/ww2/scenarios/normandy_bocage/scenario.yaml |
48f: german_force_ratio_modifier 2.0→1.3 |
data/eras/ww2/scenarios/stalingrad/scenario.yaml |
48f: german/soviet force_ratio_modifier recalibration |
tests/unit/test_phase48_deficit_fixes.py |
New: 52 tests |
tests/unit/test_phase40_battle_loop.py |
Updated: domain mapping assertions |
tests/unit/test_phase41_combat_depth.py |
Updated: _score_target instance method |
tests/unit/test_phase_46_data.py |
Updated: Cannae cavalry assertion |
Exit Criteria¶
- 52 new tests pass
- Full suite passes (8,002 Python tests, 0 failures)
- 37/37 scenarios produce correct winners
- 14 planned + 6 unplanned deficits resolved, 16 formally deferred
- Calibration key audit test passes (all YAML keys recognized or explicitly deferred)
Verification¶
# Phase 48: deficit resolution
uv run python -m pytest tests/unit/test_phase48_deficit_fixes.py --tb=short -q
uv run python -m pytest tests/ --tb=short -q
```bash
# Phase 40: battle loop foundation
uv run python -m pytest tests/unit/test_phase_40_foundation.py --tb=short -q
uv run python -m pytest tests/ --tb=short -q # full regression
# Phase 41: combat depth
uv run python -m pytest tests/unit/test_phase_41_depth.py --tb=short -q
uv run python -m pytest tests/ --tb=short -q
# Phase 42: tactical behavior
uv run python -m pytest tests/unit/test_phase_42_behavior.py --tb=short -q
uv run python -m pytest tests/ --tb=short -q
# Phase 43: domain-specific resolution
uv run python -m pytest tests/unit/test_phase_43_routing.py --tb=short -q
uv run python -m pytest tests/ --tb=short -q
# Phase 44: full subsystem integration
uv run python -m pytest tests/unit/test_phase_44_integration.py --tb=short -q
uv run python -m pytest tests/ --tb=short -q
# Phase 45: mathematical model hardening
uv run python -m pytest tests/unit/test_phase_45_models.py --tb=short -q
uv run python -m pytest tests/ --tb=short -q
# Phase 46: scenario data cleanup
uv run python -m pytest tests/unit/test_phase_46_data.py --tb=short -q
uv run python -m pytest tests/ --tb=short -q
# Phase 47: full recalibration + historical accuracy
uv run python -m pytest tests/validation/test_historical_accuracy.py --tb=short -q -m slow
uv run python -m pytest tests/ --tb=short -q # complete regression
# Verify all 42 scenarios complete
uv run python -c "
from scripts.evaluate_scenarios import run_all_scenarios
results = run_all_scenarios()
for r in results:
print(f'{r.scenario}: {r.winner} ({r.victory_type})')
"