diff --git a/src/game/engine/scenes/tests/auto_play.gd b/src/game/engine/scenes/tests/auto_play.gd index ca62ed34..f855898b 100644 --- a/src/game/engine/scenes/tests/auto_play.gd +++ b/src/game/engine/scenes/tests/auto_play.gd @@ -728,81 +728,140 @@ func _manage_production(city: Variant) -> void: for u: Variant in player.units: if u.get("can_found_city") == true: has_settler = true - # Smart build order: interleave buildings with military + # Circumstance-based scoring: each turn, score candidate items and pick best var built: String = _next_building(city, player, city_count, has_settler) - if not built.is_empty(): - if built == "settler" or built == "worker": - city.add_to_queue("unit", built) - else: - city.add_to_queue("building", built) + if built.is_empty(): + built = "warrior" + var unit_ids: Array[String] = ["warrior", "settler", "worker"] + if built in unit_ids: + city.add_to_queue("unit", built) else: - city.add_to_queue("unit", "warrior") + city.add_to_queue("building", built) func _next_building(city: Variant, player: Variant, city_count: int, has_settler: bool) -> String: - ## Return the next building/unit to produce, or "" for warrior. - # Forge FIRST — doubles production from 2 to 4, accelerates everything after - if not city.has_building("forge"): - return "forge" - # Count military BEFORE economic buildings — ensures attacks start by ~turn 60 - # instead of ~turn 150. Prior build order (walls/marketplace/worker before any - # warrior) pushed first warrior to turn 51 on seed 1; enemy expanded past us. - var early_mil: int = 0 - if player != null: - for u: Variant in player.units: - if u.is_alive() and u.get("can_found_city") != true and u.get("can_build_improvements") != true: - early_mil += 1 - if early_mil < 2: - return "" # warrior — rush two before any defensive/economic building - # Then walls for defense - if not city.has_building("walls"): - return "walls" - # Marketplace for gold income — without this, warriors get disbanded from bankruptcy - if not city.has_building("marketplace"): - return "marketplace" - # Ensure a worker exists to build tile improvements (farms/mines) - var has_worker: bool = false - if player != null: - for u: Variant in player.units: - if u.get("can_build_improvements") == true and u.is_alive(): - has_worker = true - break - if not has_worker: - return "worker" - # 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 - # 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 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"], - ["marketplace", "trade_routes"], - ["barracks", "military_doctrine"], - ["monument", "ancestor_rites"], - ["castle", "fortification"], + ## Score candidates from current state; return highest. See plan: cosmic-questing-allen.md. + ## 14 factors — priorities emerge from circumstances, not prescriptive order. + var tech_req: Dictionary = { + "brewery": "brewing", "library": "scholarship", "barracks": "military_doctrine", + "monument": "ancestor_rites", "castle": "fortification", + } + var candidates: Array[String] = [ + "warrior", "forge", "walls", "marketplace", "brewery", + "library", "barracks", "monument", "castle", "settler", "worker", ] - for entry: Array in econ_buildings: - var bid: String = entry[0] - var tech: String = entry[1] - if city.has_building(bid): + var units_set: Array[String] = ["warrior", "settler", "worker"] + var scores: Dictionary = {} + for cid: String in candidates: + if not (cid in units_set) and city.has_building(cid): continue - if not tech.is_empty() and not player.has_tech(tech): + if cid in tech_req and not player.has_tech(tech_req[cid]): continue - return bid - return "" # all buildings built, produce warriors + 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"]: + 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) + 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_settler and max_pop >= 3: + _score_add(scores, "settler", 6.0) + if food_starved and own_workers == 0: + _score_add(scores, "worker", 7.0) + if unimproved_food > 0 and own_workers < city_count: + _score_add(scores, "worker", 4.0) + if gold_now < 20 and gpt < 0: + _score_add(scores, "marketplace", 7.0) + if not city.has_building("forge") and base_prod < 3: + _score_add(scores, "forge", 5.0) + if happy < -4: + _score_add(scores, "brewery", 4.0); _score_add(scores, "monument", 3.0) + if city_count >= 2 and not city.has_building("library"): + _score_add(scores, "library", 3.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) + _score_add(scores, "warrior", 1.0); _score_add(scores, "forge", 1.0) + + # Log top-3 every 20 turns — emergent strategy visibility + if _turn_count % 20 == 0 and 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] city=%s: %s" % [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 func _command_unit(unit: Variant, player: RefCounted, game_map: RefCounted) -> void: