Skip to content

animal_shogi: fix path-dependent Zobrist hash that undercounted repetitions#1326

Open
gweber wants to merge 1 commit into
sotetsuk:mainfrom
gweber:fix-animal-shogi-zobrist
Open

animal_shogi: fix path-dependent Zobrist hash that undercounted repetitions#1326
gweber wants to merge 1 commit into
sotetsuk:mainfrom
gweber:fix-animal-shogi-zobrist

Conversation

@gweber

@gweber gweber commented Jun 15, 2026

Copy link
Copy Markdown

Summary

animal_shogi's Zobrist hash is not a pure function of the position — it is path-dependent — so repetition detection silently undercounts. Two identical positions reached via different move orders can hash differently, which means sennichite (repetition) draws can be missed and the repetition observation planes (observation[:, :, 22:24]) can be wrong.

Root cause

The incremental hash updates in _step_move / _step_drop are inconsistent:

  1. Captured piece removed from the wrong square (_step_move): the captured piece sits on action.to, but it was XOR-removed from the hash using zb_from_ (the moving piece's origin square) instead of the to square.
  2. Drop hand-count off-by-one (_step_drop): drop XORed {num_hand+1, num_hand} while capture XORed {num_hand-1, num_hand}, so a drop was not the inverse of the capture that produced the hand piece.
  3. The hand term was indexed by the post-flip _turn.

Because the per-step updates aren't invertible, the hash drifts with history.

Fix

Recompute _zobrist_hash from scratch as a pure function of the canonical (turn-0 frame) position after each step (_compute_hash), and drop the now-unnecessary incremental hash bookkeeping in _step_move/_step_drop. Equal positions now hash equally by construction. INIT_ZOBRIST_HASH is derived from the same function.

Test

Adds test_zobrist_hash_is_pure_position_function: random self-play (which exercises captures and drops), asserting any two states with identical (_board, _hand, _turn) have identical _zobrist_hash. This fails on the current code (e.g. 569813267 != 1222688014 for an identical position) and passes with the fix. All existing animal_shogi tests still pass.

How it was found

Surfaced while differentially testing a re-implementation against pgx: a game had an identical position at ply 10 and ply 20 (same board/hand/turn) but different pgx hashes, so pgx reported rep == 0 where the true repetition count was ≥ 1. No king-capture or other exotic state is involved; ordinary captures trigger it.

…itions

The Zobrist hash was not a pure function of position:
- on capture, the captured piece was removed from the hash at the moving piece's `from`
  square instead of its actual `to` square (_step_move)
- the drop hand-count update used {num_hand+1, num_hand} while capture used {num_hand-1,
  num_hand}, so drop was not the inverse of capture (_step_drop)
- the hand term was indexed by the post-flip turn

This made the hash path-dependent, so repetition detection silently undercounted (two
identical positions could hash differently), affecting sennichite draws and the repetition
observation planes. Fix: recompute the hash from scratch as a pure function of the canonical
position each step. Adds a regression test (fails on the old code).
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