5.4 通用博弈(General Game Playing)
本节定位:前面几节讲的是"脑怎么想、模型怎么训"。这一节换一个角度问一个很朴素的工程问题——象棋、五子棋、围棋的"下棋流程"几乎一模一样,我们能不能不为每种棋各写一套,而是写一套"通用的",换棋只换规则? 这个想法在学术界有个正式名字,叫通用博弈(General Game Playing,简称 GGP)。把它讲透,就明白了我们项目为什么要做"通用对弈 skill",而象棋只是它的第一个适配器。
这一节怎么读:先从直觉出发(三种棋流程一样)→ 引出通用博弈这个领域和它的规则语言 GDL → 把"一盘棋到底怎么跑"的通用循环一步步拆开 → 讲清"通用循环 + 每棋适配器"的框架思路和对应的设计模式 → 看两个能落地的开源范本(OpenSpiel、Game Reasoning Arena)→ 最后落回我们自己的 Soma / Anima 项目。每个概念都先用大白话讲清楚,再上术语。
一、问题引入:三种棋,其实是同一套流程
先观察一件事。不管你下的是象棋、五子棋还是围棋,一个回合里你脑子里跑的步骤几乎完全一样:
- 看对方走了什么——对手刚落了一子 / 走了一步。
- 查规则、列出我现在能走哪些——按这种棋的规则,算出此刻所有"合法着法"。
- 自己决策,选一步——从合法着法里挑一个(靠引擎算、靠经验、或者随便选)。
- 落子,推进局面——把这一步走出去,棋盘进入新状态。
- 核对终局——这一步走完,棋是不是分出胜负 / 和了?没有就轮到对方,循环继续。
这五步,对三种棋是一字不差的。真正不同的,只是"每一步格子里的细节"——象棋的合法着法要考虑将军规则,围棋要考虑打劫和禁着点,五子棋只要是空格就能下。把它画出来看得更清楚:
既然流程是同一套,那为每种棋各写一遍"看→查→选→走→核对"的下棋程序,就是在重复劳动。一个工程师的本能反应是:能不能把"通用流程"写一次,把"每种棋的规则细节"抽出来单独喂进去? 这正是通用博弈这个领域要回答的问题。
二、通用博弈这个领域:规则与流程分离
通用博弈(General Game Playing,GGP)是人工智能里的一个研究方向,最早由斯坦福大学推动、还办过比赛。它的核心思想一句话就能说清:
做一个通用对弈程序——它事先并不知道要玩哪种棋,临场读一份用统一语言写的"规则说明书",就能把这盘棋玩下来。
对比一下就懂它的厉害:传统的象棋程序(比如深蓝、Stockfish)是"专才",规则写死在代码里,换成围棋就彻底用不了。而通用博弈程序是"通才"——你今天给它一份象棋规则它就下象棋,明天换一份井字棋规则它就下井字棋,程序本身一行不改。
2.1 规则说明书用什么写:GDL
要让"规则"能被临场读进来,就得有一种专门描述游戏规则的语言。这门语言叫 GDL(Game Description Language,游戏描述语言)。你可以把它理解成"一份结构化的棋规说明书",它用几条标准条目,把一种棋的全部规则声明出来:
- role(玩家角色):这盘棋有哪些玩家?比如黑方、白方。
- init(初始局面):开局时棋盘长什么样?棋子摆在哪?
- legal(合法着法):在某个局面下、轮到某个玩家时,他能走哪些步?
- next(走一步后的局面):某玩家走了某一步之后,棋盘变成什么样?
- terminal(终局判定):当前局面是不是已经分出胜负 / 该结束了?
- goal(得分):终局时,每个玩家各得几分(赢 100、输 0、和 50 之类)?
把这六类条目填满,一种棋的规则就完整描述出来了。注意这里最关键的一点:GDL 描述的是"规则",它本身不会下棋。下棋的本事在另一头——那个通用对弈程序。
一句话抓住本质:规则是"数据"(可以随时换一份),下棋流程是"代码"(写一次、所有棋共用)。 这个"分离"就是通用博弈全部价值所在——也是后面所有工程框架的地基。
(顺带一提:除了下棋,"一个框架装下成千上万种游戏"在学术界也已经做出了规模化的例子,比如 Ludii 这个系统,用一套通用描述就装下了上千种桌游和棋类。这进一步说明"规则与流程分离"是站得住脚的工程路线。)
三、详细的博弈过程:一盘棋到底怎么跑
现在把那个"通用下棋流程"放慢镜头,一步步看它怎么把一盘棋从开局跑到终局。这套流程业界叫通用回合循环(turn loop)。它从初始局面开始,反复转下面这个圈:
- 现在轮到谁走? 问当前局面:该哪个玩家了。术语叫
current_player(当前玩家)。 - 列出他所有合法着法。 问当前局面:此刻这个玩家能走哪些步。术语叫
legal_actions(合法着法列表)。 - 决策者从中选一步。 把这串合法着法交给"决策者"——可以是棋类引擎(Stockfish、KataGo)、可以是一个大模型 LLM、也可以是人——让它挑一个。
- 落子,推进局面。 把选中的这一步走出去,局面进入新状态。术语叫
apply_action(应用着法)。 - 判断是否终局。 问新局面:分出胜负 / 该结束了吗?术语叫
is_terminal(是否终局)。- 没终局:换人,回到第 ① 步,继续下一回合。
- 已终局:给各方算收益(谁赢谁输各得几分),术语叫
returns(收益 / 回报),然后这盘棋结束。
请特别留意:这个循环里没有任何一句话提到"象棋""围棋"。它只跟"当前局面"打交道,向它问五个问题(轮到谁、有哪些合法着法、走完变什么样、终局没、得几分)。到底是哪种棋,被完全隐藏在"局面"背后了。 这就是为什么换棋不用改循环——循环根本不知道自己在下什么棋。
四、框架思路:通用循环 + 每棋适配器
把第三节那个"局面会回答五个问题"的设想,落成真正的软件结构,就得到通用博弈框架的标准长相:一个通用层(写一次的回合循环)+ 一堆适配器(每种棋一个)。
4.1 两层各管什么
- 通用层:只懂"回合怎么转"——就是上面那个循环。它写一次,所有棋共用,从不关心是哪种棋。它只会通过一套统一接口向"局面"提问。
- 适配器层:每种棋一个适配器,负责把这种棋的规则、棋盘表示、着法表示、终局判定、以及接哪个引擎,全部实现成那套统一接口。换句话说,每种棋的所有"特殊性"都下沉到它自己的适配器里,通用层一概看不见。
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",象棋只是第一个适配器;以后加棋只写适配器,复用整套对弈编排。
参考来源
- General Game Playing(通用博弈)概念:en.wikipedia.org/wiki/General_game_playing
- GDL(游戏描述语言):en.wikipedia.org/wiki/Game_Description_Language
- OpenSpiel(DeepMind,Apache-2.0):github.com/google-deepmind/open_spiel · 接口与示例:openspiel.readthedocs.io/en/latest/concepts.html
- Game Reasoning Arena(CC BY-NC 4.0,仅限非商业):github.com/SLAMPAI/game_reasoning_arena · 论文 arXiv:2508.03368
- Ludii(一套框架装下上千种桌游 / 棋类的规模化例子):arXiv:1905.05013