Table of Contents

Chess - Programming - Zobrist Hashing

Zobrist Hashing is a hashing function that is widely used in 2 player board games.


A program to illustrate the Zobrist Hashing Algorithm


Pseudocode

// A matrix with random numbers initialized once.
Table[#ofBoardCells][#ofPieces] 

// Returns Zobrist hash function for current configuration of board.
function findhash(board):
  hash = 0
  for each cell on the board :
    if cell is not empty :
      piece = board[cell]
      hash ^= table[cell][piece]
  return hash

NOTE: Explanation :

  • The idea behind Zobrist Hashing is that for a given board state, if there is a piece on a given cell, we use the random number of that piece from the corresponding cell in the table.
  • If more bits are there in the random number the lesser chance of a hash collision.
  • Therefore 64 bit numbers are commonly used as the standard and it is highly unlikely for a hash collision to occur with such large numbers.
  • The table has to be initialized only once during the programs execution.
  • Also the reason why Zobrist Hashing is widely used in board games is because when a player makes a move, it is not necessary to recalculate the hash value from scratch.
  • Due to the nature of XOR operation we can simply use few XOR operations to recalculate the hash value.

Initialization

At program initialization, we generate an array of pseudorandom numbers:

This results in an array with 781 (12*64 + 1 + 4 + 8) random numbers.

Programs usually implement their own Pseudorandom number generator (PRNG), both for better quality random numbers than standard library functions, and also for reproducibility.


Runtime

To get the Zobrist hash code of a certain position, the hash key is initialized by xoring all random numbers linked to the given feature.

For example, the starting position:

[Hash for White Rook on a1] xor [White Knight on b1] xor [White Bishop on c1] xor ... ( all pieces )
... xor [White castling short] xor [White castling long] xor ... ( all castling rights )

NOTE: The fact that xor-operation is own inverse and can be undone by using the same xor-operation again, is often used by chess engines.

  • It allows a fast incremental update of the hash key during make or unmake moves.

Another example, for a White Knight that jumps from b1 to c3 capturing a Black Bishop, these operations are performed:

[Original Hash of position] xor [Hash for White Knight on b1] ... ( removing the knight from b1 )
... xor [Hash for Black Bishop on c3] ( removing the captured bishop from c3 )
... xor [Hash for White Knight on c3] ( placing the knight on the new square )
... xor [Hash for Black to move] ( change sides)

What size the hash keys should have

Smaller hash keys are faster and more space efficient, while larger ones reduce the risk of a hash collision.


Collisions

Using too small a hash code, such as only 32 bits, may result in different positions being assigned the same hash code.

Programs have various strategies for deciding what to do when a clash occurs.

A supposedly good hashing function workaround for clashing:

Remember that a table entry contains a “key”, score, score type, best move, etc.

NOTE: It is highly recommended to use 64-bit keys to not have to deal with collisions.

  • A value of 48 bits has shown to be just as safe as 64, but 48 is not a reasonable size as 64 bit signatures are the natural signature length for 64 bit processors.

Implementation Consideration

UINT64 getZoristKey(int piece, int color, int square)
{
  int index = piece*color*square;
  const char *p = ((char *)zorbrist) + index
  return *(UNIT64 *) p;
}

This means that the array does not need:

(piece*color*square * 64) bits;

but only

(piece*color*square * 8 + 3*8) bits;

If a bit boundary is used, the array becomes:

(piece*color*square + 63) bits;

To use this: The index to the 64-bit value that is being searched for becomes an index to the first bit of the 64-bits needed.


References

http://web.archive.org/web/20111202160335/http://www.cis.uab.edu/hyatt/collisions.html