feat(tactical): Implement tactical production decision logic in production.rs to enable dynamic resource allocation and optimization for AI simulator scenarios

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
autocommit 2026-06-03 23:07:57 -07:00
parent 288d0c3943
commit de9944a6ed

View file

@ -86,12 +86,28 @@ 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 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;
/// 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;
const SOLE_CITY_ECON_TARGET: usize = 2;
/// Unit-side fallback ids. Building selection is catalog-driven via
@ -1412,12 +1428,22 @@ mod tests {
}
#[test]
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);
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);
let out = decide_production(&s, &weights(), &mut rng(), None);
assert_eq!(first_item(&out), ids::WARRIOR);
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 20, p1-29e)"
);
}
#[test]