FunnyPitch ML-Agents 完整重构方案(2026-05-18)

本次重构针对 FunnyPitch 的 ML-Agents 训练体系,目标不是"继续修补现有流程",而是重新整理一套适合严格回合制棋类 + 间谍揭露动作的训练架构。涉及动作空间重设、episode/reset 链路清理、reveal 建模等核心问题。

最后编辑时间:瑶华

FunnyPitch ML-Agents 完整重构方案(2026-05-18)

1. 背景与目标

本次重构针对 FunnyPitch 的 ML-Agents 训练体系,目标不是"继续修补现有流程",而是重新整理一套适合严格回合制棋类 + 间谍揭露动作的训练架构

已确认的问题

  1. 非法动作是结构性产物

    • 当前动作空间是 piece + target + special 三分支。
    • piecetarget 各自可能合法,但组合起来可能非法。
    • 导致日志里会出现 action 在跑、步数在涨,但棋盘没有真实移动。
  2. episode/reset 链路不干净

    • 第一局结束后曾出现双重 reset / 双重 EndEpisode 的迹象。
    • 这会污染第二局开局状态,让训练表面继续、实则状态错乱。
  3. reveal 的建模不够清晰

    • reveal 不是普通移动,但也不是回合外插入动作。
    • 它本质上更像是"占用一个整回合的一次性特殊行动"。
  4. 训练层和规则层耦合过深

    • PitchAgentPitchEnvControllerPitchTrainingControllerPitchManager 都在部分控制回合/结束/开局。
    • 对严格回合制游戏来说,这很危险。

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)

根本问题

这套设计天然存在:

  • piece branch 单独合法
  • target branch 单独合法
  • piece + target 组合非法

这不是一个 patch 能彻底解决的问题,而是编码方式本身不贴合棋类动作结构

3.3 Reveal 的语义冲突

reveal 当前被当成特殊动作分支的一项,但它其实不是"附属行为",而是:

  • 需要在当前方回合内决定是否使用
  • 使用后消耗行动机会
  • 使用次数有限(每边每局一次)
  • 对信息态产生永久改变

因此 reveal 更接近:

一个受状态机控制的一次性合法行动

而不是"普通移动旁边顺手加个开关"。


4. 重构后的目标架构

4.1 统一控制权

新职责划分

模块 重构后职责
PitchAgent 只负责 ML 接口:CollectObservations / OnActionReceived / EndEpisode
PitchEnvController 唯一的训练/回合/episode 中枢
PitchManager 继续作为规则与棋盘执行核心
PitchTrainingController 删除或废弃
PitchActionMasker 改写为基于合法动作列表的 mask
PitchPerception 保留,继续负责观测编码

中枢原则

以后只能由 PitchEnvController 负责:

  1. reset episode
  2. 自动选间谍
  3. 设定当前方
  4. 请求当前方决策
  5. 执行动作
  6. 结算 game over / draw
  7. 触发 EndEpisode

其他模块不再各自管理阶段切换。


4.2 新回合状态机

重构后状态应简化为:

  • Resetting
  • Playing
  • GameOver

不再保留训练专用的 SpySelection 阶段进入决策流。

Episode 流程

  1. OnEpisodeBegin
  2. EnvController.ResetEpisode()
  3. PitchManager.ResetGame()
  4. 自动随机指派红蓝间谍
  5. reveal 次数清零
  6. CurrentSide = Red
  7. 进入 Playing
  8. 枚举当前方全部合法动作
  9. 当前方选择一个 action
  10. 执行 action
  11. 若终局则结算,否则切边继续

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

这样做的收益

  1. 不再有组合非法动作
  2. reveal 可以自然作为合法 action 之一
  3. mask 逻辑大幅简化
  4. 日志更直观,方便排查

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 做 mask
  • PitchAgent.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. 验收标准

本次完整重构完成时,至少要满足:

  1. 训练可连续运行多盘,不会出现"棋盘静止但日志继续刷"的假推进。
  2. 每盘只 reset 一次。
  3. 红蓝严格轮替。
  4. reveal 是合法回合行动,执行后正确切边。
  5. 不再出现 piece + target + special 三分支动作恢复逻辑。
  6. 不再需要用"连续非法动作安全网"来掩盖结构问题。
  7. 文档、代码、训练配置一致。

10. 本次设计结论

结论很明确:

  • 不需要推倒整个项目;
  • 但需要对训练体系做一次核心重构;
  • 核心重构点不是 reward,而是:
    1. 回合状态机统一
    2. 动作空间改为统一合法动作列表
    3. 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.unityinference_config.yaml 已同步到 1 个 discrete branch / 128 容量

Story 2:移除 spy selection 训练阶段

  • PitchTrainingController.OnResetGame() 直接进入 Playing,SpySelection 已废弃
  • PitchTrainingController 中以下旧函数已删除:
    • ResetSpySelection
    • StartSpySelection()
    • 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)项均为运行验证,非代码实现。这是下一步的强制检查点。


下一步建议顺序

  1. 最小验证(G 项):Unity 编译 + 训练局运行,取得第一轮可验证证据
  2. 动作上限实测(F 项):基于训练日志确认 128 是否够用
  3. 观测一致性复查(E 项):Unity 内确认 DecisionRequester 参数
  4. TrainingController 完全清理(B 项):从枚举中移除 SpySelection(可选)
  • 留下精彩的评论吧~(0条)

所有内容仅供学习与交流,转载须标明链接。未经同意,禁止作为商业用途,有特殊需求请联系站长。