You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
AdLike2048/docs/需求-AI决策.md

857 lines
52 KiB

3 weeks ago
# 需求:匹配与 AI 决策
> 状态:**正式需求(匹配 + A0 背包刷新已接线;决策细则继续完善)**
> 日期:2026-09-01(实现入口更新:2026-09-03)
> 来源:策划脑图「西游对战 ai」+ 架构讨论确认
> 关联:[需求-新项目概述.md](./需求-新项目概述.md)、[需求-背包刷新.md](./需求-背包刷新.md)、[方案-出怪逻辑实现.md](./方案-出怪逻辑实现.md)、[开发规范.md](./开发规范.md)
## 1. 目标与范围
| 项 | 说明 |
|----|------|
| 目标 | 玩家 vs AI(不做联机):开战前匹配对手档案;局内 AI 按规则刷新/拖动/落子/用道具,与玩家共用同一套战斗与合成规则 |
| 形态 | 同图双阵营(`CampAreaID` + `CampAreaPolicy=pvp`);UI 上下仅为表现,逻辑以 `playerId` / 阵营隔离 |
| 本机 | `playerId=0`:玩家操作(既有背包 UI,「召唤」= 普通刷新) |
| 对手 | `playerId=1`:AI 操作者(无头背包 + 决策,无第二套完整拖拽 UI 亦可) |
| 本期不做 | 真联机 PVP、搜索/学习型 AI、复活半图镜像 |
| **待玩家侧先实现再同步 AI** | §8 主动技能、§8.2 广告、§9 装备掉落、**桃子击杀来源(玩家侧)**、**胜负条件**——规则先定稿,**依赖项落地后再接** |
### 1.0 实现入口(先做这个)
> **局内 AI 从「背包刷新」切入**,对齐玩家底栏「召唤」:扣桃 → 字池抽满 5 槽。匹配 / 拖拽决策 / 武将武器等后置。
| 顺序 | 切片 | 范围 | 验收一眼看 |
|------|------|------|------------|
| **A0** | **AI 背包刷新** | §5.1~§5.4:`AiHeadlessBagHost` + 对手 `rankId` 字池 + AI 槽扣桃 `tryNormalRefresh`;首轮自动刷一次 | AI 有独立 5 槽;扣 Const 4/5;与玩家池 remaining 隔离 |
| **A1** | 匹配档案 | §4 Const 11/12/14 + §4.4 buff 挂载(不生效) | 产出 `AiMatchProfile`,开战传给 A0 建池 |
| **A2** | 工程对称化 | §2 / §13.3.9:camp 放置、`DeploySync(playerId=1)`、AI 槽金 | AI 刷新后的字能落 AI camp(为 A3 铺路) |
| **A3** | 局内决策 | §5.5 tick + §6~§7 + §5.6 武将武器 | 1 秒决策链;合成后挂武器 |
| **B~D** | 见 §1.2 | 击杀加桃、胜负细项、主动技/广告/掉落 | 依赖玩家侧 |
**A0 不依赖完整匹配 UI**:对手 `rankId` 可暂用 `RankDataManager.getPlayerRankId()` 或已有 `pendingOpponentRankId`;A1 落地后再换 `AiMatchProfile.opponentRankId`。
### 1.2 实现分期(总览)
| 阶段 | 范围 | 说明 |
|------|------|------|
| **A** | **先 A0 背包刷新** → A1 匹配 → A2 对称化 → A3 §5.5~§7 / §5.6 | 不依赖胜负结算 |
| **B** | 击杀加桃(玩家+AI)、胜负条件(§10) | 玩家侧先通 |
| **C** | 连胜/连败存档 + Const 15~18 匹配修正(§4.3/§4.5) | **已接线**:`MatchStreakStats` + 胜负 UI 结算;匹配读 streak |
| **D** | §8 主动技能、§8.2 广告、§9 装备掉落、buff 效果 | 各玩家能力就绪后 AI 同步 |
### 1.1 与既有文档关系
| 文档 | 关系 |
|------|------|
| [需求-背包刷新.md](./需求-背包刷新.md) | 玩家「召唤」刷新算法已定;**A0 复用加权抽字**,AI 传入 **对手段位** `rankId` 建池;**不含**玩家连败组合字保底 |
| [需求-新项目概述.md](./需求-新项目概述.md) | 概述中「匹配与 AI」条目由本文承接 |
| 方案整理中的 `AiOpponentController` | 实现侧建议类名/模块指向,本文为需求契约 |
---
## 2. 架构约束(实现须遵守)
```text
匹配 MatchProfile(段位、buff 数、策略种子)
↓
共享 MX 战斗内核(双 PlayerSlot / 双 port / CampAreaPolicy)
↓
┌───────────────────┬───────────────────┐
│ PlayerController │ AiOpponentController │
│ 背包 UI + 拖拽 │ 无头 Bag + 决策 tick │
└─────────┬─────────┴─────────┬─────────┘
└──── Merge / deploy(playerId) ────┘
```
| 规则 | 要求 |
|------|------|
| 规则对称 | 刷新费用、合成、落子合法性、城墙、经济按 `playerId`;禁止写死仅本机 |
| 输入不对称 | 仅本机绑定拖拽 UI;AI 输出动作落到同一套 Merge/`deploy` |
| 字池 | AI 使用与玩家相同的建池/抽取逻辑,**传入对手段位 `rankId`** |
| 广告 | AI **不播**激励视频;仅按概率判定是否获得广告类效果 |
| 音效 | 仅本机播;AI 侧武将普攻/技能/命中不播(`BattleAudio` 按 `Team.ownerId`) |
| 异常 | 配表缺失、权重全 0、字池耗尽等须 `warn`,禁止静默兜底糊弄 |
**开战前须对称化的耦合点(实现前置,非 AI 算法本身):** 放置 camp 校验、`DeploySync` 按 `playerId`、墙血/胜负/波次金币作用到对应槽、远端/AI 无头 `bagSession`。
---
## 3. 配表契约
### 3.1 Const
路径:[`assets/bundles/first/config/Const.json`](../assets/bundles/first/config/Const.json)(已与策划同步,以下摘自现表)。
| id | 含义 | `para1`(现网) | 规则 |
|----|------|-----------------|------|
| **11** | 匹配星级偏移权重:少 5 星 → 大于 4 星 | `10,10,10,10,10,10,10,10,10,10` | **10** 档,对应相对玩家总星偏移 **`-5,-4,-3,-2,-1,0,+1,+2,+3,+4`** |
| **12** | 对方 **主动** 道具数量权重(`-1,0,+1`) | `20,30,10` | 结果 `clamp(…, 0, Const**19**)` |
| **13** | 攻击力等级系数(既有,与匹配无关) | `1,1.5,2.1,2.73,3.276` | — |
| **14** | 对方 **被动** 道具数量权重(`-2,-1,0,+1`) | `20,30,20,10` | 结果 `clamp(…, 0, Const**20**)` |
| **15** | 玩家连胜 → 主动「+1」档权重加成(加强 AI) | `5` | 连胜 **≥3** 起;每多一场再加一份;玩家主动已达上限(Const19)时,加成改加到 **0** 档 |
| **16** | 玩家连胜 → 被动「+1」档权重加成(加强 AI) | `5` | 同上;被动已达上限(Const20)时加成改加到 **0** 档 |
| **17** | 玩家连败 → 主动「-1」档权重加成(削弱 AI) | `5` | 连败 **≥3** 起;每多一场再加一份 |
| **18** | 玩家连败 → 被动「-2」档权重加成(削弱 AI) | `5` | 连败 **≥3** 起;每多一场再加一份 |
| **19** | **最大主动技能数量** | `2` | 玩家/AI 主动槽与 §4.2 clamp 上界 |
| **20** | **最大被动技能数量** | `6` | 玩家/AI 被动槽与 §4.2 clamp 上界 |
| 4 / 5 | 战斗刷新桃子消耗(初值 / 递增) | `10` / `2` | AI 普通刷新与玩家同一套费用 |
边界:星偏移后落在非法星、或某档不可达时,该档权重视为 **0**;若总权重 ≤ 0,须 `warn` 并 **中止匹配**(见 §13.3.8 说明;正常配表一般不会触发)。
### 3.2 Rank(对手行)
| 字段 | AI 用途 |
|------|---------|
| `charactersweight` | **AI 字池选档**:与玩家相同算法,读 **对手段位** 行 |
| `aiweapon` | **合成武将首次装备**:按品质权重随机 `Weapon.id`(§5.6);与击杀掉落无关 |
| `aiusead` | AI「广告类效果」触发概率(万分比) |
| `aiusechanzi` | AI **铲子**使用类型权重:`[类型1, 类型2, 类型3]`(见 §6.2) |
| `aiusetext` | AI **普通字**使用类型权重:`[类型1, 类型2]`(见 §6.3) |
| `weapondrop` / `weaponweight` | 击杀掉落(见 §9,非拖拽主循环) |
| `diffcult` | 局内难度加值(若战斗侧已消费则沿用) |
### 3.3 其它表
| 表 | 用途 |
|----|------|
| `Text` | 字池候选与 `numberN` 上限;`type∈{1,2,4}` 入池;**`quality`:0白 1绿 2蓝 3紫 4橙**(§6.4 取舍) |
| `Hero` | 合成、等级上限、武将判定 |
| `Battlebuff` | `type=1` 主动 / `type=2` 被动;按 `weight` 加权抽取 id(§4.4);本期仅挂载不生效 |
| `Level` | 战场、初始桃子、出怪;双营共用关卡配置 |
---
## 4. 匹配(开战前)
### 4.0 总原则(已确认)
对手段位与对手 buff **都不是独立随机**,一律:
```text
AI 值 = 玩家当前值 + 加权随机得到的偏移
```
| 项 | 玩家基准 | 偏移权重表 | 结果 |
|----|----------|------------|------|
| **AI 段位/星** | 玩家当前总星 | Const **11**(相对星偏移 `-5~+4`) | `对手星 = 玩家星 + 偏移` → 映射 `rankId` |
| **AI 主动 buff 数** | 玩家本局主动数量 | Const **12**(`-1,0,+1`);Const **15~18** 修正权重(§4.3) | `clamp(玩家主动数 + 偏移, 0, Const19)` |
| **AI 被动 buff 数** | 玩家本局被动数量 | Const **14**(`-2,-1,0,+1`);Const **15~18**(§4.3) | `clamp(玩家被动数 + 偏移, 0, Const20)` |
**连胜/连败(Const 15~18):** 已实现。字段 `matchWinStreak` / `matchLoseStreak` 跨天不清零;胜负结算(`MatchWinStats.record`)互斥增减;匹配前 `AiMatchService` 按份数修正 Const 12/14 权重。
**设计意图(已确认):** 计数对象是 **玩家** 的连胜/连败,不是 AI。
| 玩家状态 | 对 AI 的效果 | 现表手段 | 实现 |
|----------|--------------|----------|------|
| **连胜** | **加强 AI**(对手更容易抽到更多 buff) | Const 15/16 提高「+1」档权重 | ✅ |
| **连败** | **削弱 AI**(对手更容易抽到更少 buff) | Const 17/18 提高「−1/−2」档权重 | ✅ |
```mermaid
flowchart TD
PStar[玩家总星] --> W11[Const11 十档权重]
W11 --> RollStar[加权随机星偏移]
RollStar --> AiStar["对手星 = 玩家星 + 偏移"]
PAct[玩家主动数] --> W12[Const12 三档权重]
Streak[连胜/连败] --> Mod1518[Const15~18 修正对应档权重]
Mod1518 --> W12
Mod1518 --> W14
W12 --> RollAct[加权随机数量偏移]
PAct --> SumAct["AI主动数 = clamp(玩家+偏移,0,Const19)"]
RollAct --> SumAct
PPas[玩家被动数] --> W14[Const14 四档权重]
W14 --> RollPas[加权随机数量偏移]
PPas --> SumPas["AI被动数 = clamp(玩家+偏移,0,Const20)"]
RollPas --> SumPas
```
### 4.1 对手段位(按星)
1. 取玩家 **总星** `RankDataManager.getPlayerRankStars()`(存档 `GameDoc.rankStars`,详见 §13.3.1)。
2. 读 Rank 表得到 **全局最小星** `minStar`(首档 `level[0]`)、**全局最大星** `maxStar`(末档 `level[1]`,`-1` 表示无上限时取末档 `level[0]` 或表内最大星)。
3. 复制 Const **11** 十档权重;对每一档偏移 `d`:若 `玩家星 + d < minStar` 或 `> maxStar`,该档 **权重视为 0**(不参与随机)。
4. 在剩余权重中加权随机偏移 `d ∈ {−5…+4}`。
5. `opponentStars = clamp(玩家星 + d, minStar, maxStar)`(**不会低于最小星、不会超过最高星**)→ `opponentRankId = resolveTierByStars(opponentStars).id`。
> 实现备注:现网 `pickOpponentRankId` 仍按 **段位 id 三档**读 Const 11,须改为 **总星十档偏移**(§13.3.1)。
### 4.2 对手 buff 数量(AI 带入数量)
1. 取玩家本局带入的主动数、被动数(存档/当日道具栏,与道具商店对齐)。
2. 复制 Const **12** / **14** 各档权重。
3. 按连胜/连败用 Const 15~18 修正权重(见 §4.3)。
4. 分别加权随机数量偏移;
`AI主动数 = clamp(玩家主动数 + 偏移, 0, Const19)`,
`AI被动数 = clamp(玩家被动数 + 偏移, 0, Const20)`。
5. 按 §4.4 从 `Battlebuff` 表加权抽取 **不重复** 的 buff id 列表,写入 `MatchProfile`。
6. 开战:`BattleBuffCarryStore` 按 `playerId=1` 挂载列表;被动效果与玩家对称生效(基地血、召神/召唤费、行军血/速、敌军减速、塔攻速、还魂/神雷/打断/掘宝/击杀桃等)。**主动**列表已挂载;CD 满自动释放见 §8.1。
匹配结果写入本局 `MatchProfile`(对手 `rankId`/星、主动/被动 **数量与 id 列表**、随机种子等),进战斗后只读。
### 4.4 Battlebuff 抽取(已确认)
数量由 §4.2 确定;**具体 id** 从 `Battlebuff` 表按类型与权重抽取。
| 项 | 规则 |
|----|------|
| 表 | [`Battlebuff.json`](../assets/bundles/first/config/Battlebuff.json) |
| 类型 | `type=1` → **主动**;`type=2` → **被动**(`type=3` 局外功能如人参果加体力,**不入局内 AI 池**) |
| 候选 | 对应 `type`、`weight > 0`,且 **`functiontype ∈ [2,27]`** |
| 抽取 | 在候选内按 `weight` **加权随机**,抽满目标个数;**同一局不可重复**(已抽 id 从候选移除) |
| 与 `aiweapon` | **无关联**;buff 池与 `Rank.aiweapon`(装备品质)独立 |
| 效果 | **挂载后被动对称生效**;主动列表供 §8.1 自动释放 |
| 失败 | 候选不足以抽满目标数 → `warn` 并暴露,禁止静默少抽或重复凑数 |
### 4.3 连胜 / 连败如何改权重(已实现)
> 结算入口:`UIBattleSuccess` / `UIBattleFail` → `MatchWinStats.record` → `MatchStreakStats`;匹配:`AiMatchService._applyStreakWeightBonus`。
**只改「权重值」,不改偏移档位集合:** 加权随机 **之前**,把 Const 15~18 加成加到 Const 12/14 对应档,再抽偏移。15~18 **不**修正 Const 11。
计数读 **玩家** 连续 streak(§4.5),非 AI。
| Const | 玩家状态 | 作用 | 效果语义 |
|-------|----------|------|----------|
| **15** | 连胜 | 主动(12)**+1** 档 `+= 份数×para1`;玩家主动已满 Const19 则加到 **0** 档 | 加强 AI 主动数 |
| **16** | 连胜 | 被动(14)**+1** 档同上;被动已满 Const20 则加到 **0** 档 | 加强 AI 被动数 |
| **17** | 连败 | 主动(12)**−1** 档 `+= 份数×para1` | 削弱 AI 主动数 |
| **18** | 连败 | 被动(14)**−2** 档 `+= 份数×para1` | 削弱 AI 被动数 |
- 触发:连续场次 **≥ 3**;`生效份数 = max(0, 连续场次 - 2)`。
- 修正仅本局匹配;**不写回** Const 表。
- Const 11 **不受** 连胜/连败影响。
### 4.5 连胜 / 连败存档(已实现)
| 项 | 约定 |
|----|------|
| 状态 | ✅ `GameDoc.matchWinStreak` / `matchLoseStreak`;`MatchStreakStats` 读写 |
| 语义 | **连续** 胜/负(非 `matchWinCount` 累计总场) |
| 互斥 | 胜 → 连败清零;负 → 连胜清零 |
| 跨天 | **不重置、不清零** |
| 字段 | `matchWinStreak` / `matchLoseStreak`(**勿**用 `matchWinCount`/`matchLoseCount`) |
| 匹配 | 开战前读 streak,Const 15~18 生效 |
---
## 5. AI 字池与银币刷新(**A0 首期**)
> **实现顺序第一刀。** 对齐玩家底栏「召唤」:普通刷新扣桃、字池填满 **5** 槽。不要求本切片做拖拽落子 UI;AI 侧无头 bag 即可。细则对齐 [需求-背包刷新.md](./需求-背包刷新.md)。
### 5.0 A0 范围与验收
| 项 | 约定 |
|----|------|
| 做 | `AiHeadlessBagHost`:`playerId=1` 独立 `BattleBagSession` + `PlayerBagTextPool.beginBattle(opponentRankId)`;开战 **首轮自动刷新一次(扣桃)**;之后按 §5.4 有桃可再刷 |
| 扣费 | 与玩家相同 Const **4/5**;读/写 **AI 槽** `PlayerSlot.gold`(禁止只扣 `localSlot`) |
| 不做 | §6 拖拽决策、§7 落点、§5.6 武器、完整匹配 UI、广告刷新、第二套底栏 UI |
| 对手 rank | A1 前可用临时 `opponentRankId`;A1 后换成 `AiMatchProfile.opponentRankId` |
| 手工验收 | 开战后 AI bag 5 槽有字;AI 金减少;玩家再点「召唤」不影响 AI remaining;字池耗尽/`rankId` 非法须 `warn` |
```text
开战
→ AiHeadlessBagHost.bind(opponentRankId)
→ 字池 beginBattle(对手 rank)
→ tryNormalRefresh(扣 AI 槽桃子,填满 5 槽) ← 对齐玩家「召唤」
后续(A3)
→ 1s tick:每拍一步动作,无动作且有桃则刷 1 次(§5.4 / §5.5)
```
### 5.1 字池(复用玩家逻辑)
现网已有实现可对照:[`PlayerBagTextPool`](../assets/script/battle/bag/PlayerBagTextPool.ts) + [需求-背包刷新.md](./需求-背包刷新.md) §4。
| 步骤 | 规则 |
|------|------|
| 建池 | `beginBattle(对手 rankId)`:从该行 `charactersweight` **等概率**抽一个整数 `N`(1~20),整局固定 |
| 候选 | `Text.type∈{1,2,4}` 且 `weight>0` 且 `numberN>0` |
| 抽取 | 每次刷新填满 5 槽;`最终权重 = weight × remaining`;抽出后 `remaining-=1`,整局累计 |
| 失败 | 未建池/耗尽 → `warn` + 失败,禁止默默塞默认字 |
**确认结论:** AI **不另配字池字段**;直接读 **对手段位** `Rank.charactersweight`。玩家与 AI 各建一份池实例,互不共享 remaining。
**现网注意:** `BattleBagSession` 构造里现写死 `beginBattle(玩家 rankId)`,且 `tryNormalRefresh` 扣 `battle.localSlot` —— A0 须包装或扩展为 **可传 rankId + 按 playerId 扣金**,决策逻辑仍放 `ai/`,共享文件改动单独小 PR。
### 5.2 Buff 对刷新的影响
| 项 | 约定 |
|----|------|
| 挂载 | 匹配阶段确定的主动/被动 buff id 写入 `BattleBuffCarryStore(playerId=1)` |
| 表现 | 被动效果与玩家同管线;主动 UI 可不展示,逻辑层持有列表 |
| 效果 | **被动已生效**(对称 Host);主动自动释放见 §8.1 |
### 5.3 桃子(银币)经济
| 项 | 规则 |
|----|------|
| 来源 | **击杀 1 只怪 → +1 桃子**(双方槽位各自记账,按 `playerId` / camp 归属) |
| 初始 | 读 `Level.startnumber`(与现网开战初始金一致) |
| 刷新扣费 | AI **与玩家相同**:Const 4/5;**首轮自动刷新也扣桃**(不免单) |
| 玩家侧 | **桃子击杀来源尚未实现**;AI 侧可先按本规则接 `PlayerSlot.gold`,待玩家对齐后统一管线 |
| 广告刷 | 无(与玩家背包需求一致);广告类见 §8 |
| A0 | 至少保证:初始金可读、刷新扣 AI 槽;击杀 +1 可后置到阶段 B |
### 5.4 刷新节奏
| 时机 | 行为 |
|------|------|
| 首轮 | **自动刷新**一次(**扣桃**,费用 Const 4/5)—— **A0 必做** |
| 一轮动作后 | 完成当前可做的动作后,若 **桃子足够** → **按 tick 逐次刷新**(每秒最多刷 1 次,直到不够或字池空)—— **A3** |
| 桃子不足 | **不刷新**,等待后续 **击杀 +1 桃**;有钱后再刷(不在一轮内强行结束 tick) |
AI 无「广告刷新」入口。A0 可只做「开战首刷」;连刷挂到 `AiOpponentController`(A3)即可。
### 5.5 AI 运行时节奏(已确认,**A3**)
| 项 | 规则 |
|----|------|
| **Tick 间隔** | **每 1 秒** 触发一次 `AiOpponentController.tick()`(固定节拍,非每帧) |
| 开战延迟 | 进入战斗后 **2 秒** 才开始首次决策(避免一进场就摆字) |
| 单次 tick | **最多一步**原子操作(铲/升级/合成/重摆一步/互换/落子…);若无可做且有桃则 **刷 1 次** |
| 多步重摆 | 跨度 map→map 按拍排队,每秒挪一格字,避免一瞬堆字 |
| 与玩家 | 不同步玩家拖拽;双轨并行 |
| 表现 | 地图新字播简易落下;1 秒节拍控制操作频率 |
### 5.6 AI 合成武将武器(已确认,**A3**)
> **与 §9 击杀掉落无关**:`Rank.aiweapon` 用于 **局内首次合成武将** 时的武器品质掷骰;`weapondrop` / `weaponweight` 仍走 §9 击杀掉落管线(阶段 D)。
读 **对手段位** `Rank.aiweapon`(5 个数,对应 **白 / 绿 / 蓝 / 紫 / 橙**,即 `Weapon.quality` 0~4)。
```text
AI 首次合成武将 heroId
→ Hero.weapon 限定可穿武器类型(同 Weapon.type,与 gg.equip.canWearType 一致)
→ Rank.aiweapon 加权随机品质 q∈{0..4}
→ 在 Weapon 表中选 type=Hero.weapon 且 quality=q 的行作为装备
→ 写入本局 registry[heroId]=weaponId(整局只掷一次)
再次合成同一 heroId(拆开又合)→ 直接读 registry,不再掷骰
```
| 项 | 规则 |
|----|------|
| 触发 | **仅合成武将**(`Hero.herotype===2`)**第一次**进入 AI 阵营(落图 / deploy)时分配 |
| 类型约束 | `Hero.weapon` ↔ `Weapon.type`(1 刀 / 2 枪 / 3 棍 / 4 弓);专属 `Hero.personalweapon` 若 **品质与 q 一致且可穿**,优先于同品质其它行 |
| 品质掷骰 | 复制 `Rank.aiweapon` 五档权重;**每次独立**加权随机 `q`;权重全 ≤0 → `warn` + 中止分配(禁止静默默认橙/白) |
| **允许重复** | **不同武将**各自按权重掷骰,**同一品质 / 同一 `Weapon.id` 可以重复出现**(与 Battlebuff「不重复」无关,不要求去重) |
| 具体 id | 同 `type+quality` 多行时:专属优先 → 否则 **`Weapon.id` 升序首行**(确定性,便于 seed 回放) |
| 无候选 | 该品质下无合法 `Weapon` → `warn` + 回退 `Hero.startweapon`(`startweapon<=0` 则暴露并跳过武器技能) |
| 局内存档 | `AiHeroWeaponRegistry`:`heroId → weaponId`,**仅本局**;战斗结束清空 |
| 复用 | **仅同一 `heroId`**:拆下再合成、换格再 deploy → **只读 registry**,不重新随机(避免同一武将反复掷骰) |
| 战斗装载 | AI 塔 `TowerLoadout` 解析武器时 **不走** `gg.equip`;改读 `AiBattleBridge.getHeroBattleWeaponId(heroId)`(集成 PR,见 §11.2) |
| 与 buff | 独立;`aiweapon` **不**参与 Battlebuff 抽取(§4.4) |
```mermaid
flowchart TD
Merge[AI 合成武将 heroId] --> Has{registry 已有?}
Has -->|是| Use[使用已存 weaponId]
Has -->|否| Type[Hero.weapon 定 type]
Type --> Roll[Rank.aiweapon 掷品质 q]
Roll --> Pick[Weapon 表 type+q 选 id]
Pick --> Save[registry 写入]
Save --> Deploy[deploy / TowerLoadout]
Use --> Deploy
```
---
## 6. 文字拖动规则(局内决策核心)
开战时按 **对手段位** Rank 行的权重,随机本局策略类型(**整局不变**),记入 `AiBattlePersonality`:
| 策略 | 配表字段 | 权重数组含义 |
|------|----------|--------------|
| 铲子使用类型 | `Rank.aiusechanzi` | 3 个数:类型 1 / 2 / 3 权重 |
| 普通字使用类型 | `Rank.aiusetext` | 2 个数:类型 1 / 2 权重 |
加权随机选中一个类型后整局固定;权重全 ≤ 0 或数组长度不符须 `warn`,禁止静默当成某默认类型。
> **字品质:** 读 `Text.quality`(0 白 / 1 绿 / 2 蓝 / 3 紫 / 4 橙)。空格取舍时:`quality===4` 为橙合成字侧,`quality===3` 为紫合成字侧(通常对应 `type===2` 组合字;以表为准)。
### 6.1 动作优先级(高 → 低,已确认)
1. **可合成**优先(背包/地图 → 空位成将,或 **逆序邻接互换** 成正序;可成将候选取阵型分更高;**散字已凑齐 Hero.text 时,清开可移动挡格并挪到横向跨度逐步拼装**)
2. **优先使用铲子**(背包有铲且有可铲格时,按 §6.2 本局类型执行)
3. **可升级**优先:**背包内**同级单字合成 → 背包→地图 → 地图→地图
4. **空位摆放**(§7;背包有字且有空 Put 时必须先摆完,避免被重摆占拍)
- **例外(神将贴路):** 场上多格合成神将存在更优贴路/贴出口重摆时,**优先阵型重摆**,暂缓落 **1–3 级普通单字** 与无攻击功能字;高等级单字与组合散字仍可先摆
5. **空位/空格不够、必须取舍时**:按 §6.4 **合成字相对普通字** 的优先级决定留谁、换谁
6. **普通字策略**(§6.3 本局 `aiusetext` 类型)
7. **散字去重/替换**(仅无空位时):同 `Text.id` 自由散字收回多余份,或背包字换到重复散字格
8. **阵型重摆**(多格神将优先贴路边 / 路径出口;可清走/互换单格战斗单位,被挤单位优先落到仍可攻击空位;有多格神将时 **1–3 级单字不抢路边阵型位**;单格单位有空位先挪空位)——**排在摆放之后**(见上条例外)
9. 若本拍无可做动作且有桃,按 §5.4 **刷 1 次**;无桃则等下一 tick(§5.5)
说明:日常有空位时主路径是 **铲 → 升级 → 摆放**;神将可贴路时插队重摆,避免 1–3 级单字长期占满路边。
同优先级内扫描顺序(背包从左到右、地图行列)实现时固定一种并保持稳定,避免帧间抖动。
### 6.2 铲子(`aiusechanzi`)——全局最高操作优先
读对手 `Rank.aiusechanzi`,开战加权随机本局类型:
| 类型 | 权重下标 | 行为(与 `AutoTable` / 策划一致) |
|------|----------|----------------------------------|
| 1 | `[0]` | 在可铲地块中 **随机** 选择 |
| 2 | `[1]` | **逐步扩张**:优先纯 Dig(未勾可放置)与已开 Put 相邻可铲格;`putText`+`dig` 双标按 **可放置优先** 当 Put,不当铲目标 |
| 3 | `[2]` | **贴岛/贴路**:优先白格 Dig 与已开邻接,其次路径邻接;边条 Dig 降权 |
| 项 | 规则 |
|----|------|
| 时机 | **无可合成/拼装时**再铲:有铲可用则开地;有可成将时先合成 |
| 来源 | 字池刷出的 `Text.type=4`;广告铲见 §8 |
### 6.3 普通字(`aiusetext`)——优先级靠后
读对手 `Rank.aiusetext`,开战加权随机本局类型;在铲子、升级、合成、以及(必要时)§6.4 取舍之后执行:
| 类型 | 权重下标 | 行为 |
|------|----------|------|
| 1(等级优先) | `[0]` | **全场谁等级高留谁**:不盯某一种字,背包/地图上任意普通字,优先保留 **局内等级更高** 的单位,低的更愿意被挪走或换掉 |
| 2(单字冲级) | `[1]` | **只盯一种字把它升满**;候选来自 **格子 + 背包**;满级后改盯下一种未满级字(§6.3.1) |
#### 6.3.1 类型 2「单字冲级」说明
**含义:** AI 在某一时刻认定 **一个目标基础字**(一个 `Text.id`,如「刀」),尽量把 **这一种字** 升到 `Hero.level_limit`;其它字都为该目标服务。
**「每 tick 可换目标字」是什么意思?(不是每次操作都换)**
| 概念 | 说明 |
|------|------|
| **tick** | AI **每 1 秒** 一次决策节拍(§5.5) |
| **一次操作** | tick 内的一步:铲一下 / 合成一次 / 落子一次 … |
| **换目标字** | 重新决定「本阶段要冲哪一种 `Text.id`」 |
**不是**「每做一步操作就换一个目标字」。
**而是**「最多 **每 1 秒**(每个 tick 开始时)重新评估一次要不要换目标」;**同一个 tick 里** 连续多步操作 **共用同一个目标字**。
**已确认规则:**
1. **每个 tick 开始时** 按下列规则 **选/更新** 目标字(可与上一 tick 相同,也可换)。
2. 该 tick 内所有 §6.3 操作 **共用** 此目标,直到下一秒下一个 tick。
3. 若目标字 **已满级** → 从 **AI 己方战场格子 + 背包** 中的未满级普通字里,改选「当前局内等级最高」的下一种作为新目标;若 **均无** 未满级普通字,则本 tick 退化为类型 1 行为。
**怎么选目标字:**
1. 扫描 **AI 战场格子上 + 背包里** 的全部 **普通字**(`Text.type=1`)。同一 `Text.id` 多处存在时,以 **该 id 当前最高局内等级** 参与比较(背包里也可能有高级字)。
2. 只保留 **局内等级 < level_limit** 的字种。
3. 取其中 **最高局内等级** 的字 id 作为目标;并列 → 若在格上则比覆盖分 → 固定扫描序。
**选定目标字之后:**
| 动作 | 说明 |
|------|------|
| **保留** | 该字 **等级最高的那一颗**(格上或背包)尽量留在/放到地图好位置 |
| **合成** | 优先 **同 id 低级副本** 合成升级(背包 ↔ 格子均可) |
| **腾位** | 优先动 **其它字** 或 **同字低级副本**,不动该 id 最高等级那颗 |
| **落子** | 若最高等级仅在背包,优先 **落到格子** 以提升覆盖(§7) |
**举例:** 格上 3 级「刀」将满级;背包里有 2 级「枪」→ 刀满级后下一目标可选「枪」。若格上 3 级刀、背包 4 级「棍」,则 tick 初就可能以「棍」为目标并优先落子到格上。
### 6.4 合成字相对普通字(空格不够时的 **互换**)
仅当 **己方可建造空位(Put)不足** 时启用。本质是 **格子上的单位互换**(被换下的进背包),不是凭空覆盖。
背包有铲且仍有 Dig 时由 §6.1 **先铲**(铲子优先于互换),不走到本条;**无铲**时允许互换,避免 Dig 未开完、又刷不起背包时长期空转。
**编辑防御**:地图格若同时勾选 `isCanPutText` + `isCanBeDig`,逻辑层 **可放置优先**(按 Put 入场,不当 Dig);纯 Dig(仅勾挖掘)仍须铲子。
**占格优先级(高 → 低,已确认):**
```text
2级普通字 > 橙色字(quality=4) > 紫色字(quality=3) > 1级普通字
```
| 规则 | 说明 |
|------|------|
| 橙色字(`quality=4`) | 可挤 **1 级普通字**、以及 **紫色合成字**(`type=2` 且 `quality=3`);**不可**挤 2 级普通字 |
| 紫色字(`quality=3`) | 仅可挤 **1 级普通字**(**不可**挤 2 级普通字;2 级普通高于橙/紫) |
| 选目标 | 有多格可挤时,优先挤更低档:1 级普通 → 紫 |
| 神将 / 多格成将 | **不可**被橙/紫互换挤占;挤占成将格会导致 DeploySync 卖掉战斗实体(表现为「合成了但不打」) |
| 被换下单位 | 进入 **AI 背包**(与 Merge 互换规则一致;互换成对出现,**不应**出现背包满无法接收) |
| **神将** | **默认不可** 被橙/紫字挤占 |
| **神将换神将** | 若新落子是 **神将** 且格上已有 **更低等级神将**,允许 **高替低**(被换下的低级神将 **回背包**) |
「普通字」= `Text.type=1`;等级 = 局内合成等级。有空位时不走本条,直接落子(§7)。
---
## 7. 摆放位置
在 AI 己方 `CampAreaID` 可建造空位上选点。目标按 **字典序**(先比 1,同分再比 2,再比 3,再比 4):
1. **最多武将**:落子后场上合成武将(神将)数量尽量多
2. **Hero.text 字序**:与表内左→右序列贴合(成将 / 正序邻接优先;逆序邻接强罚,避免「靖|李」)
3. **阵型(路径出口火力 + 覆盖 + 短前长后)**:能战斗的单位在能打到己方路径的前提下,**优先靠出口/攻击点**(守门火力);同档再比覆盖格数;再 **短射程贴路、长射程可稍离路**
4. **无攻击功能字(如营)**:**不贴路**且 **靠入口侧**(最里面、远离出口),把出口火力位让给战斗单位;落子扫描序靠后
5. **平局**:更小 `gridIndex`
背包落子扫描序同档内 **短射程先落**(先占出口侧贴路),**营等无攻击字最后落**。拼装成将时 **阵型(含路径出口)优先于步数**,同档再短射程武将优先。
### 7.1 覆盖分与阵型分(已确认)
```text
覆盖分(格子, 单位) = 该单位以该格为锚点、按实战索敌同一套射程判定时,
己方阵营路径上落入射程内的「路径格」数量
路径出口分(格子, 单位) = 射程内己方路径格中,(-距攻击点路径代价) 的最大值
(无 pathfinder 时用 +距己方出怪口格数 近似)
→ 越大越靠近出口 / 攻击点
阵型分(格子, 单位):
- 无攻击功能字(营,isNonCombatWeapon1):
阵型分 = 距路格数 * 10000 - 最近路径格出口分 * 100
→ 越靠里、越靠入口侧越高;不要求覆盖;避免占出口火力位
- 能战斗单位:须覆盖分>0;
frontBias = 2.5 - 射程格数;
阵型分 = 路径出口分 * 100000 + 覆盖分 * 1000 + frontBias * (-距路格数) * 100
→ **路径出口优先**(守门);覆盖与短前长后为次
```
| 项 | 约定 |
|----|------|
| 度量 | **A:射程内的路径格数**(不是射程圆面积、不是任意空地格数) |
| 路径出口 | 所有能战斗单位主项:射程内路径格越靠近出口/攻击点越高(守门火力) |
| 射程 | 与实战一致(如 `Hero.atk_range` / 逻辑层索敌射程;全屏等特殊值按战斗同一规则) |
| 路径格 | 本阵营 `CampArea` 上怪物可走路径单元格;只计己方路,不计对方路 |
| 锚点 | 落子格中心(组合体等多格占位时与战斗索敌锚点一致) |
| 阵型 | 能战斗:**路径出口 + 覆盖 + 短前长后**;无攻击(营):**最里面且靠入口侧**;**仅无空位时**凑不齐的组合散字可被战斗单位换位让路 |
| 平局 | 阵型分相同则选更小 `gridIndex`,禁止再引入静默随机除非策划要求 |
铲子选格走 §6.2(`aiusechanzi`),**不**用本覆盖/阵型分。本条仅用于「有字要落入空位」时的选点。
禁止落到对方 camp;合法性走与玩家相同的放置校验(按 `playerId`/camp)。
### 7.2 合成武将阵型重摆(已确认)
多格武将(`Hero.herotype===2` 且占多格)成将后,决策链在摆放之后再检:
| 项 | 约定 |
|----|------|
| 时机 | §6.1:摆放之后;**例外**:可贴路时优先于落 1–3 级单字;可跨多个 1s tick 逐步挪完 |
| 目标 | **贴路边 / 路径出口(怪物出口侧)** 阵型分更高的横向 Put 跨度(`formationScore`:路径出口优先 + 神将贴路加成) |
| 空跨度 | 目标全空且与当前占位不重叠 → 左→右 `map→map` 挪过去 |
| 换位 | 目标被 **单格战斗单位**(或营等无攻击字)占用时:先把挡路单位挪到跨度外空位(**战斗单位优先仍能打到路的格**),再挪神将;若无足够空位且目标整段满占 → 与挡路单位 **一一互换** |
| 优先级 | **多格合成神将 > 1–3 级普通单字**(有神将时低级单字不主动抢路边;神将重摆可清走路边低级单字) |
| 禁止 | 拆其它多格神将占格;不可落到对方 camp |
| 动作 | **每拍一步**排队执行(不经背包) |
> **字序未成将:** 场上/背包已有一套 Hero.text 散字但未贴齐时,走 §6.1「可合成」:一步成将(落空/邻接互换)或 **全局拼装**(散字凑齐后逐步 `map/bag→` 横向空跨度,**阵型/路径出口优先于步数**;**同一决策拍内连续做完拼装队列**,避免悟/空长时间竖拆)。有清桩空位时先挪挡路再落子;**无空位时直接与挡路单位互换**(含背包字换下占格),禁止因空 Put=0 干等刷新/铲地。组合字落点 **优先** 横向可拼跨度(`composeOrderScore`);**凑不齐时仍落到空 Put**(仅避开逆序邻接强罚格),避免背包积压。组合字(`Text.type=2`)之间 **不能** 像普通字那样互叠升级;成将后多余同组字再喂入武将升级。
---
## 8. 主动技能与广告模拟
> **实现顺序:** 本节规则为 AI 侧契约;**玩家主动技能 / 广告 / 装备掉落** 功能尚未完整实现。AI 须 **等玩家侧能力就绪后**,复用同一套逻辑与数据再同步(禁止 AI 先行猜一套独立规则)。
### 8.1 主动技能(道具使用)
对应策划「道具使用」:**局内主动技能**(匹配挂载的 Battlebuff type=1)。
| 项 | 规则 |
|----|------|
| 时机 | **CD 结束立即释放**(无合法目标 / 条件不足则 **保持就绪、不进 CD**,下帧再试)。**例外:乾坤袋**见下行 |
| 乾坤袋 | **不**在帧 tick 自动放;仅当决策拍 **升级/放字等已穷尽、准备刷新** 时:背包仍有可回收多余字 → 收 1 格换桃(低等级优先);**场上字永不回收**;无多余字则直接刷新 |
| 目标优先 | 默认:合成武将 → 普通字;同档内 **高等级优先** |
| 文昌笔目标 | **紫字优先**(赌变橙可合成)→ 其余 **低等级优先** → **橙字垫底**(已可合成,尽量不改);同档再比稳定格序。实现:`AiActiveSkillTargetPicker.pickRewrite` |
| 来源 | 匹配阶段主动 id 列表(`BattleBuffCarryStore`) |
| 实现 | `AiActiveSkillHost` + `AiActiveSkillTargetPicker` |
| 已接线 | 封神榜、授仙箓、乾坤袋(**仅刷新拍**:升级/放字穷尽后,背包有多余字才回收,**场上永不回收**)、文昌笔、攻速符(**仅已上场攻击单位**)、千里眼(**仅已上场远程单位**)、万神幡(**给玩家**出征 Boss,玩家出怪口)、七十二变(roll 后条件不足则等待该技能;非法 kind 重 roll);缚仙网/三昧真火(无格等待);幻魂镜(无神将等待);妙手神偷(没空位/无可偷则等待;**仅场上 Put 英雄散字 Combo,不可偷背包**;偷完**不改格子语义**) |
| 施放表现 | AI 主动施放时 **复制** `ui_otherskill1List` 对应槽 icon,飞向单位格 / 出怪口 / 神将中心 / 偷取源格等受体(对齐玩家拖放落点;逻辑仍即时生效,不阻塞 CD) |
### 8.2 广告触发几率
读对手 `Rank.aiusead`(万分比)。实现:`AiAdHost`(经 `AiBattleBridge` 每帧 tick)。
| 项 | 规则 |
|----|------|
| 概率 | `Rank.aiusead`(`session.rng` 掷万分比;失败则本机会不发放) |
| 类型 | **铲子广告**、**防御广告(门神盾牌)**;效果对齐玩家,**不调用**广告 SDK |
| 铲子条件 | 与玩家一致:己方无可放置 Put + 背包有空位且无铲;空位>1 发 2 把否则 1 把 |
| 盾牌条件 | 与玩家一致:战斗中、进攻怪离 AI 终点 ≤ Const23;成功则布置门神陷阱 |
| 投掷上限 | 铲子 Const **22**、盾牌 Const **25**:**每次条件满足后的掷骰均消耗 1 次**(成败都计),用尽后本局不再掷 |
| 优先级 | Const23 有威胁时优先盾牌,否则铲子(对齐玩家 btnItem) |
| 状态 | **已实现** |
---
## 9. 击杀装备掉落
| 项 | 规则 |
|----|------|
| 归属 | 击杀怪物时按 **对手段位** `weapondrop` / `weaponweight` 掷骰 |
| 与 AI 决策 | **不进入**拖拽状态机;独立掉落/展示/结算管线 |
| 与 §5.6 | **分离**:合成武将武器走 `Rank.aiweapon`(§5.6);本节仅 **击杀掉落** |
| 状态 | **等待实现玩家功能后再同步 AI**(玩家击杀掉落/展示/结算先落地,AI 复用同一掷骰与展示规则) |
细节若与局外神将武器需求重叠,结算 UI 另文;本文只要求战斗侧按表掷骰且不静默吞掉。
---
## 10. 胜负条件
| 项 | 状态 |
|----|------|
| 规则 | 门血 / 波次等细则可继续完善;结算 UI(胜/败)已调 `MatchWinStats.record` |
| 连胜 | streak 已在胜负结算中更新(§4.5),不再阻塞匹配 |
| 参考 | [`方案-出怪逻辑实现.md`](./方案-出怪逻辑实现.md) |
| AI | 与玩家共用 `BattleSession` 结果,不单独判赢 |
---
## 11. 代码组织与冲突隔离(已确认方向)
> **目标:** AI 匹配 + 局内决策 **独立目录、独立 PR**,他人改玩家 UI / 战斗表现时 **尽量不碰 AI 文件**,降低合并冲突。
### 11.1 目录约定(新增代码放此,勿散进 `CocosBagBoardView` 等)
```text
assets/script/battle/ai/
├── match/ # 开战前匹配(阶段 A)
│ ├── AiMatchProfile.ts # 本局对手档案(只读数据结构)
│ ├── AiMatchService.ts # Const 11/12/14、buff 抽取 → Profile
│ └── AiBattlebuffPicker.ts # §4.4 加权不重复抽取
├── runtime/ # 局内生命周期
│ ├── AiOpponentController.ts # 开战延迟;决策 tick
│ ├── AiBattlePersonality.ts # aiusechanzi / aiusetext 开局抽样
│ ├── AiHeadlessBagHost.ts # playerId=1 无头 BattleBagSession + 字池
│ ├── AiHeroWeaponRegistry.ts # §5.6 本局 heroId→weaponId 存档
│ ├── AiHeroWeaponAssigner.ts # §5.6 aiweapon 品质掷骰 + Weapon 解析
│ ├── AiPassiveCdHost.ts # CD 被动产桃/铲
│ ├── AiActiveSkillHost.ts # §8.1 主动技能
│ └── AiAdHost.ts # §8.2 广告模拟(铲子/门神)
├── decision/ # 纯决策(无 Cocos 节点依赖)
│ ├── AiActionPlanner.ts # §6.1 优先级链
│ ├── AiShovelPlanner.ts # §6.2
│ ├── AiTextPolicyPlanner.ts # §6.3 / §6.3.1
│ ├── AiSwapPlanner.ts # §6.4 互换
│ ├── AiBoardProbe.ts # 探测 / 合成 / §7.2 重摆
│ └── AiPlacementScorer.ts # §7 覆盖分
├── bridge/
│ └── AiBattleBridge.ts # **唯一对外集成层**(见 11.2)
└── types/
└── AiTypes.ts # 动作枚举、Planner 入参/出参
```
- **禁止** 在 `ui/`、`CocosBagBoardView.ts` 内堆 AI 决策逻辑。
- **禁止** 在 `assets/mx/` 写 AI 策划规则(MX 保持通用内核)。
- 复用现有类:`PlayerBagTextPool`、`BattleBagSession`、`MergeSession` —— **只调用**,扩展时优先 **包装**(`AiHeadlessBagHost`),少改原文件。
### 11.2 集成边界(减少与他人冲突的改法)
| 层级 | 他人常改 | AI 侧做法 |
|------|----------|-----------|
| **入口** | `GameController` / `UIBattleGame` | 各 **最多 2~3 行**:开战调 `AiMatchService.buildProfile()`;战斗就绪调 `AiBattleBridge.start(profile)`;结束 `stop()` |
| **桥接** | — | 全部进 `AiBattleBridge`:取 `BattleRuntime`、按 `playerId=1` 执行 refresh/drop/deploy;**武将武器**对外 `getHeroBattleWeaponId` |
| **必须改旧文件时** | `DeploySync`、`MapPlaceValidator`、`BattleRuntime`、`TowerLoadoutService` | **单独小 PR**:`playerId` / 第二 bag / AI 武器解析挂钩;**决策逻辑仍放 `ai/`** |
| **表现** | `TextGrid`、战斗特效 | AI 只发 **与玩家相同的 Merge/deploy 事件**;网格由现有 presenter 刷新 |
| **匹配 UI** | `UImatch` | AI **不碰**;UI 只读 `MatchWinStats` / 已有展示;Profile 由 `AiMatchService` 产出给战斗 |
```text
GameController / UIBattleGame
│ (薄调用)
▼
AiBattleBridge ──► AiOpponentController ──► decision/*
│ │
▼ ▼
BattleRuntime / Merge / DeploySync(playerId=1,共享规则)
```
### 11.3 模块职责(与目录对应)
| 模块 | 职责 |
|------|------|
| `AiMatchService` | Const 11/12/14/15~18/19/20 → `AiMatchProfile` |
| `AiBattlebuffPicker` | §4.4 |
| `AiBattlePersonality` | `aiusechanzi` / `aiusetext` |
| `AiHeadlessBagHost` | `playerId=1` 字池 + bag |
| `AiHeroWeaponRegistry` / `AiHeroWeaponAssigner` | §5.6 合成武将武器:首次掷骰、局内复用 |
| `AiOpponentController` | 开战 2 秒后激活;每 2 秒一步;§6~§7;无动作时刷 1 次 |
| `AiPlacementScorer` | §7 覆盖分 |
| `AiBattleBridge` | 对外唯一入口;生命周期与 BattleRuntime 对齐 |
### 11.4 协作约定
1. **AI 功能 PR** 默认只改 `assets/script/battle/ai/**` + 必要的 1~2 个挂钩文件。
2. 挂钩文件改动须在 PR 说明中 **列出路径与行号**,便于 review。
3. `RankDataManager.pickOpponentRankId` 重构 → 可迁入 `ai/match/` 或保留 manager 但 **实现搬至 AI 模块调用**,减少 manager 长期冲突。
4. 类型定义优先放 `ai/types/`,避免与他人共改 `ProjectBattleTypes.ts`;若必须扩展,`MatchProfile` 字段 **追加可选**,不改玩家字段语义。
---
## 12. 验收清单
### 12.0 A0:AI 背包刷新(先过)
- [ ] `AiHeadlessBagHost`:`playerId=1` 独立 bag + `PlayerBagTextPool.beginBattle(opponentRankId)`
- [ ] 与玩家池 **remaining 隔离**;玩家点「召唤」不消耗 AI 池
- [ ] 开战 **首轮自动刷新** 填满 5 槽,并按 Const 4/5 **扣 AI 槽桃子**(不免单)
- [ ] `tryNormalRefresh`(或等价)按 **playerId=1** 扣金,禁止写死 `localSlot`
- [ ] 字池未就绪 / 耗尽 / 非法 rank → `warn` + 失败,无静默默认字
### 12.1 其余阶段 A~D
- [ ] Const 11 按 **星** `-5~+4` 十档匹配;越界权重为 0
- [ ] Const 12 / 14 驱动主动 / 被动数量偏移,结果分别 clamp 到 0~2 / 0~6
- [x] 连续 streak + Const 15~18 修正 12/14 权重;跨天不清零;胜/败互斥
- [x] Battlebuff:按 `type` 分池 + `functiontype∈[2,27]` + 排除 `type=3`;`weight` 加权不重复;被动对称接线中
- [ ] §5.6:合成武将首次 `Rank.aiweapon` 独立加权掷品质(**可重复**)→ `Hero.weapon` 约束下选 `Weapon.id`;**同一 heroId** 局内 registry 复用(拆开再合不重掷)
- [ ] **每 1 秒** tick:**一步**动作或 **一次**刷新;无桃则等击杀加桃
- [ ] 击杀 +1 桃(AI 槽独立);玩家侧击杀加桃 **待对齐**
- [ ] 动作序:**可合成/拼装** → **铲** → **升级(包内→包图→图图)** → **摆放** →(空不够)§6.4 → 普通字策略 → 散字去重 → 阵型重摆;开局抽 `aiusechanzi` / `aiusetext` 整局固定
- [ ] §6.4:优先级 **2级普通 > 橙 > 紫 > 1级普通**;橙可挤 1 级普通与紫字(不可挤 2 级普通);紫仅可挤 1 级普通;多目标时先挤低档;非法 quality 须 `warn`
- [ ] 落点字典序:武将数 → Hero.text 字序 → **阵型(路径出口火力 + 覆盖 + 短前长后)**(营最里且靠入口侧)→ gridIndex;拼装阵型优先于步数;短射程先落、无攻击最后落
- [x] 主动技能:CD 好即放;默认目标优先合成武将→普通字、等级高;文昌笔紫字赌橙→低等级→橙垫底;缚仙网/真火/幻魂镜/神偷已接
- [x] **`Rank.aiusead` 广告模拟**:铲子 / 门神盾牌;只模拟效果、不播广告;Const22/25 限制投掷次数(成败均消耗)
- [ ] **(待玩家功能后同步)** 击杀装备掉落:`weapondrop` / `weaponweight`(§9,与 §5.6 `aiweapon` 无关)
- [ ] **(待玩家功能后同步)** 胜负条件
- [ ] 无静默兜底;缺表/空权重 `warn`
---
## 13. 待补项(实现前须对齐)
### 13.0 需求文档:AI 决策规则 **已基本可开发**
| 状态 | 内容 |
|------|------|
| **首期切入** | **A0 = §5 AI 背包刷新**(对齐玩家「召唤」) |
| **已齐** | 匹配、battle 决策、tick/刷新、单字冲级、互换、覆盖分、**§5.6 武将武器**、**§11 独立目录** |
| **仍缺(不阻塞 A0)** | 完整匹配 UI、A2 对称化小 PR、下表 |
| **整段后置** | §8/§9 主动/广告/掉落;CD 被动(产桃树/产铲)AI 运行时 |
| 仍须对齐 | 说明 |
|----------|------|
| A0 扣金 / 建池 | `BattleBagSession` 现写死玩家 rank + `localSlot`;须可按 AI `playerId`/`rankId` 调用 |
| 玩家本局 buff 数量读档 | 道具商店需求;AI 只消费个数(A1) |
| 工程对称化小 PR | `DeploySync` / `MapPlaceValidator` / slot 金币(A2,与 `ai/` 分 PR) |
| 胜负规则 | 定稿后接 streak |
| 玩家侧 §8/§9 | 阶段 D 再同步 AI |
---
### 13.1 连胜 / streak(已完成)
| 项 | 说明 |
|----|------|
| **连胜/连败字段** | `matchWinStreak` / `matchLoseStreak`;胜负 UI 结算互斥增减;跨天不清零 |
| **Const 15~18** | 匹配前修正 12/14 权重(§4.3) |
### 13.2 阻塞于其它玩家能力(阶段 B / D)
| 项 | 说明 |
|----|------|
| **击杀 +1 桃(玩家侧)** | AI 可先按 §5.3 接槽位 gold;玩家管线待对齐 |
| **主动技能 / 广告 / 装备掉落** | §8、§9;玩家实现后 AI 同步 |
| **Battlebuff 效果** | 本期只挂载;字池/刷新加成等 **效果层**待 buff 系统 |
| **玩家本局 buff 数量** | 读当日道具栏的存档结构(依赖道具商店需求) |
### 13.3 算法与工程细则(已确认 / 已对齐)
---
#### 13.3.1 总星算法(Const 11 偏移基准)
(算法同前;**AI 侧不需要处理总星 UI 展示**,匹配只产出 `opponentRankId` / 内部 `opponentStars` 供字池与难度。)
```text
minStar = Rank 首档 level[0];maxStar = Rank 末档 level[1](-1 则取表内最大星)
playerStars = getPlayerRankStars()
对每个偏移 d∈[-5..+4]:若 playerStars+d 不在 [minStar,maxStar] → 该档权重=0
随机 d → opponentStars = clamp(playerStars+d, minStar, maxStar) // 绝不越界
opponentRankId = resolveTierByStars(opponentStars).id
```
**代码锚点:** `RankDataManager`(`pickOpponentRankId` 须改为按 **星** 十档)。
---
#### 13.3.2 AI tick(已确认)
| 项 | 规则 |
|----|------|
| 间隔 | **每 1 秒** 一次 tick;开战 **2 秒** 后才激活 |
| 单次 tick | **一步** §6.1 动作;无动作且有桃则 **刷 1 次**(§5.4) |
| 桃子不够 | 等待击杀加桃,**下一 tick 再试**,不结束 AI |
---
#### 13.3.3 一轮动作(已确认)
```text
操作节拍 = 每 1 秒最多一步原子操作(§6.1 优先级链取最高可执行项)
无动作且有桃 → 本拍刷新 1 次;刷新后的落子等下一拍
```
- 每步一个原子操作(铲 / 合成 / 升级 / 互换 / 落子 / 普通字整理 / 重摆一步)。
- 有钱刷新:**按拍逐次**,不在同一秒内连刷多轮。
- 不含 §8 主动技能、§8.2 广告。
---
#### 13.3.4 挤占 / 互换(已确认)
- 占格冲突走 **互换**:新单位上图格,原单位 **回 AI 背包**(Merge 互换语义,成对出现)。
- 橙/紫对普通字:见 §6.4。
- **神将**:不可被普通字橙/紫挤掉;**新神将等级更高** 时可与格上 **更低等级神将** 互换(低的回背包)。
---
#### 13.3.5 单字冲级
见 §6.3.1:候选 = **战场格子 + 背包**;每 1 秒 tick 更新目标;满级后切下一种未满级字(比最高等级,背包与格子均参与)。
---
#### 13.3.6 被动 buff(已确认)
| 项 | 规则 |
|----|------|
| 匹配 | §4.4 抽到 id 列表 |
| 局内 | 写入 `BattleBuffCarryStore`;被动对称生效;主动列表可供 §8.1 自动释放 |
| 上限 | Const **19**(主动)/ **20**(被动) |
---
#### 13.3.7 匹配 UI
**本文不规定匹配页 UI**;由匹配 UI 模块自行实现。逻辑层仍产出 `MatchProfile`(段位、buff ids、策略类型、seed 等)供战斗读取。
---
#### 13.3.8 Const 11「有效权重为 0」是什么?
**含义(极端容错,正常玩不到):**
匹配时按 Const 11 随机「对手比玩家多/少几颗星」。不合法档位(会使 `玩家星+偏移` **低于 minStar 或高于 maxStar**)权重 **先置 0**,再随机;最终 `opponentStars` 再 **clamp** 到 `[minStar, maxStar]`,**保证不会越界**。
若置 0 后 **十档权重全为 0**(多为 **配表错误**),则无法抽对手 → `warn` + 中止匹配。现网均匀权重下 **极少触发**。
---
#### 13.3.9 §2 工程对称化验收(阶段 A 必过)
实现 AI 决策前,下列项须 **按 playerId=0/1 对称**;现网缺口括号标注。
| # | 项 | 验收标准 | 现网参考 |
|---|-----|----------|----------|
| 1 | **放置 camp** | `MapPlaceValidator(ownerPlayerId)`:`playerId+1` 对 `CampAreaID`(0→1 / 1→2);AI bag 传 `ownerPlayerId=1`,**不**再覆盖门禁 | 出怪口反查会错;须按 playerId 而非写死本机 |
| 2 | **DeploySync** | 监听 **指定 playerId** 的 `bagSession`;`reconcile` 时 `getPlayer(id)` + `deploy({ playerId })` | 已支持 `bind(..., playerId)`;AI 走 `DeploySync(1)` |
| 3 | **无头 bag** | `playerId=1` 有独立 `BattleBagSession` + `PlayerBagTextPool(opponentRankId)`;`Player.bagSession` 远端非 null 或等价服务 | 远端 `bagSession` **null** |
| 4 | **桃子** | 击杀 +1 写入 **对应 slot.gold**;刷新扣 **对应 slot**;UI 只显示本机 | `BattleRuntime` **偏 localGold** |
| 5 | **墙血 / 胜负** | 怪到门扣 **对应 owner** 的 `wallHp`;双槽独立(阶段 B 与 §10 一并验收) | 部分 **仅 localSlot** |
| 6 | **波次金币** | 若有波次奖励,按 camp 归属入账 | 待查 |
| 7 | **塔归属** | AI `ownerId=1` 打攻 AI 的车道怪;落子仅 `CampAreaID=2` | `enemy(2) put=0` 则地图未标敌方放置区 |
| 8 | **表现** | AI 阵营 `TextGrid` 同步;可无底部 5 格 UI | 仅本机 `CocosBagBoardView` |
| 9 | **AI 武将武器** | 合成首次分配;registry 复用;`TowerLoadout` 读 AI 武器 id | 现网仅 `gg.equip` |
| 10 | **快照** | 若做断线重连,须含 AI bag/金/部署/武器 registry;本期可 **声明不做** | `BattleSnapshot` 偏单 bag |
**通过标准:** 阶段 A 至少 **#1~#4、#7~#9** 可手工验证(AI 能 Refresh、Place、Deploy 到 AI camp 且 ECS 塔战斗、合成武将有稳定武器)。
---
## 14. 相关代码锚点(只读参考)
| 路径 | 说明 |
|------|------|
| `assets/script/battle/ai/` | **新建**:匹配 + 决策 + 桥接(§11,独立 PR) |
| `assets/script/battle/bag/PlayerBagTextPool.ts` | 字池(AI 复用,换 `rankId`) |
| `assets/script/manager/RankDataManager.ts` | 总星 API;`pickOpponentRankId` 待改十档 |
| `assets/script/battle/runtime/DeploySync.ts` | 集成 PR:支持 `playerId=1` |
| `assets/script/battle/bag/MapPlaceValidator.ts` | 集成 PR:按 player camp 校验 |
| `assets/mx/module/battle/player/PvPBattleMode` 等 | 双槽与 camp 策略 |