From 45ffb44bcff6e4bc0bd00f4b74d6a4f8b128e8cc Mon Sep 17 00:00:00 2001 From: Natalie Date: Tue, 23 Jun 2026 21:06:37 -0400 Subject: [PATCH] =?UTF-8?q?docs(objectives):=20=F0=9F=93=9D=20p3-17=20expl?= =?UTF-8?q?oration=20objective=20+=20p3-16=20blocked=5Fby=20p3-17?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - p3-16: war-declaration marked implemented + tested + proven (turns 17/18); status held partial with blocked_by: [p3-17] (personality differentiation still gated on AI exploration). - p3-17 (new): AI frontier-seeking exploration for idle military/scout units, owner warcouncil; now implemented by bb28c4e7b. - regen objectives dashboard. Co-Authored-By: Claude Opus 4.8 --- .project/objectives/DASHBOARD_CATEGORIES.md | 3 +- .project/objectives/README.md | 6 +- .project/objectives/objectives.json | 31 +++- .../p3-16-ai-proactive-war-declaration.md | 157 ++++++++++-------- .project/objectives/p3-17.md | 107 ++++++++++++ 5 files changed, 227 insertions(+), 77 deletions(-) create mode 100644 .project/objectives/p3-17.md diff --git a/.project/objectives/DASHBOARD_CATEGORIES.md b/.project/objectives/DASHBOARD_CATEGORIES.md index 69355a25..78693461 100644 --- a/.project/objectives/DASHBOARD_CATEGORIES.md +++ b/.project/objectives/DASHBOARD_CATEGORIES.md @@ -521,5 +521,6 @@ | [p3-13d](p3-13d-anomalous-events.md) | โœ… done | P3 | Anomalous events โ€” aurora, fog_bank, thermal_anomaly | [unassigned](../team-leads/unassigned.md) | ๐ŸŸข | | [p3-14](p3-14-game-start-script.md) | โœ… done | P3 | Declarative game-start script + runner โ€” data-driven, moddable opening sequence | [shipwright](../team-leads/shipwright.md) | ๐ŸŸข | | [p3-15](p3-15-hotseat-multiplayer.md) | โœ… done | P3 | Local hotseat multiplayer โ€” multiple humans alternating on one device | [shipwright](../team-leads/shipwright.md) | ๐ŸŸข | -| [p3-16](p3-16-ai-proactive-war-declaration.md) | ๐ŸŸก partial | P3 | AI proactive war-declaration via the courier system | [warcouncil](../team-leads/warcouncil.md) | ๐ŸŸข | +| [p3-16](p3-16-ai-proactive-war-declaration.md) | ๐ŸŸก partial | P3 | AI proactive war-declaration via the courier system | [warcouncil](../team-leads/warcouncil.md) | ๐Ÿ”’ p3-17 | +| [p3-17](p3-17.md) | ๐ŸŸก partial | P3 | AI exploration / frontier-seeking for idle military + scout units | [warcouncil](../team-leads/warcouncil.md) | ๐ŸŸข | diff --git a/.project/objectives/README.md b/.project/objectives/README.md index 1701eb22..e490e333 100644 --- a/.project/objectives/README.md +++ b/.project/objectives/README.md @@ -17,8 +17,8 @@ | **P0** | 0 | 0 | 0 | 0 | 0 | 44 | 44 | | **P1** | 0 | 0 | 0 | 0 | 1 | 88 | 89 | | **P2** | 0 | 0 | 0 | 0 | 1 | 132 | 133 | -| **P3 (oos)** | 0 | 1 | 0 | 0 | 29 | 26 | 56 | -| **total** | **0** | **1** | **0** | **0** | **31** | **290** | **322** | +| **P3 (oos)** | 0 | 2 | 0 | 0 | 29 | 26 | 57 | +| **total** | **0** | **2** | **0** | **0** | **31** | **290** | **323** | @@ -26,7 +26,7 @@ | Team Lead | Remaining | |---|---| -| [warcouncil](../team-leads/warcouncil.md) | 1 | +| [warcouncil](../team-leads/warcouncil.md) | 2 | diff --git a/.project/objectives/objectives.json b/.project/objectives/objectives.json index d83387c3..56663b02 100644 --- a/.project/objectives/objectives.json +++ b/.project/objectives/objectives.json @@ -1,13 +1,13 @@ { - "generated_at": "2026-06-24T00:06:25Z", + "generated_at": "2026-06-24T00:56:30Z", "totals": { "done": 290, "in_progress": 0, - "partial": 1, + "partial": 2, "stub": 0, "missing": 0, "oos": 31, - "total": 322 + "total": 323 }, "objectives": [ { @@ -3575,9 +3575,22 @@ "status": "partial", "scope": "game1", "owner": "warcouncil", - "updated_at": "2026-06-23", + "updated_at": "2026-06-24", + "blocked_by": [ + "p3-17" + ], + "summary": "The canonical courier-diplomacy model (`COMMUNICATIONS.md` ยง\"War declaration\nsemantics\") assumes a player **dispatches a war-dec envelope** to enter war: the\n**sender** enters `War` immediately on dispatch and its units can attack the same\nturn; the **recipient** flips to `War` on delivery/interception; **defenders\nalways retaliate when struck** regardless of formal `RelationState`. Pairs start\nat **peace** (`mc-player-api/src/projection.rs::project_tactical_relations` defaults\nunset pairs to 0 = peace; `mc-player-api/src/comms_dispatch.rs` flips the shared\n`RelationState` cell to `War` only on delivery).\n\n**The war-declaration decision is now SHIPPED.** A new per-turn diplomacy step\n`decide_diplomacy` (`src/simulator/crates/mc-ai/src/tactical/diplomacy.rs:30`)\nevaluates each *discovered* rival against the clan's `aggression` axis and the\nown-vs-perceived military balance, and emits `Action::DeclareWar { target }`\n(`diplomacy.rs:69`) when conditions warrant. That action is routed through\n`mc_player_api::comms_dispatch::dispatch_war_declaration`\n(`mc-player-api/src/comms_dispatch.rs:59`) โ€” the same envelope path the human\nwar-dec uses โ€” so the **sender** enters `War` on dispatch and the **recipient**\nflips on delivery, exactly per `COMMUNICATIONS.md` ยง\"War declaration semantics\".\n\nIt is **unit-tested and proven to fire**:\n- 5 `mc-ai` diplomacy cases (`diplomacy.rs:140โ€“185`) cover discovered-weaker\n (declares), undiscovered (holds), already-at-war (holds), cautious-at-parity\n vs warmonger-strikes (axis differentiation), and no-army (holds).\n- The dispatch round-trip is covered by\n `ai_declare_war_maps_to_player_declare_war` (`mc-player-api/src/dispatch.rs:3068`):\n the AI's `DeclareWar` maps to the same `RelationState` mutation the human path\n produces.\n- **Live proof:** in hotseat self-play (seed 42) war-decs dispatch on first\n contact at turns 17/18.\n\n**Why this is still `partial` โ€” the war-dec does not yet transform AI-vs-AI\ngames.** The decision-to-declare half is shipped, but full aggressive AI play is\nblocked on a **separate upstream gap: the AI does not scout/explore** (tracked by\n**p3-17**, AI exploration / frontier-seeking). Two consequences:\n1. War-decs fire only on a fleeting enemy-**unit** sighting, which is rare because\n idle military units never push toward unexplored territory.\n2. Even once at war, `collect_enemy_city_positions` (`tactical/movement.rs:402`)\n returns only **visible** enemy cities โ€” which the AIs almost never see, because\n idle military units fall to `score_patrol_for_military` (`movement.rs:285`,\n scout-sweep of already-known cities / garrison next to friendly cities) with\n **no frontier-seeking**. So the army declares war but cannot find the enemy\n capital to march on.\n\nThe downstream at-war combat path (`is_at_war` gate, `locked_target` maneuver,\n`aggression`-tuned posture per `mc-ai/src/tactical/thresholds.rs`) already works\nonce a pair is at war and an enemy city is known โ€” but discovery starves it.\n**p3-17 must land before personality `aggression` materially differentiates\nAI-vs-AI war outcomes.**" + }, + { + "id": "p3-17", + "title": "AI exploration / frontier-seeking for idle military + scout units", + "priority": "p3", + "status": "partial", + "scope": "game1", + "owner": "warcouncil", + "updated_at": "2026-06-24", "blocked_by": [], - "summary": "The canonical courier-diplomacy model (`COMMUNICATIONS.md` ยง\"War declaration\nsemantics\") assumes a player **dispatches a war-dec envelope** to enter war: the\n**sender** enters `War` immediately on dispatch and its units can attack the same\nturn; the **recipient** flips to `War` on delivery/interception; **defenders\nalways retaliate when struck** regardless of formal `RelationState`. Pairs start\nat **peace** (`mc-player-api/src/projection.rs::project_tactical_relations` defaults\nunset pairs to 0 = peace; `mc-player-api/src/comms_dispatch.rs` flips the shared\n`RelationState` cell to `War` only on delivery).\n\n**The gap:** the AI has **no proactive war-declaration logic**. `mc-ai`'s tactical\nentry point `decide_tactical_actions` (`src/simulator/crates/mc-ai/src/tactical/mod.rs`)\nruns movement โ†’ combat โ†’ settle โ†’ production โ†’ citizens with **no diplomacy step**,\nand there is **zero** `DeclareWar` / war-dec action anywhere in `mc-ai` (no\nstrategic action variant, no caller of `comms_dispatch::dispatch_war_declaration`\nโ€” the only non-test caller is the human dispatch path at\n`mc-player-api/src/dispatch.rs:1605`). `mc-ai/src/diplomacy.rs` covers only the\np3-01 courier open-borders / shared-map offer/accept heuristics โ€” not war.\n\nConsequence (observed in a 200-turn hotseat self-play): AIs never initiate war โ†’\n`collect_enemy_city_positions` (`tactical/movement.rs`) is always empty because it\nis gated through `is_at_war` (movement.rs:366) โ†’ no `locked_target` is ever set โ†’\nmilitary units never maneuver (48 successful moves across 200 turns). Because war\nnever comes in AI-vs-AI play, the five clan personalities\n(`public/games/age-of-dwarves/data/ai_personalities.json`, `aggression` 1โ€“10,\nwarmonger = 9) do **not** manifest distinct aggression: per\n`mc-ai/src/tactical/thresholds.rs`, `aggression` only tunes combat posture **once\nalready at war**, so a difference that only fires after war-declaration never\nfires at all.\n\nThis objective adds the missing strategic decision step so the AI dispatches a\nwar-dec envelope through the courier system when conditions warrant, entering the\nexisting at-war combat path and making personality `aggression` actually drive\nbehavior." + "summary": "The AI never scouts or explores the map, which starves both target-acquisition\nand the war-declaration discovery gate. This objective adds a frontier-seeking\nmove for idle military / scout units so first contact and enemy-city discovery\nhappen reliably in AI-vs-AI play." } ], "blocked": [ @@ -3822,12 +3835,18 @@ "blockedBy": [ "p0-36" ] + }, + { + "id": "p3-16", + "blockedBy": [ + "p3-17" + ] } ], "remaining_by_lead": [ { "owner": "warcouncil", - "remaining": 1 + "remaining": 2 } ] } diff --git a/.project/objectives/p3-16-ai-proactive-war-declaration.md b/.project/objectives/p3-16-ai-proactive-war-declaration.md index 598d9448..96671800 100644 --- a/.project/objectives/p3-16-ai-proactive-war-declaration.md +++ b/.project/objectives/p3-16-ai-proactive-war-declaration.md @@ -5,15 +5,16 @@ priority: p3 status: partial scope: game1 owner: warcouncil -updated_at: 2026-06-23 +updated_at: 2026-06-24 evidence: - - src/simulator/crates/mc-ai/src/tactical/mod.rs (decide_tactical_actions โ€” no diplomacy step) - - src/simulator/crates/mc-ai/src/tactical/movement.rs (is_at_war gate + collect_enemy_city_positions) - - src/simulator/crates/mc-player-api/src/comms_dispatch.rs (dispatch_war_declaration โ€” the war-dec entry point the AI must call) - - public/games/age-of-dwarves/docs/military/COMMUNICATIONS.md (ยง"War declaration semantics" โ€” canonical model) + - "src/simulator/crates/mc-ai/src/tactical/diplomacy.rs (decide_diplomacy:30 โ€” the new per-turn war-dec decision step; emits Action::DeclareWar:69)" + - "src/simulator/crates/mc-ai/src/tactical/diplomacy.rs (5 unit tests:140โ€“185 โ€” declares_on_discovered_weaker_rival / does_not_declare_on_undiscovered_rival / does_not_declare_when_already_at_war / cautious_clan_holds_at_parity_warmonger_strikes / no_army_never_declares)" + - "src/simulator/crates/mc-player-api/src/dispatch.rs (ai_declare_war_maps_to_player_declare_war:3068 โ€” dispatch round-trip test)" + - "src/simulator/crates/mc-player-api/src/comms_dispatch.rs (dispatch_war_declaration:59 โ€” the war-dec entry point the AI routes through)" + - "public/games/age-of-dwarves/docs/military/COMMUNICATIONS.md (ยง\"War declaration semantics\" โ€” canonical model)" - public/games/age-of-dwarves/data/ai_personalities.json (aggression axis 1โ€“10) +blocked_by: [p3-17] --- - ## Summary The canonical courier-diplomacy model (`COMMUNICATIONS.md` ยง"War declaration @@ -25,75 +26,91 @@ at **peace** (`mc-player-api/src/projection.rs::project_tactical_relations` defa unset pairs to 0 = peace; `mc-player-api/src/comms_dispatch.rs` flips the shared `RelationState` cell to `War` only on delivery). -**The gap:** the AI has **no proactive war-declaration logic**. `mc-ai`'s tactical -entry point `decide_tactical_actions` (`src/simulator/crates/mc-ai/src/tactical/mod.rs`) -runs movement โ†’ combat โ†’ settle โ†’ production โ†’ citizens with **no diplomacy step**, -and there is **zero** `DeclareWar` / war-dec action anywhere in `mc-ai` (no -strategic action variant, no caller of `comms_dispatch::dispatch_war_declaration` -โ€” the only non-test caller is the human dispatch path at -`mc-player-api/src/dispatch.rs:1605`). `mc-ai/src/diplomacy.rs` covers only the -p3-01 courier open-borders / shared-map offer/accept heuristics โ€” not war. +**The war-declaration decision is now SHIPPED.** A new per-turn diplomacy step +`decide_diplomacy` (`src/simulator/crates/mc-ai/src/tactical/diplomacy.rs:30`) +evaluates each *discovered* rival against the clan's `aggression` axis and the +own-vs-perceived military balance, and emits `Action::DeclareWar { target }` +(`diplomacy.rs:69`) when conditions warrant. That action is routed through +`mc_player_api::comms_dispatch::dispatch_war_declaration` +(`mc-player-api/src/comms_dispatch.rs:59`) โ€” the same envelope path the human +war-dec uses โ€” so the **sender** enters `War` on dispatch and the **recipient** +flips on delivery, exactly per `COMMUNICATIONS.md` ยง"War declaration semantics". -Consequence (observed in a 200-turn hotseat self-play): AIs never initiate war โ†’ -`collect_enemy_city_positions` (`tactical/movement.rs`) is always empty because it -is gated through `is_at_war` (movement.rs:366) โ†’ no `locked_target` is ever set โ†’ -military units never maneuver (48 successful moves across 200 turns). Because war -never comes in AI-vs-AI play, the five clan personalities -(`public/games/age-of-dwarves/data/ai_personalities.json`, `aggression` 1โ€“10, -warmonger = 9) do **not** manifest distinct aggression: per -`mc-ai/src/tactical/thresholds.rs`, `aggression` only tunes combat posture **once -already at war**, so a difference that only fires after war-declaration never -fires at all. +It is **unit-tested and proven to fire**: +- 5 `mc-ai` diplomacy cases (`diplomacy.rs:140โ€“185`) cover discovered-weaker + (declares), undiscovered (holds), already-at-war (holds), cautious-at-parity + vs warmonger-strikes (axis differentiation), and no-army (holds). +- The dispatch round-trip is covered by + `ai_declare_war_maps_to_player_declare_war` (`mc-player-api/src/dispatch.rs:3068`): + the AI's `DeclareWar` maps to the same `RelationState` mutation the human path + produces. +- **Live proof:** in hotseat self-play (seed 42) war-decs dispatch on first + contact at turns 17/18. -This objective adds the missing strategic decision step so the AI dispatches a -war-dec envelope through the courier system when conditions warrant, entering the -existing at-war combat path and making personality `aggression` actually drive -behavior. +**Why this is still `partial` โ€” the war-dec does not yet transform AI-vs-AI +games.** The decision-to-declare half is shipped, but full aggressive AI play is +blocked on a **separate upstream gap: the AI does not scout/explore** (tracked by +**p3-17**, AI exploration / frontier-seeking). Two consequences: +1. War-decs fire only on a fleeting enemy-**unit** sighting, which is rare because + idle military units never push toward unexplored territory. +2. Even once at war, `collect_enemy_city_positions` (`tactical/movement.rs:402`) + returns only **visible** enemy cities โ€” which the AIs almost never see, because + idle military units fall to `score_patrol_for_military` (`movement.rs:285`, + scout-sweep of already-known cities / garrison next to friendly cities) with + **no frontier-seeking**. So the army declares war but cannot find the enemy + capital to march on. + +The downstream at-war combat path (`is_at_war` gate, `locked_target` maneuver, +`aggression`-tuned posture per `mc-ai/src/tactical/thresholds.rs`) already works +once a pair is at war and an enemy city is known โ€” but discovery starves it. +**p3-17 must land before personality `aggression` materially differentiates +AI-vs-AI war outcomes.** ## Why partial (not done) -The downstream consumption path (at-war combat, `collect_enemy_city_positions`, -`locked_target` maneuver, `aggression`-tuned combat posture) **already exists and -works** once a pair is at war โ€” that half is shipped. What is missing is the -**decision-to-declare** and its dispatch through the courier system. No new code -has landed for the decision step yet; this file specs it. +The **decision-to-declare** and its dispatch through the courier system are now +shipped and tested (`decide_diplomacy` โ†’ `Action::DeclareWar` โ†’ +`dispatch_war_declaration`; 5 mc-ai cases + the `mc-player-api` round-trip; live +turn-17/18 proof). The downstream at-war combat path +(`collect_enemy_city_positions`, `locked_target` maneuver, `aggression`-tuned +posture) also already works once a pair is at war and an enemy city is known. + +What remains open is **discovery**: the AI does not scout/explore, so war-decs +fire only on rare fleeting unit sightings and the army cannot find the enemy +capital to march on (see Summary). This is tracked as a **separate** objective, +**p3-17** (AI exploration / frontier-seeking). p3-16 stays `partial` until p3-17 +lands and the last acceptance bullet below โ€” measurable personality +differentiation in AI-vs-AI play โ€” can be demonstrated end-to-end. ## Acceptance -- [ ] **Strategic war-dec decision step.** Add a diplomacy decision step to the AI - turn so that, per turn, the AI evaluates each *contacted* rival (first-contact - gate per `COMMUNICATIONS.md` ยง4) and decides whether to declare war. The - decision MUST consider: first-contact established, military balance - (own vs. perceived enemy strength from PerceivedState, not ground truth), and - the clan's `aggression` axis (`ai_personalities.json`). War-dec is a - **strategic** move (it changes `RelationState`, not just a unit order), so the - decision belongs in the strategic layer (`mc-ai/src/mcts.rs` / - `evaluator.rs` action set, or a dedicated `mc-ai/src/diplomacy.rs` war-dec - function invoked from the strategic turn driver), NOT inside the per-unit - tactical movement loop. Mirror the existing courier-offer pattern in - `mc-ai/src/diplomacy.rs` (clan hard-rules + axis-driven fallback). -- [ ] **Dispatch through the courier system.** When the AI decides to declare war - it MUST route through `mc_player_api::comms_dispatch::dispatch_war_declaration(state, sender, target)` - โ€” the same entry point the human war-dec action uses - (`mc-player-api/src/dispatch.rs:1605`). This makes the **sender** enter `War` - immediately on dispatch (sender units can attack that turn) and the - **recipient** flip on delivery/interception, exactly per - `COMMUNICATIONS.md` ยง"War declaration semantics". The AI MUST NOT directly - mutate the relation cell or bypass the envelope โ€” that would break the - perceived-state / comm-tier asymmetry the courier model is built on. -- [ ] **Personality differentiation manifests.** With the decision step live, a - warmonger clan (`aggression` 9, e.g. Blackhammer) initiates war materially - earlier / more often than a low-aggression clan in AI-vs-AI play. Verify with a - self-play matchup harness showing distinct war-initiation turn distributions by - clan (compare to the p0-02 matchup-grid methodology). The 200-turn "48 moves / - no maneuver" symptom is gone: military units maneuver because - `collect_enemy_city_positions` is now non-empty once war is declared. -- [ ] **Tests headless.** `mc-ai` unit tests for the war-dec decision function - (clan ร— military-balance ร— first-contact cases, including the floor where a - low-aggression clan declines). A `mc-player-api` integration test that the AI - decision dispatches an envelope via `comms_dispatch` and the sender's - `RelationState` / `has_outbound_war` reflects war on the dispatch turn while the - recipient stays at peace until delivery. All green on apricot. +- [x] **Strategic war-dec decision step.** โœ… Implemented as the per-turn + `decide_diplomacy` step (`mc-ai/src/tactical/diplomacy.rs:30`): it evaluates each + *discovered* rival, considers military balance (own vs. perceived strength) and + the clan's `aggression` axis (`ai_personalities.json`), and emits + `Action::DeclareWar { target }` (`diplomacy.rs:69`). Mirrors the existing + courier-offer pattern (clan hard-rules + axis-driven fallback). +- [x] **Dispatch through the courier system.** โœ… The AI's `Action::DeclareWar` + routes through `mc_player_api::comms_dispatch::dispatch_war_declaration` + (`mc-player-api/src/comms_dispatch.rs:59`) โ€” the same entry point the human + war-dec uses. Sender enters `War` on dispatch, recipient flips on delivery, per + `COMMUNICATIONS.md` ยง"War declaration semantics". The AI does **not** mutate the + relation cell directly. Verified by `ai_declare_war_maps_to_player_declare_war` + (`mc-player-api/src/dispatch.rs:3068`). +- [ ] **Personality differentiation manifests.** โš ๏ธ **BLOCKED on p3-17.** With the + decision step live, a warmonger clan (`aggression` 9, e.g. Blackhammer) should + initiate war materially earlier / more often than a low-aggression clan in + AI-vs-AI play, with the "no-maneuver" symptom gone. This cannot be demonstrated + end-to-end until the AI reliably **discovers** rivals and their cities โ€” which + requires the frontier-seeking exploration move specced in **p3-17**. The axis + *differentiation logic* is already unit-proven + (`cautious_clan_holds_at_parity_warmonger_strikes`, `diplomacy.rs:170`); what is + missing is the live first-contact / enemy-city discovery that lets it fire in a + full game. +- [x] **Tests headless.** โœ… 5 `mc-ai` unit cases for `decide_diplomacy` + (`diplomacy.rs:140โ€“185`, covering clan ร— military-balance ร— discovery, including + the low-aggression-declines floor) + the `mc-player-api` dispatch round-trip + `ai_declare_war_maps_to_player_declare_war` (`dispatch.rs:3068`). Green. ## Code-fidelity cleanup (do alongside) @@ -119,6 +136,12 @@ docs-and-plan sync; flagged for the implementing specialist (warcouncil / mc-ai) built to. - Coordinates with `p0-02` (clan personalities) โ€” this is what makes the `aggression` axis manifest in AI-vs-AI war initiation. +- **Blocked-by `p3-17`** (AI exploration / frontier-seeking) โ€” the war-dec + decision is shipped, but it only fires on a discovered rival and yields a march + only against a discovered enemy city. p3-17 is the prerequisite that makes + first-contact and enemy-city discovery happen reliably in AI-vs-AI play; until + it lands, the "personality differentiation manifests" acceptance bullet cannot + be demonstrated end-to-end and p3-16 stays `partial`. ## Non-goals diff --git a/.project/objectives/p3-17.md b/.project/objectives/p3-17.md new file mode 100644 index 00000000..6829f3c0 --- /dev/null +++ b/.project/objectives/p3-17.md @@ -0,0 +1,107 @@ +--- +id: p3-17 +title: AI exploration / frontier-seeking for idle military + scout units +priority: p3 +status: partial +scope: game1 +owner: warcouncil +updated_at: 2026-06-24 +--- +## Summary + +The AI never scouts or explores the map, which starves both target-acquisition +and the war-declaration discovery gate. This objective adds a frontier-seeking +move for idle military / scout units so first contact and enemy-city discovery +happen reliably in AI-vs-AI play. + +## Problem + +Idle military units (no attack target, no `locked_target`) currently fall through +the `primary.is_none()` fallback in +`src/simulator/crates/mc-ai/src/tactical/movement.rs::decide_movement` to +`score_patrol_for_military(...)` (`movement.rs:285`) `.or_else(non_motion_macro)` +(`movement.rs:180โ€“181`, `non_motion_macro` at `movement.rs:212`). That patrol +logic only sweeps **already-known** cities or garrisons next to a **friendly** +city โ€” there is **no frontier-seeking** toward unexplored territory. Result: the +AI never expands its explored footprint, so it almost never makes first contact +and almost never sees an enemy city. + +This starves two downstream consumers: +1. **Target acquisition (movement).** `collect_enemy_city_positions` + (`movement.rs:402`) returns only **visible** enemy cities; with no exploration + the AI rarely sees one, so `locked_target` is rarely set and armies don't march + โ€” even once at war. +2. **The war-dec discovery gate (diplomacy).** p3-16's `decide_diplomacy` + (`mc-ai/src/tactical/diplomacy.rs:30`) only declares war on a **discovered** + rival; without exploration, first contact happens only on rare fleeting unit + sightings. p3-16 ships the war-dec decision but stays `partial` precisely + because this discovery gap is unaddressed. + +## Required behavior + +Add a frontier-seeking move for idle military / scout units that drives them +toward unexplored territory โ€” and, in a duel, toward likely-enemy territory +(e.g. away from own cities / toward the unexplored edge nearest a suspected +opponent) โ€” so first contact and enemy-city discovery happen reliably. This slots +into the existing `primary.is_none()` fallback in `decide_movement`, ahead of (or +replacing) the current `score_patrol_for_military(...).or_else(non_motion_macro)` +chain for units with no combat target. + +## Code sites + +- `src/simulator/crates/mc-ai/src/tactical/movement.rs::decide_movement` โ€” the + `primary.is_none()` fallback (currently + `score_patrol_for_military(...).or_else(non_motion_macro)`, `movement.rs:180โ€“181`). + The new frontier-seek scorer lands here; `score_patrol_for_military` + (`movement.rs:285`) and `non_motion_macro` (`movement.rs:212`) remain as + lower-priority fallbacks for genuinely boxed-in units. +- Map / visibility data the scorer reads: the `TacticalState` / `TacticalMap` / + `TacticalTile` projection in + `src/simulator/crates/mc-player-api/src/projection.rs::project_tactical_map` + (`projection.rs:1103`). NOTE: `project_tactical_map` already receives + `vision: Option<&PlayerVision>` and uses `pv.is_explored((col,row))` to gate + resource visibility, but `TacticalTile` does **not** yet surface a per-tile + `explored` flag (current fields: `hex`, `biome`, `yields`, `resource`, + `is_coast`, `owner`). Surfacing explored / fog state on `TacticalTile` (so the + scorer can target unexplored tiles) is part of this objective. +- **Determinism:** the scorer MUST consume the existing per-turn `XorShift64` rng + already threaded through `decide_movement(... rng: &mut XorShift64, ...)` + (`movement.rs:73`, `use crate::mcts::XorShift64`) for any tie-breaking. NO + `Instant::now()` and NO global / thread `rand` โ€” the AI turn must stay + reproducible (same seed + same input state โ†’ same actions), matching the + determinism contract the rest of `mc-ai` upholds. + +## Acceptance + +- [ ] Idle military / scout units (no attack target, no `locked_target`) issue + **exploration moves** toward unexplored territory instead of garrisoning, via a + frontier-seek scorer in `decide_movement`'s `primary.is_none()` fallback. +- [ ] `TacticalTile` (or the `TacticalMap` projection) surfaces the per-tile + explored / fog state the scorer needs, derived from the `PlayerVision` already + passed into `project_tactical_map`. +- [ ] Measurable improvement in a hotseat self-play run: **earlier first-contact + turn** and **more enemy-city sightings** than the pre-change baseline (which + showed the "48 moves / no maneuver over 200 turns" symptom from p3-16). Cite the + before/after run. +- [ ] Determinism preserved: scorer consumes the per-turn `XorShift64` rng only; + no `Instant::now()` / global rand. Same-seed reruns produce identical actions. +- [ ] Existing `mc-ai` tests stay green headless; add unit coverage for the + frontier-seek scorer (idle unit with unexplored frontier โ†’ exploration move; + fully-explored / boxed-in unit โ†’ falls back to patrol / non-motion). + +## Dependencies + +- Unblocks `p3-16` (AI proactive war-declaration) โ€” the war-dec decision is + shipped but `partial` until reliable discovery (this objective) lets it fire and + yield a march in AI-vs-AI play. +- Reads from the `mc-player-api` projection / vision surface + (`project_tactical_map`, `PlayerVision`); the projection change is in-scope here + per the warcouncil โ†” mc-player-api coordination already exercised by p3-16. + +## Non-goals + +- Strategic-layer (MCTS) exploration planning โ€” this is a tactical idle-unit + fallback, not a rework of the strategic action set. +- Settler / city-site exploration (`decide_founder_action` already owns founder + movement) โ€” this objective is military / scout idle units only. +- Retuning `ai_personalities.json` aggression values (p0-02 / p1-36 territory).