June 10, 2024
Representing the board state using bitboards and FEN notation
The first step in writing a chess engine is deciding how to represent the board state. This choice will heavily affect the process and efficiency of move generation, an integral part of the chess engine.
Modern computers are built and optimized to work with 64-bit values very efficiently. Conveniently, there are also exactly 64 squares on a chess board. This is the inspiration behind a bitboard, which is simply a 64 bit integer, able to store 1 bit of information per square on the board.
If you're familiar with binary, the most-significant bit (MSB) corresponds to the upper-left square a8
, while the least-significant bit (LSB) corresponds to the bottom-right square, h1
. The entire board as bit indices, 0
for MSB to 63
for LSB, is as follows:
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
Since there can be 12 different pieces per square, we need to decide what data to store to have full information on the board, but without working with too many bitboards. Typically, engines store 8 different bitboards to represent the current positions on a full chess board:
colors: [Bitboard; 2]
, indexed by Black
and White
pieces: [Bitboard; 6]
, indexed by King
, Queen
, Rook
, Bishop
, Knight
, and Pawn
For example, some bitboards describing the initial position would be stored as follows:
colors[Black]
colors[White]
pieces[Pawn]
pieces[Rook]
Since we are working with simple integers, we can use quick bitwise operations to find the intersection and union of any set of bitboards, using bitwise AND
and OR
, respectively. Say we want to find all black pawns:
colors[Black] & pieces[Pawn]
There is some more data that has to be stored apart from piece positions, such as the current moving color and castling rights. This is stored in a separate GameState
struct, which when combined with the colors
and pieces
bitboard lists, make up the basis of a Board
.
struct Board {
pieces: [Bitboard; 6], // array of 6 bitboards, one per piece type
colors: [Bitboard; 2], // array of 2 bitboards, one per color
game_state: GameState, // remaining non-bitboard game values
}
struct GameState {
current_turn: Color, // current moving color
castle_rights: CastleRights, // stores 4 booleans, king/queen-side per color
en_passant_square: Option<Square>, // if there is, stores en passant target square
halfmove: u32, // halfmove counter, incremented after each color's move
fullmove: u32, // fullmove counter, only incremented after black's move
}
Another way to represent the state of a chess game is through Forsyth-Edwards Notation, or FEN.
A FEN string is a moderately short ASCII string, which like our Board
struct, contains all of the necessary information to represent any board state. For example, this is the FEN string for the initial position:
rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
Notice the FEN string is 6 space-separated fields, each describing part of the game state:
rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR
/
-separated segments, with each segment storing a rank of the board, starting from the top-most rank.KkQqRrBbNnPp
), there is a piece on this square, K
for King
, Q
for Queen
, R
for Rook
, B
for Bishop
, N
for Knight
, P
for Pawn
. Uppercase symbols are White
pieces and lowercase are Black
pieces.1-8
), that number designates how many empty squares there are in a row.w
w
for White
, b
for Black
KQkq
K
and queenside Q
castling, uppercase for White
and lowercase for Black
.-
fills this segment.-
d3
).-
fills this segment.0
1
1
Having a standardized and easily parsable format is convenient for importing and exporting chess positions, especially because the internals of different chess engine vary wildly.