magicciv/.project/objectives/p1-29j-autoplay-rust-action-application.md

9.7 KiB
Raw Permalink Blame History

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
.project/objectives/p1-29j-autoplay-rust-action-application.md + game-ai sub: dispatch.gd:205 apply_found_city *first* + lib.rs:4168 + handlers:248 + MCP end_turn ai_turn_completed + cargo/greps + cd=5 json:16 + history + FoundCity replay + sub 399s; K=6/6; status done post-sub

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 GDScriptai_turn_bridge_dispatch.gd:170 dispatch_found_city (CityScript.new()player.cities.appendEventBus.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_cityCityScript.new()player.cities.appendEventBus.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_turn path (api-gdext::GdGameState::apply_found_citymc_player_api::apply_actionaction_handlers::handle_found_city which 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:138 calls gd_state.apply_city_capture which stamps cities_lost_total + last_city_lost_turn on 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). 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_intoset_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:170dispatch_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.rstry_found_city / process_siege / process_science, the canonical Rust action handlers where balance levers live.