Misty 依標準西洋棋規則記錄吃過路兵的目標格

python-chess 預設以 EnPassantMode.LEGAL 寫出吃過路兵欄位:只有在該吃子於標準西洋棋中合法時才記下那個格子,其中包括不讓吃子一方的王處於被將軍的狀態。Misty 的 Rust 熱路徑刻意複製了同樣的過濾條件,好讓它的字串與 python-chess 逐位元組一致。

所以那個格子會在一種局面中遺失:一個兵剛走了兩格,停在敵方兵的旁邊,而吃過路兵會讓吃子一方的王處於被攻擊的狀態,例如那個王本來就正被將軍時。格子一遺失,重建出來的局面就看不見落子的格子,也看不見它可以吃的那個兵,於是真實棋盤不再與 Misty 所見相符,從 P 中掉了出去。這個觸發條件在 12,866 著中出現 1 著,發生在 273 盤已完成棋局中的 1 盤。

一盤棋在第 29 著遺失了真實棋盤

棋局 e751c6c4,Misty 執黑,第 29 著。白方將 g 兵推進兩格,落在黑方 f4 兵的旁邊。白方在 d6 的后攻擊黑王。

29.g4 之後,從黑方視角。上色的是:白方 g4 兵、黑方 f4 兵,以及兩者之間的空格 g3。箭頭表示白方的后正在攻擊黑王。

黑方正被將軍,所以標準西洋棋禁止 f4xg3 吃過路兵。在迷霧規則下這是一個普通的著法,而且它決定了黑方看得見什麼。

有吃過路兵的目標格時。14 個格子。黑方看得見 g3,也看得見 g4 上的兵。
沒有該格時。12 個格子。g3 與 g4 變暗。同樣的棋子,同樣的棋盤。

依標準規則,那個格子被丟掉了:

stored  5k2/2Q4p/P2Qp3/4N3/5pP1/B7/4PP1P/RN2KB1R b KQ -  0 29
needed  5k2/2Q4p/P2Qp3/4N3/5pP1/B7/4PP1P/RN2KB1R b KQ g3 0 29

Misty 在沒有那個格子的情況下建出 29.g4 之後的局面,數出黑方可見 12 個格子,與真實觀察中的 14 個比較,於是排除了真實的著法。P 在那一著之內由 625 個棋盤掉到 41 個,而真實棋盤不在這 41 個之中。Misty 這盤棋最後四次決策的搜尋根節點,都是從這 41 個中抽取的。

把黑王移到旁邊一格,那裡沒有任何棋子攻擊它,其餘一概不變:那個格子就會被記為 g3,真實棋盤也會留在 P 中。

修正方式是只要有兵能吃就記下該格

只要有兵能吃,就記下那個格子,不管吃下去是否明智。這就是 EnPassantMode.XFEN,也是迷霧規則。當旁邊沒有敵方兵時它仍會省略該格,所以信念不會因為一個沒人能用的格子而分裂。

Rust 路徑現在只檢查被推進的兵旁邊是否有敵方兵。在 Python 這一側,每個寫入 P 的棋盤都走同一個函式 belief_fen,而成員檢查接受一個棋盤,並以同樣的方式寫出它,因為直接傳入原始的 board.fen() 正是這個錯誤混進來的方式。

在出錯的那一著,P 現在是由 625 變為 617,真值保留。重播全部 271 盤棋後,抽樣棋局以外沒有任何真值遺失的決策,先前則有四例。

為什麼沒有任何機制抓到它

列舉器只在 P 變成空集合時拋出錯誤,而從 41 個的集合中遺失真值是無聲的,也必然如此:實戰中引擎從來不知道真值。比對 Rust 移植版與 python-chess 的測試在 178,145 次著法套用中一直是綠的,因為兩者犯了同樣的錯誤。重播會記錄真值是否在 P 中,但那一欄只在超過抽樣上限的棋局上被讀過,在那些棋局中遺失真值是預期的。

已於 2026-09-10 在引擎中修正,並隨 Misty 0.1.3 發佈。上方每一張棋盤都是由引擎自己的可見性程式碼從該局面繪製的;繪圖的腳本若發現兩個視野的差異不再剛好是 g3 與 g4,就會拒絕執行。