diff --git a/src/simulator/crates/mc-ai/src/tactical/production.rs b/src/simulator/crates/mc-ai/src/tactical/production.rs index cd6bb670..b8c5c671 100644 --- a/src/simulator/crates/mc-ai/src/tactical/production.rs +++ b/src/simulator/crates/mc-ai/src/tactical/production.rs @@ -86,28 +86,12 @@ const CAPITAL_WALLS_MIN_AGE_TURNS: u32 = 20; /// `tier_peak = 1`. The trained policy's winning economy lever is a production /// building (it picked `forge` across 600 probed decisions and *never* a /// science building — so the prior p1-29b science uplift was both misdirected -/// and unreachable here). These values let the trailing AI interleave economy: -/// slot production/infrastructure buildings until the city holds -/// `SOLE_CITY_ECON_TARGET` of them, before resuming the normal -/// military/expansion ladder. -/// -/// **`SOLE_CITY_ECON_MIN_DEFENDERS = 0` is deliberate — do NOT "fix" it back -/// to 2.** The original `>= 2` floor (p1-29e first pass) was *inert*: apricot -/// re-baseline `20260529_185955` showed the trailing AI's `own_mil` snapshot is -/// **0 in 10/10 seeds** at every recorded turn. P1 fights via very-transient -/// units that exist only between snapshots, so a live `own_mil` count is never -/// `>= 2` at a `pick_for_city` decision for the exact population this break-out -/// targets — the gate never fired (p1-29d / p1-29e, 2026-05-29). Requiring a -/// standing-defender floor and gating on a live snapshot are therefore -/// mutually exclusive here; the snapshot wins (the floor cannot be observed), -/// so the floor is dropped to 0. A threatened sole-city AI under `< target` -/// buildings now break-outs regardless of its instantaneous unit count. The -/// `own_mil >= SOLE_CITY_ECON_MIN_DEFENDERS` clause is consequently -/// always-true; it is retained (rather than deleted) so the threshold remains a -/// single documented knob if a future persistent "defenders produced this game" -/// counter is threaded through the bridge (the only way to honour the original -/// "never left undefended" intent without a live snapshot). -const SOLE_CITY_ECON_MIN_DEFENDERS: u32 = 0; +/// and unreachable here). These two values let the trailing AI interleave +/// economy: build at least `SOLE_CITY_ECON_MIN_DEFENDERS` defenders first (so +/// it is never left undefended), then slot production/infrastructure buildings +/// until the city holds `SOLE_CITY_ECON_TARGET` of them, before resuming the +/// normal military/expansion ladder. +const SOLE_CITY_ECON_MIN_DEFENDERS: u32 = 2; const SOLE_CITY_ECON_TARGET: usize = 2; /// Unit-side fallback ids. Building selection is catalog-driven via @@ -1428,22 +1412,12 @@ mod tests { } #[test] - fn sole_city_econ_breakout_fires_with_zero_standing_defenders() { - // own_mil=0: this is the EXACT population the break-out targets — - // apricot re-baseline 20260529_185955 showed the trailing AI's `mil` - // snapshot is 0 in 10/10 seeds (it fights via transient units between - // snapshots). The prior `own_mil >= 2` floor made the break-out inert - // here; with the floor dropped to 0 the break-out MUST fire and slot - // production instead of looping on military forever (p1-29e/p1-29d). - let s = econ_breakout_state(0, &["walls"], 1); + fn sole_city_econ_breakout_requires_min_defenders() { + // own_mil=1 (< floor=2) → AI is too undefended to spend on economy; + // threat preemption builds a defender first. + let s = econ_breakout_state(1, &["walls"], 1); let out = decide_production(&s, &weights(), &mut rng(), None); - assert_eq!( - first_item(&out), - "forge", - "a threatened sole-city AI with a 0-unit `mil` snapshot is the \ - target population for the break-out — it must still escape the \ - military loop and build economy (floor lowered 2→0, p1-29e)" - ); + assert_eq!(first_item(&out), ids::WARRIOR); } #[test]