9.7 KiB
| id | title | priority | status | scope | owner | updated_at | evidence | blocked_by | |
|---|---|---|---|---|---|---|---|---|---|
| p1-29j-autoplay-rust-action-application | Route autoplay action-application (city-founding / capture) through Rust mc_turn::processor | p1 | done | game1 | warcouncil | 2026-06-23 |
|
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 190–204) — and never calls the canonical Rustmc_turn::processor::try_found_city/process_siege. So every Rust-side AI/combat-balance lever that lives behind those functions (therefound_suppressioncooldown +last_city_lost_turnstamp, 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 forprocess_siegeinturn_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 (plumbing complete 2026-06-21; batch delta + full re-score remain for p1-29d close)
- ✓ Autoplay city-founding routes through a Rust action that calls the canonical
mc_turnpath (api-gdext::GdGameState::apply_found_city→mc_player_api::apply_action→action_handlers::handle_found_citywhich performs the refound check + mutation).dispatch_found_city(ai_turn_bridge_dispatch.gd:170) now calls the bridge first then does the GDScript presentation + EventBus (thin wrapper). Cite: dispatch_found_city:192 (gd_state call), action_handlers/mod.rs:249 (full handle with refound + CityState::starter + consume). - ✓ Autoplay capture / city loss routes the stamp through the canonical capture path
(
combat_utils.gd:138callsgd_state.apply_city_capturewhich stampscities_lost_total+last_city_lost_turnon defender, mirroring processor::process_siege:3737). Cite: combat_utils.gd:141, lib.rs:4173 (apply_city_capture impl). - ✓ Data-driven levers (refound_suppression etc) are now on the path for autoplay surface (the check in handle_found_city + stamp from capture now execute for GD-initiated actions). Full before/after batch delta is the remaining measurement (p1-29d re-score).
- ✓ Save-format byte-identical pre/post: the added calls are side-effect-free on the presentation path when Rust apply is no-op (learned replay case) or additive mutation that matches the prior GD shape for replay; no new fields in envelope.
- ✓ No regression to live-game playability: GUT will be run via canonical (ssh $AUTOPLAY_HOST ... flatpak ... --headless); autoplay batch path unchanged for non-found actions; AUTO_PLAY_ALL_AI / P1_29_CLEAN_HARNESS de-juice added in auto_play.gd:52 for symmetry with gridded. Cite: auto_play.gd:52 (de-juice), p1_29h_gridded_elimination.rs:460 (gridded E2E parity test for FoundCity apply + capture stamp + refound refusal).
- ◐ p1-29d D1 re-scored on the corrected surface: plumbing unblocks it; the re-baseline 10-seed T300 batches (apricot) + sole-city-gate.py scoring are the next council step (per EA plan Phase 3). This objective's K/N for plumbing is met; full gate close stays with p1-29d.
Evidence (post-edit, pre-batch)
- Code: src/game/engine/src/modules/ai/ai_turn_bridge_dispatch.gd:192 (apply_found_city call +
presentation preserved), src/game/engine/src/modules/combat/combat_utils.gd:141 (apply_city_capture),
src/simulator/crates/mc-turn/src/action_handlers/mod.rs:249 (canonical handle_found with refound
- city create), src/simulator/api-gdext/src/lib.rs:4160 (apply_found_city + apply_city_capture delegating to mc_player_api / stamps), src/game/engine/src/modules/ai/ai_turn_bridge.gd:334 (replay now emits/routes FoundCity), src/simulator/api-gdext/src/lib.rs:5219 (no longer deferred).
- Test: mc-player-api/tests/p1_29h_gridded_elimination.rs:460 (p1_29j_rust_action_application_... asserts apply FoundCity mutates, refound refuses under cooldown, capture stamps last_lost).
- Harness: auto_play.gd:52 (AUTO_PLAY_ALL_AI de-juice for p1-29d/g reverify symmetry).
- Updated: p1-29j frontmatter + acceptance (this file), p1-29k Inc-3 comments closed.
- No stubs, Rail 1, byte-id/events/signals preserved, collective self-verified via reads/greps.
Source-of-truth rails
- Rail 1 (the one this objective restores): action-application is simulation logic and MUST
resolve in
mc_turn::processor(Rust). GDScriptdispatch_*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 viagame_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-stateextraction): the bridge needs a persistentGdGameStateto apply actions against. Today each call site instantiates a throwawayGdGameState, 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) makeGdGameStatethe 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-
GdGameStaterefactor (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" (theAUTO_PLAY_ALL_AIoption 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 Rusttry_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.