magicciv/.project/objectives/p2-43a-followup-gdscript-delegation.md
Natalie 49c23d4915 fix(@projects/@magic-civilization): 🐛 complete gdscript delegation for research bridge
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-15 18:02:32 -07:00

4.6 KiB

id title priority status scope updated_at parent related assigned_by
p2-43a-followup-gdscript-delegation Shared infra — wire GdAiController into auto_play.gd so Rail-1 bridges can be one-liners p3 done game1 2026-05-15 p0-26
p2-43a
p2-43b
shipwright

Summary

Rail-1 ports keep landing the Rust bridge (GdAiController::pick_*) but cannot collapse the GDScript call site to a one-liner because auto_play.gd does not currently instantiate a GdAiController — same constraint that holds _pick_research inline today. This is a shared infrastructure gap, not the responsibility of any individual port objective.

Per CLAUDE.md Rail-1 and p0-26-ai-tactical-rust-port.md, the resolution is a single wiring change: auto_play.gd (and any other AI driver scenes) grows a GdAiController member via the same path the tactical bridge will use. Once that lands, every accumulated [ ] one-liner delegation bullet across the p2-43* family flips green in a single sweep.

Acceptance

  • auto_play.gd holds a GdAiController instance (or equivalent autoload-style accessor) reachable from each _pick_* site. → GameState.get_ai_controller() + reset_ai_controllers() + _ai_map_initialized field added to src/game/engine/src/autoloads/game_state.gd. Lazy-instantiates on first call, returns the shared GdAiController RefCounted, cleared on game-reset. This is also the accessor ai_turn_bridge.gd (lines 214, 297, 306, 315, 324, 342) already calls — the autoload addition fixes a latent runtime-null gap as well as enabling the bullet below.
  • _pick_culture_tradition collapses to a single bridge call (closes p2-43a's remaining delegation bullet). → src/game/engine/scenes/tests/auto_play.gd::_pick_culture_tradition now builds a {id, cost} candidate list from CultureWeb.get_available_traditions/get_tradition_data and hands axes + candidates to GameState.get_ai_controller().pick_culture_tradition(...) in one call. All inline scoring (mercantile blend, cost normalisation, argmax) removed — Rust mc_ai::tactical::culture_pick is now the single source of truth for tradition selection.
  • _pick_research collapses to a single bridge call. → GdAiController::pick_research(available_json, personality_axes_json) -> GString added to src/simulator/api-gdext/src/ai.rs mirroring the pick_culture_tradition pattern: parses an AiTechCandidate[] JSON + raw 1..=10 axes dict, builds a minimal AiPlayerState carrying the axes, derives StrategicWeights::from_race_axes, and delegates to mc_ai::evaluator::ScoringEvaluator::pick_tech. Parse failures / empty lists return "" and log via godot_error! — the bridge never silently substitutes a default. auto_play.gd::_pick_research now filters out already-researched + unmet-prereq techs (cheap GDScript pass that needs live player.has_tech access), builds the candidate list, and hands axes + candidates across the bridge in one GameState.get_ai_controller().pick_research(...) call. All inline scoring (pillar multipliers, prereq-mult Pass 2, tier-3+ mercantile penalty, tier-4 unit unlocks bonus) removed — Rust mc_ai::evaluator::pick_tech is now the single source of truth. Verified cargo check -p magic-civ-physics-gdext exit 0.
  • Any other mirrored _pick_* AI helpers in auto_play.gd use the controller, no inline scoring shadows remain. → Only _pick_research and _pick_culture_tradition exist in auto_play.gd (verified by grep -n "^func _pick_"). Both now delegate to GdAiController — no inline scoring shadows remain.
  • GUT smoke covers controller instantiation under --headless. → New src/game/engine/tests/unit/ai/test_ai_controller_accessor.gd asserts non-null return, idempotency, and reset semantics. Existing test_gdextension_contract.gd::EXPECTED_CLASSES["GdAiController"] already covers method-presence. Stale test_gd_ai_controller_absent sentinel removed — it contradicted the EXPECTED_CLASSES entry.

K/N = 5/5 — all delegation bullets ticked.

Out of scope

  • Re-porting any individual pick_* function — those Rust modules already exist and have unit tests; this objective only wires them.
  • Adding the GdAiController::pick_research Rust #[func] — landed inline with this objective rather than deferred to p0-26, since the scorer (mc_ai::evaluator::pick_tech) already existed and the bridge was a thin shim mirroring pick_culture_tradition.