feat(ai): ✨ Implement military threat detection and defensive unit purchasing heuristics
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
dad8673a8d
commit
8ae2193bf3
1 changed files with 73 additions and 2 deletions
|
|
@ -46,6 +46,10 @@ static func process_player(player: RefCounted) -> Array:
|
|||
player
|
||||
)
|
||||
|
||||
var threat: Dictionary = _enemy_military_threat(player)
|
||||
if bool(threat.get("threatens_city", false)):
|
||||
_rush_buy_defenders(player, threat)
|
||||
|
||||
# Units: founders first (expansion), then military.
|
||||
for idx: int in player.units.size():
|
||||
var unit: Variant = player.units[idx]
|
||||
|
|
@ -194,6 +198,54 @@ static func _tile_has_enemy_unit(
|
|||
return false
|
||||
|
||||
|
||||
static func _enemy_military_threat(player: RefCounted) -> Dictionary:
|
||||
## count=in-range(<=8), total_count=all enemy combat. threatens_city @<=5.
|
||||
var count: int = 0
|
||||
var total: int = 0
|
||||
var nearest: int = INF_DISTANCE
|
||||
var spawn: RefCounted = null
|
||||
for eu: Variant in _collect_enemy_units(player):
|
||||
if int(eu.get("attack")) <= 0 and int(eu.get("ranged_attack")) <= 0:
|
||||
continue
|
||||
total += 1
|
||||
var min_d: int = INF_DISTANCE
|
||||
var best_c: RefCounted = null
|
||||
for c: RefCounted in player.cities:
|
||||
var d: int = HexUtilsScript.hex_distance(eu.position, c.position)
|
||||
if d < min_d: min_d = d; best_c = c
|
||||
if min_d <= 8: count += 1
|
||||
if min_d < nearest: nearest = min_d; spawn = best_c
|
||||
return {"count": count, "total_count": total, "nearest_dist": nearest,
|
||||
"threatens_city": nearest <= 5, "spawn_city": spawn}
|
||||
|
||||
|
||||
static func _rush_buy_defenders(player: RefCounted, threat: Dictionary) -> void:
|
||||
## Spawn warriors at threat.spawn_city while gold >= 50 and under
|
||||
## max(2, threat+1) defenders (hard cap 3x cities). Direct-spawn pattern
|
||||
## from auto_play.gd so the bridge needs no new action type.
|
||||
var spawn_city: RefCounted = threat.get("spawn_city")
|
||||
if spawn_city == null: return
|
||||
var mil: int = 0
|
||||
for u: Variant in player.units:
|
||||
if u == null or not u.is_alive() or u.get("can_found_city") == true:
|
||||
continue
|
||||
if int(u.get("attack")) > 0 or int(u.get("ranged_attack")) > 0:
|
||||
mil += 1
|
||||
var target: int = maxi(2, int(threat.get("count", 0)) + 1)
|
||||
var cap: int = player.cities.size() * 3
|
||||
while player.gold >= 50 and mil < target and mil < cap:
|
||||
var nu: RefCounted = UnitScript.new(
|
||||
"warrior", player.index, spawn_city.position
|
||||
)
|
||||
nu.id = "rush_%d_%d" % [GameState.turn_number, mil]
|
||||
nu.display_name = "Warrior"
|
||||
player.units.append(nu)
|
||||
GameState.get_primary_layer().get("units", []).append(nu)
|
||||
player.gold -= 50
|
||||
mil += 1
|
||||
EventBus.unit_created.emit(nu, player.index)
|
||||
|
||||
|
||||
# ── Founder logic ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
|
@ -374,6 +426,19 @@ static func _decide_production(
|
|||
var city: RefCounted = player.cities[city_index]
|
||||
var city_count: int = player.cities.size()
|
||||
|
||||
var threat: Dictionary = _enemy_military_threat(player)
|
||||
var threatened: bool = bool(threat.get("threatens_city", false))
|
||||
var enemy_total: int = int(threat.get("total_count", 0))
|
||||
|
||||
# Threat preemption: when an enemy stack is closing on a city, force
|
||||
# military production over walls/happiness/founders until we can field
|
||||
# at least enemy_total + 1 defenders (matches opponent's full army, not
|
||||
# just the in-range slice — in-range saturates while reserves escalate).
|
||||
if threatened and military_count < maxi(3, enemy_total + 1):
|
||||
var rush_unit: String = _pick_buildable_military_unit_id(city, player)
|
||||
if not rush_unit.is_empty():
|
||||
return _prod_unit(city_index, rush_unit)
|
||||
|
||||
# Priority 0: Emergency garrison — no military at all means the next
|
||||
# enemy stack wins uncontested before any wall is finished. A single
|
||||
# warrior buys ~10 turns of breathing room at a fraction of walls'
|
||||
|
|
@ -408,8 +473,14 @@ static func _decide_production(
|
|||
):
|
||||
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)
|
||||
# Priority 4: Military — maintain 2 warriors per city, scaling up to
|
||||
# match enemy's FULL army when they're closing on us so we don't lose
|
||||
# on parity once reserves arrive.
|
||||
var enemy_mil: int = enemy_total if threatened else 0
|
||||
var mil_target: int = maxi(2, city_count * 2)
|
||||
if enemy_mil > 0:
|
||||
mil_target = maxi(mil_target, enemy_mil + 1)
|
||||
var want_military: bool = military_count < mil_target
|
||||
if want_military:
|
||||
var unit_id: String = _pick_buildable_military_unit_id(city, player)
|
||||
if not unit_id.is_empty():
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue