feat(@projects/@magic-civilization): 🔭 p3-22 — AI builds dedicated scouts for exploration

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) <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-06-25 13:57:20 -04:00
parent ed3c836e2f
commit d01d72082d
4 changed files with 92 additions and 13 deletions

View file

@ -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** |
</td><td valign='top' style='padding-left:2em'>
@ -26,7 +26,7 @@
| Team Lead | Remaining |
|---|---|
| [warcouncil](../team-leads/warcouncil.md) | 6 |
| [warcouncil](../team-leads/warcouncil.md) | 5 |
</td></tr></table>

View file

@ -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

View file

@ -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",

View file

@ -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