AI algorithm

Last updated:

Published on

Overview

概述

StrongStrategy is the default solo autoplay pipeline (no LLM). Maintainer deep-dive: code under src/AI/, regression via tools/ai-bench/, mod hooks in Mod AI integration. Full write-up in Chinese below.

DevMode AI Host → AutoPlay 默认使用 StrongStrategy(设置项 AutoPlayStrategy: Strong)。这是一套纯规则、无 LLM 的单机自动化决策管线,目标是在 A10 左右具备可玩的宏观路线与战斗表现。

相关代码:src/AI/ · 回归脚本:tools/ai-bench/ · Mod 扩展说明:Mod AI 集成


总览

层级职责主要类型
循环轮询当前阶段、触发决策、写 AiDecisionLogAiPlayModule, GameLoop
快照把 Run / 战斗 / UI 状态序列化为 JsonObjectGameSnapshot, GameSnapshotPhaseCapture
规划根据牌组 / 遗物 / 层数推断「想要什么牌」DeckPlanInferer, DeckPlan
评分宏观阶段选最优 GameAction*Scorer(Map/Shop/…)
战斗搜索主路径:beam + 贪心线评估;fallback:CombatScorerCombatPlanner, CombatBeamSearch, CombatSetupEvaluator
执行点击 UI / 出牌 / 购货Sts2ActionExecutor

策略解析顺序(Companion / 多人场景同样适用):netId 注册表 → 角色 CharacterAiRegistry → 默认 StrongStrategyAutoPlayStrategy=Simple 时回退 SimpleStrategy)。


决策循环

  1. 单机 Run 开始且 AutoPlayEnabled=true 时,AiPlayModule 启动 GameLoop
  2. PollIntervalMs 轮询一次当前 GamePhase;Harmony patch 也会在决策点调用 OnDecisionPoint
  3. Sts2StateProvider 调用 GameSnapshot.Capture,再经 GameSnapshotPhaseCapture.Enrich 补充 overlay 数据。
  4. IDecisionMaker.DecideAsync 返回 GameAction(含 TypeTargetIndexReason)。
  5. Sts2ActionExecutor 执行动作;Reason 写入日志,便于复盘。

多人 hand-play 时 AutoPlay 自动关闭;LAN / Pseudo Co-op 走独立队列,见 LAN host-drive & AFK co-op


快照字段(StrongStrategy 依赖)

全局

字段含义
totalFloor, actIndex, actFloor层数 / Act
gold, currentHp, maxHp经济与健康
characterId, ascensionLevel角色与进阶
deck[]牌组(id、name、rarity、cost、keywords、upgradeLevel)
relics[]已拥有遗物
potions[]药水:id、slot、category、usage、rarity、retainScore
hasOpenPotionSlots, potionSlotCount腰带空位

战斗 combat

字段含义
currentEnergy当前能量
playerBlock玩家格挡
hand[]手牌(canPlay、cost、damage、block、targetType)
drawPile[] / discardPile[] / exhaustPile[]牌堆顺序(index 0 = 顶)
rngShuffle{ seed, counter } — 官方 RunRngType.Shuffle 流,用于模拟回洗
turnNumber战斗回合数(CombatState.RoundNumber
enemies[]敌人 HP、block、intentDamage、intentBlock、isAlive、intentSteps[](下 1–2 回合预测)

阶段扩展(GameSnapshotPhaseCapture

字段阶段含义
offeredCards[]CardReward奖励 / 选牌界面上的卡
deckSelectContextCardReward"reward" / "upgrade" / "remove" / "deckPick"
offeredRelics[]RelicSelection可选遗物
shopOffers[]Shopcard / relic / potion / removeCard
restOptions[]RestSite休息选项按钮
restProceedReadyRestSiteProceed 是否可点(选完 heal/smith 后离开)
mapNodes[]MapSelection可选地图节点
eventOptions[], eventIdEventChoice事件选项

Mod 扩展写入 snapshot["extensions"][yourKey],StrongStrategy 不会读取 mod 专有字段,需注册自定义 IDecisionMakerIAiMoveModifier(经 SimMoveScoring 作用于 beam / QuickScore)。


DeckPlan(牌组规划向量)

DeckPlanInferer.Infer(snapshot) 在每次宏观决策前构建规划,供所有 Scorer 共用。

输出

属性默认说明
TargetDeckSize18理想牌组大小
ThinPreference动态 ∈ [-0.3, 1]删牌 / 跳过奖励的倾向
Weights[AiTag]见下各语义 tag 权重

基础权重(推断起点)

  • Attack +1.0,Block +0.8,Draw +0.6
  • 牌组含 ≥3 张 Exhaust 卡或 Exhaust 遗物 → Exhaust +1.2
  • A7+ → Block +0.4;A10+ → Attack +0.3

ThinPreference 调整

条件调整
牌组 >18 且抽牌 tag 卡 <2+0.35
牌组 > TargetDeckSize+4+0.25
拥有 Thin 类遗物+0.2
Act1 且 floor<15-0.15(早期略厚)
Act3+ 且牌组 >22+0.30

最后调用 DeckPlanContributorHub:原版五角色在 src/AI/Characters/Vanilla/*Pack.cs 注册额外权重与 TargetDeckSize(Silent/Defect 15、Ironclad 17、Necrobinder/Regent 16)。

CodexPriorCatalog(社区 prior,可选)

离线从 Spire Codex A10 宏观样本训练(tools/codex-train/ → 嵌入 src/AI/Data/codex-priors.json)。启动时由 AiKnowledgeBootstrap 加载;不替代规则层,只在各 *Scorer 上叠加有界 bonus(±15,× CodexPriorWeight,默认 1,settings.json 可调 0 关闭)。

用途接入点
cards选牌 pick_rate / bonusMacroScorerHelper.ScoreCardOffer
skipskip 阈值偏移DeckEvaluator.SkipOpportunityCost
restHEAL/SMITH/LIFT 偏好RestScorer(HP 45–85% 区间)
remove常删牌 bonusShopScorerDeckSelectScorer
relics遗物 pick bonus(event / combat_reward / shop context)MacroScorerHelper.ScoreRelicOffer
eventsNeow / 事件选项 pick bonusEventChoiceScorer via GetEventOptionBonus

NormalizeContext 支持 shopeventcombat_reward(此前 event 被误映射为 combat_reward,已修复)。

重训流程:tools/codex-crawl export-parquetuv run codex-train --parquet ../codex-crawl/data/macro_samples.parquet traindotnet build

Holdout(v1,4944 runs):card_choice top-1 56.7%(随机 33%);rest 60.4%

辅助公式

ScoreTags(tags, plan) = Σ plan.GetWeight(tag)
DilutionPenalty(deckSize, plan) = max(0, deckSize - TargetDeckSize) × ThinPreference × 0.8

DeckEvaluator(牌组质量与删牌边际收益)

DeckEvaluator.Evaluate(snapshot, plan) 是删牌 / skip / 选牌删谁 的单一真相源src/AI/Planning/DeckEvaluator.cs)。

DeckMetrics

字段含义
MeanValue牌组内单卡均分
WorstValue / WorstCardName最低分卡(删牌首选)
RemovalUplift删最差卡的边际收益(含 burn 压力项)
StarterBloatStrike/Defend/Curse 冗余度(相对 plan 目标)
StrikeSurplus超出 TargetStrikeCount 的 Strike 张数
DefendSurplus超出 TargetDefendCount 的 Defend 张数
ThinGap超出 TargetDeckSize 的牌数
ExhaustCount带 Exhaust tag 的卡数
CardsNeedingBurn宏观「还需烧/删」债务:thinGap + starter 冗余;exhaust 构筑额外计入非 exhaust 填充卡
BlockSourceCount非 starter、cost≤2 的过渡防张数(DeckSurvivability
DrawSourceCount非 starter 抽牌源张数
BlockDeficitmax(0, TargetBlockSources − BlockSourceCount)
DrawDeficitmax(0, TargetDrawSources − DrawSourceCount)
SurvivalGapBlockDeficit×2 + DrawDeficit
ConsistencyScore一致性 0…1

DeckPlan 续航目标(*Pack.cs):Silent block/draw 1/3;Ironclad/Defect 2/2;默认 block/draw 2/2。Strike/Defend 目标:Silent 0/1;Ironclad 2/2

宏观选牌续航 bonusMacroScorerHelper):过渡防 +6+BlockDeficit×4;抽牌源 +4+DrawDeficit×3。Skip:BlockDeficit≥2 → −6;DrawDeficit≥2 且 deck 厚 → −4;续航满且 MeanValue≥12 → +4。

单卡价值 ScoreInDeck(DeckCardScoring

已在牌组内的卡,不含 dilution 项,但含冗余惩罚:

因素逻辑
基础ScoreTags + RarityScore + 0费/高费/已升级
诅咒−40
Strike 冗余每张 −15×max(0, strikeCount−1),与 deckSize 无关
Defend 冗余每张 −12×max(0, defendCount−1)
Starter 且 tag 分低−10
0 费且 tag 分低−5

RemovalUplift

RemovalUplift = (MeanValue - WorstValue)
              + StarterBloat × 4
              + round(DilutionPenalty(deckSize))
              + FutureThinBonus(actIndex, floor)
              + round(CardsNeedingBurn × 1.5)
  • StarterBloatStrikeSurplus + DefendSurplus×0.8 + 每张诅咒 +3(目标来自 TargetStrikeCount / TargetDefendCount
  • CardsNeedingBurnThinGap + StrikeSurplus + DefendSurplus;若 IsExhaustFocused,再加 max(0, nonExhaustFiller − ExhaustCount×2)
  • FutureThinBonus:Act1 +5/+3,Act2 +2,Act3 +0(越早删,长期收益越高)
  • 门槛RemovalUplift < MinRemovalUplift(11) 时不买商店删牌(无 deckSize 硬门槛

示例:12 张牌、4 Strike、均分 14、最差 Strike 值 2 → uplift ≈ (14−2)+8+future ≈ 24,小卡组仍会删 Strike。


知识层:Tag 与 Catalog

AiTag 枚举

Attack, Block, Draw, Exhaust, Scaling, Thin, Energy, Aoe, Setup, Utility

CardCatalog

  • 启动时在 ModelDb.Init 之后索引全卡(AiKnowledgeBootstrap + ModelDbInitPatch)。
  • CardTagRules卡 id 前缀CardTypeCardKeyword(Exhaust/Retain 等)推断 tag,避免逐卡硬编码。
  • CardMechanicIndex + OfficialMechanicProbe:CardKeyword / DynamicVars / 类型图 / LocString key / 字段引用 → CardMechanicFlags(含 AddsCardsToDeck 如劫掠);DeckSynergyEvaluator 按机制类型算 deck 协同;仅无结构信号时才用英文描述 fallback。
  • Mod 可通过 ICardTagProvider 合并额外 tag。

RelicCatalog / RelicMechanicIndex

  • 从遗物 id 推断 tag(如 Exhaust、Thin),用于 DeckPlanInferer 与遗物评分。
  • RelicMechanicIndex 通过 OfficialMechanicProbe 解析 RelicModel 结构与 loc key(如 HeftyTablet → 稀有三选一 + Injury);描述文本为最后兜底。

StrongStrategy 阶段路由

GamePhase决策器动作类型
CombatPotionScorerCombatSearch(beam)→ CombatScorer(fallback)UsePotion / PlayCard / EndTurn
MapSelectionMapScorerMapPathPlannerSelectMapNode
CardRewardDeckSelectScorerPickCardReward / SkipCardReward
RelicSelectionRelicScorerPickRelic
ShopShopScorerPurchaseShopItem / RemoveCardAtShop / LeaveShop
RestSiteRestScorerRest / UpgradeCard / Proceed
EventChoiceEventChoiceScorerSelectEventChoice
RewardScreen固定CollectReward
PostCombatTransition固定Proceed
TreasureRoom固定HandleTreasureRoom

宏观评分器

CardRewardScorer(战斗后三选一)

用于 deckSelectContext == "reward"

单卡得分MacroScorerHelper.ScoreCardOffer)— 边际牌组模型:

marginal = DeckQualityScore(deck ∪ {card}) − DeckQualityScore(deck)
score    = marginal
         + DeckSynergyEvaluator(机制协同,如变形×攻击池)
         + ScoreDeckDilutionOffer(AddsCardsToDeck 如劫掠)
         + EarlyCardRewardAdjustments
         + CodexBonus × deckFitFactor

DeckQualityScore 综合 TotalValueMeanValueDilutionPenaltySurvivalGapStarterBloatConsistencyScore(与删牌评估同一套 DeckEvaluator)。

deckFitFactor:缺口已填(BlockDeficit=0DrawDeficit=0)、ThinGap>0MeanValue≥12、已有变形核心且候选非变形 → 衰减 Codex 先验。

RarityScore(在 ScoreInDeck 内):Ancient 30 · Rare 25 · Uncommon 15 · Common 8 · Starter 3 · Event 12

跳过逻辑DeckEvaluator.SkipOpportunityCost,与边际 pick 同一质量模型):

skipScore = ThinPreference×14
          + DilutionPenalty×8
          + ThinGap×3
          + max(0, DeckSize − TargetDeckSize)×2
          + max(0, DeckQualityScore/DeckSize − 12)×2
          + (SurvivalGap=0 ? 5 : 0)
          + 后期 act/floor 加厚项
          + Codex skip offset
  • StarterBloat≥3 / StrikeSurplus≥3 → 降低 skip(更倾向拿牌或去删敲)
  • SurvivalGap≥2BlockDeficit≥2 → 降低 skip(前期仍缺过渡)
  • exhaust 构筑且 CardsNeedingBurn≥5 → +8 skip
  • bestScore < max(MinPickScore, skipScore)SkipCardReward(日志含 quality / thin / survival

DeckSelectScorer(休息 smith / 商店删牌)

deckSelectContext 分流:

context行为
upgradeScoreUpgradeCandidate 最高、未升满的卡(与 AiCombatCardSelector 篝火 smith 同源)
removeScoreInDeck 最低的卡(与 DeckEvaluator 同一套评分)
其他委托 CardRewardScorer

篝火 smith 实际由 CardSelectCmdAiCombatCardSelector 选牌(非 UI 点击路径时也走同一评分)。ScoreUpgradeCandidate = ScoreInDeck + 机制协同 + 变形/脆弱加分 − strike/defend/基础牌惩罚

ShopScorer

约束:购物后至少保留 MinGoldAfterShopping = 25 金币。

删牌分(边际对比,非牌数门槛):

removeScore = RemovalUplift - cost/4 - OpportunityCost + 金币充裕加成
  • RemovalUplift < 11 → 不删
  • OpportunityCost:Act3 或 gold 紧张时额外扣分(留金给后续商店)
  • 日志示例:Remove [Strike] uplift=28 strikes+2 burnDebt=5 score=24 vs buy=relic(22)

购买分

  • card → ScoreCardOffer − cost/8
  • relic → ScoreRelicOffer − cost/12
  • potion → PotionInventoryScorer.ValueOffer − cost/25(瓶满且不值得腾槽 → 0)

决策:若 removeScore > 0removeScore ≥ bestPurchaseScore → 删牌;否则买最高分商品;都不值得 → LeaveShop

购买 TargetIndex 与快照一致:仅计 card/relic/potion 顺序,不含 removal slot。

RestScorer

  1. restProceedReady无 rest 选项 → 立即 Proceed 离开。
  2. heal 已消耗healIdx < 0)→ 仅 HP≥75% 且有升级目标时 smith,否则 Proceed(避免 heal 后 poll 误触 SMITH)。
  3. 否则按 HP、路径与选项 id:
    • HP <55%,或 HP <70% 且 planner 下一步为 Elite → heal
    • Codex rest prior(HP 45–85%);若 prior 为 SMITH 且 HP<75% 且下一步 Elite → 跳过 prior
    • HP≥75% 且无 Elite ahead → smith(有升级目标)
    • HP <75% → heal
    • 否则 smith 或 Proceed

TargetIndex 为休息站按钮在 UI 中的绝对索引(与快照 restOptions[].index 对齐)。

RelicScorer

score = RarityScore + Σ(plan.GetWeight(tag) × 3) + CodexRelicBonus(context)

已拥有同名/id → −100。context 来自快照 relicChoiceContext(EventRoom → event,Boss/精英 → combat_reward),同一遗物在 event/combat 下 Codex bonus 可不同。

MapPathPlanner(Act 全程路径 + 画线)

替代原单步贪心 MapScorer:对 RunState.MapBoss 后向 DP(DAG 拓扑序 O(V+E)),选「当前 → Boss 总分最高」路径上的下一步。

文献依据:STS 地图为 DAG,MIT 15.053 最长路径 DP;Bazzaz & Cooper (FDG 2025) 路径枚举;Miles Oram macro 三要素 flexibility/safety/resource。

算法

bestToBoss(p) = nodeScore(p, ctx) + max_{c ∈ children} ( edgeBonus(p,c) + bestToBoss(c) )
  • ctx = MapRouteContext(HP、gold、DeckPlan、DeckMetrics、WantsShopRemoval)
  • 决策:从当前位置选 argmax bestToBoss(child);日志 path=Rest→Shop→M→…

MapNodeWeightScorer 节点分(动态)

类型逻辑
RestSite低 HP 高;成型牌组低
ShopRemovalUplift≥11 + gold → 高;StarterBloat 高 → 高;StrikeSurplus≥2 +10;CardsNeedingBurn≥4 +14
Elite高 HP + 高 MeanValue → 高;低 HP / A7+ → 负;Act1 floor 6–9 且 HP<75% → −15
Monsterbaseline;缺金略升
Treasure+20
UnknownMonster×0.7 + Event×0.3 期望
Ancient同 Unknown

邻接边加成:Rest→Elite(低 HP −10);Shop→*(需删牌 +6);Elite→Elite(低 HP −12);Rest→Rest −5;Treasure→Elite +4;pathRisk 高时 Rest→Elite、Elite→Elite 额外扣分。

连战续航(MapSurvivalIndex + PathSurvivalRisk

  • 自 Boss 行反向 DP:每格 CombatsToRest(到下 Rest 的预期战斗数)、ElitesToRest
  • 战斗权重:M/Elite=1,Unknown=0.7,Ancient=0.5
  • PathRisk = combats×10×blockFactor×drawFactor×hpFactor + elites×12×hpFactor(block/draw deficit 来自 DeckMetrics)
  • DP 选边时对 child 叠加 PathRiskNodeAdjust:Rest +risk;Elite −risk;Monster/Shop/Unknown 高压时略降
  • 日志:risk=N fightsToRest=M blockDef=D;Rest 在 CombatsToRest≥3 && BlockDeficit≥1 时 heal 阈值 55%→65%

MapScorer:委托 MapPathPlanner;无 run 数据时 greedy fallback。

地图画线(仅 AutoPlayEnabled + loop 运行中):

  • 打开地图:MapPathPlanner.Plan + MapPathOverlay 将规划边染为金色
  • 关闭地图:恢复 path dot 原色;MapPathPlanner.ClearCache

源码:MapPathPlanner.csMapNodeWeightScorer.csMapPathOverlay.cs

EventChoiceScorer

  • 识别 Neow(eventId 或 option textKey 含 NEOW)。
  • 快照 eventOptions[]optionKeyEventOptionInfer:textKey/modelId/中文标题)。
  • 评分:keyword baseline + GetEventOptionBonus(无 event prior 时 fallback GetRelicBonus)+ DeckSynergyEvaluator 机制分(遗物/卡选项)。
  • 当 event prior n≥20 时 Codex 权重 ×1.5(codex_primary 模式,keyword 降为 baseline 10)。
  • Reason 日志:Neow pick [title] score=N key=HEFTY_TABLET codex=+4 synergy=+12 codex_primary
  • 普通事件同样接 GetEventOptionBonus;无 prior 时保留关键词兜底。

战斗决策

架构关系图(主路径 vs 快路径)

主战斗决策只信 beam + 贪心线评估CanSkipBlockForKill / CombatScorer 不参与起手选择。

贪心攻击(本回合有伤害时)OrderEnemiesForGreedyAttacks 先打 EffectiveIncoming>0 的敌人;incomingSlack=0(不为集火容忍本回合挨打)。攻击清掉威胁后 SimulateGreedyBlock 不跑 → 线评估自然体现「能杀就不防」。

快路径启发式(不决定 beam 起手,仅辅助排序 / fallback / 药水):

是否决定起手核心类型
Beam + EvaluateLineCombatPlanner, CombatBeamSearch, CombatSetupEvaluator
QuickScore否(beam 展开排序 + 剪枝)CombatActionHeuristic
IAiMoveModifier否(加分修正)SimMoveScoring → beam / QuickScore
CanSkipBlockForKillBlockDefensePolicy → NeedsBlock / ScoreBlock / 药水
CombatScorer否(beam 无结果时 fallback)最小 lethal/block/transform 启发式

顺序

DecideCombat:
  1. PotionScorer.TryEmergencyPotion  → fatal heal / block(非 sim 紧急药)
  2. CombatSearch.PickBestMove        → CombatPlanner beam(主决策)
  3. PotionScorer.TryFallbackPotion   → 非 sim 药;基准分 = QuickScore 最高
  4. CombatScorer.PickBestCombatMove  → 最小 fallback(beam 无路径时)
  5. EndTurn

PotionScorer(战斗:全瓶打分,取最高 ≥25)

启动时 PotionMechanicIndex 索引官方 PotionModel(category / usage / rarity);PotionTierCatalog 提供 retain 分(potion-tiers.json)。

potions[] 每格按 category + IntentCalculator + DeckPlan 打分,日志 potion candidates [ID:+score] …TargetIndexslot(非枚举序)。

Category典型加分局面
Heal / Block低 HP、fatal、NeedsBlock
DamageSingle / AoE斩杀差伤害、多敌
Energy能量不够打出关键牌且接近 lethal
Buff / Debuff / RandomDeckPlan 权重 + 局面
浪费惩罚HP 高、非精英、高 retain 药(如 SHAPED_ROCK)

PotionInventoryScorer(宏观腰带)

场景行为
奖励屏瓶满ShouldMakeRoom → 先 DiscardPotion 最弱 slot 再收;不值替换则 DismissRewards;UI 弹选瓶时 PotionRewardHelper 点 holder
篝火每访仅一次:Heal/Smith 禁用后只 ProceedProceed() 优先点篝火 Proceed,不走战后等待
商店瓶满ShopScorerDiscardPotion 再买;购买分 = ValueOffer − cost/25
腾槽条件incomingValue > lowestHeld + 8

DiscardPotion / UsePotion 均使用 slot indexplayer.GetPotionAtSlotIndex)。

BlockThreatEvaluator

集中「本回合受伤风险」判定,供 IntentCalculatorCombatScorerCombatSearch 共用:

方法语义
ShouldScoreBlockNeedsBlocknetIncoming ≥ 阈值(floor≤15 用 6,否则 8)
ShouldSuppressTransformIsFatalIfUnblocked NeedsBlock(非安全斩杀豁免);轻度 ShouldScoreBlock alone 不压制
ThreatDiscountScale威胁下变形 follow-up 折扣(0.4–1.0,随 BlockUrgency
HasAffordableHandTransform手牌有能量可出的 TransformsHandAttacks 且存在可变形攻击
IsStarterDefendDEFEND_* 或 STARTER 稀有度挡牌

CombatSetupEvaluator

从 snapshot 实时比较 setup vs 立刻攻击(无固定阶段门):

方法语义
ComputeVulnerableDeferValue挂易伤+过回合价值:后续攻击×1.5 估算、incoming/urgency、残血可斩则减分
ComputeVulnerableDeferOpportunityCost攻击牌机会成本:max(0, deferValue − attackDamage) / 2

BlockDefensePolicy

集中挡牌 vs 击杀权衡(snapshot + sim 共用):

API语义
NeedsBlockfatal / early floor / 阈值 / 低 HP; CanSkipBlockForKill 为 true 时豁免
CanSkipBlockForKill快路径:CanSecureKillThisTurnnet≤0不参与 EvaluateLine / beam 叶评分
CanFullyBlock / ShouldPrioritizeBlock手牌可负担格挡是否覆盖 net
FullBlockValueEvaluator / ConsiderLeaf 挡满潜力分

IntentCalculator

TotalIncomingDamage = Σ 存活敌人 intentDamage
NetDamageAfterBlock = max(0, incoming − playerBlock)
NeedsBlock → BlockDefensePolicy(无 CanLethal blanket 豁免)
BlockUrgency 0–100 驱动攻击惩罚与 EndTurn 惩罚

CombatScorer(fallback 最小启发式)

CombatPlanner beam 无可用路径时使用;不参与主路径起手。无 IAiMoveModifier、无完整机制分。

情况加分
Attack 可斩杀lethal +80;CanSkipBlockForKill 时再 secure +20
Attack 且 NeedsBlock 且不能 secure killunsafe 惩罚
Block 且 NeedsBlock/fatalblock + 30 + 有效格挡×2
Block 无威胁block-waste −10
Transform 技能+50
Junk−200
EndTurn fatalint.MinValue

完整单步评分(挡牌权重、易伤 defer、机制分等)已迁移至 beam + CombatSetupEvaluatorCombatActionHeuristic.QuickScore

机制驱动加分(主路径在 beam / QuickScore,非 CombatScorer):

机制来源效果
TransformsHandAttacks原始力量等EstimateTurnDamageDelta;QuickScore / beam 剪枝
AppliesVulnerableDynamicVar 探测(痛击等)CombatSetupEvaluator defer;QuickScore ScoreVulnerableSetup
AppliesWeakDynamicVar类似,权重略低
Setup Skill上述机制牌不再吃「非挡牌 Skill −40」惩罚

伤害读取:CombatCardStats.ResolveDamage — 快照 damage 缺失时回退 CardMechanicIndex.Damage(修复巨石 0 伤评分)。

战斗日志AiCombatVerboseLog=true,默认开):每次出牌记录 top pick + 最多 4 个备选,含分项 [attack:+31, mechanic:+48, …]。见 CombatDecisionLog

Mod 可通过 IAiMoveModifier.ModifyScore 调整 beam / QuickScore 中的 move 分数(经 SimMoveScoring;调试日志可见 mod:+N)。

Enemy Intelligence Layer

启动时镜像卡牌 CardMechanicIndexMonsterMechanicIndexModelDb.AllEncounters 枚举怪物,经 OfficialMonsterProbe + MonsterMoveScanner 探测:

探测源输出
类型图(IllusionPower / MinionPower / AdaptablePowerEnemyMechanicFlags
状态机 Intent 扫描(SetUpForCombat 后遍历 MoveState每怪 moveId → IntentType[]
Encounter 共现 + monster-probe-overrides.jsonspawnedMonsterIds[]

运行时快照 enrich(GameSnapshot.CaptureEnemies):

  • mechanicFlagsintentTags[]nonDamageThreat
  • intentSteps[] 扩展为 { moveId, intentDamage, intentTypes[], nonDamageThreat, isUncertain }
  • CombatEnemyGraph 观测召唤并写入 summonerIndex

验证:MCP dev_dump_monster_mechanicstools/monster-probe-dump/dump-monster-mechanics.ps1 导出 JSON。

调优边界:当前数据支持权重网格(EnemyThreatWeights)+ tools/ai-bench 胜率;不支持端到端 ML(缺目标选择标签)。场景回归见 tools/ai-bench/scenarios.json

EnemyTargetPriority / MinionEngagementPolicy

isMinionIsSecondaryEnemy + power)标记爪牙。MinionEngagementPolicymechanicFlags 动态偏置(替代固定 ±30):

场景偏置
标准爪牙 + 主人存活主人 +35,爪牙 −30
HasIllusionRevive(Parafright 等)爪牙 −60,模拟层不 wipe
PeerSummon(TwoTailedRat)正常 HP/伤害排序
高 debuff 威胁爪牙−10 ~ +15(按 nonDamageThreat

ThreatModel.EffectiveIncoming = intentDamage(挡牌/净伤仅计攻击)。nonDamageThreat(塞牌/debuff)与 NextTurnIncoming(下一步攻击)分开:ScaledNonDamagePressure 在「本回合无攻击、下回合大伤害」时降权;ScaledNextTurnPressure 在安全回合给满下回合攻击权重以推动 kill-before-hit。幻象爪牙(HasIllusionRevive)在主体存活时不进入 beam 攻击枚举。OrderByPriorityLethalChecker 使用上述策略。

LethalChecker

对每个存活敌人(经 OrderByPriority 排序):若 EstimateMaxDamage(手牌, 能量)hp + block,判定可斩杀并返回 targetIndex。EstimateMaxDamage 按伤害降序贪心消耗能量(含 id 猜测伤害)。

Combat Simulation Layer

战斗层采用 Immutable State → Legal Actions → Apply → Evaluate → Bounded Search 前向模拟。动机是修正常见 STS 战斗 bot 的静态威胁求和、AOE 误枚举、召唤目标错误等问题;模块位于 src/AI/Combat/Simulation/CombatSearch.PickBestMove 仅委托 CombatPlanner

架构思路:为何从权重走向 Deck EV

早期做法把 nonDamageThreat(塞牌 / debuff / 召唤)压成单一 magic number(EnemyThreatWeights),与 intentDamage 相加后触发挡牌。这在官方 STS2 语义下是错的:

威胁类型官方 Intent实际机制典型代表
HP 伤害AttackIntentDamageCmdFOGMOG SWIPE_MOVE 8–9
塞状态牌StatusIntent(count)CardPileCmd.AddToCombatAndPreview<T>EyeWithTeeth → 3× DAZED → Discard
牌面 afflictionCardDebuffIntentPowerCmd.Apply<SmoggyPower>LivingFog、VineShambler
数值 debuffDebuffIntentShrinkPowerShrinkerBeetle(不塞牌

Intent 只有 UI 元数据;牌 ID、目标堆、power 类型都在 move handler 的 imperative C# 里。因此 L3 的核心思路是:

  1. 解析 move → 效果MoveEffectIndex = 运行时 intent 扫描 + monster-move-effects.json 静态提取)
  2. 在战斗堆上推进CombatTurnResolverEndTurn 时弃牌、敌人行动、塞牌、召唤、抽 5)
  3. 同尺比较路径效用ThreatEconomy:HP 伤害 vs 污染 EV vs power debuff,而非混在一个 incoming 里)

塞牌的真实代价是「未来抽到废牌、少打伤害」,不是「本回合挨打」;应在 DeckPollutionEvaluator 里用堆组成估算,并与 KillBeforeHitBonus(下回合攻击)放在同一把尺子上比。

决策流:beam 展开玩家出牌 → 叶节点 EndTurn 时先跑 CombatTurnResolver(模拟敌人回合与抽牌)→ 在推进后状态上由 CombatEvaluator + ThreatEconomy 评分。这样 FOGMOG 召唤回合能「看到」:若不过牌 rush 雾菇,下回合会挨 9 点且每回合多 3 张 Dazed,而非把 debuff 当成 8 点 incoming 去挡。

设计原则与常见局限

常见 STS 战斗 bot 往往在以下环节失真;DevMode 模拟层针对这些点做了显式建模:

常见局限典型表现DevMode 做法
威胁静态求和incoming 不因击杀重算ThreatModel:只计存活且 intentDamage>0;模拟击杀后自动下降
AOE 当单体枚举AllEnemy 按每个 target 重复评分LegalActionGenerator 单动作 + CombatSimulator 群伤一次转移
召唤语义缺失打爪牙不打主人MonsterMechanicIndex + MinionEngagementPolicy + 主怪死后 ThreatModel.OnPrimaryEnemyKilled(跳过幻象爪牙)
评分与局面混用一套启发式既评牌又评回合末CombatScorer(单步/fallback)与 CombatEvaluator(叶节点局面)分离
浅层搜索depth 1–2 或全枚举不剪枝CombatPlanner 迭代加深 beam + CombatActionHeuristic 走法排序
仅本回合 intent不看下回合高伤快照 intentSteps[] + CombatEvaluator 下回合权重
非伤害 intent 忽略debuff/summon 不计威胁MoveEffectIndex + ThreatEconomy + DeckPollutionEvaluator
塞牌 vs 攻击不同尺挡牌应对 debuff/塞牌EffectiveIncoming 仅攻击;污染 EV 单独计入 POLL=
硬编码 / 不透明card id 列表、缺什么靠调权重掩盖monster-move-effects.json(官方 handler 提取)+ MonsterMechanicProfile.effects[]

已知局限(不假装完美):非完整协程/动画时序;无 rngShuffle seed 时回洗用牌堆哈希确定性 RNG;Confused 抽牌费用为 EV 近似(非逐张 CombatEnergyCosts 回放);遗物仅白名单规则(relic-combat-effects.jsonsimulatable);复杂遗物(PenNib、VelvetChoker 等 needsManual)仍忽略;偷牌不模拟死亡还牌。

类型职责
CombatState不可变战斗状态(HP/block/能量、手牌、draw/discard/exhaust、modifiers、relicIds[]、敌人 intent/moveId)
MoveEffectIndex合并运行时 intent 与 monster-move-effects.json 静态 handler 效果
RelicCombatRules快照 relics[] + relic-combat-effects.json:抽牌加成、手牌保留、空手无歇抽牌、开战 block、AppliesPower
CombatTurnResolverEndTurn 推进:弃牌(Runic Pyramid 保留手牌)→敌人行动→按遗物规则抽牌
DeckPollutionEvaluator堆内废牌数、ProjectedPollutionCostExpectedPlayableDamage
ThreatEconomyHP + 污染 + power debuff 同尺比较;KillBeforeHitBonus
LegalActionGenerator枚举合法动作;幻象爪牙在主体存活时不可攻击
CombatSimulatorApply(action) 出牌模拟(弃牌/消耗/抽牌/控顶);EndTurn 委托 CombatTurnResolver
DrawPlanner顶牌 PeekTopWillReshuffle、抽牌堆期望伤害/格挡
CombatPileManipulatorHeadbutt 等:与 AiCombatCardSelector 同尺 CombatDiscardPickScorer 选弃牌堆顶牌
PlayerCombatModifierRegistry快照 / move effect → Strength/Dexterity + Shrink/Smog/Tangle/Bind/Weak/Frail/Confused
CombatDamageCalc玩家出牌伤害/格挡:flat(力敏)+ debuff 乘数 + 易伤
StealEffectSimulatorThievingHopper / Swipe:启发式从 draw/discard 移除最高价值牌
PostTurnSimulatorEndTurn 前完整下回合 mini-beam(职业级节奏)
CardPileEffectResolver官方 DynamicVar → draw/discard/scry 数量
PileRhythmEvaluator顶牌视野 + 回洗风险的牌堆节奏分
ThreatModelIncoming = 仅 intentDamageScaledNonDamagePressureThreatEconomy
AoeDamageEstimator群伤斩杀判定、FindBestAoeLethalAction
CombatEvaluator叶节点效用含 ThreatEconomyExpectedPlayableDamage
CombatPlannerbeam search;EndTurn 叶节点在 resolver 后状态评分

CombatPlanner 参数(按可出牌数自适应,迭代加深 3→6→…→MaxDepth):

参数典型值
时间预算560–730 ms
最大深度10–16(玩家出牌步,不含敌人回合)
Beam 宽度28–52
每节点展开上限20–56(GenerateOrdered 启发式截断)
下回合模拟PostTurnSimulator 独立 mini-beam(打满下回合能量)

CombatActionHeuristic 为 beam 排序:未挡满时剪枝非击杀攻击;挡牌 > 易伤 setup > 击杀攻击。叶节点用 survival-first EvaluateTerminal + ConsiderLeaf 满挡奖励区分「挡满结束」与「换血快攻」。

无 lethal 快捷路径:所有 combat poll 走 beam;日志仅 [beam d=…] / [beam end …],不出现 [lethal] / [aoe-lethal] / [lethal-transform]

L4 打牌节奏(pile + shuffle + beam):

  • 抽牌堆非空时:DrawPlanner.PeekTop 精确预知下 N 张;CombatSimulator 出牌更新 discard/exhaust。
  • 抽牌堆空且弃牌堆非空:CombatPileSimulator.ReshuffleIfNeeded 使用官方 StableShuffle(Rng.Shuffle),beam 分支各自携带 ShuffleRngCounter
  • 日志:NEXT= 顶牌、RESHUF=1 将回洗、POST_PLAY= / POST_BLK= 过回合后新手牌期望、OUTLOOK= 牌堆节奏价值。

L5 职业级模拟(pro pile + full next-turn beam):

  • PostTurnSimulatorEndTurn 评分前对整段下回合做 mini-beam(打满能量或无可出牌),而非仅 2 张。
  • CardPileEffectResolver:从官方 DynamicVars 解析 draw/discard/scry 数量;CombatSimulator 模拟弃牌、窥视沉底。
  • PileRhythmEvaluator:10 张顶牌视野 + 回洗污染惩罚,接入 EvaluateMidTurn / EvaluateTerminal
  • Beam 参数(职业级):深度 10–16、宽度 24–48、预算 480–650 ms;迭代加深从 depth 5 起。

故意不模拟(Phase D 后仍保留):通用 Hook 链(事件房/商店/奖励副作用)、多玩家时序、Scry UI 等价控牌、Innate/PerfectFit 回合初排序、偷牌死亡还牌、计数/状态型遗物(PenNib、Shuriken 等)。已模拟:有序 move 效果、力敏/debuff、Swipe 偷牌、Confused EV;遗物白名单(抽牌/保留/空手无歇/开战 block/开战 power)。

效果数据

  • tools/monster-move-effect-dump/extract-move-effects.pymonster-move-effects.json(怪物 move handler)
  • tools/relic-combat-effect-dump/extract-relic-combat-effects.pyrelic-combat-effects.json(扫描 RelicModel 覆写的战斗 Hook + 可解析的 DynamicVar / PowerCmd.Apply / CardPileCmd.Draw);simulatable: true 的条目由 RelicCombatRules 接入 beam。

NeedsBlock 与多攻击者CanEliminateIncomingThreats 不再要求单一威胁;可 AOE/逐个斩杀全部 intentDamage>0 敌人,或模拟击杀最高 intent 后 Incoming 归零且 net ≤ SafeLethalNetMax

快照 enrich:GameSnapshot.CaptureEnemies 调用 MonsterIntentReader.CaptureIntentSteps,每敌最多 3 步 { moveId, intentDamage, intentTypes[], nonDamageThreat, isUncertain }

战斗日志AiCombatVerboseLog):CombatDecisionLog 输出 IN= / ND= / NXT= / JUNK= / POLL= / PLAY= / NEXT= / RESHUF= / POST_PLAY=tgt= bias= flags= 便于 bench 调参。

CombatScorer 保留为 最小 fallback(beam 无结果时)。Mod IAiMoveModifierSimMoveScoring 作用于 beam 排序与 QuickScore,不再挂 CombatScorer。


AI HUD(游戏内托管叠加层)

单机开启 AI HostAiHudEnabled=true 时,AiHudOverlayUINGlobalUi 左上角以纯文字堆叠显示:

内容
标题AI hosting
阶段当前 GamePhase 简写
Plan阶段策略摘要(AiHudModel.BuildStrategyLine
Next最近一次 GameActionAiHudStateGameLoop 发布)
可选参数战斗:HP/BLK/IN/E;非战斗:F/G/HPAiHudShowParams
可选分项战斗 Reason 中的 [block:+N, mechanic:+N, …]AiHudShowScoreTerms

显示条件AutoPlayEnabled && AiPlayModule.IsRunning && !多人联机

与侧边栏 AI Terminal 分工:HUD 只展示当前一步与少量参数;完整决策历史仍在 AiDecisionLog / session log。设置项在 AI Host 面板 Controls 区。


GameLoop poll 去重

AiPlayModule 每 500ms 轮询当前 phase;GameLoop 在决策前:

  • CombatisPlayPhaseActive=false → 跳过(等敌方回合/动画);Sts2StateProviderCombatManager.IsInProgress 时仍返回 Combat(避免敌方回合误判为 UnknownAdvanceOverlay 刷屏)
  • EndTurn 已提交_endTurnPending)→ 跳过,直到 phase 变化或 play phase 结束
  • 相同 fingerprint(phase+action+target)2s 内 → 跳过(避免 EndTurn 刷屏、Rest 双动作)

出牌同步由 Sts2ActionExecutorTryManualPlay + WaitForManualPlayAsync 阻塞完成,不在 loop 层做 fingerprint / 能量账本等待。

快照手牌 cost 使用 EnergyCost.GetWithModifiers(All)(实战费用),不再只用 Canonical

Run 结束时 ResetDedupeState() 清空状态。


执行层要点(Sts2ActionExecutor)

动作行为
PlayCard单机:TryManualPlay → action queue → SpendResources(扣能量 + UI);WaitForManualPlayAsync 等到牌离开手牌。勿用 CardCmd.AutoPlay(遗物/效果自动打出路径,不扣玩家能量)。Pseudo Co-op:PlayCardAction 入队
PickCardReward奖励屏点卡;NDeckCardSelectScreen 点选后点 Proceed
SkipCardRewardSkip / Back
SelectRestSiteOption绝对按钮 index 点击;disabled 则失败
Proceedoverlay 或 room 内 ProceedButton
RemoveCardAtShop购买 removal slot → 进入 DeckSelect(context=remove)
PurchaseShopItem按非 removal 顺序的 affordable slot 购买

决策日志中的 Reason= 字段与上述 Scorer 字符串一一对应,调参时优先看日志。


SimpleStrategy 对比

设置 AutoPlayStrategy: Simple 启用旧版启发式:

方面SimpleStrong
地图第一个节点MapScorer 多因素
卡牌早期拿第一张 / 后期 skipDeckPlan + 阈值 skip
商店买第一张 / 不删牌ShopScorer 删牌 vs 购买
休息HP<60% rest 否则 upgradeRestScorer + Proceed + smith 目标
战斗低血 block → 高费 attackPotion + CombatSearch + 意图/block

Mod 扩展点(摘要)

接口用途
IDecisionMaker完全接管决策
IDeckPlanContributor调整 DeckPlan.Builder
ICardTagProvider扩展 CardCatalog tag
IAiMoveModifier战斗 move 加分(SimMoveScoring → beam / QuickScore)
IAiSnapshotContributor写入 extensions.*

注册入口:CompanionBridge.Register*(见 README)。


调参与回归

  • 固定 seed 列表:tools/ai-bench/seeds.json
  • 跑完一局后:powershell -File tools/ai-bench/run-bench.ps1
  • 目标:各角色 A10 胜率约 40–50%(见 bench README)

常用调参旋钮:

文件旋钮
CardRewardScorerMinPickScore;skip 委托 SkipOpportunityCost
ShopScorerMinGoldAfterShoppingDeckEvaluator.MinRemovalUplift
DeckEvaluator / DeckCardScoring冗余惩罚系数、FutureThinBonus
RestScorerHP 比例阈值
MapNodeWeightScorer / MapPathPlanner节点分、边加成
CombatSearchTimeBudgetMsMaxDepth
DeckPlanInferer / *Pack.cstag 权重、ThinPreference

源码索引

路径内容
src/AI/AutoPlay/Strategies/StrongStrategy.cs阶段分发
src/AI/Planning/DeckPlanInferer.cs规划推断
src/AI/Planning/DeckEvaluator.cs牌组质量、RemovalUplift
src/AI/Planning/DeckCardScoring.cs牌组内单卡评分
src/AI/Planning/MapPathPlanner.csAct 路径 DP
src/AI/Planning/MapNodeWeightScorer.cs地图节点/边权重
src/Map/MapPathOverlay.csAutoPlay 路径高亮
src/AI/AutoPlay/Scoring/*.cs宏观评分
src/AI/AutoPlay/Scoring/CombatScorer.cs战斗 fallback 最小评分
src/AI/Combat/Simulation/SimMoveScoring.csIAiMoveModifier → sim/beam 桥接
src/AI/Combat/CombatSearch.cs浅层搜索
src/AI/Combat/LethalChecker.cs斩杀
src/AI/Combat/IntentCalculator.cs意图伤害
src/AI/Sts2/Snapshots/快照捕获
src/AI/Sts2/Helpers/Sts2CombatPlayHelper.cs等待手动出牌 action queue 完成
src/AI/Sts2/Sts2ActionExecutor.csUI 执行