Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Piece Movement and Incremental Updates

One of the most frequently performed operations in a chess engine is updating the board after a move. During a search, millions or even billions of positions may be reached, and each of these positions requires pieces to be moved, captured, promoted, or otherwise manipulated. As a result, the efficiency of board updates has a direct impact on the overall performance of the engine.

A common mistake made by beginning engine programmers is to recompute all derived information after every move. While this approach is simple, it quickly becomes too expensive once the search reaches any significant depth. Modern chess engines therefore rely heavily on incremental updates. Instead of recalculating information from scratch, only the data affected by a move is adjusted.

The functions in this section form the foundation of the engine’s position-update mechanism. Besides modifying the piece bitboards and piece list, they also maintain the Zobrist hash key, evaluation information, game phase value, castling rights, en passant status, and side-to-move information. All of these updates are performed incrementally, ensuring that board modifications remain extremely fast.

remove_piece()

Removes a piece from a given square and updates all associated board structures. Besides modifying the piece and side bitboards, the function updates the piece list, Zobrist hash key, evaluation data, and game phase information incrementally.

#![allow(unused)]
fn main() {
pub fn remove_piece(&mut self, side: Side, piece: Piece, square: Square) {
    self.bb_pieces[side][piece] ^= BB_SQUARES[square.idx()];
    self.bb_side[side] ^= BB_SQUARES[square.idx()];
    self.piece_list[square] = Piece::None;
    self.game_state.zobrist_key ^= self.zobrist_randoms.piece(side, piece, square);

    let values = Evaluation::eval_base_update(side, piece, square, Evaluation::parameters());
    self.game_state.phase_value -= Evaluation::phase_value(piece, Evaluation::parameters());
    self.game_state.eval_base[side].subtract(values);
}
}

put_piece()

Places a piece onto a square and updates all dependent data structures. Like remove_piece(), every affected value is updated incrementally rather than being recomputed from scratch.

#![allow(unused)]
fn main() {
pub fn put_piece(&mut self, side: Side, piece: Piece, square: Square) {
    self.bb_pieces[side][piece] |= BB_SQUARES[square.idx()];
    self.bb_side[side] |= BB_SQUARES[square.idx()];
    self.piece_list[square] = piece;
    self.game_state.zobrist_key ^= self.zobrist_randoms.piece(side, piece, square);

    let values = Evaluation::eval_base_update(side, piece, square, Evaluation::parameters());
    self.game_state.phase_value += Evaluation::phase_value(piece, Evaluation::parameters());
    self.game_state.eval_base[side].add(values);
}
}

move_piece()

Moves a piece from one square to another by combining the functionality of remove_piece() and put_piece(). This ensures that all board data remains synchronized and that all incremental updates are applied automatically.

#![allow(unused)]
fn main() {
pub fn move_piece(&mut self, side: Side, piece: Piece, from: Square, to: Square) {
    self.remove_piece(side, piece, from);
    self.put_piece(side, piece, to);
}
}

set_ep_square()

Sets the current en passant square while keeping the Zobrist hash key synchronized with the new position state.

#![allow(unused)]
fn main() {
pub fn set_ep_square(&mut self, square: Square) {
    self.game_state.zobrist_key ^= self.zobrist_randoms.en_passant(self.game_state.en_passant);
    self.game_state.en_passant = Some(square);
    self.game_state.zobrist_key ^= self.zobrist_randoms.en_passant(self.game_state.en_passant);
}
}

clear_ep_square()

Removes the current en passant square and updates the Zobrist key accordingly.

#![allow(unused)]
fn main() {
pub fn clear_ep_square(&mut self) {
    self.game_state.zobrist_key ^= self.zobrist_randoms.en_passant(self.game_state.en_passant);
    self.game_state.en_passant = None;
    self.game_state.zobrist_key ^= self.zobrist_randoms.en_passant(self.game_state.en_passant);
}
}

swap_side()

Changes the side to move from White to Black or vice versa. The Zobrist key is updated before and after the change to preserve hash consistency.

#![allow(unused)]
fn main() {
pub fn swap_side(&mut self) {
    self.game_state.zobrist_key ^= self.zobrist_randoms.side(self.game_state.active_color);
    self.game_state.active_color.flip_mut();
    self.game_state.zobrist_key ^= self.zobrist_randoms.side(self.game_state.active_color);
}
}

update_castling_permissions()

Updates the current castling rights and adjusts the Zobrist hash key to reflect the new permission set.

#![allow(unused)]
fn main() {
pub fn update_castling_permissions(&mut self, new_permissions: Bitboard) {
    self.game_state.zobrist_key ^= self.zobrist_randoms.castling(self.game_state.castling);
    self.game_state.castling = new_permissions;
    self.game_state.zobrist_key ^= self.zobrist_randoms.castling(self.game_state.castling);
}
}

These functions form the low-level building blocks for all position updates performed by the engine. Every move, capture, promotion, castle, and en passant operation ultimately relies on them to keep the board state consistent.

Perhaps the most important aspect of this implementation is that all dependent information is maintained incrementally. Piece locations, occupancy information, Zobrist keys, evaluation terms, game phase values, castling rights, en passant status, and side-to-move information are updated at the moment a change occurs. This eliminates the need for expensive recomputation and allows the engine to update positions millions of times per second during search.

With these movement primitives in place, we can now move on to higher-level move execution, where complete chess moves are applied to the board using the functions introduced in this section: let’s move on to make() and unmake().