我做了 MistyBanqi,一套用 Rust 寫的暗棋(中國暗棋)古典引擎。你可以在 Mistboard 上與 MistyBanqi 對局。它使用 alpha-beta 搭配 Star1 expectiminimax、手工撰寫的評估函式,對局版本裡沒有任何神經網路。

這不是在宣稱它最強。強力的暗棋程式 CLAP_CDC 與 DarkKnight 都是閉源的,而且更強。這是一份開發紀錄,講的是在我願意花的預算內,我能做出最強的開源引擎。真正有用的成果不是某個神奇的評估項。幾乎所有進展都來自修正我衡量棋力的方式。

我的評估函式起點是george0828Zhang 的開源 CDC 引擎,然後把那套引擎留下來,當成衡量每一項改動的固定對手。MistyBanqi 現在有相當的水準,與那個基準大致打平,而且是開源的。

規則在 mistboard.com/rules/banqi,附有可下的棋盤。這裡唯一重要的規則是:一著要麼是普通的移動或吃子,要麼是翻棋,把一枚暗子翻開,從未揭示的棋子堆裡隨機露出一枚棋子。翻棋是機率事件,所以在普通的 alpha-beta 之上,搜尋還要處理機率節點,以 Star1 expectiminimax 來應付;其餘就是常見的那套機制,置換表與靜態搜尋。所有為局面評分的部分都是手寫的,而這正是接下來要談的全部主題。

不要拿自己當衡量對象

我的第一項升級是常見的搜尋機制,置換表與重複局面偵測。我用成對自我對戰,拿它跟自己引擎的早期版本比。它拿到 60.9%,約 +77 Elo,而且每一項統計都說這個增益是真的。

接著我拿同一項改動去對參考引擎。+77 Elo 不見了;結果是打平。一個拿自己來調校的機器人,會朝著打敗自己盲點的方向最佳化,而自我對戰的數字衡量的是我的引擎對「困難對手」的想像,那不過就是我的引擎。所以我把那套引擎當成尺規,之後每一項改動都對它跑一場大規模的成對比拚,每邊幾百局,分散到 Modal 上的多個容器,讓每次測試都在一小時內完成。暗棋的和棋很多,所以幾個百分點的優勢需要這麼多局才看得出來;我一開始跑的 20 局對抗全是雜訊。

調校評估函式

這把誠實的尺規找出的第一件事,是我一路帶著的一個錯誤,就在子力值表裡。以下是修正後的版本:

棋子
將/帥 30
16(原為 12)
14
11
9(原為 14)
7
兵/卒 4

炮和車顛倒了。炮靠隔著炮架跳吃,這使它成為棋盤上戰術威脅最大的棋子,即使它在吃子等級中排得很低;車只是普通的直線棋子。單這一項修正本身就值好幾個百分點,也是評估函式增益的主要部分。自我對戰永遠不可能讓我看見它,因為我自己引擎的每個版本都帶著同一張錯誤的表;要有外部對手才能把它揭露出來。

更有意思的調校是:一枚棋子的價值取決於棋盤上還剩什麼,而不是一個常數。參考引擎已經針對將帥做了一個小規模的版本,那是唯一會被兵卒吃掉的棋子:隨著敵方兵卒被消掉,它的價值往上爬,因為唯一威脅它的東西正在消失。我把這件事推廣到每一枚棋子。每枚棋子都有一組能吃它的敵方棋子,隨著那些棋子被換掉,它的價值就朝「不可侵犯」上升。這一項,適應性支配,是單一最大的評估增益,而整套疊起來對參考引擎達到 +16.6%,敗局少了三分之二。即使我故意把它和舊的、錯誤的子力值表配在一起,它仍然給出 +8.7%,這是它抓到了暗棋真實結構、而不是為了某一個對手硬套數字的跡象。

整個項只有十來行。對棋盤上的每枚棋子,數出還活著(在盤上或在棋子堆裡)且能吃它的敵方棋子數量,然後以反比縮放一個加分:

// dom_val: a piece's value grows as the enemy pieces that can capture it
// disappear — toward "immortal" when its dominators are gone.
const DOM_K: f64 = 0.5;
for i in 0..NSQ {
    let c = self.sq[i];
    if !is_piece(c) { continue; }
    let role = code_role(c);
    let enemy = (1 - code_color(c)) as usize;
    // living enemy pieces that can capture this role
    // (role 5 = cannon, which screen-captures anything, so it always counts)
    let mut dominators = 0;
    for d in 0..7 {
        if d == 5 || can_capture(d, role) { dominators += alive[enemy][d]; }
    }
    let bonus = values[role] * DOM_K / (1.0 + dominators as f64);
    total += if code_color(c) == persp { bonus } else { -bonus };
}

敵方兵卒全被換掉的將帥,或任何能吃它的棋子都已離開棋盤的子,價值會朝不可侵犯漂移,而這正是固定表格抓不住的直覺。(完整函式

棋力不在於搜尋更多

靜態評估項到了平原期,於是接下來我去追求深度。有兩件事擋住我。

第一,翻棋。在翻棋節點,引擎必須對這枚暗子可能翻出的每一種棋子取加權平均,所以單一次翻棋就展開成十幾個加權結果,而可翻的暗子又很多。這種機率分支讓樹爆炸;你沒辦法像在西洋棋裡那樣,天真地在暗棋中搜得很深。(Star1 expectiminimax 會修剪機率分支,這有幫助,但並不讓問題消失。)

第二,也更重要:即使給更多節點,結果也不再動了。每一著超過幾十萬個節點之後,搜得更深並沒有更能打贏參考引擎。瓶頸不是深度,而是評估函式,也就是搜尋落底時為局面評分的那個函式。所以我不再想靠搜尋換取棋力,開始看棋。勝率只會告訴你你在輸;它永遠不會告訴你該看哪一著。

病徵一:贏定的棋走成和棋

這裡有一局。一著一著走看看。

Misty(紅方)以十子對兩子領先,卻因三次重複局面而和棋。兩個問題都跟它怎麼理解和棋有關。在正式環境中,它只拿到當前局面而沒有歷史,所以對自己正走進去的重複局面是盲的。而它的評估函式對轉化取勝沒有任何獎勵,所以一個大優的局面和一個已經贏下的局面分數差不多;它沒有理由去推進,只會原地晃來晃去。

修法是把真實對局的著法歷史串進搜尋,讓引擎看見重複局面要來了,在領先時避開它,只在落後時去追求它(一個小小的 contempt 設定)。這值大約 +53 Elo。另外有一道防護處理最醜的版本:引擎把一枚棋子送進虧損的吃子,然後還是接受和棋,嚴格比直接和棋更差。那看起來像 contempt 的錯誤,但在 contempt 為零時也重現了;真正的原因是評估函式把那個虧損的吃子評得比和棋值高出一絲。

病徵二:讓自己的將帥被獵殺

兵卒是唯一能吃將帥的棋子,所以鬆散的將帥處境真的危險。看一枚敵方兵卒沿著 a 路獵殺 Misty 的將。

Misty(此處為黑方)把將漂到 a1 角。一枚紅兵沿著這一路推進,將就被封死了:旁邊的格子還是暗子,你不能走到暗子上,而將又不能吃兵。結局是完全沒有合法著法,原地凍住,28 著之後被吃掉。

解法就是棋手所說的開通風口:在獵人到達之前,翻開旁邊一枚暗子,替將帥開出逃路。我加了一項將帥安全項,正好誘導出這種下法,把 Misty 損失自己將帥的比例從 35.5% 降到 26%。看看要衡量這件事需要什麼:對參考引擎的勝率幾乎沒動,因為它不獵殺將帥,而這些棋 Misty 本來大多也贏了。基準測試看不見這項修正,所以我不再相信分數,而是直接衡量那個災難本身。(我也抓到自己竟然去問引擎自己的評估函式,看某個解法有沒有效。一個對危險視而不見的評估,無法為自己的盲點評分,所以我改成手動檢查。)

這就是手工調校能達到的誠實邊界。威脅是一段緩慢、安靜、多著的行軍,任何靜態項在來不及之前都看不見它,而這正是學習式評估要處理的長程判斷。

最後一項衡量:這段爬升值得付錢嗎?

便宜的增益已經用完了,而兩個病徵都指向同一個解法:一個能理解局面、而不是只會數子的評估函式。那就是學習式價值網路,也就是 CLAP_CDC 用來在這個項目奪下電腦奧林匹亞冠軍的 AlphaZero 配方。

所以我把整條流程搭了起來(棋盤編碼、一個小型 ResNet、機率感知的 MCTS、帶閘門的自我對戰,讓壞的世代不會毒害下一代),然後在付錢跑完整訓練之前,再做一次衡量:一次便宜的本機去風險測試,確認價值網路是否真的能打贏手工調校的引擎。

它沒有。我在筆電的 GPU 上跑去風險測試,三次執行下來,網路對 alpha-beta 引擎的勝率頂在 35% 左右,從未達到算得上改進的 55%。把搜尋預算加倍幾乎沒讓它動,所以這不是局數太少的問題;最可能的原因是機率節點產生的價值目標太吵,以及網路太小。這些都可以修,但要測試這些修正,需要遠超過一台跑幾小時就降頻的筆電的算力,而雲端 GPU 在這裡也不是隨插即用:遊戲邏輯是單執行緒 Python,所以快速的 GPU 只會餓著等它,除非那條路徑改用 Rust 重寫,後面接一個批次推論伺服器。那是一到兩週的工作,接著要花數百到一兩千美元去跑。以我手上的證據來看,這是一場賭注,不是穩當的一步,所以我還沒下手;更便宜的本機實驗要先做。

所以 MistyBanqi 是一套有水準的 alpha-beta 引擎,不是最先進的宣稱,而這是經過衡量的選擇,不是死路。我還沒對 CLAP_CDC、DarkKnight 或其他頂尖 CDC 程式跑過可重現的正面對抗;學習式網路是下一段爬升,等哪天去風險測試說它值得付錢。

這裡每一節底下的共同模式:整個專案裡最便宜、槓桿最高的決定,就是選擇要衡量什麼。自我對戰分數、勝率、將帥損失率、去風險閘門,每一次挑一個能看見我真正在意的東西的量測工具,效果都勝過對引擎本身做的任何改動。

資源: