部落象棋:楚河汉界是一道悬崖
部落象棋是只有兵的一方对阵完整一军的中国象棋:一大群兵,没有将,对面是全军。兵团靠将死取胜;全军靠吃掉最后一个兵取胜。我们先设计了它,在动手做任何东西之前用引擎把每一个版本都下了一遍。没有一个版本是一盘棋。
兵的走法就是整个设计空间;兵数、起始行、阵形、无吃子限制都只是旋钮。用象棋自己的兵,全军从己方半场的任何一种开局都能赢下每一局,靠的是同一招。让兵从第一步起就有过河兵的走法,这一招就没了,但棋局变成了一场围城,四局里有三局没人赢。河的这边兵阵守不住自己;河的那边全军攻不动它。
规则
原版是部落国际象棋:36 个兵、没有王,对阵正常的一军,2015 年起在 Lichess 上可玩。此前没有人把它搬到象棋棋盘上(检索过程在文末的注记里)。这次移植尽量保留象棋原有的一切。
- 红方是兵团:只有兵,没有将。红方先走。黑方是标准全军。
- 红方靠将死取胜。黑方靠吃光红方所有棋子取胜。兵团不存在被将军一说,也没有白脸将规则,因为棋盘上只有一个将。
- 轮到走棋却无子可动的一方判负,和象棋一样。(Lichess 的部落棋判和;这一差别有多大,下面的数字会说明。)
- 三次重复局面判和;连续 60 步或 120 步无吃子也判和。
- 标准兵按象棋走法:向前一步,过河后才能横走,永不后退。老兵从第一步起就有过河兵的走法。无论哪种,全军的五个兵都保持标准走法。
设计空间
五个旋钮。一种设计是一种兵的走法加一种开局阵形;共下了二十四种,其中八种又在第二种无吃子限制下重下了一遍。
| 旋钮 | 试过的设定 |
|---|---|
| 兵的走法 | 标准;老兵(第一步起可横走) |
| 兵的数量 | 18, 27, 31, 32, 36, 40, 45 |
| 兵阵的起始位置 | 靠后(从第 1 行起)、靠前(从第 2 或第 3 行起)、过河(前排在第 6 行) |
| 兵阵的形状 | 整行排满;原版阵形(整行加成对的前哨);象棋原有的五个兵位 |
| 终局规则 | 60 步或 120 步的无吃子限制;困毙判负(象棋)或判和(原版) |
每种设计都跑在未经改动的 Fairy-Stockfish 上,只用一段十二行的配置,不改一行引擎代码,由一个规则内核担任裁判;在读取任何一局之前,这个内核已与引擎的着法生成器在每一步合法着法上达成一致(检验关卡和测量工具见「这些数字有多可信」)。每个棋盘下面的统计是该阵形的四局棋,引擎自战,四种搜索量,没有随机因素;点击可打开 100 万节点的那一局。棋盘上没有红将时引擎无法使用它的象棋神经网络,于是改用传统评估,它给标准开局的估值是全军领先 22 个兵。它照样把棋下完。
标准兵:车从背后吃光兵团
每种阵形在四种搜索量下各下四局;棋盘下面一行是统计。点击任一棋盘可观看它在 100 万节点下的对局。标有 ▶ 的附有讲解。
十二种阵形里有九种四局全归全军,机制只有一个。
标准兵只控制正前方一个点,所以兵团是九根纵列,而不是一堵墙。哪一列一空,车就顺着它落到底线,从背后每着吃一个兵,而兵团里没有任何子能向后攻击。阵形改变不了什么:前哨是靶子,不是掩护。
另外三格是同一种例外:在车吃完之前有足够多的兵过了河。第 1 到 5 行的 45 兵是五行厚的兵阵,结果分化为全军 2 胜、兵团 1 次将死、1 和。开局就在河对岸的两行一上来就与对方接触;27 兵依然输,36 兵则是掷硬币(七种搜索量下兵团 3 胜、全军 2 胜、2 和),它跳过了行军阶段,预演了老兵的棋局。
| 兵数 | 起始位置 | 四局,150,000 到 500,000 节点 | 100 万 | 500 万 |
|---|---|---|---|---|
| 18 | 第 1-2 行 | 全军 4-0,50 到 62 步 | 全军全灭获胜,46 | |
| 18 | 第 3-4 行 | 全军 4-0 | 全军全灭获胜,70 | |
| 27 | 第 1-3 行 | 全军 4-0 | 全军全灭获胜,84 | |
| 27 | 第 2-4 行 | 全军 4-0 | 全军全灭获胜,108 | |
| 27 | 第 4-6 行 | 全军 3 胜,1 和(重复局面,393) | 全军全灭获胜,154 | |
| 36 | 第 1-4 行 | 全军 4-0 | 全军全灭获胜,134 | 全军全灭获胜,156 |
| 36 | 第 2-5 行 | 全军 4-0 | 全军全灭获胜,184 | 全军全灭获胜,146 |
| 36 | 第 3-6 行 | 兵团 2 胜(困毙 207,将死 233),全军 2 胜 | 兵团将死获胜,195 | 重复局面和棋,104 |
| 45 | 第 1-5 行 | 全军 2 胜,兵团 1 胜(将死,287),1 和(无吃子限制,354) | 全军全灭获胜,198 | |
| 32 | 象棋原有五个兵位 | 全军 4-0 | 全军全灭获胜,122 | 全军全灭获胜,106 |
| 31 | 原版阵形 | 全军 4-0 | 全军全灭获胜,112 | |
| 40 | 原版阵形 | 全军 4-0 | 全军全灭获胜,158 |
每格四局,每着 150,000、200,000、300,000 和 500,000 节点,120 步无吃子限制,每一着都是引擎的首选;然后是 100 万节点和 500 万节点各一局(60 步无吃子限制)。空格表示没有跑过。
老兵:兵团守得住,围城是和棋
车之所以有路可走,是因为标准兵不攻击身边的点,而象棋本来就有一种会这样攻击的兵:过河兵,可以向前或横向走一步并吃子。老兵就是一开局便已升变的兵团。每个兵团的兵从第一步起就有这种走法,其他一切不变,棋手不需要学任何新东西。它把行军阶段从棋局里拿掉,让围城从底线就开始。
每种阵形在四种搜索量下各下四局;统计在每个棋盘下面。点击任一棋盘可观看它在 100 万节点下的对局。
这就把路堵上了。一列老兵攻击身边的点,车不能白白停在它旁边,全军的吃子速度从每着一个兵降到每 60 到 100 步一个。兵团成了一方棋,而在势均力敌之间棋局成了围城:48 局,37 和。全军只赢得了 18 兵,27 兵以上它一局也没赢过。
残局。兵团一路推进,顺手吃掉全军的炮和象,到了九宫前发现一个将带着双车在等着。从这里起兵冲不过双车,双车也吃不动互相保护的一片兵阵。起始行和阵形只决定兵团在路上能吃掉多少全军的子,别的什么也决定不了。原版的 31 兵展示了同一条规则的另一面:四局都在 155 到 245 步判和,没有一个兵过河,因为没动过的兵阵攻不了,而一动的兵阵就要丢掉那个动了的兵。
我把 40 兵的残局在第 754 步处取出,让引擎以每方 500 万节点再下 200 步:只吃了一个子,而它自己对兵团优势的评估从 2.3 个兵滑到了 0.7。在 100 万节点下,全军在同一段里吃到了两个子,之后 120 步一个也没有,无吃子限制判和。它正以每八十步一个兵的速度赢这个残局,也就是说它根本赢不了。
那一胜。前移一行的 36 兵吃掉双车,在第 435 步困毙了光将:无子可动,也没有被将军,象棋判负。它五局赢了四局,是兵团占优的唯一一种阵形:离河近一行,到达时全军已经损失更多,剩下的兵也足够用来换掉双车。兵团的其他胜局都是从更大的兵阵走到同样的收官,全都在 390 步之后,六胜里有四胜是困毙。
| 兵数 | 起始位置 | 四局,150,000 到 500,000 节点 | 100 万 | 全军损失子数 |
|---|---|---|---|---|
| 18 | 第 1-2 行 | 全军 2 胜,2 和(无吃子限制 653,重复局面 616) | 全军全灭获胜,258 步 | 9 |
| 18 | 第 3-4 行 | 全军 3 胜,1 和(重复局面,588) | 全军全灭获胜,308 | 10 |
| 27 | 第 1-3 行 | 4 和,无吃子限制,372 到 456 | 无吃子限制和棋,459 | 7 |
| 27 | 第 2-4 行 | 4 和,无吃子限制,354 到 488 | 无吃子限制和棋,490 | 8 |
| 27 | 第 4-6 行 | 4 和,无吃子限制,137 到 233 | 无吃子限制和棋,166 | 9 |
| 36 | 第 1-4 行 | 4 和,无吃子限制,590 到 900 | 无吃子限制和棋,1,058*:9 个兵对将加双车 | 13 |
| 36 | 第 2-5 行 | 兵团 3 胜(将死 473,困毙 475,将死 569),1 和(重复局面,1,033) | 兵团困毙获胜,435 | 15 |
| 36 | 第 3-6 行 | 兵团 1 胜(困毙,395),3 和,无吃子限制,714 到 966 | 重复局面和棋,703:9 个兵对将加双车 | 13 |
| 45 | 第 1-5 行 | 兵团 1 胜(困毙,961),3 和,1,027 到 1,098 | 无吃子限制和棋,1,102*:11 个兵对将、象加双车 | 12 |
| 32 | 象棋原有五个兵位 | 4 和,293 到 394 | 无吃子限制和棋,449 | 7 |
| 31 | 原版阵形 | 4 和,无吃子限制,155 到 245,没有兵过河 | 无吃子限制和棋,247 | 5 |
| 40 | 原版阵形 | 兵团 1 胜(困毙,713),3 和,694 到 1,062 | 无吃子限制和棋,1,016*:10 个兵对将、士加双车 | 12 |
每格四局,每着 150,000、200,000、300,000 和 500,000 节点,每一着都是引擎的首选,120 步无吃子限制,2,000 步上限没有一局碰到;然后是 100 万节点的一局。*三局 100 万节点对局撞上了测试框架的 1,000 步上限,从该局面在无吃子限制下以 200,000 节点继续走;之后没有吃子,所示步数是限制判和的位置。全军损失子数取自 100 万节点的对局。
终局规则
这里的棋局有三种没有胜者的停法,三者并不相同。重复局面:同一局面、同一方走棋出现三次;判和。无吃子限制:连续若干步没有吃子,60 步或 120 步;判和,兵向前走一步并不重置计数。步数上限:测试框架在 400、1,000 或 2,000 步时停止棋局;这不是棋的规则,所以撞上上限的棋局是未完成,不是和棋。三局 100 万节点的老兵对局撞上了 1,000 步上限,之后继续走到无吃子限制判和为止,四种搜索量的对局没有一局达到 2,000 步,所以网格里的每一个结果都是前两种之一。
两个不改变任何走法的旋钮,都是通过重新判分而不是重新下棋来决定的。
无吃子限制。象棋的无吃子规则是 60 步。用标准兵时它在 48 局里只触发了一次。用老兵时它是常态结局:37 和里有 30 和是它,其余七局是重复局面。60 步会切断还在进行的棋局:兵团 100 万节点的那一胜在 400 步上限处未完成,最接近的一局在第 360 步判和时行军还在进行。因此这里的一切都用 120 步。无吃子限制也是引擎照着下的一条规则(它写进了引擎的变体定义),所以 60 步限制下的一局和 120 步限制下的一局从第 2 步起就是两盘不同的棋,而不是同一盘棋在不同位置截断。
困毙。按原版规则,将无子可动且未被将军是和棋。把实验室里的每一局按这条规则重新判分:老兵势均力敌的七胜里有五胜变成和棋,标准兵四胜里有一胜也是;对随机走子的全军,标准兵 35 胜里的 20 胜、老兵 38 胜里的 16 胜变成和棋;梯队对局的十胜里有五胜也一样。困毙差不多占了兵团将死手段的一半,原版规则会把它拿走。
兵为什么失败,换一种兵需要什么
这盘棋有两个阶段,象棋的兵在两个阶段里各以不同的方式失败。
河的这边,兵阵守不住自己。标准兵只攻击正前方一个点。所以兵团是九根纵列,不是一堵墙:车在一列旁边或后面都碰不到,任何一条空出来的线都是通向底线的路。40 兵及以下、从兵团己方半场开始的每一种阵形都在这里由那一个车决定,四局全是。到了 45 兵,兵阵厚到有时能撑过它,那一格结果分化。
河的那边,兵阵动不了。过河兵(或老兵)也能横向攻击,所以兵阵站在原地时能互相保护。但它保护的只是自己的兵能走到的点,永远不是邻兵正前方的那个点。向前走的兵落在一个没人保护的点上,在邻兵跟上来之前就是白送。所以兵阵只能整行一起推进,每行九着,而全军在一旁等着,任何提前动的兵都会被吃掉。全军这边也攻不了没动过的兵阵。十二种老兵设计里有十种就停在这里,每一局或者只差一局:将和双车在九宫里,一片兵阵在九宫外,双方都无事可做。
国际象棋的兵构造不同。它斜着吃子,所以 d4 的兵在 e 兵走到 e5 之前就已经保护着 e5:兵成链推进,一次一个,每一步落点都有保护,而链条可以从根部被破坏。这就是部落棋在国际象棋里成立的原因,也是国际象棋棋手从来不用想这件事的原因。在象棋棋盘上,河是一道悬崖:路上毫无掩护,到了那边掩护全面,兵数、起始行、阵形、无吃子限制的任何设定都改变不了这一点。这些旋钮我们都转过了。我们没有动的那一个,老兵之外,就是兵本身。
下一次尝试就从这里开始。兵团缺的性质是路上的掩护:一个能保护邻兵即将走到的那个点的兵,让兵阵可以一个一个地推进,而不是一行一行地推进。任何能提供这一点的东西都是候选,从小改到大改:直走但斜向前吃子的兵(那个没人保护的点有了保护,而它仍然不是国际象棋的兵)、第一步可走两格、像旧时五虎让子那样每回合走两个兵、走到底线再升变一次,或者干脆用国际象棋的兵,那就把它变成了更大棋盘上的部落国际象棋。我们止步于老兵,因为它是这些里面唯一象棋本来就有的;其余每一种都是新棋子,而新棋子就是另一盘棋,值得另写一篇。规则在实验室里只是数据,所以任何一种都只需一行配置加一个下午的引擎时间,这里的统计就是要超越的基线。
自己跑一遍
整个变体就是 Fairy-Stockfish variants.ini 里的一段配置。下面是老兵版原版阵形加 120 步无吃子限制;要用标准兵,去掉 customPiece1 那一行,并把起始局面里的 V 写成 P。
[hordexiangqi:xiangqi]
startFen = rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/VVVVVVVVV/VVVVVVVVV/VVVVVVVVV/VVVVVVVVV w - - 0 1
customPiece1 = v:fsW
extinctionValue = loss
extinctionPieceTypes = *
flyingGeneral = false
chasingRule = none
stalemateValue = loss
perpetualCheckIllegal = false
nFoldValue = draw
nMoveRule = 60
stockfish check horde.ini # parses the stanza
setoption name VariantPath value horde.ini
setoption name UCI_Variant value hordexiangqi
position startpos
go nodes 1000000
有两行很重要,又很容易漏掉。chasingRule = none 是必需的:引擎的追子检测会去读没有将的一方那个不存在的将的位置。flyingGeneral = false 什么也不改变,只是记录一个事实:引擎把白脸将规则挂在它的王棋上,当一方没有王时会悄悄关掉它。
这些数字有多可信
上面每一格背后有三道检验,按运行顺序列出。
引擎下的确实是这些规则。我写的一个规则内核(跑原子象棋用的同一个,加上无将一方、全灭获胜和老兵的钩子)给每一局当裁判:引擎提出一着,内核验证并执行,一个悄悄下着另一套规则的引擎会让运行中止,而不是留下一份别的棋的记录。在读取任何一局之前,两者必须在每一步合法着法上一致:从每种阵形出发 3 层 perft,九个手工构造的局面按精确着法集合比对,再加 300 个随机走子得到的局面。零分歧,两种兵的走法,全部十二种阵形。
引擎懂这盘棋。100,000 节点下它对随机走子 60-0(标准兵),老兵 80 局赢 78 局。十倍搜索量对十分之一搜索量,标准兵前移阵形 17-1-2,老兵 11-0-9。一条脚注:原版阵形加标准兵时,十倍搜索量只拿到 55%,因为每一局分出胜负的棋都归了全军,不管哪边拿着更大的搜索量;决定结果的是座位,不是搜索。
搜索有多深。用节点数而不是时间,所以不依赖机器:每着 200,000、100 万和 500 万节点,在这些局面上大约达到 12 层、15 到 20 层、24 到 33 层。在这些搜索量下棋力尚未饱和(标准兵过河那一格在七种搜索量下依次是和、全军、兵团、兵团、全军、兵团、和)。标准兵的结论在试过的每一种搜索量下都成立,机制相同,这个结论是可靠的。围城是搜索最不擅长的局面类型:一百步的计划在 20 层深度上看不见,传统评估也完全不知道这个残局值多少(全军正在蚕食堡垒的时候,它给兵团的评估是 +2.3)。所以和棋的结论弱于负局的结论,一个有计划的人在任何一方都可能比引擎下得更好。实验室记录了每一步达到的深度。
一局只是一个样本。每一格都下了四局,每着 150,000、200,000、300,000 和 500,000 节点,每一着都是引擎的首选,没有随机开局,所以棋盘下面那一行是一个比率,可点击的 100 万节点对局是五局之一,不是结论。确定性的引擎从同一起点会重放同一盘棋,所以四局之所以不同全靠搜索量,脚本拒绝接受任何两局完全相同的格子(没有出现过)。哪一格结果分化,分化本身就是结果。关于老兵数字还有一条保留:引擎把老兵当成自定义棋子,其价值按机动性估算而非调校,所以它对兵换子的判断比对标准兵粗糙。它在 500 万节点下攻不破的堡垒是真的;它在进攻路上让掉的子力未必是最佳着法。
现状
对这个系列来说,一个变体算得上一盘棋,要在实测对局中过三道门槛:高手之间能分出胜负而不是默认和棋;没有哪一方从开局就赢定;双方都有事可做。部落象棋试过的所有设计里没有一种过得了任何一道。
- 标准兵:40 兵及以下、从己方半场开始的每一种阵形全军都赢,36 局全胜,靠同一招。能分胜负,但从第一步起就定了。到 45 兵,或者从河对岸开始,那一格是由开局交换决定的掷硬币。
- 老兵:一场两位高手 48 局里 37 局下成和棋的围城,四百到一千步。兵团赢六局,其中四局靠困毙;全军赢五局,全是对 18 兵。分不出胜负,双方也都没有计划:全军的最佳着法是待在九宫里等,兵团的最佳着法是把兵阵一格一格地挪。兵团占优的唯一阵形(第 2 到 5 行的 36 兵,五局四胜)是同一场围城加一个更好的比分,不是另一盘棋。
- 面对弱手兵团很危险(100,000 节点下它对随机走子的全军 30 局全胜,19 局靠困毙),而人比 100 万节点的搜索弱。这让它成了一个陷阱,而不是一盘棋。
什么能重新打开它:一个人类对局的结果、一种能给兵阵路上掩护的兵的走法(候选见上文),或者远高于这里测到的 48 局中 6 次的车被吃比率。就象棋自己的兵而言,目前没有任何迹象指向那里。兵就是设计空间,河是其中的一道悬崖,下一个设计必须改动兵。
前人工作:Fairy-Stockfish 的变体文件、PyChess、Lichess、Chess.com 的官方列表、PlayStrategy、Ludii、中英两个维基百科的象棋变体页、chessvariants.com 的 9x10 索引、知乎上一份 231 种民间象棋变体的清单,以及其中九种以兵命名、在百度百科上有规则的变体:没有一种是部落棋。贴吧的民间变体版块和 Chess.com 的用户自定义变体我读不到,所以这是一个彻底但并非穷尽的否定结论。象棋自己的不对称玩法(满洲棋、五虎、让九子)方向正相反,是从一方拿走棋子,先拿的就是兵。部落国际象棋源自 BrainKing 的 Horde Chess(2002)和 Lord Dunsany 的「Pieces against Pawns」(1942);Horde 的中文是部落。
资源:
- 兵团的胜局,附讲解 - 第 2 到 5 行的 36 个老兵吃掉双车,在第 435 步困毙将
- horde-xiangqi - 证据:24 段配置、全部 387 局引擎对局、裁判内核,以及
npm run verify,它按规则重放每一局并重新计算统计 - 变体实验室 - 产出这些数据的框架:Fairy-Stockfish 检验关卡和各条命令
- scalachess 的部落棋提交 - Lichess 在 2015 年定下的规则
- Fairy-Stockfish - 引擎;整个变体就是
variants.ini里的一段配置,含extinctionValue、customPiece1 = v:fsW和chasingRule = none