fix(@projects/@magic-civilization): 🐛 resolve ai personality loading and turn processing

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-11 12:27:23 -07:00
parent 5c9800cb77
commit 7d111acb1a
4 changed files with 40 additions and 13 deletions

View file

@ -152,7 +152,7 @@ count
- ✓ `mc-player-api` no longer contains `run_scripted_ai_turn` — call site replaced. Verified Wave 4 — function fully deleted; `apply_end_turn` now calls `drive_ai_slot` which threads `project_tactical``run_ai_turn``apply_ai_action`. - ✓ `mc-player-api` no longer contains `run_scripted_ai_turn` — call site replaced. Verified Wave 4 — function fully deleted; `apply_end_turn` now calls `drive_ai_slot` which threads `project_tactical``run_ai_turn``apply_ai_action`.
- ✓ Headless harness loads `ai_personalities.json` at boot. Verified Wave-final 2026-05-11 — `claude_player_main.gd::_apply_ai_personalities` reads `res://public/games/age-of-dwarves/data/ai_personalities.json` once via `FileAccess`, parses for clan key list, deterministic slot→clan mapping (`clan_ids[ai_index % count]` over sorted clan ids), and calls new `GdGameState::set_player_personality_json(slot, clan_id, json)` per AI slot. The setter delegates JSON parsing into `mc_core::ScoringWeights::from_personality_json` (single SoT). 3-player smoke output evidence: `{"clan_id":"blackhammer","slot":1,"type":"ai_personality_assigned"}{"clan_id":"deepforge","slot":2,"type":"ai_personality_assigned"}`. Commit `2de1880db`. - ✓ Headless harness loads `ai_personalities.json` at boot. Verified Wave-final 2026-05-11 — `claude_player_main.gd::_apply_ai_personalities` reads `res://public/games/age-of-dwarves/data/ai_personalities.json` once via `FileAccess`, parses for clan key list, deterministic slot→clan mapping (`clan_ids[ai_index % count]` over sorted clan ids), and calls new `GdGameState::set_player_personality_json(slot, clan_id, json)` per AI slot. The setter delegates JSON parsing into `mc_core::ScoringWeights::from_personality_json` (single SoT). 3-player smoke output evidence: `{"clan_id":"blackhammer","slot":1,"type":"ai_personality_assigned"}{"clan_id":"deepforge","slot":2,"type":"ai_personality_assigned"}`. Commit `2de1880db`.
- ✓ `cargo check --workspace` green. Evidence: `cargo check --manifest-path src/simulator/Cargo.toml --workspace``Finished dev profile in 2.75s` (17 doc-comment warnings, 0 errors). Unblocked by p2-69 closing the api-gdext `mc_turn::snapshot` import gap (commit `be088c3ad`). `cargo test --workspace --lib` green for every owned crate (`mc-ai` 240, `mc-player-api` 74, `mc-turn` 207, `mc-observation` 24, `magic-civ-physics-gdext` 10); pre-existing `mc-flora::generation::tests::*authored*` failures are unrelated tech debt (confirmed via stash-test: failures present with no local changes on origin/main). - ✓ `cargo check --workspace` green. Evidence: `cargo check --manifest-path src/simulator/Cargo.toml --workspace``Finished dev profile in 2.75s` (17 doc-comment warnings, 0 errors). Unblocked by p2-69 closing the api-gdext `mc_turn::snapshot` import gap (commit `be088c3ad`). `cargo test --workspace --lib` green for every owned crate (`mc-ai` 240, `mc-player-api` 74, `mc-turn` 207, `mc-observation` 24, `magic-civ-physics-gdext` 10); pre-existing `mc-flora::generation::tests::*authored*` failures are unrelated tech debt (confirmed via stash-test: failures present with no local changes on origin/main).
- ⚠ Headless smoke test: 5 EndTurns vs an AI clan produces non-trivial AI action chains. **Plumbing verified end-to-end; AI behavioural inertness past turn 0 is a separate (deeper) issue.** Re-run 2026-05-12 at p2-67 Phase 11 commit `ff7198346` (TurnProcessor::step now ticks every EndTurn): Claude's view advances visibly turn-over-turn (food_stored 0→10, gold 60→100, unit_count 3→6, science_per_turn 0→42 — recorded at `apricot:/tmp/wave2-smoke-output.txt`), confirming the original Wave-final diagnosis ("bench isn't ticking, AI sees the same state every turn") was partially wrong. AI side still emits `actions_applied=1` on turn 0 per slot and `=0` on turns 1-4. Root cause is NOT a step-ticking gap; it is the bench projector's degenerate search space — `project_tactical` populates an empty unit_catalog, zero per-tile yields, and no move-cost data, so `decide_tactical_actions` returns empty after the turn-0 founding pass. Open follow-up `pX-bench-projector-enrichment` to widen the projector. Flips to ✓ once the projector has enough surface for `decide_tactical_actions` to find work past turn 0. - ⚠ Headless smoke test: 5 EndTurns vs an AI clan produces non-trivial AI action chains. **Plumbing verified end-to-end; AI behavioural inertness past turn 0 is a separate (deeper) issue.** Re-run 2026-05-12 at p2-67 Phase 11 commit `ff7198346` (TurnProcessor::step now ticks every EndTurn): Claude's view advances visibly turn-over-turn (food_stored 0→10, gold 60→100, unit_count 3→6, science_per_turn 0→42 — recorded at `apricot:/tmp/wave2-smoke-output.txt`), confirming the original Wave-final diagnosis ("bench isn't ticking, AI sees the same state every turn") was partially wrong. AI side still emits `actions_applied=1` on turn 0 per slot and `=0` on turns 1-4. Root cause is NOT a step-ticking gap; it is the bench projector's degenerate search space — `project_tactical` populates an empty unit_catalog, zero per-tile yields, and no move-cost data, so `decide_tactical_actions` returns empty after the turn-0 founding pass. Open follow-up `pX-bench-projector-enrichment` to widen the projector. Flips to ✓ once the projector has enough surface for `decide_tactical_actions` to find work past turn 0. **2026-05-11 partial update (post-p2-71)**: catalogs now plumbed (160 units, 165 buildings, clan_id + promotion weights). Turn 1 emits `actions_applied=1` on both AI slots (`Action::EnqueueBuild` of tier-1 units, varying by clan); turns 2-5 still emit `0` — root cause is no longer the projector, it's (a) single production queue saturates after turn 1, (b) militarist starter has no settler/founder so `FoundCity` never fires, (c) idle warriors find no movement target. Bullet stays ⚠ until p2-71b widens starter inventory or `decide_tactical_actions` emits fallback Fortify.
- ✓ Determinism: same `(seed, state, weights)` produces byte-identical action sequences across two runs. Verified Wave 3 `run_ai_turn_is_byte_deterministic` — JSON-string equality, not just `len()`. - ✓ Determinism: same `(seed, state, weights)` produces byte-identical action sequences across two runs. Verified Wave 3 `run_ai_turn_is_byte_deterministic` — JSON-string equality, not just `len()`.
**Status:** `partial` (8/9 ✓, 1 ⚠ conditional on p2-67 Phase 11). Harness loader bullet flipped ✓ via Wave-final 2026-05-11 (commit `2de1880db`, gdext rebuild on apricot, 2-player + 3-player smoke runs both emit `ai_personality_assigned` notification per AI slot with deterministic clan mapping). 5-EndTurn smoke bullet flips to ⚠ — plumbing verified, action depth limited to turn 0 because `TurnProcessor::step` is not wired into `apply_end_turn` (owned by p2-67 Phase 11). Will flip to ✓ when Phase 11 lands as a downstream consequence — the same smoke harness will then surface a multi-turn chain. **Status:** `partial` (8/9 ✓, 1 ⚠ conditional on p2-67 Phase 11). Harness loader bullet flipped ✓ via Wave-final 2026-05-11 (commit `2de1880db`, gdext rebuild on apricot, 2-player + 3-player smoke runs both emit `ai_personality_assigned` notification per AI slot with deterministic clan mapping). 5-EndTurn smoke bullet flips to ⚠ — plumbing verified, action depth limited to turn 0 because `TurnProcessor::step` is not wired into `apply_end_turn` (owned by p2-67 Phase 11). Will flip to ✓ when Phase 11 lands as a downstream consequence — the same smoke harness will then surface a multi-turn chain.

View file

@ -2,12 +2,12 @@
id: p2-71 id: p2-71
title: "Bench projector enrichment — make MCTS see a real tactical surface" title: "Bench projector enrichment — make MCTS see a real tactical surface"
priority: p2 priority: p2
status: open status: partial
scope: game1 scope: game1
category: simulation category: simulation
owner: simulator-infra owner: simulator-infra
created: 2026-05-12 created: 2026-05-12
updated_at: 2026-05-12 updated_at: 2026-05-11
blocked_by: [] blocked_by: []
follow_ups: [p2-67] follow_ups: [p2-67]
--- ---
@ -62,14 +62,38 @@ After enrichment, re-run the 3-player 5-EndTurn smoke. Acceptance: AI slots emit
## Acceptance ## Acceptance
- ☐ `mc-player-api::projection::project_tactical` populates `unit_catalog`, `building_catalog`, per-tile yields, `strategic_axes`, personality weights. - ☑ `mc-player-api::projection::project_tactical` populates `unit_catalog`, `building_catalog`, `strategic_axes`, personality weights (`clan_id`, `promotion_*_weight`). [evidence: `crates/mc-player-api/src/projection.rs:442-470`; tests `tactical_carries_unit_catalog_from_state`, `tactical_carries_building_catalog_from_state`, `tactical_clan_id_round_trips_through_player_state`, `tactical_promotion_weights_round_trip`]
- ☐ `BuildingsCatalog` exists (or `UnitsCatalog`'s pattern is reused). - ⚠ Per-tile yields: **deferred** — bench `TileState` carries no food/prod/gold triple (only biome label + ecology fields). The closest formula `mc_city::yield_fold::tile_yields_from_collectibles` reads collectibles, not biome→yield. p2-71-followup tracks porting a biome-yield lookup into the projector. The 5-turn smoke does not block on this — see findings below.
- ☐ `GdPlayerApi` accepts catalog handles at construction. - ☑ `BuildingsCatalog` exists as `Vec<TacticalBuildingSpec>` held on `GameState::ai_building_catalog` (mirror of `UnitsCatalog` pattern, simpler since the building catalog is consumed only by the projector — no runtime sim need). [evidence: `crates/mc-turn/src/game_state.rs:336-358`]
- ☐ 5-EndTurn smoke shows `actions_applied > 0` for AI slots on turns 1-5. - ☑ `GdPlayerApi` accepts catalog handles via setters: `set_units_catalog_json`, `set_buildings_catalog_json`, `set_difficulty_threshold_mult`, plus `unit_catalog_len` / `building_catalog_len` debug readers. [evidence: `api-gdext/src/player_api.rs`]
- ☐ AI action variants differ by personality (sample: blackhammer vs deepforge produce non-identical chains). - ⚠ 5-EndTurn smoke shows `actions_applied > 0` on AI slots **for turn 1 only**; turns 2-5 emit `actions_applied=0`. [evidence: `scripts/claude-smoke-5endturn.sh` run on apricot 2026-05-11, both AI slots emit 1 action on turn 1 (`Action::EnqueueBuild` of a tier-1 unit), zero thereafter.]
- ☐ Unit + integration tests prove the projector enrichment. - **Root cause**: `mc_ai::tactical::production::pick_for_city` skips cities with non-empty queues; once a unit is queued on turn 1, the queue stays full for ~30 turns at 1 prod/turn. The other action sources (movement, fortify, founder→FoundCity, attack) are not emitting for the bench geometry: starter units are 3 warriors clustered at the capital with no enemy contact, no settler/founder unit (the militarist starter ships only warriors), and no resource targets within scout range. Personality/threshold scoring correctly returns "no productive action" for this turn topology.
- ☐ `cargo test -p mc-player-api && cargo test -p mc-ai` green. - ☑ AI action variants differ by personality on turn 1 — both slots emit one EnqueueBuild action; the *item* picked differs by clan (blackhammer slot 1 vs goldvein slot 2 in observed run). Differentiation across turns 2-5 is moot because zero actions emit. **Follow-up gap**: a richer smoke needs an initial state with a settler unit or visible enemies.
- ☐ p2-68 acceptance bullet "smoke-non-trivial-AI-chains" flips from ⚠ to ✓. - ☑ Unit tests prove projector enrichment: 7 new tests in `crates/mc-player-api/src/projection.rs` (84/84 passing, was 77/77). Integration test for full 5-turn chain is the smoke script.
- ☑ `cargo test -p mc-player-api --lib` 84/84 green; `cargo test -p mc-ai --lib` 240/240 green; workspace `cargo check` clean.
- ⚠ p2-68 acceptance bullet "smoke-non-trivial-AI-chains" — turn 1 now non-trivial (catalogs working). Turns 2-5 still zero. Bullet stays ⚠ pending the follow-up.
## Findings (2026-05-11) — what enrichment proved
Before p2-71: ALL AI turns (0..N) emitted `actions_applied = 0`. The projector was returning empty `unit_catalog` / `building_catalog`, so `pick_for_city` had nothing to queue and `mc_ai` correctly returned an empty action chain every turn.
After p2-71: Turn 1 emits 1 action per AI slot — both slots successfully pick a tier-1 unit from the 160-entry unit catalog (via `pick_best_melee`) and queue it via `Action::EnqueueBuild`. This proves the catalog plumbing + projection are wired correctly end-to-end (GD → setter → `GameState::ai_unit_catalog` → projector → `TacticalState``pick_for_city` → AI dispatch).
The remaining zero-emission gap on turns 2-5 is **not a projector defect**. It is the combined effect of:
1. Single-slot per-city production queue blocks `EnqueueBuild` once filled.
2. Starter inventory has no settler/founder, so `FoundCity` actions never fire.
3. Bench mapgen places capitals far apart, so warrior `MoveUnit` has no productive target (no enemy contact, no resource hex within move range).
4. `Fortify` actions are not in the chain emitted by `decide_tactical_actions` for this state shape.
The right next move is a **follow-up objective** widening the starter inventory (add a settler/founder to the militarist init) or the AI's idle behaviour (emit `Fortify` for stationary military units when no movement target scores).
## p2-71 Status
**Status**: `partial`. Catalog plumbing + personality projection landed and proven. Per-tile yields deferred. 5-EndTurn smoke shows hard-stop condition on turns 2-5; documented above.
Follow-up objectives (will be filed separately):
- p2-71a — Port `mc_city::biome_yield` semantics into a `TacticalTile::yields` lookup so city placement / citizen scoring has terrain signal.
- p2-71b — Widen militarist starter inventory to include a settler/founder OR teach `decide_tactical_actions` to emit Fortify/Skip for idle military as a fallback action.
## Why this size ## Why this size

View file

@ -31,7 +31,7 @@ trap "rm -rf '$TMP'" EXIT
# Build the request stream — N end_turn acts followed by shutdown. # Build the request stream — N end_turn acts followed by shutdown.
{ {
for i in $(seq 1 "$SMOKE_TURNS"); do for i in $(seq 1 "$SMOKE_TURNS"); do
printf '{"type":"act","id":%d,"action":{"action":"end_turn"}}\n' "$i" printf '{"type":"act","id":%d,"action":{"type":"end_turn"}}\n' "$i"
done done
printf '{"type":"shutdown","id":999}\n' printf '{"type":"shutdown","id":999}\n'
} > "$TMP/in.jsonl" } > "$TMP/in.jsonl"

View file

@ -153,7 +153,10 @@ func _hydrate_player_api(num_players: int) -> void:
## JSON-serialise the Array and hand it to the new Rust setters ## JSON-serialise the Array and hand it to the new Rust setters
## (`set_units_catalog_json` / `set_buildings_catalog_json`). ## (`set_units_catalog_json` / `set_buildings_catalog_json`).
func _apply_ai_catalogs() -> void: func _apply_ai_catalogs() -> void:
var AiTurnBridgeState: Script = load("res://src/game/engine/src/modules/ai/ai_turn_bridge_state.gd") # Project root is mounted at `src/game/` (see claude-player-server.sh
# `--path` arg), so the bridge module's `res://` form drops the
# `src/game/` prefix.
var AiTurnBridgeState: Script = load("res://engine/src/modules/ai/ai_turn_bridge_state.gd")
if AiTurnBridgeState == null: if AiTurnBridgeState == null:
_emit_protocol_error("could not load AiTurnBridgeState — AI catalogs empty") _emit_protocol_error("could not load AiTurnBridgeState — AI catalogs empty")
return return