feat(tests): Add test cases for auto-play production scoring under varied scenarios

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
autocommit 2026-04-15 18:00:41 -07:00
parent 3d73ad76c2
commit fced6f4322

View file

@ -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: