簡而言之

  • MistyBanqi 是一個開源的暗棋(Chinese Dark Chess)引擎,與我拿來做基準的那個開源引擎大致打成平手。閉源程式更強。
  • 自我對弈說某項搜尋改動值 +77 Elo;對上那個外部引擎卻毫無價值。之後每一項改動都改以該引擎來衡量。
  • 外部基準測試找出炮與車的子力值被互換了。最大的評估值收益是「能吃它的子離開棋盤後,該子價值上升」,讓整套改動對基準引擎達到 +16.6%。
  • 一次低成本的 AlphaZero 測試對手調引擎的勝率最高只到 35%,因此付費的正式訓練暫緩。

我做了 MistyBanqi,一個用 Rust 寫的古典暗棋(Chinese Dark Chess)引擎。你可以 在 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 程式跑過可重現的對決;學習式網路是下一段攀升,等到哪天風險排除說它值得付錢的時候。

貫穿本文每一節的模式是:整個專案裡最便宜、槓桿最高的決定,是選擇要衡量什麼。自我對弈分數、勝率、丟將率、風險排除閘門,每一次挑到一個能看見我真正在意的東西的量測工具,效果都勝過對引擎本身做的任何改動。

資源: