From 4e856b2eda1f7b69c7a56cc63610e1db263e75f0 Mon Sep 17 00:00:00 2001 From: Paul Tsouchlos Date: Fri, 28 Nov 2025 12:05:16 -0500 Subject: [PATCH 1/8] chore(move-gen): make function public Make `get_attacked_squares` public so we can use it for threat detection. bench: 1176279 --- chess/src/move_generation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chess/src/move_generation.rs b/chess/src/move_generation.rs index ba98cda..5ca1170 100644 --- a/chess/src/move_generation.rs +++ b/chess/src/move_generation.rs @@ -553,7 +553,7 @@ impl MoveGenerator { /// # Returns /// /// A bitboard representing all squares currently being attacked by the given side. - pub(crate) fn get_attacked_squares( + pub fn get_attacked_squares( &self, board: &Board, side: Side, From fd2a2dfda7f82af3673098e2cbcee8973ad855f6 Mon Sep 17 00:00:00 2001 From: Paul Tsouchlos Date: Fri, 28 Nov 2025 12:05:32 -0500 Subject: [PATCH 2/8] feat: add threats to history table bench: 1176279 --- engine/src/history_table.rs | 95 +++++++++++++++++++++++++++++-------- engine/src/move_order.rs | 42 +++++++++++++--- engine/src/search.rs | 60 ++++++++++++++++++++--- 3 files changed, 166 insertions(+), 31 deletions(-) diff --git a/engine/src/history_table.rs b/engine/src/history_table.rs index e71230d..f0a23f2 100644 --- a/engine/src/history_table.rs +++ b/engine/src/history_table.rs @@ -7,7 +7,9 @@ use chess::{ use crate::score::{LargeScoreType, Score}; pub struct HistoryTable { - table: [[[LargeScoreType; NumberOf::SQUARES]; NumberOf::PIECE_TYPES]; NumberOf::SIDES], + /// The history table is a 5D array indexed by [is_from_attacked][is_to_attacked][side][piece_type][square (to)] + table: [[[[[LargeScoreType; NumberOf::SQUARES]; NumberOf::PIECE_TYPES]; NumberOf::SIDES]; + NumberOf::SIDES]; NumberOf::SIDES], } /// Safe calculation of the bonus applied to quiet moves that are inserted into the history table. @@ -28,28 +30,61 @@ pub(crate) fn calculate_bonus_for_depth(depth: i16) -> i16 { impl HistoryTable { pub(crate) fn new() -> Self { - let table = - [[[Default::default(); NumberOf::SQUARES]; NumberOf::PIECE_TYPES]; NumberOf::SIDES]; + let table = [[[[[Default::default(); NumberOf::SQUARES]; NumberOf::PIECE_TYPES]; + NumberOf::SIDES]; NumberOf::SIDES]; NumberOf::SIDES]; Self { table } } - pub(crate) fn get(&self, side: Side, piece: Piece, square: u8) -> LargeScoreType { - self.table[side as usize][piece as usize][square as usize] + pub(crate) fn get( + &self, + side: Side, + piece: Piece, + square: u8, + is_from_attacked: bool, + is_to_attacked: bool, + ) -> LargeScoreType { + self.table[is_from_attacked as usize][is_to_attacked as usize][side as usize] + [piece as usize][square as usize] } - pub(crate) fn update(&mut self, side: Side, piece: Piece, square: u8, bonus: LargeScoreType) { - let current_value = self.table[side as usize][piece as usize][square as usize]; + fn get_mut( + &mut self, + side: Side, + piece: Piece, + square: u8, + is_from_attacked: bool, + is_to_attacked: bool, + ) -> &mut LargeScoreType { + &mut self.table[is_from_attacked as usize][is_to_attacked as usize][side as usize] + [piece as usize][square as usize] + } + + pub(crate) fn update( + &mut self, + side: Side, + piece: Piece, + square: u8, + is_from_attacked: bool, + is_to_attacked: bool, + bonus: LargeScoreType, + ) { + let current_value = self.get(side, piece, square, is_from_attacked, is_to_attacked); let clamped_bonus = bonus.clamp(-Score::MAX_HISTORY, Score::MAX_HISTORY); let new_value = current_value + clamped_bonus - current_value * clamped_bonus.abs() / Score::MAX_HISTORY; - self.table[side as usize][piece as usize][square as usize] = new_value; + *self.get_mut(side, piece, square, is_from_attacked, is_to_attacked) = new_value; } pub(crate) fn clear(&mut self) { for side in 0..NumberOf::SIDES { for piece_type in 0..NumberOf::PIECE_TYPES { for square in 0..NumberOf::SQUARES { - self.table[side][piece_type][square] = Default::default(); + for is_from_attacked in 0..2 { + for is_to_attacked in 0..2 { + self.table[side][piece_type][square][is_from_attacked] + [is_to_attacked] = Default::default(); + } + } } } } @@ -63,7 +98,11 @@ impl HistoryTable { print!("|"); for file in 0..NumberOf::FILES { let square = file + rank * NumberOf::FILES; - print!("{:5} ", self.table[side as usize][piece_type][square]); + let grid = self.table[side as usize][piece_type][square]; + print!( + "{:5} | {:5}\n------\n[{:5} | {:5} ]", + grid[0][0], grid[0][1], grid[1][0], grid[1][1] + ); } println!("|"); } @@ -88,13 +127,23 @@ mod tests { fn initialize_history_table() { let history_table = HistoryTable::new(); // loop through all sides, piece types, and squares - for side in 0..2 { + for side in 0..2u8 { for piece_type in 0..6 { for square in 0..64 { - assert_eq!( - history_table.table[side][piece_type][square], - Default::default() - ); + for is_from_attacked in 0..2 { + for is_to_attacked in 0..2 { + assert_eq!( + history_table.get( + Side::try_from(side).unwrap(), + Piece::try_from(piece_type).unwrap(), + square as u8, + is_from_attacked != 0, + is_to_attacked != 0, + ), + Default::default() + ); + } + } } } } @@ -107,10 +156,18 @@ mod tests { let piece = Piece::Pawn; let square = Squares::A1; let score = 37; - history_table.update(side, piece, square, score); - assert_eq!(history_table.get(side, piece, square), score); - history_table.update(side, piece, square, score); - assert_eq!(history_table.get(side, piece, square), score + score); + let is_from_attacked = true; + let is_to_attacked = false; + history_table.update(side, piece, square, is_from_attacked, is_to_attacked, score); + assert_eq!( + history_table.get(side, piece, square, is_from_attacked, is_to_attacked), + score + ); + history_table.update(side, piece, square, is_from_attacked, is_to_attacked, score); + assert_eq!( + history_table.get(side, piece, square, is_from_attacked, is_to_attacked), + score + score + ); } #[test] diff --git a/engine/src/move_order.rs b/engine/src/move_order.rs index 265df1b..b154a18 100644 --- a/engine/src/move_order.rs +++ b/engine/src/move_order.rs @@ -2,7 +2,9 @@ use std::cmp::Ordering; use anyhow::{Ok, Result}; use arrayvec::ArrayVec; -use chess::{definitions::MAX_MOVE_LIST_SIZE, moves::Move, pieces::Piece, side::Side}; +use chess::{ + bitboard::Bitboard, definitions::MAX_MOVE_LIST_SIZE, moves::Move, pieces::Piece, side::Side, +}; use crate::{ evaluation::Evaluation, hce_values::ByteKnightValues, history_table, score::LargeScoreType, @@ -60,6 +62,7 @@ impl MoveOrder { mv: &Move, tt_move: &Option, history_table: &history_table::HistoryTable, + attacked_by_opponent: &Bitboard, ) -> Self { if tt_move.is_some_and(|tt| *mv == tt) { return Self::TtMove; @@ -71,7 +74,9 @@ impl MoveOrder { return Self::Capture(victim, attacker); } - let score = history_table.get(stm, mv.piece(), mv.to()); + let is_from_attacked = attacked_by_opponent.is_square_occupied(mv.from()); + let is_to_attacked = attacked_by_opponent.is_square_occupied(mv.to()); + let score = history_table.get(stm, mv.piece(), mv.to(), is_from_attacked, is_to_attacked); Self::Quiet(score) } @@ -80,12 +85,19 @@ impl MoveOrder { moves: &[Move], tt_move: &Option, history_table: &history_table::HistoryTable, + attacked_by_opponent: &Bitboard, move_order: &mut ArrayVec, ) -> Result<()> { move_order.clear(); for mv in moves.iter() { - move_order.try_push(Self::classify(stm, mv, tt_move, history_table))?; + move_order.try_push(Self::classify( + stm, + mv, + tt_move, + history_table, + attacked_by_opponent, + ))?; } Ok(()) @@ -94,10 +106,13 @@ impl MoveOrder { #[cfg(test)] mod tests { - use chess::{board::Board, move_generation::MoveGenerator, move_list::MoveList, moves::Move}; + use chess::{ + board::Board, move_generation::MoveGenerator, move_list::MoveList, moves::Move, side::Side, + }; use itertools::Itertools; use crate::{ + history_table::HistoryTable, move_order::MoveOrder, score::Score, ttable::{EntryFlag, TranspositionTable, TranspositionTableEntry}, @@ -106,7 +121,7 @@ mod tests { #[test] fn verify_move_ordering() { let mut tt = TranspositionTable::from_capacity(10); - let mut history_table = crate::history_table::HistoryTable::new(); + let mut history_table = HistoryTable::new(); let move_gen = MoveGenerator::new(); let mut move_list = MoveList::new(); @@ -114,6 +129,12 @@ mod tests { Board::from_fen("rnbqkb1r/pppppppp/8/8/8/8/PPPPPPPP/RNBQKB1R w KQkq - 0 1").unwrap(); move_gen.generate_legal_moves(&board, &mut move_list); + let attacked_by_opponent = move_gen.get_attacked_squares( + &board, + Side::opposite(board.side_to_move()), + &board.all_pieces(), + ); + assert!(move_list.len() >= 6); let depth = 3i32; let first_mv = move_list.at(4).unwrap(); @@ -130,15 +151,24 @@ mod tests { board.side_to_move(), second_mv.piece(), second_mv.to(), + attacked_by_opponent.is_square_occupied(second_mv.from()), + attacked_by_opponent.is_square_occupied(second_mv.to()), 300 * depth - 250, ); let tt_entry = tt.get_entry(board.zobrist_hash()).unwrap(); let tt_move = tt_entry.board_move; + // sort the moves let moves = move_list .iter() .sorted_by_key(|mv| { - MoveOrder::classify(board.side_to_move(), mv, &Some(tt_move), &history_table) + MoveOrder::classify( + board.side_to_move(), + mv, + &Some(tt_move), + &history_table, + &attacked_by_opponent, + ) }) .collect::>(); diff --git a/engine/src/search.rs b/engine/src/search.rs index 56999ec..de745ba 100644 --- a/engine/src/search.rs +++ b/engine/src/search.rs @@ -25,8 +25,14 @@ use std::{ use anyhow::{Result, bail}; use arrayvec::ArrayVec; use chess::{ - board::Board, definitions::MAX_MOVE_LIST_SIZE, move_generation::MoveGenerator, - move_list::MoveList, moves::Move, pieces::Piece, + bitboard::Bitboard, + board::Board, + definitions::MAX_MOVE_LIST_SIZE, + move_generation::MoveGenerator, + move_list::MoveList, + moves::Move, + pieces::Piece, + side::{self, Side}, }; use uci_parser::{UciInfo, UciResponse, UciSearchOptions}; @@ -429,7 +435,11 @@ impl<'a, Log: LogLevel> Search<'a, Log> { let mut move_list = MoveList::new(); let mut order_list = ArrayVec::::new(); self.move_gen.generate_legal_moves(board, &mut move_list); - + let attacked_by_opponent = self.move_gen.get_attacked_squares( + board, + side::Side::opposite(board.side_to_move()), + &board.all_pieces(), + ); // do we have moves? if move_list.is_empty() { return if board.is_in_check(&self.move_gen) { @@ -444,6 +454,7 @@ impl<'a, Log: LogLevel> Search<'a, Log> { move_list.as_slice(), &tt_move, self.history_table, + &attacked_by_opponent, &mut order_list, ); @@ -535,12 +546,24 @@ impl<'a, Log: LogLevel> Search<'a, Log> { if alpha_use >= beta { // update history table for quiets if mv.is_quiet() { + let occupancy = board.all_pieces(); + let us = board.side_to_move(); + let them = Side::opposite(us); + // calculate the threats + let attacked_by_opponent = + self.move_gen.get_attacked_squares(board, them, &occupancy); // calculate history bonus let bonus = history_table::calculate_bonus_for_depth(depth); + let is_from_attacked = + Bitboard::from_square(mv.from()) & attacked_by_opponent != 0; + let is_to_attacked = + Bitboard::from_square(mv.to()) & attacked_by_opponent != 0; self.history_table.update( board.side_to_move(), mv.piece(), mv.to(), + is_from_attacked, + is_to_attacked, bonus as LargeScoreType, ); @@ -550,6 +573,8 @@ impl<'a, Log: LogLevel> Search<'a, Log> { board.side_to_move(), mv.piece(), mv.to(), + is_from_attacked, + is_to_attacked, -bonus as LargeScoreType, ); } @@ -690,6 +715,11 @@ impl<'a, Log: LogLevel> Search<'a, Log> { let mut move_list = MoveList::new(); let mut move_order_list = ArrayVec::::new(); self.move_gen.generate_legal_moves(board, &mut move_list); + let attacked_by_opponent = self.move_gen.get_attacked_squares( + board, + side::Side::opposite(board.side_to_move()), + &board.all_pieces(), + ); let mut local_pv = PrincipleVariation::new(); // clear the current PV because this is a new position @@ -731,6 +761,7 @@ impl<'a, Log: LogLevel> Search<'a, Log> { captures.as_slice(), &tt_move, self.history_table, + &attacked_by_opponent, &mut move_order_list, ); // TODO(PT): Should we log a message to the CLI or a log? @@ -808,7 +839,10 @@ impl<'a, Log: LogLevel> Search<'a, Log> { mod tests { use std::time::Duration; - use chess::{board::Board, pieces::ALL_PIECES}; + use chess::{ + bitboard::Bitboard, board::Board, move_generation::MoveGenerator, pieces::ALL_PIECES, + side::Side, + }; use crate::{ evaluation::ByteKnightEvaluation, @@ -1026,9 +1060,17 @@ mod tests { } } + let move_gen = MoveGenerator::new(); + for fen in TEST_FENS { let mut board = Board::from_fen(fen).unwrap(); - + let attacked_by_opponent = move_gen.get_attacked_squares( + &board, + Side::opposite(board.side_to_move()), + &board.all_pieces(), + ); + let is_from_attacked = |sq: u8| Bitboard::from_square(sq) & attacked_by_opponent != 0; + let is_to_attacked = |sq: u8| Bitboard::from_square(sq) & attacked_by_opponent != 0; let mut ttable = Default::default(); let mut history_table = Default::default(); let mut search = Search::::new(&config, &mut ttable, &mut history_table); @@ -1040,7 +1082,13 @@ mod tests { let mut max_history = LargeScoreType::MIN; for piece in ALL_PIECES { for square in 0..64 { - let score = history_table.get(side, piece, square); + let score = history_table.get( + side, + piece, + square, + is_from_attacked(square), + is_to_attacked(square), + ); if score > max_history { max_history = score; } From b803237f2161b29d80f5ce492356644110d41e73 Mon Sep 17 00:00:00 2001 From: Paul Tsouchlos Date: Tue, 2 Dec 2025 16:42:22 -0500 Subject: [PATCH 3/8] chore: add test for bitboard piece get for side --- chess/src/board.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/chess/src/board.rs b/chess/src/board.rs index aa5d4b4..0dce816 100644 --- a/chess/src/board.rs +++ b/chess/src/board.rs @@ -916,4 +916,20 @@ mod tests { assert_eq!(piece_kind_bb, *black_pawns_bb | *white_pawns_bb); assert_eq!(piece_kind_bb.number_of_occupied_squares(), 16); } + + #[test] + fn piece_bitboards_for_side() { + let board = Board::default_board(); + let white_pieces_bb = board.pieces(Side::White); + let black_pieces_bb = board.pieces(Side::Black); + + let expected_white_bb = 0x000000000000FFFF; + let expected_black_bb = 0xFFFF000000000000; + + assert_eq!(white_pieces_bb, Bitboard::new(expected_white_bb)); + assert_eq!(black_pieces_bb, Bitboard::new(expected_black_bb)); + + let all = board.all_pieces(); + assert_eq!(all, white_pieces_bb | black_pieces_bb); + } } From c5e32f3189d9a4fd3290867b48ca5170434f55c7 Mon Sep 17 00:00:00 2001 From: Paul Tsouchlos Date: Tue, 2 Dec 2025 16:42:44 -0500 Subject: [PATCH 4/8] fix: use ALL_PIECES for iteration in move gen --- chess/src/move_generation.rs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/chess/src/move_generation.rs b/chess/src/move_generation.rs index 5ca1170..0dde1ee 100644 --- a/chess/src/move_generation.rs +++ b/chess/src/move_generation.rs @@ -26,7 +26,7 @@ use crate::{ moves::{Move, MoveDescriptor, MoveType, PromotionDescriptor}, non_slider_piece::NonSliderPiece, piece_category::PieceCategory, - pieces::{Piece, SQUARE_NAME}, + pieces::{ALL_PIECES, Piece, SQUARE_NAME}, rank::Rank, side::Side, sliding_piece_attacks::SlidingPieceAttacks, @@ -561,17 +561,8 @@ impl MoveGenerator { ) -> Bitboard { let mut attacks = Bitboard::default(); - // get the squares attacked by each piece - for piece in [ - Piece::Bishop, - Piece::Rook, - Piece::Queen, - Piece::King, - Piece::Knight, - Piece::Pawn, - ] - .iter() - { + // Get the squares attacked by each piece + for piece in ALL_PIECES.iter() { let mut piece_bb = *board.piece_bitboard(*piece, side); if piece_bb.as_number() == 0 { continue; From 610ebe51576046149c3af33ac1e7b1de328d73a7 Mon Sep 17 00:00:00 2001 From: Paul Tsouchlos Date: Tue, 2 Dec 2025 16:42:51 -0500 Subject: [PATCH 5/8] fix: remove dead (commented) code --- chess/src/move_generation.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/chess/src/move_generation.rs b/chess/src/move_generation.rs index 0dde1ee..05d6e03 100644 --- a/chess/src/move_generation.rs +++ b/chess/src/move_generation.rs @@ -609,14 +609,6 @@ impl MoveGenerator { self.get_non_slider_attacks(Side::opposite(attacking_side), non_slider, square) } } - // TODO(PT): Remove this old code - // if piece.is_slider() { - // self.get_slider_attacks(piece, square, occupancy) - // } else if piece == Piece::Pawn { - // self.pawn_attacks[Side::opposite(attacking_side) as usize][square as usize] - // } else { - // self.get_non_slider_attacks(piece, square) - // } } /// Generates pseudo-legal moves for the current board state. From 0a13307ec934a4e3e9e0255dcd0c3191e4342004 Mon Sep 17 00:00:00 2001 From: Paul Tsouchlos Date: Tue, 2 Dec 2025 16:42:58 -0500 Subject: [PATCH 6/8] fix: commenting style --- chess/src/move_generation.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/chess/src/move_generation.rs b/chess/src/move_generation.rs index 05d6e03..cd2611e 100644 --- a/chess/src/move_generation.rs +++ b/chess/src/move_generation.rs @@ -877,9 +877,8 @@ impl MoveGenerator { // en passant let bb_en_passant = match board.en_passant_square() { Some(en_passant_square) => { - // we only want to add the en passant square if it is within range of the pawn - // this means that the en passant square is within 1 rank of the pawn and the en passant square - // is in the pawn's attack table + // We only want to add the en passant square if it is within range of the pawn. + // This means that the en passant square is within 1 rank of the pawn and the en passant square is in the pawn's attack table let en_passant_bb = Bitboard::from_square(en_passant_square); let result = en_passant_bb & !(attack_bb); let is_in_range = result == 0; From 07f06c11e39cd7c12c52267f5dcea4cda90eec11 Mon Sep 17 00:00:00 2001 From: Paul Tsouchlos Date: Tue, 2 Dec 2025 16:43:06 -0500 Subject: [PATCH 7/8] chore: simplify clear in hist table --- engine/src/history_table.rs | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/engine/src/history_table.rs b/engine/src/history_table.rs index f0a23f2..d684ecc 100644 --- a/engine/src/history_table.rs +++ b/engine/src/history_table.rs @@ -76,18 +76,7 @@ impl HistoryTable { } pub(crate) fn clear(&mut self) { - for side in 0..NumberOf::SIDES { - for piece_type in 0..NumberOf::PIECE_TYPES { - for square in 0..NumberOf::SQUARES { - for is_from_attacked in 0..2 { - for is_to_attacked in 0..2 { - self.table[side][piece_type][square][is_from_attacked] - [is_to_attacked] = Default::default(); - } - } - } - } - } + *self = Self::new(); } pub(crate) fn print_for_side(&self, side: Side) { @@ -178,4 +167,24 @@ mod tests { assert!(bonus as i32 <= i16::MAX.into()); } } + + #[test] + fn update_and_then_clear() { + let mut history_table = HistoryTable::new(); + let side = Side::White; + let piece = Piece::Knight; + let square = Squares::E4; + let is_from_attacked = false; + let is_to_attacked = true; + let bonus = 50; + + history_table.update(side, piece, square, is_from_attacked, is_to_attacked, bonus); + let stored_value = history_table.get(side, piece, square, is_from_attacked, is_to_attacked); + assert_eq!(stored_value, bonus); + + history_table.clear(); + let cleared_value = + history_table.get(side, piece, square, is_from_attacked, is_to_attacked); + assert_eq!(cleared_value, 0); + } } From 745c1847ba19d64c56bc43be621af3a9eabe1bf9 Mon Sep 17 00:00:00 2001 From: Paul Tsouchlos Date: Tue, 2 Dec 2025 16:43:33 -0500 Subject: [PATCH 8/8] chore: simplify attacked square calc in search Use move gens `is_square_attacked` function instead. bench: 1176279 --- engine/src/search.rs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/engine/src/search.rs b/engine/src/search.rs index de745ba..20813f7 100644 --- a/engine/src/search.rs +++ b/engine/src/search.rs @@ -25,7 +25,6 @@ use std::{ use anyhow::{Result, bail}; use arrayvec::ArrayVec; use chess::{ - bitboard::Bitboard, board::Board, definitions::MAX_MOVE_LIST_SIZE, move_generation::MoveGenerator, @@ -33,6 +32,7 @@ use chess::{ moves::Move, pieces::Piece, side::{self, Side}, + square::Square, }; use uci_parser::{UciInfo, UciResponse, UciSearchOptions}; @@ -546,18 +546,21 @@ impl<'a, Log: LogLevel> Search<'a, Log> { if alpha_use >= beta { // update history table for quiets if mv.is_quiet() { - let occupancy = board.all_pieces(); let us = board.side_to_move(); let them = Side::opposite(us); - // calculate the threats - let attacked_by_opponent = - self.move_gen.get_attacked_squares(board, them, &occupancy); + // calculate history bonus let bonus = history_table::calculate_bonus_for_depth(depth); - let is_from_attacked = - Bitboard::from_square(mv.from()) & attacked_by_opponent != 0; - let is_to_attacked = - Bitboard::from_square(mv.to()) & attacked_by_opponent != 0; + let is_from_attacked = self.move_gen.is_square_attacked( + board, + &Square::from_square_index(mv.from()), + them, + ); + let is_to_attacked = self.move_gen.is_square_attacked( + board, + &Square::from_square_index(mv.to()), + them, + ); self.history_table.update( board.side_to_move(), mv.piece(),