Tune AI director tactical defense loop

This commit is contained in:
wuwenbo 2026-07-02 21:43:30 +08:00
parent 7ea8f7c852
commit 45f3cffd73
7 changed files with 395 additions and 15 deletions

View File

@ -178,6 +178,8 @@ Defense 的行为倾向:
- Emergency 车道优先阻止丢城;危险城市如果空防或只剩唯一守军,先补兵/城墙,再考虑攻击和外派。
- 城市优先训练防守单位、建城墙、保留占城格单位。
- 如果危险城市中心为空,城市生产要优先补可站城/守城单位;扩张变强后不能让空城威胁长期升高。
- 如果危险城市中心为空且敌方下回合可占Emergency 改为“补兵/回城心优先”;只有能直接击杀最后一个威胁单位的攻击才能压过驻守,普通攻击不能继续把城心暴露给占领。
- 正在站危险城市中心的守军不能被调离城心去救另一座城;其他周边守军仍可参与回防,避免防线互相抽空。
- 科技优先防御、基础兵种、移动和克制。
- 英雄优先治疗、保护、控场、守城。
@ -441,7 +443,7 @@ HeroPlaybook 的判断顺序:
- 按英雄等级和技能入口识别可用 Action。
- 治疗和保护类动作优先给残血英雄和高价值友军。
- 地面攻击类动作优先选敌军密集、敌城中心或可触发召唤的格子。
- 刺客和先锋英雄在有合法有效攻击目标时,攻击优先级略高于普通 MoveToFront否则已上场英雄容易只移动、不形成个人威胁。
- 刺客、先锋、法师、控制英雄在有合法有效攻击或地面控制目标时,动作优先级高于普通 MoveToFront否则已上场英雄容易只移动、不形成个人威胁。
- 自身主动类动作只在满足局部价值时使用。
- 没有专属规则时退回通用英雄攻击、恢复和 Front 站位。
@ -598,6 +600,14 @@ AI 可以重新实现,但必须遵守 TH1 的游戏架构:
| Fallback | Fallback 总数、Fallback actionType、Fallback noEffect | 越接近 0 越好;非 0 时优先把有意义动作归入正式车道 |
| 性能 | actions/sec、avgGame、decision avg/p95/max | 先保证聪明,再把明显尖峰纳入下一轮优化 |
小兵定位调优原则:
- 远程和攻城单位不是普通前排。它们应优先找安全输出位,有伤害且低反击风险时更愿意攻击。
- 远程和攻城单位前线移动时,不直接选择离目标最近的格子;应从该单位本回合少量合法移动候选中挑“能进射程、威胁低、仍在推进”的输出位,避免为了聪明移动引入全量搜索。
- 特殊单位分为压力单位和脆弱衍生单位。压力单位可以参与推进;脆弱召唤物、幻象、灵球、骨堆、小萃香等不能被 Front 当普通近战送进高威胁格。
- 脆弱特殊衍生单位不承担危险扩张任务;目标格已经有即时威胁时,让普通兵、机动兵或主力英雄去处理。
- 特色单位的目标不是单纯提高出场次数,而是要在伤害、控制、召唤、献祭、占领或技能签名变化中表现出可读价值。
防守指标解释:
| 指标 | 含义 |

View File

@ -1150,6 +1150,18 @@ SkillBase
如果 IsUrgentCityDefenseAction(candidate):
返回 candidate
如果 threat.CityGrid 没有己方驻守 且 threat.CanBeThreatenedNextTurn:
attack = TryEmergencyAttack(ctx, threat)
如果 attack 能直接击杀最后一个威胁单位:
返回 attack
move = TryEmergencyMoveToCity(ctx, threat)
如果 move.IsValid:
返回 move
如果 attack.IsValid:
返回 attack
candidate = TryEmergencyAttack(ctx, threat)
如果 candidate.IsValid:
返回 candidate
@ -1219,6 +1231,11 @@ SkillBase
score += 180
如果 CanKillByAction(ctx, action):
score += 160
如果 threat.CityGrid 没有己方驻守 且 threat.CanBeThreatenedNextTurn:
如果 本次攻击不能直接击杀最后一个威胁单位:
score -= threat.IsCapital ? 340 : 240
如果 action.Unit 是该威胁的唯一守军 或 action.Unit 本可回到城心:
score -= 180
返回 score
```
@ -1245,6 +1262,8 @@ SkillBase
endGrid = GetActionEndGrid(action)
如果 !IsUsefulMoveToward(ctx, startGrid, endGrid, threat.CityGrid):
继续
如果 unit 正站在某个危险城市中心 且 endGrid 会离开当前城心:
继续
candidate = Candidate(action, Emergency, ScoreEmergencyMove(ctx, unit, action, threat), "回防城市")
candidate.IntentKey = intentKey
@ -1264,6 +1283,13 @@ SkillBase
score -= Distance(endGrid, threat.CityGrid) * 14
如果 unit 是英雄:
score += 60
如果 threat.CityGrid 没有己方驻守:
如果 Distance(endGrid, threat.CityGrid) == 0:
score += 360
否则:
score += max(0, 220 - Distance(endGrid, threat.CityGrid) * 35)
如果 threat.IsCapital:
score += 140
返回 score
```
@ -1296,6 +1322,8 @@ SkillBase
score += 120
如果 threat.CanBeThreatenedNextTurn:
score += 100
如果 threat.CityGrid 没有己方驻守:
score += threat.IsCapital ? 300 : 220
如果 id.ActionType == CityAction 且 id.CityActionType == BuildCityWall:
score = 740
@ -1614,6 +1642,8 @@ Expansion Lane 只处理早期扩张的刚性目标,不处理普通资源开
如果 ShouldSkipExpansionUnit(ctx, unit, target, startGrid, startDistance):
继续
如果 unit 是脆弱特殊衍生单位 且 target.Grid 有即时威胁:
继续
action = FindBestMove(unit, target.Grid)
endGrid = ActionEndGrid(action)
@ -1819,10 +1849,24 @@ Expansion Lane 只处理早期扩张的刚性目标,不处理普通资源开
attack = FindBestUsefulAttack(ctx, hero, BestHeroOrKillableEnemy(ctx, hero))
如果 attack 可用:
roleBonus = 0
如果 state.Role 是 Assassin 或 Vanguard:
roleBonus = 90
如果 state.Role 是 Assassin:
roleBonus = 140
否则如果 state.Role 是 Vanguard:
roleBonus = 115
否则如果 state.Role 是 Caster 或 Control:
roleBonus = 75
返回 Candidate(attack, HeroPlaybook, 760 + TargetValue(attack.Target) + roleBonus, "英雄通用高价值攻击")
如果 state.Role 是 Caster 或 Control:
ground = FindBestAttackGround(ctx, hero, state.Front.TargetGrid 或 state.Front.AnchorGrid 或 PrimaryFront.TargetGrid)
如果 ground 可用:
score = 710
如果 state.IsOnFront:
score += 80
如果 ctx.Cache.HasAnyEnemyContact:
score += 60
返回 Candidate(ground, HeroPlaybook, score, "英雄通用地面控制")
返回 None
```
@ -2675,6 +2719,100 @@ Fallback 的目标是不断回合,不负责聪明。大量进入 Fallback 说
对每个 enemy in 当前进攻目标玩家单位:
score += CounterValue(unitType, enemy.UnitFullType)
如果 当前需要扩张:
score += TrainUnitExpansionValue(unitType)
如果 unitType 是远程单位:
如果 已接敌:
score += 95
否则:
score += 45
如果 unitType 是攻城单位或机动单位:
score += 45
如果 unitType 是特殊压力单位:
如果 已接敌:
score += 70
否则:
score += 30
返回 score
```
```text
函数 FindBestFrontMove(ctx, unit, target, front):
如果 unit 不是远程或攻城:
返回 FindBestMove(unit, target)
如果 front 不是 Attack 或 Defense:
返回 FindBestMove(unit, target)
best = 空
bestScore = 负无穷
对每个 moveAction in ActionIndex.GetMoveActions(unit) 的前 8 个候选:
endGrid = ActionEndGrid(moveAction)
如果 endGrid 没有朝 target 推进:
continue
distance = Distance(endGrid, target)
threat = GridThreat(ctx, endGrid)
score = -distance * 18 - threat * 0.7
如果 distance <= unit.AttackRange:
score += 280
如果 threat <= 0:
score += 120
否则如果 threat <= UnitPower(unit) * 0.25:
score += 55
否则:
score -= 160
best = Max(score)
如果 best 存在:
返回 best
返回 FindBestMove(unit, target)
```
```text
函数 ScoreFrontMove(ctx, unit, action, front):
endGrid = ActionEndGrid(action)
target = ResolveFrontTarget(front)
distance = Distance(endGrid, target)
threat = GridThreat(ctx, endGrid)
score = FrontBaseScore(front)
score -= distance * 16
score -= threat * 0.4
如果 unit 下回合能压到 target:
score += 80
如果 unit 是远程或攻城:
如果 front 是 Attack 或 Defense:
如果 distance <= unit.AttackRange:
score += 150
否则:
score -= 35
如果 threat <= UnitPower(unit) * 0.25:
score += 115
否则:
score -= 140
如果 distance <= unit.AttackRange 且 threat <= 0:
score += 80
如果 unit 是脆弱特殊衍生单位 且 front 是 Attack:
如果 unit 血量偏低:
score -= 180
如果 不能安全压到 target:
score -= 220
否则:
score -= 50
返回 score
```

View File

@ -5,6 +5,7 @@ param(
[int]$Players = 17,
[int]$Width = 30,
[int]$Height = 30,
[Alias("MaxTurns")]
[int]$Turns = 100,
[int]$TimeoutSeconds = 1800,
[int]$MaxActions = 20000,

View File

@ -272,6 +272,13 @@ namespace Logic.AI.Director
return best;
}
public IEnumerable<AIActionBase> GetMoveActions(UnitData unit)
{
if (unit == null) yield break;
if (!_movesByUnit.TryGetValue(unit.Id, out var actions)) yield break;
foreach (var action in actions) yield return action;
}
public AIActionBase FindAnyByType(CommonActionType actionType)
{
foreach (var action in AllActions)

View File

@ -110,6 +110,32 @@ namespace Logic.AI.Director
return true;
}
if (ShouldPrioritizeCityCenterGuard(threat))
{
var guardAttack = TryEmergencyAttack(ctx, decision, threat);
if (CanEmergencyAttackResolveCityCenterRisk(ctx, threat, guardAttack))
{
candidate = guardAttack;
decision.AddTrace($"Emergency: clear last city-center threat for city={threat.City?.Id}.", ctx.Config.MaxCandidateTraceCount);
return true;
}
var guardMove = TryEmergencyMove(ctx, decision, threat);
if (guardMove.IsValid)
{
candidate = guardMove;
decision.AddTrace($"Emergency: guard empty threatened city={threat.City?.Id}.", ctx.Config.MaxCandidateTraceCount);
return true;
}
if (guardAttack.IsValid)
{
candidate = guardAttack;
decision.AddTrace($"Emergency: fallback attack city threat for city={threat.City?.Id}.", ctx.Config.MaxCandidateTraceCount);
return true;
}
}
var attack = TryEmergencyAttack(ctx, decision, threat);
if (attack.IsValid)
{
@ -138,6 +164,13 @@ namespace Logic.AI.Director
return false;
}
private static bool ShouldPrioritizeCityCenterGuard(AIDirectorCityThreat threat)
{
return threat != null
&& !threat.HasCityCenterDefender
&& threat.CanBeThreatenedNextTurn;
}
private bool TryExpansionLane(AIDirectorContext ctx, AIDirectorDecision decision, out AIDirectorActionCandidate candidate)
{
candidate = AIDirectorActionCandidate.None;
@ -209,6 +242,7 @@ namespace Logic.AI.Director
if (target?.Grid == null || startGrid.Id == target.Grid.Id) continue;
var startDistance = AIDirectorMath.Distance(ctx.Map, startGrid, target.Grid);
if (ShouldSkipExpansionUnit(ctx, unit, target, startGrid, startDistance)) continue;
if (IsFragileSpecialUnit(unit) && GridThreat(ctx, target.Grid) > 0f) continue;
var action = ctx.ActionIndex.FindBestMove(unit, target.Grid);
var endGrid = action?.Param?.TargetGridData ?? action?.Param?.GridData;
@ -259,9 +293,11 @@ namespace Logic.AI.Director
var target = ChooseBestThreatTarget(ctx, threat);
var action = FindBestUsefulAttack(ctx, defender, target) ?? FindBestUsefulAttack(ctx, defender);
var targetValue = UnitTargetValue(ctx, action?.Param?.TargetUnitData ?? target);
var score = 960f + threat.DangerScore * 20f + targetValue;
var attackScore = ScoreAttackAction(ctx, action);
var emptyCityRiskPenalty = EmptyCityDefenseRiskPenalty(ctx, threat, defender, action, attackScore);
var score = 960f + threat.DangerScore * 20f + targetValue - emptyCityRiskPenalty;
var candidate = ctx.ActionIndex.Candidate(action, AIDirectorLane.Emergency, "Emergency.AttackCityThreat", score);
AddTerms(candidate, ("base", 960f), ("danger", threat.DangerScore * 20f), ("targetValue", targetValue));
AddTerms(candidate, ("base", 960f), ("danger", threat.DangerScore * 20f), ("targetValue", targetValue), ("emptyCityRisk", -emptyCityRiskPenalty), ("attackScore", attackScore));
candidate.Unit = candidate.Unit ?? defender;
candidate.TargetUnit = candidate.TargetUnit ?? target;
candidate.City = candidate.City ?? threat.City;
@ -290,13 +326,18 @@ namespace Logic.AI.Director
var endGrid = action?.Param?.TargetGridData ?? action?.Param?.GridData;
if (!IsUsefulMoveToward(ctx, unitGrid, endGrid, threat.CityGrid)) continue;
var endDistance = AIDirectorMath.Distance(ctx.Map, endGrid, threat.CityGrid);
if (UnitIsThreatenedCityCenterGuard(ctx, unit) && endGrid?.Id != unitGrid?.Id) continue;
var unitPower = AIDirectorMath.UnitPower(unit);
var progressBonus = Mathf.Max(0, startDistance - endDistance) * 55f;
var distancePenalty = -endDistance * 14f;
var heroBonus = unit.TreatedAsHero(ctx.Map, unit) ? 60f : 0f;
var score = 900f + threat.DangerScore * 20f + unitPower + progressBonus + distancePenalty + heroBonus;
var cityCenterBonus = !threat.HasCityCenterDefender
? endDistance == 0 ? 360f : Mathf.Max(0, 220f - endDistance * 35f)
: 0f;
var capitalBonus = threat.IsCapital && !threat.HasCityCenterDefender ? 140f : 0f;
var score = 900f + threat.DangerScore * 20f + unitPower + progressBonus + distancePenalty + heroBonus + cityCenterBonus + capitalBonus;
var candidate = ctx.ActionIndex.Candidate(action, AIDirectorLane.Emergency, "Emergency.MoveToCity", score);
AddTerms(candidate, ("base", 900f), ("danger", threat.DangerScore * 20f), ("unitPower", unitPower), ("progress", progressBonus), ("distance", distancePenalty), ("hero", heroBonus));
AddTerms(candidate, ("base", 900f), ("danger", threat.DangerScore * 20f), ("unitPower", unitPower), ("progress", progressBonus), ("distance", distancePenalty), ("hero", heroBonus), ("cityCenter", cityCenterBonus), ("capital", capitalBonus));
candidate.Unit = candidate.Unit ?? unit;
candidate.City = candidate.City ?? threat.City;
candidate.TargetGrid = candidate.TargetGrid ?? threat.CityGrid;
@ -317,6 +358,10 @@ namespace Logic.AI.Director
{
var score = ScoreCityGrowth(ctx, action, plan);
if (score <= 0f) continue;
if (!threat.HasCityCenterDefender && action?.ActionLogic?.ActionId?.ActionType == CommonActionType.TrainUnit)
{
score += threat.IsCapital ? 300f : 220f;
}
var candidate = ctx.ActionIndex.Candidate(action, AIDirectorLane.Emergency, "Emergency.CityAction", score + 250f);
AddTerms(candidate, ("cityGrowth", score), ("emergencyBonus", 250f));
candidate.City = candidate.City ?? threat.City;
@ -493,11 +538,27 @@ namespace Logic.AI.Director
var attack = FindBestUsefulAttack(ctx, state.Hero);
var attackScore = ScoreAttackAction(ctx, attack);
var targetValue = attackScore > 0f ? UnitTargetValue(ctx, attack?.Param?.TargetUnitData) : 0f;
var roleAttackBonus = state.Role is AIDirectorHeroRole.Assassin or AIDirectorHeroRole.Vanguard ? 90f : 0f;
var roleAttackBonus = state.Role switch
{
AIDirectorHeroRole.Assassin => 140f,
AIDirectorHeroRole.Vanguard => 115f,
AIDirectorHeroRole.Caster or AIDirectorHeroRole.Control => 75f,
_ => 0f
};
var attackCandidate = ctx.ActionIndex.Candidate(attack, AIDirectorLane.HeroPlaybook, "HeroPlaybook.GenericHighValueAttack", 760f + targetValue + roleAttackBonus);
AddTerms(attackCandidate, ("base", 760f), ("targetValue", targetValue), ("roleAttack", roleAttackBonus), ("attackScore", attackScore));
if (attackCandidate.IsValid && attackScore > 0f) return attackCandidate;
if (state.Role is AIDirectorHeroRole.Caster or AIDirectorHeroRole.Control)
{
var groundTarget = state.Front?.TargetGrid ?? state.Front?.AnchorGrid ?? ctx.Cache.PrimaryFront?.TargetGrid;
var ground = ctx.ActionIndex.FindBestAttackGround(state.Hero, groundTarget) ?? ctx.ActionIndex.FindBestAttackGround(state.Hero);
var groundScore = 710f + (state.IsOnFront ? 80f : 0f) + (ctx.Cache.HasAnyEnemyContact ? 60f : 0f);
var groundCandidate = ctx.ActionIndex.Candidate(ground, AIDirectorLane.HeroPlaybook, "HeroPlaybook.GenericGroundControl", groundScore);
AddTerms(groundCandidate, ("base", 710f), ("onFront", state.IsOnFront ? 80f : 0f), ("enemyContact", ctx.Cache.HasAnyEnemyContact ? 60f : 0f));
if (groundCandidate.IsValid) return groundCandidate;
}
var target = state.Front?.TargetGrid ?? state.Front?.AnchorGrid;
var frontMove = ctx.ActionIndex.FindBestMove(state.Hero, target);
var frontCandidate = ctx.ActionIndex.Candidate(frontMove, AIDirectorLane.HeroPlaybook, "HeroPlaybook.MoveToFront", 650f);
@ -577,7 +638,7 @@ namespace Logic.AI.Director
foreach (var unit in ctx.Cache.SelfUnits)
{
if (ShouldSkipFrontMove(ctx, unit, front)) continue;
var action = ctx.ActionIndex.FindBestMove(unit, target);
var action = FindBestFrontMove(ctx, unit, target, front);
var startGrid = unit.Grid(ctx.Map);
var endGrid = action?.Param?.TargetGridData ?? action?.Param?.GridData;
if (!IsUsefulMoveToward(ctx, startGrid, endGrid, target)) continue;
@ -656,7 +717,21 @@ namespace Logic.AI.Director
var score = 620f;
score += damage / Mathf.Max(1f, target.GetMaxHealth()) * UnitTargetValue(ctx, target) * 2f;
score += CounterBonus(attacker, target);
score -= CounterThreat(ctx, attacker, target);
var counterThreat = CounterThreat(ctx, attacker, target);
score -= counterThreat;
if (IsRangedUnit(attacker.UnitType))
{
score += counterThreat <= 0f ? 110f : 45f;
if (damage > 0) score += 55f;
}
else if (IsSiegeUnit(attacker.UnitType))
{
score += counterThreat <= 0f ? 95f : 35f;
}
else if (IsFragileSpecialUnit(attacker) && damage < target.Health && counterThreat > 0f)
{
score -= 120f;
}
if (damage >= target.Health) score += 160f;
if (target.TreatedAsHero(ctx.Map, target)) score += 120f;
if (IsThreateningAnyCity(ctx, target)) score += 120f;
@ -713,13 +788,68 @@ namespace Logic.AI.Director
_ => 0f
};
score -= AIDirectorMath.Distance(ctx.Map, endGrid, target) * 16f;
score -= GridThreat(ctx, endGrid) * 0.4f;
var distance = AIDirectorMath.Distance(ctx.Map, endGrid, target);
var gridThreat = GridThreat(ctx, endGrid);
score -= distance * 16f;
score -= gridThreat * 0.4f;
if (CanNextTurnPressureTarget(ctx, unit, endGrid, target)) score += 80f;
if (unit.GetActionPoint(ActionPointType.Move) >= 2 && front.FrontType != AIDirectorFrontType.Hold) score += 40f;
if (IsRangedUnit(unit.UnitType) || IsSiegeUnit(unit.UnitType))
{
if (front.FrontType is AIDirectorFrontType.Attack or AIDirectorFrontType.Defense)
{
var inRange = distance <= unit.GetAttackRange(ctx.Map);
if (inRange) score += 150f;
else score -= 35f;
if (gridThreat <= AIDirectorMath.UnitPower(unit) * 0.25f) score += 115f;
else score -= 140f;
if (inRange && gridThreat <= 0f) score += 80f;
}
}
else if (IsFragileSpecialUnit(unit) && front.FrontType == AIDirectorFrontType.Attack)
{
if (AIDirectorMath.HealthRatio(unit) <= ctx.Config.LowHealthRatio) score -= 180f;
score -= CanNextTurnPressureTarget(ctx, unit, endGrid, target) && gridThreat <= 0f ? 50f : 220f;
}
return score;
}
private AIActionBase FindBestFrontMove(
AIDirectorContext ctx,
UnitData unit,
GridData target,
AIDirectorFront front)
{
if (ctx?.ActionIndex == null || unit == null || target == null) return null;
if (!IsRangedUnit(unit.UnitType) && !IsSiegeUnit(unit.UnitType)) return ctx.ActionIndex.FindBestMove(unit, target);
if (front?.FrontType is not (AIDirectorFrontType.Attack or AIDirectorFrontType.Defense)) return ctx.ActionIndex.FindBestMove(unit, target);
var startGrid = unit.Grid(ctx.Map);
AIActionBase best = null;
var bestScore = float.MinValue;
var scanned = 0;
foreach (var action in ctx.ActionIndex.GetMoveActions(unit))
{
if (++scanned > 8) break;
var endGrid = action?.Param?.TargetGridData ?? action?.Param?.GridData;
if (endGrid == null) continue;
if (!IsUsefulMoveToward(ctx, startGrid, endGrid, target)) continue;
var distance = AIDirectorMath.Distance(ctx.Map, endGrid, target);
var threat = GridThreat(ctx, endGrid);
var score = -distance * 18f - threat * 0.7f;
if (distance <= unit.GetAttackRange(ctx.Map)) score += 280f;
if (threat <= 0f) score += 120f;
else if (threat <= AIDirectorMath.UnitPower(unit) * 0.25f) score += 55f;
else score -= 160f;
if (score <= bestScore) continue;
bestScore = score;
best = action;
}
return best ?? ctx.ActionIndex.FindBestMove(unit, target);
}
private float ScoreCityGrowth(AIDirectorContext ctx, AIActionBase action, AIDirectorCityPlan plan)
{
if (action?.ActionLogic?.ActionId == null) return 0f;
@ -1104,6 +1234,19 @@ namespace Logic.AI.Director
return false;
}
private bool UnitIsThreatenedCityCenterGuard(AIDirectorContext ctx, UnitData unit)
{
var unitGrid = unit?.Grid(ctx.Map);
if (unitGrid == null) return false;
foreach (var threat in ctx.Cache.CityThreats)
{
if (threat?.CityGrid == null || threat.CityGrid.Id != unitGrid.Id) continue;
if (ShouldUseEmergency(threat, ctx.Config) || threat.CanBeThreatenedNextTurn) return true;
}
return false;
}
private bool IsThreateningAnyCity(AIDirectorContext ctx, UnitData target)
{
if (target == null) return false;
@ -1134,6 +1277,50 @@ namespace Logic.AI.Director
return attackScore >= ctx.Config.PriorityTacticScore + 60f && counterThreat <= targetValue * 0.15f;
}
private float EmptyCityDefenseRiskPenalty(
AIDirectorContext ctx,
AIDirectorCityThreat threat,
UnitData defender,
AIActionBase action,
float attackScore)
{
if (ctx == null || threat == null || defender == null) return 0f;
if (threat.HasCityCenterDefender || !threat.CanBeThreatenedNextTurn) return 0f;
if (action?.Param?.TargetUnitData == null) return 0f;
var target = action.Param.TargetUnitData;
var damage = Table.Instance.CalcDamage(ctx.Map, defender, target);
var killThreat = damage >= target.Health;
var onlyDefender = IsOnlyDefenderForThreat(threat, defender);
var cityGrid = threat.CityGrid;
var defenderGrid = defender.Grid(ctx.Map);
var defenderCanOccupyCity = cityGrid != null
&& defenderGrid != null
&& AIDirectorMath.Distance(ctx.Map, defenderGrid, cityGrid) <= defender.GetActionPoint(ActionPointType.Move);
if (killThreat && threat.EnemyUnits.Count <= 1) return 0f;
var penalty = threat.IsCapital ? 340f : 240f;
if (onlyDefender || defenderCanOccupyCity) penalty += 180f;
if (attackScore >= ctx.Config.PriorityTacticScore + 180f && killThreat) penalty *= 0.45f;
return penalty;
}
private static bool CanEmergencyAttackResolveCityCenterRisk(
AIDirectorContext ctx,
AIDirectorCityThreat threat,
AIDirectorActionCandidate candidate)
{
if (ctx == null || threat == null || candidate?.AIAction?.Param == null) return false;
var action = candidate.AIAction;
var attacker = action.Param.UnitData;
var target = action.Param.TargetUnitData;
if (attacker == null || target == null) return false;
if (threat.EnemyUnits.Count > 1) return false;
if (!threat.EnemyUnits.Contains(target)) return false;
return Table.Instance.CalcDamage(ctx.Map, attacker, target) >= target.Health;
}
private bool TargetOwnerFeelingHighAndNotWar(AIDirectorContext ctx, UnitData target)
{
if (target == null || !ctx.Map.GetPlayerDataByUnitId(target.Id, out var owner) || owner == null) return false;
@ -1258,7 +1445,8 @@ namespace Logic.AI.Director
if (ShouldPushExpansion(ctx)) score += TrainUnitExpansionValue(id.UnitType);
if (ctx.Cache.StrategicPosture == AIDirectorStrategicPosture.Attack) score += 60f;
if (IsMobilityUnit(id.UnitType) || IsSiegeUnit(id.UnitType)) score += 45f;
if (IsRangedUnit(id.UnitType) && ctx.Cache.HasAnyEnemyContact) score += 35f;
if (IsRangedUnit(id.UnitType)) score += ctx.Cache.HasAnyEnemyContact ? 95f : 45f;
if (IsSpecialPressureUnit(id.UnitType)) score += ctx.Cache.HasAnyEnemyContact ? 70f : 30f;
return score;
}
@ -1282,9 +1470,10 @@ namespace Logic.AI.Director
UnitType.Defender or UnitType.HakureiRoundShieldman => 90f,
UnitType.Warrior or UnitType.KaguyaFrenchWarrior or UnitType.KaguyaFrenchAnimalWarrior or UnitType.NoUseHakureiBerserkWarrior or UnitType.NoUseHakureiBerserker => 70f,
UnitType.Swordsman => 95f,
UnitType.Archer or UnitType.KomeijiIndianArcher => 78f,
UnitType.Archer or UnitType.KomeijiIndianArcher => 88f,
UnitType.Rider or UnitType.Knights or UnitType.MoriyaRider or UnitType.MoriyaKnight or UnitType.KomeijiIndianRider or UnitType.KomeijiIndianKnight or UnitType.HakureiValkyrie => 88f,
UnitType.Catapult or UnitType.KaguyaFrenchCatapult or UnitType.KomeijiIndianCatapult => 105f,
UnitType.MoriyaHebi or UnitType.KaguyaFrenchWolf or UnitType.RemiliaEgyptianKoakuma or UnitType.RemiliaEgyptianKoakumaLion or UnitType.KomeijiIndianBigGuy or UnitType.KomeijiIndianJuggernaut => 118f,
UnitType.Giant or UnitType.GiantJuggernaut => 140f,
_ => 50f
};
@ -1317,6 +1506,27 @@ namespace Logic.AI.Director
return unitType is UnitType.Defender or UnitType.HakureiRoundShieldman;
}
private static bool IsSpecialPressureUnit(UnitType unitType)
{
return unitType is UnitType.MoriyaHebi or UnitType.KaguyaFrenchWolf
or UnitType.RemiliaEgyptianKoakuma or UnitType.RemiliaEgyptianKoakumaLion
or UnitType.KomeijiIndianBigGuy or UnitType.KomeijiIndianJuggernaut;
}
private static bool IsFragileSpecialUnit(UnitData unit)
{
if (unit == null) return false;
return unit.UnitType is UnitType.KaguyaFrenchReisenIllusion
or UnitType.SumirekoNorwayOrb
or UnitType.SumirekoDenmarkOrb
or UnitType.SumirekoEnglandOrb
or UnitType.SuikaMini
or UnitType.BonePile
or UnitType.Dagger
or UnitType.DaggerShip
|| unit.UnitType == UnitType.MoriyaHebi && unit.UnitLevel < 3;
}
private static float UnitMobilityExpansionBonus(UnitData unit)
{
if (unit == null) return 0f;

View File

@ -238,6 +238,7 @@ namespace Logic.AI.Director
public bool IsCritical;
public bool IsCapital;
public bool HasWall;
public bool HasCityCenterDefender;
public bool HasEnemyOnTerritory;
public bool CanBeThreatenedNextTurn;
public float DangerScore;

View File

@ -153,7 +153,8 @@ namespace Logic.AI.Director
City = city,
CityGrid = cityGrid,
IsCapital = city.IsCapital,
HasWall = city.CityWall
HasWall = city.CityWall,
HasCityCenterDefender = CityCenterHasFriendlyGuard(ctx, cityGrid)
};
foreach (var enemy in cache.EnemyUnits)
@ -193,6 +194,7 @@ namespace Logic.AI.Director
- threat.DefenderPower
+ (threat.HasEnemyOnTerritory ? 4f : 0f)
+ (threat.CanBeThreatenedNextTurn ? 6f : 0f)
+ (!threat.HasCityCenterDefender && threat.CanBeThreatenedNextTurn ? 4f : 0f)
+ (threat.IsCapital ? 3f : 0f)
- (threat.HasWall ? 2f : 0f);
threat.IsCritical = threat.CanBeThreatenedNextTurn
@ -211,6 +213,17 @@ namespace Logic.AI.Director
return AIDirectorMath.Distance(ctx.Map, enemyGrid, cityGrid) <= Mathf.Max(1, reach);
}
private static bool CityCenterHasFriendlyGuard(AIDirectorContext ctx, GridData cityGrid)
{
if (ctx?.Map == null || ctx.Player == null || cityGrid == null) return false;
return cityGrid.RealUnit(ctx.Map, out var unit)
&& unit != null
&& unit.IsAlive()
&& ctx.Map.GetPlayerDataByUnitId(unit.Id, out var owner)
&& owner != null
&& ctx.Map.SameUnion(ctx.Player.Id, owner.Id);
}
private void BuildStrategicPosture(AIDirectorContext ctx, AIDirectorWorldCache cache)
{
if (cache.HasCriticalCityThreat)