它如何思考

暗棋以自己獨特的方式隱藏資訊:每枚棋子起初都背面朝下,翻開一枚,就會從剩下的棋子裡隨機翻出一枚。因此和西洋棋不同,引擎的搜尋樹裡既有普通著法,也有隨機事件。MistyBanqi 把翻子當作一個機率節點,對這枚棋子可能翻出的各種身分取加權平均;其餘部分則像經典西洋棋引擎那樣搜尋:向前推演雙方可能走的著法,再把最佳一路的價值回傳上來。

它做不到的是憑感覺判斷盤面。這棵搜尋樹的每個葉子節點,都由一套手寫的評估來打分:基於一張修正過的子力價值表的子力(炮靠隔子吃,是盤面上最危險的棋子)、每枚棋子控制多少格、將帥有多暴露,以及另外一些項。引擎的水準完全取決於這些項的好壞——下面要講的弱點正源於此。

大多數時候,它會贏

MistyBanqi 能贏過大多數人,而它取勝的方式正是經典引擎的方式:往前算上好幾步的吃子。逐步回放下面這盤它把對手清光的棋。暗棋的局勢隨翻子起伏,這盤裡它開局甚至一度子力落後,隨後一步步扳了回來,直到對手沒有棋子可走。每枚棋子第一次被翻開時,會翻出它所發到的身分。

這種一子一子算下去的計算,是它棋力強的那一半。盲點是另一半:當勝利不再需要吃子、只需要耐心時,會發生什麼。

它是怎麼做出來的

我做了 MistyBanqi,一個用 Rust 寫的古典暗棋(中國暗棋)引擎。它使用 alpha-beta 加上 Star1 expectiminimax、手工打造的評估函式,對弈版本不含神經網路。

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

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

不要拿自己當尺子

我第一次升級是慣常的搜尋機制,置換表與重複偵測。我用配對自我對弈,拿它跟自己引擎的舊版本比。它拿下 60.9%,約 +77 Elo,所有統計數字都說這個收益是真的。

接著我拿同一項改動對上參考引擎。+77 Elo 不見了;結果是平手。一個拿自己調校的機器人,只會最佳化到打贏自己的盲點,而自我對弈的數字衡量的是我的引擎對「強對手」的想像,也就是我的引擎本身。於是我把那個引擎當成尺,之後每項改動都對它跑一場大型配對比試,每邊幾百局,分散到 Modal 上的多個容器,讓每次執行都在一小時內完成。暗棋和棋很多,幾個百分點的優勢需要這麼多局才看得出來;我一開始跑的 20 局對抗賽全是雜訊。

調校評估函式

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

棋子值
將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 會修剪機率分支,有幫助,但問題不會消失。)

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

它會把贏定的棋下成和棋

同樣這個引擎,下面處在一個它已經完全贏定的局面。它以十子對兩子領先,已經沒有子可吃,唯一要做的就是把勝勢走到底。結果它卻下成了和棋。

這盤棋下於 2026 年 10 月之前,當時任何三次重複局面都判和。按網站現行的長捉規則,第 64 著是禁著,所以這塊棋盤停在第 63 著。

評估裡沒有任何一項會因為「把優勢轉化為勝利」而比「單純守住子力」給更高的分,於是一個遙遙領先的局面和一個真正已經贏下的局面,得分幾乎一樣。既然沒有哪一項促使它取得進展,它從不主動推進,棋局便滑進三次重複局面,按暗棋規則判和。

引擎也看不見重複局面本身。在正式環境中,它只拿到當前局面而沒有歷史,所以對自己正走進去的重複局面視而不見。

修正方式是把真實棋局的著法歷史串進搜尋,讓引擎看見重複局面將至,領先時避開它,只在落後時去追求它(一個小小的蔑視值設定)。這值約 +53 Elo。另有一道防護處理最難看的版本:引擎先送掉一子任其被吃,然後還是接受和棋,嚴格來說比直接和棋更糟。那一處看起來像蔑視值的 bug,但在蔑視值為零時也會重現;真正的原因是評估值把那個送子被吃算得比和棋值高了一絲。

這對你是有好處的。如果你子力落後,不代表就輸了:MistyBanqi 仍可能來回挪子,走進重複和棋。這種事多到可以量:引擎自我對弈的 200 盤裡,每六盤就有一盤和棋,其中一半曾經走到贏定的局面。

它也可能丟掉自己的將帥

另一個相關的盲點跟將有關。只有卒、對方的將或砲的跳吃拿得下它,所以被圍住的將一次只怕一種子,而引擎很慢才會替被逼到角落的將騰出空間。它有時候會把一顆子調到棋盤另一頭,同時一顆孤零零的敵方卒走過來把它困死。跟上面那個和棋是同一個缺口:這套評估函數對於好幾步之外慢慢成形的安靜威脅,沒有什麼感覺。

看一枚敵方兵沿 a 路把 Misty 的將一路獵下來。MistyBanqi 執黑。它的將在 b1,之後獵殺它的那枚紅兵在 b4。

MistyBanqi(黑方)對 Mistboard 上的一位棋手,2026 年 6 月 18 日,第 44 至 58 著。

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

解法就是棋手所說的製造出路:在獵手抵達前翻開一個相鄰的暗子,為將開出一條逃路。我加了一個將/帥安全項來誘導這件事,把 Misty 丟掉自己將/帥的頻率從 35.5% 降到 26%。看看衡量這件事需要什麼:對參考引擎的勝率幾乎沒動,因為它不獵將,而這些棋局 Misty 本來大多也贏了。基準看不見這個修正,所以我改成直接衡量那場災難,而不是相信分數。(我還抓到自己去問引擎自己的評估值,判斷這個解法有沒有用。一個對危險視而不見的評估函式,無法為自己盲點的修正打分,所以我改用手工檢查。)

這就是手工打造能達到的誠實邊界。威脅是一場緩慢、安靜、多著的行軍,任何靜態項都要到來不及時才看見,而這正是學習式評估函式要處理的長程判斷。

為什麼會有這些問題,以及下一步

這些是手寫評估的侷限:它只能給有人想到要編碼進去的東西打分,而「把勝勢轉化為勝利」和「緩慢圍獵將帥」恰恰是那種很難寫成規則的長程判斷。最強的暗棋程式採用的解法,是一套從對局勝負中學習得到的評估,讓引擎能自己判斷這些。這也是 MistyBanqi 終將邁出的下一步。在學習版於測試中越過現有引擎這道門檻之前,你面對的仍是手寫版:強,且對自己會在哪裡出問題保持坦誠。

最後一次衡量:這趟攀升值得付錢嗎?

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

所以我把整條流程建好了(棋盤編碼、一個小型 ResNet、機率感知的 MCTS、帶閘門的自我對弈,讓壞世代無法污染下一代),然後在付錢跑完整訓練之前,再做了一次衡量:一次低成本的本地風險排除,看看價值網路是否真的能打贏手調的引擎。

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

所以 MistyBanqi 是一個有相當水準的 alpha-beta 引擎,不是最先進的宣稱,而這是衡量後的選擇,不是死路。我還沒有對 CLAP_CDC、DarkKnight 或其他頂尖 CDC 程式跑過可重現的對決;學習式網路是下一段攀升,等到哪天風險排除說它值得付錢的時候。

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

misty-banqi 是引擎本體,MIT 授權,alpha-beta + Star1 搜尋以及上述評估項。chinese-dark-chess-hw 是我用來起步評估函式、並當成固定基準的開源引擎。引擎系列更多內容:鬥獸棋引擎,棋盤更大的完全資訊近親。

來下一盤

MistyBanqi 已在 Mistboard 上線。來按你選擇的強度挑戰它,或閱讀它的原始碼。

對戰 MistyBanqiGitHub 上的 misty-banqi暗棋規則