diff --git a/bitboard/src/bit_scanner.rs b/bitboard/src/bit_scanner.rs index b708fdd..c4b0be2 100644 --- a/bitboard/src/bit_scanner.rs +++ b/bitboard/src/bit_scanner.rs @@ -4,6 +4,7 @@ use chessfriend_core::Square; macro_rules! bit_scanner { ($name:ident) => { + #[derive(Clone, Debug)] pub struct $name { bits: u64, shift: usize, @@ -21,6 +22,7 @@ bit_scanner!(LeadingBitScanner); bit_scanner!(TrailingBitScanner); fn index_to_square(index: usize) -> Square { + debug_assert!(index < Square::NUM); unsafe { #[allow(clippy::cast_possible_truncation)] Square::from_index_unchecked(index as u8) diff --git a/board/src/board.rs b/board/src/board.rs index e7e748a..e5e0cbc 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -119,6 +119,18 @@ impl Board { pub fn knights(&self, color: Color) -> BitBoard { self.find_pieces(Piece::knight(color)) } + + pub fn bishops(&self, color: Color) -> BitBoard { + self.find_pieces(Piece::bishop(color)) + } + + pub fn rooks(&self, color: Color) -> BitBoard { + self.find_pieces(Piece::rook(color)) + } + + pub fn queens(&self, color: Color) -> BitBoard { + self.find_pieces(Piece::queen(color)) + } } impl Board { diff --git a/board/src/sight.rs b/board/src/sight.rs index ea34b7d..8865097 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -94,6 +94,28 @@ impl Sight for Piece { } } +macro_rules! sight_method { + ($name:ident) => { + pub fn $name(&self, square: Square, color: Option) -> BitBoard { + let color = self.unwrap_color(color); + + let info = SightInfo { + square, + occupancy: self.occupancy(), + friendly_occupancy: self.friendly_occupancy(color), + }; + + $name(&info) + } + }; +} + +impl Board { + sight_method!(bishop_sight); + sight_method!(rook_sight); + sight_method!(queen_sight); +} + struct SightInfo { square: Square, occupancy: BitBoard, diff --git a/core/src/lib.rs b/core/src/lib.rs index 64458b4..6edd924 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -10,4 +10,4 @@ mod macros; pub use colors::Color; pub use coordinates::{Direction, File, Rank, Square, Wing}; pub use pieces::{Piece, PlacedPiece}; -pub use shapes::Shape; +pub use shapes::{Shape, Slider}; diff --git a/core/src/shapes.rs b/core/src/shapes.rs index 0bdfea8..fe549f6 100644 --- a/core/src/shapes.rs +++ b/core/src/shapes.rs @@ -69,6 +69,36 @@ impl Shape { } } +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum Slider { + Bishop, + Rook, + Queen, +} + +impl From for Shape { + fn from(value: Slider) -> Self { + match value { + Slider::Bishop => Shape::Bishop, + Slider::Rook => Shape::Rook, + Slider::Queen => Shape::Queen, + } + } +} + +impl TryFrom for Slider { + type Error = (); + + fn try_from(value: Shape) -> Result { + match value { + Shape::Bishop => Ok(Slider::Bishop), + Shape::Rook => Ok(Slider::Rook), + Shape::Queen => Ok(Slider::Queen), + _ => Err(()), + } + } +} + #[derive(Clone, Copy, Debug, Error, Eq, PartialEq)] #[error("no matching piece shape for character '{0:?}'")] pub struct ShapeFromCharError(char); diff --git a/moves/src/generators.rs b/moves/src/generators.rs index 9c84bab..d5c9e37 100644 --- a/moves/src/generators.rs +++ b/moves/src/generators.rs @@ -2,12 +2,14 @@ mod knight; mod pawn; +mod slider; #[cfg(test)] mod testing; pub use knight::KnightMoveGenerator; pub use pawn::PawnMoveGenerator; +pub use slider::{BishopMoveGenerator, QueenMoveGenerator, RookMoveGenerator}; use crate::Move; diff --git a/moves/src/generators/slider.rs b/moves/src/generators/slider.rs new file mode 100644 index 0000000..b40890e --- /dev/null +++ b/moves/src/generators/slider.rs @@ -0,0 +1,607 @@ +// Eryn Wells + +//! Sliders in chess are the pieces that move (a.k.a. "slide") along straight +//! line paths. Bishops, Rooks, and Queens all do this. All of these pieces +//! function identically, though with different sets of rays emanating outward +//! from their origin squares: rooks along orthogonal lines, bishops along +//! diagonals, and queens along both orthogonal and diagonal lines. +//! +//! This module implements the [`SliderMoveGenerator`] which iterates all the +//! slider moves from a given square. This module also exports +//! [`BishopMoveGenerator`], [`RookMoveGenerator`], and [`QueenMoveGenerator`] +//! that emit moves for their corresponding pieces. + +use super::GeneratedMove; +use crate::Move; +use chessfriend_bitboard::{bit_scanner::TrailingBitScanner, BitBoard}; +use chessfriend_board::Board; +use chessfriend_core::{Color, Slider, Square}; + +macro_rules! slider_move_generator { + ($vis:vis $name:ident, $slider:ident) => { + #[must_use] + $vis struct $name(SliderMoveGenerator); + + impl $name { + pub fn new(board: &Board, color: Option) -> Self { + Self(SliderMoveGenerator::new(board, Slider::$slider, color)) + } + } + + impl Iterator for $name { + type Item = $crate::GeneratedMove; + + fn next(&mut self) -> Option { + self.0.next() + } + } + }; +} + +slider_move_generator!(pub BishopMoveGenerator, Bishop); +slider_move_generator!(pub RookMoveGenerator, Rook); +slider_move_generator!(pub QueenMoveGenerator, Queen); + +#[must_use] +struct SliderMoveGenerator { + sliders: Vec, + next_sliders_index: usize, + current_slider: Option, + enemies: BitBoard, + friends: BitBoard, +} + +impl SliderMoveGenerator { + fn new(board: &Board, slider: Slider, color: Option) -> Self { + let color = board.unwrap_color(color); + + let pieces = match slider { + Slider::Bishop => board.bishops(color), + Slider::Rook => board.rooks(color), + Slider::Queen => board.queens(color), + }; + + let enemies = board.enemies(color); + let friends = board.friendly_occupancy(color); + + let sliders = pieces + .occupied_squares_trailing() + .map(|origin| SliderInfo::new(board, origin, slider, color)) + .collect(); + + Self { + sliders, + next_sliders_index: 0, + current_slider: None, + enemies, + friends, + } + } +} + +impl Iterator for SliderMoveGenerator { + type Item = GeneratedMove; + + fn next(&mut self) -> Option { + loop { + if self.current_slider.is_none() { + if self.next_sliders_index < self.sliders.len() { + self.current_slider = Some(self.sliders[self.next_sliders_index].clone()); + self.next_sliders_index += 1; + } else { + return None; + } + } + + if let Some(current_slider) = self.current_slider.as_mut() { + if let Some(target) = current_slider.next() { + let target_bitboard: BitBoard = target.into(); + + let is_targeting_friendly_piece = + (target_bitboard & self.friends).is_populated(); + if is_targeting_friendly_piece { + continue; + } + + let is_targeting_enemy_piece = (target_bitboard & self.enemies).is_populated(); + return Some(if is_targeting_enemy_piece { + Move::capture(current_slider.origin, target).into() + } else { + Move::quiet(current_slider.origin, target).into() + }); + } + + self.current_slider = None; + } + } + } +} + +#[derive(Clone, Debug)] +struct SliderInfo { + origin: Square, + iterator: TrailingBitScanner, +} + +impl SliderInfo { + fn new(board: &Board, origin: Square, slider: Slider, color: Color) -> Self { + let color = Some(color); + + let sight = match slider { + Slider::Bishop => board.bishop_sight(origin, color), + Slider::Rook => board.rook_sight(origin, color), + Slider::Queen => board.queen_sight(origin, color), + }; + + Self { + origin, + iterator: sight.occupied_squares_trailing(), + } + } +} + +impl Iterator for SliderInfo { + type Item = Square; + + fn next(&mut self) -> Option { + self.iterator.next() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{assert_move_list, ply}; + use chessfriend_board::test_board; + + #[test] + fn white_b5_rook() { + let board = test_board!(White Rook on B5); + assert_move_list!( + RookMoveGenerator::new(&board, None), + [ + ply!(B5 - A5), + ply!(B5 - C5), + ply!(B5 - D5), + ply!(B5 - E5), + ply!(B5 - F5), + ply!(B5 - G5), + ply!(B5 - H5), + ply!(B5 - B8), + ply!(B5 - B7), + ply!(B5 - B6), + ply!(B5 - B4), + ply!(B5 - B3), + ply!(B5 - B2), + ply!(B5 - B1), + ] + ); + } + + #[test] + fn black_f4_bishop() { + let board = test_board!(White Bishop on F4); + assert_move_list!( + BishopMoveGenerator::new(&board, None), + [ + ply!(F4 - B8), + ply!(F4 - C7), + ply!(F4 - D6), + ply!(F4 - E5), + ply!(F4 - H6), + ply!(F4 - G5), + ply!(F4 - C1), + ply!(F4 - D2), + ply!(F4 - E3), + ply!(F4 - H2), + ply!(F4 - G3), + ] + ); + } + + #[test] + fn white_d4_queen_ai_claude() { + let board = test_board!(White Queen on D4); + assert_move_list!( + QueenMoveGenerator::new(&board, None), + [ + // Horizontal moves (rook-like) + ply!(D4 - A4), + ply!(D4 - B4), + ply!(D4 - C4), + ply!(D4 - E4), + ply!(D4 - F4), + ply!(D4 - G4), + ply!(D4 - H4), + // Vertical moves (rook-like) + ply!(D4 - D1), + ply!(D4 - D2), + ply!(D4 - D3), + ply!(D4 - D5), + ply!(D4 - D6), + ply!(D4 - D7), + ply!(D4 - D8), + // Diagonal moves (bishop-like) + ply!(D4 - A1), + ply!(D4 - B2), + ply!(D4 - C3), + ply!(D4 - E5), + ply!(D4 - F6), + ply!(D4 - G7), + ply!(D4 - H8), + ply!(D4 - A7), + ply!(D4 - B6), + ply!(D4 - C5), + ply!(D4 - E3), + ply!(D4 - F2), + ply!(D4 - G1), + ] + ); + } + + #[test] + fn white_f3_bishop_with_capture_ai_claude() { + let board = test_board!( + White Bishop on F3, + Black Pawn on C6 + ); + assert_move_list!( + BishopMoveGenerator::new(&board, None), + [ + // Diagonal towards C6 (stops at capture) + ply!(F3 - E4), + ply!(F3 - D5), + ply!(F3 x C6), + // Diagonal towards H1 + ply!(F3 - G2), + ply!(F3 - H1), + // Diagonal towards A8 + ply!(F3 - G4), + ply!(F3 - H5), + // Diagonal towards E2 + ply!(F3 - E2), + ply!(F3 - D1), + ] + ); + } + + #[test] + fn white_e6_rook_with_capture_ai_claude() { + let board = test_board!( + White Rook on E6, + Black Knight on E3 + ); + assert_move_list!( + RookMoveGenerator::new(&board, None), + [ + // Horizontal moves + ply!(E6 - A6), + ply!(E6 - B6), + ply!(E6 - C6), + ply!(E6 - D6), + ply!(E6 - F6), + ply!(E6 - G6), + ply!(E6 - H6), + // Vertical moves up + ply!(E6 - E7), + ply!(E6 - E8), + // Vertical moves down (stops at capture) + ply!(E6 - E5), + ply!(E6 - E4), + ply!(E6 x E3), + ] + ); + } + + #[test] + fn white_c4_queen_with_capture_ai_claude() { + let board = test_board!( + White Queen on C4, + Black Bishop on F7 + ); + assert_move_list!( + QueenMoveGenerator::new(&board, None), + [ + // Horizontal moves + ply!(C4 - A4), + ply!(C4 - B4), + ply!(C4 - D4), + ply!(C4 - E4), + ply!(C4 - F4), + ply!(C4 - G4), + ply!(C4 - H4), + // Vertical moves + ply!(C4 - C1), + ply!(C4 - C2), + ply!(C4 - C3), + ply!(C4 - C5), + ply!(C4 - C6), + ply!(C4 - C7), + ply!(C4 - C8), + // Diagonal moves (A2-G8 diagonal) + ply!(C4 - A2), + ply!(C4 - B3), + ply!(C4 - D5), + ply!(C4 - E6), + ply!(C4 x F7), + // Diagonal moves (F1-A6 diagonal) + ply!(C4 - B5), + ply!(C4 - A6), + ply!(C4 - D3), + ply!(C4 - E2), + ply!(C4 - F1), + ] + ); + } + + #[test] + fn white_rook_blocked_by_friendly_piece_ai_claude() { + let board = test_board!(White Rook on D4, White Pawn on D6); + assert_move_list!( + RookMoveGenerator::new(&board, None), + [ + // Horizontal moves (unblocked) + ply!(D4 - A4), + ply!(D4 - B4), + ply!(D4 - C4), + ply!(D4 - E4), + ply!(D4 - F4), + ply!(D4 - G4), + ply!(D4 - H4), + // Vertical moves down (unblocked) + ply!(D4 - D1), + ply!(D4 - D2), + ply!(D4 - D3), + // Vertical moves up (blocked by friendly pawn on D6) + ply!(D4 - D5), + // Cannot move to D6 (occupied by friendly piece) + // Cannot move to D7 or D8 (blocked by friendly piece on D6) + ] + ); + } + + #[test] + fn white_bishop_blocked_by_friendly_pieces_ai_claude() { + let board = test_board!( + White Bishop on E4, + White Knight on C2, // Blocks one diagonal + White Pawn on G6 // Blocks another diagonal + ); + assert_move_list!( + BishopMoveGenerator::new(&board, None), + [ + // Diagonal towards H1 (unblocked) + ply!(E4 - F3), + ply!(E4 - G2), + ply!(E4 - H1), + // Diagonal towards A8 (blocked by pawn on G6) + ply!(E4 - F5), + // Cannot move to G6 (friendly pawn) + // Cannot move to H7 (blocked by friendly pawn) + // Diagonal towards H7 is blocked at G6 + // Diagonal towards A8 (unblocked on the other side) + ply!(E4 - D5), + ply!(E4 - C6), + ply!(E4 - B7), + ply!(E4 - A8), + // Diagonal towards D3 (blocked by knight on C2) + ply!(E4 - D3), + // Cannot move to C2 (friendly knight) + // Cannot move to B1 (blocked by friendly knight) + ] + ); + } + + #[test] + fn white_queen_multiple_friendly_blocks_ai_claude() { + let board = test_board!( + White Queen on D4, + White Pawn on D6, // Blocks vertical up + White Bishop on F4, // Blocks horizontal right + White Knight on F6 // Blocks diagonal + ); + assert_move_list!( + QueenMoveGenerator::new(&board, None), + [ + // Horizontal moves left (unblocked) + ply!(D4 - A4), + ply!(D4 - B4), + ply!(D4 - C4), + // Horizontal moves right (blocked by bishop on F4) + ply!(D4 - E4), + // Cannot move to F4 (friendly bishop) + // Cannot move to G4, H4 (blocked by friendly bishop) + + // Vertical moves down (unblocked) + ply!(D4 - D1), + ply!(D4 - D2), + ply!(D4 - D3), + // Vertical moves up (blocked by pawn on D6) + ply!(D4 - D5), + // Cannot move to D6 (friendly pawn) + // Cannot move to D7, D8 (blocked by friendly pawn) + + // Diagonal moves (some blocked, some not) + // Towards A1 + ply!(D4 - C3), + ply!(D4 - B2), + ply!(D4 - A1), + // Towards G1 + ply!(D4 - E3), + ply!(D4 - F2), + ply!(D4 - G1), + // Towards A7 + ply!(D4 - C5), + ply!(D4 - B6), + ply!(D4 - A7), + // Towards G7 (blocked by knight on F6) + ply!(D4 - E5), + // Cannot move to F6 (friendly knight) + // Cannot move to G7, H8 (blocked by friendly knight) + ] + ); + } + + #[test] + fn rook_ray_stops_at_first_piece_ai_claude() { + let board = test_board!( + White Rook on A1, + Black Pawn on A4, // First obstruction + Black Queen on A7 // Should be unreachable + ); + assert_move_list!( + RookMoveGenerator::new(&board, None), + [ + // Horizontal moves (unblocked) + ply!(A1 - B1), + ply!(A1 - C1), + ply!(A1 - D1), + ply!(A1 - E1), + ply!(A1 - F1), + ply!(A1 - G1), + ply!(A1 - H1), + // Vertical moves up (terminated at A4) + ply!(A1 - A2), + ply!(A1 - A3), + ply!(A1 x A4), + // Cannot reach A5, A6, A7, A8 due to pawn blocking at A4 + ] + ); + } + + #[test] + fn bishop_ray_terminated_by_enemy_piece() { + let board = test_board!( + White Bishop on C1, + Black Knight on F4, // Terminates one diagonal + Black Rook on H6, // Should be unreachable behind the knight + ); + assert_move_list!( + BishopMoveGenerator::new(&board, None), + [ + // Diagonal towards A3 + ply!(C1 - B2), + ply!(C1 - A3), + // Diagonal towards H6 (terminated at F4) + ply!(C1 - D2), + ply!(C1 - E3), + ply!(C1 x F4), + // Cannot reach G5, H6 due to knight blocking at F4 + ] + ); + } + + #[test] + fn queen_multiple_ray_terminations_ai_claude() { + let board = test_board!( + White Queen on D4, + Black Pawn on D7, // Terminates vertical ray + Black Bishop on A7, // Capturable along diagonal + Black Knight on G4, // Terminates horizontal ray + Black Rook on H4, // Capturable along horizontal ray + White Pawn on F6, // Terminates diagonal ray (friendly) + Black Queen on H8, + ); + assert_move_list!( + QueenMoveGenerator::new(&board, None), + [ + // Horizontal moves left (unblocked) + ply!(D4 - A4), + ply!(D4 - B4), + ply!(D4 - C4), + // Horizontal moves right (terminated at G4) + ply!(D4 - E4), + ply!(D4 - F4), + ply!(D4 x G4), + // Cannot reach H4 due to knight blocking + // Vertical moves down (unblocked) + ply!(D4 - D1), + ply!(D4 - D2), + ply!(D4 - D3), + // Vertical moves up (terminated at D7) + ply!(D4 - D5), + ply!(D4 - D6), + ply!(D4 x D7), + // Cannot reach D8 due to pawn blocking + // Diagonal moves towards A1 + ply!(D4 - C3), + ply!(D4 - B2), + ply!(D4 - A1), + // Diagonal moves towards G1 + ply!(D4 - E3), + ply!(D4 - F2), + ply!(D4 - G1), + // Diagonal moves towards A7 + ply!(D4 - C5), + ply!(D4 - B6), + ply!(D4 x A7), + // Diagonal moves towards H8 (terminated at F6 by friendly pawn) + ply!(D4 - E5), + // Cannot move to F6 (friendly pawn) + // Cannot reach G7, H8 due to friendly pawn blocking + ] + ); + } + + #[test] + fn rook_chain_of_pieces_ai_claude() { + let board = test_board!( + White Rook on A1, + Black Pawn on A3, // First enemy piece + White Knight on A5, // Friendly piece behind enemy + Black Queen on A7 // Enemy piece behind friendly + ); + assert_move_list!( + RookMoveGenerator::new(&board, None), + [ + // Horizontal moves (unblocked) + ply!(A1 - B1), + ply!(A1 - C1), + ply!(A1 - D1), + ply!(A1 - E1), + ply!(A1 - F1), + ply!(A1 - G1), + ply!(A1 - H1), + // Vertical moves (ray terminates at first piece) + ply!(A1 - A2), + ply!(A1 x A3), + // Cannot reach A4, A5, A6, A7, A8 - ray terminated at A3 + ] + ); + } + + #[test] + fn bishop_ray_termination_all_directions_ai_claude() { + let board = test_board!( + White Bishop on D4, + Black Pawn on B2, // Terminates towards A1 + Black Knight on F6, // Terminates towards H8 + Black Rook on B6, // Terminates towards A7 + Black Queen on F2 // Terminates towards G1 + ); + assert_move_list!( + BishopMoveGenerator::new(&board, None), + [ + // Diagonal towards A1 (terminated at B2) + ply!(D4 - C3), + ply!(D4 x B2), // Capture + // Cannot reach A1 + + // Diagonal towards H8 (terminated at F6) + ply!(D4 - E5), + ply!(D4 x F6), // Capture + // Cannot reach G7, H8 + + // Diagonal towards A7 (terminated at B6) + ply!(D4 - C5), + ply!(D4 x B6), // Capture + // Cannot reach A7 + + // Diagonal towards G1 (terminated at F2) + ply!(D4 - E3), + ply!(D4 x F2), + // Cannot reach G1 + ] + ); + } +}