fix(@projects/@magic-civilization): 🐛 adjust ai turn overlay text formatting and auto-play military logic

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-04-13 13:56:35 -07:00
parent 6c1f953949
commit 4e34735e0b

View file

@ -709,64 +709,7 @@ func _play_turn() -> void:
var city_pos: Vector2i = player.cities[0].position if not player.cities.is_empty() else Vector2i.ZERO
var intel: Dictionary = _get_enemy_intel()
var enemy_mil: int = intel.get("military", 0)
var attack_score: float = 0.0
var consolidate_score: float = 0.0
var p_idx: int = player.index
var my_kills: int = int(_stats[p_idx].get("kills", 0)) if _stats.has(p_idx) else 0
var my_losses: int = int(_stats[p_idx].get("units_lost", 0)) if _stats.has(p_idx) else 0
var kill_ratio: float = float(my_kills) / maxf(1.0, float(my_losses))
if kill_ratio > 1.0:
attack_score += 5.0 * kill_ratio
if military_count > enemy_mil:
attack_score += 3.0
var enemy_city_nearby: bool = false
var enemy_city_wounded: bool = false
for p_scan: Variant in GameState.players:
if p_scan.index == player.index:
continue
for c_scan: Variant in p_scan.cities:
if c_scan.hp < c_scan.max_hp * 0.5:
enemy_city_wounded = true
for u_scan: Variant in units_snapshot:
if not u_scan.is_alive() or u_scan.get("can_found_city") == true:
continue
if u_scan.get("can_build_improvements") == true:
continue
if HexUtilsScript.hex_distance(u_scan.position, c_scan.position) <= 8:
enemy_city_nearby = true
if enemy_city_nearby:
attack_score += 4.0
if enemy_city_wounded:
attack_score += 6.0
var my_captures: int = int(_stats[p_idx].get("cities_captured", 0)) if _stats.has(p_idx) else 0
if _turn_count > 100 and my_captures == 0:
attack_score += 3.0
var own_threatened: bool = false
for c_own: Variant in player.cities:
for p_en: Variant in GameState.players:
if p_en.index == player.index:
continue
for u_en: Variant in p_en.units:
if u_en.is_alive() and HexUtilsScript.hex_distance(u_en.position, c_own.position) <= 4:
own_threatened = true
if own_threatened:
consolidate_score += 5.0
var gpt_now: int = int(player.get("gold_per_turn")) if player.get("gold_per_turn") != null else 0
if gpt_now < -3:
consolidate_score += 3.0
if military_count < 2:
consolidate_score += 4.0
if _attack_commitment_turns <= 0 and attack_score > consolidate_score:
_attack_commitment_turns = 5
var should_attack: bool = _attack_commitment_turns > 0
if _attack_commitment_turns > 0:
_attack_commitment_turns -= 1
if should_attack:
if military_count >= 4:
# ATTACK PHASE: lock onto one target and march until it's destroyed
if _locked_target == Vector2i(-1, -1):
_locked_target = _find_attack_target(player)
@ -957,27 +900,26 @@ func _next_building(city: Variant, player: Variant, city_count: int, has_settler
# Forge FIRST — doubles production from 2 to 4, accelerates everything after
if not city.has_building("forge"):
return "forge"
# Then walls
# Then walls for defense
if not city.has_building("walls"):
return "walls"
# One warrior before expanding
# Count military units
var military: int = 0
for u: Variant in player.units:
if u.is_alive() and u.get("can_found_city") != true:
military += 1
# Maintain military: at least 1 warrior per city
if military < city_count:
# Need at least 2 warriors before expanding
if military < 2:
return "" # build warrior
# Expand to 3 cities
if city_count < 3 and not has_settler:
return "settler"
# Alternate: build next available building, then warrior, repeat
# Check how many buildings this city has vs warriors the player has
var city_buildings: int = city.buildings.size()
# If we have more buildings than warriors, build a warrior
if military <= city_buildings and military < city_count * 3:
return "" # build warrior
# Otherwise build next available building
# Alternate: build one economic building per 2 warriors
# This prevents the infrastructure trap where we build for 100 turns before any military
if military < city_count * 3:
# Build warriors until we have enough to attack
return "" # warrior
# One economic building, then back to warriors
var econ_buildings: Array[Array] = [
["brewery", "brewing"],
["library", "scholarship"],
@ -993,157 +935,8 @@ func _next_building(city: Variant, player: Variant, city_count: int, has_settler
continue
if cid in tech_req and not player.has_tech(tech_req[cid]):
continue
if cid in units_set:
var udata: Dictionary = DataLoader.get_unit(cid)
var req_res: String = str(udata.get("requires_resource", ""))
if req_res != "" and req_res != "null":
if not BuildableHelperScript.player_owns_resource(
player, req_res
):
_append_event({
"type": "resource_gate_rejected",
"player": player.index,
"city": city.city_name,
"unit": cid,
"resource": req_res,
})
continue
scores[cid] = 0.0
# State gathering
var intel: Dictionary = _get_enemy_intel()
var enemy_mil: int = int(intel.get("military", 0))
var own_mil: int = 0
var own_workers: int = 0
for u: Variant in player.units:
if not u.is_alive() or u.get("can_found_city") == true:
continue
if u.get("can_build_improvements") == true:
own_workers += 1
else:
own_mil += 1
var gpt: int = int(player.gold_per_turn) if player.get("gold_per_turn") != null else 0
var happy: int = int(player.happiness) if player.get("happiness") != null else 0
var gold_now: int = int(player.gold) if player.get("gold") != null else 0
var max_pop: int = 0
for oc: Variant in player.cities:
if int(oc.population) > max_pop:
max_pop = int(oc.population)
var near_enemy: int = 99
var any_siege: bool = false
for p: Variant in GameState.players:
if p.index == player.index:
continue
for eu: Variant in p.units:
if not eu.is_alive() or eu.get("can_found_city") == true:
continue
var d_self: int = HexUtilsScript.hex_distance(city.position, eu.position)
if d_self < near_enemy:
near_enemy = d_self
for oc2: Variant in player.cities:
if HexUtilsScript.hex_distance(oc2.position, eu.position) <= 3:
any_siege = true
var gm: RefCounted = GameState.get_game_map()
var base_prod: int = 0
if gm != null:
var cy: Dictionary = city.get_yields(BuildableHelperScript.build_tile_yields_json(city, gm))
base_prod = int(cy.get("production", 0))
var unimproved_food: int = 0
var food_starved: bool = false
for tp: Vector2i in city.owned_tiles:
var tl: Resource = gm.get_tile(tp) if gm != null else null
if tl == null:
continue
if str(tl.get("improvement")) == "" and tl.biome_id in ["grassland", "plains", "forest", "boreal_forest", "jungle", "tundra"]:
unimproved_food += 1
for tp2: Vector2i in city.get_worked_tiles():
var tl2: Resource = gm.get_tile(tp2) if gm != null else null
if tl2 != null and int(tl2.get_yields(player.index).get("food", 0)) == 0:
food_starved = true
# 14 factors (see plan table). Weights tuned against smoke regressions:
# forge gets strong priority early (its prod multiplier gates everything),
# worker is gated by population >=2 so starving-mountain starts don't pick
# worker they can't afford.
if enemy_mil >= own_mil and near_enemy <= 6:
_score_add(scores, "warrior", 8.0); _score_add(scores, "walls", 4.0)
if own_mil < enemy_mil:
_score_add(scores, "warrior", 5.0)
if any_siege:
_score_add(scores, "walls", 10.0); _score_add(scores, "warrior", 3.0)
if own_mil == 0 and _turn_count <= 30:
_score_add(scores, "warrior", 6.0)
if city_count < 3 and not has_founder and max_pop >= 3:
_score_add(scores, "founder", 6.0)
if food_starved and own_workers == 0 and int(city.population) >= 2:
_score_add(scores, "worker", 7.0)
if unimproved_food > 0 and own_workers < city_count and int(city.population) >= 2:
_score_add(scores, "worker", 4.0)
# First worker is a strong priority once pop can spare the food —
# tile improvements are the primary long-term yield multiplier.
if own_workers == 0 and int(city.population) >= 2:
_score_add(scores, "worker", 10.0)
# Keep worker supply replenished: one worker per city after first.
if own_workers < city_count and int(city.population) >= 3:
_score_add(scores, "worker", 3.0)
if gold_now < 20 and gpt < 0:
_score_add(scores, "marketplace", 7.0)
if not city.has_building("forge"):
# Forge is the universal production multiplier; prioritize strongly when absent.
var forge_bonus: float = 9.0 if base_prod < 3 else 6.0
_score_add(scores, "forge", forge_bonus)
if happy < -4:
_score_add(scores, "temple", 5.0); _score_add(scores, "colosseum", 4.0)
_score_add(scores, "ale_hall", 3.5); _score_add(scores, "bathhouse", 4.5)
if happy < -8:
_score_add(scores, "ale_hall", 1.5); _score_add(scores, "bathhouse", 1.5)
# Library: strong priority once scholarship is researched — science/turn
# from a single city starts at 1 so a +2 library doubles research pace.
# Gate only on tech, not on city count, so first city still builds it.
if player.has_tech("scholarship") and not city.has_building("library"):
_score_add(scores, "library", 8.0)
if own_mil >= 4 and not city.has_building("barracks"):
_score_add(scores, "barracks", 3.0)
if city.has_building("walls") and _turn_count > 150 and city_count >= 3:
_score_add(scores, "castle", 3.0)
# Monument: cheap early culture for border expansion. After forge lands,
# push hard to ensure it beats library/walls and actually gets built —
# culture expansion is a major yield multiplier through more worked tiles.
if not city.has_building("monument"):
var monument_w: float = 10.0 if city.has_building("forge") else 2.5
if _turn_count < 60 and not any_siege:
monument_w += 4.0
_score_add(scores, "monument", monument_w)
# Siege sustain: while committed to ATTACK, missing stack slots near the
# target dominate everything else — +15 per missing warrior below 3.
if _in_attack_phase and _active_attack_mil_count < 3:
var missing: int = 3 - _active_attack_mil_count
_score_add(scores, "warrior", 15.0 * float(missing))
_score_add(scores, "warrior", 1.0); _score_add(scores, "forge", 1.0)
# Log top-3 each time production is selected — emergent strategy visibility
if not scores.is_empty():
var ranked: Array = scores.keys()
ranked.sort_custom(func(a: String, b: String) -> bool: return scores[a] > scores[b])
var top: Array = []
for i: int in range(min(3, ranked.size())):
top.append("%s=%.1f" % [ranked[i], float(scores[ranked[i]])])
print(" [SCORE] t%d city=%s: %s" % [_turn_count, city.city_name, ", ".join(top)])
# Pick max; fall back to warrior if all zero
var best_id: String = "warrior"
var best_score: float = 0.0
for k: String in scores:
var s: float = scores[k]
if s > best_score:
best_score = s
best_id = k
return best_id
func _score_add(scores: Dictionary, key: String, amount: float) -> void:
if key in scores:
scores[key] = float(scores[key]) + amount
return bid
return "" # all buildings built, produce warriors
func _command_unit(unit: Variant, player: RefCounted, game_map: RefCounted) -> void: