TH1/MD/GameMDFramework/19-AI导演系统逻辑语言.md

88 KiB
Raw Blame History

19-AI导演系统逻辑语言

本文档把 18-AI导演系统策划文档 翻译成可落地到代码的逻辑语言。
本文档只描述新 AI 目标实现,不对照既有 AI 实现。


1. 文档约定

1.1 伪代码格式

函数 X(ctx, a, b):
    如果 条件:
        返回 value

    对每个 item in list:
        ...

    返回 value

1.2 动作约束

AI 只能选择合法 Action。

AIAction:
    CommonActionId
    CommonActionParams
    ActionLogicBase

执行动作必须走:

Param.RefreshParams()
ActionLogic.CheckCan(Param)
ActionLogic.CompleteExecute(Param)

AI 不直接修改:

MapData
PlayerData
CityData
UnitData
GridData
SkillBase

1.3 决策原则

车道之间用固定优先级。
车道内部用轻量分数排序。
分数只比较同类动作,不做全局万能评分。
所有最终动作必须再次 CheckCan。
同分时使用稳定排序,避免随机和不同步。

2. 核心结构

2.1 AIContext

结构 AIContext:
    Map: MapData
    Player: PlayerData
    Config: AIConfig
    Cache: WorldCache
    Actions: ActionPool
    Trace: List<string>
    BlockedActionKeysThisTurn: Set<string>
    BlockedIntentKeysThisTurn: Set<string>

2.2 AIConfig

结构 AIConfig:
    EmergencyRange = 3
    FrontRange = 6
    LocalBattleRange = 2
    DevelopmentSearchRange = 6
    MaxActionCount = 4096
    MaxFrontCount = 12
    MaxDevelopmentTargetCount = 20
    MaxExpansionTargetScanCount = 6
    MaxMoveActionsPerUnit = 2
    MaxSelfCityMoveAnchorCount = 3
    MaxEmergencyResponderScanCount = 8
    SoftGrowthActionBudget = 10
    MaxNonCriticalActionsPerPlayerTurn = 18
    MinGrowthPriorityAfterSoftBudget = 430
    MaxGrowthCityActionsPerDecision = 18
    MaxGrowthGridActionsPerDecision = 24
    MaxGrowthPlayerActionsPerDecision = 10
    MaxExpansionMoveIntentsPerTurn = 3
    MaxEmergencyMoveIntentsPerTurn = 2
    MaxFrontMoveIntentsPerTurn = 1
    MaxEmergencyRescueDistance = 6
    MaxFrontMoveDistance = 8
    PriorityTacticScore = 780
    LowHpRatio = 0.45
    CriticalHpRatio = 0.25
    HeroLowHpRatio = 0.60
    CityDangerEnemyCount = 2
    CityCriticalEnemyCount = 4
    CityThreatPowerRatio = 1.25
    HeroTaskStartTurn = 8
    HeroTaskInterval = 4
    HeroLevel2MinTurn = 15
    HeroLevel3MinTurn = 25

这些是默认值。难度、地图尺寸和关卡规则可以覆盖 Config但不能改变车道顺序。

2.3 Lane

枚举 Lane:
    Emergency
    HeroManagement
    Expansion
    HeroPlaybook
    Tactic
    UnitOpportunity
    Front
    Growth
    Fallback

2.4 Candidate

结构 Candidate:
    Action: AIAction
    Lane: Lane
    Priority: float
    Reason: string
    Unit: UnitData
    TargetUnit: UnitData
    City: CityData
    Grid: GridData
    TargetGrid: GridData
    IsFallback: bool

函数 Candidate.IsValid:
    如果 Action == null:
        返回 false
    如果 Action.Param == null:
        返回 false
    如果 Action.ActionLogic == null:
        返回 false
    Action.Param.RefreshParams()
    返回 Action.ActionLogic.CheckCan(Action.Param)

2.5 WorldCache

结构 WorldCache:
    SelfUnits: List<UnitData>
    SelfHeroes: List<UnitData>
    EnemyUnits: List<UnitData>
    EnemyHeroes: List<UnitData>
    SelfCities: List<CityData>
    EnemyCities: List<CityData>
    SelfTerritoryGridIds: HashSet<uint>
    VisibleOrKnownGrids: List<GridData>

    Diplomacy: DiplomacyView
    StrategicPosture: StrategicPosture
    WarTargetPlayers: List<PlayerData>

    CityThreats: List<CityThreat>
    CityPlans: List<CityPlan>
    DevelopmentTargets: List<DevelopmentTarget>
    Fronts: List<Front>
    LocalBattles: List<LocalBattle>
    HeroStates: List<HeroState>
    UnitOpportunities: List<UnitOpportunity>

    HasCriticalCityThreat: bool
    HasAnyEnemyContact: bool
    SelfMilitaryValue: float
    EnemyMilitaryValue: float

2.6 ActionPool

结构 ActionPool:
    All: List<AIAction>
    Attacks: List<AIAction>
    AttackAllies: List<AIAction>
    AttackGrounds: List<AIAction>
    Moves: List<AIAction>
    UnitActions: List<AIAction>
    CityActions: List<AIAction>
    GridActions: List<AIAction>
    PlayerActions: List<AIAction>
    HeroManagementActions: List<AIAction>

    ByUnit: Map<UnitId, UnitActionBucket>
    ByCity: Map<CityId, List<AIAction>>
    ByGrid: Map<GridId, List<AIAction>>
结构 UnitActionBucket:
    Attacks
    AttackAllies
    AttackGrounds
    Moves
    UnitActions

3. 总入口

3.1 Decide

函数 Decide(map, player, config, blockedActionKeysThisTurn, blockedIntentKeysThisTurn):
    ctx = new AIContext(map, player, config)
    ctx.BlockedActionKeysThisTurn = blockedActionKeysThisTurn
    ctx.BlockedIntentKeysThisTurn = blockedIntentKeysThisTurn

    如果 map == null 或 player == null 或 player.Alive == false:
        返回 NoDecision

    ctx.Cache = BuildWorldCache(ctx)
    ctx.Actions = BuildActionPool(ctx)

    如果 ctx.Actions.All.Count == 0:
        返回 NoDecision

    candidate = TryEmergency(ctx)
    如果 candidate.IsValid:
        返回 candidate

    如果 HasUnresolvedEmptyCityCoreThreat(ctx):
        返回 NoDecision

    lanes = [
        TryHeroManagement,
        TryPriorityTactic,
        TryExpansion,
        TryHeroPlaybook,
        TryTactic,
        TryUnitOpportunity,
        TryFront,
        TryGrowth,
        TryFallback
    ]

    对每个 lane in lanes:
        candidate = lane(ctx)
        如果 candidate.IsValid:
            返回 candidate

    返回 NoDecision

3.2 ExecuteDecision

函数 ExecuteDecision(candidate):
    如果 !candidate.IsValid:
        返回 false

    param = candidate.Action.Param
    param.RefreshParams()

    如果 !candidate.Action.ActionLogic.CheckCan(param):
        返回 false

    返回 candidate.Action.ActionLogic.CompleteExecute(param)

4. ActionPool

4.1 BuildActionPool

函数 BuildActionPool(ctx):
    pool = new ActionPool
    moveAnchors = BuildMoveAnchorGridIds(ctx)
    rawActions = GenerateDirectorActions(ctx.Map, ctx.Player, moveAnchors, Config.MaxMoveActionsPerUnit)

    对每个 rawAction in rawActions 按稳定顺序:
        如果 pool.All.Count >= Config.MaxActionCount:
            break

        action = CopyAction(rawAction)
        如果 action == null:
            继续

        如果 StableActionKey(action) in ctx.BlockedActionKeysThisTurn:
            继续

        action.Param.RefreshParams()
        如果 !action.ActionLogic.CheckCan(action.Param):
            继续

        如果 IsDangerousAction(action):
            继续

        AddActionToPool(pool, action)

    返回 pool
函数 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)

    selfCityAnchors = 0
    对每个 cityPlan in ctx.Cache.CityPlans:
        如果 selfCityAnchors >= Config.MaxSelfCityMoveAnchorCount:
            break
        anchors.Add(cityPlan.CityGrid)
        selfCityAnchors++

    如果 selfCityAnchors == 0:
        对每个 selfCity in ctx.Cache.SelfCities:
            如果 selfCityAnchors >= Config.MaxSelfCityMoveAnchorCount:
                break
            anchors.Add(selfCity.Grid)
            selfCityAnchors++

    对每个 localBattle in ctx.Cache.LocalBattles:
        anchors.Add(localBattle.EnemyGrid)
        anchors.Add(localBattle.SelfGrid)

    返回去重后的 anchors
函数 GenerateDirectorActions(map, player, moveAnchors, maxMoveActionsPerUnit):
    普通非移动动作按 CheckCan 生成。

    对每个可移动单位:
        先计算 UnitMoveInfo。
        收集该单位本回合所有可移动候选格。
        按“距离 moveAnchors 最近、城市格优先、已占格降权、稳定 GridId”排序。
        只保留前 maxMoveActionsPerUnit 个 UnitMove 动作。

    对每个可攻击单位:
        只在 moveAnchors 中检查可见敌方单位格是否可攻击。
        如果可攻击则生成 UnitAttack 动作。

    返回动作列表

4.2 动作分类

函数 AddActionToPool(pool, action):
    pool.All.Add(action)
    id = action.ActionId

    如果 id.ActionType == UnitAttack:
        pool.Attacks.Add(action)
        pool.ByUnit[action.UnitId].Attacks.Add(action)

    如果 id.ActionType == UnitAttackAlly:
        pool.AttackAllies.Add(action)
        pool.ByUnit[action.UnitId].AttackAllies.Add(action)

    如果 id.ActionType == UnitAttackGround:
        pool.AttackGrounds.Add(action)
        pool.ByUnit[action.UnitId].AttackGrounds.Add(action)

    如果 id.ActionType == UnitMove:
        pool.Moves.Add(action)
        pool.ByUnit[action.UnitId].Moves.Add(action)

    如果 id.ActionType == UnitAction 或 UnitSkill:
        pool.UnitActions.Add(action)
        pool.ByUnit[action.UnitId].UnitActions.Add(action)

    如果 id.ActionType in [TrainUnit, CityLevelUpAction, CityAction, StartWonder, BuildWonder]:
        pool.CityActions.Add(action)
        pool.ByCity[action.CityId].Add(action)

    如果 id.ActionType in [Gain, Build, GridMisc, HakureiEinherjarCityDevelopment]:
        pool.GridActions.Add(action)
        pool.ByGrid[action.GridId].Add(action)

    如果 id.ActionType == LearnTech:
        pool.PlayerActions.Add(action)

    如果 id.ActionType == BuyCultureCard:
        如果 IsAIHeroSlotCultureCard(player, id.CultureCardType):
            pool.HeroManagementActions.Add(action)
        否则:
            继续

    如果 id.ActionType == PlayerAction:
        如果 id.PlayerActionType in [SelectHero, FinishHeroTask]:
            pool.HeroManagementActions.Add(action)
        否则:
            pool.PlayerActions.Add(action)
函数 IsAIHeroSlotCultureCard(player, cardType):
    如果 player 没有 HeroData 或 CultureInfo:
        返回 false

    如果 player 已拥有 cardType:
        返回 false

    如果 cardType == SecondHero:
        返回 MaxHeroCount == 1 且 HeroCount >= 1

    如果 cardType == ThirdHero:
        返回 MaxHeroCount == 2 且 HeroCount >= 2 且 已拥有 SecondHero

    返回 false

4.3 危险动作过滤

函数 IsDangerousAction(action):
    id = action.ActionId

    如果 id.UnitActionType in [Disband, ForceDisband, Demolish, Disperse]:
        返回 true

    如果 id.GridMiscActionType == Destroy:
        返回 true

    返回 false

危险动作后续只能由专门车道显式调用,不能进入普通 AI 兜底。

4.4 ActionPool 查询

函数 FindBestAttack(ctx, unit, target = null):
    actions = ctx.Actions.ByUnit[unit.Id].Attacks
    best = None

    对每个 action in actions:
        如果 target != null 且 action.TargetUnit != target:
            继续
        score = ScoreAttackAction(ctx, action)
        如果 score <= 0:
            继续
        best = MaxByScore(best, action, score)

    返回 best.Action
函数 FindBestMoveToward(ctx, unit, targetGrid):
    cacheKey = (unit.Id, targetGrid.Id)
    如果 ctx.ActionIndex.BestMoveCache 包含 cacheKey:
        返回缓存结果

    actions = ctx.Actions.ByUnit[unit.Id].Moves
    best = None

    对每个 action in actions:
        endGrid = GetActionEndGrid(action)
        score = -Distance(endGrid, targetGrid)
        score -= GridThreat(ctx, unit, endGrid) * 0.5
        score += GridOpportunityBonus(ctx, unit, endGrid)
        best = MaxByScore(best, action, score)

    写入 ctx.ActionIndex.BestMoveCache[cacheKey] = best.Action
    返回 best.Action
函数 FindUnitAction(ctx, unit, unitActionType):
    actions = ctx.Actions.ByUnit[unit.Id].UnitActions
    返回第一个 ActionId.UnitActionType == unitActionType 且 CheckCan 的 action

5. WorldCache 构建

5.1 BuildWorldCache

函数 BuildWorldCache(ctx):
    cache = new WorldCache

    CollectWorldObjects(ctx, cache)
    BuildDiplomacyView(ctx, cache)
    BuildMilitarySummary(ctx, cache)
    BuildCityThreats(ctx, cache)
    BuildStrategicPosture(ctx, cache)
    BuildCityPlans(ctx, cache)
    BuildDevelopmentTargets(ctx, cache)
    BuildFronts(ctx, cache)
    BuildLocalBattles(ctx, cache)
    BuildHeroStates(ctx, cache)

    返回 cache

UnitOpportunities 依赖 ActionPool可以在 ActionPool 建好后构建:

函数 BuildPostActionCache(ctx):
    BuildUnitOpportunities(ctx, ctx.Cache, ctx.Actions)

如果实现上 ActionPool 先于 WorldCache,也可以拆成:

基础 Cache
→ ActionPool
→ 补充 UnitOpportunity

5.2 CollectWorldObjects

函数 CollectWorldObjects(ctx, cache):
    cache.SelfUnits = map.GetUnitDataListByPlayerId(player.Id).Where(IsAlive)
    cache.SelfCities = map.GetCityDataListByPlayerId(player.Id)
    cache.SelfTerritoryGridIds = map.GetGridDataSetByPlayerId(player.Id)
    cache.VisibleOrKnownGrids = GetVisibleOrKnownGrids(ctx)

    对每个 unit in map.UnitMap.UnitList:
        如果 unit == null 或 !unit.IsAlive():
            继续

        owner = map.GetPlayerDataByUnitId(unit.Id)

        如果 owner 与 player 同盟:
            如果 owner.Id == player.Id:
                如果 IsHero(unit):
                    cache.SelfHeroes.Add(unit)
            继续

        cache.EnemyUnits.Add(unit)

        如果 IsHero(unit):
            cache.EnemyHeroes.Add(unit)

    对每个 city in map.CityMap.CityList:
        owner = map.GetPlayerDataByCityId(city.Id)
        如果 owner 存在 且 owner 与 player 非同盟:
            cache.EnemyCities.Add(city)

5.3 BuildDiplomacyView

结构 DiplomacyView:
    Allies
    Enemies
    WarPlayers
    HighTrustPlayers
    LowTrustPlayers
函数 BuildDiplomacyView(ctx, cache):
    对每个 otherPlayer in map.PlayerMap.PlayerList:
        如果 otherPlayer == player 或 !otherPlayer.Alive:
            继续

        relation = GetDiplomacy(player, otherPlayer)

        如果 SameUnion(player, otherPlayer):
            Diplomacy.Allies.Add(otherPlayer)
        否则:
            Diplomacy.Enemies.Add(otherPlayer)

        如果 relation.State == War:
            Diplomacy.WarPlayers.Add(otherPlayer)

        如果 relation.Feeling >= 80:
            Diplomacy.HighTrustPlayers.Add(otherPlayer)

        如果 relation.Feeling <= 30:
            Diplomacy.LowTrustPlayers.Add(otherPlayer)

5.4 BuildMilitarySummary

函数 BuildMilitarySummary(ctx, cache):
    对每个 unit in cache.SelfUnits:
        cache.SelfMilitaryValue += UnitMilitaryValue(ctx, unit)

    对每个 unit in cache.EnemyUnits:
        cache.EnemyMilitaryValue += UnitMilitaryValue(ctx, unit)
函数 UnitMilitaryValue(ctx, unit):
    如果 unit == null 或 !unit.IsAlive():
        返回 0

    value = UnitCostOrMilitary(unit)

    如果 HealthRatio(unit) <= 0.5:
        value *= 0.5

    如果 IsHero(unit):
        value += HeroValue(unit)

    返回 max(1, value)

5.5 BuildCityThreats

结构 CityThreat:
    City
    CityGrid
    IsCapital
    HasWall
    EnemyUnits
    DefenderUnits
    EnemyPower
    DefenderPower
    RescuePower
    NearestEnemyDistance
    HasEnemyOnTerritory
    CanBeThreatenedNextTurn
    DangerScore
    IsCritical
函数 BuildCityThreats(ctx, cache):
    对每个 city in cache.SelfCities:
        threat = BuildSingleCityThreat(ctx, cache, city)
        cache.CityThreats.Add(threat)
        cache.HasCriticalCityThreat |= threat.IsCritical
        cache.HasAnyEnemyContact |= threat.EnemyUnits.Count > 0

    cache.CityThreats.SortByDescending(DangerScore)
函数 BuildSingleCityThreat(ctx, cache, city):
    cityGrid = city.Grid(ctx.Map)
    threat = new CityThreat(city, cityGrid)
    threat.IsCapital = city.IsCapital
    threat.HasWall = CityHasWall(city)

    对每个 enemy in cache.EnemyUnits:
        enemyGrid = enemy.Grid(ctx.Map)
        d = Distance(cityGrid, enemyGrid)
        isInCityTerritory = city.CheckIsInTerritory(enemyGrid.Id)

        如果 d <= Config.EmergencyRange + AttackRange(enemy) 或 isInCityTerritory:
            threat.EnemyUnits.Add(enemy)
            threat.EnemyPower += UnitMilitaryValue(ctx, enemy) / max(1, d)
            threat.NearestEnemyDistance = min(threat.NearestEnemyDistance, d)

        如果 isInCityTerritory:
            threat.HasEnemyOnTerritory = true

        如果 CanThreatenCityNextTurn(ctx, enemy, city):
            threat.CanBeThreatenedNextTurn = true

    对每个 unit in cache.SelfUnits:
        unitGrid = unit.Grid(ctx.Map)
        d = Distance(cityGrid, unitGrid)

        如果 d <= Config.EmergencyRange:
            threat.DefenderUnits.Add(unit)
            threat.DefenderPower += UnitMilitaryValue(ctx, unit) / max(1, d)

        如果 d <= Config.FrontRange:
            threat.RescuePower += UnitMilitaryValue(ctx, unit) / max(1, d)

    threat.DangerScore =
        threat.EnemyPower
        - threat.DefenderPower
        + (threat.HasEnemyOnTerritory ? 4 : 0)
        + (threat.CanBeThreatenedNextTurn ? 6 : 0)
        + (threat.IsCapital ? 3 : 0)
        - (threat.HasWall ? 2 : 0)

    threat.IsCritical =
        threat.CanBeThreatenedNextTurn
        或 threat.EnemyUnits.Count >= Config.CityCriticalEnemyCount
        或 threat.HasEnemyOnTerritory
        或 threat.EnemyPower > threat.DefenderPower * Config.CityThreatPowerRatio

    返回 threat
函数 CanThreatenCityNextTurn(ctx, enemy, city):
    enemyGrid = enemy.Grid(ctx.Map)
    cityGrid = city.Grid(ctx.Map)

    reach = MoveRange(enemy) + AttackRange(enemy)

    如果 enemy 有 Capture 行动能力:
        reach += 1

    如果 Distance(enemyGrid, cityGrid) <= reach:
        返回 true

    返回 false

5.6 BuildStrategicPosture

枚举 StrategicPosture:
    Defense
    Expansion
    Attack
    Development
函数 BuildStrategicPosture(ctx, cache):
    如果 cache.HasCriticalCityThreat:
        cache.StrategicPosture = Defense
        返回

    如果 ExistsHighValueVillageOrRuinNearCities(ctx, cache):
        cache.StrategicPosture = Expansion
        返回

    attackTargets = BuildAttackTargetPlayers(ctx, cache)
    如果 attackTargets.Count > 0:
        cache.StrategicPosture = Attack
        cache.WarTargetPlayers = attackTargets
        返回

    cache.StrategicPosture = Development
函数 BuildAttackTargetPlayers(ctx, cache):
    targets = []

    对每个 enemyPlayer in cache.Diplomacy.Enemies:
        如果 enemyPlayer in cache.Diplomacy.Allies:
            继续

        nearestDistance = NearestCityDistance(ctx.Player, enemyPlayer)
        militaryGap = MilitaryGapWithCounter(ctx, ctx.Player, enemyPlayer)
        threat = ThreatFromPlayer(ctx, enemyPlayer)
        feeling = GetFeeling(ctx.Player, enemyPlayer)

        如果 enemyPlayer in Diplomacy.WarPlayers:
            targets.Add(enemyPlayer)
            继续

        如果 feeling <= 30:
            targets.Add(enemyPlayer)
            继续

        如果 nearestDistance <= 5 且 militaryGap >= 0:
            targets.Add(enemyPlayer)
            继续

        如果 threat >= 10 且 militaryGap >= -3 且 nearestDistance <= 7:
            targets.Add(enemyPlayer)
            继续

    返回 targets 按 AttackTargetScore 降序

5.7 BuildCityPlans

枚举 CityPlanKind:
    EmergencyDefense
    Mobilize
    Frontline
    BacklineGrowth
    Wonder
结构 CityPlan:
    City
    CityGrid
    Kind
    Threat
    NeedWall
    NeedMilitary
    NeedGrowth
    NeedWonder
    Priority
函数 BuildCityPlans(ctx, cache):
    对每个 city in cache.SelfCities:
        threat = FindThreatForCity(cache, city)
        plan = new CityPlan(city, threat)

        如果 threat.IsCritical:
            plan.Kind = EmergencyDefense
        否则如果 threat.DangerScore > 0:
            plan.Kind = Mobilize
        否则如果 IsNearAttackFront(ctx, cache, city):
            plan.Kind = Frontline
        否则如果 CanReasonablyPursueWonder(ctx, city):
            plan.Kind = Wonder
        否则:
            plan.Kind = BacklineGrowth

        plan.NeedWall = plan.Kind in [EmergencyDefense, Mobilize, Frontline] 且 !CityHasWall(city)
        plan.NeedMilitary = plan.Kind in [EmergencyDefense, Mobilize, Frontline]
        plan.NeedGrowth = plan.Kind in [BacklineGrowth, Wonder]
        plan.NeedWonder = plan.Kind == Wonder
        plan.Priority = ScoreCityPlan(ctx, plan)

        cache.CityPlans.Add(plan)

    cache.CityPlans.SortByDescending(Priority)

5.8 BuildDevelopmentTargets

枚举 DevelopmentTargetType:
    Village
    EnemyEmptyCity
    Treasure
    Ruin
    Resource
    FogBoundary
结构 DevelopmentTarget:
    Grid
    Type
    Value
    NearestSelfCity
    Distance
函数 BuildDevelopmentTargets(ctx, cache):
    targets = []

    对每个 grid in cache.VisibleOrKnownGrids:
        target = TryBuildDevelopmentTarget(ctx, cache, grid)
        如果 target == null:
            继续
        如果 target.Distance > Config.DevelopmentSearchRange:
            继续
        如果 GridThreatToAnySelfUnit(ctx, grid) 太高 且 target.Type 不是 EnemyEmptyCity:
            继续
        targets.Add(target)

    targets.SortByDescending(Value - DistancePenalty)
    cache.DevelopmentTargets = targets.Take(Config.MaxDevelopmentTargetCount)
函数 TryBuildDevelopmentTarget(ctx, cache, grid):
    如果 grid.Resource == CityCenter 且 grid 上没有己方城市:
        返回 DevelopmentTarget(grid, Village, 900)

    如果 grid 是敌方空城中心:
        返回 DevelopmentTarget(grid, EnemyEmptyCity, 950)

    如果 grid 有遗迹或宝物可探索:
        返回 DevelopmentTarget(grid, Treasure, 820)

    如果 grid 有可采集或可开发高价值资源:
        返回 DevelopmentTarget(grid, Resource, 650 + ResourceValue(ctx, grid))

    如果 grid 是安全探索边界:
        返回 DevelopmentTarget(grid, FogBoundary, 420)

    返回 null

可占城中心的识别规则:

函数 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

枚举 FrontType:
    Defense
    Attack
    Development
    Hold
结构 Front:
    Type
    AnchorGrid
    TargetGrid
    City
    TargetCity
    TargetPlayer
    Pressure
    Opportunity
    Distance
    Priority
函数 BuildFronts(ctx, cache):
    fronts = []

    fronts.AddRange(BuildDefenseFronts(ctx, cache))
    fronts.AddRange(BuildAttackFronts(ctx, cache))
    fronts.AddRange(BuildDevelopmentFronts(ctx, cache))
    fronts.AddRange(BuildHoldFronts(ctx, cache))

    fronts.SortByDescending(Priority)
    cache.Fronts = fronts.Take(Config.MaxFrontCount)
函数 BuildDefenseFronts(ctx, cache):
    对每个 threat in cache.CityThreats:
        如果 threat.DangerScore <= 0:
            继续
        yield Front(
            Type = Defense,
            AnchorGrid = threat.CityGrid,
            City = threat.City,
            Pressure = threat.DangerScore,
            Priority = 1000 + threat.DangerScore
        )
函数 BuildAttackFronts(ctx, cache):
    对每个 targetCity in cache.EnemyCities:
        owner = targetCity.Owner
        如果 owner 不在 cache.WarTargetPlayers 且 cache.StrategicPosture != Attack:
            继续

        selfCity = NearestSelfCity(targetCity.Grid)
        distance = Distance(selfCity.Grid, targetCity.Grid)
        opportunity = EstimateCityAttackOpportunity(ctx, targetCity)

        yield Front(
            Type = Attack,
            AnchorGrid = selfCity.Grid,
            TargetGrid = targetCity.Grid,
            City = selfCity,
            TargetCity = targetCity,
            TargetPlayer = owner,
            Distance = distance,
            Opportunity = opportunity,
            Priority = 700 + opportunity - distance
        )
函数 BuildDevelopmentFronts(ctx, cache):
    对每个 target in cache.DevelopmentTargets:
        如果 己方城市数 < Config.ExpansionUrgentCityThreshold 且 target.Type in [Village, EnemyEmptyCity]:
            继续
        selfCity = target.NearestSelfCity
        yield Front(
            Type = Development,
            AnchorGrid = selfCity.Grid,
            TargetGrid = target.Grid,
            City = selfCity,
            Distance = target.Distance,
            Opportunity = target.Value,
            Priority = 500 + target.Value - target.Distance * 8
        )
函数 BuildHoldFronts(ctx, cache):
    对每个 city in cache.SelfCities:
        如果 city 是首都 或 高等级后方城市:
            yield Front(
                Type = Hold,
                AnchorGrid = city.Grid,
                City = city,
                Priority = 300 + CityValue(city)
            )

5.10 BuildLocalBattles

结构 LocalBattle:
    SelfUnit
    EnemyUnit
    SelfGrid
    EnemyGrid
    Distance
    AttackAction
    Value
函数 BuildLocalBattles(ctx, cache):
    battles = []

    对每个 self in cache.SelfUnits:
        对每个 enemy in cache.EnemyUnits:
            d = Distance(self.Grid, enemy.Grid)
            如果 d > Config.LocalBattleRange + AttackRange(self):
                继续

            value = EstimateAttackValue(ctx, self, enemy)
            如果 enemy 正在威胁城市:
                value += 120
            如果 enemy 是英雄:
                value += 100
            如果 CanKill(ctx, self, enemy):
                value += 160

            battles.Add(LocalBattle(self, enemy, d, value))

    cache.LocalBattles = battles.SortByDescending(Value)

5.11 BuildHeroStates

枚举 HeroContext:
    Recovery
    Defense
    FieldBattle
    Siege
    Economy
    Mobility
    Control
结构 HeroState:
    Hero
    GiantType
    Level
    HealthRatio
    Context
    NearestFront
    IsThreatened
    IsOnFront
函数 BuildHeroStates(ctx, cache):
    对每个 hero in cache.SelfHeroes:
        state = new HeroState
        state.Hero = hero
        state.GiantType = GetHeroGiantType(hero)
        state.Level = GetHeroLevel(ctx.Player, state.GiantType)
        state.HealthRatio = HealthRatio(hero)
        state.NearestFront = FindNearestFront(cache.Fronts, hero.Grid)
        state.IsOnFront = state.NearestFront != null 且 Distance(hero.Grid, state.NearestFront.AnchorGrid) <= Config.FrontRange
        state.IsThreatened = state.HealthRatio <= Config.HeroLowHpRatio 或 GridThreat(ctx, hero, hero.Grid) > 0
        state.Context = ResolveHeroContext(ctx, state)
        cache.HeroStates.Add(state)
函数 ResolveHeroContext(ctx, state):
    如果 state.HealthRatio <= Config.HeroLowHpRatio:
        返回 Recovery

    如果 state.NearestFront.Type == Defense:
        返回 Defense

    如果 state.NearestFront.Type == Attack:
        返回 Siege

    如果 state.IsOnFront 或 HasEnemyNear(ctx, state.Hero, Config.LocalBattleRange):
        返回 FieldBattle

    如果 HeroRole(state.GiantType) == Economy:
        返回 Economy

    如果 HeroRole(state.GiantType) == Mobility:
        返回 Mobility

    如果 HeroRole(state.GiantType) == Control:
        返回 Control

    返回 FieldBattle

5.12 BuildUnitOpportunities

结构 UnitOpportunity:
    Unit
    Action
    Type
    TargetGrid
    TargetUnit
    Value
函数 BuildUnitOpportunities(ctx):
    opportunities = []

    对每个 action in ctx.Actions.UnitActions:
        unit = action.Param.UnitData
        type = action.ActionId.UnitActionType
        value = ScoreUnitOpportunity(ctx, unit, action, type)

        如果 value <= 0:
            继续

        opportunities.Add(UnitOpportunity(unit, action, type, value))

    ctx.Cache.UnitOpportunities = opportunities.SortByDescending(Value)

6. Emergency Lane

6.1 TryEmergency

函数 TryEmergency(ctx):
    对每个 threat in ctx.Cache.CityThreats:
        如果 !ShouldUseEmergency(threat):
            继续

        candidate = TryEmergencyCityProduction(ctx, threat)
        如果 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

        candidate = TryEmergencyMoveToCity(ctx, threat)
        如果 candidate.IsValid:
            返回 candidate

        candidate = TryEmergencyCityProduction(ctx, threat)
        如果 candidate.IsValid:
            返回 candidate

    返回 None
函数 IsUrgentCityDefenseAction(candidate):
    如果 !candidate.IsValid:
        返回 false
    如果 candidate.ActionId.ActionType == TrainUnit:
        返回 true
    如果 candidate.ActionId.ActionType == CityAction 且 candidate.ActionId.CityActionType == BuildCityWall:
        返回 true
    返回 false
函数 ShouldUseEmergency(threat):
    如果 threat == null:
        返回 false
    如果 threat.HasEnemyOnTerritory:
        返回 true
    如果 threat.EnemyUnits.Count >= Config.CityCriticalEnemyCount:
        返回 true
    如果 threat.EnemyPower > threat.DefenderPower * Config.CityThreatPowerRatio
       且 threat.EnemyUnits.Count >= Config.CityDangerEnemyCount:
        返回 true
    如果 threat.DangerScore >= Config.EmergencyDangerScore:
        返回 true
    如果 threat.IsCritical 且 threat.CanBeThreatenedNextTurn 且 threat.DefenderPower <= 0:
        返回 true
    返回 false

6.2 EmergencyAttack

函数 TryEmergencyAttack(ctx, threat):
    best = None

    对每个 defender in threat.DefenderUnits:
        action = FindBestUsefulAttack(ctx, defender, BestThreatTarget(ctx, threat))
        如果 action == None:
            action = FindBestUsefulAttack(ctx, defender)
        candidate = Candidate(action, Emergency, ScoreEmergencyAttack(ctx, action, threat), "守军攻击城市威胁")
        best = MaxCandidate(best, candidate)

    返回 best
函数 ScoreEmergencyAttack(ctx, action, threat):
    target = action.TargetUnitData
    score = 900
    score += ThreatTargetValue(ctx, target)
    如果 target 下回合可威胁 threat.City:
        score += 180
    如果 CanKillByAction(ctx, action):
        score += 160
    如果 threat.CityGrid 没有己方驻守 且 threat.CanBeThreatenedNextTurn:
        如果 本次攻击不能直接击杀最后一个威胁单位:
            score -= threat.IsCapital ? 340 : 240
        如果 action.Unit 是该威胁的唯一守军 或 action.Unit 本可回到城心:
            score -= 180
    返回 score

6.3 EmergencyMoveToCity

函数 TryEmergencyMoveToCity(ctx, threat):
    best = None
    allowStackedCityGuard = IsUnresolvedEmptyCityCoreThreat(threat)
    如果 !allowStackedCityGuard:
        intentKey = BuildIntentKey(Emergency, "MoveToCity", threat.City.Id)
        如果 IsIntentBlocked(ctx, intentKey):
            返回 None
        如果 IsIntentBudgetReached(ctx, Emergency, Config.MaxEmergencyMoveIntentsPerTurn):
            返回 None
    否则:
        intentKey = ""

    对每个 unit in ctx.Cache.SelfUnits:
        如果 !CanUseAsDefender(ctx, unit, threat):
            继续

        startGrid = unit.Grid(ctx.Map)
        startDistance = Distance(startGrid, threat.CityGrid)
        如果 !ShouldScanEmergencyResponder(ctx, threat, unit, startGrid, startDistance, scannedResponders):
            继续
        scannedResponders += 1

        action = FindBestMoveToward(ctx, unit, threat.CityGrid)
        endGrid = GetActionEndGrid(action)
        如果 !IsUsefulMoveToward(ctx, startGrid, endGrid, threat.CityGrid):
            继续
        如果 unit 正站在某个危险城市中心 且 endGrid 会离开当前城心:
            继续

        candidate = Candidate(action, Emergency, ScoreEmergencyMove(ctx, unit, action, threat), "回防城市")
        candidate.IntentKey = intentKey
        best = MaxCandidate(best, candidate)

    返回 best
函数 ShouldScanEmergencyResponder(ctx, threat, unit, startGrid, startDistance, scannedResponders):
    如果 startGrid 为空:
        返回 false
    如果 startDistance > Config.MaxEmergencyRescueDistance 且 !threat.IsCritical:
        返回 false
    如果 startDistance > Config.MaxEmergencyRescueDistance + 2 且 unit 不是英雄:
        返回 false
    如果 Config.MaxEmergencyResponderScanCount <= 0:
        返回 true
    如果 scannedResponders < Config.MaxEmergencyResponderScanCount:
        返回 true
    如果 unit 是英雄 且 startDistance <= Config.MaxEmergencyRescueDistance + 1:
        返回 true
    如果 unit 是 Defender 且 startDistance <= 2:
        返回 true
    返回 false
函数 ScoreEmergencyMove(ctx, unit, action, threat):
    startGrid = unit.Grid(ctx.Map)
    endGrid = GetActionEndGrid(action)
    score = 900
    score += threat.DangerScore * 20
    score += UnitMilitaryValue(ctx, unit)
    score += max(0, Distance(startGrid, threat.CityGrid) - Distance(endGrid, threat.CityGrid)) * 55
    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

6.4 EmergencyCityProduction

函数 TryEmergencyCityProduction(ctx, threat):
    plan = FindCityPlan(threat.City)
    actions = ctx.Actions.ByCity[threat.City.Id]
    best = None

    对每个 action in actions:
        score = ScoreEmergencyCityAction(ctx, action, plan, threat)
        candidate = Candidate(action, Emergency, score, "危险城市生产或防御")
        best = MaxCandidate(best, candidate)

    返回 best
函数 ScoreEmergencyCityAction(ctx, action, plan, threat):
    id = action.ActionId
    score = 0

    如果 id.ActionType == TrainUnit:
        score = 760 + TrainUnitDefenseValue(ctx, action, threat)
        如果 threat.DefenderPower <= 0:
            score += 240
        如果 threat.IsCapital:
            score += 120
        如果 threat.CanBeThreatenedNextTurn:
            score += 100
        如果 threat.CityGrid 没有己方驻守:
            score += threat.IsCapital ? 300 : 220

    如果 id.ActionType == CityAction 且 id.CityActionType == BuildCityWall:
        score = 740
        如果 threat.DefenderPower <= 0:
            score += 180

    如果 id.ActionType == CityLevelUpAction 且 能选城墙或人口防守收益:
        score = 700

    如果 id.ActionType in [StartWonder, BuildWonder]:
        score = 0

    返回 score

7. HeroManagement Lane

7.1 TryHeroManagement

函数 TryHeroManagement(ctx):
    candidate = TrySelectHero(ctx)
    如果 candidate.IsValid:
        返回 candidate

    candidate = TryBuyHeroSlotCultureCard(ctx)
    如果 candidate.IsValid 且 ShouldPrioritizeHeroSlotBeforeSpawn(ctx):
        返回 candidate

    candidate = TrySpawnSelectedHero(ctx)
    如果 candidate.IsValid:
        返回 candidate

    candidate = TryBuyHeroSlotCultureCard(ctx)
    如果 candidate.IsValid:
        返回 candidate

    candidate = TryForceFinishHeroTask(ctx)
    如果 candidate.IsValid:
        返回 candidate

    返回 None

7.2 SelectHero

函数 TrySelectHero(ctx):
    action = FindHeroManagementAction(ctx, SelectHero)
    返回 Candidate(action, HeroManagement, 890, "选择英雄")

7.3 SpawnSelectedHero

函数 TrySpawnSelectedHero(ctx):
    如果 ctx.Player 没有已选择英雄:
        返回 None

    best = None

    对每个 cityPlan in ctx.Cache.CityPlans:
        对每个 action in ctx.Actions.ByCity[cityPlan.City.Id]:
            如果 !IsSelectedHeroSpawnAction(ctx.Player, action):
                继续

            score = 880
            score += max(cityPlan.Priority, 0) * 0.08

            如果 cityPlan.Kind == EmergencyDefense:
                score += 40
            如果 cityPlan.Kind == Mobilize:
                score += 30
            如果 cityPlan.Kind == Frontline:
                score += 25

            candidate = Candidate(action, HeroManagement, score, "英雄出场")
            candidate.City = cityPlan.City
            candidate.TargetGrid = cityPlan.CityGrid
            best = MaxCandidate(best, candidate)

    如果 best.IsValid:
        返回 best

    对每个 action in ctx.Actions.CityActions:
        如果 IsSelectedHeroSpawnAction(ctx.Player, action):
            candidate = Candidate(action, HeroManagement, 880, "英雄出场")
            best = MaxCandidate(best, candidate)

    返回 best
函数 IsSelectedHeroSpawnAction(player, action):
    id = action.ActionId
    返回 id.ActionType == TrainUnit
        且 id.UnitType == Giant
        且 id.GiantType != None
        且 player.PlayerHeroData.HasHero(id.GiantType)

英雄出场仍然走 TrainUnitAction 的 CheckCan 和 Execute。AI 只负责更早选择这个合法动作,不直接改变英雄状态。

7.4 BuyHeroSlotCultureCard

函数 TryBuyHeroSlotCultureCard(ctx):
    action = FindHeroSlotCultureCard(ctx.Player)
    如果 action == None:
        返回 None

    score = ScoreCultureCard(ctx, action.CultureCardType)
    返回 Candidate(action, HeroManagement, score, "购买英雄槽位卡")
函数 ShouldPrioritizeHeroSlotBeforeSpawn(ctx):
    heroData = ctx.Player.PlayerHeroData
    如果 heroData == null:
        返回 false
    如果 heroData.MaxHeroCount >= 3:
        返回 false
    如果 ctx.Cache.SelfHeroes.Count < heroData.HeroCount:
        返回 false
    如果 heroData.HeroCount > 0 且 heroData.HeroCount >= heroData.MaxHeroCount:
        返回 true
    返回 false

7.5 FinishHeroTask

函数 TryForceFinishHeroTask(ctx):
    如果 ctx.Player.Turn < Config.HeroTaskStartTurn:
        返回 None
    如果 ctx.Player.Turn % Config.HeroTaskInterval != 0:
        返回 None

    best = None

    对每个 action in ctx.Actions.HeroManagementActions:
        如果 action.PlayerActionType != FinishHeroTask:
            继续

        giant = action.ActionId.GiantType
        level = GetHeroLevel(ctx.Player, giant)

        如果 !CanForceFinishHeroTask(ctx, giant, level):
            继续

        score = 860 - level * 20
        candidate = Candidate(action, HeroManagement, score, "推进最低等级英雄任务")
        best = MaxCandidate(best, candidate)

    返回 best
函数 CanForceFinishHeroTask(ctx, giant, level):
    如果 HeroTask 已完成或已强制完成:
        返回 false

    如果 level == 0:
        返回 true

    如果 level == 1:
        返回 ctx.Player.Turn >= Config.HeroLevel2MinTurn

    如果 level == 2:
        返回 ctx.Player.Turn >= Config.HeroLevel3MinTurn

    返回 false

8. Expansion Lane

Expansion Lane 只处理早期扩张的刚性目标,不处理普通资源开发。 它的职责是让 AI 在没有严重城防危机时,优先拿到第二座城市或明显高价值的可占城中心。

8.1 TryExpansion

函数 TryExpansion(ctx):
    如果 !ShouldPushExpansion(ctx):
        返回 None

    best = None
    scannedTargetCount = 0
    requireSafeUrgentTarget = 有严重城市威胁

    对每个 target in ctx.Cache.DevelopmentTargets:
        如果 !IsExpansionTarget(ctx, target):
            继续
        如果 requireSafeUrgentTarget 且 !IsSafeUrgentExpansionTarget(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

函数 ShouldPushExpansion(ctx):
    如果 有严重城市威胁 且 不存在安全紧急扩张目标:
        返回 false

    如果 ctx.Cache.DevelopmentTargets 为空:
        返回 false

    如果 己方城市数 < Config.ExpansionUrgentCityThreshold:
        返回 true

    如果 当前回合 <= Config.ExpansionHardPressureTurn 且存在扩张目标:
        返回 true

    返回 false
函数 HasSafeUrgentExpansionTarget(ctx):
    对每个 target in ctx.Cache.DevelopmentTargets:
        如果 IsSafeUrgentExpansionTarget(ctx, target):
            返回 true
    返回 false

函数 IsSafeUrgentExpansionTarget(ctx, target):
    如果 己方城市数 >= Config.ExpansionUrgentCityThreshold:
        返回 false
    如果 target.Type 不在 [Village, EnemyEmptyCity]:
        返回 false
    如果 target.Distance > 3:
        返回 false
    如果 GridThreat(ctx, target.Grid) > 0:
        返回 false
    返回 true

8.3 IsExpansionTarget

函数 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

函数 TryExpansionCapture(ctx, target):
    best = None

    对每个 action in ctx.Actions.UnitActions:
        如果 action.UnitActionType != Capture:
            继续

        unit = action.Unit
        unitGrid = unit.Grid
        如果 unitGrid != target.Grid:
            继续

        riskPenalty = ExpansionTargetRiskPenalty(ctx, target, unitGrid)
        score = ScoreExpansionTarget(ctx, target) + 420 - riskPenalty
        如果 score <= 0:
            继续
        candidate = Candidate(action, Expansion, score, "扩张占领")
        best = MaxCandidate(best, candidate)

    返回 best

8.5 TryExpansionMove

函数 TryExpansionMove(ctx, target):
    best = None
    intentKey = BuildIntentKey(Expansion, "Move." + target.Type, target.Grid.Id)
    如果 IsIntentBlocked(ctx, intentKey):
        返回 None
    如果 IsIntentBudgetReached(ctx, Expansion, Config.MaxExpansionMoveIntentsPerTurn):
        返回 None

    对每个 unit in ctx.Cache.SelfUnits:
        如果 unit 没有移动行动点:
            继续

        startGrid = unit.Grid
        如果 startGrid == target.Grid:
            继续
        startDistance = Distance(startGrid, target.Grid)

        如果 ShouldSkipExpansionUnit(ctx, unit, target, startGrid, startDistance):
            继续
        如果 unit 是脆弱特殊衍生单位 且 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)
        riskPenalty = ExpansionTargetRiskPenalty(ctx, target, endGrid)
        score -= riskPenalty
        如果 score <= 0:
            继续

        candidate = Candidate(action, Expansion, score, "扩张移动")
        candidate.IntentKey = intentKey
        best = MaxCandidate(best, candidate)

    返回 best

8.6 ScoreExpansionTarget

函数 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
函数 ExpansionTargetRiskPenalty(ctx, target, actingGrid):
    penalty = GridThreat(ctx, target.Grid) * 0.75
    如果 actingGrid != null:
        penalty += GridThreat(ctx, actingGrid) * 0.25

    如果 target.Type == EnemyEmptyCity:
        penalty += GridThreat(ctx, target.Grid) * 0.35

    如果 有严重城市威胁:
        penalty += 180

    如果 己方单位数 <= 己方城市数 + 1 且 GridThreat(ctx, target.Grid) > 0:
        penalty += 120

    返回 penalty
函数 ShouldSkipExpansionUnit(ctx, unit, target, startGrid, startDistance):
    如果 startDistance > Config.DevelopmentSearchRange + 2:
        返回 true

    如果 unit 低血且 startGrid 有威胁:
        返回 true

    如果 unit 不是危险城市守军:
        返回 false

    如果 unit 是危险城市唯一守军:
        返回 true

    如果 己方单位数 <= 己方城市数 + 1:
        返回 true

    如果 target.Grid 有威胁:
        返回 true

    返回 false

9. HeroPlaybook Lane

9.1 TryHeroPlaybook

函数 TryHeroPlaybook(ctx):
    best = None

    对每个 state in ctx.Cache.HeroStates:
        candidate = EvaluateHeroPlaybook(ctx, state)
        best = MaxCandidate(best, candidate)

    返回 best

9.2 HeroRule

结构 HeroRule:
    GiantType
    MinLevel
    Context
    ActionKind:
        UnitAction
        AttackEnemy
        AttackAlly
        AttackGround
        MoveToward
        MoveRetreat
        Hold
    UnitActionType
    TargetPolicy
    Priority
    Condition
    Reason

9.3 EvaluateHeroPlaybook

函数 EvaluateHeroPlaybook(ctx, state):
    rules = GetHeroRules(state.GiantType)
    best = None

    对每个 rule in rules:
        如果 state.Level < rule.MinLevel:
            继续
        如果 !HeroContextMatches(ctx, state, rule.Context):
            继续
        如果 !rule.Condition(ctx, state):
            继续

        action = BuildHeroAction(ctx, state, rule)
        candidate = Candidate(action, HeroPlaybook, ScoreHeroRule(ctx, state, rule, action), rule.Reason)
        best = MaxCandidate(best, candidate)

    如果 best.IsValid:
        返回 best

    返回 EvaluateGenericHeroRule(ctx, state)

9.4 BuildHeroAction

函数 BuildHeroAction(ctx, state, rule):
    hero = state.Hero
    targetUnit = ResolveHeroTargetUnit(ctx, state, rule.TargetPolicy)
    targetGrid = ResolveHeroTargetGrid(ctx, state, rule.TargetPolicy)

    如果 rule.ActionKind == UnitAction:
        返回 FindUnitAction(ctx, hero, rule.UnitActionType)

    如果 rule.ActionKind == AttackEnemy:
        返回 FindBestAttack(ctx, hero, targetUnit)

    如果 rule.ActionKind == AttackAlly:
        返回 FindBestAttackAlly(ctx, hero, targetUnit)

    如果 rule.ActionKind == AttackGround:
        返回 FindBestAttackGround(ctx, hero, targetGrid)

    如果 rule.ActionKind == MoveToward:
        返回 FindBestMoveToward(ctx, hero, targetGrid)

    如果 rule.ActionKind == MoveRetreat:
        retreatGrid = FindLowestThreatGrid(ctx, hero)
        返回 FindBestMoveToward(ctx, hero, retreatGrid)

    返回 null

9.5 通用英雄保命

函数 EvaluateGenericHeroRule(ctx, state):
    hero = state.Hero

    如果 state.HealthRatio <= Config.CriticalHpRatio:
        action = FindUnitAction(ctx, hero, Recover)
        如果 action 可用:
            返回 Candidate(action, HeroPlaybook, 840, "英雄低血恢复")

        retreat = FindLowestThreatGrid(ctx, hero)
        move = FindBestMoveToward(ctx, hero, retreat)
        如果 move 可用:
            返回 Candidate(move, HeroPlaybook, 820, "英雄低血撤退")

    attack = FindBestUsefulAttack(ctx, hero, BestHeroOrKillableEnemy(ctx, hero))
    如果 attack 可用:
        roleBonus = 0
        如果 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, "英雄通用地面控制")

    target = state.Front.TargetGrid 或 state.Front.AnchorGrid
    move = FindBestMoveToward(ctx, hero, target)
    score = ScoreHeroFrontMove(ctx, state, move, target)
    如果 move 可用 且 score > 0:
        返回 Candidate(move, HeroPlaybook, score, "英雄前线补位")

    返回 None
函数 ScoreHeroFrontMove(ctx, state, move, target):
    如果 move 不存在 或 target 不存在:
        返回 0
    start = hero 当前格
    end = move 目标格
    如果 end 没有缩短到 target 的距离:
        返回 0

    如果 hero 已在前线 且 end 距离 target 仍大于 1:
        返回 0
    如果 hero 是 Support / Economy 且当前不是 Defense front:
        返回 0
    如果 hero 是 Caster / Control 且当前没有敌我接触 且 end 距离 target 大于 2:
        返回 0

    score = 500
    score += 缩短距离 * 30
    score -= end 到 target 距离 * 18

    如果 front 是 Defense:
        score += 70
    如果 hero 是 Mobility:
        score += 45
    否则如果 hero 是 Vanguard / Assassin:
        score += 20
    如果 hero 是 Caster / Control:
        score -= 45
    如果 hero 已受威胁:
        如果低血: score -= 170
        否则: score -= 60

    返回 score

9.6 默认英雄规则

Flandre:
    如果有可击杀高价值敌人 -> AttackEnemy(KillableEnemy)
    如果友军低血且攻击友军能触发收益 -> AttackAlly(InjuredAlly)

Remilia:
    如果接战且红雾/增益不足 -> UnitAction(REMILIABUFF)
    如果低血且可吸收 -> UnitAction(REMILIAABSORB)

Sakuya:
    如果有可击杀目标 -> AttackEnemy(KillableEnemy)
    如果残血英雄或关键友军附近有敌人 -> MoveToward(ProtectAllyGrid)
    如果可用保护型友军攻击 -> AttackAlly(InjuredAlly)

Meiling:
    如果低血 -> UnitAction(Recover)
    如果城市防守缺前排 -> MoveToward(DefenseFront)

Patchouli:
    如果元素/地面攻击收益可触发 -> AttackGround(BestGroundTarget)
    如果友军缺血且元素治疗可用 -> AttackAlly(InjuredAlly)
    如果危险高 -> MoveRetreat
Kaguya:
    如果 Lv>=2 且周围友军占敌城或低血 -> UnitAction(KAGUYAFRENCHAROUND)
    如果射程内友军占敌城或低血 -> AttackAlly(BestAlly)

Reisen:
    如果真实伤害可击杀 -> AttackEnemy(KillableEnemy)
    如果敌人密集且爆破可用 -> UnitAction(ReisenFrenchBoom)

Tewi:
    如果 Lv>=3 且相邻有作战友军 -> UnitAction(TEWIFRENCHBUFF)

Eirin:
    如果射程内残血英雄 -> AttackAlly(InjuredHero)
    如果射程内敌方英雄且无治疗目标 -> AttackEnemy(BestEnemyHero)
    如果射程内高价值友军需要治疗或协同 -> AttackAlly(BestAlly)

Mokou:
    如果 Lv>=3 且低血且敌人密集且友军风险低 -> UnitAction(MOKOUFRENCHBOOM)
    如果蛋形态低血 -> MoveRetreat
    如果蛋形态安全且需要牵制 -> MoveToward(EnemyDenseGrid)
Kanako:
    如果坐镇且需要移动或接敌 -> UnitAction(KANAKOUNSIT)
    如果安全且可坐镇且附近有战略收益 -> UnitAction(KANAKOSIT)

Suwako:
    如果可攻击地块并能召唤/压制 -> AttackGround(FarthestUsefulGround)

Sanae:
    如果射程内残血友军 -> AttackAlly(InjuredAlly)
    如果射程内敌方英雄且无治疗目标 -> AttackEnemy(BestEnemyHero)

Aya:
    如果二动能追击、回防或进入高收益格 -> UnitAction(AYAMOVEAGAIN)
    移动优先选接近敌人但威胁可控的远端格

Momiji:
    如果猎物标记目标可攻击 -> AttackEnemy(PreyTarget)
    如果猎物标记目标附近有安全格 -> MoveToward(PreyGrid)
Satori:
    如果可攻击未被封禁的敌方英雄 -> AttackEnemy(BestEnemyHeroWithoutSkillBan)
    如果恐惧目标收益高 -> AttackEnemy(FearTarget)

Koishi:
    如果骨堆友军不满血 -> MoveToward(BonePileAlly)
    如果可友军施法给骨堆目标 -> AttackAlly(BonePileAlly)

Rin:
    如果有 CorpseBuff 层数且可行动 -> UnitAction(KomeijiRinFire)
    如果城市经验转换收益更高 -> UnitAction(KomeijiRinCityExp)

Utsuho:
    如果骨堆爆破命中价值高 -> UnitAction(KomeijiBonePileBoom)
    如果移动范围内有敌方城市中心 -> MoveToward(EnemyCityCenter)

Yuugi:
    如果三格移动可进入高攻击收益格 -> MoveToward(BestAttackGrid)
    否则攻击高价值近战目标
Reimu:
    如果关键友军低血或即将承伤 -> AttackAlly(InjuredAlly)
    如果退魔层数/压力过高 -> UnitAction(ReimuPayClearExtermination)

Sumireko:
    如果灵球/地面目标能控线 -> AttackGround(BestGroundTarget)

Kasen:
    如果高压战斗且切形态收益高 -> UnitAction(KasenToggleOniForm)

Aunn:
    如果防守线需要支援 -> MoveToward(DefenseFront)
    保持与支援对象距离不过远

Suika:
    如果安全且生成小萃香收益高 -> UnitAction(SuikaCreateMiniByHp)
    如果被围攻或小萃香可解围 -> UnitAction(SuikaShakeOffMinis)
Byakuren / Miko / Zanmu:
    如果没有专属规则:
        使用 GenericHeroRule
    如果后续补专属技能:
        只增加 HeroRule不改车道结构

10. Tactic Lane

函数 TryPriorityTactic(ctx):
    返回 TryTactic(ctx, priorityOnly = true)
函数 TryTactic(ctx, priorityOnly = false):
    best = None

    对每个 battle in ctx.Cache.LocalBattles:
        action = FindBestUsefulAttack(ctx, battle.SelfUnit, battle.EnemyUnit)
        attackScore = ScoreAttackAction(ctx, action)
        如果 attackScore <= 0:
            继续
        score = 700 + battle.Value + attackScore * 0.1
        如果 priorityOnly 且 (score < Config.PriorityTacticScore 或 !IsPriorityTacticAction(ctx, action, attackScore)):
            继续
        candidate = Candidate(action, Tactic, score, priorityOnly ? "高价值局部战斗攻击" : "局部战斗攻击")
        best = MaxCandidate(best, candidate)

    对每个 action in ctx.Actions.Attacks:
        score = ScoreAttackAction(ctx, action)
        如果 score <= 0:
            继续
        如果 priorityOnly 且 (score < Config.PriorityTacticScore 或 !IsPriorityTacticAction(ctx, action, score)):
            继续
        candidate = Candidate(action, Tactic, score, priorityOnly ? "高价值攻击收益" : "普通攻击收益")
        best = MaxCandidate(best, candidate)

    返回 best
函数 IsPriorityTacticAction(ctx, action, attackScore):
    attacker = action.UnitData
    target = action.TargetUnitData
    damage = CalcDamage(ctx.Map, attacker, target)

    如果 !CanTreatAttackAsUseful(attacker, target, damage):
        返回 false

    如果 damage >= target.Health:
        返回 true
    如果 target 是英雄:
        返回 true
    如果 target 正在威胁城市:
        返回 true

    targetValue = UnitTargetValue(ctx, target)
    counterThreat = EstimateCounterDamageValue(ctx, target, attacker)
    如果 attackScore >= Config.PriorityTacticScore + 60 且 counterThreat <= targetValue * 0.15:
        返回 true

    返回 false
函数 ScoreAttackAction(ctx, action):
    attacker = action.UnitData
    target = action.TargetUnitData
    damage = CalcDamage(ctx.Map, attacker, target)

    如果 !CanTreatAttackAsUseful(attacker, target, damage):
        返回 0

    score = 620
    score += EstimateDamageValue(ctx, attacker, target) * 2
    score -= EstimateCounterDamageValue(ctx, target, attacker)
    score += CounterBonus(ctx, attacker, target)

    如果 CanKillByAction(ctx, action):
        score += 160

    如果 target 是英雄:
        score += 120

    如果 target 正在威胁城市:
        score += 120

    如果 attacker 站在己方城市中心:
        score -= EstimateCounterDamageValue(ctx, target, attacker)

    如果 与 target 所属玩家好感很高且未交战:
        score -= 80

    返回 score
函数 FindBestUsefulAttack(ctx, unit, preferredTarget = null):
    best = None
    bestScore = -INF

    对每个 action in ctx.Actions.GetAttackActions(unit):
        target = action.TargetUnitData
        如果 preferredTarget != null 且 target != preferredTarget:
            继续
        score = ScoreAttackAction(ctx, action)
        如果 score <= 0:
            继续
        如果 score > bestScore:
            bestScore = score
            best = action

    返回 best
函数 CanTreatAttackAsUseful(attacker, target, damage):
    如果 attacker == null 或 target == null:
        返回 false
    如果 damage > 0:
        返回 true
    如果 attacker 带有攻击附加收益:
        返回 true
    返回 false

攻击附加收益包括:
    YuugiPush
    HakureiDragonshipRam
    REISENFRENCHATTAK
    KaguyaFrenchWolf

11. UnitOpportunity Lane

函数 TryUnitOpportunity(ctx):
    BuildUnitOpportunities(ctx)

    对每个 opportunity in ctx.Cache.UnitOpportunities:
        candidate = Candidate(opportunity.Action, UnitOpportunity, opportunity.Value, "单位机会")
        如果 candidate.IsValid:
            返回 candidate

    返回 None
函数 ScoreUnitOpportunity(ctx, unit, action, type):
    如果 type == Capture:
        返回 ScoreCapture(ctx, unit, action)
    如果 type == Examine:
        返回 ScoreExamine(ctx, unit, action)
    如果 type == Gather:
        返回 ScoreGather(ctx, unit, action)
    如果 type == HeroUpgrade:
        返回 ScoreHeroUpgrade(ctx, unit, action)
    如果 type == Upgrade:
        返回 ScoreUpgrade(ctx, unit, action)
    如果 type == CultureUnitUpgrade:
        返回 ScoreCultureUpgrade(ctx, unit, action)
    如果 type == Recover:
        返回 ScoreRecover(ctx, unit, action)
    如果 type == ShipUpgrade:
        返回 ScoreShipUpgrade(ctx, unit, action)
    如果 type == AbsorbMarker:
        返回 ScoreAbsorbMarker(ctx, unit, action)
    返回 0
函数 ScoreCapture(ctx, unit, action):
    grid = ActionTargetGrid(action)
    score = 820

    如果 grid 是敌方城市中心:
        score += 220
    如果 grid 是村庄:
        score += 180
    如果 grid 是敌方首都:
        score += 120
    如果 GridThreat(ctx, unit, grid) 高:
        score -= 120
    如果 unit 是危险城市关键守军:
        score -= 200

    返回 score
函数 ScoreExamine(ctx, unit, action):
    grid = ActionTargetGrid(action)
    score = 760
    score += DevelopmentTargetValue(ctx, grid)
    score -= GridThreat(ctx, unit, grid) * 0.3
    返回 score
函数 ScoreGather(ctx, unit, action):
    grid = ActionTargetGrid(action)
    score = 680 + ResourceValue(ctx, grid)
    如果 当前战略是 Expansion 或 Development:
        score += 80
    如果 unit 是前线主力且城市危急:
        score -= 160
    返回 score
函数 ScoreRecover(ctx, unit, action):
    hp = HealthRatio(unit)
    hasNegativeState = unit 有 KomeijiFear 或 SAKUYATIRED
    如果 hp > Config.LowHpRatio 且 !hasNegativeState:
        返回 0
    score = 520 + (1 - hp) * 220
    如果 hasNegativeState:
        score += 160
    如果 unit 是英雄:
        score += 120
    如果 unit 当前能击杀高价值目标:
        score -= 180
    返回 score
函数 ScoreShipUpgrade(ctx, unit, action):
    如果 action 不是合法船升级:
        返回 0
    cost = GetShipUpgradeCost(unit, action.ActionId.UnitType)
    如果 ctx.Player.Coin < cost:
        返回 0

    score = 560 + UnitPower(unit) * 0.2
    如果 cost == 0:
        score += 120
    否则:
        score -= cost * 18
    如果 ctx.Cache.StrategicPosture 是 Expansion 或 Attack:
        score += 90
    返回 score
函数 ScoreAbsorbMarker(ctx, unit, action):
    missing = 1 - HealthRatio(unit)
    如果 missing <= 0.05:
        返回 0

    score = 500 + missing * 260
    如果 unit 是英雄:
        score += 100
    返回 score
函数 ScoreUpgrade(ctx, unit, action):
    如果 unit 当前承担 Emergency 或可击杀高价值目标:
        返回 0
    score = 610
    如果 升级后解锁克制当前敌军的兵种:
        score += 120
    如果 升级是海军转换且附近有海战目标:
        score += 80
    返回 score
函数 ScoreHeroUpgrade(ctx, unit, action):
    如果 unit 不是英雄:
        返回 0
    score = 760
    如果 unit 在前线或即将参战:
        score += 100
    如果 unit 低血:
        score += 60
    返回 score

12. Front Lane

函数 TryFront(ctx):
    best = None

    对每个 front in ctx.Cache.Fronts:
        对每个 unit in ctx.Cache.SelfUnits:
            如果 ShouldSkipFrontMove(ctx, unit, front):
                继续

            action = FindBestMoveToward(ctx, unit, ResolveFrontTarget(front))
            candidate = Candidate(action, Front, ScoreFrontMove(ctx, unit, action, front), "战线移动")
            best = MaxCandidate(best, candidate)

    返回 best
函数 ShouldSkipFrontMove(ctx, unit, front):
    如果 unit 没有移动行动点:
        返回 true

    如果 unit 是英雄 且 HeroPlaybook 对它有合法动作:
        返回 true

    如果 unit 低血 且 front.Type == Attack:
        返回 true

    如果 unit 是危险城市的守军,且城市处于 Emergency:
        返回 true

    如果 unit 是某个危险城市的唯一守军:
        返回 true

    返回 false
函数 ResolveFrontTarget(front):
    如果 front.Type == Defense:
        返回 front.AnchorGrid
    如果 front.TargetGrid != null:
        返回 front.TargetGrid
    返回 front.AnchorGrid
函数 ScoreFrontMove(ctx, unit, action, front):
    endGrid = GetActionEndGrid(action)
    target = ResolveFrontTarget(front)

    score = 0

    如果 front.Type == Defense:
        score = 620 + front.Pressure * 20
    如果 front.Type == Attack:
        score = 560 + front.Opportunity
    如果 front.Type == Development:
        score = 500 + front.Opportunity * 0.5
    如果 front.Type == Hold:
        score = 420

    score -= Distance(endGrid, target) * 16
    score -= GridThreat(ctx, unit, endGrid) * 0.4

    如果 endGrid 可在下回合攻击/占领目标:
        score += 80

    如果 unit 是高机动单位 且 target 较远:
        score += 40

    返回 score

13. Growth Lane

函数 TryGrowth(ctx):
    best = None
    如果 ShouldStopNonCriticalLane(ctx):
        返回 None

    best = MaxCandidate(best, TryCityGrowth(ctx))
    best = MaxCandidate(best, TryGridGrowth(ctx))
    best = MaxCandidate(best, TryPlayerGrowth(ctx))

    返回 best

13.1 CityGrowth

函数 TryCityGrowth(ctx):
    best = None
    scanned = 0

    对每个 plan in ctx.Cache.CityPlans:
        对每个 action in ctx.Actions.GetCityActions(plan.City):
            如果 scanned >= Config.MaxGrowthCityActionsPerDecision:
                跳出
            scanned += 1
            score = ScoreCityGrowth(ctx, action, plan)
            如果 ShouldSkipLowValueGrowth(ctx, score):
                继续
            candidate = Candidate(action, Growth, score, "城市发展")
            best = MaxCandidate(best, candidate)

    返回 best
函数 ScoreCityGrowth(ctx, action, plan):
    id = action.ActionId
    score = 360 + plan.Priority

    如果 plan.Kind == EmergencyDefense:
        如果 id.ActionType == TrainUnit:
            score += TrainUnitDefenseValue(ctx, action, plan.Threat)
            如果 plan.Threat.DefenderPower <= 0:
                score += 240
            如果 plan.Threat.IsCapital:
                score += 120
            如果 plan.Threat.CanBeThreatenedNextTurn:
                score += 100
        如果 id.ActionType == CityAction 且 id.CityActionType == BuildCityWall:
            score += 220
            如果 plan.Threat.DefenderPower <= 0:
                score += 180
        如果 id.ActionType in [StartWonder, BuildWonder]:
            score -= 300

    如果 plan.Kind == Mobilize 或 plan.Kind == Frontline:
        如果 id.ActionType == TrainUnit:
            score += TrainUnitWarValue(ctx, action)
        如果 id.ActionType == CityAction 且 id.CityActionType == BuildCityWall:
            score += 100
        如果 id.ActionType == CityLevelUpAction:
            score += 50

    如果 plan.Kind == BacklineGrowth:
        如果 id.ActionType == CityLevelUpAction:
            score += 140
        如果 id.ActionType == TrainUnit:
            score += NeedStandingArmy(ctx, plan.City) ? 80 : 20

    如果 plan.Kind == Wonder:
        如果 id.ActionType in [StartWonder, BuildWonder]:
            score += 160 + WonderValue(ctx, action)

    返回 score

13.2 GridGrowth

函数 TryGridGrowth(ctx):
    best = None
    scanned = 0

    对每个 action in ctx.Actions.GridActions:
        如果 scanned >= Config.MaxGrowthGridActionsPerDecision:
            跳出
        scanned += 1
        score = ScoreGridGrowth(ctx, action)
        如果 ShouldSkipLowValueGrowth(ctx, score):
            继续
        candidate = Candidate(action, Growth, score, "地块发展")
        best = MaxCandidate(best, candidate)

    返回 best
函数 ScoreGridGrowth(ctx, action):
    grid = ActionGrid(action)
    score = 330

    如果 grid 属于危险城市附近 且 action 不是防守/道路/关键资源:
        score -= 100

    如果 action 是 Build:
        score += BuildResourceValue(ctx, action)

    如果 action 是 Gain:
        score += ResourceValue(ctx, grid)

    如果 action 是 GridMisc:
        score += GridMiscValue(ctx, action)

    如果 grid 接近 DevelopmentTarget:
        score += 60

    返回 score

13.3 PlayerGrowth

函数 TryPlayerGrowth(ctx):
    best = None
    scanned = 0

    对每个 action in ctx.Actions.PlayerActions:
        如果 scanned >= Config.MaxGrowthPlayerActionsPerDecision:
            跳出
        scanned += 1
        score = ScorePlayerGrowth(ctx, action)
        如果 ShouldSkipLowValueGrowth(ctx, score):
            继续
        candidate = Candidate(action, Growth, score, "玩家发展")
        best = MaxCandidate(best, candidate)

    返回 best
函数 ShouldStopNonCriticalLane(ctx):
    如果 HasUnresolvedEmptyCityCoreThreat(ctx):
        返回 true
    如果 Config.MaxNonCriticalActionsPerPlayerTurn <= 0:
        返回 false
    如果 ctx.Cache.SelfCities.Count < Config.ExpansionUrgentCityThreshold:
        返回 false
    返回 ctx.ExecutedActionCountThisTurn >= Config.MaxNonCriticalActionsPerPlayerTurn

函数 HasUnresolvedEmptyCityCoreThreat(ctx):
    对每个 threat in ctx.Cache.CityThreats:
        如果 IsUnresolvedEmptyCityCoreThreat(threat):
            返回 true
    返回 false

函数 IsUnresolvedEmptyCityCoreThreat(threat):
    返回 threat 非空
       且 !threat.HasCityCenterDefender
       且 threat.CanBeThreatenedNextTurn
       且 (threat.IsCapital 或 threat.IsCritical)

函数 ShouldSkipLowValueGrowth(ctx, score):
    如果 Config.SoftGrowthActionBudget <= 0:
        返回 false
    如果 ctx.ExecutedActionCountThisTurn < Config.SoftGrowthActionBudget:
        返回 false
    返回 score < Config.MinGrowthPriorityAfterSoftBudget
函数 ScorePlayerGrowth(ctx, action):
    id = action.ActionId

    如果 id.ActionType == LearnTech:
        返回 ScoreTech(ctx, action)

    如果 id.ActionType == BuyCultureCard:
        返回 0

    如果 id.ActionType == PlayerAction:
        返回 ScorePlayerAction(ctx, action)

    返回 0
函数 ScoreTech(ctx, action):
    score = 300

    如果 ctx.Cache.StrategicPosture == Defense:
        score += TechDefenseValue(ctx, action)

    如果 ctx.Cache.StrategicPosture == Attack:
        score += TechMilitaryCounterValue(ctx, action)

    如果 ctx.Cache.StrategicPosture == Expansion:
        score += TechResourceAndMobilityValue(ctx, action)

    如果 ctx.Cache.StrategicPosture == Development:
        score += TechEconomicValue(ctx, action)

    score += TechSuccessorPreviewValue(ctx, action) * 0.5

    如果 TechRequiresSeaButNoSeaNearby(ctx, action):
        score = 0

    返回 score
函数 ScoreCultureCard(ctx, action):
    如果 !IsAIHeroSlotCultureCard(ctx.Player, action.CultureCardType):
        返回 0

    score = 860

    如果 action.CultureCardType == SecondHero 且 MaxHeroCount == 1:
        score += 80

    如果 action.CultureCardType == ThirdHero 且 MaxHeroCount == 2:
        score += 70

    如果 当前没有在场英雄:
        score -= 120

    返回 score
函数 ScorePlayerAction(ctx, action):
    id = action.ActionId.PlayerActionType

    如果 id 是宝物选项:
        返回 520 + TreasureOptionValue(ctx, action)

    如果 id == Embassy:
        如果 目标好感 >= 70 且 金币足够:
            返回 360
        返回 0

    如果 id == OfferAlly:
        如果 目标好感 >= 90 且 无直接扩张冲突:
            返回 340
        返回 0

    如果 id == BreakAlly:
        如果 目标好感 <= 35 且 非队友:
            返回 220
        返回 0

    返回 180

14. Fallback

函数 TryFallback(ctx):
    如果 ShouldStopNonCriticalLane(ctx):
        返回 None

    如果 IsPastSoftGrowthBudget(ctx):
        action = FindStrictFallbackAction(ctx)
        reason = "软预算后严格兜底"
    否则:
        action = FindBestFallbackAction(ctx)
        reason = "兜底合法动作"

    candidate = Candidate(action, Fallback, 1, reason)
    如果 candidate.IsValid:
        candidate.IsFallback = true
        返回 candidate

    返回 None
函数 FindBestFallbackAction(ctx):
    order = [
        ctx.Actions.Attacks,
        ctx.Actions.AttackGrounds,
        ctx.Actions.CityActions,
        ctx.Actions.GridActions,
        ctx.Actions.PlayerActions,
        ctx.Actions.UnitActions
    ]

    对每个 list in order:
        对每个 action in list:
            如果 IsFallbackAction(action):
                返回 action

    返回 None

函数 FindStrictFallbackAction(ctx):
    对每个 action in ctx.Actions.Attacks:
        如果 ScoreAttackAction(ctx, action) > 0:
            返回 action

    对每个 action in ctx.Actions.AttackGrounds:
        返回 action

    对每个 action in ctx.Actions.UnitActions:
        如果 action.UnitActionType in [Capture, Recover, Upgrade, CultureUnitUpgrade, HeroUpgrade, AbsorbRedMist, HakureiAbsorbRune, WarCry]:
            返回 action

    对每个 action in ctx.Actions.PlayerActions:
        如果 action.PlayerActionType 是宝藏选择或宝藏收益:
            返回 action

    返回 None

Fallback 的目标是不断回合,不负责聪明。大量进入 Fallback 说明前面车道缺规则。软预算之后 Fallback 只能执行明确收益动作,避免强国后期用低价值建设或清理动作把回合拖长。


15. 评分辅助函数

15.1 稳定排序

函数 MaxCandidate(a, b):
    如果 a 无效:
        返回 b
    如果 b 无效:
        返回 a
    如果 b.Priority > a.Priority:
        返回 b
    如果 b.Priority == a.Priority 且 StableActionKey(b.Action) < StableActionKey(a.Action):
        返回 b
    返回 a
函数 StableActionKey(action):
    返回 (
        action.ActionId.ActionType,
        action.Param.PlayerId,
        action.Param.CityId,
        action.Param.UnitId,
        action.Param.GridId,
        action.Param.TargetUnitId,
        action.Param.TargetGridId,
        action.ActionId 子类型字段
    )
函数 BuildIntentKey(lane, kind, targetId):
    如果 targetId == 0:
        返回 ""
    返回 lane + ":" + kind + ":" + targetId

函数 IsIntentBlocked(ctx, intentKey):
    如果 intentKey 为空:
        返回 false
    返回 intentKey in ctx.BlockedIntentKeysThisTurn

函数 IsIntentBudgetReached(ctx, lane, maxCount):
    如果 maxCount <= 0:
        返回 false
    count = ctx.BlockedIntentKeysThisTurn 中以 lane + ":" 开头的数量
    返回 count >= maxCount

函数 IsUsefulMoveToward(ctx, startGrid, endGrid, targetGrid):
    如果 startGrid == null 或 endGrid == null 或 targetGrid == null:
        返回 false
    startDistance = Distance(startGrid, targetGrid)
    endDistance = Distance(endGrid, targetGrid)
    返回 endDistance < startDistance 或 endDistance <= 1

15.2 伤害和威胁

函数 EstimateDamageValue(ctx, attacker, target):
    damage = EstimateDamage(ctx, attacker, target)
    return damage / MaxHealth(target) * UnitMilitaryValue(ctx, target)
函数 GridThreat(ctx, unit, grid):
    如果 ctx.GridThreatCache 包含 grid.Id:
        返回 ctx.GridThreatCache[grid.Id]

    threat = 0

    对每个 enemy in ctx.Cache.EnemyUnits:
        enemyGrid = enemy.Grid
        如果 Distance(enemyGrid, grid) <= MoveRange(enemy) + AttackRange(enemy):
            threat += EstimateDamageValue(ctx, enemy, unit)

    ctx.GridThreatCache[grid.Id] = threat
    返回 threat
函数 CanKill(ctx, attacker, target):
    return EstimateDamage(ctx, attacker, target) >= target.Health

15.3 资源和建设价值

函数 ResourceValue(ctx, grid):
    value = 0

    如果 grid 资源能补当前短缺:
        value += 100

    如果 grid 资源可被当前科技开发:
        value += 80

    如果 grid 靠近后方安全城市:
        value += 40

    如果 grid 处于高威胁区:
        value -= 80

    返回 value
函数 BuildResourceValue(ctx, action):
    grid = ActionGrid(action)
    value = ResourceValue(ctx, grid)

    如果建筑能形成加工链:
        value += 60

    如果建筑是阵营特色高收益建筑:
        value += 80

    返回 value

15.4 训练单位价值

函数 TrainUnitDefenseValue(ctx, action, threat):
    unitType = action.ActionId.UnitFullType
    score = UnitBaseDefenseValue(unitType)

    对每个 enemy in threat.EnemyUnits:
        score += CounterValue(unitType, enemy.UnitFullType)
        score += SimulatedFightValue(unitType, enemy)

    返回 score
函数 TrainUnitWarValue(ctx, action):
    unitType = action.ActionId.UnitFullType
    score = UnitBaseMilitaryValue(unitType)

    对每个 enemy in 当前进攻目标玩家单位:
        score += CounterValue(unitType, enemy.UnitFullType)

    如果 当前需要扩张:
        score += TrainUnitExpansionValue(unitType)

    如果 unitType 是远程单位:
        如果 已接敌:
            score += 95
        否则:
            score += 45

    如果 unitType 是攻城单位或机动单位:
        score += 45

    如果 unitType 是特殊压力单位:
        如果 已接敌:
            score += 70
        否则:
            score += 30

    返回 score
函数 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)
函数 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

16. 正确性约束

16.1 权威行为

所有最终动作:
    必须来自 ActionPool
    必须 CheckCan
    必须 CompleteExecute

AI 不复制:

资源扣除
行动点扣除
城市易主
单位死亡
技能触发
网络同步
回放记录

16.2 确定性

禁止在权威选择路径使用:

UnityEngine.Random
当前系统时间
未排序 Dictionary/HashSet 迭代结果作为最终选择
真实 Main.MapData 假设
UI/Renderer 状态

同分必须用稳定 key 解决。

16.3 技能安全

英雄和单位技能只通过 Action 入口触发:

UnitAttack
UnitAttackAlly
UnitAttackGround
UnitAction
UnitMove

AI 不直接调用 SkillBase 的效果方法。


17. 性能约束

每次 Decide:
    构建一次基础 WorldCache
    构建一次 ActionPool
    补一次 UnitOpportunity
    每个车道只查缓存和 ActionPool

复杂度目标:

CityThreat: SelfCities * nearby EnemyUnits
LocalBattle: SelfUnits * nearby EnemyUnits
Front: SelfCities * EnemyCities + DevelopmentTargets
ActionPool: MaxActionCount 上限
Growth: 按 CityPlan 顺序扫描 TopN 城市动作、TopN 地块动作、TopN 玩家动作

性能保护:

限制移动候选数量
限制 DevelopmentTarget TopN
限制 Front TopN
限制自城移动锚点 TopN
限制 Emergency responder 扫描数量
限制 Growth 每次决策扫描数量
软预算后过滤低价值 Growth硬预算后 Growth/Fallback 停手
限制 LocalBattle 搜索半径
一次决策内缓存 FindBestMoveToward(unit, targetGrid)
一次决策内缓存 GridThreat(grid)
ActionPool 构建阶段完成 CopyAction + CheckCan
各车道 Candidate 只引用已验证 action不重复 CopyAction + CheckCan
最终 Execute 前再 CheckCan 一次兜底
compact batch 诊断只写分析所需字段Decision 保留 lane/reason/action/actionPoolExecution 保留 action、关键 delta、轻量 before/after完整候选明细只在非 compact 调试时打开
科技只看当前可学和一层后继预览

18. 测试脚本标准

18.0 批量诊断指标

批量指标不参与单次决策,只服务测试循环。

结构 DefenseMetrics:
    CriticalCityThreatTurns
    CapitalThreatTurns
    EmptyThreatenedCityTurns
    CityLost
    CityGained
    CityThreatResolved
    CityThreatWorsened
    EmergencyDecisions
    EmergencyExecutions
    DefenderReturn
    HeroDefensiveUse
    EmergencyResponseRate
    DefenseScore
结构 ActorExpressionMetrics:
    Decisions
    Executions
    Kills
    SelfDeaths
    DamageToTarget
    DamageTaken
    SelfHeal
    Moves
    Captures
    RecoverActions
    SkillChanged
    ThreatResolved
    ThreatWorsened
    CityOwnerChanged
    NoEffect
    TopActions
结构 BatchExpressionMetrics:
    HeroPersonalExpression: Map<GiantType, ActorExpressionMetrics>
    UnitRoleExpression: Map<UnitRole, ActorExpressionMetrics>
    UnitTypeExpression: Map<UnitType, ActorExpressionMetrics>
    SkillSignatureExpression: Map<CompactSkillSignature, ActorExpressionMetrics>
函数 BuildTurnDefenseSnapshot(map, player):
    snapshot.CityIds = 当前 player 拥有城市 Id 排序签名
    snapshot.CapitalCityIds = 当前 player 首都 Id 排序签名
    snapshot.CityThreatCount = 受威胁城市数量
    snapshot.CriticalCityThreatCount = 危急城市数量
    snapshot.CapitalThreatCount = 受威胁首都数量
    snapshot.EmptyThreatenedCityCount = 受威胁且城市中心无己方/同盟单位驻守的城市数量
    snapshot.MaxCityDangerScore = 所有城市威胁中的最高危险值
    返回 snapshot
函数 BuildActionDefenseDelta(before, after):
    delta.CityThreatResolved =
        before.CityThreatCount > 0
        且 (
            after.CriticalCityThreatCount < before.CriticalCityThreatCount
            或 after.CityThreatCount < before.CityThreatCount
            或 after.MaxCityDangerScore < before.MaxCityDangerScore
        )

    delta.CityThreatWorsened =
        after.CriticalCityThreatCount > before.CriticalCityThreatCount
        或 after.CityThreatCount > before.CityThreatCount
        或 after.EmptyThreatenedCityCount > before.EmptyThreatenedCityCount
        或 after.MaxCityDangerScore > before.MaxCityDangerScore

    返回 delta

城市丢失不从单个 action 直接统计,按同一玩家两次 TurnStart 对比:

函数 AccumulateCityLoss(previousTurnStart, currentTurnStart):
    如果 current.CityCount < previous.CityCount:
        CityLost += previous.CityCount - current.CityCount
    如果 current.CityCount > previous.CityCount:
        CityGained += current.CityCount - previous.CityCount
    如果 current.CapitalCityIds != previous.CapitalCityIds:
        CapitalOwnershipChanged += 1

防守综合分只用于批量对比:

DefenseScore =
    CityThreatResolved * 3
    + EmergencyExecutions
    + DefenderReturn
    + HeroDefensiveUse * 2
    - CityLost * 8
    - CapitalThreatTurns * 2
    - EmptyThreatenedCityTurns * 2
    - CityThreatWorsened * 2
EmergencyResponseRate =
    EmergencyDecisions / max(1, CriticalCityThreatTurns + EmptyThreatenedCityTurns)

英雄风格表达按 reason/action 归桶:

函数 ClassifyHeroStyle(reason, actionKey):
    如果 包含 LowHp、Recover、Heal、Eirin、Sanae、Patchouli、Absorb、Revive:
        返回 Recovery
    如果 包含 Defense、Protect、Guard、MoveToCity、Aunn、Meiling、Sakuya、Reimu:
        返回 Defense
    如果 包含 Kill、Flandre、AttackValue、Boom、Mokou、Reisen、Yuugi:
        返回 Burst
    如果 包含 Ban、Fear、Control、Satori、Koishi、Sumireko、Orb、Ground:
        返回 Control
    如果 包含 Summon、CreateMini、Suwako、Snake、BonePile、Mini:
        返回 Summon
    如果 包含 Economy、CityExp、Corpse、KanakoSit、Rin、Tewi、Kaguya:
        返回 Economy
    如果 包含 MoveAgain、MoveToFront、Retreat、Mobility、Aya、Kasen:
        返回 Mobility
    如果 包含 SelectHero、SpawnHero、FinishLowestTask:
        返回 HeroLifecycle
    返回 General

特色单位表达按技能签名统计:

函数 AccumulateUnitSkillExpression(execution):
    before = execution.Before
    action = execution.Action
    delta = execution.Delta

    如果 before.UnitSkillSignature 为空:
        返回

    UnitSkillActions[action.ActionKey] += 1
    ActorSkillSignatures[Compact(before.UnitSkillSignature)] += 1

    如果 delta.UnitSkillSignatureChanged
       或 delta.TargetUnitSkillSignatureChanged
       或 delta.UnitSkillDelta != 0
       或 delta.TargetUnitSkillDelta != 0:
        UnitSkillChangedActions[action.ActionKey] += 1

英雄个人表达按真实执行统计,不只看 HeroPlaybook

函数 AccumulateHeroPersonalExpression(decision, execution):
    action = execution.Action
    delta = execution.Delta
    hero = action.ActorGiantType 或 action.GiantType 或 action.TargetActorGiantType

    如果 hero 为空:
        返回

    metrics = HeroPersonalExpression[hero]
    metrics.Executions += 1
    metrics.DamageToTarget += max(0, -delta.TargetUnitHealthDelta)
    metrics.DamageTaken += max(0, -delta.UnitHealthDelta)
    metrics.SelfHeal += max(0, delta.UnitHealthDelta)
    如果 delta.TargetUnitDied: metrics.Kills += 1
    如果 delta.UnitDied: metrics.SelfDeaths += 1
    如果 delta.UnitMoved: metrics.Moves += 1
    如果 delta.UnitSkillSignatureChanged 或 delta.TargetUnitSkillSignatureChanged: metrics.SkillChanged += 1
    如果 delta.SelectedHeroDelta > 0: metrics.Selected += delta.SelectedHeroDelta
    如果 delta.HeroDelta > 0: metrics.Spawned += delta.HeroDelta
    metrics.TopActions[action.ActionKey] += 1

小兵定位表达按角色和具体类型同时统计:

函数 ResolveUnitRole(unitType):
    如果 unitType in [Warrior, Swordsman, KaguyaFrenchWarrior, KaguyaFrenchAnimalWarrior, HakureiBerserk]:
        返回 Melee
    如果 unitType in [Rider, Knights, MoriyaRider, MoriyaKnight, KomeijiIndianRider, KomeijiIndianKnight, HakureiValkyrie]:
        返回 Mobility
    如果 unitType in [Archer, KomeijiIndianArcher]:
        返回 Ranged
    如果 unitType in [Catapult, KaguyaFrenchCatapult, KomeijiIndianCatapult]:
        返回 Siege
    如果 unitType in [Defender, HakureiRoundShieldman]:
        返回 Defender
    如果 unitType 是船:
        返回 Naval
    如果 unitType 是召唤物:
        返回 Summon
    返回 Special

函数 AccumulateUnitRoleExpression(execution):
    action = execution.Action
    delta = execution.Delta
    unitType = action.ActorUnitType 或 action.UnitType

    如果 unitType 为空 或 unitType 是英雄 Giant:
        返回

    role = ResolveUnitRole(unitType)
    同时累加 UnitRoleExpression[role] 和 UnitTypeExpression[unitType]:
        Executions、Kills、SelfDeaths、DamageToTarget、DamageTaken、Moves、Captures、SkillChanged、NoEffect、TopActions

18.1 城市防守

给 AI 城市附近放敌军。
期望:
    Emergency 返回攻击威胁、回防、或危险城市生产。
    批量日志中 cityLost 尽量为 0。
    capitalThreatTurns 不应长期升高。
    resolved 应高于 worsened。

18.2 占领扩张

给 AI 单位脚下放村庄、敌空城、遗迹。
期望:
    无更高车道时 UnitOpportunity 执行占领或探索。

18.3 英雄机制

给 AI 放残血友军、敌方英雄、地面攻击目标、自爆窗口。
期望:
    HeroPlaybook 选择对应英雄动作。
    styleBuckets 中出现对应英雄风格,不应长期只有 General。

18.3.1 特色小兵机制

给 AI 放置新阵营特色小兵和普通小兵混合场景。
期望:
    unitSkillActions 中出现特色小兵动作。
    actorSkillSignatures 能看到该阵营技能组合。
    如果技能会改变状态unitSkillChangedActions 应出现对应 action。

18.4 战线移动

无局部战斗、无机会动作。
期望:
    DefenseFront 优先于 AttackFront。
    AttackFront 优先于 DevelopmentFront。
    DevelopmentFront 优先于 HoldFront。

18.5 内政发展

安全局面。
期望:
    后方城市升级、建设、科技文化。
    前线城市生产军力。

18.6 性能

中大型地图 AI 连续执行。
期望:
    单次 Decide 不重复全量生成动作。
    候选数量不长期超过 MaxActionCount。
    无明显卡顿尖峰。