FunnyPitch ML-Agents 完整重构方案(2026-05-18)
本次重构针对 FunnyPitch 的 ML-Agents 训练体系,目标不是"继续修补现有流程",而是重新整理一套适合严格回合制棋类 + 间谍揭露动作的训练架构。涉及动作空间重设、episode/reset 链路清理、reveal 建模等核心问题。
FunnyPitch ML-Agents 完整重构方案(2026-05-18)
1. 背景与目标
本次重构针对 FunnyPitch 的 ML-Agents 训练体系,目标不是"继续修补现有流程",而是重新整理一套适合严格回合制棋类 + 间谍揭露动作的训练架构。
已确认的问题
-
非法动作是结构性产物
- 当前动作空间是
piece + target + special三分支。 piece和target各自可能合法,但组合起来可能非法。- 导致日志里会出现 action 在跑、步数在涨,但棋盘没有真实移动。
- 当前动作空间是
-
episode/reset 链路不干净
- 第一局结束后曾出现双重 reset / 双重 EndEpisode 的迹象。
- 这会污染第二局开局状态,让训练表面继续、实则状态错乱。
-
reveal 的建模不够清晰
- reveal 不是普通移动,但也不是回合外插入动作。
- 它本质上更像是"占用一个整回合的一次性特殊行动"。
-
训练层和规则层耦合过深
PitchAgent、PitchEnvController、PitchTrainingController、PitchManager都在部分控制回合/结束/开局。- 对严格回合制游戏来说,这很危险。
2. 重构原则
主上已定的原则如下:
- 规则正确性优先:先保证训练循环能稳定完整跑完一盘棋。
- 动作空间改为统一合法动作列表。
- reveal 是一种 move/action:
- 只能在己方回合执行
- 执行后即结束该回合
- 然后轮到对方回合
- self-play 采用单 policy 控双边。
- 本轮允许顺手清理明显不合理奖励,但不重写整套 reward 设计。
3. 当前实现的结构问题
3.1 当前核心文件与职责
| 文件 | 当前职责 | 问题 |
|---|---|---|
PitchAgent.cs |
观测、动作、episode 生命周期 | 职责过重,还掺了非法动作安全网 |
PitchEnvController.cs |
reward、reset、训练控制、reveal、轮换 | 已接近中枢,但还不够唯一 |
PitchTrainingController.cs |
phase 管理、换边、阶段切换 | 与 EnvController 职责重叠,建议移除 |
PitchActionMasker.cs |
三分支 mask | 动作编码模型本身不适合当前棋类 |
PitchManager.cs |
规则执行、棋盘逻辑 | 应继续作为规则核心,不负责训练流程 |
3.2 三分支动作空间的问题
当前动作空间:
- Branch 0: piece
- Branch 1: target
- Branch 2: special(move/reveal/skip)
根本问题
这套设计天然存在:
piecebranch 单独合法targetbranch 单独合法- 但
piece + target组合非法
这不是一个 patch 能彻底解决的问题,而是编码方式本身不贴合棋类动作结构。
3.3 Reveal 的语义冲突
reveal 当前被当成特殊动作分支的一项,但它其实不是"附属行为",而是:
- 需要在当前方回合内决定是否使用
- 使用后消耗行动机会
- 使用次数有限(每边每局一次)
- 对信息态产生永久改变
因此 reveal 更接近:
一个受状态机控制的一次性合法行动
而不是"普通移动旁边顺手加个开关"。
4. 重构后的目标架构
4.1 统一控制权
新职责划分
| 模块 | 重构后职责 |
|---|---|
PitchAgent |
只负责 ML 接口:CollectObservations / OnActionReceived / EndEpisode |
PitchEnvController |
唯一的训练/回合/episode 中枢 |
PitchManager |
继续作为规则与棋盘执行核心 |
PitchTrainingController |
删除或废弃 |
PitchActionMasker |
改写为基于合法动作列表的 mask |
PitchPerception |
保留,继续负责观测编码 |
中枢原则
以后只能由 PitchEnvController 负责:
- reset episode
- 自动选间谍
- 设定当前方
- 请求当前方决策
- 执行动作
- 结算 game over / draw
- 触发 EndEpisode
其他模块不再各自管理阶段切换。
4.2 新回合状态机
重构后状态应简化为:
ResettingPlayingGameOver
不再保留训练专用的 SpySelection 阶段进入决策流。
Episode 流程
OnEpisodeBeginEnvController.ResetEpisode()PitchManager.ResetGame()- 自动随机指派红蓝间谍
- reveal 次数清零
CurrentSide = Red- 进入
Playing - 枚举当前方全部合法动作
- 当前方选择一个 action
- 执行 action
- 若终局则结算,否则切边继续
5. 新动作空间设计
5.1 统一合法动作列表
思路
不再让模型拼装:
- 先选棋子
- 再选目标
- 再选 special
而是改为:
由规则层先枚举当前局面的全部合法 action,模型只从中选一个。
统一动作结构
建议新增:
public enum PitchActionType
{
Move,
Reveal,
}
public class PitchLegalAction
{
public PitchActionType ActionType;
public PitchStepData StepData; // Move 用
public e_PitchSide Side;
public string DebugLabel;
}
当前方每次行动前,生成:
List<PitchLegalAction> legalActions;
例如:
- Move: piece 12 -> grid 38
- Move: piece 5 -> grid 29
- Reveal
然后给 agent 一个统一动作索引:
- action 0
- action 1
- action 2
这样做的收益
- 不再有组合非法动作
- reveal 可以自然作为合法 action 之一
- mask 逻辑大幅简化
- 日志更直观,方便排查
5.2 Reveal 的正式定义
reveal 的规则地位
重构后明确规定:
- reveal 是一种 合法行动
- 只能在己方回合执行
- 一旦执行,本回合结束
- reveal 后立即轮到对方回合
- 每边每局只能使用一次
- 若当前对方无可揭间谍,则不进入合法动作列表
这意味着什么
reveal 不再通过 special branch 临时判断, 而是像普通 move 一样,以完整 action 的身份出现在 legal action list 里。
6. Story 拆分(开发故事)
下面按可执行的 story 切分,每个 story 尽量可独立验证。
Story 1:统一 episode/reset 控制链
目标
让 episode 开始、结束、重置只走一条链路。
要改的事
PitchEnvController成为唯一 reset 入口PitchTrainingController不再负责 phase/turn/reset- 清理双重
RequestEndEpisode/ 双重ResetEpisode风险 - 保证每局只 reset 一次
完成标准
- 日志中每局只出现一套 reset 流程
- 不再出现
Playing -> Playing或双重开局日志 - Red 总是第一手
Story 2:移除 spy selection 训练阶段
目标
让间谍选择彻底退出训练决策流。
要改的事
- 训练开局自动选红蓝双方间谍(士兵 + 大臣)
- 删除/废弃
SpySelection阶段逻辑 PitchTrainingController中相关接口标记废弃或删除
完成标准
- 新 episode 开始时直接进入 Playing
- 日志中不再出现 SpySelection 决策
- 选间谍不计入 step
Story 3:引入统一合法动作列表
目标
彻底替换三分支动作编码。
要改的事
- 新增
PitchLegalAction/PitchActionType - 在 EnvController 或独立 helper 中实现
BuildLegalActions(side) - Move 和 Reveal 都在同一列表中
- Agent 只输出一个 action index
完成标准
- 不再使用
piece + target + special三段式输入 - 不再出现
PieceIdxToStepData()这种组合恢复逻辑 - 不再出现"分支各自合法、组合非法"问题
Story 4:改写 ActionMask 与 Agent 接口
目标
让 ML-Agent 与新动作空间对齐。
要改的事
PitchActionMasker改为只对 action index 做 maskPitchAgent.OnActionReceived改为:- 取 index
- 从 legal action list 中找到 action
- 执行
BehaviorParameters改为单个 discrete branch
完成标准
- 日志输出 action index + debug label
- 非法 index 不应导致假推进
- 无需 piece/target/special 恢复逻辑
Story 5:回合切换与 game over 收口
目标
把 turn switch 和 game over 判断统一到 EnvController。
要改的事
- move / reveal 执行完成后统一调用
CompleteTurn() CompleteTurn()内负责:- 检查终局
- 检查 max steps
- 检查 no legal move
- 切边
- 请求对方决策
GameOver()作为唯一结算入口
完成标准
- 红蓝严格轮替
- reveal 执行后也正确切边
- 无"双重结束 / 双重换边 / 双重首手请求"
Story 6:清理奖励与观测残留
目标
让 reward / observation 与新架构一致。
要改的事
- 删除 skip 相关奖励和逻辑
- 保留核心 reward:win/loss/draw/kill/reveal
- 检查
PitchPerception是否仍含 spy selection 残留输入 - 如果单 policy 控双边,需要确保 side 信息足够清晰
完成标准
- reward 常量与动作系统不矛盾
- perception 不再包含废弃阶段信息
- 训练日志更容易读懂
7. Issues 列表(需跟踪的问题)
以下 issues 建议单独挂出来跟踪。
Issue 1:三分支动作空间导致组合非法
现象
当前 piece/target/special 结构允许:
- piece 合法
- target 合法
- 组合非法
风险
- 假训练
- 棋盘静止但步数增长
- agent 学习信号污染
处理策略
完整重构中直接移除三分支结构,改为统一合法动作列表。
Issue 2:episode 结束后双重 reset 风险
现象
第一局结束后,可能由 Red / Blue 各自触发一次 reset。
风险
- 第二局状态被重置两次
- 间谍分配被覆盖
- 首手请求重复
处理策略
只保留 EnvController 的全局 reset / global game over 入口。
Issue 3:Reveal 的语义不够稳定
现象
Reveal 目前被当成 special branch 中一项,逻辑分散。
风险
- 规则语义模糊
- 不利于学习
- 切边、mask、reward 处理易出错
处理策略
重构后将 Reveal 纳入统一 legal action list,作为回合行动之一。
Issue 4:PitchTrainingController 职责重复
现象
TrainingController 既管 phase,又参与换边、开局请求,与 EnvController 重叠。
风险
- 维护成本高
- 多点控制回合,容易冲突
处理策略
删除或彻底废弃,仅保留最小训练配置开关。
Issue 5:单 policy 控双边的训练接线尚未落地
现象
当前仍偏向双 agent 双 team 的历史结构。
风险
- reset / team / self-play 配置容易绕
- 对称棋类没有充分利用参数共享优势
处理策略
重构中明确单 policy 控双边,必要时保留两个 scene agent 实例,但共享同一 behavior。
Issue 6:最大合法动作数上限需要实测
现象
统一 legal action list 后,需要给 discrete branch 一个最大容量。
风险
- 上限过小会截断合法动作
- 上限过大虽可 mask,但会浪费输出空间
处理策略
先理论估算,再用日志统计实测上限,建议从 128 起步。
8. 推荐实施顺序
第一阶段:先把训练循环地基修稳
优先做:
- Story 1
- Story 2
- Story 5
目标:
- 先保证一盘棋能稳定完整跑完
- 无双重 reset
- reveal 占回合
- 严格轮替
第二阶段:重做动作空间
优先做:
- Story 3
- Story 4
目标:
- 彻底消除组合非法动作
- 让 reveal 和 move 在同一 action catalog 中
第三阶段:清尾
优先做:
- Story 6
- 处理所有 issues 的遗留项
9. 验收标准
本次完整重构完成时,至少要满足:
- 训练可连续运行多盘,不会出现"棋盘静止但日志继续刷"的假推进。
- 每盘只 reset 一次。
- 红蓝严格轮替。
- reveal 是合法回合行动,执行后正确切边。
- 不再出现
piece + target + special三分支动作恢复逻辑。 - 不再需要用"连续非法动作安全网"来掩盖结构问题。
- 文档、代码、训练配置一致。
10. 本次设计结论
结论很明确:
- 不需要推倒整个项目;
- 但需要对训练体系做一次核心重构;
- 核心重构点不是 reward,而是:
- 回合状态机统一
- 动作空间改为统一合法动作列表
- reveal 明确为一种回合行动
这是把 FunnyPitch 从"能勉强训练"推进到"适合长期训练"的关键一步。
11. 当前实施进度与待验证项(2026-05-18 核查版)
⚠️ 本章节已根据代码实地核查结果更新。标记说明:
- ✅ = 代码已实现,与目标一致
- ⚠️ = 代码已实现,但需实际运行验证
- ? = 需进一步确认
✅ 已完成(代码已落地)
Story 3:引入统一合法动作列表
- ✅
PitchLegalAction.cs已建立(PitchActionType.Move / Reveal) - ✅
PitchActionSpace.MAX_LEGAL_ACTIONS = 128 - ✅
PitchEnvController.BuildLegalActions(side)已实现,Move / Reveal 共入列表 - ✅
PitchEnvController.TryGetLegalAction()/DescribeLegalAction()已实现 - ✅
PitchEnvController.HasAnyLegalMove()改为基于 legal action list 判断
Story 4:改写 ActionMask 与 Agent 接口
- ✅
PitchAgent已改为只读取单个离散 action index(BRANCH_ACTION = 0) - ✅
PitchActionMasker已改为对单一 action branch 做 mask,不再使用 piece/target/special 三分支 - ✅
OnActionReceived中无PieceIdxToStepData()组合恢复逻辑 - ✅
MLTrainingScene.unity与inference_config.yaml已同步到 1 个 discrete branch / 128 容量
Story 2:移除 spy selection 训练阶段
- ✅
PitchTrainingController.OnResetGame()直接进入Playing,SpySelection 已废弃 - ✅
PitchTrainingController中以下旧函数已删除:ResetSpySelectionStartSpySelection()OnSpySelectionComplete()CompleteSpySelection()AutoPickMinisterSpy()AutoAssignSoldierSpies()
- ✅
PitchTrainingController旧 SpySelection 字段已清理
Story 5:回合切换与 game over 收口(部分)
- ✅
PitchEnvController.CompleteTurn(agent)已作为统一回合收尾入口 - ✅
ExecuteAgentStep()与HandleRevealSpy()共用同一回合收尾链 - ✅
_episodeTerminationInProgress防重入闸门已加入 - ✅
ComputeGameOverReward()/ComputeDrawReward()均已有双重结算保护 - ✅
ResetEpisode()重置时清空_episodeTerminationInProgress
Story 6:Reward 清理(主体完成)
已删除的旧常量/字段:
- ✅
PENALTY_SKIP - ✅
REWARD_MINISTER_SPY_FEED_SUCCESS - ✅
PENALTY_MINISTER_SPY_CAPTURED - ✅
REWARD_TACTIC_ALIGN - ✅
PENALTY_TACTIC_VIOLATE - ✅
_ministerSpyUpgraded - ✅
_baitDeployed - ✅
ComputeStepReward()中"目标在 spy list → 追加 reveal reward / spy captured penalty"旧逻辑
当前保留的 reward 体系:
- 核心:
win/loss/draw - 吃子/晋级:
kill / upgrade / advance / center / check - 间谍:
reveal及其延迟/过早/劣势揭露相关奖励 - 士兵间谍:
soldier spy hidden/snowball penalty - 军官系统:
upgrade officer/priest/censor/soldier reached zone等
Story 1:统一 episode/reset 控制链(部分)
- ✅
PitchEnvController作为唯一重置入口(_resetGameInProgress锁) - ✅
PitchAgent.RequestEndEpisode()有_hasRequestedEndEpisode防重入 - ✅ 双 Agent 场景下先触发者执行真实重置,后续者跳过
⚠️ 已实现但待运行验证
A. 生命周期收口(代码已就绪,运行时验证)
PitchEnvController已作为唯一中枢CompleteTurn()已统一收尾_episodeTerminationInProgress防重入已就位- ✅ 代码层面确认完成,需实际训练验证:
- [ ] 每局只出现一套 reset 流程
- [ ] 不再出现双重
EndEpisode - [ ] 不再出现重复请求首手
C. Move / Reveal 共用收尾链(代码已就绪,运行时验证)
CompleteTurn()统一处理切边HandleRevealSpy()走同一链路- ✅ 代码层面确认完成,需实际训练验证:
- [ ] reveal 执行后严格切边(对方回合)
- [ ] GameOver / Draw 只有单一结算入口
E. 观测与 scene 参数一致性
CollectObservations()维度约 230 维,已与新架构对齐PitchPerception已接入EnvController.IsSpyUncovered()- ✅ 代码层面确认完成,需 Unity 内复核:
- [ ]
CollectObservations()输出维度与 BehaviorParameters 匹配 - [ ]
PitchPerception不再依赖废弃 phase 输入 - [ ]
DecisionRequester参数与新回合制逻辑匹配(TakeActionsBetweenDecisions变化需确认)
- [ ]
F. 128 动作上限实测
PitchActionSpace.MAX_LEGAL_ACTIONS = 128已设定- ✅ 代码层面就绪,需训练日志统计:
- [ ] 每回合 legal action 数量
- [ ] 单局峰值
- [ ] 128 是否截断过合法动作
? 待进一步确认
B. PitchTrainingController 完全废弃
OnResetGame()已改为直接进入Playing- 旧 SpySelection 流程函数已删除
- 但
TrainingPhase枚举仍保留SpySelection(未从枚举中移除,仅不再使用) - ⚠️ 若确定完全不需要,可从枚举中移除
SpySelection;当前保留无副作用
? 最小验证(未进行)
以下验证项尚未执行,为后续强制检查点:
- [ ] Unity 编译通过
- [ ] 场景能正常进入训练局
- [ ] 第一局 Red 先手走子
- [ ] reveal 占一整回合(执行后对方回合)
- [ ] 红蓝严格轮替
- [ ] 无"棋盘静止但日志推进"假象
- [ ] 无双重 reset / 双重 EndEpisode
更新后结论
本次代码核查确认:
- 动作空间骨架 ✅ 已完成且方向正确
- SpySelection 退出决策流 ✅ 已完成
- 回合收尾统一 ✅ 已完成
- Reward 清理 ✅ 主体已完成
- 双重 EndEpisode 防护 ✅ 已就位
剩余工作:E(F/G)项均为运行验证,非代码实现。这是下一步的强制检查点。
下一步建议顺序
- 最小验证(G 项):Unity 编译 + 训练局运行,取得第一轮可验证证据
- 动作上限实测(F 项):基于训练日志确认 128 是否够用
- 观测一致性复查(E 项):Unity 内确认 DecisionRequester 参数
- TrainingController 完全清理(B 项):从枚举中移除
SpySelection(可选)
所有内容仅供学习与交流,转载须标明链接。未经同意,禁止作为商业用途,有特殊需求请联系站长。
