feat(ai): ✨ Add validation checks to prevent invalid city production decisions in simple heuristic AI
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
fb1046052c
commit
94a80efa16
1 changed files with 50 additions and 32 deletions
|
|
@ -368,7 +368,7 @@ static func _decide_production(
|
|||
var city_count: int = player.cities.size()
|
||||
|
||||
# Priority 1: Build walls if city has none (defense first)
|
||||
if not city.has_building("walls"):
|
||||
if not city.has_building("walls") and city.can_build("walls", player):
|
||||
var wdata: Dictionary = DataLoader.get_building("walls")
|
||||
if not wdata.is_empty():
|
||||
return _prod_building(city_index, "walls")
|
||||
|
|
@ -376,41 +376,55 @@ static func _decide_production(
|
|||
# Priority 2: Happiness building when unhappy
|
||||
if player.happiness < 0:
|
||||
var hb_id: String = _pick_happiness_building_id(city, player)
|
||||
if not hb_id.is_empty():
|
||||
if not hb_id.is_empty() and city.can_build(hb_id, player):
|
||||
return _prod_building(city_index, hb_id)
|
||||
|
||||
# Priority 3: Expand — build founder if fewer than 3 cities and none in progress
|
||||
if city_count < 3 and founder_count == 0 and city_index == 0:
|
||||
if (
|
||||
city_count < 3
|
||||
and founder_count == 0
|
||||
and city_index == 0
|
||||
and city.can_build("founder", player)
|
||||
):
|
||||
return _prod_unit(city_index, "founder")
|
||||
|
||||
# Priority 4: Military — maintain 2 warriors per city
|
||||
var want_military: bool = military_count < maxi(2, city_count * 2)
|
||||
if want_military:
|
||||
var unit_id: String = _pick_military_unit_id()
|
||||
var unit_id: String = _pick_buildable_military_unit_id(city, player)
|
||||
if not unit_id.is_empty():
|
||||
return _prod_unit(city_index, unit_id)
|
||||
|
||||
# Priority 5: Production building (forge boosts future output)
|
||||
if not city.has_building("forge"):
|
||||
if not city.has_building("forge") and city.can_build("forge", player):
|
||||
var fdata: Dictionary = DataLoader.get_building("forge")
|
||||
if not fdata.is_empty():
|
||||
return _prod_building(city_index, "forge")
|
||||
|
||||
# Priority 6: Castle (upgrades walls, enables bombard)
|
||||
if city.has_building("walls") and not city.has_building("castle"):
|
||||
if (
|
||||
city.has_building("walls")
|
||||
and not city.has_building("castle")
|
||||
and city.can_build("castle", player)
|
||||
):
|
||||
var cdata: Dictionary = DataLoader.get_building("castle")
|
||||
if not cdata.is_empty():
|
||||
return _prod_building(city_index, "castle")
|
||||
|
||||
# Priority 7: Any other available building
|
||||
var building_id: String = _pick_building_id(city)
|
||||
var building_id: String = _pick_building_id(city, player)
|
||||
if not building_id.is_empty():
|
||||
return _prod_building(city_index, building_id)
|
||||
|
||||
# Fallback: more military
|
||||
var fallback_unit: String = _pick_military_unit_id()
|
||||
# Fallback: any buildable military unit (warrior is ungated by default).
|
||||
var fallback_unit: String = _pick_buildable_military_unit_id(city, player)
|
||||
if not fallback_unit.is_empty():
|
||||
return _prod_unit(city_index, fallback_unit)
|
||||
# Last-resort fallback: worker (ungated) — keeps production flowing so
|
||||
# the city never stalls on an empty queue when exotic data states hide
|
||||
# every combat unit behind tech gates.
|
||||
if city.can_build("worker", player):
|
||||
return _prod_unit(city_index, "worker")
|
||||
return {}
|
||||
|
||||
|
||||
|
|
@ -449,14 +463,28 @@ static func _pick_next_tech(player: Variant) -> String:
|
|||
return best_id
|
||||
|
||||
|
||||
static func _pick_military_unit_id() -> String:
|
||||
var preferred: String = "warrior"
|
||||
var data: Dictionary = DataLoader.get_unit(preferred)
|
||||
if not data.is_empty():
|
||||
return preferred
|
||||
static func _pick_buildable_military_unit_id(
|
||||
city: RefCounted, player: RefCounted
|
||||
) -> String:
|
||||
## Pick the first military unit the city is allowed to build, preferring
|
||||
## warrior (ungated baseline). Walks the priority list and falls back to
|
||||
## any melee unit whose tech/race/school gates the player already clears.
|
||||
## Returns "" only if every combat unit is gated (extremely unlikely in
|
||||
## Age of Dwarves, where warrior has no requirements).
|
||||
var priority: Array[String] = [
|
||||
"warrior", "spearmen", "pikeman", "archer", "cavalry",
|
||||
]
|
||||
for unit_id: String in priority:
|
||||
if DataLoader.get_unit(unit_id).is_empty():
|
||||
continue
|
||||
if city.can_build(unit_id, player):
|
||||
return unit_id
|
||||
for u: Dictionary in DataLoader.get_all_units():
|
||||
if String(u.get("combat_type", "")) == "melee":
|
||||
return String(u.get("id", ""))
|
||||
if String(u.get("combat_type", "")) != "melee":
|
||||
continue
|
||||
var uid: String = String(u.get("id", ""))
|
||||
if not uid.is_empty() and city.can_build(uid, player):
|
||||
return uid
|
||||
return ""
|
||||
|
||||
|
||||
|
|
@ -470,7 +498,9 @@ static func _pick_happiness_building_id(
|
|||
var bid: String = str(b.get("id", ""))
|
||||
if bid.is_empty() or bid in existing:
|
||||
continue
|
||||
if not _can_build(b, player):
|
||||
if b.get("wonder_type") != null:
|
||||
continue
|
||||
if not city.can_build(bid, player):
|
||||
continue
|
||||
var happiness_value: int = _sum_effect(b, "happiness")
|
||||
if happiness_value > best_happiness:
|
||||
|
|
@ -479,18 +509,6 @@ static func _pick_happiness_building_id(
|
|||
return best_id
|
||||
|
||||
|
||||
static func _can_build(building_data: Dictionary, player: RefCounted) -> bool:
|
||||
if building_data.get("wonder_type") != null:
|
||||
return false
|
||||
var tech_req: String = str(building_data.get("tech_required", ""))
|
||||
if not tech_req.is_empty() and not player.has_tech(tech_req):
|
||||
return false
|
||||
var culture_req: String = str(building_data.get("culture_required", ""))
|
||||
if not culture_req.is_empty():
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
static func _sum_effect(building_data: Dictionary, effect_type: String) -> int:
|
||||
var total: int = 0
|
||||
var effects: Array = building_data.get("effects", []) as Array
|
||||
|
|
@ -503,7 +521,7 @@ static func _sum_effect(building_data: Dictionary, effect_type: String) -> int:
|
|||
return total
|
||||
|
||||
|
||||
static func _pick_building_id(city: RefCounted) -> String:
|
||||
static func _pick_building_id(city: RefCounted, player: RefCounted) -> String:
|
||||
var existing: Array = Array(city.buildings)
|
||||
# Skip buildings handled by priority logic
|
||||
var skip: Array = ["walls", "forge", "castle"]
|
||||
|
|
@ -511,10 +529,10 @@ static func _pick_building_id(city: RefCounted) -> String:
|
|||
var bid: String = str(b.get("id", ""))
|
||||
if bid.is_empty() or bid in existing or bid in skip:
|
||||
continue
|
||||
if not str(b.get("tech_required", "")).is_empty():
|
||||
continue
|
||||
if b.get("wonder_type") != null:
|
||||
continue
|
||||
if not city.can_build(bid, player):
|
||||
continue
|
||||
return bid
|
||||
return ""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue