[bitboard, board, core, moves] Implement SliderMoveGenerator

This generator produces moves for slider pieces: bishops, rooks, and queens. All
of these pieces behave identically, though with different sets of rays that
emanate from the origin square. Claude helped me significantly with the
implementation and unit testing. All the unit tests that took advantage of Claude
for implementation are marked as such with an _ai_claude suffix to the test name.

One unique aspect of this move generator that Claude suggested to me was to use
loop { } instead of a recursive call to next() when the internal iterators expire.
I may try to port this to the other move generators in the future.

To support this move generator, implement a Slider enum in core that represents
one of the three slider pieces.

Add Board::bishops(), Board::rooks() and Board::queens() to return BitBoards of
those pieces. These are analogous to the pawns() and knights() methods that return
their corresponding pieces.

Also in the board create, replace the separate sight method implementations with
a macro. These are all the same, but with a different sight method called under
the hood.

Finally, derive Clone and Debug for the bit_scanner types.
This commit is contained in:
Eryn Wells 2025-05-26 17:41:43 -07:00
parent 2c6a7828bc
commit f005d94fc2
7 changed files with 676 additions and 1 deletions

View file

@ -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)

View file

@ -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 {

View file

@ -94,6 +94,28 @@ impl Sight for Piece {
}
}
macro_rules! sight_method {
($name:ident) => {
pub fn $name(&self, square: Square, color: Option<Color>) -> 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,

View file

@ -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};

View file

@ -69,6 +69,36 @@ impl Shape {
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Slider {
Bishop,
Rook,
Queen,
}
impl From<Slider> for Shape {
fn from(value: Slider) -> Self {
match value {
Slider::Bishop => Shape::Bishop,
Slider::Rook => Shape::Rook,
Slider::Queen => Shape::Queen,
}
}
}
impl TryFrom<Shape> for Slider {
type Error = ();
fn try_from(value: Shape) -> Result<Self, Self::Error> {
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);

View file

@ -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;

View file

@ -0,0 +1,607 @@
// Eryn Wells <eryn@erynwells.me>
//! 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<Color>) -> Self {
Self(SliderMoveGenerator::new(board, Slider::$slider, color))
}
}
impl Iterator for $name {
type Item = $crate::GeneratedMove;
fn next(&mut self) -> Option<Self::Item> {
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<SliderInfo>,
next_sliders_index: usize,
current_slider: Option<SliderInfo>,
enemies: BitBoard,
friends: BitBoard,
}
impl SliderMoveGenerator {
fn new(board: &Board, slider: Slider, color: Option<Color>) -> 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<Self::Item> {
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::Item> {
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
]
);
}
}