Improve AI Director expansion and action pruning
This commit is contained in:
parent
694df5ac4c
commit
05db312da7
@ -55,6 +55,7 @@ AI 不是一次性规划完整回合,而是重复执行“读局势,选一
|
||||
```text
|
||||
如果城市危险,它去救城。
|
||||
否则如果眼前能打有价值目标,它攻击。
|
||||
否则如果还没有稳定二城,且附近有可占城中心,它优先去扩张。
|
||||
否则如果脚下或附近有占领、遗迹、资源、恢复、升级机会,它做机会动作。
|
||||
否则如果它空闲,它向防守、进攻或发展战线移动。
|
||||
否则它交给内政或兜底逻辑。
|
||||
@ -87,17 +88,18 @@ Director 使用固定优先级车道。车道之间不做统一评分,车道
|
||||
|---|---|---|
|
||||
| 1 | Emergency | 阻止立即丢城、被偷家、关键单位暴毙 |
|
||||
| 2 | HeroManagement | 处理选英雄、英雄任务、出场和复活节奏 |
|
||||
| 3 | HeroPlaybook | 发挥英雄机制和阵营特色 |
|
||||
| 4 | Tactic | 处理当前可攻击或近距离交战 |
|
||||
| 5 | UnitOpportunity | 抢占领、遗迹、采集、恢复、升级等确定收益 |
|
||||
| 6 | Front | 把空闲单位送往正确战略方向 |
|
||||
| 7 | Growth | 推进城市、地块、科技、文化、外交 |
|
||||
| 8 | Fallback | 防止规则缺口导致 AI 停摆 |
|
||||
| 3 | Expansion | 二城前和早期高价值扩张优先抢可占城中心 |
|
||||
| 4 | HeroPlaybook | 发挥英雄机制和阵营特色 |
|
||||
| 5 | Tactic | 处理当前可攻击或近距离交战 |
|
||||
| 6 | UnitOpportunity | 抢占领、遗迹、采集、恢复、升级等确定收益 |
|
||||
| 7 | Front | 把空闲单位送往正确战略方向 |
|
||||
| 8 | Growth | 推进城市、地块、科技、文化、外交 |
|
||||
| 9 | Fallback | 防止规则缺口导致 AI 停摆 |
|
||||
|
||||
车道顺序的核心含义:
|
||||
|
||||
```text
|
||||
活命 > 英雄体系 > 英雄特色 > 当前战斗 > 短期机会 > 战略移动 > 长期发展 > 兜底
|
||||
活命 > 英雄体系 > 早期扩张 > 英雄特色 > 当前战斗 > 短期机会 > 战略移动 > 长期发展 > 兜底
|
||||
```
|
||||
|
||||
---
|
||||
@ -144,6 +146,7 @@ Director 使用固定优先级车道。车道之间不做统一评分,车道
|
||||
- 不保存跨多回合的大型黑板式复杂状态。
|
||||
- 不建立永久军团状态机。
|
||||
- 大范围搜索只保留 TopN 目标。
|
||||
- 同一玩家同一回合内,已经执行过的同一个 action stableKey 不再进入 Director 候选,避免单位移动、城市训练等动作反复被选中。
|
||||
|
||||
---
|
||||
|
||||
@ -186,7 +189,12 @@ Defense 的行为倾向:
|
||||
|
||||
Expansion 的行为倾向:
|
||||
|
||||
- UnitOpportunity 优先占领和探索。
|
||||
- 二城前,非严重城市威胁下,可占城中心优先于普通战线移动。
|
||||
- 可占城中心包括 `ResourceType.CityCenter` 村庄、无归属城市,也包括当前地图数据中表现为非同盟 owner 的空城/村点。
|
||||
- 早期扩张只追可在有限回合内转化的目标,避免全地图远距离追城导致移动量很大但二城率不变。
|
||||
- Expansion 只扫描已排序的少量高价值扩张目标;扩大城市数不能让每次决策退化成“所有目标 × 所有单位”的全量搜索。
|
||||
- 单位移动优先满足“本回合能占”或“下回合能站到可占点旁边”,远距离目标只保留作低优先级战线。
|
||||
- Expansion 车道优先处理占领和向目标靠近;UnitOpportunity 负责脚下的占领、遗迹和采集补漏。
|
||||
- Development Front 指向村庄、遗迹、资源和边界。
|
||||
- 城市优先增长和基础建设。
|
||||
- 科技优先资源开发、移动能力和道路。
|
||||
@ -462,7 +470,7 @@ Director 不直接推演行为结果,而是从合法 Action 池中选择。
|
||||
| 单位行为 | Capture、Examine、Gather、Recover、Upgrade、HeroUpgrade、CultureUnitUpgrade、英雄主动 |
|
||||
| 城市行为 | TrainUnit、CityLevelUpAction、CityAction、StartWonder、BuildWonder |
|
||||
| 地块行为 | Gain、Build、GridMisc |
|
||||
| 玩家行为 | LearnTech、BuyCultureCard、PlayerAction |
|
||||
| 玩家行为 | LearnTech、PlayerAction;BuyCultureCard 暂不进入 AI 候选,等文化卡策略单独回归 |
|
||||
| 英雄管理 | SelectHero、FinishHeroTask、出场、复活 |
|
||||
|
||||
危险动作默认不进入普通 AI 选择:
|
||||
@ -510,7 +518,10 @@ AI 可以重新实现,但必须遵守 TH1 的游戏架构:
|
||||
- 局部战斗只看可接触范围。
|
||||
- Front 只保留少量高价值目标。
|
||||
- DevelopmentTarget 只保留 TopN。
|
||||
- Expansion 从 DevelopmentTarget 中只扫描前 N 个扩张目标。
|
||||
- UnitMove 不生成整张地图的所有可走格;先从城市威胁、扩张目标、战线、自城、局部战斗收集移动锚点,每个单位只保留最靠近锚点的少量移动候选。
|
||||
- 行动候选只生成一次。
|
||||
- 同一玩家回合内已经执行过的 stableKey 不再参与下一次候选选择。
|
||||
- 车道只查缓存和动作池。
|
||||
|
||||
如果卡顿,优先削减:
|
||||
|
||||
@ -74,6 +74,7 @@ SkillBase
|
||||
Cache: WorldCache
|
||||
Actions: ActionPool
|
||||
Trace: List<string>
|
||||
BlockedActionKeysThisTurn: Set<string>
|
||||
```
|
||||
|
||||
### 2.2 AIConfig
|
||||
@ -87,6 +88,8 @@ SkillBase
|
||||
MaxActionCount = 4096
|
||||
MaxFrontCount = 12
|
||||
MaxDevelopmentTargetCount = 20
|
||||
MaxExpansionTargetScanCount = 8
|
||||
MaxMoveActionsPerUnit = 8
|
||||
LowHpRatio = 0.45
|
||||
CriticalHpRatio = 0.25
|
||||
HeroLowHpRatio = 0.60
|
||||
@ -107,6 +110,7 @@ SkillBase
|
||||
枚举 Lane:
|
||||
Emergency
|
||||
HeroManagement
|
||||
Expansion
|
||||
HeroPlaybook
|
||||
Tactic
|
||||
UnitOpportunity
|
||||
@ -208,8 +212,9 @@ SkillBase
|
||||
### 3.1 Decide
|
||||
|
||||
```text
|
||||
函数 Decide(map, player, config):
|
||||
函数 Decide(map, player, config, blockedActionKeysThisTurn):
|
||||
ctx = new AIContext(map, player, config)
|
||||
ctx.BlockedActionKeysThisTurn = blockedActionKeysThisTurn
|
||||
|
||||
如果 map == null 或 player == null 或 player.Alive == false:
|
||||
返回 NoDecision
|
||||
@ -223,6 +228,7 @@ SkillBase
|
||||
lanes = [
|
||||
TryEmergency,
|
||||
TryHeroManagement,
|
||||
TryExpansion,
|
||||
TryHeroPlaybook,
|
||||
TryTactic,
|
||||
TryUnitOpportunity,
|
||||
@ -264,7 +270,8 @@ SkillBase
|
||||
```text
|
||||
函数 BuildActionPool(ctx):
|
||||
pool = new ActionPool
|
||||
rawActions = GenerateAllLegalLikeActions(ctx.Map, ctx.Player)
|
||||
moveAnchors = BuildMoveAnchorGridIds(ctx)
|
||||
rawActions = GenerateDirectorActions(ctx.Map, ctx.Player, moveAnchors, Config.MaxMoveActionsPerUnit)
|
||||
|
||||
对每个 rawAction in rawActions 按稳定顺序:
|
||||
如果 pool.All.Count >= Config.MaxActionCount:
|
||||
@ -274,6 +281,9 @@ SkillBase
|
||||
如果 action == null:
|
||||
继续
|
||||
|
||||
如果 StableActionKey(action) in ctx.BlockedActionKeysThisTurn:
|
||||
继续
|
||||
|
||||
action.Param.RefreshParams()
|
||||
如果 !action.ActionLogic.CheckCan(action.Param):
|
||||
继续
|
||||
@ -286,6 +296,48 @@ SkillBase
|
||||
返回 pool
|
||||
```
|
||||
|
||||
```text
|
||||
函数 BuildMoveAnchorGridIds(ctx):
|
||||
anchors = []
|
||||
|
||||
对每个 cityThreat in ctx.Cache.CityThreats:
|
||||
anchors.Add(cityThreat.CityGrid)
|
||||
anchors.Add(每个威胁单位所在格)
|
||||
|
||||
对每个 developmentTarget in ctx.Cache.DevelopmentTargets:
|
||||
anchors.Add(developmentTarget.Grid)
|
||||
|
||||
对每个 front in ctx.Cache.Fronts:
|
||||
anchors.Add(front.TargetGrid)
|
||||
anchors.Add(front.AnchorGrid)
|
||||
|
||||
对每个 selfCity in ctx.Cache.SelfCities:
|
||||
anchors.Add(selfCity.Grid)
|
||||
|
||||
对每个 localBattle in ctx.Cache.LocalBattles:
|
||||
anchors.Add(localBattle.EnemyGrid)
|
||||
anchors.Add(localBattle.SelfGrid)
|
||||
|
||||
返回去重后的 anchors
|
||||
```
|
||||
|
||||
```text
|
||||
函数 GenerateDirectorActions(map, player, moveAnchors, maxMoveActionsPerUnit):
|
||||
普通非移动动作按 CheckCan 生成。
|
||||
|
||||
对每个可移动单位:
|
||||
先计算 UnitMoveInfo。
|
||||
收集该单位本回合所有可移动候选格。
|
||||
按“距离 moveAnchors 最近、城市格优先、已占格降权、稳定 GridId”排序。
|
||||
只保留前 maxMoveActionsPerUnit 个 UnitMove 动作。
|
||||
|
||||
对每个可攻击单位:
|
||||
只在 moveAnchors 中检查可见敌方单位格是否可攻击。
|
||||
如果可攻击则生成 UnitAttack 动作。
|
||||
|
||||
返回动作列表
|
||||
```
|
||||
|
||||
### 4.2 动作分类
|
||||
|
||||
```text
|
||||
@ -321,9 +373,12 @@ SkillBase
|
||||
pool.GridActions.Add(action)
|
||||
pool.ByGrid[action.GridId].Add(action)
|
||||
|
||||
如果 id.ActionType == LearnTech 或 BuyCultureCard:
|
||||
如果 id.ActionType == LearnTech:
|
||||
pool.PlayerActions.Add(action)
|
||||
|
||||
如果 id.ActionType == BuyCultureCard:
|
||||
继续
|
||||
|
||||
如果 id.ActionType == PlayerAction:
|
||||
如果 id.PlayerActionType in [SelectHero, FinishHeroTask]:
|
||||
pool.HeroManagementActions.Add(action)
|
||||
@ -756,6 +811,8 @@ SkillBase
|
||||
target = TryBuildDevelopmentTarget(ctx, cache, grid)
|
||||
如果 target == null:
|
||||
继续
|
||||
如果 target.Distance > Config.DevelopmentSearchRange:
|
||||
继续
|
||||
如果 GridThreatToAnySelfUnit(ctx, grid) 太高 且 target.Type 不是 EnemyEmptyCity:
|
||||
继续
|
||||
targets.Add(target)
|
||||
@ -766,7 +823,7 @@ SkillBase
|
||||
|
||||
```text
|
||||
函数 TryBuildDevelopmentTarget(ctx, cache, grid):
|
||||
如果 grid 是可占村庄:
|
||||
如果 grid.Resource == CityCenter 且 grid 上没有己方城市:
|
||||
返回 DevelopmentTarget(grid, Village, 900)
|
||||
|
||||
如果 grid 是敌方空城中心:
|
||||
@ -784,6 +841,27 @@ SkillBase
|
||||
返回 null
|
||||
```
|
||||
|
||||
可占城中心的识别规则:
|
||||
|
||||
```text
|
||||
函数 ResolveExpansionCityTarget(ctx, city):
|
||||
owner = GetPlayerDataByCityId(city.Id)
|
||||
|
||||
如果 owner == null:
|
||||
返回 Village
|
||||
|
||||
如果 owner 与 ctx.Player 同盟:
|
||||
返回 None
|
||||
|
||||
如果 city 中心可被 Capture 行为占领:
|
||||
返回 EnemyEmptyCity
|
||||
|
||||
返回 None
|
||||
```
|
||||
|
||||
说明:TH1 的地图数据里,有些策划意义上的“村庄/空城”已经存在 `CityData` 和 owner 映射。
|
||||
所以 Expansion 不能只认 owner 为空的城;二城前也必须把可占的非同盟城中心当作扩张目标处理。
|
||||
|
||||
### 5.9 BuildFronts
|
||||
|
||||
```text
|
||||
@ -862,6 +940,8 @@ SkillBase
|
||||
```text
|
||||
函数 BuildDevelopmentFronts(ctx, cache):
|
||||
对每个 target in cache.DevelopmentTargets:
|
||||
如果 己方城市数 < Config.ExpansionUrgentCityThreshold 且 target.Type in [Village, EnemyEmptyCity]:
|
||||
继续
|
||||
selfCity = target.NearestSelfCity
|
||||
yield Front(
|
||||
Type = Development,
|
||||
@ -1232,9 +1312,169 @@ SkillBase
|
||||
|
||||
---
|
||||
|
||||
## 8. HeroPlaybook Lane
|
||||
## 8. Expansion Lane
|
||||
|
||||
### 8.1 TryHeroPlaybook
|
||||
Expansion Lane 只处理早期扩张的刚性目标,不处理普通资源开发。
|
||||
它的职责是让 AI 在没有严重城防危机时,优先拿到第二座城市或明显高价值的可占城中心。
|
||||
|
||||
### 8.1 TryExpansion
|
||||
|
||||
```text
|
||||
函数 TryExpansion(ctx):
|
||||
如果 !ShouldPushExpansion(ctx):
|
||||
返回 None
|
||||
|
||||
best = None
|
||||
scannedTargetCount = 0
|
||||
|
||||
对每个 target in ctx.Cache.DevelopmentTargets:
|
||||
如果 !IsExpansionTarget(ctx, target):
|
||||
继续
|
||||
|
||||
如果 Config.MaxExpansionTargetScanCount > 0 且 scannedTargetCount >= Config.MaxExpansionTargetScanCount:
|
||||
break
|
||||
|
||||
scannedTargetCount += 1
|
||||
|
||||
capture = TryExpansionCapture(ctx, target)
|
||||
best = MaxCandidate(best, capture)
|
||||
|
||||
move = TryExpansionMove(ctx, target)
|
||||
best = MaxCandidate(best, move)
|
||||
|
||||
返回 best
|
||||
```
|
||||
|
||||
### 8.2 ShouldPushExpansion
|
||||
|
||||
```text
|
||||
函数 ShouldPushExpansion(ctx):
|
||||
如果 有严重城市威胁:
|
||||
返回 false
|
||||
|
||||
如果 ctx.Cache.DevelopmentTargets 为空:
|
||||
返回 false
|
||||
|
||||
如果 己方城市数 < Config.ExpansionUrgentCityThreshold:
|
||||
返回 true
|
||||
|
||||
如果 当前回合 <= Config.ExpansionHardPressureTurn 且存在扩张目标:
|
||||
返回 true
|
||||
|
||||
返回 false
|
||||
```
|
||||
|
||||
### 8.3 IsExpansionTarget
|
||||
|
||||
```text
|
||||
函数 IsExpansionTarget(ctx, target):
|
||||
如果 target == null 或 target.Grid == null:
|
||||
返回 false
|
||||
|
||||
如果 target.Type == Village:
|
||||
返回 true
|
||||
|
||||
如果 target.Type == EnemyEmptyCity:
|
||||
如果 target.Distance > Config.DevelopmentSearchRange:
|
||||
返回 false
|
||||
如果 己方城市数 < Config.ExpansionUrgentCityThreshold:
|
||||
返回 true
|
||||
如果 当前回合 > Config.ExpansionHardPressureTurn:
|
||||
返回 true
|
||||
|
||||
返回 false
|
||||
```
|
||||
|
||||
### 8.4 TryExpansionCapture
|
||||
|
||||
```text
|
||||
函数 TryExpansionCapture(ctx, target):
|
||||
best = None
|
||||
|
||||
对每个 action in ctx.Actions.UnitActions:
|
||||
如果 action.UnitActionType != Capture:
|
||||
继续
|
||||
|
||||
unit = action.Unit
|
||||
unitGrid = unit.Grid
|
||||
如果 unitGrid != target.Grid:
|
||||
继续
|
||||
|
||||
score = ScoreExpansionTarget(ctx, target) + 420
|
||||
candidate = Candidate(action, Expansion, score, "扩张占领")
|
||||
best = MaxCandidate(best, candidate)
|
||||
|
||||
返回 best
|
||||
```
|
||||
|
||||
### 8.5 TryExpansionMove
|
||||
|
||||
```text
|
||||
函数 TryExpansionMove(ctx, target):
|
||||
best = None
|
||||
|
||||
对每个 unit in ctx.Cache.SelfUnits:
|
||||
如果 unit 没有移动行动点:
|
||||
继续
|
||||
如果 unit 是严重城市威胁中的关键守军:
|
||||
继续
|
||||
如果 unit 低血且脚下有威胁:
|
||||
继续
|
||||
|
||||
startGrid = unit.Grid
|
||||
如果 startGrid == target.Grid:
|
||||
继续
|
||||
|
||||
action = FindBestMove(unit, target.Grid)
|
||||
endGrid = ActionEndGrid(action)
|
||||
如果 endGrid == null:
|
||||
继续
|
||||
|
||||
如果 endGrid 没有更接近 target 且 startGrid 距离 target > 1:
|
||||
继续
|
||||
|
||||
score = ScoreExpansionTarget(ctx, target)
|
||||
score += 接近距离 * 90
|
||||
如果 endGrid == target.Grid: score += 360
|
||||
如果 endGrid 距离 target == 1: score += 160
|
||||
如果 unit 是高机动单位: score += 80
|
||||
如果 startGrid 距离 target >= 5 且 本次只推进 1 格:
|
||||
score -= 120
|
||||
score -= endGrid 到 target 的距离 * 28
|
||||
score -= GridThreat(ctx, endGrid) * ThreatFactor(target)
|
||||
|
||||
candidate = Candidate(action, Expansion, score, "扩张移动")
|
||||
best = MaxCandidate(best, candidate)
|
||||
|
||||
返回 best
|
||||
```
|
||||
|
||||
### 8.6 ScoreExpansionTarget
|
||||
|
||||
```text
|
||||
函数 ScoreExpansionTarget(ctx, target):
|
||||
score = 740 + target.Value
|
||||
|
||||
如果 target.Type == Village:
|
||||
score += 520
|
||||
如果 target.Type == EnemyEmptyCity:
|
||||
score += 180
|
||||
|
||||
如果 己方城市数 < Config.ExpansionUrgentCityThreshold:
|
||||
score += 360
|
||||
|
||||
如果 当前回合 <= Config.ExpansionHardPressureTurn:
|
||||
score += 180
|
||||
|
||||
score -= target.Distance * 20
|
||||
返回 score
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. HeroPlaybook Lane
|
||||
|
||||
### 9.1 TryHeroPlaybook
|
||||
|
||||
```text
|
||||
函数 TryHeroPlaybook(ctx):
|
||||
@ -1247,7 +1487,7 @@ SkillBase
|
||||
返回 best
|
||||
```
|
||||
|
||||
### 8.2 HeroRule
|
||||
### 9.2 HeroRule
|
||||
|
||||
```text
|
||||
结构 HeroRule:
|
||||
@ -1269,7 +1509,7 @@ SkillBase
|
||||
Reason
|
||||
```
|
||||
|
||||
### 8.3 EvaluateHeroPlaybook
|
||||
### 9.3 EvaluateHeroPlaybook
|
||||
|
||||
```text
|
||||
函数 EvaluateHeroPlaybook(ctx, state):
|
||||
@ -1294,7 +1534,7 @@ SkillBase
|
||||
返回 EvaluateGenericHeroRule(ctx, state)
|
||||
```
|
||||
|
||||
### 8.4 BuildHeroAction
|
||||
### 9.4 BuildHeroAction
|
||||
|
||||
```text
|
||||
函数 BuildHeroAction(ctx, state, rule):
|
||||
@ -1324,7 +1564,7 @@ SkillBase
|
||||
返回 null
|
||||
```
|
||||
|
||||
### 8.5 通用英雄保命
|
||||
### 9.5 通用英雄保命
|
||||
|
||||
```text
|
||||
函数 EvaluateGenericHeroRule(ctx, state):
|
||||
@ -1347,7 +1587,7 @@ SkillBase
|
||||
返回 None
|
||||
```
|
||||
|
||||
### 8.6 默认英雄规则
|
||||
### 9.6 默认英雄规则
|
||||
|
||||
```text
|
||||
Flandre:
|
||||
@ -1469,7 +1709,7 @@ Byakuren / Miko / Zanmu:
|
||||
|
||||
---
|
||||
|
||||
## 9. Tactic Lane
|
||||
## 10. Tactic Lane
|
||||
|
||||
```text
|
||||
函数 TryTactic(ctx):
|
||||
@ -1518,7 +1758,7 @@ Byakuren / Miko / Zanmu:
|
||||
|
||||
---
|
||||
|
||||
## 10. UnitOpportunity Lane
|
||||
## 11. UnitOpportunity Lane
|
||||
|
||||
```text
|
||||
函数 TryUnitOpportunity(ctx):
|
||||
@ -1629,7 +1869,7 @@ Byakuren / Miko / Zanmu:
|
||||
|
||||
---
|
||||
|
||||
## 11. Front Lane
|
||||
## 12. Front Lane
|
||||
|
||||
```text
|
||||
函数 TryFront(ctx):
|
||||
@ -1703,7 +1943,7 @@ Byakuren / Miko / Zanmu:
|
||||
|
||||
---
|
||||
|
||||
## 12. Growth Lane
|
||||
## 13. Growth Lane
|
||||
|
||||
```text
|
||||
函数 TryGrowth(ctx):
|
||||
@ -1716,7 +1956,7 @@ Byakuren / Miko / Zanmu:
|
||||
返回 best
|
||||
```
|
||||
|
||||
### 12.1 CityGrowth
|
||||
### 13.1 CityGrowth
|
||||
|
||||
```text
|
||||
函数 TryCityGrowth(ctx):
|
||||
@ -1765,7 +2005,7 @@ Byakuren / Miko / Zanmu:
|
||||
返回 score
|
||||
```
|
||||
|
||||
### 12.2 GridGrowth
|
||||
### 13.2 GridGrowth
|
||||
|
||||
```text
|
||||
函数 TryGridGrowth(ctx):
|
||||
@ -1802,7 +2042,7 @@ Byakuren / Miko / Zanmu:
|
||||
返回 score
|
||||
```
|
||||
|
||||
### 12.3 PlayerGrowth
|
||||
### 13.3 PlayerGrowth
|
||||
|
||||
```text
|
||||
函数 TryPlayerGrowth(ctx):
|
||||
@ -1824,7 +2064,7 @@ Byakuren / Miko / Zanmu:
|
||||
返回 ScoreTech(ctx, action)
|
||||
|
||||
如果 id.ActionType == BuyCultureCard:
|
||||
返回 ScoreCultureCard(ctx, action)
|
||||
返回 0
|
||||
|
||||
如果 id.ActionType == PlayerAction:
|
||||
返回 ScorePlayerAction(ctx, action)
|
||||
@ -1858,6 +2098,9 @@ Byakuren / Miko / Zanmu:
|
||||
|
||||
```text
|
||||
函数 ScoreCultureCard(ctx, action):
|
||||
当前 AI 不生成 BuyCultureCard 动作。
|
||||
此函数只作为未来恢复文化卡策略时的评分入口。
|
||||
|
||||
score = 300
|
||||
|
||||
如果 前线吃紧 且文化卡提供即时战力:
|
||||
@ -1899,7 +2142,7 @@ Byakuren / Miko / Zanmu:
|
||||
|
||||
---
|
||||
|
||||
## 13. Fallback
|
||||
## 14. Fallback
|
||||
|
||||
```text
|
||||
函数 TryFallback(ctx):
|
||||
@ -1927,9 +2170,9 @@ Fallback 的目标是不断回合,不负责聪明。大量进入 Fallback 说
|
||||
|
||||
---
|
||||
|
||||
## 14. 评分辅助函数
|
||||
## 15. 评分辅助函数
|
||||
|
||||
### 14.1 稳定排序
|
||||
### 15.1 稳定排序
|
||||
|
||||
```text
|
||||
函数 MaxCandidate(a, b):
|
||||
@ -1958,7 +2201,7 @@ Fallback 的目标是不断回合,不负责聪明。大量进入 Fallback 说
|
||||
)
|
||||
```
|
||||
|
||||
### 14.2 伤害和威胁
|
||||
### 15.2 伤害和威胁
|
||||
|
||||
```text
|
||||
函数 EstimateDamageValue(ctx, attacker, target):
|
||||
@ -1983,7 +2226,7 @@ Fallback 的目标是不断回合,不负责聪明。大量进入 Fallback 说
|
||||
return EstimateDamage(ctx, attacker, target) >= target.Health
|
||||
```
|
||||
|
||||
### 14.3 资源和建设价值
|
||||
### 15.3 资源和建设价值
|
||||
|
||||
```text
|
||||
函数 ResourceValue(ctx, grid):
|
||||
@ -2018,7 +2261,7 @@ Fallback 的目标是不断回合,不负责聪明。大量进入 Fallback 说
|
||||
返回 value
|
||||
```
|
||||
|
||||
### 14.4 训练单位价值
|
||||
### 15.4 训练单位价值
|
||||
|
||||
```text
|
||||
函数 TrainUnitDefenseValue(ctx, action, threat):
|
||||
@ -2045,9 +2288,9 @@ Fallback 的目标是不断回合,不负责聪明。大量进入 Fallback 说
|
||||
|
||||
---
|
||||
|
||||
## 15. 正确性约束
|
||||
## 16. 正确性约束
|
||||
|
||||
### 15.1 权威行为
|
||||
### 16.1 权威行为
|
||||
|
||||
```text
|
||||
所有最终动作:
|
||||
@ -2068,7 +2311,7 @@ AI 不复制:
|
||||
回放记录
|
||||
```
|
||||
|
||||
### 15.2 确定性
|
||||
### 16.2 确定性
|
||||
|
||||
禁止在权威选择路径使用:
|
||||
|
||||
@ -2082,7 +2325,7 @@ UI/Renderer 状态
|
||||
|
||||
同分必须用稳定 key 解决。
|
||||
|
||||
### 15.3 技能安全
|
||||
### 16.3 技能安全
|
||||
|
||||
英雄和单位技能只通过 Action 入口触发:
|
||||
|
||||
@ -2098,7 +2341,7 @@ AI 不直接调用 `SkillBase` 的效果方法。
|
||||
|
||||
---
|
||||
|
||||
## 16. 性能约束
|
||||
## 17. 性能约束
|
||||
|
||||
```text
|
||||
每次 Decide:
|
||||
@ -2130,9 +2373,9 @@ Growth: 只遍历合法动作
|
||||
|
||||
---
|
||||
|
||||
## 17. 测试脚本标准
|
||||
## 18. 测试脚本标准
|
||||
|
||||
### 17.1 城市防守
|
||||
### 18.1 城市防守
|
||||
|
||||
```text
|
||||
给 AI 城市附近放敌军。
|
||||
@ -2140,7 +2383,7 @@ Growth: 只遍历合法动作
|
||||
Emergency 返回攻击威胁、回防、或危险城市生产。
|
||||
```
|
||||
|
||||
### 17.2 占领扩张
|
||||
### 18.2 占领扩张
|
||||
|
||||
```text
|
||||
给 AI 单位脚下放村庄、敌空城、遗迹。
|
||||
@ -2148,7 +2391,7 @@ Growth: 只遍历合法动作
|
||||
无更高车道时 UnitOpportunity 执行占领或探索。
|
||||
```
|
||||
|
||||
### 17.3 英雄机制
|
||||
### 18.3 英雄机制
|
||||
|
||||
```text
|
||||
给 AI 放残血友军、敌方英雄、地面攻击目标、自爆窗口。
|
||||
@ -2156,7 +2399,7 @@ Growth: 只遍历合法动作
|
||||
HeroPlaybook 选择对应英雄动作。
|
||||
```
|
||||
|
||||
### 17.4 战线移动
|
||||
### 18.4 战线移动
|
||||
|
||||
```text
|
||||
无局部战斗、无机会动作。
|
||||
@ -2166,7 +2409,7 @@ Growth: 只遍历合法动作
|
||||
DevelopmentFront 优先于 HoldFront。
|
||||
```
|
||||
|
||||
### 17.5 内政发展
|
||||
### 18.5 内政发展
|
||||
|
||||
```text
|
||||
安全局面。
|
||||
@ -2175,7 +2418,7 @@ Growth: 只遍历合法动作
|
||||
前线城市生产军力。
|
||||
```
|
||||
|
||||
### 17.6 性能
|
||||
### 18.6 性能
|
||||
|
||||
```text
|
||||
中大型地图 AI 连续执行。
|
||||
|
||||
@ -231,6 +231,8 @@ namespace Logic.AI
|
||||
public CommonActionParams TargetParam;
|
||||
public Strategy TargetStrategy;
|
||||
public List<uint> TargetList;
|
||||
public bool UseTargetListAsMoveAnchors;
|
||||
public int MaxMoveActionsPerUnit;
|
||||
|
||||
public HashSet<string> Marks;
|
||||
|
||||
@ -328,6 +330,8 @@ namespace Logic.AI
|
||||
ForeachLegion = new List<uint>();
|
||||
ForeachCity = new List<CityData>();
|
||||
TargetList = new List<uint>();
|
||||
UseTargetListAsMoveAnchors = false;
|
||||
MaxMoveActionsPerUnit = 0;
|
||||
AroundGridBuffer = new List<GridData>();
|
||||
TmpUnitSetBuffer = new HashSet<UnitData>();
|
||||
TmpCityListBuffer = new List<CityData>();
|
||||
@ -446,6 +450,8 @@ namespace Logic.AI
|
||||
ForeachLegion.Clear();
|
||||
ForeachCity.Clear();
|
||||
TargetList.Clear();
|
||||
UseTargetListAsMoveAnchors = false;
|
||||
MaxMoveActionsPerUnit = 0;
|
||||
|
||||
CityStrategy.Clear();
|
||||
FreeUnitStrategy.Clear();
|
||||
|
||||
@ -346,6 +346,70 @@ namespace Logic.AI
|
||||
|
||||
return data.AIActions;
|
||||
}
|
||||
|
||||
public static List<AIActionBase> GeneratorDirectorActionIdsForUse(
|
||||
MapData map,
|
||||
PlayerData selfPlayer,
|
||||
IEnumerable<uint> moveAnchorGridIds,
|
||||
int maxMoveActionsPerUnit)
|
||||
{
|
||||
var data = new AICalculatorData();
|
||||
data.Map = map;
|
||||
data.Player = selfPlayer;
|
||||
data.UseTargetListAsMoveAnchors = true;
|
||||
data.MaxMoveActionsPerUnit = maxMoveActionsPerUnit;
|
||||
if (moveAnchorGridIds != null)
|
||||
{
|
||||
foreach (var gridId in moveAnchorGridIds)
|
||||
{
|
||||
if (!data.TargetList.Contains(gridId)) data.TargetList.Add(gridId);
|
||||
}
|
||||
}
|
||||
|
||||
using var pooledSelfUnits = THCollectionPool.GetHashSetHandle<UnitData>(out var selfUnits);
|
||||
map.GetUnitDataListByPlayerId(selfPlayer.Id, selfUnits);
|
||||
using var pooledSelfCities = THCollectionPool.GetHashSetHandle<CityData>(out var selfCities);
|
||||
map.GetCityDataListByPlayerId(selfPlayer.Id, selfCities);
|
||||
|
||||
data.TargetParam.MapData = map;
|
||||
data.TargetParam.PlayerData = selfPlayer;
|
||||
GeneratorActionIds(data, CommonActionType.LearnTech);
|
||||
GeneratorActionIds(data, CommonActionType.StartWonder);
|
||||
GeneratorActionIds(data, CommonActionType.PlayerAction);
|
||||
foreach (var city in selfCities)
|
||||
{
|
||||
data.TargetParam.CityData = city;
|
||||
GeneratorActionIds(data, CommonActionType.Gain);
|
||||
GeneratorActionIds(data, CommonActionType.Build);
|
||||
GeneratorActionIds(data, CommonActionType.BuildWonder);
|
||||
GeneratorActionIds(data, CommonActionType.GridMisc);
|
||||
GeneratorActionIds(data, CommonActionType.TrainUnit);
|
||||
GeneratorActionIds(data, CommonActionType.CityLevelUpAction);
|
||||
}
|
||||
|
||||
foreach (var unit in selfUnits)
|
||||
{
|
||||
data.TargetParam.UnitData = unit;
|
||||
GeneratorActionIds(data, CommonActionType.UnitAction);
|
||||
GeneratorActionIds(data, CommonActionType.UnitSkill);
|
||||
GeneratorActionIds(data, CommonActionType.UnitMove);
|
||||
GeneratorActionIds(data, CommonActionType.UnitAttack);
|
||||
GeneratorActionIds(data, CommonActionType.UnitAttackAlly);
|
||||
GeneratorActionIds(data, CommonActionType.UnitAttackGround);
|
||||
}
|
||||
|
||||
for (int i = data.AIActions.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var id = data.AIActions[i].ActionLogic.ActionId;
|
||||
if (id.UnitActionType is UnitActionType.Disband or UnitActionType.ForceDisband or UnitActionType.Demolish or UnitActionType.Disperse
|
||||
|| id.GridMiscActionType == GridMiscActionType.Destroy)
|
||||
{
|
||||
data.AIActions.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
return data.AIActions;
|
||||
}
|
||||
|
||||
public static List<AIActionBase> GeneratorAllActionIds(MapData map, PlayerData selfPlayer)
|
||||
{
|
||||
@ -505,6 +569,11 @@ namespace Logic.AI
|
||||
|
||||
data.TargetParam.MainObjectType = ActionLogicFactory.GetMainObjectType(type);
|
||||
Main.UnitLogic.CalcUnitMoveInfo(data.Map, data.TargetParam.UnitData.Id);
|
||||
if (data.UseTargetListAsMoveAnchors)
|
||||
{
|
||||
GenerateMoveActionsTowardAnchors(data, actions, unitGrid);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.TargetList.Count > 0)
|
||||
{
|
||||
@ -572,6 +641,34 @@ namespace Logic.AI
|
||||
data.TargetParam.MainObjectType = ActionLogicFactory.GetMainObjectType(type);
|
||||
|
||||
Main.UnitLogic.CalcUnitMoveInfo(data.Map, data.TargetParam.UnitData.Id);
|
||||
if (data.UseTargetListAsMoveAnchors && data.TargetList.Count > 0)
|
||||
{
|
||||
foreach (var gridId in data.TargetList)
|
||||
{
|
||||
if (!data.Map.GridMap.GetGridDataByGid(gridId, out var grid)) continue;
|
||||
if (!grid.VisibleUnit(data.Map, data.TargetParam.PlayerData, out var targetUnit)) continue;
|
||||
var result = Main.UnitLogic.CheckUnitCanMoveOrAttack(data.Map, data.TargetParam.UnitData, grid);
|
||||
if (result != MoveAttackType.Attack) continue;
|
||||
|
||||
data.TargetParam.GridData = grid;
|
||||
data.TargetParam.TargetUnitData = targetUnit;
|
||||
data.TargetParam.OnParamChanged();
|
||||
|
||||
foreach (var action in actions)
|
||||
{
|
||||
if (!action.CheckCan(data.TargetParam)) continue;
|
||||
var param = data.TargetParam.GetCopyParam();
|
||||
param.CityData = null;
|
||||
param.TargetGridData = null;
|
||||
param.TargetPlayerData = null;
|
||||
param.OnParamChanged();
|
||||
data.AIActions.Add(new AIActionBase(param, action));
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var grid in data.Map.GridMap.GridList)
|
||||
{
|
||||
//TODO check playerData right
|
||||
@ -748,5 +845,70 @@ namespace Logic.AI
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void GenerateMoveActionsTowardAnchors(
|
||||
AICalculatorData data,
|
||||
List<ActionLogicBase> actions,
|
||||
GridData unitGrid)
|
||||
{
|
||||
if (data == null || actions == null || unitGrid == null) return;
|
||||
|
||||
data.AroundGridBuffer.Clear();
|
||||
Main.UnitLogic.CollectMoveAttackCandidateGrids(data.Map, data.TargetParam.UnitData, data.AroundGridBuffer);
|
||||
if (data.AroundGridBuffer.Count == 0) return;
|
||||
|
||||
data.AroundGridBuffer.Sort((a, b) =>
|
||||
{
|
||||
var scoreCompare = ScoreDirectorMoveGrid(data, unitGrid, b)
|
||||
.CompareTo(ScoreDirectorMoveGrid(data, unitGrid, a));
|
||||
if (scoreCompare != 0) return scoreCompare;
|
||||
return a.Id.CompareTo(b.Id);
|
||||
});
|
||||
|
||||
var generated = 0;
|
||||
var limit = data.MaxMoveActionsPerUnit <= 0 ? int.MaxValue : data.MaxMoveActionsPerUnit;
|
||||
foreach (var grid in data.AroundGridBuffer)
|
||||
{
|
||||
var result = Main.UnitLogic.CheckUnitCanMoveOrAttack(data.Map, data.TargetParam.UnitData, grid);
|
||||
if (result != MoveAttackType.Move && result != MoveAttackType.MoveToPort && result != MoveAttackType.MoveAshore && result != MoveAttackType.MoveTeleport) continue;
|
||||
|
||||
data.TargetParam.GridData = grid;
|
||||
data.TargetParam.OnParamChanged();
|
||||
foreach (var action in actions)
|
||||
{
|
||||
if (!action.CheckCan(data.TargetParam)) continue;
|
||||
var param = data.TargetParam.GetCopyParam();
|
||||
param.CityData = null;
|
||||
param.TargetUnitData = null;
|
||||
param.TargetGridData = null;
|
||||
param.TargetPlayerData = null;
|
||||
param.OnParamChanged();
|
||||
data.AIActions.Add(new AIActionBase(param, action));
|
||||
generated++;
|
||||
break;
|
||||
}
|
||||
|
||||
if (generated >= limit) break;
|
||||
}
|
||||
}
|
||||
|
||||
private static int ScoreDirectorMoveGrid(AICalculatorData data, GridData unitGrid, GridData moveGrid)
|
||||
{
|
||||
if (data == null || moveGrid == null) return int.MinValue;
|
||||
var bestDistance = int.MaxValue;
|
||||
foreach (var targetGridId in data.TargetList)
|
||||
{
|
||||
if (!data.Map.GridMap.GetGridDataByGid(targetGridId, out var targetGrid)) continue;
|
||||
var distance = data.Map.GridMap.CalcDistance(moveGrid, targetGrid);
|
||||
if (distance < bestDistance) bestDistance = distance;
|
||||
}
|
||||
|
||||
if (bestDistance == int.MaxValue) bestDistance = data.Map.GridMap.CalcDistance(unitGrid, moveGrid);
|
||||
var score = -bestDistance * 100;
|
||||
if (moveGrid.CityOnGrid(data.Map, out _)) score += 80;
|
||||
if (moveGrid.VisibleUnit(data.Map, data.Player, out _)) score -= 120;
|
||||
if (data.Player?.Sight != null && data.Player.Sight.CheckIsInSight(moveGrid.Id)) score += 10;
|
||||
return score;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@ using System.Collections.Generic;
|
||||
using Logic.Action;
|
||||
using RuntimeData;
|
||||
using TH1_Logic.Action;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Logic.AI.Director
|
||||
{
|
||||
@ -32,7 +31,11 @@ namespace Logic.AI.Director
|
||||
var index = new AIDirectorActionIndex();
|
||||
if (ctx?.Map == null || ctx.Player == null) return index;
|
||||
|
||||
var generated = AIActionGenerator.GeneratorAllActionIdsForUse(ctx.Map, ctx.Player);
|
||||
var generated = AIActionGenerator.GeneratorDirectorActionIdsForUse(
|
||||
ctx.Map,
|
||||
ctx.Player,
|
||||
BuildMoveAnchorGridIds(ctx),
|
||||
ctx.Config.MaxMoveActionsPerUnit);
|
||||
if (generated == null) return index;
|
||||
|
||||
var limit = ctx.Config.MaxGeneratedActions <= 0 ? int.MaxValue : ctx.Config.MaxGeneratedActions;
|
||||
@ -42,6 +45,7 @@ namespace Logic.AI.Director
|
||||
if (index.AllActions.Count >= limit) break;
|
||||
var copied = CopyAction(action);
|
||||
if (copied == null) continue;
|
||||
if (ctx.BlockedActionKeys != null && ctx.BlockedActionKeys.Contains(StableActionKey(copied))) continue;
|
||||
if (!copied.ActionLogic.CheckCan(copied.Param)) continue;
|
||||
if (IsDangerousAction(copied)) continue;
|
||||
index.Add(copied);
|
||||
@ -52,6 +56,49 @@ namespace Logic.AI.Director
|
||||
return index;
|
||||
}
|
||||
|
||||
private static List<uint> BuildMoveAnchorGridIds(AIDirectorContext ctx)
|
||||
{
|
||||
var result = new List<uint>();
|
||||
if (ctx?.Cache == null) return result;
|
||||
|
||||
void Add(GridData grid)
|
||||
{
|
||||
if (grid == null || result.Contains(grid.Id)) return;
|
||||
result.Add(grid.Id);
|
||||
}
|
||||
|
||||
foreach (var threat in ctx.Cache.CityThreats)
|
||||
{
|
||||
Add(threat?.CityGrid);
|
||||
if (threat?.EnemyUnits == null) continue;
|
||||
foreach (var enemy in threat.EnemyUnits)
|
||||
{
|
||||
if (enemy != null && ctx.Map.GetGridDataByUnitId(enemy.Id, out var enemyGrid)) Add(enemyGrid);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var target in ctx.Cache.DevelopmentTargets) Add(target?.Grid);
|
||||
|
||||
foreach (var front in ctx.Cache.Fronts)
|
||||
{
|
||||
Add(front?.TargetGrid);
|
||||
Add(front?.AnchorGrid);
|
||||
}
|
||||
|
||||
foreach (var city in ctx.Cache.SelfCities)
|
||||
{
|
||||
if (city != null && ctx.Map.GetGridDataByCityId(city.Id, out var cityGrid)) Add(cityGrid);
|
||||
}
|
||||
|
||||
foreach (var battle in ctx.Cache.LocalBattles)
|
||||
{
|
||||
Add(battle?.EnemyGrid);
|
||||
Add(battle?.SelfGrid);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public AIDirectorActionCandidate Candidate(AIActionBase action, AIDirectorLane lane, string reason, float priority, bool fallback = false)
|
||||
{
|
||||
if (action == null) return AIDirectorActionCandidate.Invalid(lane, reason, priority, fallback);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using Logic.Action;
|
||||
using RuntimeData;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using TH1_Logic.Action;
|
||||
using UnityEngine;
|
||||
@ -11,7 +12,7 @@ namespace Logic.AI.Director
|
||||
private readonly AIDirectorWorldCacheBuilder _cacheBuilder = new();
|
||||
private readonly AIDirectorHeroRuleEvaluator _heroEvaluator = new();
|
||||
|
||||
public AIDirectorDecision Decide(MapData map, PlayerData player, AIDirectorConfig config = null)
|
||||
public AIDirectorDecision Decide(MapData map, PlayerData player, AIDirectorConfig config = null, HashSet<string> blockedActionKeys = null)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var decision = new AIDirectorDecision();
|
||||
@ -23,7 +24,7 @@ namespace Logic.AI.Director
|
||||
return decision;
|
||||
}
|
||||
|
||||
var ctx = new AIDirectorContext(map, player, config ?? AIDirectorConfig.CreateDefault());
|
||||
var ctx = new AIDirectorContext(map, player, config ?? AIDirectorConfig.CreateDefault(), blockedActionKeys);
|
||||
ctx.Cache = _cacheBuilder.Build(ctx);
|
||||
ctx.ActionIndex = AIDirectorActionIndex.Build(ctx);
|
||||
_cacheBuilder.BuildUnitOpportunities(ctx);
|
||||
@ -66,9 +67,9 @@ namespace Logic.AI.Director
|
||||
return decision;
|
||||
}
|
||||
|
||||
public bool TryDecide(MapData map, PlayerData player, out AIDirectorActionCandidate candidate, AIDirectorConfig config = null)
|
||||
public bool TryDecide(MapData map, PlayerData player, out AIDirectorActionCandidate candidate, AIDirectorConfig config = null, HashSet<string> blockedActionKeys = null)
|
||||
{
|
||||
var decision = Decide(map, player, config);
|
||||
var decision = Decide(map, player, config, blockedActionKeys);
|
||||
candidate = decision.Candidate;
|
||||
return decision.HasAction;
|
||||
}
|
||||
@ -123,9 +124,12 @@ namespace Logic.AI.Director
|
||||
if (!ShouldPushExpansion(ctx)) return false;
|
||||
|
||||
var best = AIDirectorActionCandidate.None;
|
||||
var scannedTargets = 0;
|
||||
foreach (var target in ctx.Cache.DevelopmentTargets)
|
||||
{
|
||||
if (!IsExpansionTarget(ctx, target)) continue;
|
||||
if (ctx.Config.MaxExpansionTargetScanCount > 0 && scannedTargets >= ctx.Config.MaxExpansionTargetScanCount) break;
|
||||
scannedTargets++;
|
||||
|
||||
var capture = TryExpansionCapture(ctx, decision, target);
|
||||
best = MaxCandidate(best, capture);
|
||||
@ -190,9 +194,11 @@ namespace Logic.AI.Director
|
||||
var targetScore = ScoreExpansionTarget(ctx, target);
|
||||
var progressScore = Mathf.Max(0, startDistance - endDistance) * 90f;
|
||||
var reachBonus = endDistance == 0 ? 360f : endDistance == 1 ? 160f : 0f;
|
||||
var mobilityBonus = unit.GetActionPoint(ActionPointType.Move) >= 2 ? 80f : 0f;
|
||||
var nearConversionBonus = startDistance <= 2 ? 180f : startDistance <= 3 ? 80f : 0f;
|
||||
var mobilityBonus = unit.GetActionPoint(ActionPointType.Move) >= 2 ? 120f : 0f;
|
||||
var longDragPenalty = startDistance >= 5 && startDistance - endDistance <= 1 ? 140f : 0f;
|
||||
var safetyPenalty = GridThreat(ctx, endGrid) * (target.TargetType == AIDirectorDevelopmentTargetType.Village ? 0.25f : 0.5f);
|
||||
var score = targetScore + progressScore + reachBonus + mobilityBonus - endDistance * 28f - safetyPenalty;
|
||||
var score = targetScore + progressScore + reachBonus + nearConversionBonus + mobilityBonus - longDragPenalty - endDistance * 28f - safetyPenalty;
|
||||
|
||||
var current = ctx.ActionIndex.Candidate(action, AIDirectorLane.Expansion, $"Expansion.Move.{target.TargetType}", score);
|
||||
AddTerms(
|
||||
@ -200,7 +206,9 @@ namespace Logic.AI.Director
|
||||
("target", targetScore),
|
||||
("progress", progressScore),
|
||||
("reach", reachBonus),
|
||||
("nearConversion", nearConversionBonus),
|
||||
("mobility", mobilityBonus),
|
||||
("longDrag", -longDragPenalty),
|
||||
("distance", -endDistance * 28f),
|
||||
("threat", -safetyPenalty));
|
||||
current.Unit = current.Unit ?? unit;
|
||||
@ -784,6 +792,8 @@ namespace Logic.AI.Director
|
||||
if (target?.Grid == null) return false;
|
||||
if (target.TargetType == AIDirectorDevelopmentTargetType.Village) return true;
|
||||
if (target.TargetType != AIDirectorDevelopmentTargetType.EnemyEmptyCity) return false;
|
||||
if (target.Distance > ctx.Config.DevelopmentSearchRange) return false;
|
||||
if (ctx.Cache.SelfCities.Count < ctx.Config.ExpansionUrgentCityThreshold) return true;
|
||||
return ctx.Cache.SelfCities.Count >= ctx.Config.ExpansionUrgentCityThreshold
|
||||
|| ctx.Player.Turn > ctx.Config.ExpansionHardPressureTurn;
|
||||
}
|
||||
@ -1054,9 +1064,9 @@ namespace Logic.AI.Director
|
||||
|
||||
private bool TechLooksMobility(TechType tech)
|
||||
{
|
||||
return tech is TechType.Climbing or TechType.Roads or TechType.Sailing or TechType.Navigation or TechType.FreeSpirit
|
||||
or TechType.KaguyaRoad or TechType.KanakoClimbing or TechType.KanakoRoads or TechType.KanakoNavigation
|
||||
or TechType.KomeijiIndianSailing or TechType.KomeijiIndianNavigation or TechType.HakureiFishing;
|
||||
return tech is TechType.Climbing or TechType.Riding or TechType.Roads or TechType.Sailing or TechType.Navigation or TechType.FreeSpirit
|
||||
or TechType.KaguyaRoad or TechType.KanakoClimbing or TechType.KanakoRiding or TechType.KanakoRoads or TechType.KanakoNavigation
|
||||
or TechType.KomeijiIndianRiding or TechType.KomeijiIndianSailing or TechType.KomeijiIndianNavigation or TechType.HakureiFishing;
|
||||
}
|
||||
|
||||
private bool TechLooksEconomic(TechType tech)
|
||||
|
||||
@ -137,6 +137,8 @@ namespace Logic.AI.Director
|
||||
public int MaxGeneratedActions = 4096;
|
||||
public int MaxFrontCount = 12;
|
||||
public int MaxDevelopmentTargetCount = 20;
|
||||
public int MaxExpansionTargetScanCount = 8;
|
||||
public int MaxMoveActionsPerUnit = 8;
|
||||
public float LowHealthRatio = 0.45f;
|
||||
public float CriticalHealthRatio = 0.25f;
|
||||
public float HeroLowHealthRatio = 0.6f;
|
||||
@ -168,12 +170,14 @@ namespace Logic.AI.Director
|
||||
public readonly AIDirectorConfig Config;
|
||||
public AIDirectorWorldCache Cache;
|
||||
public AIDirectorActionIndex ActionIndex;
|
||||
public readonly HashSet<string> BlockedActionKeys;
|
||||
|
||||
public AIDirectorContext(MapData map, PlayerData player, AIDirectorConfig config)
|
||||
public AIDirectorContext(MapData map, PlayerData player, AIDirectorConfig config, HashSet<string> blockedActionKeys = null)
|
||||
{
|
||||
Map = map;
|
||||
Player = player;
|
||||
Config = config ?? AIDirectorConfig.CreateDefault();
|
||||
BlockedActionKeys = blockedActionKeys;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -310,7 +310,7 @@ namespace Logic.AI.Director
|
||||
if (grid == null) continue;
|
||||
var target = TryBuildDevelopmentTarget(ctx, cache, grid);
|
||||
if (target == null) continue;
|
||||
if (target.Distance > ctx.Config.DevelopmentSearchRange && target.TargetType != AIDirectorDevelopmentTargetType.EnemyEmptyCity) continue;
|
||||
if (target.Distance > ctx.Config.DevelopmentSearchRange) continue;
|
||||
if (GridThreat(ctx, cache, grid) > 160f && target.TargetType != AIDirectorDevelopmentTargetType.EnemyEmptyCity) continue;
|
||||
targets.Add(target);
|
||||
}
|
||||
@ -368,6 +368,12 @@ namespace Logic.AI.Director
|
||||
foreach (var target in cache.DevelopmentTargets)
|
||||
{
|
||||
if (target.Grid == null) continue;
|
||||
if (cache.SelfCities.Count < ctx.Config.ExpansionUrgentCityThreshold
|
||||
&& target.TargetType is AIDirectorDevelopmentTargetType.Village or AIDirectorDevelopmentTargetType.EnemyEmptyCity)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
cache.Fronts.Add(new AIDirectorFront
|
||||
{
|
||||
FrontType = AIDirectorFrontType.Development,
|
||||
@ -477,6 +483,11 @@ namespace Logic.AI.Director
|
||||
value = 900f + (city.IsCapital ? 160f : 0f);
|
||||
}
|
||||
}
|
||||
else if (grid.Resource == ResourceType.CityCenter)
|
||||
{
|
||||
type = AIDirectorDevelopmentTargetType.Village;
|
||||
value = 1220f + (ctx.Player.Turn <= ctx.Config.ExpansionHardPressureTurn ? 220f : 0f);
|
||||
}
|
||||
else if (grid.Resource != ResourceType.None && !grid.HasBuilding())
|
||||
{
|
||||
type = AIDirectorDevelopmentTargetType.Resource;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using Logic.AI.Director;
|
||||
using Logic.CrashSight;
|
||||
using RuntimeData;
|
||||
using System.Collections.Generic;
|
||||
using TH1_Logic.Action;
|
||||
|
||||
namespace Logic.AI
|
||||
@ -8,6 +9,7 @@ namespace Logic.AI
|
||||
public sealed class DirectorAIKernel : IAIKernel
|
||||
{
|
||||
private readonly AIDirectorLogic _director = new();
|
||||
private readonly HashSet<string> _executedActionKeysThisTurn = new();
|
||||
private MapData _mapData;
|
||||
private PlayerData _playerData;
|
||||
|
||||
@ -21,6 +23,7 @@ namespace Logic.AI
|
||||
{
|
||||
_mapData = mapData;
|
||||
_playerData = playerData;
|
||||
_executedActionKeysThisTurn.Clear();
|
||||
#if TH1_AI_DIRECTOR_DIAGNOSTICS || UNITY_EDITOR
|
||||
AIDirectorDiagnostics.RecordTurnStart(_mapData, _playerData);
|
||||
#endif
|
||||
@ -29,7 +32,7 @@ namespace Logic.AI
|
||||
public AIKernelUpdate Update()
|
||||
{
|
||||
if (_mapData == null || _playerData == null) return AIKernelUpdate.Finished;
|
||||
var decision = _director.Decide(_mapData, _playerData);
|
||||
var decision = _director.Decide(_mapData, _playerData, null, _executedActionKeysThisTurn);
|
||||
#if TH1_AI_DIRECTOR_DIAGNOSTICS || UNITY_EDITOR
|
||||
AIDirectorDiagnostics.RecordDecision(_mapData, _playerData, decision);
|
||||
#endif
|
||||
@ -43,6 +46,7 @@ namespace Logic.AI
|
||||
if (action.IsInSight) action.ActionLogic.CameraControl(action.Param);
|
||||
if (action.ActionLogic.ActionId.PlayerActionType == PlayerActionType.OfferAlly)
|
||||
LogSystem.LogInfo("AI 发起结盟");
|
||||
_executedActionKeysThisTurn.Add(AIDirectorActionIndex.StableActionKey(action));
|
||||
return AIKernelUpdate.ActionReady(action);
|
||||
}
|
||||
|
||||
@ -53,6 +57,7 @@ namespace Logic.AI
|
||||
#endif
|
||||
_mapData = null;
|
||||
_playerData = null;
|
||||
_executedActionKeysThisTurn.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user