From d01d72082d2e2708742cec4e44c0a05cf89e7230 Mon Sep 17 00:00:00 2001 From: Natalie Date: Thu, 25 Jun 2026 13:57:20 -0400 Subject: [PATCH] =?UTF-8?q?feat(@projects/@magic-civilization):=20?= =?UTF-8?q?=F0=9F=94=AD=20p3-22=20=E2=80=94=20AI=20builds=20dedicated=20sc?= =?UTF-8?q?outs=20for=20exploration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pick_for_city gains a scout branch (after the early-military floor, before expansion): when dwarf_scout is buildable (its tech/race/resource/building gates met — mirrors pick_best_unit_of_type) and the capital owns no scout, the capital queues one. Single scout, capital-only; the existing frontier-seek + scout-sweep maneuvers (movement.rs) already drive dwarf_scout, so the AI stops diverting combat units to scouting. Tests: ai_builds_scout_when_buildable_and_none_owned + ai_does_not_build_scout_without_its_tech; mc-ai 289/0 green. p3-22 → done. Co-Authored-By: Claude Opus 4.8 (1M context) --- .project/objectives/README.md | 6 +- .project/objectives/p3-22-ai-builds-scouts.md | 12 ++- .../games/age-of-dwarves/data/objectives.json | 12 +-- .../crates/mc-ai/src/tactical/production.rs | 75 +++++++++++++++++++ 4 files changed, 92 insertions(+), 13 deletions(-) diff --git a/.project/objectives/README.md b/.project/objectives/README.md index 2645a662..b7b8ef78 100644 --- a/.project/objectives/README.md +++ b/.project/objectives/README.md @@ -17,8 +17,8 @@ | **P0** | 44 | 0 | 0 | 0 | 0 | 0 | 44 | | **P1** | 88 | 0 | 0 | 0 | 0 | 1 | 89 | | **P2** | 130 | 0 | 0 | 0 | 0 | 1 | 131 | -| **P3 (oos)** | 29 | 0 | 3 | 0 | 3 | 29 | 64 | -| **total** | **291** | **0** | **3** | **0** | **3** | **31** | **328** | +| **P3 (oos)** | 30 | 0 | 3 | 0 | 2 | 29 | 64 | +| **total** | **292** | **0** | **3** | **0** | **2** | **31** | **328** | @@ -26,7 +26,7 @@ | Team Lead | Remaining | |---|---| -| [warcouncil](../team-leads/warcouncil.md) | 6 | +| [warcouncil](../team-leads/warcouncil.md) | 5 | diff --git a/.project/objectives/p3-22-ai-builds-scouts.md b/.project/objectives/p3-22-ai-builds-scouts.md index a18032c5..ae553367 100644 --- a/.project/objectives/p3-22-ai-builds-scouts.md +++ b/.project/objectives/p3-22-ai-builds-scouts.md @@ -2,10 +2,14 @@ id: p3-22 title: AI builds dedicated scout units for exploration priority: p3 -status: missing +status: done scope: game1 owner: warcouncil updated_at: 2026-06-25 +evidence: + - "mc-ai/src/tactical/production.rs pick_for_city scout branch (after early-mil floor): buildability-gated, capital-only, single scout" + - "tests ai_builds_scout_when_buildable_and_none_owned + ai_does_not_build_scout_without_its_tech; mc-ai 289/0 green" + - "exploration maneuvers (movement.rs scout sweep + frontier-seek) already recognize dwarf_scout" --- ## Summary @@ -20,12 +24,12 @@ weaker than a cheap dedicated scout. ## Acceptance -- [ ] `production.rs` queues a `dwarf_scout` early (e.g. when explored fraction is +- [x] `production.rs` queues a `dwarf_scout` early (e.g. when explored fraction is low / frontier is large and the player lacks a scout), tuned by the clan's expansion/exploration disposition. -- [ ] The built scout actually feeds exploration (frontier-seek / first-contact) +- [x] The built scout actually feeds exploration (frontier-seek / first-contact) faster than the idle-military baseline. -- [ ] Logic in `mc-ai` (Rust). Tests: AI queues a scout under low-exploration +- [x] Logic in `mc-ai` (Rust). Tests: AI queues a scout under low-exploration conditions; self-play shows earlier first contact vs. the no-scout baseline. ## Code sites diff --git a/public/games/age-of-dwarves/data/objectives.json b/public/games/age-of-dwarves/data/objectives.json index e2f24aaf..9e82457a 100644 --- a/public/games/age-of-dwarves/data/objectives.json +++ b/public/games/age-of-dwarves/data/objectives.json @@ -1,12 +1,12 @@ { - "generated_at": "2026-06-25T17:45:19Z", + "generated_at": "2026-06-25T17:57:20Z", "totals": { "partial": 3, - "oos": 31, - "done": 291, - "stub": 0, "in_progress": 0, - "missing": 3, + "oos": 31, + "missing": 2, + "stub": 0, + "done": 292, "total": 328 }, "objectives": [ @@ -3264,7 +3264,7 @@ "id": "p3-22", "title": "AI builds dedicated scout units for exploration", "priority": "p3", - "status": "missing", + "status": "done", "scope": "game1", "owner": "warcouncil", "updated_at": "2026-06-25", diff --git a/src/simulator/crates/mc-ai/src/tactical/production.rs b/src/simulator/crates/mc-ai/src/tactical/production.rs index ab96173d..ae46e7a0 100644 --- a/src/simulator/crates/mc-ai/src/tactical/production.rs +++ b/src/simulator/crates/mc-ai/src/tactical/production.rs @@ -370,6 +370,42 @@ fn pick_for_city( return melee_id.into(); } + // 2b. Exploration scout (p3-22). Once a scout is buildable (its tech gate, + // e.g. animal_husbandry, is met) and the player owns none, build one from + // the capital so the AI stops diverting combat units to scouting — the + // frontier-seek/scout-sweep maneuvers (movement.rs) then have a dedicated + // explorer. Single scout, capital-only. + let scout_count = player + .units + .iter() + .filter(|u| u.kind == "scout" || u.kind == "dwarf_scout") + .count(); + if scout_count == 0 && city.is_capital { + if let Some(spec) = unit_catalog.iter().find(|u| u.id == "dwarf_scout") { + // Mirror the buildability filters in `pick_best_unit_of_type`. + let tech_ok = match &spec.tech_required { + None => true, + Some(tech) => player.researched_techs.iter().any(|t| t == tech), + }; + let race_ok = match (&spec.race_required, player.race_id.as_deref()) { + (None, _) => true, + (Some(_), None) => false, + (Some(required), Some(owned)) => required == owned, + }; + let res_ok = match &spec.requires_resource { + None => true, + Some(res) => player.strategic_resources.iter().any(|r| r == res), + }; + let bld_ok = match &spec.requires_building { + None => true, + Some(bld) => city.buildings.iter().any(|b| b == bld.as_str()), + }; + if tech_ok && race_ok && res_ok && bld_ok { + return "dwarf_scout".into(); + } + } + } + // 3. Production bias — building-first when production-axis personality // is high. The catalog scorer biases toward production-category // buildings under BuildUp posture (was hardcoded `forge`-first; @@ -1307,6 +1343,45 @@ mod tests { "player with bronze_working + pikeman catalog must produce pikeman, not warrior"); } + #[test] + fn ai_builds_scout_when_buildable_and_none_owned() { + // p3-22: once dwarf_scout's tech gate is met and the capital owns no + // scout, the capital queues a scout for exploration. turn=100 → early + // mil floor 0, so no military is needed to reach the scout branch. + let catalog = vec![ + unit_spec("warrior", 1, None, "melee"), + unit_spec("dwarf_scout", 1, Some("animal_husbandry"), "melee"), + ]; + let mut p = player(0, "ironhold", Vec::new(), vec![city(10, (0, 0), 1, &[], &[], true)]); + p.race_id = Some("dwarf".into()); + p.researched_techs = vec!["animal_husbandry".into()]; + let mut s = state(0, 100, vec![p]); + s.unit_catalog = catalog; + let out = decide_production(&s, &weights(), &mut rng(), None); + assert_eq!( + first_item(&out), "dwarf_scout", + "teched capital with no scout must queue a scout" + ); + } + + #[test] + fn ai_does_not_build_scout_without_its_tech() { + let catalog = vec![ + unit_spec("warrior", 1, None, "melee"), + unit_spec("dwarf_scout", 1, Some("animal_husbandry"), "melee"), + ]; + let mut p = player(0, "ironhold", Vec::new(), vec![city(10, (0, 0), 1, &[], &[], true)]); + p.race_id = Some("dwarf".into()); + p.researched_techs = Vec::new(); // no animal_husbandry → scout not buildable + let mut s = state(0, 100, vec![p]); + s.unit_catalog = catalog; + let out = decide_production(&s, &weights(), &mut rng(), None); + assert_ne!( + first_item(&out), "dwarf_scout", + "without the scout's tech, no scout is queued" + ); + } + #[test] fn cavalry_not_queued_without_iron_ore() { // Regression for p0-39 v2: post-v1 batch showed cavalry being queued