Skip to content

Fix zobrist hash update for captured stones in python Board - #1256

Open
zliu1022 wants to merge 1 commit into
lightvector:masterfrom
zliu1022:python-board-zobrist-fix
Open

zliu1022 wants to merge 1 commit into
lightvector:masterfrom
zliu1022:python-board-zobrist-fix

Conversation

@zliu1022

Copy link
Copy Markdown

Problem

Board.remove_unsafe in python/katago/game/board.py removes a captured chain like this:

pla = self.board[group]      # color of the stones being removed
opp = Board.get_opp(pla)
...
self.zobrist ^= Board.ZOBRIST_STONE[opp][loc]

It XORs the key of the other color, so the captured stone's own key stays in the hash and the opposite color's key is added. The C++ Board::removeChain uses the stone's own color (pos_hash ^= ZOBRIST_BOARD_HASH[cur][colors[cur]];).

Consequences:

  • After any capture, board.zobrist differs from the hash of the same stones placed directly. The error is exactly ZOBRIST_STONE[BLACK][p] ^ ZOBRIST_STONE[WHITE][p] for each captured point p.
  • undo() of a capturing move does not restore the hash (the refill in undo XORs the correct color, so the two no longer cancel). The python ladder search uses playRecordedUnsafe/undo.
  • Distinct positions can share a hash: 3 pairs in 300 random 5x5 games. I ran into this with a transposition table keyed on pos_zobrist(): a five-point eye shape was solved as seki in one orientation and as dead in the others.

As far as I can tell nothing inside this repo relies on the python hash being correct (the assert in features.py compares two hashes computed along the same path), so this mostly affects external users of the python Board.

Minimal reproduction:

from katago.game.board import Board

b = Board(9)
b.play(Board.WHITE, b.loc(1, 0))
b.play(Board.BLACK, b.loc(0, 0))
b.play(Board.BLACK, b.loc(2, 0))
b.play(Board.BLACK, b.loc(1, 1))   # captures the white stone at (1,0)

fresh = Board(9)
for x, y in ((0, 0), (2, 0), (1, 1)):
    fresh.set_stone(Board.BLACK, fresh.loc(x, y))

print(list(b.board) == list(fresh.board))   # True
print(b.zobrist == fresh.zobrist)           # False before this fix, True after

Fix

One line: XOR ZOBRIST_STONE[pla][loc].

Tests

python/tests/test_board_zobrist.py checks the incremental hash against a from-scratch recomputation after a single capture, after every move of 100 random 5x5 games, and across playRecordedUnsafe/undo. All three fail before the fix and pass after it. (tests/test_export_prune_dead_ffn.py has 3 failures in my environment both with and without this change; they are unrelated.)

🤖 Generated with Claude Code

Board.remove_unsafe XORed ZOBRIST_STONE[opp][loc] for each captured stone,
i.e. the key of the other color rather than the captured stone's own color.
After any capture Board.zobrist no longer matched the stones on the board,
undo() of a capturing move did not restore it, and distinct positions could
share a hash. The C++ Board::removeChain uses the stone's own color
(ZOBRIST_BOARD_HASH[cur][colors[cur]]).

Add tests checking the incremental hash against a from-scratch
recomputation after a capture, over random games, and across
playRecordedUnsafe/undo.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
22nsuk added a commit to 22nsuk/KataGo that referenced this pull request Oct 6, 2026
Backport the one-line fix proposed by zliu1022 in lightvector#1256
(upstream commit 0008dd7).
The fork's original board blob matches that PR's base, so this retains
the exact upstream fixed board without touching liberties or C++ search.

The unchanged focused regressions on the previous tests-only commit
reproduced five hash failures while the pass control succeeded.
22nsuk added a commit to 22nsuk/KataGo that referenced this pull request Oct 6, 2026
Apply the one-line removed-stone-color correction proposed by zliu1022 in
lightvector#1256 (0008dd7).
Use the current-base compatible board, not the PR's older whole-file snapshot.
The resulting production diff against fork master is only opp -> pla in
remove_unsafe; neighbor liberties and all other board logic are preserved.

The unchanged focused tests reproduced five hash failures and one passing
non-capture control against the original fork board.
22nsuk added a commit to 22nsuk/KataGo that referenced this pull request Oct 6, 2026
…t-1256

Backport upstream lightvector#1256 Python Board capture hash fix with focused regressions

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant