From aec0d32c7c337a41108c9a19a1010bc04cc41e0d Mon Sep 17 00:00:00 2001 From: autocommit Date: Wed, 3 Jun 2026 05:02:39 -0700 Subject: [PATCH] =?UTF-8?q?feat(ai):=20=E2=9C=A8=20Enhance=20AI=20controll?= =?UTF-8?q?er=20interface=20with=20improved=20methods/traits=20for=20bette?= =?UTF-8?q?r=20robustness=20and=20intuitive=20API=20usage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- src/simulator/api-gdext/src/ai.rs | 37 ++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/src/simulator/api-gdext/src/ai.rs b/src/simulator/api-gdext/src/ai.rs index 3d319b1d..71169988 100644 --- a/src/simulator/api-gdext/src/ai.rs +++ b/src/simulator/api-gdext/src/ai.rs @@ -676,21 +676,30 @@ impl GdAiController { } } - /// p2-43a-followup (5th bullet) / p0-26 — pick the best research tech from - /// a prereq-filtered candidate list. + /// p0-26b / p0-26 — pick the best research tech (Rail-1). + /// + /// `available_json` is a JSON array of the **full** `AiTechCandidate` tree + /// (`{"id", "pillar", "cost", "tier", "requires", "max_unit_tier", + /// "already_researched", "unlocks_buildings", "unlocks_units"}`). The caller + /// passes *every* tech, NOT a prereq-filtered subset: `pick_tech` runs all + /// three GDScript passes itself — raw scoring, the prerequisite-of-high-value + /// boost (which needs the techs that a candidate-only contract would filter + /// out), and the availability filter (`already_researched` + `requires`). /// - /// `available_json` is a JSON array of `AiTechCandidate` records - /// (`{"id", "pillar", "cost", "tier", "unlocks_buildings", "unlocks_units"}`). - /// The GDScript caller (`auto_play.gd::_pick_research`) is expected to - /// pre-filter to techs the player has not yet researched and whose - /// prerequisites are met; this bridge does not re-check prereqs. /// `personality_axes_json` is the raw 1..=10 strategic-axes dict for the /// clan (the body of `ai_personalities.json[clan_id].strategic_axes`); /// missing/extra keys fall through to the neutral default of 5. /// - /// Returns the picked tech id, or an empty string when the list is empty - /// or on parse failure (logged via `godot_error!`). The bridge never - /// silently substitutes a default. + /// `research_behind` is the p1-29 catch-up observation: `true` when this + /// player's tech ceiling is ≥2 eras behind the highest opponent's. The + /// cross-player comparison and the `MC_DISABLE_CATCHUP_RESEARCH` kill switch + /// are resolved GDScript-side (Rail-1 split: GDScript observes, Rust decides); + /// `true` makes `score_tech` boost military/metallurgy ×1.5 and waive the + /// tier-3 mercantile penalty. + /// + /// Returns the picked tech id, or an empty string when the list is empty, + /// every tech is researched/blocked, or on parse failure (logged via + /// `godot_error!`). The bridge never silently substitutes a default. /// /// Scoring is delegated to [`mc_ai::evaluator::ScoringEvaluator::pick_tech`] /// — the single source of truth for tech selection (Rail-1). @@ -699,6 +708,7 @@ impl GdAiController { &self, available_json: GString, personality_axes_json: GString, + research_behind: bool, ) -> GString { let avail_src = available_json.to_string(); let candidates: Vec = match serde_json::from_str::>( @@ -734,12 +744,13 @@ impl GdAiController { } }; - // Build a minimal AiPlayerState carrying axes; other fields default - // (gold=0, threat_level=0, no luxury bonuses) — matches the - // information the GDScript inline scorer historically had access to. + // Build a minimal AiPlayerState carrying axes + the catch-up flag; + // other fields default (gold=0, threat_level=0, no luxury bonuses) — + // matches the information the GDScript inline scorer had access to. let mut state = AiPlayerState::default(); state.strategic_axes = axes.clone(); state.is_researching = false; + state.research_behind = research_behind; let strategic = StrategicWeights::from_race_axes(&axes); let evaluator = ScoringEvaluator::default();