feat(@projects/@magic-civilization): auto-pick research for idle ai players

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-16 21:47:25 -07:00
parent f9100f9e29
commit 9fcdbb6c95

View file

@ -139,8 +139,19 @@ func _process_research(player: RefCounted) -> void: # Player
## to Rust `GdTechWeb::process_research` (warcouncil p1-39 port,
## 2026-04-27). GDScript only assembles input JSON + applies
## completion side-effects (tech_researched signal, resource reveals).
##
## Auto-pick fallback: when a player has no current research target
## (just completed one, or never picked one — the latter case is the
## bug we hit on huge-map 5-clan runs where AI players P1-P4 stalled
## at techs=1 forever because nothing ever called `_pick_research`
## for them; auto_play.gd only picks for its impersonated current
## player slot). Re-pick from the bridge using the same scorer the
## headless autoplay uses, so AI players progress through eras
## naturally instead of sitting idle.
if player.researching.is_empty():
return
_auto_pick_research(player)
if player.researching.is_empty():
return
# Per-yield difficulty multiplier (composed by GameState).
# p1-29 cycle 4: research-yield catch-up boost. When this player is at least
@ -202,6 +213,62 @@ func _process_research(player: RefCounted) -> void: # Player
_check_resource_reveals(completed_tech, player.index)
func _auto_pick_research(player: RefCounted) -> void:
## Pick a research target for `player` via the Rust bridge scorer.
##
## Mirrors the candidate-list construction from
## `auto_play.gd::_pick_research`. Runs for every player on every turn
## that finds `researching` empty — so AI players P1..P4 (which
## auto_play does NOT impersonate via `_play_turn`) actually advance
## through eras. Without this, only the auto_play-impersonated slot
## ever picked a tech and the rest sat at techs=1 forever (huge-map
## batch `20260516_212826`).
var ctrl: RefCounted = GameState.get_ai_controller()
if ctrl == null:
return
var candidates: Array = []
for tech: Dictionary in DataLoader.get_all_techs():
var tid: String = str(tech.get("id", ""))
if tid.is_empty() or player.has_tech(tid):
continue
var met: bool = true
for req: Variant in tech.get("requires", []):
if not player.has_tech(str(req)):
met = false
break
if not met:
continue
var unlocks: Dictionary = tech.get("unlocks", {})
var unlock_buildings: Array = []
for bid: Variant in unlocks.get("buildings", []):
unlock_buildings.append(str(bid))
var unlock_units: Array = []
for uid: Variant in unlocks.get("units", []):
unlock_units.append(str(uid))
candidates.append({
"id": tid,
"pillar": str(tech.get("pillar", "")),
"cost": maxi(int(tech.get("cost", 1)), 1),
"tier": int(tech.get("tier", 0)),
"unlocks_buildings": unlock_buildings,
"unlocks_units": unlock_units,
})
if candidates.is_empty():
return
var clan_id: String = str(player.get("clan_id") if player.get("clan_id") != null else "")
var axes: Dictionary = {}
if not clan_id.is_empty():
var personality: Dictionary = DataLoader.get_ai_personality(clan_id)
if personality != null and not personality.is_empty():
axes = personality.get("strategic_axes", {})
var best_id: String = ctrl.pick_research(
JSON.stringify(candidates), JSON.stringify(axes)
)
if not best_id.is_empty():
player.researching = best_id
player.research_progress = 0
func _check_resource_reveals(completed_tech: String, player_index: int) -> void:
TurnProcessorHelpersScript._check_resource_reveals(completed_tech, player_index)