fix(@projects/@magic-civilization): 🐛 update ai port status to partial

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-04-18 08:06:38 -07:00
parent 3c0d1660f1
commit 308a31b633

View file

@ -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<Action>` 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<Action>` 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