feat(tactical): ✨ Implement sole-city economy strategy to prioritize production for a single critical city during threats
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
2637b79e15
commit
f0ae52a746
1 changed files with 121 additions and 0 deletions
|
|
@ -78,6 +78,22 @@ const DOMINANCE_FACTOR: f32 = 2.0;
|
|||
/// many turns slots walls in before the general fallback.
|
||||
const CAPITAL_WALLS_MIN_AGE_TURNS: u32 = 20;
|
||||
|
||||
/// (p1-29e) Sole-city economy break-out. A *threatened* sole-city AI hits
|
||||
/// `Posture::Threatened` and returns `melee_id` at step 1 every turn, so it
|
||||
/// never reaches the step-7 building scorer — RL divergence mining of
|
||||
/// `duel-v4-encfix-s7` plus apricot batch `20260516_183534` confirmed the
|
||||
/// trailing AI (P1) built ZERO buildings in 10/10 p1-29d seeds, capping it at
|
||||
/// `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;
|
||||
const SOLE_CITY_ECON_TARGET: usize = 2;
|
||||
|
||||
/// Unit-side fallback ids. Building selection is catalog-driven via
|
||||
/// [`pick_building_from_catalog`] (p1-42); these constants only cover the
|
||||
/// unit-side fallbacks that survive when `unit_catalog` is empty (legacy
|
||||
|
|
@ -293,6 +309,30 @@ fn pick_for_city(
|
|||
dominance_factor_t,
|
||||
);
|
||||
|
||||
// 0. (p1-29e) Sole-city economy break-out — see SOLE_CITY_ECON_* docs.
|
||||
// Interject ONE production/infrastructure building for a threatened
|
||||
// sole-city AI once a minimal defender floor is met, so it escapes the
|
||||
// step-1 perpetual-military loop and can scale toward tier 2. Gated on
|
||||
// `sole_city_threatened`, so multi-city and unthreatened players are
|
||||
// completely unaffected — no regression to the existing ladder.
|
||||
if sole_city_threatened
|
||||
&& own_mil >= SOLE_CITY_ECON_MIN_DEFENDERS
|
||||
&& city.buildings.len() < SOLE_CITY_ECON_TARGET
|
||||
{
|
||||
if let Some(id) = pick_building_from_catalog(
|
||||
city,
|
||||
player,
|
||||
building_catalog,
|
||||
weights,
|
||||
axes,
|
||||
BuildingPosture::Production,
|
||||
sole_city_threatened,
|
||||
&player.building_priors,
|
||||
) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Threat preemption (GDScript Priority 0-A).
|
||||
if posture == Posture::Threatened {
|
||||
return melee_id.into();
|
||||
|
|
@ -1316,6 +1356,87 @@ mod tests {
|
|||
assert_eq!(first_item(&out), ids::WARRIOR);
|
||||
}
|
||||
|
||||
// ── Sole-city economy break-out (p1-29e) ────────────────────────────
|
||||
|
||||
/// A *threatened sole-city* AI with the minimal defender floor met and
|
||||
/// fewer than SOLE_CITY_ECON_TARGET buildings must escape the step-1
|
||||
/// perpetual-military loop and slot a production building. Without this,
|
||||
/// P1 built ZERO buildings in 10/10 p1-29d seeds (capped at tier_peak=1).
|
||||
fn econ_breakout_state(
|
||||
own_units: u32,
|
||||
buildings: &[&str],
|
||||
cities: u32,
|
||||
) -> TacticalState {
|
||||
let defender_units: Vec<TacticalUnit> =
|
||||
(1..=own_units).map(|i| warrior(i, (1, 1))).collect();
|
||||
let attacker_units: Vec<TacticalUnit> =
|
||||
(100..105).map(|i| warrior(i, (9, 9))).collect();
|
||||
let mut city_list =
|
||||
vec![city(10, (1, 1), 3, buildings, &[], true)];
|
||||
for c in 1..cities {
|
||||
city_list.push(city(10 + c, (5 + c as i32, 5), 2, buildings, &[], false));
|
||||
}
|
||||
let mut s = state(
|
||||
0,
|
||||
50,
|
||||
vec![
|
||||
player(0, "ironhold", defender_units, city_list),
|
||||
player(1, "blackhammer", attacker_units, Vec::new()),
|
||||
],
|
||||
);
|
||||
s.building_catalog = ladder_catalog();
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sole_city_econ_breakout_builds_production_when_threatened() {
|
||||
// own_mil=2 (>= floor), 1 building (< target=2), sole city, threatened
|
||||
// by 5 attackers → break-out fires → forge (lone production entry).
|
||||
let s = econ_breakout_state(2, &["walls"], 1);
|
||||
let out = decide_production(&s, &weights(), &mut rng(), None);
|
||||
assert_eq!(
|
||||
first_item(&out),
|
||||
"forge",
|
||||
"threatened sole-city AI must break out of the military loop to \
|
||||
build economy (p1-29e)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sole_city_econ_breakout_stops_at_target() {
|
||||
// 2 buildings already (== target) → break-out does NOT fire; the
|
||||
// step-1 threat preemption resumes and the city builds a defender.
|
||||
let s = econ_breakout_state(2, &["walls", "forge"], 1);
|
||||
let out = decide_production(&s, &weights(), &mut rng(), None);
|
||||
assert_eq!(first_item(&out), ids::WARRIOR);
|
||||
}
|
||||
|
||||
#[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);
|
||||
let out = decide_production(&s, &weights(), &mut rng(), None);
|
||||
assert_eq!(first_item(&out), ids::WARRIOR);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_city_threatened_unaffected_by_econ_breakout() {
|
||||
// 2 cities → sole_city_threatened=false → break-out skipped; threat
|
||||
// preemption builds a defender (no economy interjection). Pins that
|
||||
// the patch never touches multi-city players.
|
||||
let s = econ_breakout_state(2, &["walls"], 2);
|
||||
let out = decide_production(&s, &weights(), &mut rng(), None);
|
||||
for a in &out {
|
||||
if let Action::EnqueueBuild { item_id, .. } = a {
|
||||
assert_eq!(
|
||||
item_id, ids::WARRIOR,
|
||||
"multi-city threatened players must still build military"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn production_axis_8_slots_forge_after_mil_floor() {
|
||||
// deepforge (production=8) with the legacy ladder catalog injected:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue