Zobrist Hashing In C++ - C++ Programming Tutorial
C++ Course / Miscellaneous / Zobrist Hashing In C++

Zobrist Hashing In C++

BLUF: Mastering Zobrist Hashing In C++ is a critical step in becoming a proficient C++ developer. This lesson provides a deep dive into the syntax, performance considerations, and real-world applications of this concept.
Key Performance Insight: Zobrist Hashing In C++

C++ is renowned for its efficiency. Learn how Zobrist Hashing In C++ enables low-level control and high-performance computing in the tutorial below.

Introduction to Zobrist Hashing

Zobrist hashing is a hashing technique designed to efficiently generate a distinct numerical value for various board game scenarios, commonly applied in chess, Go, and checkers. This method, conceived by Albert Zobrist during the 1960s, assigns a unique random bitstring to each potential game piece and board layout. By employing XOR (exclusive OR) operations on these bitstrings, it swiftly computes a hash value for any given game setup.

A key advantage of the algorithm is its ability to minimize recalculations when there are changes in the placement of pieces, making it well-suited for integration into fast-paced game engines. When a piece is added, rearranged, or removed, the hashing process can be updated efficiently using the XOR operation instead of recalculating for every board position individually. This efficient updating mechanism, coupled with its low collision rates, positions Zobrist hashing as a valuable asset in AI decision-making scenarios, particularly in situations requiring millions of board assessments.

Why is Zobrist hashing effective for board games like Chess and Go?

  • Zobrist hashing is highly effective for board games like chess and Go due to its speed, efficiency, and incremental nature. It allows for rapid updates to the hash value of a board position when only small changes occur, such as when a piece is moved, placed, or removed. This is crucial in games like chess or go, where each move alters only a small portion of the board. Rather than recalculating the entire board's hash from scratch, Zobrist hashing enables incremental updates using the XOR operation. This minimizes computation and speeds up game evaluations.
  • Additionally, Zobrist hashing provides a compact and unique representation of board states, which can be stored in hash tables (called transposition tables) to avoid redundant recalculations. This is especially so in games such as chess where the same position of the board can be arrived at through different sequences of moves. By comparing Zobrist hashes, the engine can easily find out whether the position was evaluated earlier and use the results.
  • The use of random bitstrings for each piece-position combination ensures a low collision rate, meaning different board configurations are unlikely to produce the same hash. This accuracy, combined with its efficiency, makes Zobrist hashing ideal for AI in board games, where millions of positions must be evaluated quickly for optimal decision-making.
  • How does Zobrist Hashing work?

    1. Zobrist hashing basic concepts

  • Zobrist hashing is a technique whereby a fresh hash value corresponding to the current state of the position of a game is created by giving a new value to each piece on every square or position on the board at random. The purpose is to design the data structure as simple and fast to reorganize as needed in the middle of a game. Zobrist hashing computes an entire board hash. That can be sufficiently replaced by small movements that require minimal computations of the same.
  • Every piece and position may also be characterized by the specific bit string number assigned randomly (e.g. in chess case, each white pawn on a particular square).h piece on every possible square or position on the board. The purpose is to have an optimized display of the board that is easy to update with new values on the fly during the game. While the conventional board hashing will require computation of a new hash from the movement of the piece from one cell to another, Zobrist hashing only demands a small incremental calculation, hence reducing computation time greatly.
  • 2. XOR operation and its role in hashing

The XOR (exclusive OR) operation plays a vital role in Zobrist hashing. It possesses characteristics that lend themselves well to the efficient update of hash values:

  • Maintains commutativity: The sequence of operations does not impact the result.
  • Self-reversing: Repeatedly XORing with the same value will revert the value back to its original state.

For instance, when a piece transitions from one square to another, you have the option to XOR the bitstring representing the initial position and XOR the bitstring representing the new position to modify the hash accordingly.

3. Generating random bitstrings for hashing board states

  • To generate random bitstrings, Zobrist hashing assigns a unique random bitstring to every possible piece-position pair on the board. For example, if chess were played with 64 squares and the 12 types of pieces as black and white, then there would be 64 x 12 = random bitstrings. These bitstrings range from 64 bits, which makes it possible to achieve uniqueness and reduce the possibility of getting two different board states yielding the same hash
  • After all the bitstrings are given to all the pieces, a hash of the first board state can be calculated as a bitwise XOR of all bitstrings of pieces in starting positions square or position on the board. The goal is to create a compact and efficient representation of the board that can be quickly updated as the game progresses. Instead of recalculating the entire board's hash every time a move is made, Zobrist hashing allows for small, incremental updates that require minimal computational effort.
  • Implementing Zobrist Hashing in C++

Below is a detailed walkthrough on how to integrate Zobrist hashing for a board game, like chess, in C++:

Zobrist hashing is a strategy employed to efficiently hash board configurations in games such as Go and various two-player games, distinct from chess. This method enables rapid updates to hash values as the game state undergoes transitions.

Steps to Implement Zobrist Hashing

1. Initialization

Zobrist hashing depends on precalculated random bit strings assigned to individual game pieces and board positions. A 2D array is necessary to store these unique bit strings corresponding to each piece and position on the board.

Example

const int BOARD_SIZE = 64; // Example: chessboard has 64 squares
const int PIECE_TYPES = 12; // 6 pieces for each player in chess
unsigned long long zobristTable[PIECE_TYPES][BOARD_SIZE];

void initializeZobristTable() {
    srand(time(NULL)); // Seed for randomness
    for (int i = 0; i < PIECE_TYPES; ++i) {
        for (int j = 0; j < BOARD_SIZE; ++j) {
            zobristTable[i][j] = ((unsigned long long)rand() << 32) | rand();
        }
    }
}

2. Hash Calculation

For every chess piece on the board, perform an XOR operation with its respective bitstring to update the overall hash value.

Example

unsigned long long computeZobristHash(const int board[BOARD_SIZE], const int pieceMapping[BOARD_SIZE]) {
    unsigned long long hash = 0;
    for (int i = 0; i < BOARD_SIZE; ++i) {
        if (board[i] != -1) { // -1 indicates an empty square
            hash ^= zobristTable[pieceMapping[board[i]]][i];
        }
    }
    return hash;
}

3. Incremental Updates

When a change is executed (such as adding, deleting, or relocating a piece), the hash should be updated incrementally by performing XOR operation on the bitstrings of the pieces that are affected.

Example

void updateZobristHash(unsigned long long &hash, int piece, int from, int to) {
    hash ^= zobristTable[piece][from]; // Remove piece from old position
    hash ^= zobristTable[piece][to];   // Add piece to new position
}

Example 1:

Here is a basic illustration of implementing Zobrist hashing in a game similar to chess:

Example

#include <iostream>
#include <cstdlib>
#include <ctime>

const int BOARD_SIZE = 64;
const int PIECE_TYPES = 12;

unsigned long long zobristTable[PIECE_TYPES][BOARD_SIZE];

void initializeZobristTable() {
    srand(time(NULL));
    for (int i = 0; i < PIECE_TYPES; ++i) {
        for (int j = 0; j < BOARD_SIZE; ++j) {
            zobristTable[i][j] = ((unsigned long long)rand() << 32) | rand();
        }
    }
}

unsigned long long computeZobristHash(const int board[BOARD_SIZE], const int pieceMapping[BOARD_SIZE]) {
    unsigned long long hash = 0;
    for (int i = 0; i < BOARD_SIZE; ++i) {
        if (board[i] != -1) {
            hash ^= zobristTable[pieceMapping[board[i]]][i];
        }
    }
    return hash;
}

void updateZobristHash(unsigned long long &hash, int piece, int from, int to) {
    hash ^= zobristTable[piece][from];
    hash ^= zobristTable[piece][to];
}

int main() {
    initializeZobristTable();
    
    int board[BOARD_SIZE] = { /* Initialize with piece positions */ };
    int pieceMapping[BOARD_SIZE] = { /* Piece type mappings */ };

    unsigned long long zobristHash = computeZobristHash(board, pieceMapping);
    std::cout << "Initial Zobrist Hash: " << zobristHash << std::endl;
    
    return 0;
}

Output:

Output

Initial Zobrist Hash: 4443322467265810049

Example 2:

Example

#include <iostream>
#include <cstdlib>
#include <ctime>

const int BOARD_SIZE = 64;
const int PIECE_TYPES = 12;

unsigned long long zobristTable[PIECE_TYPES][BOARD_SIZE];

void initializeZobristTable() {
    srand(time(NULL));
    for (int i = 0; i < PIECE_TYPES; ++i) {
        for (int j = 0; j < BOARD_SIZE; ++j) {
            zobristTable[i][j] = ((unsigned long long)rand() << 32) | rand();
        }
    }
}

unsigned long long computeZobristHash(const int board[BOARD_SIZE], const int pieceMapping[BOARD_SIZE]) {
    unsigned long long hash = 0;
    for (int i = 0; i < BOARD_SIZE; ++i) {
        if (board[i] != -1) {
            hash ^= zobristTable[pieceMapping[board[i]]][i];
        }
    }
    return hash;
}

void updateZobristHash(unsigned long long &hash, int piece, int from, int to) {
    hash ^= zobristTable[piece][from];
    hash ^= zobristTable[piece][to];
}

int main() {
    //Initialize the Zobrist table
    initializeZobristTable();
    
    // Example board configuration (using -1 for empty squares)
    int board[BOARD_SIZE] = {
        0, 1, 2, 3, 4, 2, 1, 0, // Row 0 (Black pieces)
        -1, -1, -1, -1, -1, -1, -1, -1, // Row 1
        -1, -1, -1, -1, -1, -1, -1, -1, // Row 2
        -1, -1, -1, -1, -1, -1, -1, -1, // Row 3
        -1, -1, -1, -1, -1, -1, -1, -1, // Row 4
        -1, -1, -1, -1, -1, -1, -1, -1, // Row 5
        5, 6, 7, 8, 9, 7, 6, 5  // Row 6 (White pieces)
    };

    // Piece mapping: Example mapping for piece types (0: King, 1: Queen, 2: Rook, 3: Bishop, 4: Knight, 5: Pawn)
    int pieceMapping[BOARD_SIZE] = {
        2, 1, 0, 3, 4, 0, 1, 2, // Black pieces
        -1, -1, -1, -1, -1, -1, -1, -1, // Empty squares
        -1, -1, -1, -1, -1, -1, -1, -1, // Empty squares
        -1, -1, -1, -1, -1, -1, -1, -1, // Empty squares
        -1, -1, -1, -1, -1, -1, -1, -1, // Empty squares
        -1, -1, -1, -1, -1, -1, -1, -1, // Empty squares
        5, 6, 7, 8, 9, 7, 6, 5  // White pieces
    };

    // Compute the initial Zobrist hash
    unsigned long long zobristHash = computeZobristHash(board, pieceMapping);
    std::cout << "Initial Zobrist Hash: " << zobristHash << std::endl;

    return 0;
}

Output:

Output

Initial Zobrist Hash: 8714018581147955026

Optimizing Zobrist Hashing in C++

1. Reducing collisions in Zobrist hashing

Nevertheless, as any hash table stemming from the fact that hash space is finite is prone to collisions, they are not excluded in Zobrist hashing. To further reduce collisions:

  • Use Larger Hash Sizes: By simply expanding the bit size of the hash from 64-bit to 128-bit, the degree of collision is greatly reduced exponentially. As the size of the hash increases, more memory is required, but the number of possible hash values is also greater.
  • Randomized Bitstrings: Generate high quality random numbers in order to create the corresponding bitstring for each piece-square pair. This makes sure that the distribution of the hash values is spread and doesn't have repetitions that may cause collision.
  • Double Hashing: Many distinct Zobrist hash functions should be used. Just like when you keep two different hash values (for example, Zobrist and another cryptographic hash), you slice the chances of getting a false positive greatly.
  • 2. Performance optimization techniques

To enhance the performance of Zobrist hashing:

  • Incremental Updates: In the moves, only the part of the hash that is relevant must be updated, and this is through XOR operations. This is the primary advantage of Zobrist hashing, avoiding computational delay.
  • Transposition Tables: One of of the techniques utilized is the ability to cache previous state evaluations through transposition tables. This cuts off the need to constantly compute a given position and accelerates the assessment of similar positions.
  • Parallel Processing: If your application supports multithreading, you should parallelize fragments of the used evaluation process. Zobrist hashing can be updated in parallel across a thread to update for different positions.
  • 3. Memory management and efficiency considerations

Effective memory management is crucial for Zobrist hashing:

  • Compact data structures : To save information from memory, store Zobrist bitstrings and transposition tables in a small data structure. It is therefore recommendable to make use of structures that enable easy storage and retrieval.
  • Fixed-size Arrays : Instead of using dynamic memory allocation for the Zobrist table and board representation, use fixed-size arrays. This makes distribution less fragmented and improves the cache efficiency.
  • Memory Pools: Develop memory pools for the transposition tables. This approach lowers the overhead incurred with the allocation and deallocation of objects as well as being more efficient and a less fragmented memory space.
  • Use Cases of Zobrist Hashing

Zobrist hashing proves to be extremely valuable in board games and different artificial intelligence scenarios because of its effective management of state representation and collision resolution. Below are some primary applications:

1. Chess Engines

Zobrist hashing is used by chess engines to evaluate an enormous number of board positions in a relatively short time. The engine can:

  • Store Previous States: It is done through the transposition table created with the use of Zobrist hash, which was read by the engine and contains the previously analyzed positions.
  • Handle Incremental Updates: Whenever the movement is done, only the associated section of the hash is modified with the exclusive OR operations. It permits the switch from one state in the game to another to be provided as fast as possible.
  • Recognize Repeated Positions: Thus, Zobrist hashing is used as a way to check for repeated positions, which is very important for detecting a draw and maximizing the engine's chances.
  • 2. Go Game AI

In the Go programming language, Zobrist hashing is employed for larger game boards, typically with dimensions of 19 by 19. Implementing Zobrist hashing in Go engines provides various benefits:

  • Enhanced State Evaluation Efficiency: Zobrist hashing simplifies the process of evaluating board states for AI, enabling it to analyze potential moves more efficiently.
  • Optimized Memory Usage: By creating a compact representation of board states, Zobrist hashing helps conserve memory, a critical aspect given the vast number of states in the game of Go.
  • 3. Checkers and Othello

Other dual-player board games such as Checkers and Reversi also make use of Zobrist hashing to effectively handle game states:

  • Game State Caching: Both games utilize transposition tables to store and retrieve optimal board configurations, avoiding redundant computations by the AI for identical positions.
  • Quick Move Assessment: Zobrist hashing enables swift adjustments when pieces are added or relocated, simplifying game analysis.
  • 4. Game Simulators and Training Environments

Zobrist hashing is applied in different game simulators and artificial intelligence training environments for efficient state representation management:

  • Game Scenarios Simulation: Zobrist hashing facilitates the storage and retrieval of actual game states when multiple game scenarios are executed concurrently.
  • Training for Reinforcement Learning: By using Zobrist hashing, a history of states can be maintained to evaluate the effectiveness of various strategies in model training.
  • 5. Computer Vision and Image Recognition

Although commonly linked with games, Zobrist hashing can be repurposed for use in computer vision:

  • Object Identification: Zobrist hashing proves valuable in hashing object characteristics or designs, facilitating streamlined comparison and identification of akin objects within images.
  • Comparing Zobrist Hashing with Other Hashing Techniques

Zobrist hashing serves as a specialized hashing method commonly applied in game AI, especially in strategic board games such as chess and Go. Unlike conventional cryptographic hash functions like MD5 and SHA (Secure Hash Algorithm) that find broad use across various applications, Zobrist hashing presents unique benefits specifically tailored for game AI contexts.

Zobrist vs. other hash functions (e.g., MD5, SHA)

  • Purpose and Speed: Zobrist hashing is designed for use in games that need to use game states, and one can easily update the result when pieces move or change. It uses XOR operations to update this hash incrementally, which will be faster than cryptographic hash functions, which, in general, are optimized for performance rather than speed.
  • Collision Management: Although there may be a number of collisions with MD5 and SHA where they are used in order to maximize the amount of uniqueness it is short, there is a trade-off to using these two types of hashes, they take longer to compute because of the complexity of their algorithms. Zobrist hashing, on the other hand, has a lower collision probability because the bitstring mechanism is created using random numbers, and as such, state evaluations of games can be made faster without the added burden of collision resistance appropriate for a hash in a security context.
  • Memory Efficiency: Compared to the output of standard cryptographic hash functions, Zobrist hashing employs a small representation specific to games boards, thus incurring less memory overhead. It is this efficiency that makes it possible to handle many game states in real time given the game environments that are most common.
  • Why is Zobrist preferable in game AI scenarios?

  • Incremental Updates: Zobrist hashing is most beneficial in the context of updates since, ideally, our hash function should come up with updated hash values for the change in the board state. This feature will be especially helpful in board games, as moves occur often, and re-computation of the hash from the raw board state would be time-consuming.
  • Transposition Tables: Zobrist hashing supports the use of transposition tables to help the artificial intelligence cache evaluated positions properly so that the artificial intelligence can readily distinguish between previously evaluated positions and current positions, which is important for the optimization of the games.
  • Conclusion:

Nonetheless, Zobrist hashing stands out as a method for structuring and assessing game board configurations in Artificial Intelligence scenarios, like chess, Go, checkers, and Othello. By leveraging XOR operation characteristics with random bit sequences, Zobrist hashing eliminates the need to fully recompute hash values every time. This circumvents the usual need for excessive computational power, leading to remarkably swift incremental updates. Such efficiency is crucial when analyzing millions of potential game moves that require prompt decision-making.

A benefit of the method is its low collision rate, which allows AI engines to recognize previously suggested positions using transposition tables. This feature not only speeds up the evaluation process but also enhances gameplay by leveraging outcomes from past calculations.

Furthermore, the concept of Zobrist hashing can be applied in a broader array of situations, ranging from simulating games to training artificial intelligence, and even extending to its application in diverse areas like computer vision. By optimizing RAM usage over ROM and implementing space-efficient data types, software engineers can establish robust computational settings that effectively handle complex game configurations.

In contrast to conventional cryptographic hash functions, Zobrist hashing is tailor-made for game AI applications, providing enhanced speed, efficiency, and minimal memory footprint. These characteristics render it ideal for scenarios requiring rapid state assessments and optimal decision-making in dynamic environments. Ultimately, the methodologies associated with Zobrist hashing continue to be a significant asset in the realms of computer science and game development, shaping the landscape of AI and gameplay for both present and future advancements.

Input Required

This code uses input(). Please provide values below:

Logic Practice
Install Logic Practice
Add to home screen for a faster app-like experience