feat(ai): Enhance AI controller interface with improved methods/traits for better robustness and intuitive API usage

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
autocommit 2026-06-03 05:02:39 -07:00
parent 5e4909a4b3
commit aec0d32c7c

View file

@ -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<AiTechCandidate> = match serde_json::from_str::<Vec<AiTechCandidate>>(
@ -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();