From 6a5a508b2ac8c8b21df64595aaf4e5f3e9d660bc Mon Sep 17 00:00:00 2001 From: Paul Tsouchlos Date: Wed, 18 Mar 2026 21:59:57 -0400 Subject: [PATCH 01/10] chore: add promo filt to enumerate helper Add a promotion filter for what promotions to consider when enumerating moves. bench: 729284 --- chess/src/move_generation/enumerate.rs | 81 +++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 7 deletions(-) diff --git a/chess/src/move_generation/enumerate.rs b/chess/src/move_generation/enumerate.rs index e47d228..521d165 100644 --- a/chess/src/move_generation/enumerate.rs +++ b/chess/src/move_generation/enumerate.rs @@ -15,6 +15,17 @@ use crate::{ square::{self, Square}, }; +/// Controls which promotion types are generated during move enumeration. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum PromotionFilter { + /// Generate all 4 promotion types (Queen, Rook, Bishop, Knight). + All, + /// Generate only queen promotions. Used for tactical move generation. + QueenOnly, + /// Generate only underpromotions (Rook, Bishop, Knight). Used for quiet move generation. + UnderOnly, +} + /// Enumerate all moves in a given bitboard and add them to the given [`MoveList`] /// /// # Arguments @@ -23,6 +34,7 @@ use crate::{ /// - `piece`: The `piece` that is moving. /// - `board`: The current [`Board`]. /// - `move_list`: The [`MoveList`] to push enumerated moves into. +/// - `promotion_filter`: Controls which promotion types are generated. #[allow(clippy::panic)] pub(crate) fn enumerate_moves( bitboard: &Bitboard, @@ -30,6 +42,7 @@ pub(crate) fn enumerate_moves( piece: Piece, board: &Board, move_list: &mut MoveList, + promotion_filter: PromotionFilter, ) { // Stop if the bitboard is empty. if bitboard.as_number() == 0 { @@ -82,13 +95,21 @@ pub(crate) fn enumerate_moves( let to_square = square::to_square_object(file, rank); if is_promotion { - // we have to add 4 moves for each promotion type - for promotion_type in [ - PromotionDescriptor::Queen, - PromotionDescriptor::Rook, - PromotionDescriptor::Bishop, - PromotionDescriptor::Knight, - ] { + let promotion_types: &[PromotionDescriptor] = match promotion_filter { + PromotionFilter::All => &[ + PromotionDescriptor::Queen, + PromotionDescriptor::Rook, + PromotionDescriptor::Bishop, + PromotionDescriptor::Knight, + ], + PromotionFilter::QueenOnly => &[PromotionDescriptor::Queen], + PromotionFilter::UnderOnly => &[ + PromotionDescriptor::Rook, + PromotionDescriptor::Bishop, + PromotionDescriptor::Knight, + ], + }; + for promotion_type in promotion_types { let mv = Move::new( from, &to_square, @@ -108,3 +129,49 @@ pub(crate) fn enumerate_moves( } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{board::Board, move_list::MoveList, pieces::Piece}; + + #[test] + fn enumerate_queen_promotion_only() { + // White pawn on e7, no captures — push to e8 + let board = Board::from_fen("4k3/4P3/8/8/8/8/8/4K3 w - - 0 1").unwrap(); + let pawn_sq = Square::from_file_rank('e', 6).unwrap(); // e7 + let bb = Bitboard::from_square(60); // e8 + + let mut all = MoveList::new(); + enumerate_moves(&bb, &pawn_sq, Piece::Pawn, &board, &mut all, PromotionFilter::All); + assert_eq!(all.len(), 4); // Q, R, B, N + + let mut queen_only = MoveList::new(); + enumerate_moves( + &bb, + &pawn_sq, + Piece::Pawn, + &board, + &mut queen_only, + PromotionFilter::QueenOnly, + ); + assert_eq!(queen_only.len(), 1); + assert!(queen_only + .iter() + .all(|mv| mv.promotion_piece() == Some(Piece::Queen))); + + let mut under_only = MoveList::new(); + enumerate_moves( + &bb, + &pawn_sq, + Piece::Pawn, + &board, + &mut under_only, + PromotionFilter::UnderOnly, + ); + assert_eq!(under_only.len(), 3); + assert!(under_only + .iter() + .all(|mv| mv.promotion_piece() != Some(Piece::Queen))); + } +} From fb2816df81e3edf71d14214deeb16971d3d466d6 Mon Sep 17 00:00:00 2001 From: Paul Tsouchlos Date: Wed, 18 Mar 2026 22:00:25 -0400 Subject: [PATCH 02/10] chore: add new metadata submodule This will replace the function that returns a massive tuple of data for legal move gen. bench: 729284 --- chess/src/move_generation/metadata.rs | 162 ++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 chess/src/move_generation/metadata.rs diff --git a/chess/src/move_generation/metadata.rs b/chess/src/move_generation/metadata.rs new file mode 100644 index 0000000..f68ba97 --- /dev/null +++ b/chess/src/move_generation/metadata.rs @@ -0,0 +1,162 @@ +// Part of the byte-knight project. +// Author: Paul Tsouchlos (ptsouchlos) (developer.paul.123@gmail.com) +// GNU General Public License v3.0 or later +// https://www.gnu.org/licenses/gpl-3.0-standalone.html + +use crate::{ + attacks, + bitboard::Bitboard, + bitboard_helpers, + board::Board, + move_generation::{NORTH, SOUTH}, + pieces::Piece, + rays, + side::Side, + square::{self}, +}; + +/// Precomputed check and pin metadata for the current position. +/// +/// This is computed once per position and shared across staged move generation +/// (tacticals and quiets). All fields are needed by the legal move generators +/// to enforce pin, check evasion, and capture/push mask constraints. +#[derive(Clone, Debug)] +pub struct CheckPinMetadata { + /// Bitboard of enemy pieces currently giving check to the king. + pub checkers: Bitboard, + /// Bitboard of squares that can be captured (filtered when in check). + pub capture_mask: Bitboard, + /// Bitboard of squares that can be pushed to (ray between checker and king for slider checks). + pub push_mask: Bitboard, + /// Bitboard of our pieces that are pinned to the king. + pub pinned: Bitboard, + /// Bitboard of orthogonal pin rays (rook/queen pins along ranks/files). + pub orthogonal_pin_rays: Bitboard, + /// Bitboard of diagonal pin rays (bishop/queen pins along diagonals). + pub diagonal_pin_rays: Bitboard, +} + +impl CheckPinMetadata { + /// Returns true if the side to move is in check. + pub fn in_check(&self) -> bool { + !self.checkers.is_empty() + } + + /// Returns the number of pieces giving check. + pub fn num_checkers(&self) -> u32 { + self.checkers.number_of_occupied_squares() + } +} + +/// Compute check and pin metadata for the current position. +/// +/// Uses the "super-piece" method: projects attacks from the king square +/// to find checkers and pinners in a single pass over enemy sliding pieces. +pub fn compute(board: &Board) -> CheckPinMetadata { + let us = board.side_to_move(); + let them = us.opposite(); + let occupancy = board.all_pieces(); + let empty = !occupancy; + let their_pieces = board.pieces(them); + let our_pieces = board.pieces(us); + let enemy_or_empty = their_pieces | empty; + let king_sq = board.king_square(us); + + let mut pinned = Bitboard::default(); + let mut capture_mask = enemy_or_empty & !(*board.piece_bitboard(Piece::King, them)); + let mut orthogonal_pin_rays = Bitboard::default(); + let mut diagonal_pin_rays = Bitboard::default(); + + // Super-piece method: project attacks from king square with opposite side semantics + let mut checkers = *board.piece_bitboard(Piece::Knight, them) & attacks::knight(king_sq) + | *board.piece_bitboard(Piece::Pawn, them) & attacks::pawn(king_sq, us); + + let enemy_sliding_attacks = attacks::rook(king_sq, Bitboard::default()) + & (*board.piece_bitboard(Piece::Rook, them) | *board.piece_bitboard(Piece::Queen, them)) + | attacks::bishop(king_sq, Bitboard::default()) + & (*board.piece_bitboard(Piece::Bishop, them) + | *board.piece_bitboard(Piece::Queen, them)); + + for next_attacker_sq in enemy_sliding_attacks.iter() { + let attacker_bb = Bitboard::from_square(next_attacker_sq); + + let ray = rays::between(king_sq, next_attacker_sq); + + let (king_file, king_rank) = square::from_square(king_sq); + let (attacker_file, attacker_rank) = square::from_square(next_attacker_sq); + let is_orthogonal = king_file == attacker_file || king_rank == attacker_rank; + let is_diagonal = (king_sq as i16 - next_attacker_sq as i16).abs() % 9 == 0 + || (king_sq as i16 - next_attacker_sq as i16).abs() % 7 == 0; + + match (ray & occupancy).number_of_occupied_squares() { + 0 => { + checkers |= Bitboard::from_square(next_attacker_sq); + } + 1 => { + let overlap = ray & our_pieces; + if overlap.number_of_occupied_squares() == 1 { + pinned |= ray & our_pieces; + if is_orthogonal { + orthogonal_pin_rays |= ray | attacker_bb; + } else if is_diagonal { + diagonal_pin_rays |= ray | attacker_bb; + } + } + } + _ => {} + } + } + + let mut push_mask = Bitboard::FULL; + + if checkers.number_of_occupied_squares() >= 1 { + let is_single_check = checkers.number_of_occupied_squares() == 1; + + capture_mask = checkers & !(*board.piece_bitboard(Piece::King, them)); + + if is_single_check { + let mut checkers_clone = checkers; + let checker = bitboard_helpers::next_bit(&mut checkers_clone) as u8; + + let ray = rays::between(king_sq, checker as u8); + + if let Some((piece, side)) = board.piece_on_square(checker as u8) { + debug_assert!(side == them); + let is_slider = piece.is_slider(); + if is_slider { + push_mask = ray; + } else { + push_mask = Bitboard::default(); + } + } + } + } + + let en_passant_bb = board + .en_passant_square() + .map(Bitboard::from) + .unwrap_or_default(); + match board.side_to_move() { + Side::White => { + let left = en_passant_bb >> SOUTH; + if left & checkers != 0 { + capture_mask |= en_passant_bb; + } + } + Side::Black => { + let right = en_passant_bb << NORTH; + if right & checkers != 0 { + capture_mask |= en_passant_bb; + } + } + } + + CheckPinMetadata { + checkers, + capture_mask, + push_mask, + pinned, + orthogonal_pin_rays, + diagonal_pin_rays, + } +} From 7bd11f9fe113075c4d341723d9983726299b48d9 Mon Sep 17 00:00:00 2001 From: Paul Tsouchlos Date: Wed, 18 Mar 2026 22:04:16 -0400 Subject: [PATCH 03/10] chore: refactor movegen and legal movegen Integrate submodule and updated enumerate moves function into main movegen and legal movegen. bench: 729284 --- chess/src/legal_move_generation.rs | 343 +++++++++++++++++++++++++++-- chess/src/move_generation.rs | 212 ++++-------------- 2 files changed, 362 insertions(+), 193 deletions(-) diff --git a/chess/src/legal_move_generation.rs b/chess/src/legal_move_generation.rs index b094387..176695c 100644 --- a/chess/src/legal_move_generation.rs +++ b/chess/src/legal_move_generation.rs @@ -82,7 +82,7 @@ fn calculate_en_passant_bitboard( /// /// These moves need to be enumerated to get the actual moves. See [`move_generation::enumerate_moves`] #[allow(clippy::too_many_arguments)] -fn generate_legal_pawn_mobility( +pub(crate) fn generate_legal_pawn_mobility( board: &Board, square: Square, pinned_pieces: Bitboard, @@ -197,7 +197,7 @@ fn generate_legal_pawn_mobility( /// /// These moves need to be enumerated to get the actual moves. See [`move_generation::enumerate_moves`] #[allow(clippy::too_many_arguments)] -fn generate_normal_piece_legal_mobility( +pub(crate) fn generate_normal_piece_legal_mobility( piece: Piece, square: Square, board: &Board, @@ -257,7 +257,7 @@ fn generate_normal_piece_legal_mobility( /// # Returns /// /// A [`Bitboard`] of legal moves for the king -fn generate_king_legal_mobility( +pub(crate) fn generate_king_legal_mobility( square: Square, board: &Board, capture_mask: Bitboard, @@ -317,7 +317,7 @@ fn generate_king_legal_mobility( /// /// A [`Bitboard`] of legal moves for the piece that can them be enumerated. #[allow(clippy::too_many_arguments)] -fn generate_legal_mobility( +pub(crate) fn generate_legal_mobility( piece: Piece, square: Square, board: &Board, @@ -372,56 +372,234 @@ fn generate_legal_mobility( /// move_generation::generate_legal_moves(&board, &mut move_list); /// assert_eq!(20, move_list.len()) /// ``` -pub fn generate_legal_moves(board: &Board, move_list: &mut MoveList) { +/// Generate legal tactical moves: captures, en passant, and queen promotions. +/// +/// Capture-promotions generate all 4 promotion types (they are captures first). +/// Non-capture promotions generate only the queen promotion variant. +/// +/// Must be called with metadata from [`move_generation::metadata::compute`]. +pub fn generate_legal_tacticals( + board: &Board, + meta: &move_generation::metadata::CheckPinMetadata, + move_list: &mut MoveList, +) { let us = board.side_to_move(); + let their_pieces = board.pieces(us.opposite()); + let king_bb = board.piece_bitboard(Piece::King, us); + let king_square = board.king_square(us); + let occupancy = board.all_pieces(); + let en_passant_bb = board + .en_passant_square() + .map(Bitboard::from) + .unwrap_or_default(); + let promotion_rank_bb = Rank::promotion_rank(us).to_bitboard(); + + // King captures (castling excluded naturally since castling squares are empty) + let king_sq = Square::from_square_index(king_square); + let king_mobility = + generate_king_legal_mobility(king_sq, board, meta.capture_mask, meta.checkers); + let king_captures = king_mobility & their_pieces; + move_generation::enumerate::enumerate_moves( + &king_captures, + &king_sq, + Piece::King, + board, + move_list, + move_generation::enumerate::PromotionFilter::All, + ); + + // Double check: only king moves are legal + if meta.num_checkers() > 1 { + return; + } + let our_pieces = board.pieces(us); + let moveable_pieces = our_pieces & !(*king_bb); + + for from_sq in moveable_pieces.iter() { + let (piece, _) = match board.piece_on_square(from_sq) { + Some(p) => p, + None => continue, + }; + let from_square = Square::from_square_index(from_sq); + let mobility = generate_legal_mobility( + piece, + from_square, + board, + meta.pinned, + meta.capture_mask, + meta.push_mask, + meta.orthogonal_pin_rays, + meta.diagonal_pin_rays, + meta.checkers, + ); + + if piece == Piece::Pawn { + // Pawn captures (including en passant and capture-promotions with all 4 types) + let captures = mobility & (their_pieces | en_passant_bb); + move_generation::enumerate::enumerate_moves( + &captures, + &from_square, + piece, + board, + move_list, + move_generation::enumerate::PromotionFilter::All, + ); + + // Non-capture queen promotions only (pushes to promotion rank) + let queen_promo_pushes = mobility & !occupancy & promotion_rank_bb; + move_generation::enumerate::enumerate_moves( + &queen_promo_pushes, + &from_square, + piece, + board, + move_list, + move_generation::enumerate::PromotionFilter::QueenOnly, + ); + } else { + // Non-pawn piece captures + let captures = mobility & their_pieces; + move_generation::enumerate::enumerate_moves( + &captures, + &from_square, + piece, + board, + move_list, + move_generation::enumerate::PromotionFilter::All, + ); + } + } +} + +/// Generate legal quiet moves: non-captures, castling, and underpromotions. +/// +/// Non-capture promotion pushes generate only underpromotion types (Rook, Bishop, Knight). +/// Queen promotions are generated by [`generate_legal_tacticals`] instead. +/// +/// Must be called with metadata from [`move_generation::metadata::compute`]. +pub fn generate_legal_quiets( + board: &Board, + meta: &move_generation::metadata::CheckPinMetadata, + move_list: &mut MoveList, +) { + let us = board.side_to_move(); + let their_pieces = board.pieces(us.opposite()); let king_bb = board.piece_bitboard(Piece::King, us); let king_square = board.king_square(us); + let occupancy = board.all_pieces(); + let promotion_rank_bb = Rank::promotion_rank(us).to_bitboard(); - let (checkers, capture_mask, push_mask, pinned, orthogonal_pin_rays, diagonal_pin_rays) = - move_generation::calculate_check_and_pin_metadata(board); - + // King quiet moves (non-captures + castling) let king_sq = Square::from_square_index(king_square); - let king_moves = generate_king_legal_mobility(king_sq, board, capture_mask, checkers); - + let king_mobility = + generate_king_legal_mobility(king_sq, board, meta.capture_mask, meta.checkers); + let king_quiets = king_mobility & !their_pieces; move_generation::enumerate::enumerate_moves( - &king_moves, + &king_quiets, &king_sq, Piece::King, board, move_list, + move_generation::enumerate::PromotionFilter::All, ); - let num_checkers = checkers.as_number().count_ones(); - if num_checkers > 1 { + // Double check: only king moves are legal + if meta.num_checkers() > 1 { return; } + let our_pieces = board.pieces(us); let moveable_pieces = our_pieces & !(*king_bb); + for from_sq in moveable_pieces.iter() { - let piece = match board.piece_on_square(from_sq) { - Some((piece, _)) => piece, + let (piece, _) = match board.piece_on_square(from_sq) { + Some(p) => p, None => continue, }; let from_square = Square::from_square_index(from_sq); - let moves = generate_legal_mobility( + let mobility = generate_legal_mobility( piece, from_square, board, - pinned, - capture_mask, - push_mask, - orthogonal_pin_rays, - diagonal_pin_rays, - checkers, + meta.pinned, + meta.capture_mask, + meta.push_mask, + meta.orthogonal_pin_rays, + meta.diagonal_pin_rays, + meta.checkers, ); - move_generation::enumerate::enumerate_moves(&moves, &from_square, piece, board, move_list); + if piece == Piece::Pawn { + // Non-capture, non-promotion pawn pushes + // Exclude en passant square — it's empty but captured as a tactical + let en_passant_bb = board + .en_passant_square() + .map(Bitboard::from) + .unwrap_or_default(); + let quiet_pushes = mobility & !occupancy & !promotion_rank_bb & !en_passant_bb; + move_generation::enumerate::enumerate_moves( + &quiet_pushes, + &from_square, + piece, + board, + move_list, + move_generation::enumerate::PromotionFilter::All, + ); + + // Non-capture underpromotions (pushes to promotion rank, R/B/N only) + let underpromo_pushes = mobility & !occupancy & promotion_rank_bb; + move_generation::enumerate::enumerate_moves( + &underpromo_pushes, + &from_square, + piece, + board, + move_list, + move_generation::enumerate::PromotionFilter::UnderOnly, + ); + } else { + // Non-pawn quiet moves (mobility excluding enemy pieces) + let quiets = mobility & !their_pieces; + move_generation::enumerate::enumerate_moves( + &quiets, + &from_square, + piece, + board, + move_list, + move_generation::enumerate::PromotionFilter::All, + ); + } } } +/// Generate all legal moves for the current [`Board`] state. +/// +/// This is a convenience wrapper that generates tacticals followed by quiets. +/// +/// # Arguments +/// +/// - `board` - The current board state +/// - `move_list` - The list of moves to append to +/// +/// # Examples +/// +/// ``` +/// use chess::board::Board; +/// use chess::move_list::MoveList; +/// use chess::move_generation; +/// +/// let board = Board::default_board(); +/// let mut move_list = MoveList::new(); +/// move_generation::generate_legal_moves(&board, &mut move_list); +/// assert_eq!(20, move_list.len()) +/// ``` +pub fn generate_legal_moves(board: &Board, move_list: &mut MoveList) { + let meta = move_generation::metadata::compute(board); + generate_legal_tacticals(board, &meta, move_list); + generate_legal_quiets(board, &meta, move_list); +} + #[cfg(test)] mod tests { use crate::definitions::Squares; @@ -498,4 +676,123 @@ mod tests { let ray = rays::between(Squares::A1, Squares::C2); assert!(ray == Bitboard::default()); } + + #[test] + fn staged_generation_equals_full_generation() { + let positions = [ + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", + "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1", + "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1", + "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1", + "rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8", + "r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10", + "r6r/1b2k1bq/8/8/7B/8/8/R3K2R b KQ - 3 2", + "8/8/8/2k5/2pP4/8/B7/4K3 b - d3 0 3", + "4k3/4P3/8/8/8/8/8/4K3 w - - 0 1", + "r3k2r/p1pp1pb1/bn2Qnp1/2qPN3/1p2P3/2N5/PPPBBPPP/R3K2R b KQkq - 3 2", + ]; + + for fen in &positions { + let board = Board::from_fen(fen).unwrap(); + let meta = move_generation::metadata::compute(&board); + + let mut all_moves = MoveList::new(); + generate_legal_moves(&board, &mut all_moves); + + let mut staged_moves = MoveList::new(); + generate_legal_tacticals(&board, &meta, &mut staged_moves); + generate_legal_quiets(&board, &meta, &mut staged_moves); + + assert_eq!( + all_moves.len(), + staged_moves.len(), + "Move count mismatch for position: {fen}\nAll: {}\nStaged: {}", + all_moves.len(), + staged_moves.len() + ); + + for mv in all_moves.iter() { + assert!( + staged_moves.iter().any(|sm| *sm == *mv), + "Move {} from full gen not found in staged gen for position: {fen}", + mv.to_long_algebraic() + ); + } + } + } + + #[test] + fn tacticals_are_captures_and_queen_promotions() { + let board = + Board::from_fen("rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8") + .unwrap(); + let meta = move_generation::metadata::compute(&board); + + let mut tacticals = MoveList::new(); + generate_legal_tacticals(&board, &meta, &mut tacticals); + + for mv in tacticals.iter() { + let is_capture = mv.is_capture(); + let is_queen_promo = mv.promotion_piece() == Some(Piece::Queen); + assert!( + is_capture || is_queen_promo, + "Non-tactical move {} found in tacticals", + mv.to_long_algebraic() + ); + } + } + + #[test] + fn quiets_are_non_captures_and_underpromotions() { + let board = + Board::from_fen("rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8") + .unwrap(); + let meta = move_generation::metadata::compute(&board); + + let mut quiets = MoveList::new(); + generate_legal_quiets(&board, &meta, &mut quiets); + + for mv in quiets.iter() { + let is_capture = mv.is_capture(); + let is_queen_promo = mv.promotion_piece() == Some(Piece::Queen); + assert!( + !is_capture && !is_queen_promo, + "Tactical move {} found in quiets (capture={}, queen_promo={})", + mv.to_long_algebraic(), + is_capture, + is_queen_promo, + ); + } + } + + #[test] + fn en_passant_is_tactical() { + let board = Board::from_fen("8/8/8/2k5/3Pp3/8/8/4K3 b - d3 0 1").unwrap(); + let meta = move_generation::metadata::compute(&board); + + let mut tacticals = MoveList::new(); + generate_legal_tacticals(&board, &meta, &mut tacticals); + + let has_ep = tacticals.iter().any(|mv| mv.is_en_passant_capture()); + assert!(has_ep, "En passant capture should be in tacticals"); + } + + #[test] + fn castling_is_quiet() { + let board = + Board::from_fen("r3k2r/pppppppp/8/8/8/8/PPPPPPPP/R3K2R w KQkq - 0 1").unwrap(); + let meta = move_generation::metadata::compute(&board); + + let mut quiets = MoveList::new(); + generate_legal_quiets(&board, &meta, &mut quiets); + + let has_castle = quiets.iter().any(|mv| mv.is_castle()); + assert!(has_castle, "Castling should be in quiets"); + + let mut tacticals = MoveList::new(); + generate_legal_tacticals(&board, &meta, &mut tacticals); + + let castle_in_tacticals = tacticals.iter().any(|mv| mv.is_castle()); + assert!(!castle_in_tacticals, "Castling should NOT be in tacticals"); + } } diff --git a/chess/src/move_generation.rs b/chess/src/move_generation.rs index 110f253..39759b6 100644 --- a/chess/src/move_generation.rs +++ b/chess/src/move_generation.rs @@ -6,7 +6,6 @@ use crate::{ attacks, bitboard::Bitboard, - bitboard_helpers, board::Board, move_generation::{self, enumerate::enumerate_moves}, move_list::MoveList, @@ -20,6 +19,7 @@ use crate::{ pub mod castling; pub mod enumerate; +pub mod metadata; pub mod square_state; pub(crate) const NORTH: u64 = 8; @@ -138,133 +138,6 @@ pub(crate) fn calculate_checkers(board: &Board, occupancy: Bitboard) -> Bitboard attacks::all_attackers_of(king_square, board, us.opposite(), kingless_occupancy) } -/// Calculates checkers, pinned pieces, capture mask, push mask and pin rays for the current position. -/// -/// # Arguments -/// -/// - board - The current board state -/// -/// # Returns -/// -/// A tuple containing: -/// - A [`Bitboard`] representing the squares that are checking the king -/// - A [`Bitboard`] representing the squares can be attacked -/// - A [`Bitboard`] representing the squares that can be pushed to -/// - A [`Bitboard`] representing the squares that are pinned -/// - A [`Bitboard`] representing the orthogonal pin rays -/// - A [`Bitboard`] representing the diagonal pin rays -/// -pub(crate) fn calculate_check_and_pin_metadata( - board: &Board, -) -> (Bitboard, Bitboard, Bitboard, Bitboard, Bitboard, Bitboard) { - let us = board.side_to_move(); - let them = us.opposite(); - let occupancy = board.all_pieces(); - let empty = !occupancy; - let their_pieces = board.pieces(them); - let our_pieces = board.pieces(us); - let enemy_or_empty = their_pieces | empty; - let king_sq = board.king_square(us); - - let mut pinned = Bitboard::default(); - let mut capture_mask = enemy_or_empty & !(*board.piece_bitboard(Piece::King, them)); - let mut orthogonal_pin_rays = Bitboard::default(); - let mut diagonal_pin_rays = Bitboard::default(); - - // Super-piece method: project attacks from king square with opposite side semantics - let mut checkers = *board.piece_bitboard(Piece::Knight, them) & attacks::knight(king_sq) - | *board.piece_bitboard(Piece::Pawn, them) & attacks::pawn(king_sq, us); - - let enemy_sliding_attacks = attacks::rook(king_sq, Bitboard::default()) - & (*board.piece_bitboard(Piece::Rook, them) | *board.piece_bitboard(Piece::Queen, them)) - | attacks::bishop(king_sq, Bitboard::default()) - & (*board.piece_bitboard(Piece::Bishop, them) - | *board.piece_bitboard(Piece::Queen, them)); - - for next_attacker_sq in enemy_sliding_attacks.iter() { - let attacker_bb = Bitboard::from_square(next_attacker_sq); - - let ray = rays::between(king_sq, next_attacker_sq); - - let (king_file, king_rank) = square::from_square(king_sq); - let (attacker_file, attacker_rank) = square::from_square(next_attacker_sq); - let is_orthogonal = king_file == attacker_file || king_rank == attacker_rank; - let is_diagonal = (king_sq as i16 - next_attacker_sq as i16).abs() % 9 == 0 - || (king_sq as i16 - next_attacker_sq as i16).abs() % 7 == 0; - - match (ray & occupancy).number_of_occupied_squares() { - 0 => { - checkers |= Bitboard::from_square(next_attacker_sq); - } - 1 => { - let overlap = ray & our_pieces; - if overlap.number_of_occupied_squares() == 1 { - pinned |= ray & our_pieces; - if is_orthogonal { - orthogonal_pin_rays |= ray | attacker_bb; - } else if is_diagonal { - diagonal_pin_rays |= ray | attacker_bb; - } - } - } - _ => {} - } - } - - let mut push_mask = Bitboard::FULL; - - if checkers.number_of_occupied_squares() >= 1 { - let is_single_check = checkers.number_of_occupied_squares() == 1; - - capture_mask = checkers & !(*board.piece_bitboard(Piece::King, them)); - - if is_single_check { - let mut checkers_clone = checkers; - let checker = bitboard_helpers::next_bit(&mut checkers_clone) as u8; - - let ray = rays::between(king_sq, checker as u8); - - if let Some((piece, side)) = board.piece_on_square(checker as u8) { - debug_assert!(side == them); - let is_slider = piece.is_slider(); - if is_slider { - push_mask = ray; - } else { - push_mask = Bitboard::default(); - } - } - } - } - - let en_passant_bb = board - .en_passant_square() - .map(Bitboard::from) - .unwrap_or_default(); - match board.side_to_move() { - Side::White => { - let left = en_passant_bb >> SOUTH; - if left & checkers != 0 { - capture_mask |= en_passant_bb; - } - } - Side::Black => { - let right = en_passant_bb << NORTH; - if right & checkers != 0 { - capture_mask |= en_passant_bb; - } - } - } - - ( - checkers, - capture_mask, - push_mask, - pinned, - orthogonal_pin_rays, - diagonal_pin_rays, - ) -} - fn get_castling_moves(board: &Board, move_list: &mut MoveList) { /* * For castling, the king and rook must not have moved. @@ -293,6 +166,7 @@ fn get_castling_moves(board: &Board, move_list: &mut MoveList) { Piece::King, board, move_list, + enumerate::PromotionFilter::All, ); } @@ -325,6 +199,7 @@ fn get_piece_moves(piece: Piece, board: &Board, move_list: &mut MoveList, move_t piece, board, move_list, + enumerate::PromotionFilter::All, ); } } @@ -419,6 +294,7 @@ fn get_pawn_moves(board: &Board, move_list: &mut MoveList, move_type: &MoveType) Piece::Pawn, board, move_list, + enumerate::PromotionFilter::All, ); } } @@ -463,7 +339,9 @@ pub fn are_legal(board: &Board, list: &MoveList) -> bool { } /// Re-export from legal_move_generation for convenience. -pub use crate::legal_move_generation::generate_legal_moves; +pub use crate::legal_move_generation::{ + generate_legal_moves, generate_legal_quiets, generate_legal_tacticals, +}; #[cfg(test)] mod tests { @@ -482,20 +360,20 @@ mod tests { Board::from_fen("2kr3r/p1ppqpb1/bn2Qnp1/3PN3/1p2P3/2N5/PPPBBPPP/R3K2R b KQ - 3 2") .unwrap(); let occupancy = board.all_pieces(); - let (_, _, _, pinned, _, _) = calculate_check_and_pin_metadata(&board); + let meta = metadata::compute(&board); let checkers = calculate_checkers(&board, occupancy); assert_eq!(checkers, 0); - assert_eq!(pinned, Bitboard::from_square(Squares::D7)); + assert_eq!(meta.pinned, Bitboard::from_square(Squares::D7)); } #[test] fn calculate_pinned_pieces_2() { let board = Board::from_fen("8/8/8/8/k2Pp2Q/8/8/3K4 b - d3 0 1").unwrap(); let occupancy = board.all_pieces(); - let (_, _, _, pinned, _, _) = calculate_check_and_pin_metadata(&board); + let meta = metadata::compute(&board); let checkers = calculate_checkers(&board, occupancy); assert_eq!(checkers, 0); - assert_eq!(pinned, Bitboard::default()); + assert_eq!(meta.pinned, Bitboard::default()); } #[test] @@ -504,12 +382,11 @@ mod tests { Board::from_fen("rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQKR2 b Q - 2 8").unwrap(); let occupancy = board.all_pieces(); - let (_, _, _, pinned, orthogonal_rays, diagonal_rays) = - calculate_check_and_pin_metadata(&board); - let pin_rays = orthogonal_rays | diagonal_rays; + let meta = metadata::compute(&board); + let pin_rays = meta.orthogonal_pin_rays | meta.diagonal_pin_rays; let checkers = calculate_checkers(&board, occupancy); assert_eq!(checkers, 0); - assert_eq!(pinned, 0); + assert_eq!(meta.pinned, 0); assert_eq!(pin_rays, 0); } @@ -518,52 +395,47 @@ mod tests { let board = Board::from_fen("r3k2r/Pppp1ppp/1b3nbN/nPB5/B1P1P3/5N2/q2P1KPP/b2Q1R2 w kq - 0 3") .unwrap(); - let (_, _, _, pinned_pieces, horizontal_pin_rays, diagonal_pin_rays) = - calculate_check_and_pin_metadata(&board); + let meta = metadata::compute(&board); - assert_eq!(pinned_pieces.number_of_occupied_squares(), 2); - println!("horizontal pin rays:\n{horizontal_pin_rays}"); - println!("diagonal pin rays:\n{diagonal_pin_rays}"); + assert_eq!(meta.pinned.number_of_occupied_squares(), 2); + println!("horizontal pin rays:\n{}", meta.orthogonal_pin_rays); + println!("diagonal pin rays:\n{}", meta.diagonal_pin_rays); - assert!(pinned_pieces.intersects(Bitboard::from_square(Squares::C5))); - assert!(pinned_pieces.intersects(Bitboard::from_square(Squares::D2))); + assert!(meta.pinned.intersects(Bitboard::from_square(Squares::C5))); + assert!(meta.pinned.intersects(Bitboard::from_square(Squares::D2))); } #[test] fn check_pinned_and_capture_mask() { let board = Board::from_fen("rnQq1k1r/pp2bppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R b KQ - 0 8").unwrap(); - let (checkers, capture_mask, push_mask, pinned, orthogonal_rays, diagonal_rays) = - calculate_check_and_pin_metadata(&board); - println!("checkers:\n{checkers}"); - println!("check mask:\n{capture_mask}"); - println!("push mask:\n{push_mask}"); - println!("pinned:\n{pinned}"); - println!("orthogonal rays:\n{orthogonal_rays}"); - println!("diagonal rays:\n{diagonal_rays}"); - - assert_eq!(checkers, 0); - assert_eq!(pinned, Bitboard::from_square(Squares::D8)); - println!("capture mask:\n{capture_mask}"); - println!("push mask:\n{push_mask}"); + let meta = metadata::compute(&board); + println!("checkers:\n{}", meta.checkers); + println!("check mask:\n{}", meta.capture_mask); + println!("push mask:\n{}", meta.push_mask); + println!("pinned:\n{}", meta.pinned); + println!("orthogonal rays:\n{}", meta.orthogonal_pin_rays); + println!("diagonal rays:\n{}", meta.diagonal_pin_rays); + + assert_eq!(meta.checkers, 0); + assert_eq!(meta.pinned, Bitboard::from_square(Squares::D8)); } #[test] fn check_pinned_and_capture_mask_2() { let board = Board::from_fen("4B1r1/2q2p2/QP4k1/3P2p1/7B/8/6K1/7R b - - 3 59").unwrap(); - let (checkers, capture_mask, push_mask, pinned, orthogonal_rays, diagonal_rays) = - calculate_check_and_pin_metadata(&board); - println!("checkers:\n{checkers}"); - println!("check mask:\n{capture_mask}"); - println!("push mask:\n{push_mask}"); - println!("pinned:\n{pinned}"); - println!("orthogonal rays:\n{orthogonal_rays}"); - println!("diagonal rays:\n{diagonal_rays}"); - - assert_eq!(checkers, 0); - assert_eq!(pinned, Bitboard::from_square(Squares::F7)); - assert_eq!(orthogonal_rays, 0); - assert!(diagonal_rays > 0); + let meta = metadata::compute(&board); + println!("checkers:\n{}", meta.checkers); + println!("check mask:\n{}", meta.capture_mask); + println!("push mask:\n{}", meta.push_mask); + println!("pinned:\n{}", meta.pinned); + println!("orthogonal rays:\n{}", meta.orthogonal_pin_rays); + println!("diagonal rays:\n{}", meta.diagonal_pin_rays); + + assert_eq!(meta.checkers, 0); + assert_eq!(meta.pinned, Bitboard::from_square(Squares::F7)); + assert_eq!(meta.orthogonal_pin_rays, 0); + assert!(meta.diagonal_pin_rays > 0); } #[test] From 3fd3b705f988a084d6aa44aaabede2a901bbf842 Mon Sep 17 00:00:00 2001 From: Paul Tsouchlos Date: Wed, 18 Mar 2026 22:20:48 -0400 Subject: [PATCH 04/10] chore: auto format code bench: 729284 --- chess/src/legal_move_generation.rs | 9 +++------ chess/src/move_generation/enumerate.rs | 25 ++++++++++++++++++------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/chess/src/legal_move_generation.rs b/chess/src/legal_move_generation.rs index 176695c..a4cb6f4 100644 --- a/chess/src/legal_move_generation.rs +++ b/chess/src/legal_move_generation.rs @@ -724,8 +724,7 @@ mod tests { #[test] fn tacticals_are_captures_and_queen_promotions() { let board = - Board::from_fen("rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8") - .unwrap(); + Board::from_fen("rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8").unwrap(); let meta = move_generation::metadata::compute(&board); let mut tacticals = MoveList::new(); @@ -745,8 +744,7 @@ mod tests { #[test] fn quiets_are_non_captures_and_underpromotions() { let board = - Board::from_fen("rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8") - .unwrap(); + Board::from_fen("rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8").unwrap(); let meta = move_generation::metadata::compute(&board); let mut quiets = MoveList::new(); @@ -779,8 +777,7 @@ mod tests { #[test] fn castling_is_quiet() { - let board = - Board::from_fen("r3k2r/pppppppp/8/8/8/8/PPPPPPPP/R3K2R w KQkq - 0 1").unwrap(); + let board = Board::from_fen("r3k2r/pppppppp/8/8/8/8/PPPPPPPP/R3K2R w KQkq - 0 1").unwrap(); let meta = move_generation::metadata::compute(&board); let mut quiets = MoveList::new(); diff --git a/chess/src/move_generation/enumerate.rs b/chess/src/move_generation/enumerate.rs index 521d165..0af12ea 100644 --- a/chess/src/move_generation/enumerate.rs +++ b/chess/src/move_generation/enumerate.rs @@ -143,7 +143,14 @@ mod tests { let bb = Bitboard::from_square(60); // e8 let mut all = MoveList::new(); - enumerate_moves(&bb, &pawn_sq, Piece::Pawn, &board, &mut all, PromotionFilter::All); + enumerate_moves( + &bb, + &pawn_sq, + Piece::Pawn, + &board, + &mut all, + PromotionFilter::All, + ); assert_eq!(all.len(), 4); // Q, R, B, N let mut queen_only = MoveList::new(); @@ -156,9 +163,11 @@ mod tests { PromotionFilter::QueenOnly, ); assert_eq!(queen_only.len(), 1); - assert!(queen_only - .iter() - .all(|mv| mv.promotion_piece() == Some(Piece::Queen))); + assert!( + queen_only + .iter() + .all(|mv| mv.promotion_piece() == Some(Piece::Queen)) + ); let mut under_only = MoveList::new(); enumerate_moves( @@ -170,8 +179,10 @@ mod tests { PromotionFilter::UnderOnly, ); assert_eq!(under_only.len(), 3); - assert!(under_only - .iter() - .all(|mv| mv.promotion_piece() != Some(Piece::Queen))); + assert!( + under_only + .iter() + .all(|mv| mv.promotion_piece() != Some(Piece::Queen)) + ); } } From c8aa08bf0e33f9e59391620d46f40d901ff743b0 Mon Sep 17 00:00:00 2001 From: Paul Tsouchlos Date: Thu, 19 Mar 2026 12:53:29 -0400 Subject: [PATCH 05/10] chore: simplify legal move gen The new approach had significantly worse performance (~13% worse) in search bench. This simplifies our approach to just provide a move type (all, capture, quiet) when generating moves and then the mobility bitboards are just &'d with a mask. This makes the new move gen only ~3% worse on average compared to main. bench: 773383 --- chess/src/legal_move_generation.rs | 389 +++++-------------------- chess/src/move_generation/enumerate.rs | 78 +---- 2 files changed, 75 insertions(+), 392 deletions(-) diff --git a/chess/src/legal_move_generation.rs b/chess/src/legal_move_generation.rs index a4cb6f4..3085056 100644 --- a/chess/src/legal_move_generation.rs +++ b/chess/src/legal_move_generation.rs @@ -8,7 +8,10 @@ use crate::definitions::RANK_BITBOARDS; use crate::move_generation; use crate::move_generation::NORTH; use crate::move_generation::SOUTH; +use crate::move_generation::enumerate::enumerate_moves; +use crate::move_generation::metadata::CheckPinMetadata; use crate::move_list::MoveList; +use crate::moves::MoveType; use crate::rays; use crate::square; use crate::{ @@ -321,40 +324,39 @@ pub(crate) fn generate_legal_mobility( piece: Piece, square: Square, board: &Board, - pinned_mask: Bitboard, - capture_mask: Bitboard, - push_mask: Bitboard, - orthogonal_pin_rays: Bitboard, - diagonal_pin_rays: Bitboard, - checkers: Bitboard, + metadata: &CheckPinMetadata, ) -> Bitboard { match piece { Piece::Pawn => generate_legal_pawn_mobility( board, square, - pinned_mask, - capture_mask, - push_mask, - orthogonal_pin_rays, - diagonal_pin_rays, - checkers, + metadata.pinned, + metadata.capture_mask, + metadata.push_mask, + metadata.orthogonal_pin_rays, + metadata.diagonal_pin_rays, + metadata.checkers, ), - Piece::King => generate_king_legal_mobility(square, board, capture_mask, checkers), + Piece::King => { + generate_king_legal_mobility(square, board, metadata.capture_mask, metadata.checkers) + } _ => generate_normal_piece_legal_mobility( piece, square, board, - capture_mask, - pinned_mask, - push_mask, - orthogonal_pin_rays, - diagonal_pin_rays, + metadata.capture_mask, + metadata.pinned, + metadata.push_mask, + metadata.orthogonal_pin_rays, + metadata.diagonal_pin_rays, ), } } /// Generate all legal moves for the current [`Board`] state. /// +/// This is a convenience wrapper that generates tacticals followed by quiets. +/// /// # Arguments /// /// - `board` - The current board state @@ -372,232 +374,55 @@ pub(crate) fn generate_legal_mobility( /// move_generation::generate_legal_moves(&board, &mut move_list); /// assert_eq!(20, move_list.len()) /// ``` -/// Generate legal tactical moves: captures, en passant, and queen promotions. -/// -/// Capture-promotions generate all 4 promotion types (they are captures first). -/// Non-capture promotions generate only the queen promotion variant. -/// -/// Must be called with metadata from [`move_generation::metadata::compute`]. -pub fn generate_legal_tacticals( - board: &Board, - meta: &move_generation::metadata::CheckPinMetadata, - move_list: &mut MoveList, -) { +pub fn generate_legal_moves(board: &Board, move_types: MoveType) -> MoveList { let us = board.side_to_move(); - let their_pieces = board.pieces(us.opposite()); - let king_bb = board.piece_bitboard(Piece::King, us); - let king_square = board.king_square(us); - let occupancy = board.all_pieces(); - let en_passant_bb = board - .en_passant_square() - .map(Bitboard::from) - .unwrap_or_default(); - let promotion_rank_bb = Rank::promotion_rank(us).to_bitboard(); - - // King captures (castling excluded naturally since castling squares are empty) - let king_sq = Square::from_square_index(king_square); - let king_mobility = - generate_king_legal_mobility(king_sq, board, meta.capture_mask, meta.checkers); - let king_captures = king_mobility & their_pieces; - move_generation::enumerate::enumerate_moves( - &king_captures, - &king_sq, - Piece::King, - board, - move_list, - move_generation::enumerate::PromotionFilter::All, - ); - - // Double check: only king moves are legal - if meta.num_checkers() > 1 { - return; - } - + let them = us.opposite(); let our_pieces = board.pieces(us); - let moveable_pieces = our_pieces & !(*king_bb); - - for from_sq in moveable_pieces.iter() { - let (piece, _) = match board.piece_on_square(from_sq) { - Some(p) => p, - None => continue, - }; + let their_pieces = board.pieces(them); + let filter = match move_types { + MoveType::All => Bitboard::FULL, + MoveType::Capture => their_pieces, + MoveType::Quiet => !their_pieces, + }; - let from_square = Square::from_square_index(from_sq); - let mobility = generate_legal_mobility( - piece, - from_square, - board, - meta.pinned, - meta.capture_mask, - meta.push_mask, - meta.orthogonal_pin_rays, - meta.diagonal_pin_rays, - meta.checkers, - ); - - if piece == Piece::Pawn { - // Pawn captures (including en passant and capture-promotions with all 4 types) - let captures = mobility & (their_pieces | en_passant_bb); - move_generation::enumerate::enumerate_moves( - &captures, - &from_square, - piece, - board, - move_list, - move_generation::enumerate::PromotionFilter::All, - ); + let king_sq_idx = board.king_square(us); + let king_sq = Square::from_square_index(king_sq_idx); + let king_bb = Bitboard::from_square(king_sq_idx); - // Non-capture queen promotions only (pushes to promotion rank) - let queen_promo_pushes = mobility & !occupancy & promotion_rank_bb; - move_generation::enumerate::enumerate_moves( - &queen_promo_pushes, - &from_square, - piece, - board, - move_list, - move_generation::enumerate::PromotionFilter::QueenOnly, - ); - } else { - // Non-pawn piece captures - let captures = mobility & their_pieces; - move_generation::enumerate::enumerate_moves( - &captures, - &from_square, - piece, - board, - move_list, - move_generation::enumerate::PromotionFilter::All, - ); - } - } -} + let mut move_list = MoveList::new(); + let meta = move_generation::metadata::compute(board); -/// Generate legal quiet moves: non-captures, castling, and underpromotions. -/// -/// Non-capture promotion pushes generate only underpromotion types (Rook, Bishop, Knight). -/// Queen promotions are generated by [`generate_legal_tacticals`] instead. -/// -/// Must be called with metadata from [`move_generation::metadata::compute`]. -pub fn generate_legal_quiets( - board: &Board, - meta: &move_generation::metadata::CheckPinMetadata, - move_list: &mut MoveList, -) { - let us = board.side_to_move(); - let their_pieces = board.pieces(us.opposite()); - let king_bb = board.piece_bitboard(Piece::King, us); - let king_square = board.king_square(us); - let occupancy = board.all_pieces(); - let promotion_rank_bb = Rank::promotion_rank(us).to_bitboard(); - - // King quiet moves (non-captures + castling) - let king_sq = Square::from_square_index(king_square); - let king_mobility = - generate_king_legal_mobility(king_sq, board, meta.capture_mask, meta.checkers); - let king_quiets = king_mobility & !their_pieces; - move_generation::enumerate::enumerate_moves( - &king_quiets, - &king_sq, - Piece::King, + // King moves first + let king_moves = generate_king_legal_mobility( + Square::from_square_index(king_sq_idx), board, - move_list, - move_generation::enumerate::PromotionFilter::All, - ); + meta.capture_mask, + meta.checkers, + ) & filter; + + enumerate_moves(&king_moves, &king_sq, Piece::King, board, &mut move_list); - // Double check: only king moves are legal + // Return early if in double check since only king moves are legal if meta.num_checkers() > 1 { - return; + return move_list; } - let our_pieces = board.pieces(us); - let moveable_pieces = our_pieces & !(*king_bb); + // Proceed with non-king pieces + let moveable_pieces = our_pieces & !king_bb; - for from_sq in moveable_pieces.iter() { - let (piece, _) = match board.piece_on_square(from_sq) { - Some(p) => p, + for from_sq_idx in moveable_pieces.iter() { + let piece = match board.piece_on_square(from_sq_idx) { + Some((piece, _)) => piece, None => continue, }; - let from_square = Square::from_square_index(from_sq); - let mobility = generate_legal_mobility( - piece, - from_square, - board, - meta.pinned, - meta.capture_mask, - meta.push_mask, - meta.orthogonal_pin_rays, - meta.diagonal_pin_rays, - meta.checkers, - ); - - if piece == Piece::Pawn { - // Non-capture, non-promotion pawn pushes - // Exclude en passant square — it's empty but captured as a tactical - let en_passant_bb = board - .en_passant_square() - .map(Bitboard::from) - .unwrap_or_default(); - let quiet_pushes = mobility & !occupancy & !promotion_rank_bb & !en_passant_bb; - move_generation::enumerate::enumerate_moves( - &quiet_pushes, - &from_square, - piece, - board, - move_list, - move_generation::enumerate::PromotionFilter::All, - ); + let from_sq = Square::from_square_index(from_sq_idx); + let moves = generate_legal_mobility(piece, from_sq, board, &meta) & filter; - // Non-capture underpromotions (pushes to promotion rank, R/B/N only) - let underpromo_pushes = mobility & !occupancy & promotion_rank_bb; - move_generation::enumerate::enumerate_moves( - &underpromo_pushes, - &from_square, - piece, - board, - move_list, - move_generation::enumerate::PromotionFilter::UnderOnly, - ); - } else { - // Non-pawn quiet moves (mobility excluding enemy pieces) - let quiets = mobility & !their_pieces; - move_generation::enumerate::enumerate_moves( - &quiets, - &from_square, - piece, - board, - move_list, - move_generation::enumerate::PromotionFilter::All, - ); - } + enumerate_moves(&moves, &from_sq, piece, board, &mut move_list); } -} -/// Generate all legal moves for the current [`Board`] state. -/// -/// This is a convenience wrapper that generates tacticals followed by quiets. -/// -/// # Arguments -/// -/// - `board` - The current board state -/// - `move_list` - The list of moves to append to -/// -/// # Examples -/// -/// ``` -/// use chess::board::Board; -/// use chess::move_list::MoveList; -/// use chess::move_generation; -/// -/// let board = Board::default_board(); -/// let mut move_list = MoveList::new(); -/// move_generation::generate_legal_moves(&board, &mut move_list); -/// assert_eq!(20, move_list.len()) -/// ``` -pub fn generate_legal_moves(board: &Board, move_list: &mut MoveList) { - let meta = move_generation::metadata::compute(board); - generate_legal_tacticals(board, &meta, move_list); - generate_legal_quiets(board, &meta, move_list); + move_list } #[cfg(test)] @@ -606,12 +431,14 @@ mod tests { use super::*; + fn generate_moves_for_fen(fen: &str) -> MoveList { + let board = Board::from_fen(fen).unwrap(); + generate_legal_moves(&board, MoveType::All) + } + #[test] fn en_passant_capture_causes_discovered_check() { - let board = Board::from_fen("8/8/8/8/k2Pp2Q/8/8/3K4 b - d3 0 1").unwrap(); - let mut move_list = MoveList::new(); - generate_legal_moves(&board, &mut move_list); - + let move_list = generate_moves_for_fen("8/8/8/8/k2Pp2Q/8/8/3K4 b - d3 0 1"); for mv in move_list.iter() { println!("{mv}"); } @@ -621,27 +448,19 @@ mod tests { #[test] fn king_cannot_move_away_from_slider() { - let board = Board::from_fen("4k3/8/8/8/4R3/8/8/4K3 b - - 0 1").unwrap(); - - let mut move_list = MoveList::new(); - generate_legal_moves(&board, &mut move_list); + let move_list = generate_moves_for_fen("4k3/8/8/8/4R3/8/8/4K3 b - - 0 1"); assert_eq!(move_list.len(), 4); } #[test] fn king_cannot_slide_away_from_bishop() { - let board = Board::from_fen("r6r/1b2k1bq/8/8/7B/8/8/R3K2R b KQ - 3 2").unwrap(); - - let mut move_list = MoveList::new(); - generate_legal_moves(&board, &mut move_list); + let move_list = generate_moves_for_fen("r6r/1b2k1bq/8/8/7B/8/8/R3K2R b KQ - 3 2"); assert_eq!(move_list.len(), 8); } #[test] fn evade_check_with_en_passant_capture() { - let board = Board::from_fen("8/8/8/2k5/3Pp3/8/8/4K3 b - d3 0 1").unwrap(); - let mut move_list = MoveList::new(); - generate_legal_moves(&board, &mut move_list); + let move_list = generate_moves_for_fen("8/8/8/2k5/3Pp3/8/8/4K3 b - d3 0 1"); for mv in move_list.iter() { println!("{mv}"); @@ -694,14 +513,16 @@ mod tests { for fen in &positions { let board = Board::from_fen(fen).unwrap(); - let meta = move_generation::metadata::compute(&board); - let mut all_moves = MoveList::new(); - generate_legal_moves(&board, &mut all_moves); + let all_moves = generate_legal_moves(&board, MoveType::All); + let captures = generate_legal_moves(&board, MoveType::Capture); + let quiets = generate_legal_moves(&board, MoveType::Quiet); - let mut staged_moves = MoveList::new(); - generate_legal_tacticals(&board, &meta, &mut staged_moves); - generate_legal_quiets(&board, &meta, &mut staged_moves); + let staged_moves = captures + .iter() + .chain(quiets.iter()) + .cloned() + .collect::>(); assert_eq!( all_moves.len(), @@ -720,76 +541,4 @@ mod tests { } } } - - #[test] - fn tacticals_are_captures_and_queen_promotions() { - let board = - Board::from_fen("rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8").unwrap(); - let meta = move_generation::metadata::compute(&board); - - let mut tacticals = MoveList::new(); - generate_legal_tacticals(&board, &meta, &mut tacticals); - - for mv in tacticals.iter() { - let is_capture = mv.is_capture(); - let is_queen_promo = mv.promotion_piece() == Some(Piece::Queen); - assert!( - is_capture || is_queen_promo, - "Non-tactical move {} found in tacticals", - mv.to_long_algebraic() - ); - } - } - - #[test] - fn quiets_are_non_captures_and_underpromotions() { - let board = - Board::from_fen("rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8").unwrap(); - let meta = move_generation::metadata::compute(&board); - - let mut quiets = MoveList::new(); - generate_legal_quiets(&board, &meta, &mut quiets); - - for mv in quiets.iter() { - let is_capture = mv.is_capture(); - let is_queen_promo = mv.promotion_piece() == Some(Piece::Queen); - assert!( - !is_capture && !is_queen_promo, - "Tactical move {} found in quiets (capture={}, queen_promo={})", - mv.to_long_algebraic(), - is_capture, - is_queen_promo, - ); - } - } - - #[test] - fn en_passant_is_tactical() { - let board = Board::from_fen("8/8/8/2k5/3Pp3/8/8/4K3 b - d3 0 1").unwrap(); - let meta = move_generation::metadata::compute(&board); - - let mut tacticals = MoveList::new(); - generate_legal_tacticals(&board, &meta, &mut tacticals); - - let has_ep = tacticals.iter().any(|mv| mv.is_en_passant_capture()); - assert!(has_ep, "En passant capture should be in tacticals"); - } - - #[test] - fn castling_is_quiet() { - let board = Board::from_fen("r3k2r/pppppppp/8/8/8/8/PPPPPPPP/R3K2R w KQkq - 0 1").unwrap(); - let meta = move_generation::metadata::compute(&board); - - let mut quiets = MoveList::new(); - generate_legal_quiets(&board, &meta, &mut quiets); - - let has_castle = quiets.iter().any(|mv| mv.is_castle()); - assert!(has_castle, "Castling should be in quiets"); - - let mut tacticals = MoveList::new(); - generate_legal_tacticals(&board, &meta, &mut tacticals); - - let castle_in_tacticals = tacticals.iter().any(|mv| mv.is_castle()); - assert!(!castle_in_tacticals, "Castling should NOT be in tacticals"); - } } diff --git a/chess/src/move_generation/enumerate.rs b/chess/src/move_generation/enumerate.rs index 0af12ea..8cf88b3 100644 --- a/chess/src/move_generation/enumerate.rs +++ b/chess/src/move_generation/enumerate.rs @@ -42,7 +42,6 @@ pub(crate) fn enumerate_moves( piece: Piece, board: &Board, move_list: &mut MoveList, - promotion_filter: PromotionFilter, ) { // Stop if the bitboard is empty. if bitboard.as_number() == 0 { @@ -95,20 +94,12 @@ pub(crate) fn enumerate_moves( let to_square = square::to_square_object(file, rank); if is_promotion { - let promotion_types: &[PromotionDescriptor] = match promotion_filter { - PromotionFilter::All => &[ - PromotionDescriptor::Queen, - PromotionDescriptor::Rook, - PromotionDescriptor::Bishop, - PromotionDescriptor::Knight, - ], - PromotionFilter::QueenOnly => &[PromotionDescriptor::Queen], - PromotionFilter::UnderOnly => &[ - PromotionDescriptor::Rook, - PromotionDescriptor::Bishop, - PromotionDescriptor::Knight, - ], - }; + let promotion_types: &[PromotionDescriptor] = &[ + PromotionDescriptor::Queen, + PromotionDescriptor::Rook, + PromotionDescriptor::Bishop, + PromotionDescriptor::Knight, + ]; for promotion_type in promotion_types { let mv = Move::new( from, @@ -129,60 +120,3 @@ pub(crate) fn enumerate_moves( } } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::{board::Board, move_list::MoveList, pieces::Piece}; - - #[test] - fn enumerate_queen_promotion_only() { - // White pawn on e7, no captures — push to e8 - let board = Board::from_fen("4k3/4P3/8/8/8/8/8/4K3 w - - 0 1").unwrap(); - let pawn_sq = Square::from_file_rank('e', 6).unwrap(); // e7 - let bb = Bitboard::from_square(60); // e8 - - let mut all = MoveList::new(); - enumerate_moves( - &bb, - &pawn_sq, - Piece::Pawn, - &board, - &mut all, - PromotionFilter::All, - ); - assert_eq!(all.len(), 4); // Q, R, B, N - - let mut queen_only = MoveList::new(); - enumerate_moves( - &bb, - &pawn_sq, - Piece::Pawn, - &board, - &mut queen_only, - PromotionFilter::QueenOnly, - ); - assert_eq!(queen_only.len(), 1); - assert!( - queen_only - .iter() - .all(|mv| mv.promotion_piece() == Some(Piece::Queen)) - ); - - let mut under_only = MoveList::new(); - enumerate_moves( - &bb, - &pawn_sq, - Piece::Pawn, - &board, - &mut under_only, - PromotionFilter::UnderOnly, - ); - assert_eq!(under_only.len(), 3); - assert!( - under_only - .iter() - .all(|mv| mv.promotion_piece() != Some(Piece::Queen)) - ); - } -} From a88c1869d72ef3d9673fd5b6801af8c4e7fc93a9 Mon Sep 17 00:00:00 2001 From: Paul Tsouchlos Date: Thu, 19 Mar 2026 12:54:01 -0400 Subject: [PATCH 06/10] fix: legal move gen call sites due to api change We changed the function signature so we have to update all the call sites. bench: 773383 --- chess/src/move_generation.rs | 13 ++++--------- chess/src/move_making.rs | 8 +++----- chess/src/perft.rs | 15 ++++++++------- engine/src/killers_table.rs | 9 ++++----- engine/src/move_order.rs | 9 ++++++--- engine/src/search.rs | 20 ++++++++++---------- 6 files changed, 35 insertions(+), 39 deletions(-) diff --git a/chess/src/move_generation.rs b/chess/src/move_generation.rs index 39759b6..412c21a 100644 --- a/chess/src/move_generation.rs +++ b/chess/src/move_generation.rs @@ -166,7 +166,6 @@ fn get_castling_moves(board: &Board, move_list: &mut MoveList) { Piece::King, board, move_list, - enumerate::PromotionFilter::All, ); } @@ -199,7 +198,6 @@ fn get_piece_moves(piece: Piece, board: &Board, move_list: &mut MoveList, move_t piece, board, move_list, - enumerate::PromotionFilter::All, ); } } @@ -294,7 +292,6 @@ fn get_pawn_moves(board: &Board, move_list: &mut MoveList, move_type: &MoveType) Piece::Pawn, board, move_list, - enumerate::PromotionFilter::All, ); } } @@ -315,8 +312,8 @@ pub fn is_checkmate(board: &Board) -> bool { if !is_in_check(board) { return false; } - let mut move_list = MoveList::new(); - generate_legal_moves(board, &mut move_list); + + let move_list = generate_legal_moves(board, MoveType::All); move_list.is_empty() } @@ -339,9 +336,7 @@ pub fn are_legal(board: &Board, list: &MoveList) -> bool { } /// Re-export from legal_move_generation for convenience. -pub use crate::legal_move_generation::{ - generate_legal_moves, generate_legal_quiets, generate_legal_tacticals, -}; +pub use crate::legal_move_generation::generate_legal_moves; #[cfg(test)] mod tests { @@ -711,7 +706,7 @@ mod tests { assert_eq!(move_list.len(), 20); move_list.clear(); - move_generation::generate_legal_moves(&board, &mut move_list); + let move_list = move_generation::generate_legal_moves(&board, MoveType::All); for mv in move_list.iter() { println!("{mv}"); diff --git a/chess/src/move_making.rs b/chess/src/move_making.rs index ee75d12..6aa4804 100644 --- a/chess/src/move_making.rs +++ b/chess/src/move_making.rs @@ -558,8 +558,7 @@ mod tests { #[test] fn test_making_en_passant_move() { let mut board = Board::from_fen("8/2k5/8/2Pp3r/K7/8/8/8 w - d6 0 1").unwrap(); - let mut move_list = MoveList::new(); - move_generation::generate_legal_moves(&board, &mut move_list); + let move_list = move_generation::generate_legal_moves(&board, MoveType::All); let en_passant_move = move_list .iter() @@ -745,9 +744,8 @@ mod tests { { // start with default board let mut board = Board::default_board(); - - move_generation::generate_legal_moves(&board, &mut move_list); - // only move pawns and do 2 up move + let move_list = move_generation::generate_legal_moves(&board, MoveType::All); + // only move pawns and do 2 up movelet mut move_list = MoveList::new(); let first_mv = move_list .iter() .find(|mv| mv.to_long_algebraic() == "e2e4") diff --git a/chess/src/perft.rs b/chess/src/perft.rs index 701f77b..073df4c 100644 --- a/chess/src/perft.rs +++ b/chess/src/perft.rs @@ -3,7 +3,11 @@ // GNU General Public License v3.0 or later // https://www.gnu.org/licenses/gpl-3.0-standalone.html -use crate::{board::Board, move_generation, move_list::MoveList, moves::Move}; +use crate::{ + board::Board, + move_generation, + moves::{Move, MoveType}, +}; use anyhow::{Result, bail}; pub struct SplitPerftResult { @@ -29,8 +33,7 @@ pub fn split_perft( depth: usize, print_moves: bool, ) -> Result> { - let mut move_list = MoveList::new(); - move_generation::generate_legal_moves(board, &mut move_list); + let move_list = move_generation::generate_legal_moves(board, crate::moves::MoveType::All); let mut results = Vec::new(); for mv in move_list.iter() { @@ -69,8 +72,7 @@ pub fn split_perft( #[cfg_attr(debug_assertions, inline(never))] pub fn perft(board: &mut Board, depth: usize, print_moves: bool) -> Result { let mut nodes = 0; - let mut move_list = MoveList::new(); - move_generation::generate_legal_moves(board, &mut move_list); + let move_list = move_generation::generate_legal_moves(board, MoveType::All); if print_moves { for mv in move_list.iter() { @@ -712,9 +714,8 @@ mod tests { fn kz_many_moves() { let board = Board::from_fen("R6R/3Q4/1Q4Q1/4Q3/2Q4Q/Q4Q2/pp1Q4/kBNN1KB1 w - - 0 1").unwrap(); - let mut move_list = MoveList::new(); - move_generation::generate_legal_moves(&board, &mut move_list); + let move_list = move_generation::generate_legal_moves(&board, MoveType::All); assert_eq!(move_list.len(), 218); } diff --git a/engine/src/killers_table.rs b/engine/src/killers_table.rs index ec01362..0f2ee69 100644 --- a/engine/src/killers_table.rs +++ b/engine/src/killers_table.rs @@ -52,7 +52,7 @@ impl Default for KillerMovesTable { mod tests { use super::KillerMovesTable; use crate::defs::{MAX_DEPTH, MAX_KILLERS_PER_PLY}; - use chess::{board::Board, move_generation, move_list::MoveList}; + use chess::{board::Board, move_generation, moves::MoveType}; #[test] fn initialize_killers_table() { @@ -68,8 +68,8 @@ mod tests { fn killer_update_no_duplicate_in_slot0() { let mut kt = KillerMovesTable::new(); let board = Board::default_board(); - let mut move_list = MoveList::default(); - move_generation::generate_legal_moves(&board, &mut move_list); + + let move_list = move_generation::generate_legal_moves(&board, MoveType::All); let mv_a = *move_list.at(0).unwrap(); let mv_b = *move_list.at(1).unwrap(); @@ -86,8 +86,7 @@ mod tests { fn killer_update_rotates_slots() { let mut kt = KillerMovesTable::new(); let board = Board::default_board(); - let mut move_list = MoveList::default(); - move_generation::generate_legal_moves(&board, &mut move_list); + let move_list = move_generation::generate_legal_moves(&board, MoveType::All); let mv_a = *move_list.at(0).unwrap(); let mv_b = *move_list.at(1).unwrap(); diff --git a/engine/src/move_order.rs b/engine/src/move_order.rs index 29eeb11..3976d82 100644 --- a/engine/src/move_order.rs +++ b/engine/src/move_order.rs @@ -127,7 +127,11 @@ impl MoveOrder { #[cfg(test)] mod tests { - use chess::{board::Board, move_generation, move_list::MoveList, moves::Move}; + use chess::{ + board::Board, + move_generation, + moves::{Move, MoveType}, + }; use itertools::Itertools; use crate::{ @@ -142,10 +146,9 @@ mod tests { let mut history_table = crate::history_table::HistoryTable::new(); let mut killers_table = crate::killers_table::KillerMovesTable::new(); - let mut move_list = MoveList::new(); let board = Board::from_fen("rnbqkb1r/pppppppp/8/8/8/8/PPPPPPPP/RNBQKB1R w KQkq - 0 1").unwrap(); - move_generation::generate_legal_moves(&board, &mut move_list); + let move_list = move_generation::generate_legal_moves(&board, MoveType::All); assert!(move_list.len() >= 6); let depth = 3i32; diff --git a/engine/src/search.rs b/engine/src/search.rs index 104bd6b..320aa24 100644 --- a/engine/src/search.rs +++ b/engine/src/search.rs @@ -17,8 +17,11 @@ use std::{ use anyhow::{Result, bail}; use arrayvec::ArrayVec; use chess::{ - board::Board, definitions::MAX_MOVE_LIST_SIZE, move_generation, move_list::MoveList, - moves::Move, pieces::Piece, + board::Board, + definitions::MAX_MOVE_LIST_SIZE, + move_generation, + moves::{Move, MoveType}, + pieces::Piece, }; use uci_parser::{UciInfo, UciResponse, UciScore, UciSearchOptions}; @@ -216,8 +219,7 @@ impl<'a, Log: LogLevel> Search<'a, Log> { self.send_message(format!("searching {}", self.parameters)); } - let mut ml = MoveList::new(); - move_generation::generate_legal_moves(board, &mut ml); + let ml = move_generation::generate_legal_moves(board, MoveType::All); let mut result = match ml.len() { 0 => { // Draw or something else? @@ -322,9 +324,8 @@ impl<'a, Log: LogLevel> Search<'a, Log> { fn iterative_deepening(&mut self, board: &mut Board) -> SearchResult { // initialize the best result let mut best_result = SearchResult::default(); - let mut move_list = MoveList::new(); - move_generation::generate_legal_moves(board, &mut move_list); + let move_list = move_generation::generate_legal_moves(board, MoveType::All); if !move_list.is_empty() { best_result.best_move = Some(*move_list.at(0).unwrap()) } @@ -495,9 +496,9 @@ impl<'a, Log: LogLevel> Search<'a, Log> { } // get all legal moves - let mut move_list = MoveList::new(); let mut order_list = ArrayVec::::new(); - move_generation::generate_legal_moves(board, &mut move_list); + let mut move_list = + move_generation::generate_legal_moves(board, chess::moves::MoveType::All); // do we have moves? if move_list.is_empty() { @@ -828,9 +829,8 @@ impl<'a, Log: LogLevel> Search<'a, Log> { alpha }; - let mut move_list = MoveList::new(); let mut move_order_list = ArrayVec::::new(); - move_generation::generate_legal_moves(board, &mut move_list); + let move_list = move_generation::generate_legal_moves(board, MoveType::All); let mut local_pv = PrincipleVariation::new(); // clear the current PV because this is a new position From 3407c7d7d3a0c5b09573a4c2b67d8faf780947e0 Mon Sep 17 00:00:00 2001 From: Paul Tsouchlos Date: Thu, 19 Mar 2026 12:55:10 -0400 Subject: [PATCH 07/10] chore: appease clippy bench: 773383 --- chess/src/legal_move_generation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chess/src/legal_move_generation.rs b/chess/src/legal_move_generation.rs index 3085056..9fa7723 100644 --- a/chess/src/legal_move_generation.rs +++ b/chess/src/legal_move_generation.rs @@ -534,7 +534,7 @@ mod tests { for mv in all_moves.iter() { assert!( - staged_moves.iter().any(|sm| *sm == *mv), + staged_moves.contains(mv), "Move {} from full gen not found in staged gen for position: {fen}", mv.to_long_algebraic() ); From cb4c0500c50bdcbaf1bf4bec42764c948e508de0 Mon Sep 17 00:00:00 2001 From: Paul Tsouchlos Date: Thu, 19 Mar 2026 13:35:26 -0400 Subject: [PATCH 08/10] fix: broken doc test bench: 773383 --- chess/src/legal_move_generation.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chess/src/legal_move_generation.rs b/chess/src/legal_move_generation.rs index 9fa7723..d6d7bd5 100644 --- a/chess/src/legal_move_generation.rs +++ b/chess/src/legal_move_generation.rs @@ -366,12 +366,12 @@ pub(crate) fn generate_legal_mobility( /// /// ``` /// use chess::board::Board; +/// use chess::moves::MoveType; /// use chess::move_list::MoveList; /// use chess::move_generation; /// /// let board = Board::default_board(); -/// let mut move_list = MoveList::new(); -/// move_generation::generate_legal_moves(&board, &mut move_list); +/// let move_list = move_generation::generate_legal_moves(&board, MoveType::All); /// assert_eq!(20, move_list.len()) /// ``` pub fn generate_legal_moves(board: &Board, move_types: MoveType) -> MoveList { From 59cc27ee09c39eafc4d6a5da661802f0ea84e9bd Mon Sep 17 00:00:00 2001 From: Paul Tsouchlos Date: Thu, 19 Mar 2026 13:35:35 -0400 Subject: [PATCH 09/10] chore: add doc tests to just test command bench: 773383 --- Justfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Justfile b/Justfile index f506870..600a7a3 100644 --- a/Justfile +++ b/Justfile @@ -30,6 +30,7 @@ build config="debug": test config="debug": echo "Running tests..." cargo test --workspace --all-targets {{ if config == "release" { "--release" } else { "" } }} -- --include-ignored + cargo test --workspace --doc export LLVM_PROFILE_FILE := "./target/coverage/byte_knight-%p-%m.profraw" From d3453405fa2ab7468c0f50fce0f352d4c4c758b9 Mon Sep 17 00:00:00 2001 From: Paul Tsouchlos Date: Thu, 19 Mar 2026 16:52:28 -0400 Subject: [PATCH 10/10] chore: minor optimizations bench: 773383 --- chess/src/move_generation/enumerate.rs | 6 ++++-- chess/src/move_generation/metadata.rs | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/chess/src/move_generation/enumerate.rs b/chess/src/move_generation/enumerate.rs index 8cf88b3..e1f50b7 100644 --- a/chess/src/move_generation/enumerate.rs +++ b/chess/src/move_generation/enumerate.rs @@ -48,13 +48,15 @@ pub(crate) fn enumerate_moves( return; } + let from_sq_idx = from.to_square_index(); + let (from_file, _from_rank) = square::from_square(from_sq_idx); + let us = board.side_to_move(); let them = us.opposite(); let enemy_pieces = board.pieces(them); let promotion_rank = Rank::promotion_rank(us); for to_square in bitboard.iter() { let (file, rank) = square::from_square(to_square); - let (from_file, _) = square::from_square(from.to_square_index()); let en_passant = match board.en_passant_square() { Some(en_passant_square) => en_passant_square == to_square && piece == Piece::Pawn, @@ -64,7 +66,7 @@ pub(crate) fn enumerate_moves( let is_capture: bool = enemy_pieces.is_square_occupied(to_square) || en_passant; // 2 rows = 16 squares let is_double_move = - piece == Piece::Pawn && (to_square as i8 - from.to_square_index() as i8).abs() == 16; + piece == Piece::Pawn && (to_square as i8 - from_sq_idx as i8).abs() == 16; let is_promotion = piece == Piece::Pawn && square::is_square_on_rank(to_square, promotion_rank as u8); diff --git a/chess/src/move_generation/metadata.rs b/chess/src/move_generation/metadata.rs index f68ba97..7693171 100644 --- a/chess/src/move_generation/metadata.rs +++ b/chess/src/move_generation/metadata.rs @@ -109,8 +109,9 @@ pub fn compute(board: &Board) -> CheckPinMetadata { let mut push_mask = Bitboard::FULL; - if checkers.number_of_occupied_squares() >= 1 { - let is_single_check = checkers.number_of_occupied_squares() == 1; + let checkers_count = checkers.number_of_occupied_squares(); + if checkers_count >= 1 { + let is_single_check = checkers_count == 1; capture_mask = checkers & !(*board.piece_bitboard(Piece::King, them));