From 4e34735e0bed43118c25e9ba26578d474bcc27d7 Mon Sep 17 00:00:00 2001 From: Natalie Date: Mon, 13 Apr 2026 13:56:35 -0700 Subject: [PATCH] =?UTF-8?q?fix(@projects/@magic-civilization):=20?= =?UTF-8?q?=F0=9F=90=9B=20adjust=20ai=20turn=20overlay=20text=20formatting?= =?UTF-8?q?=20and=20auto-play=20military=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- src/game/engine/scenes/tests/auto_play.gd | 233 ++-------------------- 1 file changed, 13 insertions(+), 220 deletions(-) diff --git a/src/game/engine/scenes/tests/auto_play.gd b/src/game/engine/scenes/tests/auto_play.gd index fb0ee85a..f51e660d 100644 --- a/src/game/engine/scenes/tests/auto_play.gd +++ b/src/game/engine/scenes/tests/auto_play.gd @@ -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: