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