Visualizing Chess Bitboards

When simulating board games on a computer, one of the challenges is keeping track of the game pieces. Bitboards are an efficient way to store game state in (usually) 64-bit integers. There are 64 positions on a chess board so we can use each bit as an on/off switch.

Let's say I want to describe a board where f5 is occupied. For that, we can use the number 67108864. In decimal notation, it doesn't look that much like a chessboard. In hex, we can see that there's a little more structure: 0x0000000004000000.

For me, it starts to make more sense when representated as a binary number with 64 digits.

0 0 0 0 0 0 0 0 I've added a line break
0 0 0 0 0 0 0 0 after every eight numbers
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 1 0 0 looks kinda like a chessboard, right?
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0

We can't pack an entire game's state into a number (e.g. piece color, piece type, castling rights) so we use groups of numbers.

We can use bitwise operations on these numbers to manipulate individual bits using operators like AND, OR, XOR, NOT, and bit shifts. These operations are are among the fastest operations a CPU can perform. Bitboards are also cache-friendly since they pack data into fewer memory locations.

In order to move a piece at f5 forwards we can shift the bitboard left by 8 bits.

pieceOnF5 = 0x0000000004000000;
// Move one rank forward (from f5 to f6)
pieceOnF5 << 8; // 0x0000000400000000

Why 8? Well, if you picture all of a chessboard's squares lined up in one long row, moving upwards would require you to move 8 places to the left.

Masks

A mask is a bitboard used to isolate, modify, or test specific squares using bitwise operations.

An example of a mask might be the A file: 0x0101010101010101. To check if there are any pieces on the A file, we can use bitwise AND.

if (pieces & 0x0101010101010101) {
// At least one piece on the A file
}

Even though I know a little bit about bitboards, I still find them confusing and unintuitive. I'm also prone to mistakes while working with them.

I've found that visual examples (and interactive debugging tools) can be very helpful. Let's take a look at how we can generate the initial white pawn attacks.

Two numbers will probably stick out in the below example; 7 and 9. If you picture that long row of chessboard squares I mentioned before, think about how many squares you'd have to move to be diagonally forwards or backwards from your starting position.

// Initial white pawn attacks
FILE_A = 0x0101010101010101;
FILE_H = 0x8080808080808080;
white_pawns = 0x000000000000FF00;
attacks_left = (white_pawns & ~FILE_A) << 7;
attacks_right = (white_pawns & ~FILE_H) << 9;
pawn_attacks = attacks_left | attacks_right;
H80
G81
F82
E83
D84
C85
B86
A87
H78
G79
F710
E711
D712
C713
B714
A715
H616
G617
F618
E619
D620
C621
B622
A623
H524
G525
F526
E527
D528
C529
B530
A531
H432
G433
F434
E435
D436
C437
B438
A439
H340
G341
F342
E343
D344
C345
B346
A347
H248
G249
F250
E251
D252
C253
B254
A255
H156
G157
F158
E159
D160
C161
B162
A163
Binary: 00000001 00000001 00000001 00000001 00000001 00000001 00000001 00000001
Set bits: none                            

Without bitboards, a program has to perform many more (magnitudes more!) instructions. The equivalent code, without bitwise operations, would look something like this:

attacks = [];
for (i = 0; i < 64; i++) {
piece = board.getPiece(i);
if (piece === "P") { // White pawn
// Check left attack (forward-left)
if (!board.isOnFileA(i)) {
attacks.push(i + 7);
}
// Check right attack (forward-right)
if (!board.isOnFileH(i)) {
attacks.push(i + 9);
}
}
}

And I haven't included the board class, or the data structures being operated on behind-the-scenes.

I've got one more example where we generate a knight's attacks. We start with the position of a knight on the board (encoded in a bitboard) and calculate all the psuedo-legal knight attacks. Remember: the result is also a bitboard.

Inside a chess engine, we would then use more bitwise operations to see if those positions were occupied so we could evaluated the strength of each potential move.

The "not file" masks here are used to prevent invalid wraparounds. From g5, these masks stop the knight from moving to l6 and l4 (which aren't real squares).

// Knight attacks from G5
KNIGHT_POS = 0x0000000002000000;
FILE_A = 0x0101010101010101;
FILE_H = 0x8080808080808080;
FILE_AB = FILE_A | (FILE_A << 1);
FILE_GH = FILE_H | (FILE_H >> 1);
attacks = (KNIGHT_POS << 17 & ~FILE_A) |
(KNIGHT_POS << 15 & ~FILE_H) |
(KNIGHT_POS << 10 & ~FILE_AB) |
(KNIGHT_POS << 6 & ~FILE_GH) |
(KNIGHT_POS >> 17 & ~FILE_H) |
(KNIGHT_POS >> 15 & ~FILE_A) |
(KNIGHT_POS >> 10 & ~FILE_GH) |
(KNIGHT_POS >> 6 & ~FILE_AB);
H80
G81
F82
E83
D84
C85
B86
A87
H78
G79
F710
E711
D712
C713
B714
A715
H616
G617
F618
E619
D620
C621
B622
A623
H524
G525
F526
E527
D528
C529
B530
A531
H432
G433
F434
E435
D436
C437
B438
A439
H340
G341
F342
E343
D344
C345
B346
A347
H248
G249
F250
E251
D252
C253
B254
A255
H156
G157
F158
E159
D160
C161
B162
A163
Binary: 00000000 00000000 00000000 00000000 00000010 00000000 00000000 00000000
Set bits: none                            

I hope this helps show how bitboards can be the building blocks of any kind of chess computation.

In practice, chess engines often pre-compute and store attack masks in lookup tables for even better performance, rather than calculating them on the fly like this – as well as other sophisticated techniques like zobrist hashing and magic bitboards.

For further reading, I've found the Chess Programming Wiki and the source code for python-chess (e.g. calculating attack masks) to both be very useful.

I also have an older post which serves as a gentle introduction to chess engines: Building My Own Chess Engine.