diff --git a/.project/objectives/README.md b/.project/objectives/README.md index 08bc0a01..867e46e4 100644 --- a/.project/objectives/README.md +++ b/.project/objectives/README.md @@ -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/.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 | diff --git a/.project/objectives/p1-29j-autoplay-rust-action-application.md b/.project/objectives/p1-29j-autoplay-rust-action-application.md new file mode 100644 index 00000000..a83d70b2 --- /dev/null +++ b/.project/objectives/p1-29j-autoplay-rust-action-application.md @@ -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 190โ€“204) โ€” 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.