feat(ai): ✨ Add military unit health-based retreat logic to trigger strategic withdrawals when units fall below configurable health thresholds
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
926ee5777e
commit
ced9601387
1 changed files with 40 additions and 8 deletions
|
|
@ -25,7 +25,7 @@ const FOUND_MIN_DIST_OWN: int = 4
|
|||
## deadlock founders that spawned near each other (observed in arena
|
||||
## smoke tests where start placement put both players on tile 0,0).
|
||||
const FOUND_MIN_DIST_ENEMY: int = 1
|
||||
const RETREAT_HP_FRACTION: float = 0.0
|
||||
const RETREAT_HP_FRACTION: float = 0.4
|
||||
const DEFENSIVE_CHASE_RANGE: int = 12
|
||||
const MILITARY_COMBAT_TYPES: Array[String] = [
|
||||
"melee", "ranged", "cavalry", "siege",
|
||||
|
|
@ -208,6 +208,22 @@ static func _tile_has_enemy_unit(
|
|||
return false
|
||||
|
||||
|
||||
static func _count_own_military_at(
|
||||
player: RefCounted, pos: Vector2i
|
||||
) -> int:
|
||||
var total: int = 0
|
||||
for u: Variant in player.units:
|
||||
if u == null or not u.is_alive():
|
||||
continue
|
||||
if u.get("can_found_city") == true:
|
||||
continue
|
||||
if int(u.get("attack")) <= 0 and int(u.get("ranged_attack")) <= 0:
|
||||
continue
|
||||
if u.position == pos:
|
||||
total += 1
|
||||
return total
|
||||
|
||||
|
||||
static func _enemy_military_threat(player: RefCounted) -> Dictionary:
|
||||
## count=in-range(<=8), total_count=all enemy combat. threatens_city @<=5.
|
||||
var count: int = 0
|
||||
|
|
@ -337,11 +353,25 @@ static func _decide_military_action(
|
|||
_score_away_from_pos(nearest_enemy.position),
|
||||
)
|
||||
|
||||
# Adjacent attack if healthy enough.
|
||||
# Garrison — hold home city when we're the only defender and no enemy
|
||||
# is adjacent. Prevents the lone warrior from marching out to attack
|
||||
# and leaving the city open, which cascaded into attrition spirals
|
||||
# (warrior dies → rebuild → dies → ...).
|
||||
var enemy_dist: int = INF_DISTANCE
|
||||
if nearest_enemy != null:
|
||||
var enemy_dist: int = HexUtilsScript.hex_distance(
|
||||
enemy_dist = HexUtilsScript.hex_distance(
|
||||
unit.position, nearest_enemy.position
|
||||
)
|
||||
if not player.cities.is_empty() and enemy_dist > 1:
|
||||
var home_pos: Vector2i = (player.cities[0] as RefCounted).position
|
||||
if (
|
||||
unit.position == home_pos
|
||||
and _count_own_military_at(player, home_pos) <= 1
|
||||
):
|
||||
return {}
|
||||
|
||||
# Adjacent attack if healthy enough.
|
||||
if nearest_enemy != null:
|
||||
if enemy_dist == 1:
|
||||
return {
|
||||
"type": "attack",
|
||||
|
|
@ -455,11 +485,13 @@ static func _decide_production(
|
|||
if not rush_unit.is_empty():
|
||||
return _prod_unit(city_index, rush_unit)
|
||||
|
||||
# Priority 0: Early military floor — reach 2 warriors before committing
|
||||
# to walls (70 prod, ~35 turns) or founder (70 prod). Two warriors at
|
||||
# 20 prod each get us to a baseline garrison by ~T20 at 2 prod/turn,
|
||||
# so walls land around T50 with a usable army rather than T35 with none.
|
||||
if military_count < 2:
|
||||
# Priority 0: Early military floor — maintain 4 warriors during the
|
||||
# first 80 turns before committing to walls/happiness/founder. Early
|
||||
# combat attrition (p0 harasses) was dropping mil to 1-2 by T75; this
|
||||
# keeps the replacement pipeline ahead of losses so we hit mil≥4 by
|
||||
# T100. After T80 the standard Priority 4 target takes over.
|
||||
var early_mil_floor: int = 4 if GameState.turn_number <= 80 else 0
|
||||
if military_count < maxi(1, early_mil_floor):
|
||||
var emergency_unit: String = _pick_buildable_military_unit_id(
|
||||
city, player
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue