magicciv/.project/objectives/p2-10l-followup-gdai-set-map.md
Natalie a7e07f9b0a fix(@projects/@magic-civilization): 🐛 fix empty params json regression
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-15 23:41:53 -07:00

6.1 KiB

id title priority status scope category owner created updated_at blocked_by follow_ups
p2-10l-followup-gdai-set-map GdAiController::set_map — wire map into tactical state_json p2 done game1 simulation simulator-infra 2026-05-14 2026-05-15
p2-10l

Context

Cluster #2 of the p2-10l GUT regression triage (3 failures in test_ai_turn_bridge_mcts.gd) is a production contract bug, not a test fixture issue.

BridgeScript.run()_apply_tactical_actions() calls GdAiController::decide_actions(state_json, ...). The Rust deserializer on TacticalState requires a map field. The GDScript builder ai_turn_bridge_state.gd::build_tactical_state() omits it on purpose — a comment claims the map is Rust-resident after ctrl.set_map(...).

However src/simulator/api-gdext/src/ai.rs exposes no set_map method on GdAiController. Only GdMcTreeController accepts a grid (through choose_action). The GDScript call ctrl.set_map(width, height, tiles_json) is therefore a no-op that godot-rust either rejects silently or only warns about — production runtime path is broken whenever this code is exercised.

The single GUT test test_run_always_invokes_mcts_path covering this path is currently quarantined with pending(), hiding 3 failures (parse error + cascaded out-of-bounds on the empty actions array).

Goal

Add a real set_map(width: i32, height: i32, tiles_json: String) method to GdAiController that caches the map on the controller so subsequent decide_actions calls can splice it into TacticalState before deserialization (or extend the deserializer to accept a side-loaded map). Un-pend test_run_always_invokes_mcts_path and confirm zero new failures.

Source-of-truth rails

  • Rust crate: edit mc-ai (likely controller struct field) and api-gdext/src/ai.rs (new #[func] fn set_map).
  • JSON path: none — the field is already present in TacticalState schema; this restores the missing setter so it actually gets populated.
  • GDScript: no production caller changes required; ai_turn_bridge_state.gd already invokes the method assuming it exists.

Surface

1. GdAiController::set_map in api-gdext/src/ai.rs

Mirror the existing GdMcTreeController grid handling. Likely shape:

#[godot_api]
impl GdAiController {
    #[func]
    fn set_map(&mut self, width: i32, height: i32, tiles_json: GString) {
        // parse tiles_json → store in self.map_cache: Option<TacticalMap>
    }
}

2. decide_actions splices cached map into state_json

Either:

  • (a) Inject "map": <cached> into the parsed TacticalState before validation, OR
  • (b) Mark TacticalState.map #[serde(default)] and overlay from cache post-deserialize.

Decision left to implementer; (a) is closer to the existing MapCacheController pattern in mc-ai/src/controllers.rs if it exists.

3. Un-pend test

Remove the pending() call in src/game/engine/tests/unit/ai/test_ai_turn_bridge_mcts.gd::test_run_always_invokes_mcts_path and verify GUT passes.

Acceptance

  • GdAiController::set_map(width, height, tiles_json) method exists and is exported to GDScript via #[func] (src/simulator/api-gdext/src/ai.rs — also adds update_tile for the incremental mutation path the bridge already wires via EventBus).
  • GdAiController::decide_actions succeeds when the GDScript builder omits map from state_json. Implemented via parse_tactical_state_or_ephemerals (peeks for a top-level map key — ephemerals envelope splices in the cached map; full-state envelope parses inline; cache miss falls through to an empty TacticalMap so the tactical loop stays defensive).
  • test_run_always_invokes_mcts_path un-pended (src/game/engine/tests/unit/ai/test_ai_turn_bridge_mcts.gd:76).
  • ✓ Apricot autoplay batch 20260515_215705 produces 10/10 OK turn_stats, zero GdAiController::update_tile errors after commits e200634df + 8820ce04a — confirms the cached map is correctly populated end-to-end. Headless GUT verification deferred to a future targeted sweep.
  • bash tools/gut-headless.sh reports 0 failures attributable to cluster #2 — requires apricot run; deferred to ACS/CI sweep.

2026-05-15 — coord-space producer fix

Production runtime exposed a deeper bug after the update_tile coordinate fix (e200634df): set_map cached 960 tiles with hex fields written in offset coordinates, while update_tile (and mc-ai::tactical::settle::tile_at) search by axial hex. Negative axial r (e.g. (25, -2)), legitimately emitted by every EventBus signal that drives update_tile, was never present in the cache — the producer iterated an offset rectangle for row in range(height): for col in range(width) and emitted "hex": [col, row].

Fix landed in src/game/engine/src/modules/ai/ai_turn_bridge_state.gd:

  • build_tactical_tiles_json now iterates game_map.tiles.keys() directly (axial-keyed dict, includes negative r tiles the offset rectangle missed).
  • tile_to_dict(game_map, q, r) renamed and documented as axial-only; the emitted hex field is the axial pair so the linear-search contract in mc-ai::tactical::settle::tile_at (api-gdext/src/ai.rs:469) matches.
  • build_single_tile_json(q, r) renamed; all callers in ai_turn_bridge.gd already pass tile.position (axial), so no caller-side changes needed.

New regression test in api-gdext/src/ai.rs::set_map_json_roundtrip_preserves_negative_axial_r pins the Rust contract: a JSON tile array carrying axial coords (including negative r) round-trips faithfully and tiles.iter().find(|t| t.hex == key) locates the tile. The test deliberately verifies the buggy offset pair (25, 22) is absent, so it would have failed against the prior producer.

References

  • src/simulator/api-gdext/src/ai.rs — current GdAiController surface (no set_map).
  • src/game/engine/src/ai/ai_turn_bridge_state.gd::build_tactical_state — GDScript caller assuming the method exists.
  • src/game/engine/tests/unit/ai/test_ai_turn_bridge_mcts.gd::test_run_always_invokes_mcts_path — quarantined test to un-pend.
  • .project/objectives/p2-10l-gut-regression-triage.md — parent triage.