要点

  • Mistyは自分が観測した内容と矛盾しないすべての盤面をFEN文字列として保持し追跡する。ある対局では41個の盤面を相手に終局したが、そのどれも本物ではなかった。
  • python-chessは、通常チェスでその取りが合法な場合のみアンパサンのマスを書き出す。霧の下ではチェック中でもその取りは合法であり、アンパサンで取れるポーンはそのマスを見ているため、真の局面がMistyの観測内容と一致しなくなった。
  • 修正では、ポーンが取れる場合には常にそのマスを記録する。271局すべてを再生すると、サンプリング対象外の対局で真が失われたケースは4件から0件になった。
  • 何も検出できなかった。Rust移植版はpython-chessをバイト単位で再現していたため両者を比較するテストは通過してしまい、真を失った信念はより曖昧になるどころか、より確信を強める。

Mistyは、観測した内容と矛盾しないすべての盤面の集合Pを保持してフォグ・オブ・ウォー・チェスを指す。PはFEN文字列として保存されるが、FENのあるフィールドは局面ではなくルールによって書き出される。

   
症状 Pが1手で625盤面から41盤面に減り、その41の中に真の盤面が含まれていなかった
影響 Mistyはその対局の残りを、どれも実在しない41の世界から探索の根を引きながら指し切った
原因 キングの安全に関するルールが存在しないバリアントで、通常チェスの合法性に従ってアンパサンのマスを記録していた
修正 擬似合法のルール(EnPassantMode.XFEN)に基づき、共通の関数1つを通して記録する
頻度 終局した273局のうち、12,866手中1手
検出 なし。4か月後、別の目的で信念のサイズを計測していて発見した

バグ

対局e751c6c4、Mistyが黒、29手目。白がg列のポーンを2マス進め、黒の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

信念の更新はそれらの文字列から局面を再構築する。保存された文字列から再構築すると、真の局面は黒に14マスではなく12マスしか見せず、自ら生成した観測と一致しなくなり、Pから脱落した。

修正

取ることが得かどうかに関わらず、ポーンが取れるならそのマスを記録する。これがEnPassantMode.XFENであり、霧のルールでもある。隣に敵のポーンがいない場合はマスを省略するので、誰も使えないマスのために信念が分裂することはない。

いまは2つの書き出し側が、それぞれ独自の既定値でシリアライザを呼ぶのではなく、名前の付いた1つの関数を通る。所属判定は盤面を受け取って正規化する。生のboard.fen()を渡していたことが、元のミスの形だったからだ。

問題の手ではPは625から617になり、真が保たれる。271局すべてを再生すると、サンプリング対象外の対局で真を失った判断は報告されず、以前の4件から0件になった。

なぜ何も検出できなかったのか

警報は逆の極端でしか鳴らない。 列挙器はPが空になったときに例外を投げるが、それには候補が1つも生き残らないほど小さな信念が必要だ。41個の集合から真が失われても無音であり、そうならざるを得ない。対局中エンジンは真を知らないので、照合する相手が存在しないのだ。

差分テストが通過したのは、両方が間違っていたからだ。 Mistyのホットパスは、python-chessをバイト単位で再現するように書かれた関数を含むRust拡張である。それは成功していた。178,145回の手の適用にわたって両者を比較するテストはグリーンのままで、同じ間違いをした2つのエンコーダを比べていた。

再生なら気づけたはずだが、見ていなかった。 解析パスは真がPに含まれるかを記録するが、その列はサンプリング上限を超えた対局でしか参照されておらず、そこでは真を失うのは想定内だ。上限のない対局では無害な説明が付かない。そのクエリを誰も実行していなかった。

一般化できること

通常ルールのライブラリ由来のシリアライザは、中立なエンコーダではない。 それはそのライブラリの判断をバイト列に書き込み、その判断とはルールである。出力をキーとして使えば、読んだことのないルールブックを取り込んだことになる。

2つの実装が一致していても、分かるのは一致しているということだけだ。 私の2つは何か月も一致しており、その一致がバグを隠した。Rust移植がずれていないかと疑うたび、グリーンのテストが「いいえ」と答えた。正しいが、問うべきことではなかった。

真を失った信念は、曖昧になるのではなく鋭くなる。 41の世界は、マスを正しく記録したときに生き残る617よりも確信の強い信念だ。この失敗は内側から見ると不確実さのようには見えない。

2026-09-10にエンジン側で修正し、Misty 0.1.3でリリースした。上のすべての盤面は、その局面からエンジン自身の視界コードで描いたものだ。それらを描くスクリプトは、2つの視界の差がg3とg4だけでなくなった場合、実行を拒否する。