Soma Zero Tutorials
🔍 搜索功能尚未开启,敬请期待。

5.4 通用博弈(General Game Playing)

本节定位:前面几节讲的是"脑怎么想、模型怎么训"。这一节换一个角度问一个很朴素的工程问题——象棋、五子棋、围棋的"下棋流程"几乎一模一样,我们能不能不为每种棋各写一套,而是写一套"通用的",换棋只换规则? 这个想法在学术界有个正式名字,叫通用博弈(General Game Playing,简称 GGP)。把它讲透,就明白了我们项目为什么要做"通用对弈 skill",而象棋只是它的第一个适配器。

这一节怎么读:先从直觉出发(三种棋流程一样)→ 引出通用博弈这个领域和它的规则语言 GDL → 把"一盘棋到底怎么跑"的通用循环一步步拆开 → 讲清"通用循环 + 每棋适配器"的框架思路和对应的设计模式 → 看两个能落地的开源范本(OpenSpiel、Game Reasoning Arena)→ 最后落回我们自己的 Soma / Anima 项目。每个概念都先用大白话讲清楚,再上术语。


一、问题引入:三种棋,其实是同一套流程

先观察一件事。不管你下的是象棋、五子棋还是围棋,一个回合里你脑子里跑的步骤几乎完全一样:

  1. 看对方走了什么——对手刚落了一子 / 走了一步。
  2. 查规则、列出我现在能走哪些——按这种棋的规则,算出此刻所有"合法着法"。
  3. 自己决策,选一步——从合法着法里挑一个(靠引擎算、靠经验、或者随便选)。
  4. 落子,推进局面——把这一步走出去,棋盘进入新状态。
  5. 核对终局——这一步走完,棋是不是分出胜负 / 和了?没有就轮到对方,循环继续。

这五步,对三种棋是一字不差的。真正不同的,只是"每一步格子里的细节"——象棋的合法着法要考虑将军规则,围棋要考虑打劫和禁着点,五子棋只要是空格就能下。把它画出来看得更清楚:

三种棋共用同一套下棋流程
图 5.4.0 三种棋的"步骤格"一一对齐——流程是同一套,只有每格里的"棋规细节"不同

既然流程是同一套,那为每种棋各写一遍"看→查→选→走→核对"的下棋程序,就是在重复劳动。一个工程师的本能反应是:能不能把"通用流程"写一次,把"每种棋的规则细节"抽出来单独喂进去? 这正是通用博弈这个领域要回答的问题。


二、通用博弈这个领域:规则与流程分离

通用博弈(General Game Playing,GGP)是人工智能里的一个研究方向,最早由斯坦福大学推动、还办过比赛。它的核心思想一句话就能说清:

做一个通用对弈程序——它事先并不知道要玩哪种棋,临场读一份用统一语言写的"规则说明书",就能把这盘棋玩下来

对比一下就懂它的厉害:传统的象棋程序(比如深蓝、Stockfish)是"专才",规则写死在代码里,换成围棋就彻底用不了。而通用博弈程序是"通才"——你今天给它一份象棋规则它就下象棋,明天换一份井字棋规则它就下井字棋,程序本身一行不改

2.1 规则说明书用什么写:GDL

要让"规则"能被临场读进来,就得有一种专门描述游戏规则的语言。这门语言叫 GDL(Game Description Language,游戏描述语言)。你可以把它理解成"一份结构化的棋规说明书",它用几条标准条目,把一种棋的全部规则声明出来:

  • role(玩家角色):这盘棋有哪些玩家?比如黑方、白方。
  • init(初始局面):开局时棋盘长什么样?棋子摆在哪?
  • legal(合法着法):在某个局面下、轮到某个玩家时,他能走哪些步
  • next(走一步后的局面):某玩家走了某一步之后,棋盘变成什么样
  • terminal(终局判定):当前局面是不是已经分出胜负 / 该结束了?
  • goal(得分):终局时,每个玩家各得几分(赢 100、输 0、和 50 之类)?

把这六类条目填满,一种棋的规则就完整描述出来了。注意这里最关键的一点:GDL 描述的是"规则",它本身不会下棋。下棋的本事在另一头——那个通用对弈程序。

把游戏规则和通用下棋流程分离
图 5.4.1 通用博弈的核心:把"游戏规则"(左,用 GDL 写的说明书)和"通用下棋流程"(右,写一次的程序)彻底分开

一句话抓住本质规则是"数据"(可以随时换一份),下棋流程是"代码"(写一次、所有棋共用)。 这个"分离"就是通用博弈全部价值所在——也是后面所有工程框架的地基。

(顺带一提:除了下棋,"一个框架装下成千上万种游戏"在学术界也已经做出了规模化的例子,比如 Ludii 这个系统,用一套通用描述就装下了上千种桌游和棋类。这进一步说明"规则与流程分离"是站得住脚的工程路线。)


三、详细的博弈过程:一盘棋到底怎么跑

现在把那个"通用下棋流程"放慢镜头,一步步看它怎么把一盘棋从开局跑到终局。这套流程业界叫通用回合循环(turn loop)。它从初始局面开始,反复转下面这个圈:

  1. 现在轮到谁走? 问当前局面:该哪个玩家了。术语叫 current_player(当前玩家)。
  2. 列出他所有合法着法。 问当前局面:此刻这个玩家能走哪些步。术语叫 legal_actions(合法着法列表)。
  3. 决策者从中选一步。 把这串合法着法交给"决策者"——可以是棋类引擎(Stockfish、KataGo)、可以是一个大模型 LLM、也可以是人——让它挑一个。
  4. 落子,推进局面。 把选中的这一步走出去,局面进入新状态。术语叫 apply_action(应用着法)。
  5. 判断是否终局。 问新局面:分出胜负 / 该结束了吗?术语叫 is_terminal(是否终局)。
    • 没终局:换人,回到第 ① 步,继续下一回合。
    • 已终局:给各方算收益(谁赢谁输各得几分),术语叫 returns(收益 / 回报),然后这盘棋结束。
通用回合循环流程图
图 5.4.2 通用回合循环:轮到谁 → 列合法着法 → 选一步 → 落子推进 → 判终局;没终局就换人继续,终局就结算收益

请特别留意:这个循环里没有任何一句话提到"象棋""围棋"。它只跟"当前局面"打交道,向它问五个问题(轮到谁、有哪些合法着法、走完变什么样、终局没、得几分)。到底是哪种棋,被完全隐藏在"局面"背后了。 这就是为什么换棋不用改循环——循环根本不知道自己在下什么棋。


四、框架思路:通用循环 + 每棋适配器

把第三节那个"局面会回答五个问题"的设想,落成真正的软件结构,就得到通用博弈框架的标准长相:一个通用层(写一次的回合循环)+ 一堆适配器(每种棋一个)

4.1 两层各管什么

  • 通用层:只懂"回合怎么转"——就是上面那个循环。它写一次,所有棋共用,从不关心是哪种棋。它只会通过一套统一接口向"局面"提问。
  • 适配器层:每种棋一个适配器,负责把这种棋的规则、棋盘表示、着法表示、终局判定、以及接哪个引擎,全部实现成那套统一接口。换句话说,每种棋的所有"特殊性"都下沉到它自己的适配器里,通用层一概看不见。
通用层与适配器层的分工
图 5.4.3 通用层(绿,写一次的循环)通过统一接口对话每棋适配器(象棋 / 围棋 / 五子棋各一个),新增一种棋只写一个适配器

4.2 对应的软件设计模式

这套结构不是凭空发明,它正好对应三个经典的软件设计模式(设计模式 = 前人总结的"常见问题的标准解法"):

  • 策略模式(Strategy):通用层把"具体怎么算合法着法、怎么判终局"当成一个可替换的"策略"插进来;换棋就是换策略,调用方(循环)不变。
  • 适配器模式(Adapter):每种棋背后往往是一个现成的引擎,而各家引擎说的是不同"方言"(通信协议)。适配器的活就是把这些方言翻译成统一接口
  • 插件模式(Plugin):新增一种棋 = 往框架里"插"一个新适配器,主程序不用动。

第二条(适配器翻译方言)值得展开。当我们让每种棋背后接现成的强引擎时,麻烦在于各家引擎的"接口语言"五花八门:

  • 象棋引擎 Stockfish 说的是 UCI 协议(Universal Chess Interface)。
  • 围棋引擎 KataGo 说的是 GTP 协议(Go Text Protocol)。
  • 五子棋 又是另一套自家约定。

这些协议互不相通。适配器就是中间的翻译官:通用层永远只说"给我合法着法 / 帮我走这一步"这一种标准话,适配器负责把它翻成 Stockfish 听得懂的 UCI、或 KataGo 听得懂的 GTP,再把引擎的回答翻回标准话。这样通用层就被彻底隔离在"引擎方言"之外了。

4.3 通用层 vs 适配器层 · 边界对照表

关注点归"通用层"(写一次,所有棋共用)归"适配器层"(每种棋写一个)
回合怎么转✅ 轮到谁 → 列着法 → 选 → 落子 → 判终局 → 结算
这种棋的规则✅ 将军 / 打劫 / 连五 等
棋盘怎么表示✅ 8×8 棋格 / 19×19 交叉点 / …
着法怎么表示✅ e2e4 / 落子坐标 / …
合法着法怎么算只负责"调用接口拿到"✅ 负责"按本棋规则算出"
终局怎么判只负责"调用接口问一下"✅ 负责"按本棋规则判定"
接哪个引擎、说什么协议✅ Stockfish/UCI · KataGo/GTP · …
谁来决策(引擎 / LLM / 人)✅ 由通用层统一调度提供"候选着法"供决策

记住这张表的"分界线"就抓住了全部要领:凡是"所有棋都一样"的东西,归通用层;凡是"这种棋特有"的东西,归它自己的适配器。


五、开源范本一:DeepMind OpenSpiel

上面讲的是思路。OpenSpiel(DeepMind 开源,许可证 Apache-2.0,地址 github.com/google-deepmind/open_spiel)把这套思路做成了真正能跑的框架,是学界事实上的通用博弈研究平台,里面已经实现了几十上百种游戏。

5.1 它怎么体现"通用接口"

OpenSpiel 里所有游戏,都实现同一套接口,主要是两个对象:Game(一种游戏)State(一个局面)。它们暴露的方法,几乎和我们第三节列的"五个问题"一一对应:

OpenSpiel 接口它回答的问题(就是第三节那五个)
game.new_initial_state()给我这盘棋的初始局面(init)
state.current_player()现在轮到谁走
state.legal_actions()当前玩家有哪些合法着法
state.apply_action(a)走 a 这一步,把局面推进到下一状态
state.is_terminal()是否终局
state.returns()终局时各方得几分(收益)
state.observation_tensor()把当前局面编码成一串数字,喂给神经网络看

因为所有游戏都长着同一张"接口脸",所以那些通用的对弈算法——MCTS(蒙特卡洛树搜索)、minimax(极小化极大搜索)、AlphaZero——只要写一次,就能拿去跑任意一种已注册的棋。算法只跟这套接口打交道,根本不在乎背后是井字棋还是围棋。

5.2 通用循环长什么样(伪代码)

它官方教程里那段"把一盘棋走到底"的通用循环,几乎就是第三节那张流程图的逐字翻译:

# 通用循环:任何已注册的棋都跑这一段,循环本身不认识"棋"
state = game.new_initial_state()        # 初始局面
while not state.is_terminal():          # 没终局就一直转
    player  = state.current_player()    # ① 轮到谁
    actions = state.legal_actions()     # ② 有哪些合法着法
    action  = choose(player, actions)   # ③ 决策者选一步(引擎/LLM/人/随机)
    state.apply_action(action)          # ④ 落子,推进局面
returns = state.returns()               # ⑤ 终局:各方收益

"换个 --game= 就玩别的棋":OpenSpiel 自带一个 MCTS 例子程序,你只要把启动参数从 --game=tic_tac_toe(井字棋)改成 --game=chess(国际象棋)、--game=go(围棋)、--game=connect_four(四子棋)……上面这段循环代码一行都不用改,就能玩另一种棋。这就是"通用循环 + 每棋适配器"最直观的证据。


六、开源范本二:Game Reasoning Arena

如果说 OpenSpiel 解决的是"用一套接口装下很多棋",那 Game Reasoning Arena(开源,地址 github.com/SLAMPAI/game_reasoning_arena,对应论文 arXiv:2508.03368)解决的是更贴近我们项目的问题:让"一个大模型 LLM"去玩多种棋。它是 2025 年的框架,建在 OpenSpiel 之上,专门用来考察大模型的策略推理能力。

它的两个关键设计正好就是我们要的范本:

  • 用 liteLLM 统一接上百家大模型:liteLLM 是一个"模型接口适配层",把上百家大模型供应商(OpenAI、Groq、Together 等)的不同调用方式,统一成一种调用法。于是换模型只是换个名字,上层逻辑不动——这本身就是"适配器模式"在大模型这一侧的又一次应用。
  • 一个 LLM agent,玩多种棋:它支持井字棋、四子棋、Kuhn 扑克、石头剪刀布、Hex、国际象棋等近十种游戏。LLM 的决策逻辑只写一次——靠 OpenSpiel 的统一接口拿到当前局面和 legal_actions(合法着法),把它们组织成提示词喂给大模型,让大模型"推理着选一步"再走出去。换棋时,LLM 这边的代码不变,变的只是底层 OpenSpiel 加载了哪种棋的适配器。

这恰好就是我们项目想要的形态:LLM 大脑(决策者)+ 通用对弈循环 + 每棋适配器。它给出了一个现成的、可参考的工程答案。

⚠️ 许可证提醒:Game Reasoning Arena 采用 CC BY-NC 4.0 许可证,其中 NC = Non-Commercial,仅限非商业用途。也就是说,可以学习、参考、做学术 / 非营利使用,但不能直接拿它的代码做商业产品。我们引用它的设计思路没问题,但若要进商业产品线,得自己实现、或选 OpenSpiel(Apache-2.0,商用友好)这类许可证宽松的底座。


七、落到我们项目:通用对弈 skill,象棋是第一个适配器

现在把上面这一切收回到我们自己的 Soma / Anima 项目上,就明白了一个之前看起来"想多了"的决定为什么是对的——我们要做的不是"一个象棋程序",而是"一个通用对弈 skill(对弈能力),象棋只是它的第一个适配器"。

按通用博弈的两层结构,套到我们的体系上是这样分工的:

  • 世界 / 接口(统一接口):对应通用博弈里那套"向局面问五个问题"的标准接口。我们的对弈能力只通过它和具体棋打交道。
  • skill(对弈能力 = 通用回合循环):对应通用层。"看对方走 → 列合法着法 → 让决策者选 → 落子 → 判终局"这套编排写一次,被所有棋复用。决策者可以是 Stockfish、可以是 LLM 大脑(这正是第六节 Game Reasoning Arena 的路子)。
  • 适配器(每种棋一个):对应适配器层。象棋适配器接 Stockfish(UCI)、持有 python-chess 当逻辑真值;以后加五子棋、围棋,只写新适配器(围棋接 KataGo / GTP,等等),整套对弈编排原封不动地复用。

这样一来,"以后能不能下围棋 / 五子棋"就从一个"要重写多少东西"的大工程,变成了"再写一个适配器"的小工程。这就是通用博弈这个概念给我们项目带来的真正价值。

本节聚焦说明:这一节只讲"通用博弈"这个概念本身,以及它为什么值得我们采用。至于"通用对弈 skill 在 Anima 编排里具体怎么挂、和 World / 感知闭环怎么接",属于项目实现细节,见4.2 顶层框架与 Anima 编排4.9 挑战课题:吸盘工具更换下围棋/五子棋。本节给出的是这套设计背后的"为什么"。


八、本节小结

  • 观察:象棋、五子棋、围棋的下棋流程(看对方走 → 查合法着法 → 决策 → 落子 → 核对终局)几乎完全一样。
  • 领域:通用博弈(GGP)就是要做"事先不知道玩哪种棋、临场读规则就能玩"的通用程序;规则用 GDL(role / init / legal / next / terminal / goal)描述。核心是"规则"与"流程"分离
  • 框架通用层(写一次的回合循环)+ 每棋适配器(把本棋规则和引擎方言翻成统一接口),对应策略 / 适配器 / 插件三个设计模式。
  • 范本OpenSpiel(Apache-2.0)用一套 Game / State 接口装下几十上百种棋,MCTS / minimax / AlphaZero 写一次跑所有棋,换 --game= 即可;Game Reasoning Arena(CC BY-NC 4.0,仅非商业)在它之上用 liteLLM 让一个 LLM agent 玩多种棋。
  • 落地:我们做"通用对弈 skill",象棋只是第一个适配器;以后加棋只写适配器,复用整套对弈编排。

参考来源