feat(engine): ✨ update auto-play building selection logic
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
3097c3f982
commit
e6f83fd5f0
1 changed files with 107 additions and 108 deletions
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue