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" diff --git a/chess/src/legal_move_generation.rs b/chess/src/legal_move_generation.rs index b094387..d6d7bd5 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::{ @@ -82,7 +85,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 +200,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 +260,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,44 +320,43 @@ 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, - 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 @@ -364,62 +366,63 @@ 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_list: &mut MoveList) { +pub fn generate_legal_moves(board: &Board, move_types: MoveType) -> MoveList { let us = board.side_to_move(); + let them = us.opposite(); let our_pieces = board.pieces(us); + let their_pieces = board.pieces(them); + let filter = match move_types { + MoveType::All => Bitboard::FULL, + MoveType::Capture => their_pieces, + MoveType::Quiet => !their_pieces, + }; - let king_bb = board.piece_bitboard(Piece::King, us); - let king_square = board.king_square(us); - - let (checkers, capture_mask, push_mask, pinned, orthogonal_pin_rays, diagonal_pin_rays) = - move_generation::calculate_check_and_pin_metadata(board); + 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); - let king_sq = Square::from_square_index(king_square); - let king_moves = generate_king_legal_mobility(king_sq, board, capture_mask, checkers); + let mut move_list = MoveList::new(); + let meta = move_generation::metadata::compute(board); - move_generation::enumerate::enumerate_moves( - &king_moves, - &king_sq, - Piece::King, + // King moves first + let king_moves = generate_king_legal_mobility( + Square::from_square_index(king_sq_idx), board, - move_list, - ); + meta.capture_mask, + meta.checkers, + ) & filter; + + enumerate_moves(&king_moves, &king_sq, Piece::King, board, &mut move_list); - let num_checkers = checkers.as_number().count_ones(); - if num_checkers > 1 { - return; + // Return early if in double check since only king moves are legal + if meta.num_checkers() > 1 { + return move_list; } - let moveable_pieces = our_pieces & !(*king_bb); - for from_sq in moveable_pieces.iter() { - let piece = match board.piece_on_square(from_sq) { + // Proceed with non-king pieces + let moveable_pieces = our_pieces & !king_bb; + + 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 moves = generate_legal_mobility( - piece, - from_square, - board, - pinned, - capture_mask, - push_mask, - orthogonal_pin_rays, - diagonal_pin_rays, - checkers, - ); - - move_generation::enumerate::enumerate_moves(&moves, &from_square, piece, board, move_list); + let from_sq = Square::from_square_index(from_sq_idx); + let moves = generate_legal_mobility(piece, from_sq, board, &meta) & filter; + + enumerate_moves(&moves, &from_sq, piece, board, &mut move_list); } + + move_list } #[cfg(test)] @@ -428,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}"); } @@ -443,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}"); @@ -498,4 +495,50 @@ 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 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 staged_moves = captures + .iter() + .chain(quiets.iter()) + .cloned() + .collect::>(); + + 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.contains(mv), + "Move {} from full gen not found in staged gen for position: {fen}", + mv.to_long_algebraic() + ); + } + } + } } diff --git a/chess/src/move_generation.rs b/chess/src/move_generation.rs index 110f253..412c21a 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. @@ -439,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() } @@ -482,20 +355,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 +377,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 +390,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] @@ -839,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_generation/enumerate.rs b/chess/src/move_generation/enumerate.rs index e47d228..e1f50b7 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, @@ -36,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, @@ -52,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); @@ -82,13 +96,13 @@ 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 [ + let promotion_types: &[PromotionDescriptor] = &[ PromotionDescriptor::Queen, PromotionDescriptor::Rook, PromotionDescriptor::Bishop, PromotionDescriptor::Knight, - ] { + ]; + for promotion_type in promotion_types { let mv = Move::new( from, &to_square, diff --git a/chess/src/move_generation/metadata.rs b/chess/src/move_generation/metadata.rs new file mode 100644 index 0000000..7693171 --- /dev/null +++ b/chess/src/move_generation/metadata.rs @@ -0,0 +1,163 @@ +// 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; + + 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)); + + 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, + } +} 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