Bitboard Notes

Table of Contents

1 Definitions

RACT
Data structure with random access in constant time
LSB
Least significant bit
MSB
Most significant bit

2 Conclusions

2.1 General

2.1.1 Precomputing bitmaps

A theme throughout Hyatt's document is to precompute bitmaps for later use, in order to decrease computation time needed while the chess program is running. The precomputed bitmaps are often stored in a data structure with O(1) random access behavior, to enable an efficient lookup.

Precomputed bitmaps can be stored on disk to be read every time the chess program starts or calculated at the very beginning of the chess program, so that the computation of such bitmaps does not negatively affect the performance chess program later.

2.1.2 Calculations with bitmaps

Calculations with bitmaps should make as much use as possible of bitwise logical operations, instead of iterating through the bits of a bitmap / an integer. Some bitwise logical operations:

  • and
  • or
  • not
  • xor
  • nand

2.1.3 Mapping coordinates to bits in bitmaps or integers

Generally speaking, this mapping can be done arbitrarily, as long as it is consistend.

Hyatt chooses a mapping, which is quite "chess centric". He defined the square A1 to be the MSB in the bitmap or integer and chooses to enumerate squares row by row, so that the squares A1-H1 are the 8 MSBs in the bitmap or integer.

It would also make sense to have the convention of the lowest coordinates (A1) being the LSB.

The question is how to represent this convention in the code.

TODO: Describe how one could represent the coordinates->bitposition mapping in the code.

TODO: Think about the following idea. Is it a valid one?: One can imagine some data structure with O(1) random access complexity, which for every square stores the bitmap containing a 1 bit in the position, which is internally used to do all calculations. The index to this data structure would be the another bitmap, which shall be mapped.

2.1.4 Stored bitmaps

There is a number of bitmaps one might want to store, to improve performance, by avoiding computation of those bitmaps on-the-fly:

  • bitmaps for piece types
    • pawns
    • knights
    • bishops
    • rooks
    • queens
    • kings
  • bitmaps for square occupation
    • occupied squares
    • white pieces
    • black pieces
  • bitmaps for movement of sliding pieces
    • plus1
    • plus7
    • plus8
    • plus9
    • minus1
    • minus7
    • minus8
    • minus9
    • rank-attacks
    • file-attacks
    • diagonal-a1-h8-attacks
    • diagonal-h1-a8-attacks
  • bitmaps for non-sliding piece moves
    • knight-attacks
    • pawn-attacks
    • pawn-moves
    • king-moves
  • bitmaps for single moves
    • set-mask

Of some of those bitmaps or RACTs containing bitmaps, one probably wants to store multiples, for each rotation one.

2.2 Not rotated bitmaps

2.2.1 How to move a piece?

A piece can be moved in any bitmap, rotated or not, using the same approach:

occupied-squares XOR (from OR to)

Where:

  • occupied-squares: bitmap which contains 1 bits for all positions occupied by a piece
  • from: bitmap which contains a 1 bit at the position, from where the piece moves
  • to: bitmap which contains a 1 bit at the position, to where the piece moves

Hyatt names both from and to "set_mask", because they are actually from an array/vector of 64 bitmaps, each being a bitmap, which only has one 1 bit set and each having a different 1 bit set. For example:

  • set_mask[0] -> bitmap with a 1 bit at the position of the square A1
  • set_mask[31] -> bitmap with a 1 bit at the position of the square H4
  • set_mask[32] -> bitmap with a 1 bit at the position of the square A5
  • set_mask[63] -> bitmap with a 1 bit at the position of the square H8

The set_mask array/vector is precalculated in order to make using any of the positions only a lookup in the array. It could be done by calculating 2^postion, but that might not work for rotated bitmaps.

2.2.2 How to take a piece of the other player?

???

2.2.3 How to calculate truncated rays of sliding pieces (bishop, rook, queen)?

This is useful for example for calculating squares a bishop, rook or queen could move to. However, the approach when using rotated bitmaps is different.

In a not rotated bitmap there is always the same number of positions to shift to get from one square on a ray to the next square. For example:

  • Bishop on A1:
    • Lets assume, that …
      • (1) A1 is position 0 in the integer and …
      • (2) we enumerate squares row by row, from left to right from the perspective of the player with the white pieces.
    • The next square on a ray of movement of the bishop will be B2.
    • This would be at position 9.
    • The next square would be C3.
    • This would be at position 18.
    • The difference in position for the bits of this ray of movement is always 9.

Based on this, the idea is to store/precalculate rays in 8 directions for all squares, so that the rays do not need to be calculated when the chess programm is running.

This would consume the following memory:

  • 64 squares
  • 8 rays per square
  • 64 bit per bitmap in chess
  • 64 * 64 * 8 bit = 32768 bit = 4096 byte = 4 kilobyte

Not very much these days.

Hyatt calls the arrays/vectors storing the rays by their 1 bit square position distances:

  • plus1
  • plus7
  • plus8
  • plus9
  • minus1
  • minus7
  • minus8
  • minus9

Each of those carrying the rays in one direction for all 64 squares in chess.

To calculate valid moves of a sliding piece also considering occupied squares, one can do the following algorithm:

  1. get the position pos of the moving piece
  2. get the bitmap for the ray of the direction and position ray[pos], where ray is one of the arrays/vectors containing the rays for one direction
  3. use bitwise and operation with the occupied-squares and ray[pos], to get ray-blockers, a bitmap of all blocked squares of the ray
  4. get the first blocked square first-blocked from ray-blockers, by looking for the first bit or last bit (depending on the direction of the ray) in the integer that is ray-blockers.
  5. get the ray (truncator-ray) in the same direction as the ray from the piece, by looking up ray[first-blocked].
  6. use bitwise xor operation of truncator-ray and ray[pos] to get truncated-ray.
  7. use bitwise and operation of first-blocked and occupied-same-color-as-piece to get a bitmap first-blocked-own-piece?, which will be not equal to zero, if the color of the blocking piece at first-blocked is the same as the color of the moving piece and zero if the piece at first-blocked has a different color than the moving piece.
  8. use bitwise xor of first-blocked-own-piece? and truncated-ray, to get the final bitmap of possible moves of the sliding piece along the ray.

Here is an ASCII art example:

The bitmap with the piece, a bishop in this case, at pos:

0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 B 0
0 0 0 0 0

The not yet truncated ray plus7[pos]:

1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 B 0
0 0 0 0 0

The occupied-squares bitmap:

1 0 0 1 0
0 1 1 1 1
0 0 0 0 0
0 1 0 B 0
1 0 1 0 0

The bitwise and operation result of occupied-squares and plus7[pos]:

1 0 0 1 0         1 0 0 0 0        1 0 0 0 0
0 1 1 1 1         0 1 0 0 0        0 1 0 0 0
0 0 0 0 0   AND   0 0 1 0 0   ->   0 0 0 0 0
0 1 0 B 0         0 0 0 B 0        0 0 0 B 0
1 0 1 0 0         0 0 0 0 0        0 0 0 0 0

The first-blocked bitmap:

0 0 0 0 0
0 1 0 0 0
0 0 0 0 0
0 0 0 B 0
0 0 0 0 0

The plus7[first-blocked] ray bitmap, which is truncator-ray:

1 0 0 0 0
0 F 0 0 0
0 0 0 0 0
0 0 0 B 0
0 0 0 0 0

The bitwise xor operation result of truncator-ray and plus7[pos], which is truncated-ray:

1 0 0 0 0         1 0 0 0 0        0 0 0 0 0
0 F 0 0 0         0 1 0 0 0        0 1 0 0 0
0 0 0 0 0   XOR   0 0 1 0 0  ->    0 0 1 0 0
0 0 0 B 0         0 0 0 B 0        0 0 0 B 0
0 0 0 0 0         0 0 0 0 0        0 0 0 0 0

The occupied-same-color-as-piece bitmap:

0 0 0 0 0
0 1 1 0 0
0 0 0 0 0
0 1 0 B 0
1 0 1 0 0

The bitwise and operation result of occupied-same-color-as-piece and first-blocked, which is first-blocked-own-piece?:

0 0 0 0 0         0 0 0 0 0        0 0 0 0 0
0 1 1 0 0         0 1 0 0 0        0 1 0 0 0
0 0 0 0 0   AND   0 0 0 0 0   ->   0 0 0 0 0
0 1 0 B 0         0 0 0 B 0        0 0 0 B 0
1 0 1 0 0         0 0 0 0 0        0 0 0 0 0

So the piece blocking is of the same color and thus cannot be taken, which means the square is also not available as destination square for the moving bishop.

The bitwise xor operation result of truncated-ray and first-blocked-own-piece?, which is valid-moves-along-ray:

0 0 0 0 0         0 0 0 0 0        0 0 0 0 0
0 1 0 0 0         0 1 0 0 0        0 0 0 0 0
0 0 1 0 0   XOR   0 0 0 0 0   ->   0 0 1 0 0
0 0 0 B 0         0 0 0 B 0        0 0 0 B 0
0 0 0 0 0         0 0 0 0 0        0 0 0 0 0

The same algorithm can be used for movement along ranks or files.

  1. Algorithm

    Hyatt writes the algorithm for calculating all valid moves for a bishop as follows:

    diag_attacks=plus7[F5];
    blockers=diag_attacks & occupied_squares;
    blocking_square=FirstOne(blockers);
    bishop_attacks=diag_attacks^plus7[blocking_square];
    
    diag_attacks=plus9[F5];
    blockers=diag_attacks & occupied_squares;
    blocking_square=FirstOne(blockers);
    bishop_attacks|=diag_attacks^plus9[blocking_square];
    
    diag_attacks=minus7[F5];
    blockers=diag_attacks & occupied_squares;
    blocking_square=LastOne(blockers);
    bishop_attacks|=diag_attacks^minus7[blocking_square];
    
    diag_attacks=minus9[F5];
    blockers=diag_attacks & occupied_squares;
    blocking_square=LastOne(blockers);
    bishop_attacks|=diag_attacks^minus9[blocking_square];
    

    This does not take into consideration, that a square may be blocked by a same or opposite colored piece for simplicity. Lets write this down in a more readable way, to get a better understanding:

    1. Initialize the bishop-attacks as a bitmap with all bits set to 0 (false).
    2. For each ray or direction of movement do the following:
      1. calculate the ray or diagonal attacks from the bishop square
        1. look it up in the RACT of the diagonal with the square as index
      2. calculate the blocked squares on the ray
        1. bitwise logical AND of diagonal-attacks and occupied squares
      3. calculate the first blocked square blocking-square on the diagonal
      4. calculate the truncated-ray
        1. calculate truncator-ray
          1. lookup in the RACT of the diagonal with the blocking-square as index
        2. bitwise logical XOR of diagonal-attacks and truncator-ray
      5. update the bishop-attacks
        1. bitwise logical OR of bishop-attacks and truncated-ray

2.2.4 How to rotate bitmaps?

Do not actually rotate bitmaps. Instead of rotating bitmaps on-the-fly, one can keep 4 of bitmaps for each kind of bitmap, one for each rotation:

  1. rotation by 0°
  2. rotation by 90°
  3. rotation by 45°
  4. rotation by -45°

This way using rotated bitmaps becomes less computationally expensive.

The difficulty is then in the way of accessing these rotated bitmaps and updating the 4 variations of different kinds of bitmaps.

2.2.5 What do rotated bitmaps look like?

  1. Rotation of 0°
    A8 B8 C8 D8 E8 F8 G8 H8
    A7 B7 C7 D7 E7 F7 G7 H7
    A6 B6 C6 D6 E6 F6 G6 H6
    A5 B5 C5 D5 E5 F5 G5 H5
    A4 B4 C4 D4 E4 F4 G4 H4
    A3 B3 C3 D3 E3 F3 G3 H3
    A2 B2 C2 D2 E2 F2 G2 H2
    A1 B1 C1 D1 E1 F1 G1 H1
    
  2. Rotation of 90°
    H8 H7 H6 H5 H4 H3 H2 H1
    G8 G7 G6 G5 G4 G3 G2 G1
    F8 F7 F6 F5 F4 F3 F2 F1
    E8 E7 E6 E5 E4 E3 E2 E1
    D8 D7 D6 D5 D4 D3 D2 D1
    C8 C7 C6 C5 C4 C3 C2 C1
    B8 B7 B6 B5 B4 B3 B2 B1
    A8 A7 A6 A5 A4 A3 A2 A1
    
  3. Rotation of 45°
                   A8
                 A7  B8
               A6  B7  C8
             A5  B6  C7  D8
           A4  B5  C6  D7  E8
         A3  B4  C5  D6  E7  F8
       A2  B3  C4  D5  E6  F7  G8
     A1  B2  C3  D4  E5  F6  G7  H8
       B1  C2  D3  E4  F5  G6  H7
         C1  D2  E3  F4  G5  H6
           D1  E2  F3  G4  H5
             E1  F2  G3  H4
               F1  G2  H3
                 G1  H2
                   H1
    

    The positions in the bitmap or integer could be mapped to the following squares:

    • 0: H1
    • 1: G1
    • 2: H2
    • 61: A7
    • 62: B8
    • 63: A8

    Or in reverse.

  4. Rotation of -45°
                  H8
                G8  H7
              F8  G7  H6
            E8  F7  G6  H5
          D8  E7  F6  G5  H4
        C8  D7  E6  F5  G4  H3
      B8  C7  D6  E5  F4  G3  H2
    A8  B7  C6  D5  E4  F3  G2  H1
      A7  B6  C5  D4  E3  F2  G1
        A6  B5  C4  D3  E2  F1
          A5  B4  C3  D2  E1
            A4  B3  C2  D1
              A3  B2  C1
                A2  B1
                  A1
    

    The positions in the bitmap or integer could be mapped to the following squares:

    • 0: A1
    • 1: A2
    • 2: B1
    • 61: G8
    • 62: H7
    • 63: H8

    Or in reverse.

2.3 Rotated Bitmaps

2.3.1 How to calculate sliding moves along a rank?

A rank is represented by 8 bits in the bitmaps for chess.

A rank has 8 squares, which can all either be occupied or not occupied. There are 2^8 configurations. For each of these configurations, the valid moves of a piece sliding along a rank can be calculated. Those calculated valid moves bitmaps of 8 bits length are stored in a RACT.

To know which index to the RACT is needed to get the correct valid moves bitmap out of it, one can use a trick:

  1. Bitshift the bitmap of occupied-squares so that the rank of interest is at the 8 LSBs of the bitmap.
  2. Do logical operation AND of the bitshifted bitmap and 255 (which is 56 bits 0 and 8 bits 1). This will ensure, that everything except the rank at the 8 LSBs is 0 in the resulting bitmap rank-only. This means, that the result will be an integer between 0 and 255.
  3. Use the rank-only bitmap as an index to the RACT storing the valid moves of a rank-sliding piece. The valid moves bitmaps must be put into the RACT in such a way, that the index into the RACT always maps to the correct valid moves bitmap.

This way of looking up the valid moves is possible, becaues the bits of a rank are all stored next to each other in the bitmap representing the occupied-squares.

If a piece of the same color is occupying the target square, the piece wanting to move there cannot. However, if it is a piece of the other color blocking the square, it can be taken. This means, that is does make a difference, what color the piece on that square is. To distinguish between same color occupation and opposite color occupation blocking a square, one can bitwise logical XOR the valid moves bitmap with the bitmap for same color pieces occupation of squares. This way the squares at the end of the rays of valid moves will either become a 0 or a 1, depending on whether a piece of the same color or a piece of the opposite color is blocking the square.

  1. The problem with diagonals

    Calculating the valid moves of a sliding move piece along a rank can be done as explained above. The valid moved along files can be calculate the same way, in the 90° rotated bitmap. However, with diagonals there is a problem:

    Diagonals are of inequal lengths. The difference of length between neighboring dialgonals is always 1. The different lengths mean, that we cannot simply bitshift by multiples of 8 until the diagonal we want is at the LSBs.

2.3.2 How to get a diagonal from a 45° rotated bitmap?

Lets look at the rotated bitmap again:

              H8
            G8  H7
          F8  G7  H6
        E8  F7  G6  H5
      D8  E7  F6  G5  H4
    C8  D7  E6  F5  G4  H3
  B8  C7  D6  E5  F4  G3  H2
A8  B7  C6  D5  E4  F3  G2  H1
  A7  B6  C5  D4  E3  F2  G1
    A6  B5  C4  D3  E2  F1
      A5  B4  C3  D2  E1
        A4  B3  C2  D1
          A3  B2  C1
            A2  B1
              A1

We write this down Hyatt's mapping of bit positions to squares for this bitmap:

    A1 A2 B1 A3 B2 C1 A4 B3 C2 D1 A5 B4 C3 D2 F1 A6 B5 C4 D3 E2 F1 A7 B6 C5 D4 E3 F2 G1 A8 B7 C6 D5 ...
MSB 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 ...

... E4 F3 G2 H1 B8 C7 D6 E5 F4 G3 H2 C8 D7 E6 F5 G4 H3 D8 E7 F6 G5 H4 E8 F7 G6 H5 F8 G7 H6 G8 H7 H8
... 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 LSB

Let us say, that we want to calculate the bishop moves from the square D2. On an empty board this should include the following squares:

  • E1
  • C1
  • E3 F4 G5 H6
  • C3 B4 A5

Author: Zelphir Kaltstahl

Validate