diff --git a/.project/objectives/p3-30-wild-creature-ai-rust-port.md b/.project/objectives/p3-30-wild-creature-ai-rust-port.md index 992d5a11..939a539b 100644 --- a/.project/objectives/p3-30-wild-creature-ai-rust-port.md +++ b/.project/objectives/p3-30-wild-creature-ai-rust-port.md @@ -2,7 +2,7 @@ id: p3-30 title: Port wild-creature AI from GDScript to Rust (Rail-1 compliance) priority: p3 -status: stub +status: partial scope: game1 owner: warcouncil updated_at: 2026-06-27 @@ -49,20 +49,28 @@ GDScript decision system. ## Acceptance -- [ ] `mc-ai` (or `mc-combat`/`mc-turn` per infra call) exposes a deterministic wild-creature - decision fn — `decide_wild_actions(state, rng) -> Vec` — covering target-select, - step-toward, attack, and leashed roam, reusing the existing `Action`/tactical plumbing from - p0-26 where it fits. +- [x] `mc-ai` exposes a deterministic wild-creature decision fn — + `mc_ai::wild::decide_wild_actions(ctx, rng) -> Vec` (wild.rs, d08099dbe) — covering + target-select, step-toward (chase), attack (iff adjacent, per the player-tactical convention), + leash-return, city-drift, and leashed roam, reusing the existing `Action` taxonomy + `mc_core` + hex helpers + `XorShift64`. Combat resolution stays in `mc_combat::wilds` (no formula drift). - [ ] Driven from inside `mc_turn::TurnProcessor::step` as a turn phase (so the p3-29 swap needs no wild carve-out), OR via a `GdWildAiController` bridge if it must stay engine-driven — - infra decides; default is in-`step`. + infra decides; default is in-`step`. **BLOCKED ON FORK DECISION (see Notes 2026-06-27):** the + headless `GameState` has NO roaming wild-unit substrate (units are implicit-owned per + `PlayerState::units`; no `owner == -1`; wilds are modeled as `npc_buildings` lairs + + `process_fauna_encounters`). "Drive in `step`" therefore requires first BUILDING a headless + roaming-wild substrate (spawn at lairs, store, persist) that partly duplicates the encounter + system — a real scope/architecture fork vs. the allowed `GdWildAiController` bridge path. - [ ] `wild_creature_ai.gd` DELETED (or reduced to a thin bridge with zero decision logic); - `_process_wild_creatures` becomes a no-op or bridge call. -- [ ] Determinism preserved — same seed + state → same wild actions (XorShift64, per the - `p1-09` contract). Regression test alongside the p0-26 tactical suite. + `_process_wild_creatures` becomes a no-op or bridge call. **(render-gated — live turn swap.)** +- [x] Determinism preserved — same seed + state → same wild actions (XorShift64). Covered by + `wild::tests::is_deterministic_for_same_context_and_seed` (d08099dbe). - [ ] Behaviour parity: headless run shows wilds still guard lairs, attack units in radius, and - roam within leash — compared against a pre-port baseline (not just "compiles"). -- [ ] GUT green; `cargo test` green. + roam within leash — compared against a pre-port baseline. **(needs the integration above.)** +- [~] GUT green (no GDScript touched yet); `cargo test` green — `mc-ai` lib **301/0** incl. 12 + new `wild::tests::*` (target-select, chase, attack-iff-adjacent, leash return, leashed roam, + city drift, passable gating, no-movement skip, determinism, `wilds.json` config parse). ## Notes @@ -72,3 +80,31 @@ decision system**, so it must close before Rail-1 can be called complete. Sequen p3-29's event surface + dict keystone (T1-T6) so the swap has a clean target; it can land before or alongside the p3-29 swap (steps 3-5). Cross-ref `mc-combat::wilds` (resolution already Rust) to avoid duplicating combat math. + +## Progress 2026-06-27 — decision core landed; integration is a fork (owner call) + +**Done (headless, fork-neutral): `mc_ai::wild`** (d08099dbe) — the pure decision logic ported + +12 tests, `mc-ai` 301/0. This brick is identical regardless of how it's driven, so it was safe +to build before resolving the fork. + +**The fork (verified premise, not inferred):** `decide_wild_actions` needs roaming wild units to +act on. The headless `GameState` has none — `PlayerState::units` are implicit-owned (no +`owner == -1`); the headless sim models wilds as `npc_buildings` lairs (game_state.rs:575) + +`process_fauna_encounters` (probabilistic combat), NOT autonomous roamers. So the two integration +paths are materially different in cost/scope: +- **A. In-`step` (the acceptance default):** first build a headless roaming-wild substrate — + spawn `owner == -1` creatures at lairs, store + persist them on `GameState`, tick them through + `decide_wild_actions` + a movement/attack applier. Large; partly **duplicates the existing + encounter system** (wild combat near lairs already happens via encounters) → risks modeling the + same thing twice / gold-plating the headless sim. +- **B. `GdWildAiController` bridge (explicitly allowed):** the live game keeps its roaming wild + units; a thin bridge projects them into `WildContext`, calls `decide_wild_actions`, and applies + the returned `Action`s. Rail-1-clean (decision logic is Rust), no headless substrate, but the + wild pass stays engine-driven (a tracked carve-out outside `step()`) and the proof is + render-gated. + +Recommendation: **B** — it removes the GDScript *decision logic* (the actual Rail-1 violation) +without building a second wild model headless. "Everything inside `step()`" is the purist ideal, +but a headless roamer substrate duplicating encounters is the kind of disabled/parallel system the +rails warn against. Surfaced to owner; integration (A vs B) + the `.gd` deletion are the remaining +work and are render/decision-gated.