docs(p1-29j): 📋 File autoplay→Rust action-application objective (p1-29d unblock)

p1-29i's full-game validation pinned the root cause that gates all of Wave-C
AI convergence: the autoplay AI applies city-founding/capture via GDScript
(`ai_turn_bridge_dispatch.gd:170 dispatch_found_city` → CityScript.new() →
EventBus.city_founded.emit), NEVER calling Rust `mc_turn::processor::
try_found_city` / `process_siege` where the balance levers live. So every
Rust-side founding/siege lever (p1-29i refound_suppression, and successors) is
inert by construction on the p1-29d gate surface — a Rail-1 violation on the
action-application seam.

Spec-only (Rail-1, scope game1, owner warcouncil, status stub). Large separate
effort, gated on p2-65 (mc-state gives the bridge a persistent GdGameState to
apply actions against). References p1-29d Findings A/2/3 and p1-29i's terminal
result. Added to objectives README dashboard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
autocommit 2026-06-04 19:00:19 -07:00
parent 0bace0e6ce
commit 39bf244f74
2 changed files with 123 additions and 0 deletions

View file

@ -124,6 +124,7 @@
| [p1-29a](p1-29a-last-stand-defense.md) | 🟡 partial | Last-stand defense — combat-strength multiplier when defender is at last city | [combat-dev](../team-leads/combat-dev.md) | 2026-05-13 |
| [p1-29b](p1-29b-tier-gap-ai-quality.md) | ✅ done | "AI tech tier gap — structural research path quality (low-pop AI fails to reach t1+)" | [warcouncil](../team-leads/warcouncil.md) | 2026-05-07 |
| [p1-29c](p1-29c-sole-city-research-path.md) | 🔴 stub | "Sole-city research path — lift trailing AI from tier_peak=1 to ≥2" | [unassigned](../team-leads/unassigned.md) | 2026-05-13 |
| [p1-29j](p1-29j-autoplay-rust-action-application.md) | 🔴 stub | "Route autoplay action-application (city-founding / capture) through Rust mc_turn::processor — real unblock for p1-29d D1" | [warcouncil](../team-leads/warcouncil.md) | 2026-06-04 |
| [p1-30](p1-30.md) | ✅ done | "Optimize `_build_tactical_state` — 8000-tile GDScript dict-build per AI turn blocks p1-22 huge-map gate" | [warcouncil](../team-leads/warcouncil.md) | 2026-05-04 |
| [p1-31](p1-31-split-bundled-building-resources.md) | ✅ done | Split bundled `resources/buildings/<category>.json` into per-file pattern matching `resources/units/` | — | 2026-04-27 |
| [p1-32](p1-32-food-chain-buildings.md) | ✅ done | Author the two missing food/processing buildings (sawmill, herbalist) | — | 2026-05-03 |

View file

@ -0,0 +1,122 @@
---
id: p1-29j-autoplay-rust-action-application
title: "Route autoplay action-application (city-founding / capture) through Rust mc_turn::processor"
priority: p1
status: stub
scope: game1
category: architecture
owner: warcouncil
created: 2026-06-04
updated_at: 2026-06-04
blocked_by: []
relates_to: [p1-29d-p1-survival, p1-29i-refound-suppression, p1-29h-stateful-tactical-decisiveness]
---
## Why this exists
p1-29i ran the deferred full-game validation of the refound-suppression lever and resolved a
deeper architectural root cause that gates **all** of Wave-C AI-convergence work, not just that
one lever:
> The autoplay AI applies city-founding (and city capture) via **pure GDScript**
> `ai_turn_bridge_dispatch.gd:170 dispatch_found_city` (`CityScript.new()`
> `player.cities.append``EventBus.city_founded.emit`, lines 190204) — and **never calls** the
> canonical Rust `mc_turn::processor::try_found_city` / `process_siege`. So every Rust-side
> AI/combat-balance lever that lives behind those functions (the `refound_suppression` cooldown +
> `last_city_lost_turn` stamp, and by extension any future founding/siege gate) is **inert by
> construction on the p1-29d gate surface.** (Grep-verified: no GDScript refound gate /
> `last_city_lost` / cooldown exists anywhere; capture in the autoplay loop has zero matches for
> `process_siege` in `turn_processor.gd` / dispatch.)
This is the same class of trap as the `process_science` / `GdTechWeb` split p1-29d Finding §2/§3
documents: the headless Rust path and the live GDScript autoplay path are two different
implementations of the "apply this action" step, and the gate measures the GDScript one while
the levers land in the Rust one. **Rail 1 (Rust is the simulation source of truth) is violated on
the action-application seam**, and that violation is precisely what makes the AI-convergence gate
(p1-29d D1) unmovable by Rust-side levers.
This objective is the **real unblock for p1-29d D1**: until autoplay applies founding/capture
through `mc_turn::processor`, data-driven combat-balance levers (p1-29i and successors) cannot
take effect on the surface the convergence gate measures, and the AI-quality work is measuring a
GDScript shadow of the simulation rather than the simulation itself.
## Scope (author-the-spec only; do NOT implement here)
This is a **large, separate effort** — the action-application seam touches the GDScript
turn/dispatch layer, the `api-gdext` bridge surface, and the Rust `mc_turn::processor` action
handlers, and it must preserve byte-identical save format + not regress the live game's
playability. It is sequenced AFTER the p2-65 `mc-state` extraction (which gives the bridge a
persistent `GdGameState` to apply actions against, instead of the throwaway-per-call pattern). It
is filed now so the root cause is tracked and p1-29d/p1-29i can re-point their blockers at it; the
implementation is its own multi-session objective.
## The seam (verified 2026-06-04)
| Action | GDScript path (today; bypasses Rust) | Rust canonical (where levers live) |
|---|---|---|
| Found city | `ai_turn_bridge_dispatch.gd:170 dispatch_found_city``CityScript.new()``player.cities.append``EventBus.city_founded.emit` | `mc_turn::processor::try_found_city` (refound gate, `last_city_lost_turn`, event emission) |
| Capture / siege | autoplay turn loop (`turn_processor.gd` / dispatch) — direct ownership flip; **no `process_siege` call** | `mc_turn::processor::process_siege` (`cities_lost_total` increment, `last_city_lost_turn` stamp) |
| Research | GDScript `TurnManager.get_tech_web()``GdTechWeb` | `mc_turn::processor::process_science` (the p1-29d §2/§3 precedent — same split) |
The capture/siege row is the most consequential for AI convergence: p1-29h's army-lock produces
captures, but the *consequence* of a capture (loss stamp, elimination bookkeeping, refound gate)
is computed differently on the two paths.
## Acceptance (for the future implementation — not met yet; status: stub)
- ✗ Autoplay city-founding routes through a Rust action that calls `mc_turn::processor::try_found_city`
(via the `api-gdext` bridge / `mc_player_api::apply_action` surface), so `dispatch_found_city`
becomes a thin presentation wrapper over the Rust result instead of an independent reimplementation.
- ✗ Autoplay capture / city loss routes through `mc_turn::processor::process_siege` (or its bridge
equivalent), so `cities_lost_total` / `last_city_lost_turn` are stamped on the gate surface.
- ✗ Data-driven combat-balance levers behind those functions (starting with p1-29i's
`refound_suppression.cooldown_turns`) demonstrably fire on the autoplay surface — proven by a
controlled before/after full-game batch (same build, only the JSON value changed) showing a
measurable behavioural delta where p1-29i measured **none**.
- ✗ Save-format byte-identical pre/post (the action-application change must not alter on-disk shape).
- ✗ No regression to live-game playability: GUT headless suite green, autoplay 10-seed batch
completes, and the juiced-P0 asymmetry (p1-29d Finding A) is either preserved deliberately or
explicitly de-juiced as part of the `AUTO_PLAY_ALL_AI` option (p1-29d §3 option (a)).
- ✗ p1-29d D1 re-scored on the corrected surface once levers are live (this objective unblocks that
re-score; it does not itself promise convergence — that remains balance-design-paced per the
FINISH_GAME1_PLAN Wave-C risk box).
## Source-of-truth rails
- **Rail 1 (the one this objective restores):** action-application is simulation logic and MUST
resolve in `mc_turn::processor` (Rust). GDScript `dispatch_*` becomes presentation — it asks the
bridge to apply the action and renders the result; it does not compute the result.
- **Rail 2:** combat-balance levers stay data-driven (`combat_balance.json`), loaded at runtime via
`game_state.gd:224 _load_combat_balance_into``set_combat_balance_json`. This objective makes
that already-loaded config actually reach the founding/capture code path.
- **Save format:** byte-identical pre/post.
## Dependencies / sequencing
- **Gated on p2-65** (`mc-state` extraction): the bridge needs a persistent `GdGameState` to apply
actions against. Today each call site instantiates a throwaway `GdGameState`, discards it after
one Rust step, and the GDScript-side state (`player.cities`) is the real store — which is exactly
why founding/capture were reimplemented in GDScript. p2-65 + p2-72a (canonical-render-source) make
`GdGameState` the persistent store; this objective then routes mutations through it.
- **Unblocks** p1-29d D1 re-score and re-validates p1-29i (and any future founding/siege lever).
## Out of scope
- Solving AI convergence itself (p1-29d / p1-29h-Phase-2 / p1-29i) — this is the *plumbing* that
lets those levers act on the gate surface, not the balance design.
- The learned-controller track (p1-29f/g).
- The full PlayerScript/UnitScript/GameMap → thin-view-over-`GdGameState` refactor (p2-72a) — this
objective is the action-application slice, which can land incrementally on top of p2-72a's state
store.
## References
- `.project/objectives/p1-29i-refound-suppression.md` — the full-game validation that pinned this
root cause (§"Full-game validation", §"Terminal result").
- `.project/objectives/p1-29d-p1-survival.md` — gap analysis §"Findings A / 2 / 3" (the
`AUTO_PLAY_ALL_AI` option and the GDScript/Rust split); its updated blocker re-points here.
- `src/game/engine/src/modules/ai/ai_turn_bridge_dispatch.gd:170``dispatch_found_city`, the
GDScript founding path that bypasses Rust `try_found_city`.
- `src/game/engine/src/autoloads/game_state.gd:224``_load_combat_balance_into` (runtime JSON load).
- `src/simulator/crates/mc-turn/src/processor.rs``try_found_city` / `process_siege` / `process_science`,
the canonical Rust action handlers where balance levers live.