部落象棋:楚河漢界是一道懸崖
部落象棋是只有兵的一方對上完整一軍的象棋:一大群兵,沒有將,對面是全軍。兵團靠將死取勝;全軍靠吃掉最後一個兵取勝。我們先設計了它,在動手做任何東西之前就用引擎把每個版本都下過一遍。沒有一個版本是一盤棋。
兵的走法就是整個設計空間;兵數、起始列、陣形、無吃子限制都只是旋鈕。用象棋自己的兵,全軍從己方半場的任何一種開局都能贏下每一局,靠的是同一招。讓兵從第一步起就有過河兵的走法,這一招就沒了,但棋局變成一場圍城,四局裡有三局沒人贏。河的這邊兵陣守不住自己;河的那邊全軍攻不動它。
規則
原版是部落西洋棋: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