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:
parent
6c1f953949
commit
4e34735e0b
1 changed files with 13 additions and 220 deletions
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue