Misty 下迷霧西洋棋的方式,是維護 P,也就是與它觀測到的一切都相容的所有棋盤的集合。P 以 FEN 字串儲存,而 FEN 裡有一個欄位不是由局面決定,而是由一條規則寫出來的。

   
症狀 P 在一著之內從 625 個棋盤降到 41 個,而真實棋盤不在這 41 個之中
影響 Misty 在那盤棋剩下的部分裡,從 41 個世界中抽取搜尋根節點,其中沒有一個是真的
原因 吃過路兵格按標準西洋棋的合法性記錄,而這個變體沒有王的安全規則
修正 按偽合法規則(EnPassantMode.XFEN)記錄它,並且只經由一個共用函式
頻率 273 盤已完成的對局、12,866 著之中出現 1 著
偵測 沒有。四個月後,在為別的目的測量信念集合大小時才發現

這個 bug

對局 e751c6c4,Misty 執黑,第 29 回合。白方把 g 兵推進兩格,停在黑方 f4 兵旁邊。白后在 d6 攻擊黑王。

. . . . . k . .
. . Q . . . . p
P . . Q p . . .
. . . . N . . .
. . . . . p P .
B . . . . . . .
. . . . P P . P
R N . . K B . R
After 29.g4, from Black's side. Tinted: White's pawn on g4, Black's pawn on f4, and the empty square g3 between them. The arrow is White's queen attacking the black king.

黑方的 f4 兵可以在 g3 吃過路兵。標準西洋棋禁止這樣做:黑方正被將軍,而這一吃並沒有應將。迷霧西洋棋沒有這條規則。你看不見對你王的攻擊,所以可以把王留在那裡,對局以王被吃掉而不是以將死結束。在這裡 f4xg3 是一步普通的著法。

迷霧中的視野,是你自己的棋子能走到的格子的集合。一個能吃過路兵的兵看得見落點格,也看得見它將要吃掉的那個兵。因此,吃過路兵格是一個關於感知的事實,而不只是關於合法性的事實。

. . . . . k . .
. . . . . . . p
. . . . p . . .
. . . . . . . .
. . . . . p P .
. . . . . . . .
. . . . . . . .
. . . . . . . .
With the en passant square. 14 squares. Black sees g3, and sees the pawn on g4.
. . . . . k . .
. . . . . . . p
. . . . p . . .
. . . . . . . .
. . . . . p . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
Without it. 12 squares. g3 and g4 go dark. Same pieces, same board.

python-chess 預設用 EnPassantMode.LEGAL 寫這個欄位,只在按標準規則這一吃合法時才記錄該格。於是它寫下了 -

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

信念更新是從這些字串重建局面的。從儲存的字串重建出來後,真實局面給黑方的視野是十二格而不是十四格,不再與它自己產生的那次觀測相符,於是從 P 中掉了出去。

修正

只要有兵能吃,就記錄這個格,不管吃是否明智。這就是 EnPassantMode.XFEN,也正是迷霧規則。沒有敵兵相鄰時它仍然省略該格,這樣信念集合不會因為一個誰也用不上的格而分裂。

現在兩個寫入方都經過同一個具名函式,而不是各自呼叫一個帶著自己預設值的序列化器。成員檢查接收一個棋盤並將其正規化,因為直接傳入原始的 board.fen() 正是最初那個錯誤的形狀。

在出問題的那一著,P 現在從 625 變為 617,真實局面得以保留。重播全部 271 盤對局,在抽樣對局之外沒有回報任何丟失真實局面的決策,此前是四次。

為什麼沒有任何東西抓到它

絆線裝在了錯誤的極端上。 列舉器在 P 變空時拋出例外,這需要信念集合小到沒有任何候選存活。從 41 個裡丟掉真實局面是無聲的,也只能是無聲的:對局中引擎從不知道真實局面,所以沒有東西可供核對。

差分測試通過了,因為兩邊錯得一樣。 Misty 的熱路徑是一個 Rust 擴充,其中有一個函式是為了逐位元組重現 python-chess 而寫的。它做到了。跨越 178,145 次著法套用比較兩者的測試一直是綠的,比較的是兩個犯了同一個錯誤的編碼器。

重播本可以看見它,只是沒在看。 分析流程會記錄真實局面是否在 P 中,但那一欄只在超過抽樣上限的對局上被讀過,而在那些對局裡丟失真實局面是預期之內的。在未超上限的對局上,它沒有任何無害的解釋。沒人跑過那個查詢。

可以推廣的結論

來自標準規則函式庫的序列化器不是中立的編碼器。 它把那個函式庫的判斷寫進位元組裡,而那些判斷就是規則。把它的輸出當作鍵來用,你就引入了一本從未讀過的規則書。

兩個實作一致,只能告訴你它們一致。 我的兩個實作一致了幾個月,而這種一致掩蓋了 bug:每當我懷疑 Rust 移植是否已經偏離,一個綠色的測試都說沒有。沒錯,但那不是問題所在。

丟失了真實局面的信念會變得更尖銳,而不是更模糊。 41 個世界比正確記錄該格後存活下來的 617 個世界是一個更有把握的信念。從內部看,這種失敗並不像不確定性。

已於 2026-09-10 在引擎中修正,將隨下一個 Misty 版本發布。上文的每個棋盤都由引擎自己的視野程式碼從該局面繪製;繪製腳本在兩個檢視不再恰好相差 g3 和 g4 時會拒絕執行。