Game State
The GameState struct is really simple. It is just a collection of variables which represent the state of things within the game that you can’t immediately see when looking at a position. This is the struct:
pub struct GameState {
pub active_color: Side,
pub castling: Bitboard,
pub half_move_clock: u8,
pub en_passant: Option<Square>,
pub fullmove_number: u16,
pub zobrist_key: u64,
pub phase_value: i32,
pub eval_base: EverySide<EvalBase>,
pub next_move: Move,
}
These are the descriptions for each of the state variables:
| Variable | Meaning |
|---|---|
| 1. active_color | Side to move |
| 2. castling | Castling permissions |
| 3. half_move_clock | Half moves played |
| 4. en_passant | Active en-passant square, if any |
| 5. fullmove_number | Total number of full moves played |
| 6. zobrist_key | Zobrist Key; unique per position |
| 7. phase_value | Evaluation Phase Value |
| 8. eval_base | Basis for positional evaluation |
| 9. next_move | The move played in this position |
The first 5 variable names are chosen according to their names in the FEN-specification.
The variable active_color just holds which side is to move in this position. The castling permissions variable is a bitboard, representing which side can castle where.
The variable en_passant marks the “active” en_passant square, if any. Imagine there is a black pawn on d4, and white plays c2-c4. The c4-pawn can now be captured by d4xc3 ep, the square c3 becomes the active en-passant square. Note that this is only set directly after the white c2-c4 two-square pawn move, and it will be unset after whatever move black decides to play. This is because en-passant is only a valid move directly after a two-square pawn push.
Sometimes the values for half_move_clock and fullmove_number can be confusing, so we’ll discuss them a bit further to make sure these are understood correctly.
In day-to-day chess talk, chess we tend to call it a “move” when one side moves
a piece. Technically though, this is only a half-move. The full move is only
complete when both white and black have moved a piece. It is often confusing and
misinterpreted when applying the 50 move rule for draws: it refers to 50
consecutive half-moves by each side combined (100 plies total), without a pawn
move or capture.
This is what the half_move_clock means: it is the number of half moves made since the last pawn push or piece capture. As soon as a pawn is moved or a piece is captured, the half_move_clock is reset to 0. Personally, I would have named this half_move_counter, but I have decided to stick with the official FEN-naming, to avoid introducing a new name for an already confusing variable.
Consequently, the fullmove_number is the number of completed turns in the game where both white and black have done their half-move part.
A Zobrist Key is a large 64-bit number, calculated from the position. It is calculated in such a way that the chance of two different positions ending up with the same Zobrist Key is rare. We store this in the game state, because we need it to make some engine functions work properly, such as draw detection. How Zobrist Keys are created is discussed in the previous section. We create the Zobrist Keys when the engine starts up and then update them incrementally when the position changes.
The Phase Value and Eval Base are calculated by the evaluation function to evaluate which side is better in the current position. This is the basis for the evaluation function, which we will take a look at much later; for more information about this, see the chapter about the Evaluation function. Calculating the evaluation basis is a time consuming task. Just as with the Zobrist Keys, we calculate these values when the engine starts up and then update them incrementally each time the position changes.
The variable next_move is not absolutely necessary, but it is very useful when making and unmaking moves during the search and gameplay. It holds the move that was played next while the game was in this state.
It works as follows. Before we execute a move, we make a copy of the current
game state. Then we make the move and add that to the game state copy we just
took. We put the move in the next_move field. We push this game state into the
history table. (This will be discussed in the next section.)
If we need to undo a move, we would need to reverse every change in the position that was caused by this move. This is time consuming. However, because we saved a game state history, we can just restore the last game state we pushed. This will restore the entire board, as if the last move was not made, except for the move itself that was made in that state. This move is still on the board. Because we saved it in the game state as “next_move”, we immediately know which move was played, so we can reverse it.
That is it for the GameState struct. Now we only need a history struct which keeps track of the entire game’s history. Believe it or not, but this is even simpler than the GameState struct. We’ll discuss it in the next chapter.