diff --git a/src/game/engine/scenes/tests/auto_play.gd b/src/game/engine/scenes/tests/auto_play.gd index 98fcb7de..0b7671b0 100644 --- a/src/game/engine/scenes/tests/auto_play.gd +++ b/src/game/engine/scenes/tests/auto_play.gd @@ -802,136 +802,135 @@ func _manage_production(city: Variant) -> void: func _next_building(city: Variant, player: Variant, city_count: int, has_settler: bool) -> String: - ## Score candidate productions from current game state; return highest. - ## No prescriptive ordering — priorities emerge from threat/economy/growth signals. + ## 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", + ] + var units_set: Array[String] = ["warrior", "settler", "worker"] var scores: Dictionary = {} - - var mil: int = 0 - var workers: int = 0 - for u: Variant in player.units: - if not u.is_alive(): + for cid: String in candidates: + if not (cid in units_set) and city.has_building(cid): continue - if u.get("can_found_city") == true: + if cid in tech_req and not player.has_tech(tech_req[cid]): continue - if u.get("can_build_improvements") == true: - workers += 1 - else: - mil += 1 + scores[cid] = 0.0 + # State gathering var intel: Dictionary = _get_enemy_intel() var enemy_mil: int = int(intel.get("military", 0)) - var enemy_cities: int = int(intel.get("cities", 0)) - var mil_ratio: float = float(mil) / maxf(1.0, float(enemy_mil)) - var gpt: int = int(player.get("gold_per_turn")) if player.get("gold_per_turn") != null else 0 - - # Nearest enemy unit distance from this city - var near_enemy_dist: int = 99 + 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 eu.is_alive() and eu.get("can_found_city") != true: - var d: int = HexUtilsScript.hex_distance(city.position, eu.position) - if d < near_enemy_dist: - near_enemy_dist = d - - # Worked tiles with no improvement → worker has real work - var unimproved_worked: int = 0 - for tp: Vector2i in city.get_worked_tiles(): - var gm: RefCounted = GameState.get_game_map() + 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 and str(tl.get("improvement")) == "": - unimproved_worked += 1 + 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 - # ── Warrior: threat + deficit ─────────────────────────────────────── - var warrior_score: float = 0.0 - if mil < enemy_mil: - warrior_score += 6.0 * (float(enemy_mil - mil)) # outnumbered: catch up fast - if mil_ratio >= 1.5 and enemy_cities > 0: - warrior_score += 4.0 # press advantage to attack - if near_enemy_dist <= 6: - warrior_score += 5.0 + float(6 - near_enemy_dist) # close threat - if mil < 2: - warrior_score += 3.0 # minimum garrison - scores["warrior"] = warrior_score - - # ── Forge: universal production multiplier, front-loaded ──────────── + # 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_settler and max_pop >= 3: + _score_add(scores, "settler", 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) + if gold_now < 20 and gpt < 0: + _score_add(scores, "marketplace", 7.0) if not city.has_building("forge"): - scores["forge"] = 5.0 + (3.0 if mil >= 1 else 0.0) + # 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, "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) - # ── Walls: city threatened ────────────────────────────────────────── - if not city.has_building("walls"): - var wall_score: float = 0.0 - if near_enemy_dist <= 5: - wall_score += 8.0 - elif near_enemy_dist <= 10: - wall_score += 3.0 - if mil < enemy_mil: - wall_score += 2.0 - scores["walls"] = wall_score - - # ── Marketplace: gold dragging ────────────────────────────────────── - if not city.has_building("marketplace"): - var mkt: float = 2.0 - if gpt <= 0: - mkt += 6.0 - if city_count >= 2: - mkt += 2.0 - scores["marketplace"] = mkt - - # ── Granary: growth-blocked small city ────────────────────────────── - if not city.has_building("granary") and city.population < 4: - scores["granary"] = 4.0 + float(4 - city.population) - - # ── Worker: unimproved worked tiles ───────────────────────────────── - if workers < city_count: - var worker_score: float = 2.0 + 2.0 * float(unimproved_worked) - if workers == 0: - worker_score += 3.0 - scores["worker"] = worker_score - - # ── Settler: safe expansion when ahead ────────────────────────────── - if city_count < 3 and not has_settler and mil >= 2 and near_enemy_dist >= 8: - scores["settler"] = 4.0 + float(3 - city_count) * 2.0 - - # ── Tech-gated econ (only if tech researched and not built) ───────── - var gated: Array[Array] = [ - ["library", "scholarship", 3.0], - ["barracks", "military_doctrine", 3.0 + (2.0 if near_enemy_dist <= 8 else 0.0)], - ["brewery", "brewing", 2.0], - ["monument", "ancestor_rites", 2.0], - ["castle", "fortification", 3.0 + (3.0 if near_enemy_dist <= 5 else 0.0)], - ] - for entry: Array in gated: - var bid: String = entry[0] - var tech: String = entry[1] - var base: float = entry[2] - if city.has_building(bid): - continue - if not player.has_tech(tech): - continue - scores[bid] = base - - # Pick max; log top-3 every 20 turns - var best_id: String = "warrior" - var best_score: float = -INF - for k: String in scores: - var s: float = scores[k] - if s > best_score: - best_score = s - best_id = k - if _turn_count % 20 == 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] %s near_enemy=%d mil=%d/%d gpt=%+d -> %s" % [ - city.city_name, near_enemy_dist, mil, enemy_mil, gpt, ", ".join(top) - ]) + 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 + + func _command_unit(unit: Variant, player: RefCounted, game_map: RefCounted) -> void: if unit.get("can_build_improvements") == true: _command_worker(unit, player, game_map)