c++algorithmhashhash-functiongame-ai

Storing player turn in Zobrist hash


I'm currently implementing a Transposition table in a Chinese Checkers minimax algorithm. In Chinese Checkers, no pieces are captured, and the board is functionally 81 spaces large. Players take turn moving pieces across the board.

Part of the process involves creating a hash for the board state. So far, I've got a functioning method that creates a (hopefully) unique hash for each board state:

 myHash = 0;
 //randoms[81][3] is an array filled completely with pseudorandom values
 for (int x = 0; x < 81; x++) {
      myHash ^= randoms[x][board[x]]; 
      //board[x] is the piece at space x (1=player1 piece, 2=player2 piece, 0=empty)
 }

More importantly, I do this incrementally in the applyMove function (and the undoMove function):

applyMove(int to, int from) {

   //Undo the 'from' piece in the hash
   myHash ^= randoms[from][board[from]];
   // Apply the move
   std::swap(board[from], board[to]);
   //Add the 'to' piece to the hash
   myHash ^= randoms[to][board[to]];

   // Update whose turn it is
   swapTurn();
 }

This works because of the reversibility property of the XOR function.

The issue I have now, is that the hash function doesn't store whose turn it is. That is, you could have two identical game boards, but they would return different values in the minimax algorithm because one's trying to maximize the score, and the other is trying to minimize it.

Basically, my question is this: How can I store the player's turn in an incrementally generated hash function, while keeping the ability to reverse it perfectly (and preferably inexpensively)? Assuming the player's turn is an integer rather than a boolean, since the game will eventually have 6 players rather than two players.


Solution

  • You can use a turns[6] array filled with pseudorandom values:

    unsigned n = 6;  // number of players;
    
    myHash ^= turns[active_player];           // 'undo' the old active player
    active_player = (active_player + 1) % n;  // new active player's index
    myHash ^= turns[active_player];           // 'add' new active player
    

    this is similar to the piece position incremental update and works for n ∈ [2, 6].


    As a side note...

    usually Zobrist hashing is done by scanning the location of pieces, excluding empty squares. The location of empty squares isn't explicitly hashed.

    So you could use a smaller (more cache-friendly) array. Something like:

    std::uint64_t randoms[81][2];  // two players
    
    for (unsigned x(0); x < 81; ++x)
      if (board[x])
        myHash ^= randoms[x][board[x]];