docs(@projects/@magic-civilization): 🐺 p3-30 — decision core done; integration is a verified fork

stub → partial. Acceptance #1 (decide_wild_actions) + #4 (determinism) ✓ with
cited evidence (95a2e580b, mc-ai 301/0). Record the verified premise that the
headless GameState has no roaming wild-unit substrate (units implicit-owned,
wilds = lairs + encounters), making the in-step path a substrate-build that
duplicates encounters vs. the allowed GdWildAiController bridge. Recommend the
bridge; integration + .gd deletion stay render/decision-gated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-06-27 06:53:39 -04:00
parent 95a2e580bc
commit e477784731

View file

@ -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<Action>` — 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<Action>` (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.