在鬥獸棋引擎中測試「衝獸穴競速」評估項
Mistboard 的鬥獸棋引擎沒有「衝獸穴競速」的概念。它以子力加上每個棋子朝敵方獸穴推進的加分來評估局面,卻從不問那個奔向自己獸穴的棋子是否先到。它要等到搜尋抵達獸穴時才知道,大約在十二著之後。
這看起來是個明顯的缺口,是在一局對局之後被記成議題的:當時機器人把一隻鼠來回挪動,而敵方的鼠就這樣走了進來。我花了一天試著補上它。三條路線,結果全是無效或負面。以下是量測告訴我的事,以及我會從中保留的東西。
引擎原本就會做的事
值得先分開講,因為它決定了後面的一切。引擎完全理解勝利條件:進入獸穴被計為將死、在候選著法中排在最前、也會在靜態搜尋中被搜到。它缺的是預見能力,不是理解力。
它照樣會贏,因為鬥獸棋由子力主導,而評估值正好精準地讀出子力。在進入獸穴的那一刻,勝方的子力領先中位數是 20 點,大約一隻貓。只有 4.7% 的對局是由落後的一方贏下。
沒有這個概念時它會怎麼走
這項研究中的第 10 局只有四著長,卻比下面任何表格都更能說明問題。
鬥獸棋的中路是三列水域。d 路是唯一跨越它的陸橋。虎能垂直跳水,卻絕不能橫向跳,所以站在那條通道裡的虎幾乎無處可去。
第 20 著,藍方行棋。紅色方格是剛走的一著:紅方的虎在 d6 吃掉一隻狼,現在站在通道裡。藍方的象在它上方一格,位階高於它。 在第 20 著時,紅方位於 d5 的虎有兩個合法著法。向前或向後,都還在通道裡。它選擇向前吃掉了狼,讓藍方損失 40 點,也把虎停在藍方象的旁邊。
藍方的象位階高於虎,所以這看起來像是送禮。藍方沒有立刻吃它,而這正是我被要求解釋的一著。引擎的理由是虎跑不掉:象在 d7 時,虎唯一的著法是 d5,而從 d5 唯一的著法是 d4,被一個能吃掉它的棋子沿著單線通道趕下去。去吃它只是花一個先手去收已經到手的東西。藍方走了一步免費的改進著法,兩著之後在 d5 而不是 d6 吃掉了虎。兩條路線的子力完全相同。
決定這局勝負的四著:
41 Blue elephant d7 -> d6 steps into the corridor after the tiger
42 Red lion c3 -> c7 jumps into the square the elephant just left
43 Blue elephant d6 -> d5 takes the tiger, up a piece
Red's lion is now three moves from a den Blue cannot defend
虎是誘餌。不是棄子:它本來就幾乎被困住了,而吃掉狼得分 +10,對上幾乎所有其他選擇的 +6,差的是四點,而不是一個計畫。它在離場的路上做到的,是把唯一位階高於獅的棋子拉下 d 路。在第 20 著時,獅的跳躍評估值是 -195,因為 c7 就緊鄰藍方在 d7 的象。兩著之後同樣的跳躍卻能取勝,因為象走開了。
藍方在這段之後領先 13 點,然後輸了。
把這個計畫回頭讀一遍,聽起來像是引擎理解了衝獸穴競速。它並沒有。那份評估裡從頭到尾都沒有任何衝獸穴競速項,這個專案之前沒有,之後也沒有。它搜了五百萬個節點,這條路線就出來了。整個結果就是這樣:那個議題想裝進去的行為早就存在了,是靠深度而不是靠知識產生的。
基礎比率:4.7% 的對局,而且要在出貨強度下量測
以每著五百萬節點的正式預算進行 172 局自我對局。
| 終局方式 | 佔比 |
|---|---|
| 重複局面和棋 | 52.9% |
| 進入獸穴 | 26.2% |
| 無進展和棋 | 20.9% |
| 真正的競速(勝方在十著前仍持平或落後) | 4.7% |
同一套測試框架在 200k 節點下報出 22% 的競速比率。棋力較弱會把這個數字大約放大五倍。便宜的量測本來就做得到,而它會在「證明值得多花工夫」的方向上偏差 5 倍。
上限空間:搜尋深 12 倍只換來兩著
對每一場競速,我在敗方的對局中以二分搜尋找出各個預算最早能辨認出必敗的那一著,然後取其差距。
| 差距 | 競速局數 |
|---|---|
| 0 著 | 4 |
| 2 著 | 3 |
| 4 著 | 1 |
六千萬節點對五百萬節點,較深的搜尋看出敗勢的時間中位數只早了兩著。在那個視野之前,兩者走勢貼得很近,只有在最後一兩著才分歧。
這個數字本來就該讓這個專案結束。如果十二倍的搜尋只換回兩著,那資訊就不是擺在那裡等一個更聰明的評估把它撈出來。
路線 A:搜尋延伸,免費版與付費版
沒有真的實作就先探測。測試框架的一項策略是:只要有棋子在敵方獸穴 R 格之內,就給那一方更大的節點預算,這是任何真實延伸的嚴格上界。
在談 Elo 之前的第一項發現:觸發條件並不稀疏。
| 半徑 | 觸發的著數比例 |
|---|---|
| 2 | 23% |
| 3 | 43% |
| 4 | 72% |
鬥獸棋的棋盤很小,獸穴又位於底線。在 R=4,也就是要早期看出競速所需的半徑下,這個延伸就只是「到處都多搜十二倍」,所以「精準手術」的框架從一開始就是錯的。
在 R=2、每組 300 對換色對局下:
| 組別 | Elo | 節點數對比對照組 |
|---|---|---|
| 獸穴附近深度 12 倍,免費給予 | +45 | 3.3x |
| 預算中性,基礎 3.7M + 獸穴附近 10M | -15 | 1.04x |
| 預算中性,基礎 1.1M + 獸穴附近 20M | -12 | 0.98x |
同樣的想法、同樣的程式碼,符號相反。引擎的五百萬節點是由玩家願意等多久所固定的,所以真實的延伸無法憑空變出深度:它得從搜尋的其他部分拿。免費那一組量到的是額外的算力,跟獸穴毫無關係。
我之所以跑預算中性那一組,是因為 +45 看起來好得不像真的。停在那裡的話,就會為一個價值略低於零的功能買下一週的工。
兩組各花一個下午,因為都不需要動到引擎。那個延伸只是測試框架的一項策略,依棋盤條件改變節點預算。對於任何「讓它對 X 更聰明」形式的提案,先免費給它完美的 X,是找出 X 到底有沒有意義的最便宜方法。
路線 B:評估項本身
我還是做了,因為延伸失敗的理由並不適用於評估項。延伸要花節點;評估項在每個葉節點上幾乎不花成本。
以下是當時的評估。在棋盤上跑一次迴圈,每個棋子各自獨立貢獻:
for idx in 0..N {
let v = VAL[role_of(code) as usize];
let friendly = color_of(code) == me;
s += if friendly { 2 * v } else { -2 * v };
// Advancement toward the relevant den: the enemy's for our pieces, ours for theirs.
let target = if friendly { enemy_den } else { own_den };
let dist = manhattan(idx as u8, target);
let advance = if dist <= 1 { 400 } else { (16 - dist) * 3 };
s += if friendly { advance } else { -advance };
}
那個推進項正是評估對競速視而不見的原因,而原因在於對稱性。我的奔跑者距離你的獸穴三格得 +39。你的奔跑者距離我的獸穴三格得 -39。雙方都在衝刺會得出接近零的數字,於是在局勢最已決定的那一刻,局面卻讀起來風平浪靜。
所以這一項不能是又一個逐子加分。它必須是每方一個判定、只算一次,把兩個奔跑者互相比較:
for side in [me, 1 - me] {
// Nearest enemy runner to this side's den, and this side's nearest defender
// to its own trap ring.
let (mut att, mut def) = (99, 99);
for idx in 0..N { /* ...scan once... */ }
if att > RACE_RADIUS { continue; } // no race here: contribute nothing
// The side to move gets one free step in the race for its own den.
let margin = att - def + i32::from(side == me);
let pen = if margin <= 0 { RACE_LOST }
else if margin == 1 { RACE_TIGHT }
else { 0 };
score += if side == me { -pen } else { pen };
}
防守方是以到陷阱環的距離來量測,而不是到獸穴。那才是正確的攔截區,因為獸穴的三個鄰格就是它的三個陷阱,而站在陷阱上的棋子可以被任何防守棋子吃掉,不論位階。走進你獸穴的象必須穿過一格,而你的鼠可以在那裡吃掉它。
在它所針對的那些局面中,它改變了 17% 的選擇著法,而在相同節點數下大約多花 17% 的實際時間。在每個權重各 300 對換色對局下,還沒把那項成本算進去之前:
| 權重 | Elo |
|---|---|
| 10 | -3 |
| 25 | +9 |
無效,而且是在寬鬆有利的方向上。
我會保留的東西
把預算固定住,否則你量的就是預算。實驗的免費版本是最方便做的那個,而它會遞給你一個看起來很有信心的正數。這項改動本來得從哪裡挖資源,就從那裡挖,然後看結果撐不撐得住。
這個陷阱一天之內坑了我兩次。同一個下午做的 contempt 掃描顯示,有勝負的對局從 32% 升到 50%,而這正是提高 contempt 依其構造必然造成的結果,因為自我對局掃描中雙方都吃到同一個設定。只有讓兩個數值互相對戰,才能說出它有沒有代價。結果沒有,所以那一項出貨了。
評估上的缺口是真的,診斷也正確。它背後就是什麼都沒有。在同一批對局資料中找到的兩個常數,一個是提早結束對局的無吃子和棋計時器,一個是接近零的和棋 contempt,把鬥獸棋從四分之一的對局以勝負收場拉到大約一半,而這兩者都沒有教引擎任何東西。那個議題要的是理解。理解從來都不是瓶頸。
這些對局是公開的,上面那個局面也是。分析棋盤跑的是同一個引擎,所以如果你覺得藍方應該吃掉那隻虎,你可以讓它把那條路線走完。