From 308a31b633baa382fc0e4ce8b655283d975c4dbb Mon Sep 17 00:00:00 2001 From: Natalie Date: Sat, 18 Apr 2026 08:06:38 -0700 Subject: [PATCH] =?UTF-8?q?fix(@projects/@magic-civilization):=20?= =?UTF-8?q?=F0=9F=90=9B=20update=20ai=20port=20status=20to=20partial?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../objectives/p0-26-ai-tactical-rust-port.md | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/.project/objectives/p0-26-ai-tactical-rust-port.md b/.project/objectives/p0-26-ai-tactical-rust-port.md index cc46360d..dbf1759a 100644 --- a/.project/objectives/p0-26-ai-tactical-rust-port.md +++ b/.project/objectives/p0-26-ai-tactical-rust-port.md @@ -2,17 +2,18 @@ id: p0-26 title: Port tactical AI from GDScript to mc-ai (Rail-1 compliance) priority: p0 -status: stub +status: partial scope: game1 owner: warcouncil -updated_at: 2026-04-17 +updated_at: 2026-04-18 evidence: - - src/game/engine/src/modules/ai/simple_heuristic_ai.gd - - src/game/engine/src/modules/ai/ai_tactical.gd - - src/game/engine/src/modules/ai/ai_military.gd - src/game/engine/src/modules/ai/ai_turn_bridge.gd - - src/simulator/crates/mc-ai/ + - src/simulator/crates/mc-ai/src/tactical/ - src/simulator/api-gdext/src/ai.rs + - src/simulator/crates/mc-core/src/gd_compat.rs + - src/simulator/crates/mc-turn/src/game_state.rs + - .local/iter/apricot-20260418_074209/ + - .project/experiments/p0-26-p1-inert.md --- ## Summary @@ -25,14 +26,20 @@ The prior CLAUDE.md "AI exception" clause was describing tech-debt, not a perman ## Acceptance -- ✗ `mc-ai::tactical` module exports `decide_tactical_actions(state: &AbstractRolloutState, player: u8, weights: &ScoringWeights, rng: &mut XorShift64) -> Vec` covering: unit move, attack target, fortify / heal decision, city-founding site pick, production queue pick, citizen tile assignment, scout vs garrison allocation. -- ✗ `api-gdext/src/ai.rs::GdAiController` exposes `decide_actions(state_json: String, player_index: i32) -> PackedStringArray` returning JSON-encoded Action records. -- ✗ `ai_turn_bridge.gd` calls `GdAiController.decide_actions` and dispatches results back to engine entities via EventBus / direct mutation — the bridge is the ONLY GDScript surface the AI touches. -- ✗ `simple_heuristic_ai.gd`, `ai_tactical.gd`, `ai_military.gd` deleted. `ai_player.gd` either becomes the new thin bridge or is also deleted (currently a 2-line stub). `personality_assigner.gd` stays (it's data-loading, not decision logic). -- ✗ `_predict_combat` (ai_tactical.gd:292) replaced by calls into `mc_combat::CombatResolver::predict_expected_damage` — no drift between prediction and resolution. -- ✗ Existing AI quality gates (p0-01 acceptance bullets: tier_peak ≥ 6, tier_peak_gap ≤ 2, total_combats ≥ 50) still pass after port. Add regression tests that pin the port to no worse than current Normal-vs-Normal 10-seed T300 baselines. -- ✗ Determinism gate (`p1-09`) unaffected — `mc-ai::tactical` uses the same `XorShift64` with per-turn seeded derivation as `mcts_tree`. -- ✗ `.project/team-leads/warcouncil.md` owned-surface list drops `src/game/engine/src/modules/ai/*.gd` and lists only `mc-ai/` + `api-gdext/src/ai.rs` + the thin `ai_turn_bridge.gd`. +- ✓ `mc-ai::tactical` module exports `decide_tactical_actions(state: &TacticalState, weights: &ScoringWeights, rng: &mut XorShift64) -> Vec` covering all 7 action variants (MoveUnit, AttackTarget, Fortify, Heal, Scout, FoundCity, SetProduction, AssignCitizen). Signature evolved from `&AbstractRolloutState` to a richer `&TacticalState` during implementation — the 256-byte POD cannot carry hex/unit/city fidelity needed for per-unit tactical decisions. 79 unit tests (16 movement + 8 settle + 15 production + 13 citizen + 4 combat_predict + 23 integration) pass in `cargo test -p mc-ai tactical`. +- ✓ `api-gdext/src/ai.rs::GdAiController` exposes `#[func] fn decide_actions(state_json: GString, player_index: i64) -> PackedStringArray` returning JSON-encoded Action records. 3 integration tests in `tests/ai_controller.rs` green. Parse-failure → empty PackedStringArray + `godot_error!` diagnostic. +- ✓ `ai_turn_bridge.gd` calls `GdAiController.decide_actions(state_json, player.index)` per AI player each turn; `_dispatch_*` handlers dispatch each Action back to engine entities. MCTS strategic override layered above (calls `GdMcTreeController.choose_action_with_stats`). Bridge is the ONLY GDScript surface the AI touches. +- ✓ `simple_heuristic_ai.gd` (1,255 LOC), `ai_tactical.gd` (405 LOC), `ai_military.gd` (233 LOC), `ai_player.gd` (2 LOC stub) ALL DELETED. `personality_assigner.gd` retained (data-loading, not decision logic). Total AI GDScript LOC: 2,681 → 842 (69% reduction). +- ✓ `_predict_combat` replaced by `mc_combat::CombatResolver::predict_expected_damage` — extracted from `resolve()` into a shared `compute_predicted_damage` helper so zero drift between prediction and resolution. 98/98 mc-combat tests + 10-test parity sweep (predict vs resolve within ±5% / ±1 HP) green. +- 🟡 **Smoke gate PASSED; quality sub-gates PENDING**. Smoke batch `apricot-20260418_074209` (10 seeds T300, PARALLEL=10, RAYON=6, AI_GPU_ROLLOUT=false, post-fixes applied): 10/10 produced turn_stats, 10/10 E2E gate passed, 9 victories + 1 max_turns, turn range T39-T300, both players actively playing (8/10 seeds with p1 ≥ 1 city; seed 8 p1 victory T39; seed 3 p1 outbuilt p0 2-vs-1 cities at T300). **Post-port gameplay shape matches pre-port baseline (sigterm-fix-verify2-1518: T75-T299 mixed).** Post-p0-25 quality gates (tier_peak ≥ 6, tier_peak_gap ≤ 2, total_combats ≥ 50) need to be evaluated against the new batch's `turn_stats.jsonl` — scheduled as next step in the warcouncil G1 closeout. +- ✗ Determinism gate (`p1-09`) unaffected — `mc-ai::tactical` uses `XorShift64` with per-turn seeded derivation; regression suite `tactical_port_regression.rs` includes `determinism_same_state_same_output` and `determinism_ten_invocations_identical` (both green). +- ✓ `.project/team-leads/warcouncil.md` owned-surface updated per scope shift — drops `src/game/engine/src/modules/ai/*.gd` wildcard; lists only `src/tactical/` + `api-gdext/src/ai.rs` + `ai_turn_bridge.gd` + `personality_assigner.gd`. + +## Regression debug arc (2026-04-18) + +Initial smoke batches revealed a "p1 is completely inert for 300 turns" symptom (0 cities, 0 mil, 0 actions). Three hypotheses tried against decision-logic layer (AI_GPU_ROLLOUT, climate serde, is_settler kind-match) — all fixed real bugs but didn't resolve symptom. Root cause surfaced only after instrumented trace of `decide_actions` return: **movement.rs and settle.rs both emit actions for the same settler unit**; movement's MoveUnit dispatches first, settler moves, settle's FoundCity then fails the position check. Fixed by adding `can_found_city: bool` field to `TacticalUnit` (data-driven from engine) and short-circuiting classification in both modules on the flag. Plus mc-core `gd_compat` helpers to accept GDScript's float-formatted integer JSON. + +Full debug trail at `.project/experiments/p0-26-p1-inert.md` — 7 rounds, meta-lessons on instrument-first debugging. ## Non-goals