Tune AI director tactical defense loop
This commit is contained in:
parent
7ea8f7c852
commit
45f3cffd73
@ -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 当普通近战送进高威胁格。
|
||||
- 脆弱特殊衍生单位不承担危险扩张任务;目标格已经有即时威胁时,让普通兵、机动兵或主力英雄去处理。
|
||||
- 特色单位的目标不是单纯提高出场次数,而是要在伤害、控制、召唤、献祭、占领或技能签名变化中表现出可读价值。
|
||||
|
||||
防守指标解释:
|
||||
|
||||
| 指标 | 含义 |
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user