From 284b3b68a0449af069c0c85661daf2b209f3b442 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 14 Jan 2024 10:19:20 -0800 Subject: [PATCH 001/423] [board] Add another test to the position flags --- board/src/position/flags.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/board/src/position/flags.rs b/board/src/position/flags.rs index 2aa389d..7efc41d 100644 --- a/board/src/position/flags.rs +++ b/board/src/position/flags.rs @@ -69,5 +69,11 @@ mod tests { assert!(!flags.player_has_right_to_castle(Color::White, BoardSide::Queen)); assert!(flags.player_has_right_to_castle(Color::Black, BoardSide::King)); assert!(flags.player_has_right_to_castle(Color::Black, BoardSide::Queen)); + + flags.set_player_has_right_to_castle_flag(Color::White, BoardSide::Queen); + assert!(flags.player_has_right_to_castle(Color::White, BoardSide::King)); + assert!(flags.player_has_right_to_castle(Color::White, BoardSide::Queen)); + assert!(flags.player_has_right_to_castle(Color::Black, BoardSide::King)); + assert!(flags.player_has_right_to_castle(Color::Black, BoardSide::Queen)); } } From 0d8653894a26ad58d14268a5a19ced635ec977c8 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 14 Jan 2024 10:23:35 -0800 Subject: [PATCH 002/423] An attempt at making unit structs for Color and piece Shape My idea was to implement traits on Piece that return sight lines, etc. This has turned out to be much more complex than I thought it would be. Ultimately, I don't think it's worth the effort. --- board/src/bitboard/bitboard.rs | 2 +- board/src/display.rs | 9 + board/src/lib.rs | 6 +- board/src/moves/bishop.rs | 33 +--- board/src/moves/classical.rs | 1 + board/src/moves/king.rs | 4 +- board/src/moves/mod.rs | 4 +- board/src/moves/move.rs | 95 ++++++++--- board/src/moves/move_generator.rs | 1 + board/src/moves/move_set.rs | 3 + board/src/moves/pawn.rs | 3 +- board/src/moves/queen.rs | 30 +--- board/src/moves/rook.rs | 26 +-- board/src/moves/sight.rs | 165 ++++++++++++++++++ board/src/piece.rs | 214 +++++++++--------------- board/src/position/diagram_formatter.rs | 8 +- board/src/position/flags.rs | 21 ++- board/src/position/pieces.rs | 32 ++-- board/src/position/position.rs | 162 ++++++++++++------ 19 files changed, 499 insertions(+), 320 deletions(-) create mode 100644 board/src/display.rs create mode 100644 board/src/moves/classical.rs create mode 100644 board/src/moves/sight.rs diff --git a/board/src/bitboard/bitboard.rs b/board/src/bitboard/bitboard.rs index 3a31a0e..23c2126 100644 --- a/board/src/bitboard/bitboard.rs +++ b/board/src/bitboard/bitboard.rs @@ -1,7 +1,7 @@ // Eryn Wells use super::library::{library, FILES, RANKS}; -use super::{LeadingBitScanner, TrailingBitScanner}; +use super::LeadingBitScanner; use crate::{square::Direction, Square}; use std::fmt; use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Not}; diff --git a/board/src/display.rs b/board/src/display.rs new file mode 100644 index 0000000..31757c6 --- /dev/null +++ b/board/src/display.rs @@ -0,0 +1,9 @@ +// Eryn Wells + +pub trait ASCIIDisplay { + fn fmt(&self, &mut f: fmt::Formatter) -> fmt::Result; +} + +pub trait UnicodeDisplay { + fn fmt(&self, &mut f: fmt::Formatter) -> fmt::Result; +} diff --git a/board/src/lib.rs b/board/src/lib.rs index 34f38df..bcc682a 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -1,13 +1,15 @@ // Eryn Wells mod bitboard; -mod moves; +//mod moves; #[macro_use] pub mod piece; #[macro_use] pub mod position; mod square; -pub use moves::Move; +pub(crate) use bitboard::BitBoard; +//pub use moves::Move; +pub use piece::Color; pub use position::Position; pub use square::{File, Rank, Square}; diff --git a/board/src/moves/bishop.rs b/board/src/moves/bishop.rs index 40c5bea..6aec9e4 100644 --- a/board/src/moves/bishop.rs +++ b/board/src/moves/bishop.rs @@ -1,10 +1,8 @@ // Eryn Wells -use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; +use super::{classical, move_generator_declaration, MoveGeneratorInternal, MoveSet, PieceSight}; use crate::{ - bitboard::BitBoard, piece::{Color, Piece, PlacedPiece}, - square::Direction, Move, Position, }; @@ -20,35 +18,14 @@ impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> { let color = piece.color(); let square = placed_piece.square(); - let blockers = position.occupied_squares(); - let empty_squares = !blockers; + let empty_squares = position.empty_squares(); let friendly_pieces = position.bitboard_for_color(color); let opposing_pieces = position.bitboard_for_color(color.other()); - let mut all_moves = BitBoard::empty(); + let sight = classical::BishopSight.sight(square, position); - macro_rules! update_moves_with_ray { - ($direction:ident, $occupied_squares:tt) => { - let ray = BitBoard::ray(square, Direction::$direction); - if let Some(first_occupied_square) = - BitBoard::$occupied_squares(&(ray & blockers)).next() - { - let remainder = BitBoard::ray(first_occupied_square, Direction::$direction); - let attack_ray = ray & !remainder; - all_moves |= attack_ray; - } else { - all_moves |= ray; - } - }; - } - - update_moves_with_ray!(NorthEast, occupied_squares_trailing); - update_moves_with_ray!(NorthWest, occupied_squares_trailing); - update_moves_with_ray!(SouthEast, occupied_squares); - update_moves_with_ray!(SouthWest, occupied_squares); - - let quiet_moves_bb = all_moves & (empty_squares | !friendly_pieces); - let capture_moves_bb = all_moves & opposing_pieces; + let quiet_moves_bb = sight & (empty_squares | !friendly_pieces); + let capture_moves_bb = sight & opposing_pieces; let map_to_move = |sq| Move::new(piece, square, sq); let quiet_moves = quiet_moves_bb.occupied_squares().map(map_to_move); diff --git a/board/src/moves/classical.rs b/board/src/moves/classical.rs new file mode 100644 index 0000000..8aa971d --- /dev/null +++ b/board/src/moves/classical.rs @@ -0,0 +1 @@ +// Eryn Wells diff --git a/board/src/moves/king.rs b/board/src/moves/king.rs index 07eda95..6cf0b77 100644 --- a/board/src/moves/king.rs +++ b/board/src/moves/king.rs @@ -3,7 +3,7 @@ //! Declares the KingMoveGenerator type. This struct is responsible for //! generating the possible moves for the king in the given position. -use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; +use super::{classical, move_generator_declaration, MoveGeneratorInternal, MoveSet, PieceSight}; use crate::{ bitboard::BitBoard, piece::{Color, Piece, PlacedPiece}, @@ -50,7 +50,7 @@ impl<'pos> MoveGeneratorInternal for KingMoveGenerator<'pos> { let empty_squares = position.empty_squares(); let opposing_pieces = position.bitboard_for_color(color.other()); - let all_moves = BitBoard::king_moves(square); + let all_moves = classical::KingSight.sight(square, position); let quiet_moves_bb = all_moves & empty_squares; let capture_moves_bb = all_moves & opposing_pieces; diff --git a/board/src/moves/mod.rs b/board/src/moves/mod.rs index bee7170..0d30d3f 100644 --- a/board/src/moves/mod.rs +++ b/board/src/moves/mod.rs @@ -1,6 +1,7 @@ // Eryn Wells mod bishop; +mod classical; mod king; mod knight; mod r#move; @@ -9,6 +10,7 @@ mod move_set; mod pawn; mod queen; mod rook; +pub(crate) mod sight; pub use move_generator::Moves; pub use r#move::Move; @@ -16,7 +18,6 @@ pub use r#move::Move; pub(self) use move_set::MoveSet; use crate::{ - bitboard::BitBoard, piece::{Color, Piece, PlacedPiece}, Position, Square, }; @@ -35,6 +36,7 @@ macro_rules! move_generator_declaration { move_generator_declaration!($name, getters); }; ($name:ident, struct) => { + #[derive(Clone, Debug, Eq, PartialEq)] pub(super) struct $name<'pos> { position: &'pos crate::Position, color: crate::piece::Color, diff --git a/board/src/moves/move.rs b/board/src/moves/move.rs index d952bec..0f88aaf 100644 --- a/board/src/moves/move.rs +++ b/board/src/moves/move.rs @@ -1,41 +1,86 @@ // Eryn Wells -use crate::{ - piece::{Piece, PlacedPiece, Shape}, - position::BoardSide, - Square, -}; +use crate::{piece::*, position::BoardSide, Square}; -#[derive(Debug, Clone, Eq, Hash, PartialEq)] -pub struct Move { - piece: Piece, - from: Square, - to: Square, - capturing: Option, - promoting_to: Option, +/// A move that transfers a piece from one square to another. +trait Move: Sized { + fn piece(&self) -> Piece; + fn from_square(&self) -> Square; + fn to_square(&self) -> Square; } -impl Move { - pub fn new(piece: Piece, from: Square, to: Square) -> Move { - Move { +/// A move that captures an opposing piece. +trait Capturing: Move { + type CaptureMove; + + fn capturing(self, capturing: CapturedS) -> Self::CaptureMove; + fn captured_piece(&self) -> Piece; +} + +/// A move that promotes a pawn to another piece. +trait Promoting: Move { + type PromotionMove; + + fn promoting_to(self, shape: PromotingS) -> Self::PromotionMove; + fn promoting_piece(&self) -> Piece; +} + +#[derive(Debug, Clone, Eq, Hash, PartialEq)] +pub struct SimpleMove { + piece: Piece, + from_square: Square, + to_square: Square, +} + +impl SimpleMove { + fn new(piece: Piece, from_square: Square, to_square: Square) -> Self { + SimpleMove { piece, - from, - to, - capturing: None, - promoting_to: None, + from_square, + to_square, } } - pub fn capturing(mut self, piece: PlacedPiece) -> Move { - self.capturing = Some(piece); - self + fn capturing( + self, + captured_piece: Piece, + ) -> Capture { + Capture { + r#move: self, + captured_piece, + } + } +} + +impl Move for Piece { + fn piece(&self) -> Piece { + self.piece } - pub fn promoting_to(mut self, shape: Shape) -> Move { - self.promoting_to = Some(shape); - self + fn from_square(&self) -> Square { + self.from_square } + fn to_square(&self) -> Square { + self.to_square + } +} + +pub struct Capture { + r#move: dyn Move, + captured_piece: Piece, +} + +pub struct Promotion { + r#move: dyn Move, + promoting_to_shape: PromS, +} + +pub struct CapturingMove { + capturing: PlacedPiece, +} + +impl Move { pub fn is_castle(&self) -> bool { let color = self.piece.color(); self.piece.shape() == Shape::King diff --git a/board/src/moves/move_generator.rs b/board/src/moves/move_generator.rs index b4962db..de61d3d 100644 --- a/board/src/moves/move_generator.rs +++ b/board/src/moves/move_generator.rs @@ -9,6 +9,7 @@ use super::{ use crate::piece::Color; use crate::Position; +#[derive(Clone, Eq, PartialEq)] pub struct Moves<'pos> { pawn_moves: PawnMoveGenerator<'pos>, knight_moves: KnightMoveGenerator<'pos>, diff --git a/board/src/moves/move_set.rs b/board/src/moves/move_set.rs index b3330b9..683907c 100644 --- a/board/src/moves/move_set.rs +++ b/board/src/moves/move_set.rs @@ -1,16 +1,19 @@ use crate::{bitboard::BitBoard, piece::PlacedPiece, Move}; +#[derive(Clone, Debug, Eq, PartialEq)] struct BitBoardSet { quiet: BitBoard, captures: BitBoard, } +#[derive(Clone, Debug, Eq, PartialEq)] struct MoveListSet { quiet: Vec, captures: Vec, } /// A set of moves for a piece on the board. +#[derive(Clone, Debug, Eq, PartialEq)] pub(super) struct MoveSet { piece: PlacedPiece, bitboards: BitBoardSet, diff --git a/board/src/moves/pawn.rs b/board/src/moves/pawn.rs index 118ef8b..a8f3313 100644 --- a/board/src/moves/pawn.rs +++ b/board/src/moves/pawn.rs @@ -12,7 +12,7 @@ enum MoveList { Captures = 2, } -#[derive(Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] struct MoveIterator(usize, usize); struct MoveGenerationParameters { @@ -23,6 +23,7 @@ struct MoveGenerationParameters { right_capture_shift: fn(BitBoard) -> BitBoard, } +#[derive(Clone, Eq, PartialEq)] pub(super) struct PawnMoveGenerator<'pos> { color: Color, position: &'pos Position, diff --git a/board/src/moves/queen.rs b/board/src/moves/queen.rs index 770a9ae..cdefb69 100644 --- a/board/src/moves/queen.rs +++ b/board/src/moves/queen.rs @@ -1,10 +1,8 @@ // Eryn Wells -use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; +use super::{classical, move_generator_declaration, MoveGeneratorInternal, MoveSet, PieceSight}; use crate::{ - bitboard::BitBoard, piece::{Color, Piece, PlacedPiece}, - square::Direction, Move, Position, }; @@ -25,31 +23,7 @@ impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> { let friendly_pieces = position.bitboard_for_color(color); let opposing_pieces = position.bitboard_for_color(color.other()); - let mut all_moves = BitBoard::empty(); - - macro_rules! update_moves_with_ray { - ($direction:ident, $occupied_squares:tt) => { - let ray = BitBoard::ray(square, Direction::$direction); - if let Some(first_occupied_square) = - BitBoard::$occupied_squares(&(ray & blockers)).next() - { - let remainder = BitBoard::ray(first_occupied_square, Direction::$direction); - let attack_ray = ray & !remainder; - all_moves |= attack_ray; - } else { - all_moves |= ray; - } - }; - } - - update_moves_with_ray!(NorthWest, occupied_squares_trailing); - update_moves_with_ray!(North, occupied_squares_trailing); - update_moves_with_ray!(NorthEast, occupied_squares_trailing); - update_moves_with_ray!(East, occupied_squares_trailing); - update_moves_with_ray!(SouthEast, occupied_squares); - update_moves_with_ray!(South, occupied_squares); - update_moves_with_ray!(SouthWest, occupied_squares); - update_moves_with_ray!(West, occupied_squares); + let mut all_moves = classical::QueenSight.sight(square, position); let quiet_moves_bb = all_moves & (empty_squares | !friendly_pieces); let capture_moves_bb = all_moves & opposing_pieces; diff --git a/board/src/moves/rook.rs b/board/src/moves/rook.rs index 82b8c76..783ed47 100644 --- a/board/src/moves/rook.rs +++ b/board/src/moves/rook.rs @@ -1,10 +1,8 @@ // Eryn Wells -use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; +use super::{classical, move_generator_declaration, MoveGeneratorInternal, MoveSet, PieceSight}; use crate::{ - bitboard::BitBoard, piece::{Color, Piece, PlacedPiece}, - square::Direction, Move, Position, }; @@ -25,27 +23,7 @@ impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> { let friendly_pieces = position.bitboard_for_color(color); let opposing_pieces = position.bitboard_for_color(color.other()); - let mut all_moves = BitBoard::empty(); - - macro_rules! update_moves_with_ray { - ($direction:ident, $occupied_squares:tt) => { - let ray = BitBoard::ray(square, Direction::$direction); - if let Some(first_occupied_square) = - BitBoard::$occupied_squares(&(ray & blockers)).next() - { - let remainder = BitBoard::ray(first_occupied_square, Direction::$direction); - let attack_ray = ray & !remainder; - all_moves |= attack_ray; - } else { - all_moves |= ray; - } - }; - } - - update_moves_with_ray!(North, occupied_squares_trailing); - update_moves_with_ray!(East, occupied_squares_trailing); - update_moves_with_ray!(South, occupied_squares); - update_moves_with_ray!(West, occupied_squares); + let all_moves = classical::RookSight.sight(square, position); let quiet_moves_bb = all_moves & (empty_squares | !friendly_pieces); let capture_moves_bb = all_moves & opposing_pieces; diff --git a/board/src/moves/sight.rs b/board/src/moves/sight.rs new file mode 100644 index 0000000..771a524 --- /dev/null +++ b/board/src/moves/sight.rs @@ -0,0 +1,165 @@ +// Eryn Wells + +use crate::{piece::*, square::Direction, BitBoard, Position, Square}; + +pub(crate) trait Sight { + fn sight_on_empty_board(self, square: Square) -> BitBoard; + fn sight(self, square: Square, position: &Position) -> BitBoard; +} + +impl Sight for Piece { + fn sight_on_empty_board(self, square: Square) -> BitBoard { + let pawn: BitBoard = square.into(); + pawn.shift_north_west_one() | pawn.shift_north_east_one() + } + + fn sight(self, square: Square, position: &Position) -> BitBoard { + let mut possible_squares = position.empty_squares() | position.opposing_pieces(); + if let Some(en_passant) = position.en_passant_square() { + possible_squares |= en_passant.into(); + } + + self.sight_on_empty_board(square) & possible_squares + } +} + +impl Sight for Piece { + fn sight_on_empty_board(self, square: Square) -> BitBoard { + let pawn: BitBoard = square.into(); + pawn.shift_south_west_one() | pawn.shift_south_east_one() + } + + fn sight(self, square: Square, position: &Position) -> BitBoard { + let mut possible_squares = position.empty_squares() | position.opposing_pieces(); + if let Some(en_passant) = position.en_passant_square() { + possible_squares |= en_passant.into(); + } + + self.sight_on_empty_board(square) & possible_squares + } +} + +impl Sight for Piece { + fn sight_on_empty_board(self, square: Square) -> BitBoard { + BitBoard::knight_moves(square) + } + + fn sight(self, square: Square, position: &Position) -> BitBoard { + self.sight_on_empty_board(square) & !position.friendly_pieces() + } +} + +impl Sight for Piece { + fn sight_on_empty_board(self, square: Square) -> BitBoard { + BitBoard::bishop_moves(square) + } + + fn sight(self, square: Square, position: &Position) -> BitBoard { + let mut sight = BitBoard::empty(); + + let blockers = position.occupied_squares(); + macro_rules! update_moves_with_ray { + ($direction:ident, $occupied_squares:tt) => { + let ray = BitBoard::ray(square, Direction::$direction); + if let Some(first_occupied_square) = + BitBoard::$occupied_squares(&(ray & blockers)).next() + { + let remainder = BitBoard::ray(first_occupied_square, Direction::$direction); + let attack_ray = ray & !remainder; + sight |= attack_ray; + } else { + sight |= ray; + } + }; + } + + update_moves_with_ray!(NorthEast, occupied_squares_trailing); + update_moves_with_ray!(NorthWest, occupied_squares_trailing); + update_moves_with_ray!(SouthEast, occupied_squares); + update_moves_with_ray!(SouthWest, occupied_squares); + + sight + } +} + +impl Sight for Piece { + fn sight_on_empty_board(self, square: Square) -> BitBoard { + BitBoard::rook_moves(square) + } + + fn sight(self, square: Square, position: &Position) -> BitBoard { + let mut sight = BitBoard::empty(); + + let blockers = position.occupied_squares(); + + macro_rules! update_moves_with_ray { + ($direction:ident, $occupied_squares:tt) => { + let ray = BitBoard::ray(square, Direction::$direction); + if let Some(first_occupied_square) = + BitBoard::$occupied_squares(&(ray & blockers)).next() + { + let remainder = BitBoard::ray(first_occupied_square, Direction::$direction); + let attack_ray = ray & !remainder; + sight |= attack_ray; + } else { + sight |= ray; + } + }; + } + + update_moves_with_ray!(North, occupied_squares_trailing); + update_moves_with_ray!(East, occupied_squares_trailing); + update_moves_with_ray!(South, occupied_squares); + update_moves_with_ray!(West, occupied_squares); + + sight + } +} + +impl Sight for Piece { + fn sight_on_empty_board(self, square: Square) -> BitBoard { + BitBoard::queen_moves(square) + } + + fn sight(self, square: Square, position: &Position) -> BitBoard { + let mut sight = BitBoard::empty(); + + let blockers = position.occupied_squares(); + + macro_rules! update_moves_with_ray { + ($direction:ident, $occupied_squares:tt) => { + let ray = BitBoard::ray(square, Direction::$direction); + if let Some(first_occupied_square) = + BitBoard::$occupied_squares(&(ray & blockers)).next() + { + let remainder = BitBoard::ray(first_occupied_square, Direction::$direction); + let attack_ray = ray & !remainder; + sight |= attack_ray; + } else { + sight |= ray; + } + }; + } + + update_moves_with_ray!(NorthWest, occupied_squares_trailing); + update_moves_with_ray!(North, occupied_squares_trailing); + update_moves_with_ray!(NorthEast, occupied_squares_trailing); + update_moves_with_ray!(East, occupied_squares_trailing); + update_moves_with_ray!(SouthEast, occupied_squares); + update_moves_with_ray!(South, occupied_squares); + update_moves_with_ray!(SouthWest, occupied_squares); + update_moves_with_ray!(West, occupied_squares); + + sight + } +} + +impl Sight for Piece { + fn sight_on_empty_board(self, square: Square) -> BitBoard { + BitBoard::king_moves(square) + } + + fn sight(self, square: Square, position: &Position) -> BitBoard { + self.sight_on_empty_board(square) & !position.friendly_pieces() + } +} diff --git a/board/src/piece.rs b/board/src/piece.rs index dc9b5da..0646f65 100644 --- a/board/src/piece.rs +++ b/board/src/piece.rs @@ -2,117 +2,80 @@ use crate::{bitboard::BitBoard, Square}; use std::fmt; -use std::slice::Iter; -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum Color { - White = 0, - Black = 1, +pub trait Color: Sized { + type Other: Color; } -impl Color { - pub fn iter() -> impl Iterator { - [Color::White, Color::Black].into_iter() - } - - pub fn other(&self) -> Color { - match self { - Color::White => Color::Black, - Color::Black => Color::White, +macro_rules! into_int_type { + ($name:ident, $int_type:ident, $value:expr) => { + impl Into<$int_type> for $name { + fn into(self) -> $int_type { + $value + } } - } + }; } -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum Shape { - Pawn = 0, - Knight = 1, - Bishop = 2, - Rook = 3, - Queen = 4, - King = 5, -} +macro_rules! color_struct { + ($name:ident, $index:expr, $other_color:ident) => { + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] + pub struct $name; -impl Shape { - pub fn iter() -> Iter<'static, Shape> { - const ALL_SHAPES: [Shape; 6] = [ - Shape::Pawn, - Shape::Knight, - Shape::Bishop, - Shape::Rook, - Shape::Queen, - Shape::King, - ]; - - ALL_SHAPES.iter() - } - - pub fn promotable() -> Iter<'static, Shape> { - const PROMOTABLE_SHAPES: [Shape; 4] = - [Shape::Queen, Shape::Rook, Shape::Bishop, Shape::Knight]; - - PROMOTABLE_SHAPES.iter() - } - - fn _ascii_representation(&self) -> char { - match self { - Shape::Pawn => 'p', - Shape::Knight => 'N', - Shape::Bishop => 'B', - Shape::Rook => 'R', - Shape::Queen => 'Q', - Shape::King => 'K', + impl Color for $name { + type Other = $other_color; } - } + + into_int_type!($name, u8, $index); + into_int_type!($name, usize, $index); + }; } +color_struct!(White, 0, Black); +color_struct!(Black, 1, White); + +macro_rules! shape_struct { + ($name:ident, $index:expr, $char:expr, $white_char:expr, $black_char:expr) => { + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] + pub struct $name; + + impl Shape for $name {} + + into_int_type!($name, u8, $index); + into_int_type!($name, usize, $index); + + impl Into for $name { + fn into(self) -> char { + $char + } + } + + impl fmt::Display for Piece<$name, White> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", $white_char) + } + } + + impl fmt::Display for Piece<$name, Black> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", $black_char) + } + } + }; +} + +pub trait Shape: Sized {} + +shape_struct!(Pawn, 0, 'P', '♙', '♟'); +shape_struct!(Knight, 1, 'N', '♘', '♞'); +shape_struct!(Bishop, 2, 'B', '♗', '♝'); +shape_struct!(Rook, 3, 'R', '♖', '♜'); +shape_struct!(Queen, 4, 'Q', '♕', '♛'); +shape_struct!(King, 5, 'K', '♔', '♚'); + #[derive(Debug, Eq, PartialEq)] pub struct TryFromError; -impl TryFrom for Shape { - type Error = TryFromError; - - fn try_from(value: char) -> Result { - match value { - 'p' => Ok(Shape::Pawn), - 'N' => Ok(Shape::Knight), - 'B' => Ok(Shape::Bishop), - 'R' => Ok(Shape::Rook), - 'Q' => Ok(Shape::Queen), - 'K' => Ok(Shape::King), - _ => Err(TryFromError), - } - } -} - -impl TryFrom<&str> for Shape { - type Error = TryFromError; - - fn try_from(value: &str) -> Result { - let first_char = value.chars().nth(0).ok_or(TryFromError)?; - Shape::try_from(first_char) - } -} - -impl Into for &Shape { - fn into(self) -> char { - self._ascii_representation() - } -} - -impl Into for Shape { - fn into(self) -> char { - self._ascii_representation() - } -} - -impl fmt::Display for Shape { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let self_char: char = self.into(); - write!(f, "{}", self_char) - } -} - #[derive(Debug, Eq, PartialEq)] pub enum PiecePlacementError { ExistsOnSquare, @@ -129,24 +92,28 @@ macro_rules! piece { } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub struct Piece { - color: Color, - shape: Shape, +pub struct Piece { + color: C, + shape: S, } macro_rules! piece_constructor { - ($func_name:ident, $type:tt) => { - pub fn $func_name(color: Color) -> Piece { + ($func_name:ident, $shape:tt) => { + pub fn $func_name(color: C) -> Piece { Piece { color, - shape: Shape::$type, + shape: $shape, } } }; } -impl Piece { - pub fn new(color: Color, shape: Shape) -> Piece { +impl Piece +where + C: Color, + S: Shape, +{ + pub fn new(color: C, shape: S) -> Piece { Piece { color, shape } } @@ -157,49 +124,28 @@ impl Piece { piece_constructor!(queen, Queen); piece_constructor!(king, King); - pub fn color(&self) -> Color { + pub fn color(&self) -> C { self.color } - pub fn shape(&self) -> Shape { + pub fn shape(&self) -> S { self.shape } } -impl fmt::Display for Piece { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let char = match (self.color, self.shape) { - (Color::White, Shape::Pawn) => '♟', - (Color::White, Shape::Knight) => '♞', - (Color::White, Shape::Bishop) => '♝', - (Color::White, Shape::Rook) => '♜', - (Color::White, Shape::Queen) => '♛', - (Color::White, Shape::King) => '♚', - (Color::Black, Shape::Pawn) => '♙', - (Color::Black, Shape::Knight) => '♘', - (Color::Black, Shape::Bishop) => '♗', - (Color::Black, Shape::Rook) => '♖', - (Color::Black, Shape::Queen) => '♕', - (Color::Black, Shape::King) => '♔', - }; - - write!(f, "{}", char) - } -} - #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub struct PlacedPiece { - piece: Piece, +pub struct PlacedPiece { + piece: Piece, square: Square, } -impl PlacedPiece { - pub const fn new(piece: Piece, square: Square) -> PlacedPiece { +impl PlacedPiece { + pub const fn new(piece: Piece, square: Square) -> PlacedPiece { PlacedPiece { piece, square } } #[inline] - pub fn piece(&self) -> Piece { + pub fn piece(&self) -> Piece { self.piece } diff --git a/board/src/position/diagram_formatter.rs b/board/src/position/diagram_formatter.rs index 5187ace..731830a 100644 --- a/board/src/position/diagram_formatter.rs +++ b/board/src/position/diagram_formatter.rs @@ -3,15 +3,15 @@ use crate::{File, Position, Rank, Square}; use std::{fmt, fmt::Write}; -pub struct DiagramFormatter<'a>(&'a Position); +pub struct DiagramFormatter<'a, PosC>(&'a Position); -impl<'a> DiagramFormatter<'a> { - pub fn new(position: &'a Position) -> DiagramFormatter { +impl<'a, PosC> DiagramFormatter<'a, PosC> { + pub fn new(position: &'a Position) -> DiagramFormatter { DiagramFormatter(position) } } -impl<'a> fmt::Display for DiagramFormatter<'a> { +impl<'a, PosC> fmt::Display for DiagramFormatter<'a, PosC> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut output = String::new(); diff --git a/board/src/position/flags.rs b/board/src/position/flags.rs index 2aa389d..f8b05ec 100644 --- a/board/src/position/flags.rs +++ b/board/src/position/flags.rs @@ -8,19 +8,32 @@ pub(super) struct Flags(u8); impl Flags { #[inline] - pub(super) fn player_has_right_to_castle_flag_offset(color: Color, side: BoardSide) -> u8 { + pub(super) fn player_has_right_to_castle_flag_offset( + color: C, + side: BoardSide, + ) -> u8 { ((color as u8) << 1) + side as u8 } - pub(super) fn player_has_right_to_castle(&self, color: Color, side: BoardSide) -> bool { + pub(super) fn player_has_right_to_castle(&self, color: C, side: BoardSide) -> bool { (self.0 & (1 << Self::player_has_right_to_castle_flag_offset(color, side))) != 0 } - pub(super) fn set_player_has_right_to_castle_flag(&mut self, color: Color, side: BoardSide) { + #[cfg(test)] + pub(super) fn set_player_has_right_to_castle_flag( + &mut self, + color: C, + side: BoardSide, + ) { self.0 |= 1 << Self::player_has_right_to_castle_flag_offset(color, side); } - pub(super) fn clear_player_has_right_to_castle_flag(&mut self, color: Color, side: BoardSide) { + #[cfg(test)] + pub(super) fn clear_player_has_right_to_castle_flag( + &mut self, + color: C, + side: BoardSide, + ) { self.0 &= !(1 << Self::player_has_right_to_castle_flag_offset(color, side)); } } diff --git a/board/src/position/pieces.rs b/board/src/position/pieces.rs index e3cb87e..15f6695 100644 --- a/board/src/position/pieces.rs +++ b/board/src/position/pieces.rs @@ -2,33 +2,37 @@ use super::Position; use crate::bitboard::BitBoard; -use crate::piece::{Color, Piece, PlacedPiece, Shape}; +use crate::piece::*; use crate::Square; -pub struct Pieces<'a> { - color: Color, - position: &'a Position, +pub struct Pieces<'a, PosC, C: Color> { + color: C, + position: &'a Position, - current_shape: Option, + current_shape: Option, - shape_iterator: Box>, + shape_iterator: std::array::IntoIter, square_iterator: Option>>, } -impl<'a> Pieces<'a> { - pub(crate) fn new(position: &Position, color: Color) -> Pieces { +impl<'a, PosC, C> Pieces<'a, PosC, C> { + pub(crate) fn new(position: &Position, color: C) -> Pieces { Pieces { color, position, current_shape: None, - shape_iterator: Box::new(Shape::iter()), + shape_iterator: [Pawn, Knight, Bishop, Rook, Queen, King].into_iter(), square_iterator: None, } } } -impl<'a> Iterator for Pieces<'a> { - type Item = PlacedPiece; +impl<'a, PosC, C, S> Iterator for Pieces<'a, PosC, C> +where + C: Color, + S: Shape, +{ + type Item = PlacedPiece; fn next(&mut self) -> Option { if let Some(square_iterator) = &mut self.square_iterator { @@ -80,7 +84,11 @@ mod tests { use crate::Square; use std::collections::HashSet; - fn place_piece_in_position(pos: &mut Position, piece: Piece, sq: Square) { + fn place_piece_in_position( + pos: &mut Position, + piece: Piece, + sq: Square, + ) { pos.place_piece(piece, sq) .expect("Unable to place {piece:?} queen on {sq}"); } diff --git a/board/src/position/position.rs b/board/src/position/position.rs index 3ff2db6..73de794 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -2,9 +2,9 @@ use super::{flags::Flags, Pieces}; use crate::{ - bitboard::BitBoard, - moves::Moves, + //moves::Moves, piece::{Color, Piece, PiecePlacementError, PlacedPiece, Shape}, + BitBoard, Square, }; use std::fmt; @@ -28,16 +28,9 @@ macro_rules! position { } #[derive(Clone, Eq, Hash, PartialEq)] -pub struct Position { - color_to_move: Color, +pub struct Position { + color_to_move: ColorToMove, - /// Position flags indicating various bits of game state. The flags are as - /// follows: - /// - /// 0. white can castle king-side - /// 1. white can castle queen-side - /// 2. black can castle king-side - /// 3. black can castle queen-side flags: Flags, /// Composite bitboards for all the pieces of a particular color. @@ -45,12 +38,14 @@ pub struct Position { /// Bitboards representing positions of particular piece types per color. pieces_per_type: [[BitBoard; 6]; 2], + + en_passant_square: Option, } -impl Position { - pub fn empty() -> Position { +impl Position { + pub fn empty() -> Position { Position { - color_to_move: Color::White, + color_to_move: crate::piece::White, flags: Default::default(), pieces_per_color: [BitBoard::empty(); 2], pieces_per_type: [ @@ -71,11 +66,12 @@ impl Position { BitBoard::empty(), ], ], + en_passant_square: None, } } /// Return a starting position. - pub fn starting() -> Position { + pub fn starting() -> Position { let white_pieces = [ BitBoard::new(0x00FF000000000000), BitBoard::new(0x4200000000000000), @@ -95,13 +91,14 @@ impl Position { ]; Position { - color_to_move: Color::White, + color_to_move: crate::piece::White, flags: Default::default(), pieces_per_color: [ white_pieces.iter().fold(BitBoard::empty(), |a, b| a | *b), black_pieces.iter().fold(BitBoard::empty(), |a, b| a | *b), ], pieces_per_type: [white_pieces, black_pieces], + en_passant_square: None, } } @@ -118,20 +115,15 @@ impl Position { /// 2. The king must not be in check /// 3. In the course of castling on that side, the king must not pass /// through a square that an enemy piece can see - pub(crate) fn player_has_right_to_castle(&self, color: Color, side: BoardSide) -> bool { + pub(crate) fn player_has_right_to_castle(&self, color: C, side: BoardSide) -> bool { self.flags.player_has_right_to_castle(color, side) } - fn set_player_has_right_to_castle_flag(&mut self, color: Color, side: BoardSide) { - self.flags.set_player_has_right_to_castle_flag(color, side); - } - - fn clear_player_has_right_to_castle_flag(&mut self, color: Color, side: BoardSide) { - self.flags - .clear_player_has_right_to_castle_flag(color, side); - } - - pub fn place_piece(&mut self, piece: Piece, square: Square) -> Result<(), PiecePlacementError> { + pub fn place_piece( + &mut self, + piece: Piece, + square: Square, + ) -> Result<(), PiecePlacementError> { let type_bb = self.bitboard_for_piece_mut(piece); if type_bb.has_piece_at(square) { @@ -146,40 +138,27 @@ impl Position { Ok(()) } - pub fn move_generator(&self, color: Color) -> Moves { - Moves::new(self, color) - } + // pub fn move_generator(&self, color: C) -> Moves { + // Moves::new(self, color) + // } - /// Return a BitBoard representing the set of squares containing a piece. - #[inline] - pub(crate) fn occupied_squares(&self) -> BitBoard { - self.pieces_per_color[Color::White as usize] | self.pieces_per_color[Color::Black as usize] - } - - /// Return a BitBoard representing the set of squares containing a piece. - /// This set is the inverse of `occupied_squares`. - #[inline] - pub(crate) fn empty_squares(&self) -> BitBoard { - !self.occupied_squares() - } - - pub(crate) fn bitboard_for_piece(&self, piece: Piece) -> BitBoard { + pub(crate) fn bitboard_for_piece(&self, piece: Piece) -> BitBoard { self.pieces_per_type[piece.color() as usize][piece.shape() as usize] } - fn bitboard_for_piece_mut(&mut self, piece: Piece) -> &mut BitBoard { + fn bitboard_for_piece_mut(&mut self, piece: Piece) -> &mut BitBoard { &mut self.pieces_per_type[piece.color() as usize][piece.shape() as usize] } - pub(crate) fn bitboard_for_color(&self, color: Color) -> BitBoard { + pub(crate) fn bitboard_for_color(&self, color: C) -> BitBoard { self.pieces_per_color[color as usize] } - fn bitboard_for_color_mut(&mut self, color: Color) -> &mut BitBoard { + fn bitboard_for_color_mut(&mut self, color: C) -> &mut BitBoard { &mut self.pieces_per_color[color as usize] } - pub fn piece_on_square(&self, sq: Square) -> Option { + pub fn piece_on_square(&self, sq: Square) -> Option> { for color in Color::iter() { for shape in Shape::iter() { let piece = Piece::new(color, *shape); @@ -193,19 +172,76 @@ impl Position { None } - pub fn pieces(&self, color: Color) -> Pieces { + pub fn pieces(&self, color: C) -> Pieces { Pieces::new(&self, color) } + + pub fn king(&self, color: C) -> PlacedPiece { + let king = Piece::king(color); + let bitboard = self.bitboard_for_piece(king); + let square = bitboard.occupied_squares().next().unwrap(); + PlacedPiece::new(king, square) + } + + pub fn is_king_in_check(&self) -> bool { + false + } } -impl Default for Position { +impl Position { + /// Return a BitBoard representing the set of squares containing a piece. + #[inline] + pub(crate) fn occupied_squares(&self) -> BitBoard { + self.pieces_per_color[Color::White as usize] | self.pieces_per_color[Color::Black as usize] + } + + /// A BitBoard representing the set of empty squares. This set is the + /// inverse of `occupied_squares`. + #[inline] + pub(crate) fn empty_squares(&self) -> BitBoard { + !self.occupied_squares() + } + + /// A BitBoard representing squares occupied by the current player's pieces. + pub(crate) fn friendly_pieces(&self) -> BitBoard { + self.bitboard_for_color(self.color_to_move) + } + + /// A BitBoard representing squares occupied by the opposing player's pieces. + pub(crate) fn opposing_pieces(&self) -> BitBoard { + self.bitboard_for_color(self.color_to_move.other()) + } + + /// If the previous move create an en passant elibile square, return `Some`. + /// Otherwise, return `None`. + pub(crate) fn en_passant_square(&self) -> Option { + self.en_passant_square + } +} + +impl Position { + fn sight_of_pieces_of_color(&self, color: C) -> BitBoard { + self.pieces(color) + .map(|placed_piece| { + self.sight_of_piece(placed_piece) + .sight(placed_piece.square(), self) + }) + .fold(BitBoard::empty(), std::ops::BitOr::bitor) + } + + fn sight_of_piece(&self, placed_piece: PlacedPiece) -> BitBoard { + placed_piece.piece().sight() + } +} + +impl Default for Position { fn default() -> Self { Self::empty() } } -impl FromIterator for Position { - fn from_iter>(iter: T) -> Self { +impl FromIterator> for Position { + fn from_iter>>(iter: T) -> Self { let mut position = Position::empty(); for placed_piece in iter { _ = position.place_piece(placed_piece.piece(), placed_piece.square()); @@ -215,7 +251,7 @@ impl FromIterator for Position { } } -impl fmt::Debug for Position { +impl fmt::Debug for Position { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut output = String::new(); @@ -245,7 +281,7 @@ impl fmt::Debug for Position { #[cfg(test)] mod tests { use super::*; - use crate::piece::Shape; + use crate::{piece::Shape, position::DiagramFormatter}; #[test] fn place_piece() { @@ -273,6 +309,24 @@ mod tests { } #[test] + fn king_is_not_in_check() { + let position = position![ + White King on E4, + Black Rook on D8, + ]; + println!("{}", DiagramFormatter::new(&position)); + assert!(!position.is_king_in_check()); + } + + #[test] + fn king_is_in_check() { + let position = position![ + White King on E4, + Black Rook on E8, + ]; + println!("{}", DiagramFormatter::new(&position)); + + assert!(position.is_king_in_check()); } } From e56d25681247e1cd487a4fdff3cc4ff0e5d4f9c0 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 14 Jan 2024 10:26:36 -0800 Subject: [PATCH 003/423] [board] Fix two build errors --- board/src/position/flags.rs | 2 +- board/src/position/position.rs | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/board/src/position/flags.rs b/board/src/position/flags.rs index 7efc41d..43680cf 100644 --- a/board/src/position/flags.rs +++ b/board/src/position/flags.rs @@ -34,7 +34,7 @@ impl Default for Flags { #[cfg(test)] mod tests { use super::*; - use crate::Color; + use crate::piece::Color; #[test] fn castle_flags() { diff --git a/board/src/position/position.rs b/board/src/position/position.rs index 3ff2db6..d777791 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -271,8 +271,4 @@ mod tests { .place_piece(piece, square) .expect_err("Placed white queen on e4 a second time?!"); } - - #[test] - - } } From 64b47a8d70fa5820efb94be0c14c5b00d8bba084 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 14 Jan 2024 10:27:04 -0800 Subject: [PATCH 004/423] [board] Remove unused PlacedPiece::bitboard() --- board/src/piece.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/board/src/piece.rs b/board/src/piece.rs index dc9b5da..1732864 100644 --- a/board/src/piece.rs +++ b/board/src/piece.rs @@ -207,11 +207,6 @@ impl PlacedPiece { pub fn square(&self) -> Square { self.square } - - #[inline] - pub(crate) fn bitboard(&self) -> BitBoard { - self.square.into() - } } #[cfg(test)] From ddea2c2d638ef94ab0e5436253a843d3de8cf8e0 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 14 Jan 2024 10:51:40 -0800 Subject: [PATCH 005/423] [board] Declare three new Display-like traits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ASCIIDisplay → format a type using ASCII only characters - UnicodeDisplay → format a type using any Unicode characters - FENDisplay → format a type for inclusion in a FEN string --- board/src/display.rs | 15 ++++++++++ board/src/lib.rs | 1 + board/src/piece.rs | 65 ++++++++++++++++++++++++++++++++------------ 3 files changed, 64 insertions(+), 17 deletions(-) create mode 100644 board/src/display.rs diff --git a/board/src/display.rs b/board/src/display.rs new file mode 100644 index 0000000..37621a8 --- /dev/null +++ b/board/src/display.rs @@ -0,0 +1,15 @@ +// Eryn Wells + +use std::fmt; + +pub(crate) trait ASCIIDisplay { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result; +} + +pub(crate) trait UnicodeDisplay { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result; +} + +pub(crate) trait FENDisplay { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result; +} diff --git a/board/src/lib.rs b/board/src/lib.rs index 34f38df..76b0de3 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -1,6 +1,7 @@ // Eryn Wells mod bitboard; +mod display; mod moves; #[macro_use] pub mod piece; diff --git a/board/src/piece.rs b/board/src/piece.rs index 1732864..7a20d19 100644 --- a/board/src/piece.rs +++ b/board/src/piece.rs @@ -1,6 +1,9 @@ // Eryn Wells -use crate::{bitboard::BitBoard, Square}; +use crate::{ + display::{ASCIIDisplay, FENDisplay, UnicodeDisplay}, + Square, +}; use std::fmt; use std::slice::Iter; @@ -56,7 +59,7 @@ impl Shape { fn _ascii_representation(&self) -> char { match self { - Shape::Pawn => 'p', + Shape::Pawn => 'P', Shape::Knight => 'N', Shape::Bishop => 'B', Shape::Rook => 'R', @@ -168,22 +171,50 @@ impl Piece { impl fmt::Display for Piece { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let char = match (self.color, self.shape) { - (Color::White, Shape::Pawn) => '♟', - (Color::White, Shape::Knight) => '♞', - (Color::White, Shape::Bishop) => '♝', - (Color::White, Shape::Rook) => '♜', - (Color::White, Shape::Queen) => '♛', - (Color::White, Shape::King) => '♚', - (Color::Black, Shape::Pawn) => '♙', - (Color::Black, Shape::Knight) => '♘', - (Color::Black, Shape::Bishop) => '♗', - (Color::Black, Shape::Rook) => '♖', - (Color::Black, Shape::Queen) => '♕', - (Color::Black, Shape::King) => '♔', - }; + UnicodeDisplay::fmt(self, f) + } +} - write!(f, "{}", char) +impl ASCIIDisplay for Piece { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", Into::::into(self.shape())) + } +} + +impl UnicodeDisplay for Piece { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}", + match (self.color, self.shape) { + (Color::White, Shape::Pawn) => '♟', + (Color::White, Shape::Knight) => '♞', + (Color::White, Shape::Bishop) => '♝', + (Color::White, Shape::Rook) => '♜', + (Color::White, Shape::Queen) => '♛', + (Color::White, Shape::King) => '♚', + (Color::Black, Shape::Pawn) => '♙', + (Color::Black, Shape::Knight) => '♘', + (Color::Black, Shape::Bishop) => '♗', + (Color::Black, Shape::Rook) => '♖', + (Color::Black, Shape::Queen) => '♕', + (Color::Black, Shape::King) => '♔', + } + ) + } +} + +impl FENDisplay for Piece { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let ascii = self.shape()._ascii_representation(); + write!( + f, + "{}", + match self.color { + Color::White => ascii.to_ascii_uppercase(), + Color::Black => ascii.to_ascii_lowercase(), + } + ) } } From 5961b1bcd5b8a766016b0e3392ec294c6e1e6664 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 14 Jan 2024 10:57:22 -0800 Subject: [PATCH 006/423] [board] Export BitBoard directly off of the crate for internal use Update all the imports to import from the crate directly. --- board/src/bitboard/mod.rs | 4 ++-- board/src/lib.rs | 2 ++ board/src/moves/bishop.rs | 6 ++---- board/src/moves/king.rs | 3 +-- board/src/moves/knight.rs | 3 +-- board/src/moves/mod.rs | 9 ++++----- board/src/moves/move_set.rs | 2 +- board/src/moves/pawn.rs | 3 +-- board/src/moves/queen.rs | 6 ++---- board/src/moves/rook.rs | 6 ++---- board/src/position/pieces.rs | 2 +- board/src/position/position.rs | 3 +-- 12 files changed, 20 insertions(+), 29 deletions(-) diff --git a/board/src/bitboard/mod.rs b/board/src/bitboard/mod.rs index f462e3b..6093fa8 100644 --- a/board/src/bitboard/mod.rs +++ b/board/src/bitboard/mod.rs @@ -3,5 +3,5 @@ mod bitboard; mod library; mod shifts; -pub(crate) use self::bit_scanner::{LeadingBitScanner, TrailingBitScanner}; -pub(crate) use self::bitboard::BitBoard; +pub(crate) use bit_scanner::{LeadingBitScanner, TrailingBitScanner}; +pub(crate) use bitboard::BitBoard; diff --git a/board/src/lib.rs b/board/src/lib.rs index 76b0de3..354c4e1 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -12,3 +12,5 @@ mod square; pub use moves::Move; pub use position::Position; pub use square::{File, Rank, Square}; + +pub(crate) use bitboard::BitBoard; diff --git a/board/src/moves/bishop.rs b/board/src/moves/bishop.rs index 40c5bea..6db8668 100644 --- a/board/src/moves/bishop.rs +++ b/board/src/moves/bishop.rs @@ -2,10 +2,9 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::{ - bitboard::BitBoard, piece::{Color, Piece, PlacedPiece}, square::Direction, - Move, Position, + BitBoard, Move, Position, }; move_generator_declaration!(ClassicalMoveGenerator); @@ -64,10 +63,9 @@ impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> { mod tests { use super::*; use crate::{ - bitboard::BitBoard, piece::{Color, Piece}, position::DiagramFormatter, - Position, Square, + BitBoard, Position, Square, }; #[test] diff --git a/board/src/moves/king.rs b/board/src/moves/king.rs index 07eda95..f3ae29a 100644 --- a/board/src/moves/king.rs +++ b/board/src/moves/king.rs @@ -5,10 +5,9 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::{ - bitboard::BitBoard, piece::{Color, Piece, PlacedPiece}, position::BoardSide, - Move, Position, + BitBoard, Move, Position, }; move_generator_declaration!(KingMoveGenerator, struct); diff --git a/board/src/moves/knight.rs b/board/src/moves/knight.rs index 11f6134..cf50641 100644 --- a/board/src/moves/knight.rs +++ b/board/src/moves/knight.rs @@ -2,9 +2,8 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::{ - bitboard::BitBoard, piece::{Color, Piece, PlacedPiece}, - Move, Position, + BitBoard, Move, Position, }; move_generator_declaration!(KnightMoveGenerator); diff --git a/board/src/moves/mod.rs b/board/src/moves/mod.rs index bee7170..d395991 100644 --- a/board/src/moves/mod.rs +++ b/board/src/moves/mod.rs @@ -16,9 +16,8 @@ pub use r#move::Move; pub(self) use move_set::MoveSet; use crate::{ - bitboard::BitBoard, piece::{Color, Piece, PlacedPiece}, - Position, Square, + BitBoard, Position, Square, }; use std::collections::BTreeMap; @@ -43,7 +42,7 @@ macro_rules! move_generator_declaration { }; ($name:ident, new) => { impl<'pos> $name<'pos> { - pub(super) fn new(position: &Position, color: crate::piece::Color) -> $name { + pub(super) fn new(position: &Position, color: $crate::piece::Color) -> $name { $name { position, color, @@ -58,10 +57,10 @@ macro_rules! move_generator_declaration { self.move_sets.values().map(|set| set.moves()).flatten() } - fn bitboard(&self) -> crate::bitboard::BitBoard { + fn bitboard(&self) -> $crate::BitBoard { self.move_sets .values() - .fold(crate::bitboard::BitBoard::empty(), |partial, mv_set| { + .fold($crate::BitBoard::empty(), |partial, mv_set| { partial | mv_set.bitboard() }) } diff --git a/board/src/moves/move_set.rs b/board/src/moves/move_set.rs index b3330b9..f03d177 100644 --- a/board/src/moves/move_set.rs +++ b/board/src/moves/move_set.rs @@ -1,4 +1,4 @@ -use crate::{bitboard::BitBoard, piece::PlacedPiece, Move}; +use crate::{piece::PlacedPiece, BitBoard, Move}; struct BitBoardSet { quiet: BitBoard, diff --git a/board/src/moves/pawn.rs b/board/src/moves/pawn.rs index 118ef8b..9778909 100644 --- a/board/src/moves/pawn.rs +++ b/board/src/moves/pawn.rs @@ -1,9 +1,8 @@ // Eryn Wells use crate::{ - bitboard::BitBoard, piece::{Color, Piece, Shape}, - Move, Position, + BitBoard, Move, Position, }; enum MoveList { diff --git a/board/src/moves/queen.rs b/board/src/moves/queen.rs index 770a9ae..1063653 100644 --- a/board/src/moves/queen.rs +++ b/board/src/moves/queen.rs @@ -2,10 +2,9 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::{ - bitboard::BitBoard, piece::{Color, Piece, PlacedPiece}, square::Direction, - Move, Position, + BitBoard, Move, Position, }; move_generator_declaration!(ClassicalMoveGenerator); @@ -68,10 +67,9 @@ impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> { mod tests { use super::*; use crate::{ - bitboard::BitBoard, piece::{Color, Piece}, position::DiagramFormatter, - Position, Square, + BitBoard, Position, Square, }; #[test] diff --git a/board/src/moves/rook.rs b/board/src/moves/rook.rs index 82b8c76..99f1823 100644 --- a/board/src/moves/rook.rs +++ b/board/src/moves/rook.rs @@ -2,10 +2,9 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::{ - bitboard::BitBoard, piece::{Color, Piece, PlacedPiece}, square::Direction, - Move, Position, + BitBoard, Move, Position, }; move_generator_declaration!(ClassicalMoveGenerator); @@ -64,10 +63,9 @@ impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> { mod tests { use super::*; use crate::{ - bitboard::BitBoard, piece::{Color, Piece}, position::DiagramFormatter, - Position, Square, + BitBoard, Position, Square, }; #[test] diff --git a/board/src/position/pieces.rs b/board/src/position/pieces.rs index e3cb87e..307673e 100644 --- a/board/src/position/pieces.rs +++ b/board/src/position/pieces.rs @@ -1,8 +1,8 @@ // Eryn Wells use super::Position; -use crate::bitboard::BitBoard; use crate::piece::{Color, Piece, PlacedPiece, Shape}; +use crate::BitBoard; use crate::Square; pub struct Pieces<'a> { diff --git a/board/src/position/position.rs b/board/src/position/position.rs index d777791..3a98c1e 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -2,10 +2,9 @@ use super::{flags::Flags, Pieces}; use crate::{ - bitboard::BitBoard, moves::Moves, piece::{Color, Piece, PiecePlacementError, PlacedPiece, Shape}, - Square, + BitBoard, Square, }; use std::fmt; use std::fmt::Write; From 3b40aacd5233b93a5f3c2fc959adb8b2f1025aad Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 14 Jan 2024 10:57:43 -0800 Subject: [PATCH 007/423] Quick text file of all the Unicode chess characters --- chess_pieces.txt | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 chess_pieces.txt diff --git a/chess_pieces.txt b/chess_pieces.txt new file mode 100644 index 0000000..0c641ce --- /dev/null +++ b/chess_pieces.txt @@ -0,0 +1,47 @@ +♔ +WHITE CHESS KING +Unicode: U+2654, UTF-8: E2 99 94 + +♕ +WHITE CHESS QUEEN +Unicode: U+2655, UTF-8: E2 99 95 + +♖ +WHITE CHESS ROOK +Unicode: U+2656, UTF-8: E2 99 96 + +♗ +WHITE CHESS BISHOP +Unicode: U+2657, UTF-8: E2 99 97 + +♘ +WHITE CHESS KNIGHT +Unicode: U+2658, UTF-8: E2 99 98 + +♙ +WHITE CHESS PAWN +Unicode: U+2659, UTF-8: E2 99 99 + +♚ +BLACK CHESS KING +Unicode: U+265A, UTF-8: E2 99 9A + +♛ +BLACK CHESS QUEEN +Unicode: U+265B, UTF-8: E2 99 9B + +♜ +BLACK CHESS ROOK +Unicode: U+265C, UTF-8: E2 99 9C + +♝ +BLACK CHESS BISHOP +Unicode: U+265D, UTF-8: E2 99 9D + +♞ +BLACK CHESS KNIGHT +Unicode: U+265E, UTF-8: E2 99 9E + +♟ +chess pawn +Unicode: U+265F, UTF-8: E2 99 9F \ No newline at end of file From 3ecc263701a48eaf1e67db1f49d28159f9e9f23d Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 15 Jan 2024 16:03:06 -0800 Subject: [PATCH 008/423] [board] Implement piece sight algorithms Add a new Sight trait, implemented by PlacedPiece. The implementation of this trait produces a BitBoard representing the squares visible to the placed piece. --- board/src/lib.rs | 1 + board/src/piece.rs | 10 +++ board/src/position/position.rs | 18 ++++ board/src/sight.rs | 155 +++++++++++++++++++++++++++++++++ 4 files changed, 184 insertions(+) create mode 100644 board/src/sight.rs diff --git a/board/src/lib.rs b/board/src/lib.rs index 354c4e1..03b5c96 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -7,6 +7,7 @@ mod moves; pub mod piece; #[macro_use] pub mod position; +mod sight; mod square; pub use moves::Move; diff --git a/board/src/piece.rs b/board/src/piece.rs index 7a20d19..e8a8cbb 100644 --- a/board/src/piece.rs +++ b/board/src/piece.rs @@ -238,6 +238,16 @@ impl PlacedPiece { pub fn square(&self) -> Square { self.square } + + #[inline] + pub fn color(&self) -> Color { + self.piece.color + } + + #[inline] + pub fn shape(&self) -> Shape { + self.piece.shape + } } #[cfg(test)] diff --git a/board/src/position/position.rs b/board/src/position/position.rs index 3a98c1e..cf91f0e 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -44,6 +44,8 @@ pub struct Position { /// Bitboards representing positions of particular piece types per color. pieces_per_type: [[BitBoard; 6]; 2], + + en_passant_square: Option, } impl Position { @@ -70,6 +72,7 @@ impl Position { BitBoard::empty(), ], ], + en_passant_square: None, } } @@ -101,6 +104,7 @@ impl Position { black_pieces.iter().fold(BitBoard::empty(), |a, b| a | *b), ], pieces_per_type: [white_pieces, black_pieces], + en_passant_square: None, } } @@ -155,6 +159,16 @@ impl Position { self.pieces_per_color[Color::White as usize] | self.pieces_per_color[Color::Black as usize] } + #[inline] + pub(crate) fn friendly_pieces(&self) -> BitBoard { + self.bitboard_for_color(self.color_to_move) + } + + #[inline] + pub(crate) fn opposing_pieces(&self) -> BitBoard { + self.bitboard_for_color(self.color_to_move.other()) + } + /// Return a BitBoard representing the set of squares containing a piece. /// This set is the inverse of `occupied_squares`. #[inline] @@ -195,6 +209,10 @@ impl Position { pub fn pieces(&self, color: Color) -> Pieces { Pieces::new(&self, color) } + + pub fn en_passant_square(&self) -> Option { + self.en_passant_square + } } impl Default for Position { diff --git a/board/src/sight.rs b/board/src/sight.rs new file mode 100644 index 0000000..33e716e --- /dev/null +++ b/board/src/sight.rs @@ -0,0 +1,155 @@ +// Eryn Wells + +use crate::{ + piece::{Color, PlacedPiece, Shape}, + square::Direction, + BitBoard, Position, +}; + +pub(crate) trait Sight { + fn sight_in_position(&self, position: &Position) -> BitBoard; +} + +impl Sight for PlacedPiece { + fn sight_in_position(&self, position: &Position) -> BitBoard { + match self.shape() { + Shape::Pawn => match self.color() { + Color::White => self.white_pawn_sight_in_position(position), + Color::Black => self.black_pawn_sight_in_position(position), + }, + Shape::Knight => self.knight_sight_in_position(position), + Shape::Bishop => self.bishop_sight_in_position(position), + Shape::Rook => self.rook_sight_in_position(position), + Shape::Queen => self.queen_sight_in_position(position), + Shape::King => self.king_sight_in_position(position), + } + } +} + +impl PlacedPiece { + fn white_pawn_sight_in_position(&self, position: &Position) -> BitBoard { + let pawn: BitBoard = self.square().into(); + let pawn = pawn.shift_north_west_one() | pawn.shift_north_east_one(); + + let mut possible_squares = position.empty_squares() | position.opposing_pieces(); + if let Some(en_passant) = position.en_passant_square() { + possible_squares |= en_passant.into(); + } + + pawn & possible_squares + } + + fn black_pawn_sight_in_position(&self, position: &Position) -> BitBoard { + let pawn: BitBoard = self.square().into(); + let pawn = pawn.shift_south_west_one() | pawn.shift_south_east_one(); + + let mut possible_squares = position.empty_squares() | position.opposing_pieces(); + if let Some(en_passant) = position.en_passant_square() { + possible_squares |= en_passant.into(); + } + + pawn & possible_squares + } + + fn knight_sight_in_position(&self, position: &Position) -> BitBoard { + BitBoard::knight_moves(self.square()) & !position.friendly_pieces() + } + + fn bishop_sight_in_position(&self, position: &Position) -> BitBoard { + let square = self.square(); + + let mut sight = BitBoard::empty(); + + let blockers = position.occupied_squares(); + + macro_rules! update_moves_with_ray { + ($direction:ident, $occupied_squares:tt) => { + let ray = BitBoard::ray(square, Direction::$direction); + if let Some(first_occupied_square) = + BitBoard::$occupied_squares(&(ray & blockers)).next() + { + let remainder = BitBoard::ray(first_occupied_square, Direction::$direction); + let attack_ray = ray & !remainder; + sight |= attack_ray; + } else { + sight |= ray; + } + }; + } + + update_moves_with_ray!(NorthEast, occupied_squares_trailing); + update_moves_with_ray!(NorthWest, occupied_squares_trailing); + update_moves_with_ray!(SouthEast, occupied_squares); + update_moves_with_ray!(SouthWest, occupied_squares); + + sight + } + + fn rook_sight_in_position(&self, position: &Position) -> BitBoard { + let square = self.square(); + + let mut sight = BitBoard::empty(); + + let blockers = position.occupied_squares(); + + macro_rules! update_moves_with_ray { + ($direction:ident, $occupied_squares:tt) => { + let ray = BitBoard::ray(square, Direction::$direction); + if let Some(first_occupied_square) = + BitBoard::$occupied_squares(&(ray & blockers)).next() + { + let remainder = BitBoard::ray(first_occupied_square, Direction::$direction); + let attack_ray = ray & !remainder; + sight |= attack_ray; + } else { + sight |= ray; + } + }; + } + + update_moves_with_ray!(North, occupied_squares_trailing); + update_moves_with_ray!(East, occupied_squares_trailing); + update_moves_with_ray!(South, occupied_squares); + update_moves_with_ray!(West, occupied_squares); + + sight + } + + fn queen_sight_in_position(&self, position: &Position) -> BitBoard { + let square = self.square(); + + let mut sight = BitBoard::empty(); + + let blockers = position.occupied_squares(); + + macro_rules! update_moves_with_ray { + ($direction:ident, $occupied_squares:tt) => { + let ray = BitBoard::ray(square, Direction::$direction); + if let Some(first_occupied_square) = + BitBoard::$occupied_squares(&(ray & blockers)).next() + { + let remainder = BitBoard::ray(first_occupied_square, Direction::$direction); + let attack_ray = ray & !remainder; + sight |= attack_ray; + } else { + sight |= ray; + } + }; + } + + update_moves_with_ray!(NorthWest, occupied_squares_trailing); + update_moves_with_ray!(North, occupied_squares_trailing); + update_moves_with_ray!(NorthEast, occupied_squares_trailing); + update_moves_with_ray!(East, occupied_squares_trailing); + update_moves_with_ray!(SouthEast, occupied_squares); + update_moves_with_ray!(South, occupied_squares); + update_moves_with_ray!(SouthWest, occupied_squares); + update_moves_with_ray!(West, occupied_squares); + + sight + } + + fn king_sight_in_position(&self, position: &Position) -> BitBoard { + BitBoard::king_moves(self.square()) & !position.friendly_pieces() + } +} From 9ef53b76f5f714b66f9f9a9520f466bfe5bee4c0 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 15 Jan 2024 17:16:14 -0800 Subject: [PATCH 009/423] [board] Rename BitBoard::place_piece_at and BitBoard::remove_piece_at MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit → set_square → clear_square --- board/src/bitboard/bitboard.rs | 14 ++++++++------ board/src/position/position.rs | 4 ++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/board/src/bitboard/bitboard.rs b/board/src/bitboard/bitboard.rs index 3a31a0e..0ccc713 100644 --- a/board/src/bitboard/bitboard.rs +++ b/board/src/bitboard/bitboard.rs @@ -45,7 +45,9 @@ impl BitBoard { moves_getter!(rook_moves); moves_getter!(queen_moves); moves_getter!(king_moves); +} +impl BitBoard { pub fn is_empty(&self) -> bool { self.0 == 0 } @@ -54,12 +56,12 @@ impl BitBoard { !(self & sq.into()).is_empty() } - pub fn place_piece_at(&mut self, sq: Square) { + pub fn set_square(&mut self, sq: Square) { let sq_bb: BitBoard = sq.into(); *self |= sq_bb } - fn remove_piece_at(&mut self, sq: Square) { + fn clear_square(&mut self, sq: Square) { let sq_bb: BitBoard = sq.into(); *self &= !sq_bb } @@ -224,18 +226,18 @@ mod tests { } #[test] - fn place_piece_at() { + fn set_square() { let sq = Square::E4; let mut bb = BitBoard(0b1001100); - bb.place_piece_at(sq); + bb.set_square(sq); assert!(bb.has_piece_at(sq)); } #[test] - fn remove_piece_at() { + fn clear_square() { let sq = Square::A3; let mut bb = BitBoard(0b1001100); - bb.remove_piece_at(sq); + bb.clear_square(sq); assert!(!bb.has_piece_at(sq)); } diff --git a/board/src/position/position.rs b/board/src/position/position.rs index cf91f0e..6a1345d 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -141,10 +141,10 @@ impl Position { return Err(PiecePlacementError::ExistsOnSquare); } - type_bb.place_piece_at(square); + type_bb.set_square(square); let color_bb = &mut self.bitboard_for_color_mut(piece.color()); - color_bb.place_piece_at(square); + color_bb.set_square(square); Ok(()) } From a6b98abb95e36a52d75f51ba93c568d2b0533302 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 15 Jan 2024 17:17:34 -0800 Subject: [PATCH 010/423] [board] Create a bitboard! macro and BitBoardBuilder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Builder enables cleanly building a BitBoard out of squares. The macro lets you create a BitBoard from a simple list of coordinates: bitboard!(A1, B2, C3, D4, …) --- board/src/bitboard/bitboard.rs | 21 +++++++++++++++++++++ board/src/bitboard/mod.rs | 11 ++++++++++- board/src/lib.rs | 1 + 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/board/src/bitboard/bitboard.rs b/board/src/bitboard/bitboard.rs index 0ccc713..60d4f22 100644 --- a/board/src/bitboard/bitboard.rs +++ b/board/src/bitboard/bitboard.rs @@ -188,6 +188,27 @@ impl Not for &BitBoard { } } +pub struct BitBoardBuilder(BitBoard); + +impl BitBoardBuilder { + pub const fn empty() -> BitBoardBuilder { + BitBoardBuilder(BitBoard::empty()) + } + + pub fn new(bits: u64) -> BitBoardBuilder { + BitBoardBuilder(BitBoard::new(bits)) + } + + pub fn square(mut self, square: Square) -> BitBoardBuilder { + self.0.set_square(square); + self + } + + pub fn build(&self) -> BitBoard { + self.0 + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/board/src/bitboard/mod.rs b/board/src/bitboard/mod.rs index 6093fa8..238f6ac 100644 --- a/board/src/bitboard/mod.rs +++ b/board/src/bitboard/mod.rs @@ -4,4 +4,13 @@ mod library; mod shifts; pub(crate) use bit_scanner::{LeadingBitScanner, TrailingBitScanner}; -pub(crate) use bitboard::BitBoard; +pub(crate) use bitboard::{BitBoard, BitBoardBuilder}; + +#[macro_export] +macro_rules! bitboard { + ($($sq:ident),*) => { + $crate::bitboard::BitBoardBuilder::empty() + $(.square($crate::Square::$sq))* + .build() + }; +} diff --git a/board/src/lib.rs b/board/src/lib.rs index 03b5c96..cea348e 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -1,5 +1,6 @@ // Eryn Wells +#[macro_use] mod bitboard; mod display; mod moves; From e9607573c2d83b520953d8a2dde231f89075a529 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 15 Jan 2024 17:17:57 -0800 Subject: [PATCH 011/423] [board] Use absolute paths with $crate in the piece! macro --- board/src/piece.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/board/src/piece.rs b/board/src/piece.rs index e8a8cbb..5d8fc8a 100644 --- a/board/src/piece.rs +++ b/board/src/piece.rs @@ -124,10 +124,10 @@ pub enum PiecePlacementError { #[macro_export] macro_rules! piece { ($color:ident $shape:ident) => { - Piece::new(Color::$color, Shape::$shape) + $crate::piece::Piece::new($crate::piece::Color::$color, $crate::piece::Shape::$shape) }; ($color:ident $shape:ident on $square:ident) => { - PlacedPiece::new(piece!($color $shape), Square::$square) + $crate::piece::PlacedPiece::new(piece!($color $shape), $crate::square::Square::$square) } } From 98fce9acde41488ac55608b3fbc17e743b148ebd Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 15 Jan 2024 17:18:18 -0800 Subject: [PATCH 012/423] [board] Fix the shape_into_char test --- board/src/piece.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/board/src/piece.rs b/board/src/piece.rs index 5d8fc8a..e3614f4 100644 --- a/board/src/piece.rs +++ b/board/src/piece.rs @@ -262,6 +262,6 @@ mod tests { #[test] fn shape_into_char() { - assert_eq!(>::into(Shape::Pawn) as char, 'p'); + assert_eq!(>::into(Shape::Pawn) as char, 'P'); } } From 81d544f0d5f8352f27c41da4405236bf11073d33 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 15 Jan 2024 17:42:27 -0800 Subject: [PATCH 013/423] =?UTF-8?q?[board]=20Rename=20BitBoard::has=5Fpiec?= =?UTF-8?q?e=5Fat=20=E2=86=92=20is=5Fset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- board/src/bitboard/bitboard.rs | 10 +++++----- board/src/position/position.rs | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/board/src/bitboard/bitboard.rs b/board/src/bitboard/bitboard.rs index 60d4f22..6fee968 100644 --- a/board/src/bitboard/bitboard.rs +++ b/board/src/bitboard/bitboard.rs @@ -52,7 +52,7 @@ impl BitBoard { self.0 == 0 } - pub fn has_piece_at(self, sq: Square) -> bool { + pub fn is_set(self, sq: Square) -> bool { !(self & sq.into()).is_empty() } @@ -242,8 +242,8 @@ mod tests { #[test] fn has_piece_at() { let bb = BitBoard(0b1001100); - assert!(bb.has_piece_at(Square::C1)); - assert!(!bb.has_piece_at(Square::B1)); + assert!(bb.is_set(Square::C1)); + assert!(!bb.is_set(Square::B1)); } #[test] @@ -251,7 +251,7 @@ mod tests { let sq = Square::E4; let mut bb = BitBoard(0b1001100); bb.set_square(sq); - assert!(bb.has_piece_at(sq)); + assert!(bb.is_set(sq)); } #[test] @@ -259,7 +259,7 @@ mod tests { let sq = Square::A3; let mut bb = BitBoard(0b1001100); bb.clear_square(sq); - assert!(!bb.has_piece_at(sq)); + assert!(!bb.is_set(sq)); } #[test] diff --git a/board/src/position/position.rs b/board/src/position/position.rs index 6a1345d..c9e4fe6 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -137,7 +137,7 @@ impl Position { pub fn place_piece(&mut self, piece: Piece, square: Square) -> Result<(), PiecePlacementError> { let type_bb = self.bitboard_for_piece_mut(piece); - if type_bb.has_piece_at(square) { + if type_bb.is_set(square) { return Err(PiecePlacementError::ExistsOnSquare); } @@ -197,7 +197,7 @@ impl Position { for shape in Shape::iter() { let piece = Piece::new(color, *shape); let bb = self.bitboard_for_piece(piece); - if bb.has_piece_at(sq) { + if bb.is_set(sq) { return Some(PlacedPiece::new(piece, sq)); } } From 96b04379a4d501faf631362bbead99c4ffcabb8b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 15 Jan 2024 17:43:27 -0800 Subject: [PATCH 014/423] [board] Implement Position::is_king_in_check() Returns true if the king of the current player is in check. --- board/src/position/position.rs | 45 +++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/board/src/position/position.rs b/board/src/position/position.rs index c9e4fe6..0557c27 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -4,8 +4,10 @@ use super::{flags::Flags, Pieces}; use crate::{ moves::Moves, piece::{Color, Piece, PiecePlacementError, PlacedPiece, Shape}, + sight::Sight, BitBoard, Square, }; +use std::cell::OnceCell; use std::fmt; use std::fmt::Write; @@ -26,7 +28,7 @@ macro_rules! position { }; } -#[derive(Clone, Eq, Hash, PartialEq)] +#[derive(Clone, Eq, PartialEq)] pub struct Position { color_to_move: Color, @@ -46,6 +48,8 @@ pub struct Position { pieces_per_type: [[BitBoard; 6]; 2], en_passant_square: Option, + + sight: [OnceCell; 2], } impl Position { @@ -73,6 +77,7 @@ impl Position { ], ], en_passant_square: None, + sight: [OnceCell::new(), OnceCell::new()], } } @@ -105,6 +110,7 @@ impl Position { ], pieces_per_type: [white_pieces, black_pieces], en_passant_square: None, + sight: [OnceCell::new(), OnceCell::new()], } } @@ -213,6 +219,25 @@ impl Position { pub fn en_passant_square(&self) -> Option { self.en_passant_square } + + pub(crate) fn sight_of_player(&self, color: Color) -> BitBoard { + *self.sight[color as usize].get_or_init(|| { + self.pieces(color).fold(BitBoard::empty(), |acc, pp| { + acc | pp.sight_in_position(&self) + }) + }) + } + + pub(crate) fn is_king_in_check(&self) -> bool { + let sight_of_opposing_player = self.sight_of_player(self.color_to_move.other()); + let king_square = self + .bitboard_for_piece(Piece::king(self.color_to_move)) + .occupied_squares() + .next() + .unwrap(); + sight_of_opposing_player.is_set(king_square) + } +} } impl Default for Position { @@ -288,4 +313,22 @@ mod tests { .place_piece(piece, square) .expect_err("Placed white queen on e4 a second time?!"); } + + #[test] + fn king_is_in_check() { + let pos = position![ + White King on E1, + Black Rook on E8, + ]; + assert!(pos.is_king_in_check()); + } + + #[test] + fn king_is_not_in_check() { + let pos = position![ + White King on F1, + Black Rook on E8, + ]; + assert!(!pos.is_king_in_check()); + } } From 177a4e32da9e58714fd0a6bf5ba05ae88bc9a79c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 16 Jan 2024 18:03:27 -0800 Subject: [PATCH 015/423] [board] Implement a u16 based Move Replace building a Move with the Move struct itself with a MoveBuilder that builds a Move and returns it. Update the tests and move formatter. --- board/src/lib.rs | 3 +- board/src/move.rs | 395 ++++++++++++++++++++++++++++++++++++++ board/src/moves/mod.rs | 2 - board/src/moves/move.rs | 210 -------------------- board/src/position/mod.rs | 1 + 5 files changed, 398 insertions(+), 213 deletions(-) create mode 100644 board/src/move.rs delete mode 100644 board/src/moves/move.rs diff --git a/board/src/lib.rs b/board/src/lib.rs index cea348e..442935a 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -4,6 +4,7 @@ mod bitboard; mod display; mod moves; +mod r#move; #[macro_use] pub mod piece; #[macro_use] @@ -11,8 +12,8 @@ pub mod position; mod sight; mod square; -pub use moves::Move; pub use position::Position; +pub use r#move::{Move, MoveBuilder}; pub use square::{File, Rank, Square}; pub(crate) use bitboard::BitBoard; diff --git a/board/src/move.rs b/board/src/move.rs new file mode 100644 index 0000000..2f74331 --- /dev/null +++ b/board/src/move.rs @@ -0,0 +1,395 @@ +// Eryn Wells + +use crate::{ + piece::{Piece, PlacedPiece, Shape}, + position::BoardSide, + square::Rank, + Square, +}; +use std::fmt; + +#[repr(u16)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Castle { + KingSide = 0b10, + QueenSide = 0b11, +} + +impl From for Castle { + fn from(value: BoardSide) -> Self { + match value { + BoardSide::King => Castle::KingSide, + BoardSide::Queen => Castle::QueenSide, + } + } +} + +#[repr(u16)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum PromotableShape { + Knight = 0b00, + Bishop = 0b01, + Rook = 0b10, + Queen = 0b11, +} + +impl TryFrom for PromotableShape { + type Error = (); + + fn try_from(value: Shape) -> Result { + match value { + Shape::Knight => Ok(PromotableShape::Knight), + Shape::Bishop => Ok(PromotableShape::Bishop), + Shape::Rook => Ok(PromotableShape::Rook), + Shape::Queen => Ok(PromotableShape::Queen), + _ => Err(()), + } + } +} + +#[repr(u16)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +enum Kind { + Quiet = 0b00, + DoublePush = 0b01, + Castle(Castle), + Capture(PlacedPiece) = 0b0100, + EnPassantCapture(PlacedPiece) = 0b0101, + Promotion(PromotableShape) = 0b1000, + CapturePromotion(PlacedPiece, PromotableShape) = 0b1100, +} + +impl Kind { + fn bits(&self) -> u16 { + match self { + Self::Promotion(shape) => self.discriminant() | *shape as u16, + Self::CapturePromotion(_, shape) => self.discriminant() | *shape as u16, + Self::Castle(castle) => *castle as u16, + _ => self.discriminant(), + } + } + + /// Return the discriminant value. This implementation is copied from the Rust docs. + /// See https://doc.rust-lang.org/std/mem/fn.discriminant.html + fn discriminant(&self) -> u16 { + unsafe { *<*const _>::from(self).cast::() } + } +} + +impl Default for Kind { + fn default() -> Self { + Self::Quiet + } +} + +/// A single player's move. In chess parlance, this is a "ply". +#[derive(Clone, Eq, Hash, PartialEq)] +pub struct Move(u16); + +impl Move { + pub fn from_square(&self) -> Square { + ((self.0 >> 4) & 0b111111).try_into().unwrap() + } + + pub fn to_square(&self) -> Square { + (self.0 >> 10).try_into().unwrap() + } + + pub fn is_quiet(&self) -> bool { + self.special() == 0b00 + } + + pub fn is_double_push(&self) -> bool { + self.special() == 0b01 + } + + pub fn is_castle(&self) -> bool { + self.castle().is_some() + } + + pub fn castle(&self) -> Option { + match self.special() { + 0b0010 => Some(Castle::KingSide), + 0b0011 => Some(Castle::QueenSide), + _ => None, + } + } + + pub fn is_capture(&self) -> bool { + (self.0 & 0b0100) != 0 + } + + pub fn is_en_passant(&self) -> bool { + (self.0 & 0b0101) != 0 + } + + pub fn is_promotion(&self) -> bool { + (self.0 & 0b1000) != 0 + } + + pub fn promotion(&self) -> Option { + if !self.is_promotion() { + return None; + } + + Some(match self.special() { + 0b00 => Shape::Knight, + 0b01 => Shape::Bishop, + 0b10 => Shape::Rook, + 0b11 => Shape::Queen, + _ => unreachable!(), + }) + } + + #[inline] + fn special(&self) -> u16 { + self.0 & 0b11 + } +} + +impl fmt::Debug for Move { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("Move") + .field(&format_args!("{:08b}", &self.0)) + .finish() + } +} + +pub struct MoveBuilder { + piece: Piece, + from: Square, + to: Square, + kind: Kind, +} + +impl MoveBuilder { + pub fn new(piece: Piece, from: Square, to: Square) -> Self { + let kind = match piece.shape() { + Shape::Pawn => { + let is_white_double_push = from.rank() == Rank::Two && to.rank() == Rank::Four; + let is_black_double_push = from.rank() == Rank::Seven && to.rank() == Rank::Five; + if is_white_double_push || is_black_double_push { + Kind::DoublePush + } else { + Kind::Quiet + } + } + _ => Kind::Quiet, + }; + + Self { + piece, + from, + to, + kind, + } + } + + pub fn castle(mut self, castle: Castle) -> Self { + self.kind = Kind::Castle(castle); + self + } + + pub fn capturing(mut self, captured_piece: PlacedPiece) -> Self { + self.kind = match self.kind { + Kind::Promotion(shape) => Kind::CapturePromotion(captured_piece, shape), + _ => Kind::Capture(captured_piece), + }; + self + } + + pub fn promoting_to(mut self, shape: Shape) -> Self { + if let Some(shape) = PromotableShape::try_from(shape).ok() { + self.kind = match self.kind { + Kind::Capture(piece) => Kind::CapturePromotion(piece, shape), + Kind::CapturePromotion(piece, _) => Kind::CapturePromotion(piece, shape), + _ => Kind::Promotion(shape), + }; + } + self + } + + pub fn build(&self) -> Move { + Move( + self.kind.bits() + | ((self.from as u16 & 0b111111) << 4) + | ((self.to as u16 & 0b111111) << 10), + ) + } +} + +mod move_formatter { + use super::{Castle, Move}; + use crate::{piece::Shape, Position}; + use std::fmt; + + enum Style { + Short, + Long, + } + + pub(crate) struct AlgebraicMoveFormatter<'m, 'pos> { + position: &'pos Position, + r#move: &'m Move, + style: Style, + } + + impl<'pos, 'm> AlgebraicMoveFormatter<'m, 'pos> { + pub(crate) fn new( + mv: &'m Move, + position: &'pos Position, + ) -> AlgebraicMoveFormatter<'m, 'pos> { + AlgebraicMoveFormatter { + position, + r#move: mv, + style: Style::Short, + } + } + + fn style(mut self, style: Style) -> Self { + self.style = style; + self + } + + fn fmt_kingside_castle(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "0-0") + } + + fn fmt_queenside_castle(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "0-0-0") + } + + fn fmt_short(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + unimplemented!() + } + + fn fmt_long(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // TODO: Figure out how to write the short algebraic form, where a + // disambiguating coordiate is specified when two of the same piece + // cam move to the same square. + + // TODO: Write better pawn moves. + + let mv = self.r#move; + let from_square = mv.from_square(); + let to_square = mv.to_square(); + + let piece = self + .position + .piece_on_square(from_square) + .expect(&format!("No piece on {}", from_square)); + if piece.shape() != Shape::Pawn { + write!(f, "{}", piece.shape())?; + } + + write!( + f, + "{}{}{}", + from_square, + if mv.is_capture() { 'x' } else { '-' }, + to_square, + )?; + + if let Some(promotion) = mv.promotion() { + write!(f, "={}", promotion)?; + } + + // TODO: Write check (+) and checkmate (#) symbols + + Ok(()) + } + } + + impl<'pos, 'mv> fmt::Display for AlgebraicMoveFormatter<'mv, 'pos> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mv = self.r#move; + match mv.castle() { + Some(Castle::KingSide) => return self.fmt_kingside_castle(f), + Some(Castle::QueenSide) => return self.fmt_queenside_castle(f), + _ => {} + } + + match self.style { + Style::Short => self.fmt_short(f), + Style::Long => self.fmt_long(f), + } + } + } + + #[cfg(test)] + mod tests { + use super::{AlgebraicMoveFormatter, Style}; + use crate::{piece, position}; + + macro_rules! chess_move { + ($color:ident $shape:ident $from_square:ident - $to_square:ident) => { + $crate::MoveBuilder::new( + $crate::piece::Piece::new( + $crate::piece::Color::$color, + $crate::piece::Shape::$shape, + ), + $crate::Square::$from_square, + $crate::Square::$to_square, + ) + .build() + }; + ($color:ident $shape:ident $from_square:ident x $to_square:ident, $captured_color:ident $captured_shape:ident) => { + $crate::MoveBuilder::new( + $crate::piece::Piece::new( + $crate::piece::Color::$color, + $crate::piece::Shape::$shape, + ), + $crate::Square::$from_square, + $crate::Square::$to_square, + ) + .capturing($crate::piece::PlacedPiece::new( + $crate::piece::Piece::new( + $crate::piece::Color::$captured_color, + $crate::piece::Shape::$captured_shape, + ), + $crate::Square::$to_square, + )) + .build() + }; + } + + macro_rules! test_algebraic_formatter { + ($test_name:ident, $style:ident, $color:ident $shape:ident $from_square:ident x $to_square:ident, $captured_color:ident $captured_shape:ident, $output:expr) => { + #[test] + fn $test_name() { + let pos = position![ + $color $shape on $from_square, + $captured_color $captured_shape on $to_square, + ]; + let mv = chess_move!( + $color $shape $from_square x $to_square, + $captured_color $captured_shape + ); + + println!("{:?}", &mv); + + let formatter = AlgebraicMoveFormatter::new(&mv, &pos).style(Style::$style); + assert_eq!(format!("{}", formatter), $output); + } + }; + ($test_name:ident, $style:ident, $color:ident $shape:ident $from_square:ident - $to_square:ident, $output:expr) => { + #[test] + fn $test_name() { + let pos = position![ + $color $shape on $from_square, + ]; + + let mv = chess_move!($color $shape $from_square-$to_square); + println!("{:?}", &mv); + + let formatter = AlgebraicMoveFormatter::new(&mv, &pos).style(Style::$style); + assert_eq!(format!("{}", formatter), $output); + } + }; + } + + test_algebraic_formatter!(long_pawn_move, Long, White Pawn E4 - E5, "e4-e5"); + test_algebraic_formatter!(long_bishop_move, Long, White Bishop A4 - D7, "Ba4-d7"); + test_algebraic_formatter!(long_bishop_capture, Long, White Bishop A2 x E6, Black Knight, "Ba2xe6"); + } +} diff --git a/board/src/moves/mod.rs b/board/src/moves/mod.rs index d395991..44e2f7c 100644 --- a/board/src/moves/mod.rs +++ b/board/src/moves/mod.rs @@ -3,7 +3,6 @@ mod bishop; mod king; mod knight; -mod r#move; mod move_generator; mod move_set; mod pawn; @@ -11,7 +10,6 @@ mod queen; mod rook; pub use move_generator::Moves; -pub use r#move::Move; pub(self) use move_set::MoveSet; diff --git a/board/src/moves/move.rs b/board/src/moves/move.rs deleted file mode 100644 index d952bec..0000000 --- a/board/src/moves/move.rs +++ /dev/null @@ -1,210 +0,0 @@ -// Eryn Wells - -use crate::{ - piece::{Piece, PlacedPiece, Shape}, - position::BoardSide, - Square, -}; - -#[derive(Debug, Clone, Eq, Hash, PartialEq)] -pub struct Move { - piece: Piece, - from: Square, - to: Square, - capturing: Option, - promoting_to: Option, -} - -impl Move { - pub fn new(piece: Piece, from: Square, to: Square) -> Move { - Move { - piece, - from, - to, - capturing: None, - promoting_to: None, - } - } - - pub fn capturing(mut self, piece: PlacedPiece) -> Move { - self.capturing = Some(piece); - self - } - - pub fn promoting_to(mut self, shape: Shape) -> Move { - self.promoting_to = Some(shape); - self - } - - pub fn is_castle(&self) -> bool { - let color = self.piece.color(); - self.piece.shape() == Shape::King - && self.from == Square::KING_STARTING_SQUARES[color as usize] - && Square::KING_CASTLE_TARGET_SQUARES[color as usize].contains(&self.to) - } - - pub fn is_kingside_castle(&self) -> bool { - let color = self.piece.color(); - self.piece.shape() == Shape::King - && self.from == Square::KING_STARTING_SQUARES[color as usize] - && self.to - == Square::KING_CASTLE_TARGET_SQUARES[color as usize][BoardSide::King as usize] - } - - pub fn is_queenside_castle(&self) -> bool { - let color = self.piece.color(); - self.piece.shape() == Shape::King - && self.from == Square::KING_STARTING_SQUARES[color as usize] - && self.to - == Square::KING_CASTLE_TARGET_SQUARES[color as usize][BoardSide::Queen as usize] - } - - pub fn is_capture(&self) -> bool { - self.capturing.is_some() - } - - pub fn is_promotion(&self) -> bool { - self.promoting_to.is_some() - } -} - -mod move_formatter { - use super::Move; - use crate::{piece::Shape, Position}; - use std::fmt; - - enum Style { - Short, - Long, - } - - pub(crate) struct AlgebraicMoveFormatter<'m> { - r#move: &'m Move, - style: Style, - } - - impl<'pos, 'm> AlgebraicMoveFormatter<'m> { - pub(crate) fn new(mv: &'m Move) -> AlgebraicMoveFormatter<'m> { - AlgebraicMoveFormatter { - r#move: mv, - style: Style::Short, - } - } - - fn style(mut self, style: Style) -> Self { - self.style = style; - self - } - - fn fmt_kingside_castle(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "0-0") - } - - fn fmt_queenside_castle(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "0-0-0") - } - - fn fmt_short(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - unimplemented!() - } - - fn fmt_long(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // TODO: Figure out how to write the short algebraic form, where a - // disambiguating coordiate is specified when two of the same piece - // cam move to the same square. - - // TODO: Write better pawn moves. - - let mv = self.r#move; - - let shape = mv.piece.shape(); - if shape != Shape::Pawn { - write!(f, "{}", shape)?; - } - - write!( - f, - "{}{}{}", - mv.from, - if mv.is_capture() { 'x' } else { '-' }, - mv.to, - )?; - - if let Some(promoting_to) = mv.promoting_to { - write!(f, "={}", promoting_to)?; - } - - // TODO: Write check (+) and checkmate (#) symbols - - Ok(()) - } - } - - impl<'pos, 'mv> fmt::Display for AlgebraicMoveFormatter<'mv> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.r#move.is_kingside_castle() { - return self.fmt_kingside_castle(f); - } else if self.r#move.is_queenside_castle() { - return self.fmt_queenside_castle(f); - } - - match self.style { - Style::Short => self.fmt_short(f), - Style::Long => self.fmt_long(f), - } - } - } - - #[cfg(test)] - mod tests { - use super::{AlgebraicMoveFormatter, Style}; - - macro_rules! chess_move { - ($color:ident $shape:ident $from_square:ident - $to_square:ident) => { - crate::Move::new( - crate::piece::Piece::new(crate::piece::Color::$color, crate::piece::Shape::$shape), - crate::Square::$from_square, - crate::Square::$to_square, - ) - }; - ($color:ident $shape:ident $from_square:ident x $to_square:ident, $captured_color:ident $captured_shape:ident) => { - chess_move!($color $shape $from_square - $to_square) - .capturing(crate::piece::PlacedPiece::new( - crate::piece::Piece::new( - crate::piece::Color::$captured_color, - crate::piece::Shape::$captured_shape, - ), - crate::Square::$to_square, - )) - }; - } - - macro_rules! test_algebraic_formatter { - ($test_name:ident, $style:ident, $color:ident $shape:ident $from_square:ident x $to_square:ident, $captured_color:ident $captured_shape:ident, $output:expr) => { - #[test] - fn $test_name() { - let mv = chess_move!( - $color $shape $from_square x $to_square, - $captured_color $captured_shape - ); - - let formatter = AlgebraicMoveFormatter::new(&mv).style(Style::$style); - assert_eq!(format!("{}", formatter), $output); - } - }; - ($test_name:ident, $style:ident, $color:ident $shape:ident $from_square:ident - $to_square:ident, $output:expr) => { - #[test] - fn $test_name() { - let mv = chess_move!($color $shape $from_square-$to_square); - - let formatter = AlgebraicMoveFormatter::new(&mv).style(Style::$style); - assert_eq!(format!("{}", formatter), $output); - } - }; - } - - test_algebraic_formatter!(long_pawn_move, Long, White Pawn E4 - E5, "e4-e5"); - test_algebraic_formatter!(long_bishop_move, Long, White Bishop A4 - D7, "Ba4-d7"); - test_algebraic_formatter!(long_bishop_capture, Long, White Bishop A2 x E6, Black Knight, "Ba2xe6"); - } -} diff --git a/board/src/position/mod.rs b/board/src/position/mod.rs index 650959d..6899faa 100644 --- a/board/src/position/mod.rs +++ b/board/src/position/mod.rs @@ -3,6 +3,7 @@ mod diagram_formatter; mod flags; mod pieces; +#[macro_use] mod position; pub use diagram_formatter::DiagramFormatter; From 3a15fca10ae97bead4269478bea37190d6b90643 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 17 Jan 2024 08:24:53 -0800 Subject: [PATCH 016/423] [board] Remove the intermediate string in the Debug implementation for Position --- board/src/position/position.rs | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/board/src/position/position.rs b/board/src/position/position.rs index 0557c27..b9d7133 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -259,28 +259,24 @@ impl FromIterator for Position { impl fmt::Debug for Position { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut output = String::new(); + write!(f, "Position {{\n")?; + write!(f, " color_to_move: {:?},\n", self.color_to_move)?; - output.push_str("Position {\n"); - write!(output, " color_to_move: {:?},\n", self.color_to_move)?; - - output.push_str(" pieces_per_color: [\n"); + write!(f, " pieces_per_color: [\n")?; for bb in self.pieces_per_color { - write!(output, " {bb:?},\n")?; + write!(f, " {bb:?},\n")?; } - output.push_str(" ],\n"); + write!(f, " ],\n")?; - output.push_str(" pieces_per_type: [\n"); + write!(f, " pieces_per_type: [\n")?; for color_bbs in self.pieces_per_type { - output.push_str(" [\n"); + write!(f, " [\n")?; for bb in color_bbs { - write!(output, " {bb:?},\n")?; + write!(f, " {bb:?},\n")?; } - output.push_str(" ],\n"); + write!(f, " ],\n")?; } - output.push_str(" ],\n}"); - - write!(f, "{}", output) + write!(f, " ],\n}}") } } From f27d22d57f84d41fa378a7738a4ed3382e7583c4 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 17 Jan 2024 08:35:21 -0800 Subject: [PATCH 017/423] [board] Implement some TryInto traits for Square for u8, u16, u32, and u64 --- board/src/square.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/board/src/square.rs b/board/src/square.rs index d380b29..0ad9d09 100644 --- a/board/src/square.rs +++ b/board/src/square.rs @@ -237,6 +237,23 @@ impl FromStr for Square { } } +macro_rules! try_from_integer { + ($int_type:ident) => { + impl TryFrom<$int_type> for Square { + type Error = SquareOutOfBoundsError; + + fn try_from(value: $int_type) -> Result { + Square::try_index(value as usize).ok_or(SquareOutOfBoundsError) + } + } + }; +} + +try_from_integer!(u8); +try_from_integer!(u16); +try_from_integer!(u32); +try_from_integer!(u64); + impl fmt::Display for Square { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( From c8db5a430d674cb980c7590281486c34c29b250b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 17 Jan 2024 08:35:42 -0800 Subject: [PATCH 018/423] [board] Fix a silly build error in position.rs An extra closing brace. --- board/src/position/position.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/board/src/position/position.rs b/board/src/position/position.rs index b9d7133..21b372d 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -238,7 +238,6 @@ impl Position { sight_of_opposing_player.is_set(king_square) } } -} impl Default for Position { fn default() -> Self { From 2d4ad70994415928fb82adaf3458b4138342329b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 17 Jan 2024 08:36:00 -0800 Subject: [PATCH 019/423] [board] Refer to Position with $crate in the position! macro --- board/src/position/position.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/board/src/position/position.rs b/board/src/position/position.rs index 21b372d..2311df3 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -22,7 +22,7 @@ pub(crate) enum BoardSide { #[macro_export] macro_rules! position { [$($color:ident $shape:ident on $square:ident,)*] => { - Position::from_iter([ + $crate::Position::from_iter([ $(piece!($color $shape on $square)),* ].into_iter()) }; From ca9ff94d2a11a28b1fb1f6a41eff8ed584f57214 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 17 Jan 2024 08:40:09 -0800 Subject: [PATCH 020/423] =?UTF-8?q?[board]=20Rename=20the=20moves=20module?= =?UTF-8?q?s=20=E2=86=92=20move=5Fgenerator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update the imports. Also update some references to crate symbols in move_generator macros to use $crate. --- board/src/lib.rs | 2 +- board/src/{moves => move_generator}/bishop.rs | 0 board/src/{moves => move_generator}/king.rs | 0 board/src/{moves => move_generator}/knight.rs | 0 board/src/{moves => move_generator}/mod.rs | 12 ++++++------ .../src/{moves => move_generator}/move_generator.rs | 5 ++--- board/src/{moves => move_generator}/move_set.rs | 0 board/src/{moves => move_generator}/pawn.rs | 0 board/src/{moves => move_generator}/queen.rs | 0 board/src/{moves => move_generator}/rook.rs | 0 board/src/position/position.rs | 2 +- 11 files changed, 10 insertions(+), 11 deletions(-) rename board/src/{moves => move_generator}/bishop.rs (100%) rename board/src/{moves => move_generator}/king.rs (100%) rename board/src/{moves => move_generator}/knight.rs (100%) rename board/src/{moves => move_generator}/mod.rs (84%) rename board/src/{moves => move_generator}/move_generator.rs (92%) rename board/src/{moves => move_generator}/move_set.rs (100%) rename board/src/{moves => move_generator}/pawn.rs (100%) rename board/src/{moves => move_generator}/queen.rs (100%) rename board/src/{moves => move_generator}/rook.rs (100%) diff --git a/board/src/lib.rs b/board/src/lib.rs index 442935a..969bd6f 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -3,8 +3,8 @@ #[macro_use] mod bitboard; mod display; -mod moves; mod r#move; +mod move_generator; #[macro_use] pub mod piece; #[macro_use] diff --git a/board/src/moves/bishop.rs b/board/src/move_generator/bishop.rs similarity index 100% rename from board/src/moves/bishop.rs rename to board/src/move_generator/bishop.rs diff --git a/board/src/moves/king.rs b/board/src/move_generator/king.rs similarity index 100% rename from board/src/moves/king.rs rename to board/src/move_generator/king.rs diff --git a/board/src/moves/knight.rs b/board/src/move_generator/knight.rs similarity index 100% rename from board/src/moves/knight.rs rename to board/src/move_generator/knight.rs diff --git a/board/src/moves/mod.rs b/board/src/move_generator/mod.rs similarity index 84% rename from board/src/moves/mod.rs rename to board/src/move_generator/mod.rs index 44e2f7c..eb5aee8 100644 --- a/board/src/moves/mod.rs +++ b/board/src/move_generator/mod.rs @@ -15,7 +15,7 @@ pub(self) use move_set::MoveSet; use crate::{ piece::{Color, Piece, PlacedPiece}, - BitBoard, Position, Square, + Move, Position, Square, }; use std::collections::BTreeMap; @@ -33,14 +33,14 @@ macro_rules! move_generator_declaration { }; ($name:ident, struct) => { pub(super) struct $name<'pos> { - position: &'pos crate::Position, - color: crate::piece::Color, - move_sets: std::collections::BTreeMap, + position: &'pos $crate::Position, + color: $crate::piece::Color, + move_sets: std::collections::BTreeMap<$crate::Square, $crate::move_generator::MoveSet>, } }; ($name:ident, new) => { impl<'pos> $name<'pos> { - pub(super) fn new(position: &Position, color: $crate::piece::Color) -> $name { + pub(super) fn new(position: &$crate::Position, color: $crate::piece::Color) -> $name { $name { position, color, @@ -51,7 +51,7 @@ macro_rules! move_generator_declaration { }; ($name:ident, getters) => { impl<'pos> $name<'pos> { - pub(super) fn iter(&self) -> impl Iterator + '_ { + pub(super) fn iter(&self) -> impl Iterator + '_ { self.move_sets.values().map(|set| set.moves()).flatten() } diff --git a/board/src/moves/move_generator.rs b/board/src/move_generator/move_generator.rs similarity index 92% rename from board/src/moves/move_generator.rs rename to board/src/move_generator/move_generator.rs index b4962db..6fa44b0 100644 --- a/board/src/moves/move_generator.rs +++ b/board/src/move_generator/move_generator.rs @@ -4,10 +4,9 @@ use super::{ bishop::ClassicalMoveGenerator as BishopMoveGenerator, king::KingMoveGenerator, knight::KnightMoveGenerator, pawn::PawnMoveGenerator, queen::ClassicalMoveGenerator as QueenMoveGenerator, - rook::ClassicalMoveGenerator as RookMoveGenerator, Move, + rook::ClassicalMoveGenerator as RookMoveGenerator, }; -use crate::piece::Color; -use crate::Position; +use crate::{piece::Color, Move, Position}; pub struct Moves<'pos> { pawn_moves: PawnMoveGenerator<'pos>, diff --git a/board/src/moves/move_set.rs b/board/src/move_generator/move_set.rs similarity index 100% rename from board/src/moves/move_set.rs rename to board/src/move_generator/move_set.rs diff --git a/board/src/moves/pawn.rs b/board/src/move_generator/pawn.rs similarity index 100% rename from board/src/moves/pawn.rs rename to board/src/move_generator/pawn.rs diff --git a/board/src/moves/queen.rs b/board/src/move_generator/queen.rs similarity index 100% rename from board/src/moves/queen.rs rename to board/src/move_generator/queen.rs diff --git a/board/src/moves/rook.rs b/board/src/move_generator/rook.rs similarity index 100% rename from board/src/moves/rook.rs rename to board/src/move_generator/rook.rs diff --git a/board/src/position/position.rs b/board/src/position/position.rs index 2311df3..fb1b402 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -2,7 +2,7 @@ use super::{flags::Flags, Pieces}; use crate::{ - moves::Moves, + move_generator::Moves, piece::{Color, Piece, PiecePlacementError, PlacedPiece, Shape}, sight::Sight, BitBoard, Square, From 2174bcf00988ff02c59aeeb7343d4f574f6ce3c4 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 17 Jan 2024 08:43:48 -0800 Subject: [PATCH 021/423] [board] Update all the move generator submodules to use MoveBuilder instead of Move Along the way update "manual" piece creation to use the macros instead. --- board/src/move_generator/bishop.rs | 4 +- board/src/move_generator/king.rs | 28 +++++------ board/src/move_generator/knight.rs | 30 ++++++------ board/src/move_generator/pawn.rs | 78 ++++++++++++++++-------------- board/src/move_generator/queen.rs | 4 +- board/src/move_generator/rook.rs | 4 +- board/src/position/position.rs | 4 +- 7 files changed, 79 insertions(+), 73 deletions(-) diff --git a/board/src/move_generator/bishop.rs b/board/src/move_generator/bishop.rs index 6db8668..5dfd632 100644 --- a/board/src/move_generator/bishop.rs +++ b/board/src/move_generator/bishop.rs @@ -4,7 +4,7 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::{ piece::{Color, Piece, PlacedPiece}, square::Direction, - BitBoard, Move, Position, + BitBoard, MoveBuilder, Position, }; move_generator_declaration!(ClassicalMoveGenerator); @@ -49,7 +49,7 @@ impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> { let quiet_moves_bb = all_moves & (empty_squares | !friendly_pieces); let capture_moves_bb = all_moves & opposing_pieces; - let map_to_move = |sq| Move::new(piece, square, sq); + let map_to_move = |sq| MoveBuilder::new(piece, square, sq).build(); let quiet_moves = quiet_moves_bb.occupied_squares().map(map_to_move); let capture_moves = capture_moves_bb.occupied_squares().map(map_to_move); diff --git a/board/src/move_generator/king.rs b/board/src/move_generator/king.rs index f3ae29a..5f5fcc0 100644 --- a/board/src/move_generator/king.rs +++ b/board/src/move_generator/king.rs @@ -7,7 +7,7 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::{ piece::{Color, Piece, PlacedPiece}, position::BoardSide, - BitBoard, Move, Position, + BitBoard, Move, MoveBuilder, Position, }; move_generator_declaration!(KingMoveGenerator, struct); @@ -56,7 +56,7 @@ impl<'pos> MoveGeneratorInternal for KingMoveGenerator<'pos> { // TODO: Handle checks. Prevent moving a king to a square attacked by a // piece of the opposite color. - let map_to_move = |sq| Move::new(piece, square, sq); + let map_to_move = |sq| MoveBuilder::new(piece, square, sq).build(); let king_side_castle = Self::king_side_castle(position, color); let queen_side_castle = Self::queen_side_castle(position, color); @@ -76,7 +76,7 @@ impl<'pos> MoveGeneratorInternal for KingMoveGenerator<'pos> { #[cfg(test)] mod tests { use super::*; - use crate::{piece::Piece, Position, Square}; + use crate::{piece, piece::Piece, Position, Square}; use std::collections::HashSet; #[test] @@ -96,14 +96,14 @@ mod tests { ); let expected_moves = [ - Move::new(Piece::king(Color::White), Square::E4, Square::D5), - Move::new(Piece::king(Color::White), Square::E4, Square::E5), - Move::new(Piece::king(Color::White), Square::E4, Square::F5), - Move::new(Piece::king(Color::White), Square::E4, Square::F4), - Move::new(Piece::king(Color::White), Square::E4, Square::F3), - Move::new(Piece::king(Color::White), Square::E4, Square::E3), - Move::new(Piece::king(Color::White), Square::E4, Square::D3), - Move::new(Piece::king(Color::White), Square::E4, Square::D4), + MoveBuilder::new(piece!(White King), Square::E4, Square::D5).build(), + MoveBuilder::new(piece!(White King), Square::E4, Square::E5).build(), + MoveBuilder::new(piece!(White King), Square::E4, Square::F5).build(), + MoveBuilder::new(piece!(White King), Square::E4, Square::F4).build(), + MoveBuilder::new(piece!(White King), Square::E4, Square::F3).build(), + MoveBuilder::new(piece!(White King), Square::E4, Square::E3).build(), + MoveBuilder::new(piece!(White King), Square::E4, Square::D3).build(), + MoveBuilder::new(piece!(White King), Square::E4, Square::D4).build(), ]; let mut generated_moves: HashSet = generator.iter().cloned().collect(); @@ -140,9 +140,9 @@ mod tests { ); let expected_moves = [ - Move::new(Piece::king(Color::White), Square::A1, Square::A2), - Move::new(Piece::king(Color::White), Square::A1, Square::B1), - Move::new(Piece::king(Color::White), Square::A1, Square::B2), + MoveBuilder::new(piece!(White King), Square::A1, Square::A2).build(), + MoveBuilder::new(piece!(White King), Square::A1, Square::B1).build(), + MoveBuilder::new(piece!(White King), Square::A1, Square::B2).build(), ]; let mut generated_moves: HashSet = generator.iter().cloned().collect(); diff --git a/board/src/move_generator/knight.rs b/board/src/move_generator/knight.rs index cf50641..2a3c611 100644 --- a/board/src/move_generator/knight.rs +++ b/board/src/move_generator/knight.rs @@ -3,7 +3,7 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::{ piece::{Color, Piece, PlacedPiece}, - BitBoard, Move, Position, + BitBoard, MoveBuilder, Position, }; move_generator_declaration!(KnightMoveGenerator); @@ -21,12 +21,14 @@ impl<'pos> MoveGeneratorInternal for KnightMoveGenerator<'pos> { let quiet_moves_bb = knight_moves & empty_squares; let capture_moves_bb = knight_moves & opposing_pieces; - let quiet_moves = quiet_moves_bb - .occupied_squares() - .map(|to_sq| Move::new(placed_piece.piece(), placed_piece.square(), to_sq)); + let quiet_moves = quiet_moves_bb.occupied_squares().map(|to_sq| { + MoveBuilder::new(placed_piece.piece(), placed_piece.square(), to_sq).build() + }); let capture_moves = capture_moves_bb.occupied_squares().map(|to_sq| { let captured_piece = position.piece_on_square(to_sq).unwrap(); - Move::new(placed_piece.piece(), placed_piece.square(), to_sq).capturing(captured_piece) + MoveBuilder::new(placed_piece.piece(), placed_piece.square(), to_sq) + .capturing(captured_piece) + .build() }); MoveSet::new(placed_piece) @@ -38,7 +40,7 @@ impl<'pos> MoveGeneratorInternal for KnightMoveGenerator<'pos> { #[cfg(test)] mod tests { use super::*; - use crate::Square; + use crate::{piece, Move, Square}; use std::collections::HashSet; #[test] @@ -60,14 +62,14 @@ mod tests { */ let expected_moves = [ - Move::new(Piece::knight(Color::White), Square::E4, Square::C3), - Move::new(Piece::knight(Color::White), Square::E4, Square::D2), - Move::new(Piece::knight(Color::White), Square::E4, Square::F2), - Move::new(Piece::knight(Color::White), Square::E4, Square::G3), - Move::new(Piece::knight(Color::White), Square::E4, Square::C5), - Move::new(Piece::knight(Color::White), Square::E4, Square::D6), - Move::new(Piece::knight(Color::White), Square::E4, Square::G5), - Move::new(Piece::knight(Color::White), Square::E4, Square::F6), + MoveBuilder::new(piece!(White Knight), Square::E4, Square::C3).build(), + MoveBuilder::new(piece!(White Knight), Square::E4, Square::D2).build(), + MoveBuilder::new(piece!(White Knight), Square::E4, Square::F2).build(), + MoveBuilder::new(piece!(White Knight), Square::E4, Square::G3).build(), + MoveBuilder::new(piece!(White Knight), Square::E4, Square::C5).build(), + MoveBuilder::new(piece!(White Knight), Square::E4, Square::D6).build(), + MoveBuilder::new(piece!(White Knight), Square::E4, Square::G5).build(), + MoveBuilder::new(piece!(White Knight), Square::E4, Square::F6).build(), ]; let mut generated_moves: HashSet = generator.iter().cloned().collect(); diff --git a/board/src/move_generator/pawn.rs b/board/src/move_generator/pawn.rs index 9778909..7f193bd 100644 --- a/board/src/move_generator/pawn.rs +++ b/board/src/move_generator/pawn.rs @@ -2,7 +2,7 @@ use crate::{ piece::{Color, Piece, Shape}, - BitBoard, Move, Position, + BitBoard, Move, MoveBuilder, Position, }; enum MoveList { @@ -150,14 +150,14 @@ impl<'pos> PawnMoveGenerator<'pos> { if !(push & empty_squares).is_empty() { let to_sq = push.occupied_squares().next().unwrap(); - let r#move = Move::new(piece, from_sq, to_sq); + let builder = MoveBuilder::new(piece, from_sq, to_sq); if !(push & parameters.promotion_rank).is_empty() { for shape in Shape::promotable() { self.promotion_move_list() - .push(r#move.clone().promoting_to(*shape)); + .push(builder.clone().promoting_to(*shape).build()); } } else { - self.quiet_move_list().push(r#move); + self.quiet_move_list().push(builder.build()); } if !(pawn & parameters.starting_rank).is_empty() { @@ -165,7 +165,7 @@ impl<'pos> PawnMoveGenerator<'pos> { if !(push & empty_squares).is_empty() { let to_sq = push.occupied_squares().next().unwrap(); self.quiet_move_list() - .push(Move::new(piece, from_sq, to_sq)); + .push(MoveBuilder::new(piece, from_sq, to_sq).build()); } } } @@ -178,14 +178,14 @@ impl<'pos> PawnMoveGenerator<'pos> { let to_sq = attack.occupied_squares().next().unwrap(); let captured_piece = self.position.piece_on_square(to_sq).unwrap(); - let r#move = Move::new(piece, from_sq, to_sq).capturing(captured_piece); + let builder = MoveBuilder::new(piece, from_sq, to_sq).capturing(captured_piece); if !(attack & parameters.promotion_rank).is_empty() { for shape in Shape::promotable() { self.capture_move_list() - .push(r#move.clone().promoting_to(*shape)); + .push(builder.clone().promoting_to(*shape).build()); } } else { - self.capture_move_list().push(r#move); + self.capture_move_list().push(builder.build()); } } } @@ -247,25 +247,22 @@ mod tests { use super::*; use crate::piece::PlacedPiece; use crate::position::DiagramFormatter; - use crate::{Position, Square}; + use crate::{piece, Position, Square}; use std::collections::HashSet; #[test] fn one_2square_push() { let mut pos = Position::empty(); - pos.place_piece(Piece::pawn(Color::White), Square::E2) + pos.place_piece(piece!(White Pawn), Square::E2) .expect("Failed to place pawn on e2"); let generator = PawnMoveGenerator::new(&pos, Color::White); - let expected_moves = HashSet::from_iter( - [ - Move::new(Piece::pawn(Color::White), Square::E2, Square::E3), - Move::new(Piece::pawn(Color::White), Square::E2, Square::E4), - ] - .into_iter(), - ); + let expected_moves = HashSet::from_iter([ + MoveBuilder::new(piece!(White Pawn), Square::E2, Square::E3).build(), + MoveBuilder::new(piece!(White Pawn), Square::E2, Square::E4).build(), + ]); let generated_moves: HashSet = generator.collect(); @@ -281,9 +278,12 @@ mod tests { let generator = PawnMoveGenerator::new(&pos, Color::White); - let expected_moves = HashSet::from_iter( - [Move::new(Piece::pawn(Color::White), Square::E3, Square::E4)].into_iter(), - ); + let expected_moves = HashSet::from_iter([MoveBuilder::new( + Piece::pawn(Color::White), + Square::E3, + Square::E4, + ) + .build()]); let generated_moves: HashSet = generator.collect(); @@ -303,9 +303,12 @@ mod tests { let generator = PawnMoveGenerator::new(&pos, Color::White); - let expected_moves = HashSet::from_iter( - [Move::new(Piece::pawn(Color::White), Square::E2, Square::E3)].into_iter(), - ); + let expected_moves = HashSet::from_iter([MoveBuilder::new( + Piece::pawn(Color::White), + Square::E2, + Square::E3, + ) + .build()]); let generated_moves: HashSet = generator.collect(); @@ -344,11 +347,13 @@ mod tests { let generator = PawnMoveGenerator::new(&pos, Color::White); - let expected_moves = HashSet::from_iter( - [Move::new(Piece::pawn(Color::White), Square::E4, Square::D5) - .capturing(PlacedPiece::new(Piece::knight(Color::Black), Square::D5))] - .into_iter(), - ); + let expected_moves = HashSet::from_iter([MoveBuilder::new( + Piece::pawn(Color::White), + Square::E4, + Square::D5, + ) + .capturing(piece!(Black Knight on D5)) + .build()]); let generated_moves: HashSet = generator.collect(); @@ -371,15 +376,14 @@ mod tests { let generator = PawnMoveGenerator::new(&pos, Color::White); - let expected_moves = HashSet::from_iter( - [ - Move::new(Piece::pawn(Color::White), Square::E4, Square::D5) - .capturing(PlacedPiece::new(Piece::knight(Color::Black), Square::D5)), - Move::new(Piece::pawn(Color::White), Square::E4, Square::F5) - .capturing(PlacedPiece::new(Piece::queen(Color::Black), Square::F5)), - ] - .into_iter(), - ); + let expected_moves = HashSet::from_iter([ + MoveBuilder::new(piece!(White Pawn), Square::E4, Square::D5) + .capturing(piece!(Black Knight on D5)) + .build(), + MoveBuilder::new(piece!(White Pawn), Square::E4, Square::F5) + .capturing(piece!(Black Queen on F5)) + .build(), + ]); let generated_moves: HashSet = generator.collect(); diff --git a/board/src/move_generator/queen.rs b/board/src/move_generator/queen.rs index 1063653..60667ea 100644 --- a/board/src/move_generator/queen.rs +++ b/board/src/move_generator/queen.rs @@ -4,7 +4,7 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::{ piece::{Color, Piece, PlacedPiece}, square::Direction, - BitBoard, Move, Position, + BitBoard, MoveBuilder, Position, }; move_generator_declaration!(ClassicalMoveGenerator); @@ -53,7 +53,7 @@ impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> { let quiet_moves_bb = all_moves & (empty_squares | !friendly_pieces); let capture_moves_bb = all_moves & opposing_pieces; - let map_to_move = |sq| Move::new(piece, square, sq); + let map_to_move = |sq| MoveBuilder::new(piece, square, sq).build(); let quiet_moves = quiet_moves_bb.occupied_squares().map(map_to_move); let capture_moves = capture_moves_bb.occupied_squares().map(map_to_move); diff --git a/board/src/move_generator/rook.rs b/board/src/move_generator/rook.rs index 99f1823..9a61d9b 100644 --- a/board/src/move_generator/rook.rs +++ b/board/src/move_generator/rook.rs @@ -4,7 +4,7 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::{ piece::{Color, Piece, PlacedPiece}, square::Direction, - BitBoard, Move, Position, + BitBoard, MoveBuilder, Position, }; move_generator_declaration!(ClassicalMoveGenerator); @@ -49,7 +49,7 @@ impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> { let quiet_moves_bb = all_moves & (empty_squares | !friendly_pieces); let capture_moves_bb = all_moves & opposing_pieces; - let map_to_move = |sq| Move::new(piece, square, sq); + let map_to_move = |sq| MoveBuilder::new(piece, square, sq).build(); let quiet_moves = quiet_moves_bb.occupied_squares().map(map_to_move); let capture_moves = capture_moves_bb.occupied_squares().map(map_to_move); diff --git a/board/src/position/position.rs b/board/src/position/position.rs index fb1b402..0356e94 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -155,8 +155,8 @@ impl Position { Ok(()) } - pub fn move_generator(&self, color: Color) -> Moves { - Moves::new(self, color) + pub fn moves(&self) -> Moves { + Moves::new(self, self.color_to_move) } /// Return a BitBoard representing the set of squares containing a piece. From 3ba6722697da9aca7fc5d8b3f6b5a6fb17f78f1c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 17 Jan 2024 08:44:27 -0800 Subject: [PATCH 022/423] [board] Implement a bunch of sight tests --- board/src/position/position.rs | 6 ++ board/src/sight.rs | 105 +++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/board/src/position/position.rs b/board/src/position/position.rs index 0356e94..dcb4f7f 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -236,6 +236,12 @@ impl Position { .next() .unwrap(); sight_of_opposing_player.is_set(king_square) +} + +#[cfg(test)] +impl Position { + pub(crate) fn test_set_en_passant_square(&mut self, square: Square) { + self.en_passant_square = Some(square); } } diff --git a/board/src/sight.rs b/board/src/sight.rs index 33e716e..46d88b4 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -153,3 +153,108 @@ impl PlacedPiece { BitBoard::king_moves(self.square()) & !position.friendly_pieces() } } + +#[cfg(test)] +mod tests { + macro_rules! sight_test { + ($test_name:ident, $position:expr, $piece:expr, $bitboard:expr) => { + #[test] + fn $test_name() { + let pos = $position; + let pp = $piece; + let sight = pp.sight_in_position(&pos); + + assert_eq!(sight, $bitboard); + } + }; + ($test_name:ident, $piece:expr, $bitboard:expr) => { + sight_test! {$test_name, $crate::Position::empty(), $piece, $bitboard} + }; + } + + mod pawn { + use crate::{position, sight::Sight, BitBoard, Position, Square}; + + sight_test!(e4_pawn, piece!(White Pawn on E4), bitboard!(D5, F5)); + + sight_test!( + e4_pawn_one_blocker, + position![ + White Bishop on D5, + White Pawn on E4, + ], + piece!(White Pawn on E4), + bitboard!(F5) + ); + + #[test] + fn e4_pawn_two_blocker() { + let pos = position!( + White Bishop on D5, + White Queen on F5, + White Pawn on E4, + ); + let pp = piece!(White Pawn on E4); + let sight = pp.sight_in_position(&pos); + + assert_eq!(sight, BitBoard::empty()); + } + + #[test] + fn e4_pawn_capturable() { + let pos = position!( + Black Bishop on D5, + White Queen on F5, + White Pawn on E4, + ); + let pp = piece!(White Pawn on E4); + let sight = pp.sight_in_position(&pos); + + assert_eq!(sight, bitboard!(D5)); + } + + #[test] + fn e5_en_passant() { + let mut pos = position!( + White Pawn on E5, + Black Pawn on D5, + ); + pos.test_set_en_passant_square(Square::D6); + let pp = piece!(White Pawn on E5); + let sight = pp.sight_in_position(&pos); + + assert_eq!(sight, bitboard!(D6, F6)); + } + } + + #[macro_use] + mod knight { + use crate::{sight::Sight, Position}; + + sight_test!( + f6_knight, + piece!(Black Knight on F6), + bitboard!(H7, G8, E8, D7, D5, E4, G4, H5) + ); + } + + mod bishop { + use crate::sight::Sight; + + sight_test!( + c2_bishop, + piece!(Black Bishop on C2), + bitboard!(D1, B3, A4, B1, D3, E4, F5, G6, H7) + ); + } + + mod rook { + use crate::sight::Sight; + + sight_test!( + g3_rook, + piece!(White Rook on G3), + bitboard!(G1, G2, G4, G5, G6, G7, G8, A3, B3, C3, D3, E3, F3, H3) + ); + } +} From ac5d8cc9d519cdfc82ced7382c603b8662283ce3 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 17 Jan 2024 08:44:51 -0800 Subject: [PATCH 023/423] [board] Implement a move generator test for a single king --- board/src/move_generator/move_generator.rs | 37 ++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/board/src/move_generator/move_generator.rs b/board/src/move_generator/move_generator.rs index 6fa44b0..689e47d 100644 --- a/board/src/move_generator/move_generator.rs +++ b/board/src/move_generator/move_generator.rs @@ -40,3 +40,40 @@ impl<'a> Moves<'a> { .cloned() } } + +#[cfg(test)] +mod tests { + use crate::{piece, position, r#move::AlgebraicMoveFormatter, Move, MoveBuilder, Square}; + use std::collections::HashSet; + + #[test] + fn one_king() { + let pos = position![ + White King on D3, + Black King on H6, + ]; + + let expected_moves = HashSet::from_iter([ + MoveBuilder::new(piece!(White King), Square::D3, Square::D4).build(), + MoveBuilder::new(piece!(White King), Square::D3, Square::E4).build(), + MoveBuilder::new(piece!(White King), Square::D3, Square::E3).build(), + MoveBuilder::new(piece!(White King), Square::D3, Square::E2).build(), + MoveBuilder::new(piece!(White King), Square::D3, Square::D2).build(), + MoveBuilder::new(piece!(White King), Square::D3, Square::C2).build(), + MoveBuilder::new(piece!(White King), Square::D3, Square::C3).build(), + MoveBuilder::new(piece!(White King), Square::D3, Square::C4).build(), + ]); + + let generated_moves: HashSet = pos.moves().iter().collect(); + + assert_eq!( + generated_moves, + expected_moves, + "{:?}", + generated_moves + .symmetric_difference(&expected_moves) + .map(|m| format!("{}", AlgebraicMoveFormatter::new(&m, &pos))) + .collect::>() + ); + } +} From 71e93925b9f0543c3d684acc94c9893001ded9fa Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 17 Jan 2024 08:46:41 -0800 Subject: [PATCH 024/423] [board] Clean up a handful of things in position.rs - Remove unused std::fmt::Write import - Refer to the piece! macro with $crate (I didn't know this was legal??) - Remove the doc comments on Position::flags - Remove {set,clear}_player_has_right_to_castle_flag - Remove an extraneous local variable from Position::place_piece() --- board/src/position/position.rs | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/board/src/position/position.rs b/board/src/position/position.rs index dcb4f7f..a307b71 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -9,7 +9,6 @@ use crate::{ }; use std::cell::OnceCell; use std::fmt; -use std::fmt::Write; /// A lateral side of the board relative to the player. Kingside is the side the /// player's king starts on. Queenside is the side of the board the player's @@ -23,7 +22,7 @@ pub(crate) enum BoardSide { macro_rules! position { [$($color:ident $shape:ident on $square:ident,)*] => { $crate::Position::from_iter([ - $(piece!($color $shape on $square)),* + $($crate::piece!($color $shape on $square)),* ].into_iter()) }; } @@ -31,14 +30,6 @@ macro_rules! position { #[derive(Clone, Eq, PartialEq)] pub struct Position { color_to_move: Color, - - /// Position flags indicating various bits of game state. The flags are as - /// follows: - /// - /// 0. white can castle king-side - /// 1. white can castle queen-side - /// 2. black can castle king-side - /// 3. black can castle queen-side flags: Flags, /// Composite bitboards for all the pieces of a particular color. @@ -131,15 +122,6 @@ impl Position { self.flags.player_has_right_to_castle(color, side) } - fn set_player_has_right_to_castle_flag(&mut self, color: Color, side: BoardSide) { - self.flags.set_player_has_right_to_castle_flag(color, side); - } - - fn clear_player_has_right_to_castle_flag(&mut self, color: Color, side: BoardSide) { - self.flags - .clear_player_has_right_to_castle_flag(color, side); - } - pub fn place_piece(&mut self, piece: Piece, square: Square) -> Result<(), PiecePlacementError> { let type_bb = self.bitboard_for_piece_mut(piece); @@ -149,8 +131,8 @@ impl Position { type_bb.set_square(square); - let color_bb = &mut self.bitboard_for_color_mut(piece.color()); - color_bb.set_square(square); + self.bitboard_for_color_mut(piece.color()) + .set_square(square); Ok(()) } From e2f8faad3d6931ccae9d79a066cf9260fde991a2 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 17 Jan 2024 08:47:06 -0800 Subject: [PATCH 025/423] [board] Implement Default for move::move_formatter::Style --- board/src/move.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/board/src/move.rs b/board/src/move.rs index 2f74331..de8ed68 100644 --- a/board/src/move.rs +++ b/board/src/move.rs @@ -228,6 +228,12 @@ mod move_formatter { Long, } + impl Default for Style { + fn default() -> Self { + Style::Long + } + } + pub(crate) struct AlgebraicMoveFormatter<'m, 'pos> { position: &'pos Position, r#move: &'m Move, @@ -242,7 +248,7 @@ mod move_formatter { AlgebraicMoveFormatter { position, r#move: mv, - style: Style::Short, + style: Style::default(), } } From 771081a5dfc22c25fda9b5eb26413bb1953eba78 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 17 Jan 2024 08:47:35 -0800 Subject: [PATCH 026/423] [board] Derive Clone and Debug for MoveBuilder --- board/src/move.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/board/src/move.rs b/board/src/move.rs index de8ed68..d65dff8 100644 --- a/board/src/move.rs +++ b/board/src/move.rs @@ -155,6 +155,7 @@ impl fmt::Debug for Move { } } +#[derive(Clone, Debug)] pub struct MoveBuilder { piece: Piece, from: Square, From 4fe9be036d9615108df1e64b935ce48e282ba02c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 17 Jan 2024 08:47:48 -0800 Subject: [PATCH 027/423] [board] Reexport AlgebraicMoveFormatter from the move module --- board/src/move.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/board/src/move.rs b/board/src/move.rs index d65dff8..64c7e90 100644 --- a/board/src/move.rs +++ b/board/src/move.rs @@ -8,6 +8,8 @@ use crate::{ }; use std::fmt; +pub(crate) use move_formatter::AlgebraicMoveFormatter; + #[repr(u16)] #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum Castle { From f337b8053d2e1b567d5f42f21bc4b00f3fcdd2d2 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 17 Jan 2024 08:48:23 -0800 Subject: [PATCH 028/423] [board] Break getting the square the king is on into a separate helper method Implement Position::king_square() and use it in is_king_in_check() --- board/src/position/position.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/board/src/position/position.rs b/board/src/position/position.rs index a307b71..d45fd1d 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -212,12 +212,15 @@ impl Position { pub(crate) fn is_king_in_check(&self) -> bool { let sight_of_opposing_player = self.sight_of_player(self.color_to_move.other()); - let king_square = self - .bitboard_for_piece(Piece::king(self.color_to_move)) + sight_of_opposing_player.is_set(self.king_square()) + } + + fn king_square(&self) -> Square { + self.bitboard_for_piece(Piece::king(self.color_to_move)) .occupied_squares() .next() - .unwrap(); - sight_of_opposing_player.is_set(king_square) + .unwrap() + } } #[cfg(test)] From e56b3259e2f0dc1f8aebb176472b6e9b08d85fd9 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 17 Jan 2024 08:48:46 -0800 Subject: [PATCH 029/423] [board] Implement FromIterator for BitBoard --- board/src/bitboard/bitboard.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/board/src/bitboard/bitboard.rs b/board/src/bitboard/bitboard.rs index 6fee968..43f4103 100644 --- a/board/src/bitboard/bitboard.rs +++ b/board/src/bitboard/bitboard.rs @@ -1,7 +1,7 @@ // Eryn Wells use super::library::{library, FILES, RANKS}; -use super::{LeadingBitScanner, TrailingBitScanner}; +use super::LeadingBitScanner; use crate::{square::Direction, Square}; use std::fmt; use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Not}; @@ -87,6 +87,18 @@ impl From for BitBoard { } } +impl FromIterator for BitBoard { + fn from_iter>(iter: T) -> Self { + let mut builder = BitBoardBuilder::empty(); + + for sq in iter { + builder = builder.square(sq) + } + + builder.build() + } +} + impl fmt::Binary for BitBoard { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // Delegate to u64's implementation of Binary. From 2269df2ed955063c936a87b8b653204b15c04402 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 19 Jan 2024 17:51:57 -0800 Subject: [PATCH 030/423] [board] Move piece! and position! macros to a new macros.rs module --- board/src/lib.rs | 3 ++- board/src/macros.rs | 20 ++++++++++++++++++++ board/src/piece.rs | 10 ---------- board/src/position/mod.rs | 1 - board/src/position/position.rs | 9 --------- 5 files changed, 22 insertions(+), 21 deletions(-) create mode 100644 board/src/macros.rs diff --git a/board/src/lib.rs b/board/src/lib.rs index 969bd6f..8fd104a 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -4,8 +4,9 @@ mod bitboard; mod display; mod r#move; -mod move_generator; #[macro_use] +mod macros; +mod move_generator; pub mod piece; #[macro_use] pub mod position; diff --git a/board/src/macros.rs b/board/src/macros.rs new file mode 100644 index 0000000..0b88d0f --- /dev/null +++ b/board/src/macros.rs @@ -0,0 +1,20 @@ +// Eryn Wells + +#[macro_export] +macro_rules! piece { + ($color:ident $shape:ident) => { + $crate::piece::Piece::new($crate::piece::Color::$color, $crate::piece::Shape::$shape) + }; + ($color:ident $shape:ident on $square:ident) => { + $crate::piece::PlacedPiece::new(piece!($color $shape), $crate::square::Square::$square) + } +} + +#[macro_export] +macro_rules! position { + [$($color:ident $shape:ident on $square:ident),* $(,)?] => { + $crate::PositionBuilder::new() + $(.place_piece(piece!($color $shape on $square)))* + .build() + }; +} diff --git a/board/src/piece.rs b/board/src/piece.rs index e3614f4..0b72234 100644 --- a/board/src/piece.rs +++ b/board/src/piece.rs @@ -121,16 +121,6 @@ pub enum PiecePlacementError { ExistsOnSquare, } -#[macro_export] -macro_rules! piece { - ($color:ident $shape:ident) => { - $crate::piece::Piece::new($crate::piece::Color::$color, $crate::piece::Shape::$shape) - }; - ($color:ident $shape:ident on $square:ident) => { - $crate::piece::PlacedPiece::new(piece!($color $shape), $crate::square::Square::$square) - } -} - #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct Piece { color: Color, diff --git a/board/src/position/mod.rs b/board/src/position/mod.rs index 6899faa..650959d 100644 --- a/board/src/position/mod.rs +++ b/board/src/position/mod.rs @@ -3,7 +3,6 @@ mod diagram_formatter; mod flags; mod pieces; -#[macro_use] mod position; pub use diagram_formatter::DiagramFormatter; diff --git a/board/src/position/position.rs b/board/src/position/position.rs index d45fd1d..13e9ffa 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -18,15 +18,6 @@ pub(crate) enum BoardSide { Queen, } -#[macro_export] -macro_rules! position { - [$($color:ident $shape:ident on $square:ident,)*] => { - $crate::Position::from_iter([ - $($crate::piece!($color $shape on $square)),* - ].into_iter()) - }; -} - #[derive(Clone, Eq, PartialEq)] pub struct Position { color_to_move: Color, From c413db0bf14d4e702c316deb58e52025d28095a3 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 19 Jan 2024 17:59:34 -0800 Subject: [PATCH 031/423] [board] Clean up Display and Debug implementations on BitBoard, Flags, Position, and PlacedPiece --- board/src/bitboard/bitboard.rs | 4 +--- board/src/piece.rs | 6 ++++++ board/src/position/flags.rs | 9 ++++++++- board/src/position/position.rs | 26 +------------------------- 4 files changed, 16 insertions(+), 29 deletions(-) diff --git a/board/src/bitboard/bitboard.rs b/board/src/bitboard/bitboard.rs index 43f4103..9c2ccdb 100644 --- a/board/src/bitboard/bitboard.rs +++ b/board/src/bitboard/bitboard.rs @@ -145,9 +145,7 @@ impl fmt::Display for BitBoard { impl fmt::Debug for BitBoard { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - f.debug_tuple("BitBoard") - .field(&format_args!("{:064b}", self.0)) - .finish() + write!(f, "BitBoard({:064b})", self.0) } } diff --git a/board/src/piece.rs b/board/src/piece.rs index 0b72234..de0d064 100644 --- a/board/src/piece.rs +++ b/board/src/piece.rs @@ -240,6 +240,12 @@ impl PlacedPiece { } } +impl fmt::Display for PlacedPiece { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}{}", self.piece, self.square) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/board/src/position/flags.rs b/board/src/position/flags.rs index 43680cf..f434f21 100644 --- a/board/src/position/flags.rs +++ b/board/src/position/flags.rs @@ -2,8 +2,9 @@ use super::position::BoardSide; use crate::piece::Color; +use std::fmt; -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Copy, Eq, Hash, PartialEq)] pub(super) struct Flags(u8); impl Flags { @@ -25,6 +26,12 @@ impl Flags { } } +impl fmt::Debug for Flags { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Flags({:08b})", self.0) + } +} + impl Default for Flags { fn default() -> Self { Flags(0b00001111) diff --git a/board/src/position/position.rs b/board/src/position/position.rs index 13e9ffa..1be30e0 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -8,7 +8,6 @@ use crate::{ BitBoard, Square, }; use std::cell::OnceCell; -use std::fmt; /// A lateral side of the board relative to the player. Kingside is the side the /// player's king starts on. Queenside is the side of the board the player's @@ -18,7 +17,7 @@ pub(crate) enum BoardSide { Queen, } -#[derive(Clone, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Position { color_to_move: Color, flags: Flags, @@ -238,29 +237,6 @@ impl FromIterator for Position { } } -impl fmt::Debug for Position { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Position {{\n")?; - write!(f, " color_to_move: {:?},\n", self.color_to_move)?; - - write!(f, " pieces_per_color: [\n")?; - for bb in self.pieces_per_color { - write!(f, " {bb:?},\n")?; - } - write!(f, " ],\n")?; - - write!(f, " pieces_per_type: [\n")?; - for color_bbs in self.pieces_per_type { - write!(f, " [\n")?; - for bb in color_bbs { - write!(f, " {bb:?},\n")?; - } - write!(f, " ],\n")?; - } - write!(f, " ],\n}}") - } -} - #[cfg(test)] mod tests { use super::*; From 939fac80d7251b5445182268cddcff14b399c87e Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 19 Jan 2024 18:02:10 -0800 Subject: [PATCH 032/423] [board] Implement Default for BitBoard and Color --- board/src/bitboard/bitboard.rs | 6 ++++++ board/src/piece.rs | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/board/src/bitboard/bitboard.rs b/board/src/bitboard/bitboard.rs index 9c2ccdb..2955db9 100644 --- a/board/src/bitboard/bitboard.rs +++ b/board/src/bitboard/bitboard.rs @@ -81,6 +81,12 @@ impl BitBoard { } } +impl Default for BitBoard { + fn default() -> Self { + BitBoard::empty() + } +} + impl From for BitBoard { fn from(value: Square) -> Self { BitBoard(1 << value as u64) diff --git a/board/src/piece.rs b/board/src/piece.rs index de0d064..fba6113 100644 --- a/board/src/piece.rs +++ b/board/src/piece.rs @@ -26,6 +26,12 @@ impl Color { } } +impl Default for Color { + fn default() -> Self { + Color::White + } +} + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum Shape { Pawn = 0, From 24cce95a888db46a5259c26bf95fe53288739ccd Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 19 Jan 2024 18:08:41 -0800 Subject: [PATCH 033/423] [board] Implement a PositionBuilder; refactor piece bitboards into a PieceBitBoards struct This makes Position immutable, even if declared mut. I think this will also make it easier to build positions going forward. Moving to a PieceBitBoards struct also required a lot of places that return owned BitBoards to return refs. This is basically fine except for adding a few more & here and there. This was a huge refactoring project. All the move generators and a bunch of BitBoard stuff needed to be updated. --- board/src/bitboard/bitboard.rs | 6 +- board/src/bitboard/library.rs | 6 +- board/src/bitboard/shifts.rs | 22 +-- board/src/lib.rs | 5 +- board/src/move_generator/bishop.rs | 53 +++---- board/src/move_generator/king.rs | 27 ++-- board/src/move_generator/knight.rs | 8 +- board/src/move_generator/pawn.rs | 90 +++++------- board/src/move_generator/queen.rs | 79 ++++------- board/src/move_generator/rook.rs | 61 ++------ board/src/piece.rs | 11 +- board/src/position/builders.rs | 83 +++++++++++ board/src/position/diagram_formatter.rs | 12 +- board/src/position/mod.rs | 3 + board/src/position/piece_sets.rs | 149 ++++++++++++++++++++ board/src/position/pieces.rs | 58 ++++---- board/src/position/position.rs | 180 +++++++++--------------- 17 files changed, 472 insertions(+), 381 deletions(-) create mode 100644 board/src/position/builders.rs create mode 100644 board/src/position/piece_sets.rs diff --git a/board/src/bitboard/bitboard.rs b/board/src/bitboard/bitboard.rs index 2955db9..070f43f 100644 --- a/board/src/bitboard/bitboard.rs +++ b/board/src/bitboard/bitboard.rs @@ -53,7 +53,7 @@ impl BitBoard { } pub fn is_set(self, sq: Square) -> bool { - !(self & sq.into()).is_empty() + !(self & &sq.into()).is_empty() } pub fn set_square(&mut self, sq: Square) { @@ -180,11 +180,15 @@ macro_rules! assign_op { } infix_op!(BitAnd, bitand, BitBoard, BitBoard); +infix_op!(BitAnd, bitand, BitBoard, &BitBoard); +infix_op!(BitAnd, bitand, &BitBoard, &BitBoard); assign_op!(BitAndAssign, bitand_assign, BitBoard); assign_op!(BitOrAssign, bitor_assign, BitBoard); infix_op!(BitOr, bitor, BitBoard, BitBoard); +infix_op!(BitOr, bitor, BitBoard, &BitBoard); +infix_op!(BitOr, bitor, &BitBoard, &BitBoard); impl Not for BitBoard { type Output = BitBoard; diff --git a/board/src/bitboard/library.rs b/board/src/bitboard/library.rs index 697d12a..fe2daa0 100644 --- a/board/src/bitboard/library.rs +++ b/board/src/bitboard/library.rs @@ -165,13 +165,13 @@ impl MoveLibrary { } #[inline] - fn generate_ray(sq: BitBoard, shift: fn(BitBoard) -> BitBoard) -> BitBoard { + fn generate_ray(sq: BitBoard, shift: fn(&BitBoard) -> BitBoard) -> BitBoard { let mut ray = BitBoard::empty(); - let mut iter = shift(sq); + let mut iter = shift(&sq); while !iter.is_empty() { ray |= iter; - iter = shift(iter); + iter = shift(&iter); } ray diff --git a/board/src/bitboard/shifts.rs b/board/src/bitboard/shifts.rs index 8866d4c..2370306 100644 --- a/board/src/bitboard/shifts.rs +++ b/board/src/bitboard/shifts.rs @@ -7,58 +7,58 @@ impl BitBoard { const NOT_H_FILE: u64 = 0x7f7f7f7f7f7f7f7f; #[inline] - pub fn shift_north(self, n: u8) -> BitBoard { + pub fn shift_north(&self, n: u8) -> BitBoard { BitBoard(self.0 << (8 * n)) } #[inline] - pub fn shift_north_one(self) -> BitBoard { + pub fn shift_north_one(&self) -> BitBoard { BitBoard(self.0 << 8) } #[inline] - pub fn shift_north_east_one(self) -> BitBoard { + pub fn shift_north_east_one(&self) -> BitBoard { BitBoard(self.0 << 9 & BitBoard::NOT_A_FILE) } #[inline] - pub fn shift_east(self, n: u8) -> BitBoard { + pub fn shift_east(&self, n: u8) -> BitBoard { // TODO: Implement a bounds check here. BitBoard(self.0 << n) } #[inline] - pub fn shift_east_one(self) -> BitBoard { + pub fn shift_east_one(&self) -> BitBoard { BitBoard(self.0 << 1 & BitBoard::NOT_A_FILE) } #[inline] - pub fn shift_south_east_one(self) -> BitBoard { + pub fn shift_south_east_one(&self) -> BitBoard { BitBoard(self.0 >> 7 & BitBoard::NOT_A_FILE) } #[inline] - pub fn shift_south(self, n: u8) -> BitBoard { + pub fn shift_south(&self, n: u8) -> BitBoard { BitBoard(self.0 >> (8 * n)) } #[inline] - pub fn shift_south_one(self) -> BitBoard { + pub fn shift_south_one(&self) -> BitBoard { BitBoard(self.0 >> 8) } #[inline] - pub fn shift_south_west_one(self) -> BitBoard { + pub fn shift_south_west_one(&self) -> BitBoard { BitBoard(self.0 >> 9 & BitBoard::NOT_H_FILE) } #[inline] - pub fn shift_west_one(self) -> BitBoard { + pub fn shift_west_one(&self) -> BitBoard { BitBoard(self.0 >> 1 & BitBoard::NOT_H_FILE) } #[inline] - pub fn shift_north_west_one(self) -> BitBoard { + pub fn shift_north_west_one(&self) -> BitBoard { BitBoard(self.0 << 7 & BitBoard::NOT_H_FILE) } } diff --git a/board/src/lib.rs b/board/src/lib.rs index 8fd104a..0585d6f 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -13,8 +13,9 @@ pub mod position; mod sight; mod square; -pub use position::Position; -pub use r#move::{Move, MoveBuilder}; +pub use piece::{Color, Piece}; +pub use position::{Position, PositionBuilder}; +pub use r#move::{MakeMoveError, Move, MoveBuilder}; pub use square::{File, Rank, Square}; pub(crate) use bitboard::BitBoard; diff --git a/board/src/move_generator/bishop.rs b/board/src/move_generator/bishop.rs index 5dfd632..f4f7f9f 100644 --- a/board/src/move_generator/bishop.rs +++ b/board/src/move_generator/bishop.rs @@ -21,8 +21,7 @@ impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> { let blockers = position.occupied_squares(); let empty_squares = !blockers; - let friendly_pieces = position.bitboard_for_color(color); - let opposing_pieces = position.bitboard_for_color(color.other()); + let (friendly_pieces, opposing_pieces) = position.all_pieces(); let mut all_moves = BitBoard::empty(); @@ -63,20 +62,18 @@ impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> { mod tests { use super::*; use crate::{ + piece, piece::{Color, Piece}, + position, position::DiagramFormatter, BitBoard, Position, Square, }; #[test] fn classical_single_bishop_bitboard() { - let mut pos = Position::empty(); - [(Piece::bishop(Color::White), Square::A1)] - .into_iter() - .for_each(|(p, sq)| { - pos.place_piece(p, sq) - .expect(&format!("Unable to place {:?} on {}", &p, &sq)) - }); + let pos = position![ + White Bishop on A1, + ]; let generator = ClassicalMoveGenerator::new(&pos, Color::White); @@ -91,16 +88,10 @@ mod tests { /// Test that a bishop can move up to, but not onto, a friendly piece. #[test] fn classical_single_bishop_with_same_color_blocker_bitboard() { - let mut pos = Position::empty(); - [ - (Piece::bishop(Color::White), Square::A1), - (Piece::knight(Color::White), Square::E5), - ] - .into_iter() - .for_each(|(p, sq)| { - pos.place_piece(p, sq) - .expect(&format!("Unable to place {} on {}", p, sq)) - }); + let pos = position![ + White Bishop on A1, + White Knight on E5, + ]; println!("{}", DiagramFormatter::new(&pos)); @@ -117,16 +108,10 @@ mod tests { /// Test that a rook can move up to, and then capture, an enemy piece. #[test] fn classical_single_bishop_with_opposing_color_blocker_bitboard() { - let mut pos = Position::empty(); - [ - (Piece::bishop(Color::White), Square::A1), - (Piece::knight(Color::Black), Square::C3), - ] - .into_iter() - .for_each(|(p, sq)| { - pos.place_piece(p, sq) - .expect(&format!("Unable to place {} on {}", p, sq)) - }); + let mut pos = position![ + White Bishop on A1, + Black Knight on C3, + ]; let generator = ClassicalMoveGenerator::new(&pos, Color::White); @@ -140,13 +125,9 @@ mod tests { #[test] fn classical_single_bishop_in_center() { - let mut pos = Position::empty(); - [(Piece::bishop(Color::White), Square::E4)] - .into_iter() - .for_each(|(p, sq)| { - pos.place_piece(p, sq) - .expect(&format!("Unable to place {} on {}", p, sq)) - }); + let mut pos = position![ + White Bishop on E4, + ]; println!("{}", DiagramFormatter::new(&pos)); diff --git a/board/src/move_generator/king.rs b/board/src/move_generator/king.rs index 5f5fcc0..be0749b 100644 --- a/board/src/move_generator/king.rs +++ b/board/src/move_generator/king.rs @@ -76,23 +76,18 @@ impl<'pos> MoveGeneratorInternal for KingMoveGenerator<'pos> { #[cfg(test)] mod tests { use super::*; - use crate::{piece, piece::Piece, Position, Square}; + use crate::Square; use std::collections::HashSet; #[test] fn one_king() { - let mut pos = Position::empty(); - pos.place_piece(Piece::king(Color::White), Square::E4) - .expect("Failed to place king on e4"); + let pos = position![White King on E4]; let generator = KingMoveGenerator::new(&pos, Color::White); - let bb = generator.bitboard(); assert_eq!( - bb, - BitBoard::new( - 0b00000000_00000000_00000000_00111000_00101000_00111000_00000000_00000000 - ) + generator.bitboard(), + bitboard![E5, F5, F4, F3, E3, D3, D4, D5] ); let expected_moves = [ @@ -125,18 +120,16 @@ mod tests { #[test] fn one_king_corner() { - let mut pos = Position::empty(); - pos.place_piece(Piece::king(Color::White), Square::A1) - .expect("Failed to place king on a1"); + let pos = position![White King on A1]; let generator = KingMoveGenerator::new(&pos, Color::White); - let bb = generator.bitboard(); + let generated_bitboard = generator.bitboard(); + let expected_bitboard = bitboard![A2, B2, B1]; assert_eq!( - bb, - BitBoard::new( - 0b00000000_00000000_00000000_00000000_00000000_00000000_00000011_00000010 - ) + generator.bitboard(), + bitboard![A2, B2, B1], + "Generated:\n{generated_bitboard}\nExpected:\n{expected_bitboard}" ); let expected_moves = [ diff --git a/board/src/move_generator/knight.rs b/board/src/move_generator/knight.rs index 2a3c611..3116b1e 100644 --- a/board/src/move_generator/knight.rs +++ b/board/src/move_generator/knight.rs @@ -40,14 +40,14 @@ impl<'pos> MoveGeneratorInternal for KnightMoveGenerator<'pos> { #[cfg(test)] mod tests { use super::*; - use crate::{piece, Move, Square}; + use crate::{piece, position, Move, Square}; use std::collections::HashSet; #[test] fn one_knight() { - let mut pos = Position::empty(); - pos.place_piece(Piece::knight(Color::White), Square::E4) - .expect("Failed to place knight on e4"); + let pos = position![ + White Knight on E4, + ]; let generator = KnightMoveGenerator::new(&pos, Color::White); diff --git a/board/src/move_generator/pawn.rs b/board/src/move_generator/pawn.rs index 7f193bd..7f77e15 100644 --- a/board/src/move_generator/pawn.rs +++ b/board/src/move_generator/pawn.rs @@ -17,9 +17,9 @@ struct MoveIterator(usize, usize); struct MoveGenerationParameters { starting_rank: BitBoard, promotion_rank: BitBoard, - push_shift: fn(BitBoard) -> BitBoard, - left_capture_shift: fn(BitBoard) -> BitBoard, - right_capture_shift: fn(BitBoard) -> BitBoard, + push_shift: fn(&BitBoard) -> BitBoard, + left_capture_shift: fn(&BitBoard) -> BitBoard, + right_capture_shift: fn(&BitBoard) -> BitBoard, } pub(super) struct PawnMoveGenerator<'pos> { @@ -105,7 +105,7 @@ impl<'pos> PawnMoveGenerator<'pos> { let legal_1square_pushes = (parameters.push_shift)(bb) & empty_squares; let legal_2square_pushes = - (parameters.push_shift)(legal_1square_pushes & BitBoard::rank(2)) & empty_squares; + (parameters.push_shift)(&(legal_1square_pushes & BitBoard::rank(2))) & empty_squares; self.pushes = legal_1square_pushes | legal_2square_pushes; } @@ -146,7 +146,7 @@ impl<'pos> PawnMoveGenerator<'pos> { for from_sq in bb.occupied_squares() { let pawn: BitBoard = from_sq.into(); - let push = (parameters.push_shift)(pawn); + let push = (parameters.push_shift)(&pawn); if !(push & empty_squares).is_empty() { let to_sq = push.occupied_squares().next().unwrap(); @@ -161,7 +161,7 @@ impl<'pos> PawnMoveGenerator<'pos> { } if !(pawn & parameters.starting_rank).is_empty() { - let push = (parameters.push_shift)(push); + let push = (parameters.push_shift)(&push); if !(push & empty_squares).is_empty() { let to_sq = push.occupied_squares().next().unwrap(); self.quiet_move_list() @@ -171,8 +171,8 @@ impl<'pos> PawnMoveGenerator<'pos> { } for attack in [ - (parameters.left_capture_shift)(pawn), - (parameters.right_capture_shift)(pawn), + (parameters.left_capture_shift)(&pawn), + (parameters.right_capture_shift)(&pawn), ] { if !(attack & black_pieces).is_empty() { let to_sq = attack.occupied_squares().next().unwrap(); @@ -245,17 +245,13 @@ impl<'pos> Iterator for PawnMoveGenerator<'pos> { #[cfg(test)] mod tests { use super::*; - use crate::piece::PlacedPiece; use crate::position::DiagramFormatter; use crate::{piece, Position, Square}; use std::collections::HashSet; #[test] fn one_2square_push() { - let mut pos = Position::empty(); - - pos.place_piece(piece!(White Pawn), Square::E2) - .expect("Failed to place pawn on e2"); + let pos = position![White Pawn on E2]; let generator = PawnMoveGenerator::new(&pos, Color::White); @@ -271,10 +267,7 @@ mod tests { #[test] fn one_1square_push() { - let mut pos = Position::empty(); - - pos.place_piece(Piece::pawn(Color::White), Square::E3) - .expect("Failed to place pawn on e3"); + let mut pos = position![White Pawn on E3]; let generator = PawnMoveGenerator::new(&pos, Color::White); @@ -292,12 +285,10 @@ mod tests { #[test] fn one_obstructed_2square_push() { - let mut pos = Position::empty(); - - pos.place_piece(Piece::pawn(Color::White), Square::E2) - .expect("Failed to place pawn on e2"); - pos.place_piece(Piece::knight(Color::White), Square::E4) - .expect("Failed to place knight on e4"); + let pos = position![ + White Pawn on E2, + White Knight on E4, + ]; println!("{}", DiagramFormatter::new(&pos)); @@ -317,13 +308,10 @@ mod tests { #[test] fn one_obstructed_1square_push() { - let mut pos = Position::empty(); - - pos.place_piece(Piece::pawn(Color::White), Square::E2) - .expect("Failed to place pawn on e2"); - pos.place_piece(Piece::knight(Color::White), Square::E3) - .expect("Failed to place knight on e4"); - + let mut pos = position![ + White Pawn on E2, + White Knight on E3, + ]; println!("{}", DiagramFormatter::new(&pos)); let generator = PawnMoveGenerator::new(&pos, Color::White); @@ -335,25 +323,21 @@ mod tests { #[test] fn one_attack() { - let mut pos = Position::empty(); - pos.place_piece(Piece::pawn(Color::White), Square::E4) - .expect("Failed to place pawn on e4"); - pos.place_piece(Piece::bishop(Color::White), Square::E5) - .expect("Failed to place pawn on e4"); - pos.place_piece(Piece::knight(Color::Black), Square::D5) - .expect("Failed to place knight on d5"); - + let pos = position![ + White Pawn on E4, + White Bishop on E5, + Black Knight on D5, + ]; println!("{}", DiagramFormatter::new(&pos)); let generator = PawnMoveGenerator::new(&pos, Color::White); - let expected_moves = HashSet::from_iter([MoveBuilder::new( - Piece::pawn(Color::White), - Square::E4, - Square::D5, - ) - .capturing(piece!(Black Knight on D5)) - .build()]); + let expected_moves = + HashSet::from_iter( + [MoveBuilder::new(piece!(White Pawn), Square::E4, Square::D5) + .capturing(piece!(Black Knight on D5)) + .build()], + ); let generated_moves: HashSet = generator.collect(); @@ -362,16 +346,12 @@ mod tests { #[test] fn one_double_attack() { - let mut pos = Position::empty(); - pos.place_piece(Piece::pawn(Color::White), Square::E4) - .expect("Failed to place pawn on e4"); - pos.place_piece(Piece::bishop(Color::White), Square::E5) - .expect("Failed to place pawn on e4"); - pos.place_piece(Piece::knight(Color::Black), Square::D5) - .expect("Failed to place knight on d5"); - pos.place_piece(Piece::queen(Color::Black), Square::F5) - .expect("Failed to place knight on f5"); - + let pos = position![ + White Pawn on E4, + White Bishop on E5, + Black Knight on D5, + Black Queen on F5, + ]; println!("{}", DiagramFormatter::new(&pos)); let generator = PawnMoveGenerator::new(&pos, Color::White); diff --git a/board/src/move_generator/queen.rs b/board/src/move_generator/queen.rs index 60667ea..e7d03b6 100644 --- a/board/src/move_generator/queen.rs +++ b/board/src/move_generator/queen.rs @@ -66,27 +66,20 @@ impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> { #[cfg(test)] mod tests { use super::*; - use crate::{ - piece::{Color, Piece}, - position::DiagramFormatter, - BitBoard, Position, Square, - }; + use crate::{piece, position, position::DiagramFormatter, BitBoard, Color}; #[test] fn classical_single_queen_bitboard() { - let mut pos = Position::empty(); - [(Piece::queen(Color::White), Square::A1)] - .into_iter() - .for_each(|(p, sq)| { - pos.place_piece(p, sq) - .expect(&format!("Unable to place {:?} on {}", &p, &sq)) - }); + let pos = position![White Queen on B2]; let generator = ClassicalMoveGenerator::new(&pos, Color::White); let bitboard = generator.bitboard(); - let expected = BitBoard::new( - 0b10000001_01000001_00100001_00010001_00001001_00000101_00000011_11111110, - ); + let expected = bitboard![ + A2, C2, D2, E2, F2, G2, H2, // Rank + B1, B3, B4, B5, B6, B7, B8, // File + A1, C3, D4, E5, F6, G7, H8, // Diagonal + C1, A3 // Anti-diagonal + ]; assert_eq!( bitboard, expected, @@ -98,16 +91,10 @@ mod tests { /// Test that a rook can move up to, but not onto, a friendly piece. #[test] fn classical_single_queen_with_same_color_blocker_bitboard() { - let mut pos = Position::empty(); - [ - (Piece::queen(Color::White), Square::A1), - (Piece::knight(Color::White), Square::E1), - ] - .into_iter() - .for_each(|(p, sq)| { - pos.place_piece(p, sq) - .expect(&format!("Unable to place {} on {}", p, sq)) - }); + let pos = position![ + White Queen on A1, + White Knight on E1, + ]; println!("{}", DiagramFormatter::new(&pos)); @@ -127,44 +114,40 @@ mod tests { /// Test that a rook can move up to, and then capture, an enemy piece. #[test] fn classical_single_queen_with_opposing_color_blocker_bitboard() { - let mut pos = Position::empty(); - [ - (Piece::queen(Color::White), Square::A1), - (Piece::knight(Color::Black), Square::E5), - ] - .into_iter() - .for_each(|(p, sq)| { - pos.place_piece(p, sq) - .expect(&format!("Unable to place {} on {}", p, sq)) - }); + let pos = position![ + White Queen on B2, + Black Knight on E5, + ]; + println!("{}", DiagramFormatter::new(&pos)); let generator = ClassicalMoveGenerator::new(&pos, Color::White); assert_eq!( generator.bitboard(), - BitBoard::new( - 0b00000001_00000001_00000001_00010001_00001001_00000101_00000011_11111110, - ) + bitboard![ + A2, C2, D2, E2, F2, G2, H2, // Rank + B1, B3, B4, B5, B6, B7, B8, // File + A1, C3, D4, E5, // Diagonal + C1, A3 // Anti-diagonal + ] ); } #[test] fn classical_single_queen_in_center() { - let mut pos = Position::empty(); - [(Piece::queen(Color::White), Square::E4)] - .into_iter() - .for_each(|(p, sq)| { - pos.place_piece(p, sq) - .expect(&format!("Unable to place {} on {}", p, sq)) - }); + let pos = position![White Queen on D3]; + println!("{}", DiagramFormatter::new(&pos)); let generator = ClassicalMoveGenerator::new(&pos, Color::White); assert_eq!( generator.bitboard(), - BitBoard::new( - 0b00010001_10010010_01010100_00111000_11101111_00111000_01010100_10010010 - ) + bitboard![ + A3, B3, C3, E3, F3, G3, H3, // Rank + D1, D2, D4, D5, D6, D7, D8, // File + B1, C2, E4, F5, G6, H7, // Diagonal + F1, E2, C4, B5, A6, // Anti-diagonal + ] ); } } diff --git a/board/src/move_generator/rook.rs b/board/src/move_generator/rook.rs index 9a61d9b..d82c22c 100644 --- a/board/src/move_generator/rook.rs +++ b/board/src/move_generator/rook.rs @@ -63,44 +63,29 @@ impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> { mod tests { use super::*; use crate::{ - piece::{Color, Piece}, - position::DiagramFormatter, - BitBoard, Position, Square, + piece::Piece, position, position::DiagramFormatter, BitBoard, Color, Position, Square, }; #[test] fn classical_single_rook_bitboard() { - let mut pos = Position::empty(); - [(Piece::rook(Color::White), Square::A1)] - .into_iter() - .for_each(|(p, sq)| { - pos.place_piece(p, sq) - .expect(&format!("Unable to place {:?} on {}", &p, &sq)) - }); + let pos = position![White Rook on A2]; + println!("{}", DiagramFormatter::new(&pos)); let generator = ClassicalMoveGenerator::new(&pos, Color::White); assert_eq!( generator.bitboard(), - BitBoard::new( - 0b00000001_00000001_00000001_00000001_00000001_00000001_00000001_11111110 - ) + bitboard![A1, A3, A4, A5, A6, A7, A8, B2, C2, D2, E2, F2, G2, H2] ); } /// Test that a rook can move up to, but not onto, a friendly piece. #[test] fn classical_single_rook_with_same_color_blocker_bitboard() { - let mut pos = Position::empty(); - [ - (Piece::rook(Color::White), Square::A1), - (Piece::knight(Color::White), Square::E1), - ] - .into_iter() - .for_each(|(p, sq)| { - pos.place_piece(p, sq) - .expect(&format!("Unable to place {} on {}", p, sq)) - }); + let mut pos = position![ + White Rook on A1, + White Knight on E1, + ]; println!("{}", DiagramFormatter::new(&pos)); @@ -117,44 +102,28 @@ mod tests { /// Test that a rook can move up to, and then capture, an enemy piece. #[test] fn classical_single_rook_with_opposing_color_blocker_bitboard() { - let mut pos = Position::empty(); - [ - (Piece::rook(Color::White), Square::A1), - (Piece::knight(Color::Black), Square::E1), - ] - .into_iter() - .for_each(|(p, sq)| { - pos.place_piece(p, sq) - .expect(&format!("Unable to place {} on {}", p, sq)) - }); + let pos = position![ + White Rook on A1, + Black Knight on E1, + ]; let generator = ClassicalMoveGenerator::new(&pos, Color::White); assert_eq!( generator.bitboard(), - BitBoard::new( - 0b00000001_00000001_00000001_00000001_00000001_00000001_00000001_00011110 - ) + bitboard![A2, A3, A4, A5, A6, A7, A8, B1, C1, D1, E1] ); } #[test] fn classical_single_rook_in_center() { - let mut pos = Position::empty(); - [(Piece::rook(Color::White), Square::E4)] - .into_iter() - .for_each(|(p, sq)| { - pos.place_piece(p, sq) - .expect(&format!("Unable to place {} on {}", p, sq)) - }); + let pos = position![White Rook on D4]; let generator = ClassicalMoveGenerator::new(&pos, Color::White); assert_eq!( generator.bitboard(), - BitBoard::new( - 0b00010000_00010000_00010000_00010000_11101111_00010000_00010000_00010000 - ) + bitboard![A4, B4, C4, E4, F4, G4, H4, D1, D2, D3, D5, D6, D7, D8] ); } } diff --git a/board/src/piece.rs b/board/src/piece.rs index fba6113..6afae98 100644 --- a/board/src/piece.rs +++ b/board/src/piece.rs @@ -122,11 +122,6 @@ impl fmt::Display for Shape { } } -#[derive(Debug, Eq, PartialEq)] -pub enum PiecePlacementError { - ExistsOnSquare, -} - #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct Piece { color: Color, @@ -252,6 +247,12 @@ impl fmt::Display for PlacedPiece { } } +impl From<(&Square, &Piece)> for PlacedPiece { + fn from(value: (&Square, &Piece)) -> Self { + PlacedPiece::new(*value.1, *value.0) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/board/src/position/builders.rs b/board/src/position/builders.rs new file mode 100644 index 0000000..44daccb --- /dev/null +++ b/board/src/position/builders.rs @@ -0,0 +1,83 @@ +// Eryn Wells + +use super::{flags::Flags, piece_sets::PieceBitBoards}; +use crate::{ + piece::{PlacedPiece, Shape}, + square::Rank, + BitBoard, Color, MakeMoveError, Move, Piece, Position, Square, +}; +use std::collections::BTreeMap; + +#[derive(Clone)] +pub struct Builder { + player_to_move: Color, + flags: Flags, + pieces: BTreeMap, + kings: [Square; 2], +} + +impl Builder { + pub fn new() -> Self { + Self::default() + } + + pub fn place_piece(&mut self, piece: PlacedPiece) -> &mut Self { + let square = piece.square(); + + if piece.shape() == Shape::King { + let color_index: usize = piece.color() as usize; + self.pieces.remove(&self.kings[color_index]); + self.kings[color_index] = square; + } + + self.pieces.insert(square, piece.piece()); + + self + } + + pub fn build(&self) -> Position { + let pieces = PieceBitBoards::from_iter( + self.pieces + .iter() + .map(PlacedPiece::from) + .filter(Self::is_piece_placement_valid), + ); + + Position::new(self.player_to_move, self.flags, pieces, None) + } +} + +impl Builder { + fn is_piece_placement_valid(piece: &PlacedPiece) -> bool { + if piece.shape() == Shape::Pawn { + // Pawns cannot be placed on the first (back) rank of their side, + // and cannot be placed on the final rank without a promotion. + let rank = piece.square().rank(); + return rank != Rank::One && rank != Rank::Eight; + } + + true + } +} + +impl Default for Builder { + fn default() -> Self { + let pieces = BTreeMap::from_iter([ + ( + Square::KING_STARTING_SQUARES[Color::White as usize], + piece!(White King), + ), + ( + Square::KING_STARTING_SQUARES[Color::Black as usize], + piece!(Black King), + ), + ]); + + Self { + player_to_move: Color::White, + flags: Flags::default(), + pieces: pieces, + kings: Square::KING_STARTING_SQUARES, + } + } +} diff --git a/board/src/position/diagram_formatter.rs b/board/src/position/diagram_formatter.rs index 5187ace..7bd8ccd 100644 --- a/board/src/position/diagram_formatter.rs +++ b/board/src/position/diagram_formatter.rs @@ -42,7 +42,7 @@ impl<'a> fmt::Display for DiagramFormatter<'a> { mod tests { use super::*; use crate::piece::{Color, Piece}; - use crate::Position; + use crate::{Position, PositionBuilder}; #[test] fn empty() { @@ -53,13 +53,9 @@ mod tests { #[test] fn one_king() { - let mut pos = Position::empty(); - pos.place_piece( - Piece::king(Color::Black), - Square::from_algebraic_str("h3").expect("h3"), - ) - .expect("Unable to place piece"); - + let pos = PositionBuilder::new() + .place_piece(piece!(Black King on H3)) + .build(); let diagram = DiagramFormatter(&pos); println!("{}", diagram); } diff --git a/board/src/position/mod.rs b/board/src/position/mod.rs index 650959d..987c107 100644 --- a/board/src/position/mod.rs +++ b/board/src/position/mod.rs @@ -1,10 +1,13 @@ // Eryn Wells +mod builders; mod diagram_formatter; mod flags; +mod piece_sets; mod pieces; mod position; +pub use builders::Builder as PositionBuilder; pub use diagram_formatter::DiagramFormatter; pub use pieces::Pieces; pub use position::Position; diff --git a/board/src/position/piece_sets.rs b/board/src/position/piece_sets.rs new file mode 100644 index 0000000..2e97669 --- /dev/null +++ b/board/src/position/piece_sets.rs @@ -0,0 +1,149 @@ +// Eryn Wells + +use crate::{ + piece::{Piece, PlacedPiece}, + BitBoard, Color, Square, +}; + +#[derive(Debug, Eq, PartialEq)] +pub enum PlacePieceStrategy { + Replace, + PreserveExisting, +} + +#[derive(Debug, Eq, PartialEq)] +pub enum PlacePieceError { + ExisitingPiece, +} + +impl Default for PlacePieceStrategy { + fn default() -> Self { + Self::Replace + } +} + +#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] +pub(super) struct PieceBitBoards { + by_color: ByColor, + by_color_and_shape: ByColorAndShape, +} + +#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] +struct ByColor(BitBoard, [BitBoard; 2]); + +#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] +struct ByColorAndShape([[BitBoard; 6]; 2]); + +impl PieceBitBoards { + pub(super) fn new(pieces: [[BitBoard; 6]; 2]) -> Self { + let white_pieces = pieces[Color::White as usize] + .iter() + .fold(BitBoard::empty(), std::ops::BitOr::bitor); + let black_pieces = pieces[Color::White as usize] + .iter() + .fold(BitBoard::empty(), std::ops::BitOr::bitor); + let all_pieces = white_pieces | black_pieces; + + Self { + by_color: ByColor(all_pieces, [white_pieces, black_pieces]), + by_color_and_shape: ByColorAndShape(pieces), + } + } + + pub(super) fn all_pieces(&self) -> &BitBoard { + self.by_color.all() + } + + pub(super) fn all_pieces_of_color(&self, color: Color) -> &BitBoard { + self.by_color.bitboard(color) + } + + pub(super) fn bitboard_for_color(&self, color: Color) -> &BitBoard { + self.by_color.bitboard(color) + } + + pub(super) fn bitboard_for_color_mut(&mut self, color: Color) -> &mut BitBoard { + self.by_color.bitboard_mut(color) + } + + pub(super) fn bitboard_for_piece(&self, piece: Piece) -> &BitBoard { + self.by_color_and_shape.bitboard_for_piece(piece) + } + + pub(super) fn bitboard_for_piece_mut(&mut self, piece: Piece) -> &mut BitBoard { + self.by_color_and_shape.bitboard_for_piece_mut(piece) + } + + pub(super) fn place_piece(&mut self, piece: PlacedPiece) -> Result<(), PlacePieceError> { + self.place_piece_with_strategy(piece, Default::default()) + } + + pub(super) fn place_piece_with_strategy( + &mut self, + piece: PlacedPiece, + strategy: PlacePieceStrategy, + ) -> Result<(), PlacePieceError> { + let color = piece.color(); + let square = piece.square(); + + if self.by_color.bitboard(color).is_set(piece.square()) { + match strategy { + PlacePieceStrategy::Replace => todo!(), + PlacePieceStrategy::PreserveExisting => { + return Err(PlacePieceError::ExisitingPiece) + } + } + } + + self.by_color.set_square(square, color); + self.bitboard_for_piece_mut(piece.piece()) + .set_square(square); + + Ok(()) + } +} + +impl FromIterator for PieceBitBoards { + fn from_iter>(iter: T) -> Self { + let mut pieces = Self::default(); + + for piece in iter { + let _ = pieces.place_piece(piece); + } + + pieces + } +} + +impl ByColor { + fn all(&self) -> &BitBoard { + &self.0 + } + + pub(super) fn bitboard(&self, color: Color) -> &BitBoard { + &self.1[color as usize] + } + + pub(super) fn bitboard_mut(&mut self, color: Color) -> &mut BitBoard { + &mut self.1[color as usize] + } + + fn set_square(&mut self, square: Square, color: Color) { + self.0.set_square(square); + self.1[color as usize].set_square(square) + } +} + +impl ByColorAndShape { + fn bitboard_for_piece(&self, piece: Piece) -> &BitBoard { + &self.0[piece.color() as usize][piece.shape() as usize] + } + + fn bitboard_for_piece_mut(&mut self, piece: Piece) -> &mut BitBoard { + &mut self.0[piece.color() as usize][piece.shape() as usize] + } + + fn set_square(&mut self, square: Square, piece: Piece) { + self.bitboard_for_piece_mut(piece).set_square(square); + } +} diff --git a/board/src/position/pieces.rs b/board/src/position/pieces.rs index 307673e..d1cb6c7 100644 --- a/board/src/position/pieces.rs +++ b/board/src/position/pieces.rs @@ -38,7 +38,7 @@ impl<'a> Iterator for Pieces<'a> { } let mut current_shape: Option = None; - let mut next_nonempty_bitboard: Option = None; + let mut next_nonempty_bitboard: Option<&BitBoard> = None; while let Some(shape) = self.shape_iterator.next() { let piece = Piece::new(self.color, *shape); @@ -76,15 +76,10 @@ impl<'a> Iterator for Pieces<'a> { mod tests { use super::*; use crate::piece::{Color, Piece, Shape}; - use crate::Position; use crate::Square; + use crate::{Position, PositionBuilder}; use std::collections::HashSet; - fn place_piece_in_position(pos: &mut Position, piece: Piece, sq: Square) { - pos.place_piece(piece, sq) - .expect("Unable to place {piece:?} queen on {sq}"); - } - #[test] fn empty() { let pos = Position::empty(); @@ -94,40 +89,45 @@ mod tests { #[test] fn one() { - let sq = Square::E4; - - let mut pos = Position::empty(); - place_piece_in_position(&mut pos, Piece::new(Color::White, Shape::Queen), Square::E4); - println!("{:?}", &pos); + let pos = PositionBuilder::new() + .place_piece(piece!(White Queen on E4)) + .build(); + println!("{:#?}", &pos); let mut pieces = pos.pieces(Color::White); - assert_eq!( - pieces.next(), - Some(PlacedPiece::new(Piece::new(Color::White, Shape::Queen), sq)) - ); + assert_eq!(pieces.next(), Some(piece!(White Queen on E4))); + assert_eq!(pieces.next(), Some(piece!(White King on E1))); assert_eq!(pieces.next(), None); } #[test] fn multiple_pieces() { - let mut pos = Position::empty(); - - place_piece_in_position(&mut pos, Piece::new(Color::White, Shape::Queen), Square::E4); - place_piece_in_position(&mut pos, Piece::new(Color::White, Shape::King), Square::E1); - place_piece_in_position(&mut pos, Piece::new(Color::White, Shape::Pawn), Square::B2); - place_piece_in_position(&mut pos, Piece::new(Color::White, Shape::Pawn), Square::C2); - - println!("{:?}", &pos); + let pos = PositionBuilder::new() + .place_piece(piece!(White Queen on E4)) + .place_piece(piece!(White King on A1)) + .place_piece(piece!(White Pawn on B2)) + .place_piece(piece!(White Pawn on C2)) + .build(); + println!("{}", crate::position::DiagramFormatter::new(&pos)); let expected_placed_pieces = HashSet::from([ - PlacedPiece::new(Piece::new(Color::White, Shape::Queen), Square::E4), - PlacedPiece::new(Piece::new(Color::White, Shape::King), Square::E1), - PlacedPiece::new(Piece::new(Color::White, Shape::Pawn), Square::B2), - PlacedPiece::new(Piece::new(Color::White, Shape::Pawn), Square::C2), + piece!(White Queen on E4), + piece!(White King on A1), + piece!(White Pawn on B2), + piece!(White Pawn on C2), ]); let placed_pieces = HashSet::from_iter(pos.pieces(Color::White)); - assert_eq!(placed_pieces, expected_placed_pieces); + assert_eq!( + placed_pieces, + expected_placed_pieces, + "{:#?}", + placed_pieces + .symmetric_difference(&expected_placed_pieces) + .into_iter() + .map(|pp| format!("{}", pp)) + .collect::>() + ); } } diff --git a/board/src/position/position.rs b/board/src/position/position.rs index 1be30e0..ee76ed9 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -1,11 +1,11 @@ // Eryn Wells -use super::{flags::Flags, Pieces}; +use super::{flags::Flags, piece_sets::PieceBitBoards, Pieces}; use crate::{ move_generator::Moves, - piece::{Color, Piece, PiecePlacementError, PlacedPiece, Shape}, + piece::{Color, Piece, PlacedPiece, Shape}, sight::Sight, - BitBoard, Square, + BitBoard, Move, Square, }; use std::cell::OnceCell; @@ -21,15 +21,8 @@ pub(crate) enum BoardSide { pub struct Position { color_to_move: Color, flags: Flags, - - /// Composite bitboards for all the pieces of a particular color. - pieces_per_color: [BitBoard; 2], - - /// Bitboards representing positions of particular piece types per color. - pieces_per_type: [[BitBoard; 6]; 2], - + pieces: PieceBitBoards, en_passant_square: Option, - sight: [OnceCell; 2], } @@ -38,32 +31,14 @@ impl Position { Position { color_to_move: Color::White, flags: Default::default(), - pieces_per_color: [BitBoard::empty(); 2], - pieces_per_type: [ - [ - BitBoard::empty(), - BitBoard::empty(), - BitBoard::empty(), - BitBoard::empty(), - BitBoard::empty(), - BitBoard::empty(), - ], - [ - BitBoard::empty(), - BitBoard::empty(), - BitBoard::empty(), - BitBoard::empty(), - BitBoard::empty(), - BitBoard::empty(), - ], - ], + pieces: PieceBitBoards::default(), en_passant_square: None, sight: [OnceCell::new(), OnceCell::new()], } } /// Return a starting position. - pub fn starting() -> Position { + pub fn starting() -> Self { let white_pieces = [ BitBoard::new(0x00FF000000000000), BitBoard::new(0x4200000000000000), @@ -82,19 +57,19 @@ impl Position { BitBoard::new(0x0008), ]; - Position { + Self { color_to_move: Color::White, - flags: Default::default(), - pieces_per_color: [ - white_pieces.iter().fold(BitBoard::empty(), |a, b| a | *b), - black_pieces.iter().fold(BitBoard::empty(), |a, b| a | *b), - ], - pieces_per_type: [white_pieces, black_pieces], + flags: Flags::default(), + pieces: PieceBitBoards::new([white_pieces, black_pieces]), en_passant_square: None, sight: [OnceCell::new(), OnceCell::new()], } } + pub fn player_to_move(&self) -> Color { + self.color_to_move + } + /// Returns true if the player has the right to castle on the given side of /// the board. /// @@ -112,39 +87,28 @@ impl Position { self.flags.player_has_right_to_castle(color, side) } - pub fn place_piece(&mut self, piece: Piece, square: Square) -> Result<(), PiecePlacementError> { - let type_bb = self.bitboard_for_piece_mut(piece); - - if type_bb.is_set(square) { - return Err(PiecePlacementError::ExistsOnSquare); - } - - type_bb.set_square(square); - - self.bitboard_for_color_mut(piece.color()) - .set_square(square); - - Ok(()) - } - pub fn moves(&self) -> Moves { Moves::new(self, self.color_to_move) } /// Return a BitBoard representing the set of squares containing a piece. #[inline] - pub(crate) fn occupied_squares(&self) -> BitBoard { - self.pieces_per_color[Color::White as usize] | self.pieces_per_color[Color::Black as usize] + pub(crate) fn occupied_squares(&self) -> &BitBoard { + &self.pieces.all_pieces() } #[inline] - pub(crate) fn friendly_pieces(&self) -> BitBoard { - self.bitboard_for_color(self.color_to_move) + pub(crate) fn friendly_pieces(&self) -> &BitBoard { + self.pieces.all_pieces_of_color(self.color_to_move) } #[inline] - pub(crate) fn opposing_pieces(&self) -> BitBoard { - self.bitboard_for_color(self.color_to_move.other()) + pub(crate) fn opposing_pieces(&self) -> &BitBoard { + self.pieces.all_pieces_of_color(self.color_to_move.other()) + } + + pub(crate) fn all_pieces(&self) -> (&BitBoard, &BitBoard) { + (self.friendly_pieces(), self.opposing_pieces()) } /// Return a BitBoard representing the set of squares containing a piece. @@ -154,28 +118,11 @@ impl Position { !self.occupied_squares() } - pub(crate) fn bitboard_for_piece(&self, piece: Piece) -> BitBoard { - self.pieces_per_type[piece.color() as usize][piece.shape() as usize] - } - - fn bitboard_for_piece_mut(&mut self, piece: Piece) -> &mut BitBoard { - &mut self.pieces_per_type[piece.color() as usize][piece.shape() as usize] - } - - pub(crate) fn bitboard_for_color(&self, color: Color) -> BitBoard { - self.pieces_per_color[color as usize] - } - - fn bitboard_for_color_mut(&mut self, color: Color) -> &mut BitBoard { - &mut self.pieces_per_color[color as usize] - } - pub fn piece_on_square(&self, sq: Square) -> Option { for color in Color::iter() { for shape in Shape::iter() { let piece = Piece::new(color, *shape); - let bb = self.bitboard_for_piece(piece); - if bb.is_set(sq) { + if self.pieces.bitboard_for_piece(piece).is_set(sq) { return Some(PlacedPiece::new(piece, sq)); } } @@ -206,11 +153,49 @@ impl Position { } fn king_square(&self) -> Square { - self.bitboard_for_piece(Piece::king(self.color_to_move)) + self.pieces + .bitboard_for_piece(Piece::king(self.color_to_move)) .occupied_squares() .next() .unwrap() } + + pub(crate) fn move_is_legal(&self, mv: Move) -> bool { + true + } +} + +// crate::position methods +impl Position { + pub(super) fn new( + player_to_move: Color, + flags: Flags, + pieces: PieceBitBoards, + en_passant_square: Option, + ) -> Self { + Self { + color_to_move: player_to_move, + flags, + en_passant_square, + pieces, + sight: [OnceCell::new(), OnceCell::new()], + } + } + + pub(super) fn flags(&self) -> Flags { + self.flags + } +} + +// crate methods +impl Position { + pub(crate) fn bitboard_for_color(&self, color: Color) -> &BitBoard { + self.pieces.bitboard_for_color(color) + } + + pub(crate) fn bitboard_for_piece(&self, piece: Piece) -> &BitBoard { + self.pieces.bitboard_for_piece(piece) + } } #[cfg(test)] @@ -226,46 +211,9 @@ impl Default for Position { } } -impl FromIterator for Position { - fn from_iter>(iter: T) -> Self { - let mut position = Position::empty(); - for placed_piece in iter { - _ = position.place_piece(placed_piece.piece(), placed_piece.square()); - } - - position - } -} - #[cfg(test)] mod tests { - use super::*; - use crate::piece::Shape; - - #[test] - fn place_piece() { - let mut position = Position::empty(); - - let piece = Piece::new(Color::White, Shape::Queen); - let square = Square::E4; - - position - .place_piece(piece, square) - .expect("Unable to place white queen on e4"); - - assert_eq!( - position.bitboard_for_color(piece.color()).clone(), - BitBoard::new(1 << 28) - ); - assert_eq!( - position.bitboard_for_piece(piece).clone(), - BitBoard::new(1 << 28) - ); - - position - .place_piece(piece, square) - .expect_err("Placed white queen on e4 a second time?!"); - } + use crate::position; #[test] fn king_is_in_check() { From 7f4485ed512b3cfa00556c7d1674075fea41bdec Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 19 Jan 2024 18:09:22 -0800 Subject: [PATCH 034/423] [board] Make the trailing comma in bitboard! optional This a neat trick! --- board/src/bitboard/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/board/src/bitboard/mod.rs b/board/src/bitboard/mod.rs index 238f6ac..c417abd 100644 --- a/board/src/bitboard/mod.rs +++ b/board/src/bitboard/mod.rs @@ -8,7 +8,7 @@ pub(crate) use bitboard::{BitBoard, BitBoardBuilder}; #[macro_export] macro_rules! bitboard { - ($($sq:ident),*) => { + ($($sq:ident),* $(,)?) => { $crate::bitboard::BitBoardBuilder::empty() $(.square($crate::Square::$sq))* .build() From 7071f6a74268e13ece20715bdd0d4ffd0be48342 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 19 Jan 2024 18:11:27 -0800 Subject: [PATCH 035/423] [board] Cave to pressure and implement fmt::Display for Position It prints a nice diagram! Now I can make the position module private. --- board/src/lib.rs | 3 +-- board/src/position/position.rs | 9 ++++++++- explorer/src/main.rs | 4 +--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/board/src/lib.rs b/board/src/lib.rs index 0585d6f..d91df46 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -8,8 +8,7 @@ mod r#move; mod macros; mod move_generator; pub mod piece; -#[macro_use] -pub mod position; +mod position; mod sight; mod square; diff --git a/board/src/position/position.rs b/board/src/position/position.rs index ee76ed9..c2cb5dc 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -4,10 +4,11 @@ use super::{flags::Flags, piece_sets::PieceBitBoards, Pieces}; use crate::{ move_generator::Moves, piece::{Color, Piece, PlacedPiece, Shape}, + position::DiagramFormatter, sight::Sight, BitBoard, Move, Square, }; -use std::cell::OnceCell; +use std::{cell::OnceCell, fmt}; /// A lateral side of the board relative to the player. Kingside is the side the /// player's king starts on. Queenside is the side of the board the player's @@ -211,6 +212,12 @@ impl Default for Position { } } +impl fmt::Display for Position { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", DiagramFormatter::new(self)) + } +} + #[cfg(test)] mod tests { use crate::position; diff --git a/explorer/src/main.rs b/explorer/src/main.rs index 7eebb2b..9da6a64 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -1,5 +1,4 @@ use board::piece::{Color, Piece, Shape}; -use board::position::DiagramFormatter; use board::{Position, Square}; use clap::{Arg, Command}; use rustyline::error::ReadlineError; @@ -80,8 +79,7 @@ fn main() -> Result<(), String> { let mut pos = Position::empty(); loop { - let diagram = DiagramFormatter::new(&pos); - println!("{}", diagram); + println!("{}", &pos); let readline = editor.readline("? "); match readline { From 918b68f3001ac0bb8e886e4a256ab097201c0d87 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 21 Jan 2024 09:05:42 -0800 Subject: [PATCH 036/423] [board] Use Castle as the interface type for methods related to castling Use BoardSide as an internal type for looking up generated bitboards, target squares, etc. --- board/src/move_generator/king.rs | 6 ++-- board/src/position/flags.rs | 57 ++++++++++++++++---------------- board/src/position/position.rs | 31 ++++++++++++++--- board/src/square.rs | 14 +++++++- 4 files changed, 71 insertions(+), 37 deletions(-) diff --git a/board/src/move_generator/king.rs b/board/src/move_generator/king.rs index be0749b..b3500fd 100644 --- a/board/src/move_generator/king.rs +++ b/board/src/move_generator/king.rs @@ -6,7 +6,7 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::{ piece::{Color, Piece, PlacedPiece}, - position::BoardSide, + r#move::Castle, BitBoard, Move, MoveBuilder, Position, }; @@ -17,7 +17,7 @@ move_generator_declaration!(KingMoveGenerator, getters); impl<'a> KingMoveGenerator<'a> { #[allow(unused_variables)] fn king_side_castle(position: &Position, color: Color) -> Option { - if !position.player_has_right_to_castle(color, BoardSide::King) { + if !position.player_has_right_to_castle(color, Castle::KingSide) { return None; } @@ -27,7 +27,7 @@ impl<'a> KingMoveGenerator<'a> { #[allow(unused_variables)] fn queen_side_castle(position: &Position, color: Color) -> Option { - if !position.player_has_right_to_castle(color, BoardSide::Queen) { + if !position.player_has_right_to_castle(color, Castle::QueenSide) { return None; } diff --git a/board/src/position/flags.rs b/board/src/position/flags.rs index f434f21..8bc0550 100644 --- a/board/src/position/flags.rs +++ b/board/src/position/flags.rs @@ -1,7 +1,7 @@ // Eryn Wells use super::position::BoardSide; -use crate::piece::Color; +use crate::{r#move::Castle, Color}; use std::fmt; #[derive(Clone, Copy, Eq, Hash, PartialEq)] @@ -9,20 +9,21 @@ pub(super) struct Flags(u8); impl Flags { #[inline] - pub(super) fn player_has_right_to_castle_flag_offset(color: Color, side: BoardSide) -> u8 { - ((color as u8) << 1) + side as u8 + pub(super) fn player_has_right_to_castle_flag_offset(color: Color, castle: Castle) -> u8 { + let board_side: BoardSide = castle.into(); + ((color as u8) << 1) + board_side as u8 } - pub(super) fn player_has_right_to_castle(&self, color: Color, side: BoardSide) -> bool { - (self.0 & (1 << Self::player_has_right_to_castle_flag_offset(color, side))) != 0 + pub(super) fn player_has_right_to_castle(&self, color: Color, castle: Castle) -> bool { + (self.0 & (1 << Self::player_has_right_to_castle_flag_offset(color, castle))) != 0 } - pub(super) fn set_player_has_right_to_castle_flag(&mut self, color: Color, side: BoardSide) { - self.0 |= 1 << Self::player_has_right_to_castle_flag_offset(color, side); + pub(super) fn set_player_has_right_to_castle_flag(&mut self, color: Color, castle: Castle) { + self.0 |= 1 << Self::player_has_right_to_castle_flag_offset(color, castle); } - pub(super) fn clear_player_has_right_to_castle_flag(&mut self, color: Color, side: BoardSide) { - self.0 &= !(1 << Self::player_has_right_to_castle_flag_offset(color, side)); + pub(super) fn clear_player_has_right_to_castle_flag(&mut self, color: Color, castle: Castle) { + self.0 &= !(1 << Self::player_has_right_to_castle_flag_offset(color, castle)); } } @@ -41,24 +42,24 @@ impl Default for Flags { #[cfg(test)] mod tests { use super::*; - use crate::piece::Color; + use crate::{r#move::Castle, Color}; #[test] fn castle_flags() { assert_eq!( - Flags::player_has_right_to_castle_flag_offset(Color::White, BoardSide::King), + Flags::player_has_right_to_castle_flag_offset(Color::White, Castle::KingSide), 0 ); assert_eq!( - Flags::player_has_right_to_castle_flag_offset(Color::White, BoardSide::Queen), + Flags::player_has_right_to_castle_flag_offset(Color::White, Castle::QueenSide), 1 ); assert_eq!( - Flags::player_has_right_to_castle_flag_offset(Color::Black, BoardSide::King), + Flags::player_has_right_to_castle_flag_offset(Color::Black, Castle::KingSide), 2 ); assert_eq!( - Flags::player_has_right_to_castle_flag_offset(Color::Black, BoardSide::Queen), + Flags::player_has_right_to_castle_flag_offset(Color::Black, Castle::QueenSide), 3 ); } @@ -66,21 +67,21 @@ mod tests { #[test] fn defaults() { let mut flags: Flags = Default::default(); - assert!(flags.player_has_right_to_castle(Color::White, BoardSide::King)); - assert!(flags.player_has_right_to_castle(Color::White, BoardSide::Queen)); - assert!(flags.player_has_right_to_castle(Color::Black, BoardSide::King)); - assert!(flags.player_has_right_to_castle(Color::Black, BoardSide::Queen)); + assert!(flags.player_has_right_to_castle(Color::White, Castle::KingSide)); + assert!(flags.player_has_right_to_castle(Color::White, Castle::QueenSide)); + assert!(flags.player_has_right_to_castle(Color::Black, Castle::KingSide)); + assert!(flags.player_has_right_to_castle(Color::Black, Castle::QueenSide)); - flags.clear_player_has_right_to_castle_flag(Color::White, BoardSide::Queen); - assert!(flags.player_has_right_to_castle(Color::White, BoardSide::King)); - assert!(!flags.player_has_right_to_castle(Color::White, BoardSide::Queen)); - assert!(flags.player_has_right_to_castle(Color::Black, BoardSide::King)); - assert!(flags.player_has_right_to_castle(Color::Black, BoardSide::Queen)); + flags.clear_player_has_right_to_castle_flag(Color::White, Castle::QueenSide); + assert!(flags.player_has_right_to_castle(Color::White, Castle::KingSide)); + assert!(!flags.player_has_right_to_castle(Color::White, Castle::QueenSide)); + assert!(flags.player_has_right_to_castle(Color::Black, Castle::KingSide)); + assert!(flags.player_has_right_to_castle(Color::Black, Castle::QueenSide)); - flags.set_player_has_right_to_castle_flag(Color::White, BoardSide::Queen); - assert!(flags.player_has_right_to_castle(Color::White, BoardSide::King)); - assert!(flags.player_has_right_to_castle(Color::White, BoardSide::Queen)); - assert!(flags.player_has_right_to_castle(Color::Black, BoardSide::King)); - assert!(flags.player_has_right_to_castle(Color::Black, BoardSide::Queen)); + flags.set_player_has_right_to_castle_flag(Color::White, Castle::QueenSide); + assert!(flags.player_has_right_to_castle(Color::White, Castle::KingSide)); + assert!(flags.player_has_right_to_castle(Color::White, Castle::QueenSide)); + assert!(flags.player_has_right_to_castle(Color::Black, Castle::KingSide)); + assert!(flags.player_has_right_to_castle(Color::Black, Castle::QueenSide)); } } diff --git a/board/src/position/position.rs b/board/src/position/position.rs index c2cb5dc..2ee5564 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -5,6 +5,7 @@ use crate::{ move_generator::Moves, piece::{Color, Piece, PlacedPiece, Shape}, position::DiagramFormatter, + r#move::Castle, sight::Sight, BitBoard, Move, Square, }; @@ -14,8 +15,17 @@ use std::{cell::OnceCell, fmt}; /// player's king starts on. Queenside is the side of the board the player's /// queen starts on. pub(crate) enum BoardSide { - King, - Queen, + King = 0, + Queen = 1, +} + +impl From for BoardSide { + fn from(value: Castle) -> Self { + match value { + Castle::KingSide => BoardSide::King, + Castle::QueenSide => BoardSide::Queen, + } + } } #[derive(Clone, Debug, Eq, PartialEq)] @@ -77,15 +87,26 @@ impl Position { /// The right to castle on a particular side of the board is retained as /// long as the player has not moved their king, or the rook on that side of /// the board. + pub(crate) fn player_has_right_to_castle(&self, color: Color, castle: Castle) -> bool { + self.flags.player_has_right_to_castle(color, castle) + } + + /// Returns `true` if the player is able to castle on the given side of the board. /// - /// The following requirements must also be met: + /// The following requirements must be met: /// + /// 1. The player must still have the right to castle on that side of the + /// board. The king and rook involved in the castle must not have moved. /// 1. The spaces between the king and rook must be clear /// 2. The king must not be in check /// 3. In the course of castling on that side, the king must not pass /// through a square that an enemy piece can see - pub(crate) fn player_has_right_to_castle(&self, color: Color, side: BoardSide) -> bool { - self.flags.player_has_right_to_castle(color, side) + pub(crate) fn player_can_castle(&self, player: Color, castle: Castle) -> bool { + if !self.player_has_right_to_castle(player, castle.into()) { + return false; + } + + true } pub fn moves(&self) -> Moves { diff --git a/board/src/square.rs b/board/src/square.rs index 0ad9d09..b04aede 100644 --- a/board/src/square.rs +++ b/board/src/square.rs @@ -1,5 +1,6 @@ // Eryn Wells +use crate::{position::BoardSide, r#move::Castle, Color}; use std::{fmt, str::FromStr}; pub enum Direction { @@ -157,12 +158,23 @@ impl Square { impl Square { pub(crate) const KING_STARTING_SQUARES: [Square; 2] = [Square::E1, Square::E8]; pub(crate) const KING_CASTLE_TARGET_SQUARES: [[Square; 2]; 2] = - [[Square::C1, Square::G1], [Square::C8, Square::G8]]; + [[Square::G1, Square::C1], [Square::G8, Square::C8]]; + pub(crate) const ROOK_CASTLE_TARGET_SQUARES: [[Square; 2]; 2] = + [[Square::F1, Square::D1], [Square::F8, Square::D8]]; pub fn from_algebraic_str(s: &str) -> Result { s.parse() } + pub fn king_castle_target(player: Color, castle: Castle) -> Square { + let board_side: BoardSide = castle.into(); + Self::KING_CASTLE_TARGET_SQUARES[player as usize][board_side as usize] + } + + pub fn rook_castle_target(player: Color, castle: Castle) -> Square { + let board_side: BoardSide = castle.into(); + Self::ROOK_CASTLE_TARGET_SQUARES[player as usize][board_side as usize] + } pub fn neighbor(self, direction: Direction) -> Option { match direction { Direction::North => Square::try_index(self as usize + 8), From 620701057d461fee657407e849d563cbe5d838a3 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 21 Jan 2024 09:14:02 -0800 Subject: [PATCH 037/423] [board] Rehash BitBoard's std::ops implementations Implement bitwise AND and OR for all permutations of BitBoard and &BitBoard. Refer to the std::ops traits by fully-qualified path rather than requiring the module to import those traits to implement them. Implement bitwise AND and OR assignment (&= and |=) for BitBoard and &BitBoard. Implement XOR and XOR assignment for BitBoards. --- board/src/bitboard/bitboard.rs | 71 ++++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 16 deletions(-) diff --git a/board/src/bitboard/bitboard.rs b/board/src/bitboard/bitboard.rs index 070f43f..6480e99 100644 --- a/board/src/bitboard/bitboard.rs +++ b/board/src/bitboard/bitboard.rs @@ -4,7 +4,7 @@ use super::library::{library, FILES, RANKS}; use super::LeadingBitScanner; use crate::{square::Direction, Square}; use std::fmt; -use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Not}; +use std::ops::Not; #[derive(Clone, Copy, Eq, Hash, PartialEq)] pub(crate) struct BitBoard(pub(super) u64); @@ -156,39 +156,49 @@ impl fmt::Debug for BitBoard { } macro_rules! infix_op { + ($trait_type:ident, $func_name:ident, $type:ty) => { + infix_op!($trait_type, $func_name, $type, $type); + infix_op!($trait_type, $func_name, $type, &$type); + infix_op!($trait_type, $func_name, &$type, $type); + infix_op!($trait_type, $func_name, &$type, &$type); + }; ($trait_type:ident, $func_name:ident, $left_type:ty, $right_type:ty) => { - impl $trait_type<$right_type> for $left_type { + impl std::ops::$trait_type<$right_type> for $left_type { type Output = BitBoard; #[inline] fn $func_name(self, rhs: $right_type) -> Self::Output { - BitBoard($trait_type::$func_name(self.0, rhs.0)) + BitBoard(std::ops::$trait_type::$func_name(self.0, rhs.0)) } } }; } macro_rules! assign_op { - ($trait_type:ident, $func_name:ident, $left_type:ty) => { - impl $trait_type for $left_type { + ($trait_type:ident, $func_name:ident, $type:ty) => { + impl std::ops::$trait_type for $type { #[inline] - fn $func_name(&mut self, rhs: Self) { - $trait_type::$func_name(&mut self.0, rhs.0) + fn $func_name(&mut self, rhs: $type) { + std::ops::$trait_type::$func_name(&mut self.0, rhs.0) + } + } + + impl std::ops::$trait_type<&$type> for $type { + #[inline] + fn $func_name(&mut self, rhs: &$type) { + std::ops::$trait_type::$func_name(&mut self.0, rhs.0) } } }; } -infix_op!(BitAnd, bitand, BitBoard, BitBoard); -infix_op!(BitAnd, bitand, BitBoard, &BitBoard); -infix_op!(BitAnd, bitand, &BitBoard, &BitBoard); +infix_op!(BitAnd, bitand, BitBoard); +infix_op!(BitOr, bitor, BitBoard); +infix_op!(BitXor, bitxor, BitBoard); assign_op!(BitAndAssign, bitand_assign, BitBoard); assign_op!(BitOrAssign, bitor_assign, BitBoard); - -infix_op!(BitOr, bitor, BitBoard, BitBoard); -infix_op!(BitOr, bitor, BitBoard, &BitBoard); -infix_op!(BitOr, bitor, &BitBoard, &BitBoard); +assign_op!(BitXorAssign, bitxor_assign, BitBoard); impl Not for BitBoard { type Output = BitBoard; @@ -232,13 +242,12 @@ impl BitBoardBuilder { #[cfg(test)] mod tests { use super::*; - use crate::Square; + use crate::{bitboard, Square}; #[test] fn display_and_debug() { let bb = BitBoard::file(0) | BitBoard::file(3) | BitBoard::rank(7) | BitBoard::rank(4); println!("{}", &bb); - println!("{:?}", &bb); } #[test] @@ -302,4 +311,34 @@ mod tests { assert_eq!(a, b); } } + + #[test] + fn xor() { + let a = bitboard![C5, G7]; + let b = bitboard![B5, G7, H3]; + + assert_eq!(a ^ b, bitboard![B5, C5, H3]); + assert_eq!(a ^ BitBoard::empty(), a); + assert_eq!(BitBoard::empty() ^ BitBoard::empty(), BitBoard::empty()); + } + + #[test] + fn bitand_assign() { + let mut a = bitboard![C5, G7]; + let b = bitboard![B5, G7, H3]; + + a &= b; + + assert_eq!(a, bitboard![G7]); + } + + #[test] + fn bitor_assign() { + let mut a = bitboard![C5, G7]; + let b = bitboard![B5, G7, H3]; + + a |= b; + + assert_eq!(a, bitboard![B5, C5, G7, H3]); + } } From f7951d6110f6a48540de3324943bda25303ba255 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 21 Jan 2024 09:15:41 -0800 Subject: [PATCH 038/423] [board] Implement some is_shape getters on Piece and PlacedPiece --- board/src/piece.rs | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/board/src/piece.rs b/board/src/piece.rs index 6afae98..557f96a 100644 --- a/board/src/piece.rs +++ b/board/src/piece.rs @@ -139,6 +139,14 @@ macro_rules! piece_constructor { }; } +macro_rules! is_shape { + ($func_name:ident, $shape:ident) => { + pub fn $func_name(&self) -> bool { + self.shape == Shape::$shape + } + }; +} + impl Piece { pub fn new(color: Color, shape: Shape) -> Piece { Piece { color, shape } @@ -158,6 +166,13 @@ impl Piece { pub fn shape(&self) -> Shape { self.shape } + + is_shape!(is_pawn, Pawn); + is_shape!(is_knight, Knight); + is_shape!(is_bishop, Bishop); + is_shape!(is_rook, Rook); + is_shape!(is_queen, Queen); + is_shape!(is_king, King); } impl fmt::Display for Piece { @@ -215,6 +230,14 @@ pub struct PlacedPiece { square: Square, } +macro_rules! is_shape { + ($func_name:ident, $shape:ident) => { + pub fn $func_name(&self) -> bool { + self.piece().shape == Shape::$shape + } + }; +} + impl PlacedPiece { pub const fn new(piece: Piece, square: Square) -> PlacedPiece { PlacedPiece { piece, square } @@ -239,6 +262,29 @@ impl PlacedPiece { pub fn shape(&self) -> Shape { self.piece.shape } + + is_shape!(is_pawn, Pawn); + is_shape!(is_knight, Knight); + is_shape!(is_bishop, Bishop); + is_shape!(is_rook, Rook); + is_shape!(is_queen, Queen); + is_shape!(is_king, King); + + pub fn is_kingside_rook(&self) -> bool { + self.is_rook() + && match self.color() { + Color::White => self.square == Square::H1, + Color::Black => self.square == Square::H8, + } + } + + pub fn is_queenside_rook(&self) -> bool { + self.is_rook() + && match self.color() { + Color::White => self.square == Square::A1, + Color::Black => self.square == Square::A8, + } + } } impl fmt::Display for PlacedPiece { From 1a907844d6cb54a29cd8fe675bf6854234941649 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 21 Jan 2024 09:18:16 -0800 Subject: [PATCH 039/423] [board] Update a bunch of methods to take &Piece instead of plain Piece --- board/src/move_generator/bishop.rs | 2 +- board/src/move_generator/king.rs | 2 +- board/src/move_generator/knight.rs | 4 ++-- board/src/move_generator/queen.rs | 2 +- board/src/move_generator/rook.rs | 2 +- board/src/piece.rs | 4 ++-- board/src/position/piece_sets.rs | 10 +++++----- board/src/position/position.rs | 10 +++++++--- 8 files changed, 20 insertions(+), 16 deletions(-) diff --git a/board/src/move_generator/bishop.rs b/board/src/move_generator/bishop.rs index f4f7f9f..88f7631 100644 --- a/board/src/move_generator/bishop.rs +++ b/board/src/move_generator/bishop.rs @@ -48,7 +48,7 @@ impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> { let quiet_moves_bb = all_moves & (empty_squares | !friendly_pieces); let capture_moves_bb = all_moves & opposing_pieces; - let map_to_move = |sq| MoveBuilder::new(piece, square, sq).build(); + let map_to_move = |sq| MoveBuilder::new(*piece, square, sq).build(); let quiet_moves = quiet_moves_bb.occupied_squares().map(map_to_move); let capture_moves = capture_moves_bb.occupied_squares().map(map_to_move); diff --git a/board/src/move_generator/king.rs b/board/src/move_generator/king.rs index b3500fd..06ae76e 100644 --- a/board/src/move_generator/king.rs +++ b/board/src/move_generator/king.rs @@ -56,7 +56,7 @@ impl<'pos> MoveGeneratorInternal for KingMoveGenerator<'pos> { // TODO: Handle checks. Prevent moving a king to a square attacked by a // piece of the opposite color. - let map_to_move = |sq| MoveBuilder::new(piece, square, sq).build(); + let map_to_move = |sq| MoveBuilder::new(*piece, square, sq).build(); let king_side_castle = Self::king_side_castle(position, color); let queen_side_castle = Self::queen_side_castle(position, color); diff --git a/board/src/move_generator/knight.rs b/board/src/move_generator/knight.rs index 3116b1e..50a7ad0 100644 --- a/board/src/move_generator/knight.rs +++ b/board/src/move_generator/knight.rs @@ -22,11 +22,11 @@ impl<'pos> MoveGeneratorInternal for KnightMoveGenerator<'pos> { let capture_moves_bb = knight_moves & opposing_pieces; let quiet_moves = quiet_moves_bb.occupied_squares().map(|to_sq| { - MoveBuilder::new(placed_piece.piece(), placed_piece.square(), to_sq).build() + MoveBuilder::new(*placed_piece.piece(), placed_piece.square(), to_sq).build() }); let capture_moves = capture_moves_bb.occupied_squares().map(|to_sq| { let captured_piece = position.piece_on_square(to_sq).unwrap(); - MoveBuilder::new(placed_piece.piece(), placed_piece.square(), to_sq) + MoveBuilder::new(*placed_piece.piece(), placed_piece.square(), to_sq) .capturing(captured_piece) .build() }); diff --git a/board/src/move_generator/queen.rs b/board/src/move_generator/queen.rs index e7d03b6..6694bf6 100644 --- a/board/src/move_generator/queen.rs +++ b/board/src/move_generator/queen.rs @@ -53,7 +53,7 @@ impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> { let quiet_moves_bb = all_moves & (empty_squares | !friendly_pieces); let capture_moves_bb = all_moves & opposing_pieces; - let map_to_move = |sq| MoveBuilder::new(piece, square, sq).build(); + let map_to_move = |sq| MoveBuilder::new(*piece, square, sq).build(); let quiet_moves = quiet_moves_bb.occupied_squares().map(map_to_move); let capture_moves = capture_moves_bb.occupied_squares().map(map_to_move); diff --git a/board/src/move_generator/rook.rs b/board/src/move_generator/rook.rs index d82c22c..bd48711 100644 --- a/board/src/move_generator/rook.rs +++ b/board/src/move_generator/rook.rs @@ -49,7 +49,7 @@ impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> { let quiet_moves_bb = all_moves & (empty_squares | !friendly_pieces); let capture_moves_bb = all_moves & opposing_pieces; - let map_to_move = |sq| MoveBuilder::new(piece, square, sq).build(); + let map_to_move = |sq| MoveBuilder::new(*piece, square, sq).build(); let quiet_moves = quiet_moves_bb.occupied_squares().map(map_to_move); let capture_moves = capture_moves_bb.occupied_squares().map(map_to_move); diff --git a/board/src/piece.rs b/board/src/piece.rs index 557f96a..fa8333d 100644 --- a/board/src/piece.rs +++ b/board/src/piece.rs @@ -244,8 +244,8 @@ impl PlacedPiece { } #[inline] - pub fn piece(&self) -> Piece { - self.piece + pub fn piece(&self) -> &Piece { + &self.piece } #[inline] diff --git a/board/src/position/piece_sets.rs b/board/src/position/piece_sets.rs index 2e97669..cd8f26d 100644 --- a/board/src/position/piece_sets.rs +++ b/board/src/position/piece_sets.rs @@ -66,11 +66,11 @@ impl PieceBitBoards { self.by_color.bitboard_mut(color) } - pub(super) fn bitboard_for_piece(&self, piece: Piece) -> &BitBoard { + pub(super) fn bitboard_for_piece(&self, piece: &Piece) -> &BitBoard { self.by_color_and_shape.bitboard_for_piece(piece) } - pub(super) fn bitboard_for_piece_mut(&mut self, piece: Piece) -> &mut BitBoard { + pub(super) fn bitboard_for_piece_mut(&mut self, piece: &Piece) -> &mut BitBoard { self.by_color_and_shape.bitboard_for_piece_mut(piece) } @@ -135,15 +135,15 @@ impl ByColor { } impl ByColorAndShape { - fn bitboard_for_piece(&self, piece: Piece) -> &BitBoard { + fn bitboard_for_piece(&self, piece: &Piece) -> &BitBoard { &self.0[piece.color() as usize][piece.shape() as usize] } - fn bitboard_for_piece_mut(&mut self, piece: Piece) -> &mut BitBoard { + fn bitboard_for_piece_mut(&mut self, piece: &Piece) -> &mut BitBoard { &mut self.0[piece.color() as usize][piece.shape() as usize] } - fn set_square(&mut self, square: Square, piece: Piece) { + fn set_square(&mut self, square: Square, piece: &Piece) { self.bitboard_for_piece_mut(piece).set_square(square); } } diff --git a/board/src/position/position.rs b/board/src/position/position.rs index 2ee5564..1c2831d 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -144,7 +144,7 @@ impl Position { for color in Color::iter() { for shape in Shape::iter() { let piece = Piece::new(color, *shape); - if self.pieces.bitboard_for_piece(piece).is_set(sq) { + if self.pieces.bitboard_for_piece(&piece).is_set(sq) { return Some(PlacedPiece::new(piece, sq)); } } @@ -176,7 +176,7 @@ impl Position { fn king_square(&self) -> Square { self.pieces - .bitboard_for_piece(Piece::king(self.color_to_move)) + .bitboard_for_piece(&Piece::king(self.color_to_move)) .occupied_squares() .next() .unwrap() @@ -207,6 +207,10 @@ impl Position { pub(super) fn flags(&self) -> Flags { self.flags } + + pub(super) fn piece_bitboards(&self) -> &PieceBitBoards { + &self.pieces + } } // crate methods @@ -216,7 +220,7 @@ impl Position { } pub(crate) fn bitboard_for_piece(&self, piece: Piece) -> &BitBoard { - self.pieces.bitboard_for_piece(piece) + self.pieces.bitboard_for_piece(&piece) } } From 4a5ae8ec59c68337c29db53ba604d1d1a69a7eca Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 21 Jan 2024 09:20:03 -0800 Subject: [PATCH 040/423] [board] Move position builders into a builders module Move PositionBuilder to position_builder.rs and export it from the builders module. --- board/src/position/builders/mod.rs | 5 +++++ .../position_builder.rs} | 20 ++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 board/src/position/builders/mod.rs rename board/src/position/{builders.rs => builders/position_builder.rs} (80%) diff --git a/board/src/position/builders/mod.rs b/board/src/position/builders/mod.rs new file mode 100644 index 0000000..a8886b3 --- /dev/null +++ b/board/src/position/builders/mod.rs @@ -0,0 +1,5 @@ +// Eryn Wells + +mod position_builder; + +pub use position_builder::Builder as PositionBuilder; diff --git a/board/src/position/builders.rs b/board/src/position/builders/position_builder.rs similarity index 80% rename from board/src/position/builders.rs rename to board/src/position/builders/position_builder.rs index 44daccb..cd895f9 100644 --- a/board/src/position/builders.rs +++ b/board/src/position/builders/position_builder.rs @@ -1,9 +1,11 @@ // Eryn Wells -use super::{flags::Flags, piece_sets::PieceBitBoards}; use crate::{ + bitboard::BitBoardBuilder, piece::{PlacedPiece, Shape}, - square::Rank, + position::{flags::Flags, piece_sets::PieceBitBoards, BoardSide}, + r#move::Castle, + square::{Direction, Rank}, BitBoard, Color, MakeMoveError, Move, Piece, Position, Square, }; use std::collections::BTreeMap; @@ -30,7 +32,7 @@ impl Builder { self.kings[color_index] = square; } - self.pieces.insert(square, piece.piece()); + self.pieces.insert(square, *piece.piece()); self } @@ -81,3 +83,15 @@ impl Default for Builder { } } } + +#[cfg(test)] +mod tests { + use crate::PositionBuilder; + + #[test] + fn place_piece() { + let piece = piece!(White Queen on E4); + let builder = PositionBuilder::new().place_piece(piece).build(); + assert_eq!(builder.piece_on_square(piece.square()), Some(piece)); + } +} From d298a5525481bc3541222ce1394c76049fea54b6 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 21 Jan 2024 09:20:31 -0800 Subject: [PATCH 041/423] [board] Clean up some stray muts and imports in the bishop move generator module --- board/src/move_generator/bishop.rs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/board/src/move_generator/bishop.rs b/board/src/move_generator/bishop.rs index 88f7631..7b07d54 100644 --- a/board/src/move_generator/bishop.rs +++ b/board/src/move_generator/bishop.rs @@ -16,7 +16,6 @@ impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> { fn move_set_for_piece(position: &Position, placed_piece: PlacedPiece) -> MoveSet { let piece = placed_piece.piece(); - let color = piece.color(); let square = placed_piece.square(); let blockers = position.occupied_squares(); @@ -61,13 +60,7 @@ impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> { #[cfg(test)] mod tests { use super::*; - use crate::{ - piece, - piece::{Color, Piece}, - position, - position::DiagramFormatter, - BitBoard, Position, Square, - }; + use crate::{piece, piece::Color, position, position::DiagramFormatter, BitBoard}; #[test] fn classical_single_bishop_bitboard() { @@ -108,7 +101,7 @@ mod tests { /// Test that a rook can move up to, and then capture, an enemy piece. #[test] fn classical_single_bishop_with_opposing_color_blocker_bitboard() { - let mut pos = position![ + let pos = position![ White Bishop on A1, Black Knight on C3, ]; @@ -125,7 +118,7 @@ mod tests { #[test] fn classical_single_bishop_in_center() { - let mut pos = position![ + let pos = position![ White Bishop on E4, ]; From 32ee25539dad3dd59e85ef7152f08e80aeb0a6d7 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 21 Jan 2024 09:21:27 -0800 Subject: [PATCH 042/423] [board] Fix some en passant related stuff in the the sight module --- board/src/sight.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/board/src/sight.rs b/board/src/sight.rs index 46d88b4..b45883e 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -33,7 +33,8 @@ impl PlacedPiece { let mut possible_squares = position.empty_squares() | position.opposing_pieces(); if let Some(en_passant) = position.en_passant_square() { - possible_squares |= en_passant.into(); + let en_passant_bitboard: BitBoard = en_passant.into(); + possible_squares |= en_passant_bitboard; } pawn & possible_squares @@ -45,7 +46,7 @@ impl PlacedPiece { let mut possible_squares = position.empty_squares() | position.opposing_pieces(); if let Some(en_passant) = position.en_passant_square() { - possible_squares |= en_passant.into(); + possible_squares |= &en_passant.into(); } pawn & possible_squares From 7b8ce3b31d8283b5e8b57fb84bc3dadc854b5c88 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 21 Jan 2024 09:21:41 -0800 Subject: [PATCH 043/423] [board] Derive Copy for Move --- board/src/move.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/board/src/move.rs b/board/src/move.rs index 64c7e90..424d7fb 100644 --- a/board/src/move.rs +++ b/board/src/move.rs @@ -85,7 +85,7 @@ impl Default for Kind { } /// A single player's move. In chess parlance, this is a "ply". -#[derive(Clone, Eq, Hash, PartialEq)] +#[derive(Clone, Copy, Eq, Hash, PartialEq)] pub struct Move(u16); impl Move { From 21b5266789fff474dfbc9b391663ea874b03a54b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 21 Jan 2024 09:23:39 -0800 Subject: [PATCH 044/423] [board] Implement position::builders::MoveBuilder This builder takes a Position and a Move, validates the move, and makes the move in that position. Its build() method returns a new Position with the move made. --- board/src/bitboard/bitboard.rs | 2 +- board/src/move.rs | 8 + board/src/position/builders/mod.rs | 2 + board/src/position/builders/move_builder.rs | 273 ++++++++++++++++++++ board/src/position/mod.rs | 2 +- board/src/position/piece_sets.rs | 29 ++- board/src/position/position.rs | 11 + 7 files changed, 323 insertions(+), 4 deletions(-) create mode 100644 board/src/position/builders/move_builder.rs diff --git a/board/src/bitboard/bitboard.rs b/board/src/bitboard/bitboard.rs index 6480e99..6aa4c20 100644 --- a/board/src/bitboard/bitboard.rs +++ b/board/src/bitboard/bitboard.rs @@ -61,7 +61,7 @@ impl BitBoard { *self |= sq_bb } - fn clear_square(&mut self, sq: Square) { + pub fn clear_square(&mut self, sq: Square) { let sq_bb: BitBoard = sq.into(); *self &= !sq_bb } diff --git a/board/src/move.rs b/board/src/move.rs index 424d7fb..45c3f7b 100644 --- a/board/src/move.rs +++ b/board/src/move.rs @@ -10,6 +10,14 @@ use std::fmt; pub(crate) use move_formatter::AlgebraicMoveFormatter; +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum MakeMoveError { + PlayerOutOfTurn, + NoPiece, + NoCapturedPiece, + IllegalCastle, +} + #[repr(u16)] #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum Castle { diff --git a/board/src/position/builders/mod.rs b/board/src/position/builders/mod.rs index a8886b3..9324249 100644 --- a/board/src/position/builders/mod.rs +++ b/board/src/position/builders/mod.rs @@ -1,5 +1,7 @@ // Eryn Wells +mod move_builder; mod position_builder; +pub use move_builder::Builder as MoveBuilder; pub use position_builder::Builder as PositionBuilder; diff --git a/board/src/position/builders/move_builder.rs b/board/src/position/builders/move_builder.rs new file mode 100644 index 0000000..1dac36c --- /dev/null +++ b/board/src/position/builders/move_builder.rs @@ -0,0 +1,273 @@ +// Eryn Wells + +use crate::{ + piece::{PlacedPiece, Shape}, + position::flags::Flags, + r#move::Castle, + square::Direction, + BitBoard, Color, MakeMoveError, Move, Piece, Position, Square, +}; + +/// A position builder that builds a new position by making a move. +#[derive(Clone)] +pub struct Builder<'p, M: MoveToMake> { + position: &'p Position, + move_to_make: M, +} + +pub trait MoveToMake {} + +pub struct NoMove; + +pub enum ValidatedMove { + RegularMove { + from_square: Square, + to_square: Square, + moving_piece: PlacedPiece, + captured_piece: Option, + promotion: Option, + flags: Flags, + en_passant_square: Option, + }, + Castle { + castle: Castle, + king: PlacedPiece, + rook: PlacedPiece, + flags: Flags, + }, +} + +impl MoveToMake for NoMove {} +impl MoveToMake for ValidatedMove {} + +impl<'p, M> Builder<'p, M> +where + M: MoveToMake, +{ + pub fn new(position: &'p Position) -> Builder<'p, NoMove> { + Builder { + position, + move_to_make: NoMove, + } + } + + pub fn make(self, mv: &Move) -> Result, MakeMoveError> { + let from_square = mv.from_square(); + + let piece = self + .position + .piece_on_square(from_square) + .ok_or(MakeMoveError::NoPiece)?; + println!("{}", &piece); + + let to_square = mv.to_square(); + let player = self.position.player_to_move(); + + let captured_piece: Option = if mv.is_capture() { + Some( + self.position + .piece_on_square(to_square) + .ok_or(MakeMoveError::NoCapturedPiece)?, + ) + } else { + None + }; + + // TODO: Check whether the move is legal. + + let piece_is_king = piece.is_king(); + let mut flags = self.position.flags().clone(); + + if piece_is_king { + flags.clear_player_has_right_to_castle_flag(player, Castle::KingSide); + flags.clear_player_has_right_to_castle_flag(player, Castle::QueenSide); + } else if piece.is_kingside_rook() { + flags.clear_player_has_right_to_castle_flag(player, Castle::KingSide); + } else if piece.is_queenside_rook() { + flags.clear_player_has_right_to_castle_flag(player, Castle::QueenSide); + } + + if let Some(castle) = mv.castle() { + println!("piece is king: {}", piece_is_king); + if !piece_is_king || !self.position.player_can_castle(player, castle) { + return Err(MakeMoveError::IllegalCastle); + } + + let rook = self + .position + .rook_for_castle(player, castle) + .ok_or(MakeMoveError::NoPiece)?; + + Ok(Builder { + position: self.position, + move_to_make: ValidatedMove::Castle { + castle, + king: piece, + rook, + flags, + }, + }) + } else { + let en_passant_square: Option = if mv.is_double_push() { + match piece.color() { + Color::White => to_square.neighbor(Direction::South), + Color::Black => to_square.neighbor(Direction::North), + } + } else { + None + }; + + Ok(Builder { + position: self.position, + move_to_make: ValidatedMove::RegularMove { + from_square, + to_square, + moving_piece: piece, + captured_piece, + promotion: mv.promotion(), + flags, + en_passant_square, + }, + }) + } + } +} + +impl<'p> Builder<'p, ValidatedMove> { + pub fn build(&self) -> Position { + let player = self.position.player_to_move(); + + match self.move_to_make { + ValidatedMove::RegularMove { + from_square, + to_square, + moving_piece, + captured_piece, + promotion, + flags, + en_passant_square, + } => { + let mut pieces = self.position.piece_bitboards().clone(); + + if let Some(captured_piece) = captured_piece { + pieces.remove_piece(&captured_piece); + } + + if let Some(promotion) = promotion { + pieces.remove_piece(&moving_piece); + let _ = pieces + .place_piece(PlacedPiece::new(Piece::new(player, promotion), to_square)); + } else { + pieces.move_piece(moving_piece.piece(), from_square, to_square); + } + + Position::new( + self.position.player_to_move().other(), + flags, + pieces, + en_passant_square, + ) + } + ValidatedMove::Castle { + castle, + king, + rook, + flags, + } => { + let mut pieces = self.position.piece_bitboards().clone(); + + let king_from: BitBoard = king.square().into(); + let king_to: BitBoard = Square::king_castle_target(player, castle).into(); + *pieces.bitboard_for_piece_mut(king.piece()) ^= king_from | king_to; + + let rook_from: BitBoard = rook.square().into(); + let rook_to: BitBoard = Square::rook_castle_target(player, castle).into(); + *pieces.bitboard_for_piece_mut(rook.piece()) ^= rook_from | rook_to; + + *pieces.bitboard_for_color_mut(player) &= + !(king_from | rook_from) | (king_to | rook_to); + + Position::new(player.other(), flags, pieces, None) + } + } + } +} + +impl<'p> From<&'p Position> for Builder<'p, NoMove> { + fn from(position: &'p Position) -> Self { + Self { + position, + move_to_make: NoMove, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{r#move::Castle, MoveBuilder}; + + #[test] + fn move_white_pawn_one_square() -> Result<(), MakeMoveError> { + let pos = position![White Pawn on E2]; + let mv = MoveBuilder::new(piece!(White Pawn), Square::E2, Square::E3).build(); + + let new_position = Builder::::new(&pos).make(&mv)?.build(); + println!("{}", &new_position); + + assert_eq!( + new_position.piece_on_square(Square::E3), + Some(piece!(White Pawn on E3)) + ); + + Ok(()) + } + + #[test] + fn move_white_pawn_two_squares() -> Result<(), MakeMoveError> { + let pos = position![White Pawn on E2]; + let mv = MoveBuilder::new(piece!(White Pawn), Square::E2, Square::E4).build(); + + let new_position = Builder::::new(&pos).make(&mv)?.build(); + println!("{}", &new_position); + + assert_eq!( + new_position.piece_on_square(Square::E4), + Some(piece!(White Pawn on E4)) + ); + assert_eq!(new_position.en_passant_square(), Some(Square::E3)); + + Ok(()) + } + + #[test] + fn white_kingside_castle() -> Result<(), MakeMoveError> { + let pos = position![ + White King on E1, + White Rook on H1, + White Pawn on E2, + White Pawn on F2, + White Pawn on G2, + White Pawn on H2 + ]; + println!("{}", &pos); + + let mv = MoveBuilder::new(piece!(White King), Square::E1, Square::G1) + .castle(Castle::KingSide) + .build(); + + let new_position = Builder::::new(&pos).make(&mv)?.build(); + println!("{}", &new_position); + + assert_eq!( + new_position.piece_on_square(Square::G1), + Some(piece!(White King on G1)) + ); + assert_eq!( + new_position.piece_on_square(Square::F1), + Some(piece!(White Rook on F1)) + ); + + Ok(()) + } +} diff --git a/board/src/position/mod.rs b/board/src/position/mod.rs index 987c107..b6805e3 100644 --- a/board/src/position/mod.rs +++ b/board/src/position/mod.rs @@ -7,7 +7,7 @@ mod piece_sets; mod pieces; mod position; -pub use builders::Builder as PositionBuilder; +pub use builders::{MoveBuilder, PositionBuilder}; pub use diagram_formatter::DiagramFormatter; pub use pieces::Pieces; pub use position::Position; diff --git a/board/src/position/piece_sets.rs b/board/src/position/piece_sets.rs index cd8f26d..07a1f03 100644 --- a/board/src/position/piece_sets.rs +++ b/board/src/position/piece_sets.rs @@ -95,12 +95,28 @@ impl PieceBitBoards { } } + self.by_color_and_shape.set_square(square, piece.piece()); self.by_color.set_square(square, color); - self.bitboard_for_piece_mut(piece.piece()) - .set_square(square); Ok(()) } + + pub(super) fn remove_piece(&mut self, piece: &PlacedPiece) { + let color = piece.color(); + let square = piece.square(); + + self.by_color_and_shape.clear_square(square, piece.piece()); + self.by_color.clear_square(square, color); + } + + pub(super) fn move_piece(&mut self, piece: &Piece, from_square: Square, to_square: Square) { + let color = piece.color(); + + self.by_color_and_shape.clear_square(from_square, piece); + self.by_color.clear_square(from_square, color); + self.by_color_and_shape.set_square(to_square, piece); + self.by_color.set_square(to_square, color); + } } impl FromIterator for PieceBitBoards { @@ -132,6 +148,11 @@ impl ByColor { self.0.set_square(square); self.1[color as usize].set_square(square) } + + fn clear_square(&mut self, square: Square, color: Color) { + self.0.clear_square(square); + self.1[color as usize].clear_square(square); + } } impl ByColorAndShape { @@ -146,4 +167,8 @@ impl ByColorAndShape { fn set_square(&mut self, square: Square, piece: &Piece) { self.bitboard_for_piece_mut(piece).set_square(square); } + + fn clear_square(&mut self, square: Square, piece: &Piece) { + self.bitboard_for_piece_mut(piece).clear_square(square); + } } diff --git a/board/src/position/position.rs b/board/src/position/position.rs index 1c2831d..b547d09 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -109,6 +109,17 @@ impl Position { true } + pub(crate) fn rook_for_castle(&self, player: Color, castle: Castle) -> Option { + let square = match (player, castle) { + (Color::White, Castle::KingSide) => Square::H1, + (Color::White, Castle::QueenSide) => Square::A1, + (Color::Black, Castle::KingSide) => Square::H8, + (Color::Black, Castle::QueenSide) => Square::A8, + }; + + self.piece_on_square(square) + } + pub fn moves(&self) -> Moves { Moves::new(self, self.color_to_move) } From fa1c6b452e33b079e7c7f3fe740b668afc82217e Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 21 Jan 2024 10:38:50 -0800 Subject: [PATCH 045/423] [board] Remove BoardSide enum; use Castle everywhere Move Castle to a castle module inside the move module. Implement into_index() on Castle to turn it into a usize. --- board/src/lib.rs | 2 +- board/src/move.rs | 87 ++++++++++++++++--- board/src/position/builders/move_builder.rs | 6 +- .../src/position/builders/position_builder.rs | 17 ++-- board/src/position/flags.rs | 6 +- board/src/position/mod.rs | 2 - board/src/position/position.rs | 17 ---- board/src/square.rs | 21 ++--- 8 files changed, 95 insertions(+), 63 deletions(-) diff --git a/board/src/lib.rs b/board/src/lib.rs index d91df46..b267517 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -14,7 +14,7 @@ mod square; pub use piece::{Color, Piece}; pub use position::{Position, PositionBuilder}; -pub use r#move::{MakeMoveError, Move, MoveBuilder}; +pub use r#move::{Castle, MakeMoveError, Move, MoveBuilder}; pub use square::{File, Rank, Square}; pub(crate) use bitboard::BitBoard; diff --git a/board/src/move.rs b/board/src/move.rs index 45c3f7b..3e48c50 100644 --- a/board/src/move.rs +++ b/board/src/move.rs @@ -2,12 +2,12 @@ use crate::{ piece::{Piece, PlacedPiece, Shape}, - position::BoardSide, square::Rank, Square, }; use std::fmt; +pub use castle::Castle; pub(crate) use move_formatter::AlgebraicMoveFormatter; #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -18,18 +18,81 @@ pub enum MakeMoveError { IllegalCastle, } -#[repr(u16)] -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum Castle { - KingSide = 0b10, - QueenSide = 0b11, -} +mod castle { + use crate::{Color, Square}; -impl From for Castle { - fn from(value: BoardSide) -> Self { - match value { - BoardSide::King => Castle::KingSide, - BoardSide::Queen => Castle::QueenSide, + #[repr(u16)] + #[derive(Copy, Clone, Debug, Eq, PartialEq)] + pub enum Castle { + KingSide = 0b10, + QueenSide = 0b11, + } + + pub(crate) struct Squares { + pub king: Square, + pub rook: Square, + } + + impl Castle { + const STARTING_SQUARES: [[Squares; 2]; 2] = [ + [ + Squares { + king: Square::E1, + rook: Square::H1, + }, + Squares { + king: Square::E1, + rook: Square::A1, + }, + ], + [ + Squares { + king: Square::E8, + rook: Square::H8, + }, + Squares { + king: Square::E8, + rook: Square::A8, + }, + ], + ]; + + const TARGET_SQUARES: [[Squares; 2]; 2] = [ + [ + Squares { + king: Square::G1, + rook: Square::F1, + }, + Squares { + king: Square::C1, + rook: Square::D1, + }, + ], + [ + Squares { + king: Square::G8, + rook: Square::F8, + }, + Squares { + king: Square::C8, + rook: Square::D8, + }, + ], + ]; + + pub(crate) fn starting_squares(&self, color: Color) -> &'static Squares { + &Castle::STARTING_SQUARES[color as usize][self.into_index()] + } + + pub(crate) fn target_squares(&self, color: Color) -> &'static Squares { + &Castle::TARGET_SQUARES[color as usize][self.into_index()] + } + + pub(crate) fn into_index(&self) -> usize { + match self { + Castle::KingSide => 0, + Castle::QueenSide => 1, + } } } } diff --git a/board/src/position/builders/move_builder.rs b/board/src/position/builders/move_builder.rs index 1dac36c..e899793 100644 --- a/board/src/position/builders/move_builder.rs +++ b/board/src/position/builders/move_builder.rs @@ -176,12 +176,14 @@ impl<'p> Builder<'p, ValidatedMove> { } => { let mut pieces = self.position.piece_bitboards().clone(); + let target_squares = castle.target_squares(player); + let king_from: BitBoard = king.square().into(); - let king_to: BitBoard = Square::king_castle_target(player, castle).into(); + let king_to: BitBoard = target_squares.king.into(); *pieces.bitboard_for_piece_mut(king.piece()) ^= king_from | king_to; let rook_from: BitBoard = rook.square().into(); - let rook_to: BitBoard = Square::rook_castle_target(player, castle).into(); + let rook_to: BitBoard = target_squares.rook.into(); *pieces.bitboard_for_piece_mut(rook.piece()) ^= rook_from | rook_to; *pieces.bitboard_for_color_mut(player) &= diff --git a/board/src/position/builders/position_builder.rs b/board/src/position/builders/position_builder.rs index cd895f9..d9c0a18 100644 --- a/board/src/position/builders/position_builder.rs +++ b/board/src/position/builders/position_builder.rs @@ -3,7 +3,7 @@ use crate::{ bitboard::BitBoardBuilder, piece::{PlacedPiece, Shape}, - position::{flags::Flags, piece_sets::PieceBitBoards, BoardSide}, + position::{flags::Flags, piece_sets::PieceBitBoards}, r#move::Castle, square::{Direction, Rank}, BitBoard, Color, MakeMoveError, Move, Piece, Position, Square, @@ -64,22 +64,19 @@ impl Builder { impl Default for Builder { fn default() -> Self { + let white_king_square = Square::king_starting_square(Color::White); + let black_king_square = Square::king_starting_square(Color::Black); + let pieces = BTreeMap::from_iter([ - ( - Square::KING_STARTING_SQUARES[Color::White as usize], - piece!(White King), - ), - ( - Square::KING_STARTING_SQUARES[Color::Black as usize], - piece!(Black King), - ), + (white_king_square, piece!(White King)), + (black_king_square, piece!(Black King)), ]); Self { player_to_move: Color::White, flags: Flags::default(), pieces: pieces, - kings: Square::KING_STARTING_SQUARES, + kings: [white_king_square, black_king_square], } } } diff --git a/board/src/position/flags.rs b/board/src/position/flags.rs index 8bc0550..e92b9f6 100644 --- a/board/src/position/flags.rs +++ b/board/src/position/flags.rs @@ -1,6 +1,5 @@ // Eryn Wells -use super::position::BoardSide; use crate::{r#move::Castle, Color}; use std::fmt; @@ -9,9 +8,8 @@ pub(super) struct Flags(u8); impl Flags { #[inline] - pub(super) fn player_has_right_to_castle_flag_offset(color: Color, castle: Castle) -> u8 { - let board_side: BoardSide = castle.into(); - ((color as u8) << 1) + board_side as u8 + pub(super) fn player_has_right_to_castle_flag_offset(color: Color, castle: Castle) -> usize { + ((color as usize) << 1) + castle.into_index() } pub(super) fn player_has_right_to_castle(&self, color: Color, castle: Castle) -> bool { diff --git a/board/src/position/mod.rs b/board/src/position/mod.rs index b6805e3..a03e392 100644 --- a/board/src/position/mod.rs +++ b/board/src/position/mod.rs @@ -11,5 +11,3 @@ pub use builders::{MoveBuilder, PositionBuilder}; pub use diagram_formatter::DiagramFormatter; pub use pieces::Pieces; pub use position::Position; - -pub(crate) use position::BoardSide; diff --git a/board/src/position/position.rs b/board/src/position/position.rs index b547d09..f0535f7 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -11,23 +11,6 @@ use crate::{ }; use std::{cell::OnceCell, fmt}; -/// A lateral side of the board relative to the player. Kingside is the side the -/// player's king starts on. Queenside is the side of the board the player's -/// queen starts on. -pub(crate) enum BoardSide { - King = 0, - Queen = 1, -} - -impl From for BoardSide { - fn from(value: Castle) -> Self { - match value { - Castle::KingSide => BoardSide::King, - Castle::QueenSide => BoardSide::Queen, - } - } -} - #[derive(Clone, Debug, Eq, PartialEq)] pub struct Position { color_to_move: Color, diff --git a/board/src/square.rs b/board/src/square.rs index b04aede..4501178 100644 --- a/board/src/square.rs +++ b/board/src/square.rs @@ -1,6 +1,6 @@ // Eryn Wells -use crate::{position::BoardSide, r#move::Castle, Color}; +use crate::{r#move::Castle, Color}; use std::{fmt, str::FromStr}; pub enum Direction { @@ -156,25 +156,16 @@ impl Square { } impl Square { - pub(crate) const KING_STARTING_SQUARES: [Square; 2] = [Square::E1, Square::E8]; - pub(crate) const KING_CASTLE_TARGET_SQUARES: [[Square; 2]; 2] = - [[Square::G1, Square::C1], [Square::G8, Square::C8]]; - pub(crate) const ROOK_CASTLE_TARGET_SQUARES: [[Square; 2]; 2] = - [[Square::F1, Square::D1], [Square::F8, Square::D8]]; + const KING_STARTING_SQUARES: [Square; 2] = [Square::E1, Square::E8]; + + pub fn king_starting_square(color: Color) -> Square { + Square::KING_STARTING_SQUARES[color as usize] + } pub fn from_algebraic_str(s: &str) -> Result { s.parse() } - pub fn king_castle_target(player: Color, castle: Castle) -> Square { - let board_side: BoardSide = castle.into(); - Self::KING_CASTLE_TARGET_SQUARES[player as usize][board_side as usize] - } - - pub fn rook_castle_target(player: Color, castle: Castle) -> Square { - let board_side: BoardSide = castle.into(); - Self::ROOK_CASTLE_TARGET_SQUARES[player as usize][board_side as usize] - } pub fn neighbor(self, direction: Direction) -> Option { match direction { Direction::North => Square::try_index(self as usize + 8), From 8835d8b40e3f138d1349c2a395698c4895815450 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 21 Jan 2024 10:39:24 -0800 Subject: [PATCH 046/423] [board] Add position::tests::rook_for_castle --- board/src/position/position.rs | 20 +++++++++++++++++++- board/src/sight.rs | 2 +- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/board/src/position/position.rs b/board/src/position/position.rs index f0535f7..9e1dfa6 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -239,7 +239,7 @@ impl fmt::Display for Position { #[cfg(test)] mod tests { - use crate::position; + use crate::{position, Castle, Color}; #[test] fn king_is_in_check() { @@ -258,4 +258,22 @@ mod tests { ]; assert!(!pos.is_king_in_check()); } + + #[test] + fn rook_for_castle() { + let pos = position![ + White King on E1, + White Rook on H1, + White Rook on A1, + ]; + + assert_eq!( + pos.rook_for_castle(Color::White, Castle::KingSide), + Some(piece!(White Rook on H1)) + ); + assert_eq!( + pos.rook_for_castle(Color::White, Castle::QueenSide), + Some(piece!(White Rook on A1)) + ); + } } diff --git a/board/src/sight.rs b/board/src/sight.rs index b45883e..9f4a10b 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -230,7 +230,7 @@ mod tests { #[macro_use] mod knight { - use crate::{sight::Sight, Position}; + use crate::sight::Sight; sight_test!( f6_knight, From 683d89b726c3a489bdf3e4420a0aed7d439b439a Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 21 Jan 2024 10:39:57 -0800 Subject: [PATCH 047/423] [board] Convert a few more Piece and PlacedPiece to pass by ref --- board/src/position/builders/move_builder.rs | 2 +- board/src/position/piece_sets.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/board/src/position/builders/move_builder.rs b/board/src/position/builders/move_builder.rs index e899793..97802c1 100644 --- a/board/src/position/builders/move_builder.rs +++ b/board/src/position/builders/move_builder.rs @@ -156,7 +156,7 @@ impl<'p> Builder<'p, ValidatedMove> { if let Some(promotion) = promotion { pieces.remove_piece(&moving_piece); let _ = pieces - .place_piece(PlacedPiece::new(Piece::new(player, promotion), to_square)); + .place_piece(&PlacedPiece::new(Piece::new(player, promotion), to_square)); } else { pieces.move_piece(moving_piece.piece(), from_square, to_square); } diff --git a/board/src/position/piece_sets.rs b/board/src/position/piece_sets.rs index 07a1f03..cc01eda 100644 --- a/board/src/position/piece_sets.rs +++ b/board/src/position/piece_sets.rs @@ -74,13 +74,13 @@ impl PieceBitBoards { self.by_color_and_shape.bitboard_for_piece_mut(piece) } - pub(super) fn place_piece(&mut self, piece: PlacedPiece) -> Result<(), PlacePieceError> { + pub(super) fn place_piece(&mut self, piece: &PlacedPiece) -> Result<(), PlacePieceError> { self.place_piece_with_strategy(piece, Default::default()) } pub(super) fn place_piece_with_strategy( &mut self, - piece: PlacedPiece, + piece: &PlacedPiece, strategy: PlacePieceStrategy, ) -> Result<(), PlacePieceError> { let color = piece.color(); @@ -124,7 +124,7 @@ impl FromIterator for PieceBitBoards { let mut pieces = Self::default(); for piece in iter { - let _ = pieces.place_piece(piece); + let _ = pieces.place_piece(&piece); } pieces From f90ea2d1be923550f0e549eefa7bbbf9bdb1b90c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 21 Jan 2024 11:21:37 -0800 Subject: [PATCH 048/423] [board] Add a test for capturing en passant; fix the bugs I wrote a test for capturing en passant that revealed some bugs. Cool! Implement the missing part of move validation that checks for an en passant move. Implement PositionBuilder::to_move() to set player_to_move. The most difficult part of this fix was finding the logic error in Move::is_en_passant(). Instead of checking for non-zero, check for equality against the en passant flag value. Checking for non-zero was causing this method to return true in the double push case. --- board/src/move.rs | 7 ++- board/src/position/builders/move_builder.rs | 60 ++++++++++++++++++- .../src/position/builders/position_builder.rs | 5 ++ 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/board/src/move.rs b/board/src/move.rs index 3e48c50..4f06000 100644 --- a/board/src/move.rs +++ b/board/src/move.rs @@ -193,7 +193,7 @@ impl Move { } pub fn is_en_passant(&self) -> bool { - (self.0 & 0b0101) != 0 + (self.0 & 0b0101) == 0b0101 } pub fn is_promotion(&self) -> bool { @@ -272,6 +272,11 @@ impl MoveBuilder { self } + pub fn capturing_en_passant(mut self, captured_piece: PlacedPiece) -> Self { + self.kind = Kind::EnPassantCapture(captured_piece); + self + } + pub fn promoting_to(mut self, shape: Shape) -> Self { if let Some(shape) = PromotableShape::try_from(shape).ok() { self.kind = match self.kind { diff --git a/board/src/position/builders/move_builder.rs b/board/src/position/builders/move_builder.rs index 97802c1..e0d2765 100644 --- a/board/src/position/builders/move_builder.rs +++ b/board/src/position/builders/move_builder.rs @@ -63,7 +63,20 @@ where let to_square = mv.to_square(); let player = self.position.player_to_move(); - let captured_piece: Option = if mv.is_capture() { + let captured_piece: Option = if mv.is_en_passant() { + // En passant captures the pawn directly ahead (in the player's direction) of the en passant square. + let capture_square = match player { + Color::White => to_square.neighbor(Direction::South), + Color::Black => to_square.neighbor(Direction::North), + } + .ok_or(MakeMoveError::NoCapturedPiece)?; + + Some( + self.position + .piece_on_square(capture_square) + .ok_or(MakeMoveError::NoCapturedPiece)?, + ) + } else if mv.is_capture() { Some( self.position .piece_on_square(to_square) @@ -207,7 +220,7 @@ impl<'p> From<&'p Position> for Builder<'p, NoMove> { #[cfg(test)] mod tests { use super::*; - use crate::{r#move::Castle, MoveBuilder}; + use crate::{r#move::Castle, MoveBuilder, PositionBuilder}; #[test] fn move_white_pawn_one_square() -> Result<(), MakeMoveError> { @@ -272,4 +285,47 @@ mod tests { Ok(()) } + + #[test] + fn en_passant_capture() -> Result<(), MakeMoveError> { + let pos = PositionBuilder::new() + .place_piece(piece!(White Pawn on B5)) + .place_piece(piece!(Black Pawn on A7)) + .to_move(Color::Black) + .build(); + println!("{pos}"); + + let black_pawn_move = MoveBuilder::new(piece!(Black Pawn), Square::A7, Square::A5).build(); + assert!(black_pawn_move.is_double_push()); + assert!(!black_pawn_move.is_en_passant()); + + let en_passant_position = Builder::::new(&pos).make(&black_pawn_move)?.build(); + println!("{en_passant_position}"); + + assert_eq!( + en_passant_position.piece_on_square(Square::A5), + Some(piece!(Black Pawn on A5)) + ); + assert_eq!( + en_passant_position.piece_on_square(Square::B5), + Some(piece!(White Pawn on B5)) + ); + + let white_pawn_capture = MoveBuilder::new(piece!(White Pawn), Square::B5, Square::A6) + .capturing_en_passant(piece!(Black Pawn on A5)) + .build(); + let en_passant_capture = Builder::::new(&en_passant_position) + .make(&white_pawn_capture)? + .build(); + println!("{en_passant_capture}"); + + assert_eq!(en_passant_capture.piece_on_square(Square::A5), None); + assert_eq!(en_passant_capture.piece_on_square(Square::B5), None); + assert_eq!( + en_passant_capture.piece_on_square(Square::A6), + Some(piece!(White Pawn on A6)) + ); + + Ok(()) + } } diff --git a/board/src/position/builders/position_builder.rs b/board/src/position/builders/position_builder.rs index d9c0a18..c99625b 100644 --- a/board/src/position/builders/position_builder.rs +++ b/board/src/position/builders/position_builder.rs @@ -23,6 +23,11 @@ impl Builder { Self::default() } + pub fn to_move(&mut self, player: Color) -> &mut Self { + self.player_to_move = player; + self + } + pub fn place_piece(&mut self, piece: PlacedPiece) -> &mut Self { let square = piece.square(); From 72316ad5f55534aced17d1ee3d98917f3dcaa36a Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 21 Jan 2024 13:01:03 -0800 Subject: [PATCH 049/423] [board] Write a bunch of tests to validate the flag methods on Move I caught a bunch of bugs while implementing position::MoveBuilder in the flag methods that Move exports. These tests caught many of them! --- board/src/move.rs | 98 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 92 insertions(+), 6 deletions(-) diff --git a/board/src/move.rs b/board/src/move.rs index 4f06000..ade1ff8 100644 --- a/board/src/move.rs +++ b/board/src/move.rs @@ -169,11 +169,11 @@ impl Move { } pub fn is_quiet(&self) -> bool { - self.special() == 0b00 + self.flags() == Kind::Quiet.discriminant() } pub fn is_double_push(&self) -> bool { - self.special() == 0b01 + self.flags() == Kind::DoublePush.discriminant() } pub fn is_castle(&self) -> bool { @@ -181,7 +181,7 @@ impl Move { } pub fn castle(&self) -> Option { - match self.special() { + match self.flags() { 0b0010 => Some(Castle::KingSide), 0b0011 => Some(Castle::QueenSide), _ => None, @@ -193,7 +193,7 @@ impl Move { } pub fn is_en_passant(&self) -> bool { - (self.0 & 0b0101) == 0b0101 + self.flags() == 0b0101 } pub fn is_promotion(&self) -> bool { @@ -214,6 +214,11 @@ impl Move { }) } + #[inline] + fn flags(&self) -> u16 { + self.0 & 0b1111 + } + #[inline] fn special(&self) -> u16 { self.0 & 0b11 @@ -240,8 +245,10 @@ impl MoveBuilder { pub fn new(piece: Piece, from: Square, to: Square) -> Self { let kind = match piece.shape() { Shape::Pawn => { - let is_white_double_push = from.rank() == Rank::Two && to.rank() == Rank::Four; - let is_black_double_push = from.rank() == Rank::Seven && to.rank() == Rank::Five; + let from_rank = from.rank(); + let to_rank = to.rank(); + let is_white_double_push = from_rank == Rank::Two && to_rank == Rank::Four; + let is_black_double_push = from_rank == Rank::Seven && to_rank == Rank::Five; if is_white_double_push || is_black_double_push { Kind::DoublePush } else { @@ -478,3 +485,82 @@ mod move_formatter { test_algebraic_formatter!(long_bishop_capture, Long, White Bishop A2 x E6, Black Knight, "Ba2xe6"); } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::piece; + + macro_rules! assert_flag { + ($move:expr, $left:expr, $right:expr) => { + assert_eq!($left, $right, "{:?}", $move) + }; + } + + macro_rules! assert_flags { + ($move:expr, $quiet:expr, $double_push:expr, $en_passant:expr, $capture:expr, $castle:expr, $promotion:expr) => { + assert_flag!($move, $move.is_quiet(), $quiet); + assert_flag!($move, $move.is_quiet(), $quiet); + assert_flag!($move, $move.is_double_push(), $double_push); + assert_flag!($move, $move.is_en_passant(), $en_passant); + assert_flag!($move, $move.is_capture(), $capture); + assert_flag!($move, $move.is_castle(), $castle); + assert_flag!($move, $move.is_promotion(), $promotion); + }; + } + + #[test] + fn move_flags_quiet() { + let mv = MoveBuilder::new(piece!(White Pawn), Square::A4, Square::A5).build(); + assert_flags!(mv, true, false, false, false, false, false); + } + + #[test] + fn move_flags_double_push() { + let mv = MoveBuilder::new(piece!(White Pawn), Square::C2, Square::C4).build(); + assert_flags!(mv, false, true, false, false, false, false); + } + + #[test] + fn move_flags_capture() { + let mv = MoveBuilder::new(piece!(White Pawn), Square::A4, Square::B5) + .capturing(piece!(Black Bishop on B5)) + .build(); + assert_flags!(mv, false, false, false, true, false, false); + } + + #[test] + fn move_flags_en_passant_capture() { + let mv = MoveBuilder::new(piece!(White Pawn), Square::A5, Square::B6) + .capturing_en_passant(piece!(Black Pawn on B5)) + .build(); + assert_flags!(mv, false, false, true, true, false, false); + } + + #[test] + fn move_flags_promotion() { + let mv = MoveBuilder::new(piece!(White Pawn), Square::H7, Square::H8) + .promoting_to(Shape::Queen) + .build(); + assert_flags!(mv, false, false, false, false, false, true); + assert_eq!(mv.promotion(), Some(Shape::Queen)); + } + + #[test] + fn move_flags_capture_promotion() { + let mv = MoveBuilder::new(piece!(White Pawn), Square::H7, Square::G8) + .capturing(piece!(Black Knight on G8)) + .promoting_to(Shape::Queen) + .build(); + assert_flags!(mv, false, false, false, true, false, true); + assert_eq!(mv.promotion(), Some(Shape::Queen)); + } + + #[test] + fn move_flags_castle() { + let mv = MoveBuilder::new(piece!(Black King), Square::E8, Square::G8) + .castle(Castle::KingSide) + .build(); + assert_flags!(mv, false, false, false, false, true, false); + } +} From 3c699b0b3d7c338b4bcda2b2e8c568aed4ec0a05 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 21 Jan 2024 13:02:06 -0800 Subject: [PATCH 050/423] [board] Remove the intermediate String when building a diagram representation of a Position Also ignore the tests in position::diagram_formatter::tests. These don't assert anything; they're just there for visual spot check. --- board/src/position/diagram_formatter.rs | 30 ++++++++++++------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/board/src/position/diagram_formatter.rs b/board/src/position/diagram_formatter.rs index 7bd8ccd..b007d20 100644 --- a/board/src/position/diagram_formatter.rs +++ b/board/src/position/diagram_formatter.rs @@ -1,7 +1,7 @@ // Eryn Wells use crate::{File, Position, Rank, Square}; -use std::{fmt, fmt::Write}; +use std::fmt; pub struct DiagramFormatter<'a>(&'a Position); @@ -13,38 +13,36 @@ impl<'a> DiagramFormatter<'a> { impl<'a> fmt::Display for DiagramFormatter<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut output = String::new(); - - output.push_str(" +-----------------+\n"); + write!(f, " +-----------------+\n")?; for rank in Rank::ALL.iter().rev() { - write!(output, "{} | ", rank)?; + write!(f, "{rank} | ")?; for file in File::ALL.iter() { let square = Square::from_file_rank(*file, *rank); match self.0.piece_on_square(square) { - Some(placed_piece) => write!(output, "{} ", placed_piece.piece())?, - None => output.push_str(". "), + Some(placed_piece) => write!(f, "{} ", placed_piece.piece())?, + None => write!(f, ". ")?, } } - output.push_str("|\n"); + write!(f, "|\n")?; } - output.push_str(" +-----------------+\n"); - output.push_str(" a b c d e f g h\n"); + write!(f, " +-----------------+\n")?; + write!(f, " a b c d e f g h\n")?; - write!(f, "{}", output) + Ok(()) } } #[cfg(test)] mod tests { use super::*; - use crate::piece::{Color, Piece}; - use crate::{Position, PositionBuilder}; + use crate::Position; #[test] + #[ignore] fn empty() { let pos = Position::empty(); let diagram = DiagramFormatter(&pos); @@ -52,15 +50,15 @@ mod tests { } #[test] + #[ignore] fn one_king() { - let pos = PositionBuilder::new() - .place_piece(piece!(Black King on H3)) - .build(); + let pos = position![Black King on H3]; let diagram = DiagramFormatter(&pos); println!("{}", diagram); } #[test] + #[ignore] fn starting() { let pos = Position::starting(); let diagram = DiagramFormatter(&pos); From 704ee2f4253e426f563ff00691e236bb8cebf694 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 21 Jan 2024 13:06:44 -0800 Subject: [PATCH 051/423] [board] Implement a test_position macro that prints a Position after it builds it Update the sight tests to use test_position! Remove a stray leftover mut. Clean up the test imports. --- board/src/macros.rs | 14 ++++++++++++++ board/src/sight.rs | 12 +++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/board/src/macros.rs b/board/src/macros.rs index 0b88d0f..9dd3323 100644 --- a/board/src/macros.rs +++ b/board/src/macros.rs @@ -18,3 +18,17 @@ macro_rules! position { .build() }; } + +#[cfg(test)] +#[macro_export] +macro_rules! test_position { + [$($color:ident $shape:ident on $square:ident),* $(,)?] => { + { + let pos = $crate::PositionBuilder::new() + $(.place_piece(piece!($color $shape on $square)))* + .build(); + println!("{pos}"); + pos + } + }; +} diff --git a/board/src/sight.rs b/board/src/sight.rs index 9f4a10b..33bd2d8 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -174,13 +174,13 @@ mod tests { } mod pawn { - use crate::{position, sight::Sight, BitBoard, Position, Square}; + use crate::{sight::Sight, BitBoard, Square}; sight_test!(e4_pawn, piece!(White Pawn on E4), bitboard!(D5, F5)); sight_test!( e4_pawn_one_blocker, - position![ + test_position![ White Bishop on D5, White Pawn on E4, ], @@ -190,11 +190,12 @@ mod tests { #[test] fn e4_pawn_two_blocker() { - let pos = position!( + let pos = test_position!( White Bishop on D5, White Queen on F5, White Pawn on E4, ); + let pp = piece!(White Pawn on E4); let sight = pp.sight_in_position(&pos); @@ -203,11 +204,12 @@ mod tests { #[test] fn e4_pawn_capturable() { - let pos = position!( + let pos = test_position!( Black Bishop on D5, White Queen on F5, White Pawn on E4, ); + let pp = piece!(White Pawn on E4); let sight = pp.sight_in_position(&pos); @@ -216,7 +218,7 @@ mod tests { #[test] fn e5_en_passant() { - let mut pos = position!( + let mut pos = test_position!( White Pawn on E5, Black Pawn on D5, ); From 84c9c43a7deab6ae0e7ca64e79fc8e003fa31954 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 21 Jan 2024 13:07:07 -0800 Subject: [PATCH 052/423] [board] Add a few comments in position::position One TODO And one doc comment. --- board/src/position/position.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/board/src/position/position.rs b/board/src/position/position.rs index 9e1dfa6..537208d 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -89,9 +89,12 @@ impl Position { return false; } + // TODO: Perform a real check that the player can castle. + true } + /// Return a PlacedPiece representing the rook to use for a castling move. pub(crate) fn rook_for_castle(&self, player: Color, castle: Castle) -> Option { let square = match (player, castle) { (Color::White, Castle::KingSide) => Square::H1, From 829d9af52c7949b8ed491035f1f11877df81ca85 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 21 Jan 2024 14:56:03 -0800 Subject: [PATCH 053/423] [board] Fix some bugs in the starting position MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Turns out I was doing the starting position really wrong. In an upcoming commit, I will implement FEN output for Positions. While doing that work, I found several issues that were causing the output of the FEN formatter to return garbage. Implement a handful of unit tests to track down the errors. Rename Shape::_ascii_representation() → Shape::to_ascii. Implement to_ascii() on Piece. --- board/src/bitboard/bitboard.rs | 9 +++++- board/src/piece.rs | 48 +++++++++++++++------------- board/src/position/piece_sets.rs | 9 ++++-- board/src/position/position.rs | 55 +++++++++++++++++++++++--------- board/src/square.rs | 8 ++++- 5 files changed, 87 insertions(+), 42 deletions(-) diff --git a/board/src/bitboard/bitboard.rs b/board/src/bitboard/bitboard.rs index 6aa4c20..5c35ca8 100644 --- a/board/src/bitboard/bitboard.rs +++ b/board/src/bitboard/bitboard.rs @@ -53,7 +53,8 @@ impl BitBoard { } pub fn is_set(self, sq: Square) -> bool { - !(self & &sq.into()).is_empty() + let square_bitboard: BitBoard = sq.into(); + !(self & square_bitboard).is_empty() } pub fn set_square(&mut self, sq: Square) { @@ -341,4 +342,10 @@ mod tests { assert_eq!(a, bitboard![B5, C5, G7, H3]); } + + #[test] + fn from_square() { + assert_eq!(BitBoard::from(Square::A1), BitBoard(0b1)); + assert_eq!(BitBoard::from(Square::H8), BitBoard(1 << 63)); + } } diff --git a/board/src/piece.rs b/board/src/piece.rs index fa8333d..45ed4b0 100644 --- a/board/src/piece.rs +++ b/board/src/piece.rs @@ -63,7 +63,7 @@ impl Shape { PROMOTABLE_SHAPES.iter() } - fn _ascii_representation(&self) -> char { + fn to_ascii(&self) -> char { match self { Shape::Pawn => 'P', Shape::Knight => 'N', @@ -83,12 +83,12 @@ impl TryFrom for Shape { fn try_from(value: char) -> Result { match value { - 'p' => Ok(Shape::Pawn), - 'N' => Ok(Shape::Knight), - 'B' => Ok(Shape::Bishop), - 'R' => Ok(Shape::Rook), - 'Q' => Ok(Shape::Queen), - 'K' => Ok(Shape::King), + 'P' | 'p' => Ok(Shape::Pawn), + 'N' | 'n' => Ok(Shape::Knight), + 'B' | 'b' => Ok(Shape::Bishop), + 'R' | 'r' => Ok(Shape::Rook), + 'Q' | 'q' => Ok(Shape::Queen), + 'K' | 'k' => Ok(Shape::King), _ => Err(TryFromError), } } @@ -105,13 +105,13 @@ impl TryFrom<&str> for Shape { impl Into for &Shape { fn into(self) -> char { - self._ascii_representation() + self.to_ascii() } } impl Into for Shape { fn into(self) -> char { - self._ascii_representation() + self.to_ascii() } } @@ -173,6 +173,10 @@ impl Piece { is_shape!(is_rook, Rook); is_shape!(is_queen, Queen); is_shape!(is_king, King); + + pub fn to_ascii(&self) -> char { + self.shape.to_ascii() + } } impl fmt::Display for Piece { @@ -193,18 +197,18 @@ impl UnicodeDisplay for Piece { f, "{}", match (self.color, self.shape) { - (Color::White, Shape::Pawn) => '♟', - (Color::White, Shape::Knight) => '♞', - (Color::White, Shape::Bishop) => '♝', - (Color::White, Shape::Rook) => '♜', - (Color::White, Shape::Queen) => '♛', - (Color::White, Shape::King) => '♚', - (Color::Black, Shape::Pawn) => '♙', - (Color::Black, Shape::Knight) => '♘', - (Color::Black, Shape::Bishop) => '♗', - (Color::Black, Shape::Rook) => '♖', - (Color::Black, Shape::Queen) => '♕', - (Color::Black, Shape::King) => '♔', + (Color::Black, Shape::Pawn) => '♟', + (Color::Black, Shape::Knight) => '♞', + (Color::Black, Shape::Bishop) => '♝', + (Color::Black, Shape::Rook) => '♜', + (Color::Black, Shape::Queen) => '♛', + (Color::Black, Shape::King) => '♚', + (Color::White, Shape::Pawn) => '♙', + (Color::White, Shape::Knight) => '♘', + (Color::White, Shape::Bishop) => '♗', + (Color::White, Shape::Rook) => '♖', + (Color::White, Shape::Queen) => '♕', + (Color::White, Shape::King) => '♔', } ) } @@ -212,7 +216,7 @@ impl UnicodeDisplay for Piece { impl FENDisplay for Piece { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let ascii = self.shape()._ascii_representation(); + let ascii = self.shape().to_ascii(); write!( f, "{}", diff --git a/board/src/position/piece_sets.rs b/board/src/position/piece_sets.rs index cc01eda..58921b9 100644 --- a/board/src/position/piece_sets.rs +++ b/board/src/position/piece_sets.rs @@ -36,12 +36,15 @@ struct ByColorAndShape([[BitBoard; 6]; 2]); impl PieceBitBoards { pub(super) fn new(pieces: [[BitBoard; 6]; 2]) -> Self { + use std::ops::BitOr; + let white_pieces = pieces[Color::White as usize] .iter() - .fold(BitBoard::empty(), std::ops::BitOr::bitor); - let black_pieces = pieces[Color::White as usize] + .fold(BitBoard::empty(), BitOr::bitor); + let black_pieces = pieces[Color::Black as usize] .iter() - .fold(BitBoard::empty(), std::ops::BitOr::bitor); + .fold(BitBoard::empty(), BitOr::bitor); + let all_pieces = white_pieces | black_pieces; Self { diff --git a/board/src/position/position.rs b/board/src/position/position.rs index 537208d..48c124f 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -33,22 +33,22 @@ impl Position { /// Return a starting position. pub fn starting() -> Self { - let white_pieces = [ - BitBoard::new(0x00FF000000000000), - BitBoard::new(0x4200000000000000), - BitBoard::new(0x2400000000000000), - BitBoard::new(0x8100000000000000), - BitBoard::new(0x1000000000000000), - BitBoard::new(0x0800000000000000), + let black_pieces = [ + BitBoard::new(0b0000000011111111 << 48), + BitBoard::new(0b0100001000000000 << 48), + BitBoard::new(0b0010010000000000 << 48), + BitBoard::new(0b1000000100000000 << 48), + BitBoard::new(0b0000100000000000 << 48), + BitBoard::new(0b0001000000000000 << 48), ]; - let black_pieces = [ - BitBoard::new(0xFF00), - BitBoard::new(0x0042), - BitBoard::new(0x0024), - BitBoard::new(0x0081), - BitBoard::new(0x0010), - BitBoard::new(0x0008), + let white_pieces = [ + BitBoard::new(0b1111111100000000), + BitBoard::new(0b0000000001000010), + BitBoard::new(0b0000000000100100), + BitBoard::new(0b0000000010000001), + BitBoard::new(0b0000000000001000), + BitBoard::new(0b0000000000010000), ]; Self { @@ -242,7 +242,32 @@ impl fmt::Display for Position { #[cfg(test)] mod tests { - use crate::{position, Castle, Color}; + use crate::{position, Castle, Color, Position, Square}; + + #[test] + fn piece_on_square() { + let pos = test_position![ + Black Bishop on F7, + ]; + + let piece = pos.piece_on_square(Square::F7); + assert_eq!(piece, Some(piece!(Black Bishop on F7))); + } + + #[test] + fn piece_in_starting_position() { + let pos = Position::starting(); + println!("{pos}"); + + assert_eq!( + pos.piece_on_square(Square::H1), + Some(piece!(White Rook on H1)) + ); + assert_eq!( + pos.piece_on_square(Square::A8), + Some(piece!(Black Rook on A8)) + ); + } #[test] fn king_is_in_check() { diff --git a/board/src/square.rs b/board/src/square.rs index 4501178..23565cf 100644 --- a/board/src/square.rs +++ b/board/src/square.rs @@ -1,6 +1,6 @@ // Eryn Wells -use crate::{r#move::Castle, Color}; +use crate::Color; use std::{fmt, str::FromStr}; pub enum Direction { @@ -308,6 +308,12 @@ mod tests { assert_eq!(sq.rank(), Rank::Four); } + #[test] + fn to_index() { + assert_eq!(Square::A1 as usize, 0); + assert_eq!(Square::H8 as usize, 63); + } + #[test] fn valid_neighbors() { let sq = Square::E4; From dbbf2d4c46cf2abaee1cfaec5e514eb2ea00f4e5 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 21 Jan 2024 15:10:04 -0800 Subject: [PATCH 054/423] [board] Implement ply and full move counters on Position Pipe these numbers through the Builders --- board/src/position/builders/move_builder.rs | 23 ++++++++++++++++++- .../src/position/builders/position_builder.rs | 23 ++++++++++++++++++- board/src/position/position.rs | 19 +++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/board/src/position/builders/move_builder.rs b/board/src/position/builders/move_builder.rs index e0d2765..d27e373 100644 --- a/board/src/position/builders/move_builder.rs +++ b/board/src/position/builders/move_builder.rs @@ -28,6 +28,7 @@ pub enum ValidatedMove { promotion: Option, flags: Flags, en_passant_square: Option, + increment_ply: bool, }, Castle { castle: Castle, @@ -140,6 +141,7 @@ where promotion: mv.promotion(), flags, en_passant_square, + increment_ply: !(mv.is_capture() || piece.is_pawn()), }, }) } @@ -150,6 +152,9 @@ impl<'p> Builder<'p, ValidatedMove> { pub fn build(&self) -> Position { let player = self.position.player_to_move(); + let updated_move_number = + self.position.move_number() + if player == Color::Black { 1 } else { 0 }; + match self.move_to_make { ValidatedMove::RegularMove { from_square, @@ -159,6 +164,7 @@ impl<'p> Builder<'p, ValidatedMove> { promotion, flags, en_passant_square, + increment_ply, } => { let mut pieces = self.position.piece_bitboards().clone(); @@ -174,11 +180,19 @@ impl<'p> Builder<'p, ValidatedMove> { pieces.move_piece(moving_piece.piece(), from_square, to_square); } + let ply = if increment_ply { + self.position.ply_counter() + 1 + } else { + 0 + }; + Position::new( self.position.player_to_move().other(), flags, pieces, en_passant_square, + ply, + updated_move_number, ) } ValidatedMove::Castle { @@ -202,7 +216,14 @@ impl<'p> Builder<'p, ValidatedMove> { *pieces.bitboard_for_color_mut(player) &= !(king_from | rook_from) | (king_to | rook_to); - Position::new(player.other(), flags, pieces, None) + Position::new( + player.other(), + flags, + pieces, + None, + self.position.ply_counter() + 1, + updated_move_number, + ) } } } diff --git a/board/src/position/builders/position_builder.rs b/board/src/position/builders/position_builder.rs index c99625b..1d8c3fb 100644 --- a/board/src/position/builders/position_builder.rs +++ b/board/src/position/builders/position_builder.rs @@ -16,6 +16,8 @@ pub struct Builder { flags: Flags, pieces: BTreeMap, kings: [Square; 2], + ply_counter: u16, + move_number: u16, } impl Builder { @@ -28,6 +30,16 @@ impl Builder { self } + pub fn ply_counter(&mut self, num: u16) -> &mut Self { + self.ply_counter = num; + self + } + + pub fn move_number(&mut self, num: u16) -> &mut Self { + self.move_number = num; + self + } + pub fn place_piece(&mut self, piece: PlacedPiece) -> &mut Self { let square = piece.square(); @@ -50,7 +62,14 @@ impl Builder { .filter(Self::is_piece_placement_valid), ); - Position::new(self.player_to_move, self.flags, pieces, None) + Position::new( + self.player_to_move, + self.flags, + pieces, + None, + self.ply_counter, + self.move_number, + ) } } @@ -82,6 +101,8 @@ impl Default for Builder { flags: Flags::default(), pieces: pieces, kings: [white_king_square, black_king_square], + ply_counter: 0, + move_number: 1, } } } diff --git a/board/src/position/position.rs b/board/src/position/position.rs index 48c124f..abdfbe1 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -18,6 +18,9 @@ pub struct Position { pieces: PieceBitBoards, en_passant_square: Option, sight: [OnceCell; 2], + + half_move_counter: u16, + full_move_number: u16, } impl Position { @@ -28,6 +31,8 @@ impl Position { pieces: PieceBitBoards::default(), en_passant_square: None, sight: [OnceCell::new(), OnceCell::new()], + half_move_counter: 0, + full_move_number: 1, } } @@ -57,6 +62,8 @@ impl Position { pieces: PieceBitBoards::new([white_pieces, black_pieces]), en_passant_square: None, sight: [OnceCell::new(), OnceCell::new()], + half_move_counter: 0, + full_move_number: 1, } } @@ -64,6 +71,14 @@ impl Position { self.color_to_move } + pub fn move_number(&self) -> u16 { + self.full_move_number + } + + pub fn ply_counter(&self) -> u16 { + self.half_move_counter + } + /// Returns true if the player has the right to castle on the given side of /// the board. /// @@ -191,6 +206,8 @@ impl Position { flags: Flags, pieces: PieceBitBoards, en_passant_square: Option, + half_move_counter: u16, + full_move_number: u16, ) -> Self { Self { color_to_move: player_to_move, @@ -198,6 +215,8 @@ impl Position { en_passant_square, pieces, sight: [OnceCell::new(), OnceCell::new()], + half_move_counter: 0, + full_move_number: 1, } } From 3dfebb22eb312be694da5a845785c8af29d04eaf Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 21 Jan 2024 15:10:59 -0800 Subject: [PATCH 055/423] [board] Declare ToFen and implement it on Position and supporting types I can now print a position in FEN! --- board/src/fen.rs | 139 +++++++++++++++++++++++++++++++++++++++++++++++ board/src/lib.rs | 1 + 2 files changed, 140 insertions(+) create mode 100644 board/src/fen.rs diff --git a/board/src/fen.rs b/board/src/fen.rs new file mode 100644 index 0000000..b212c94 --- /dev/null +++ b/board/src/fen.rs @@ -0,0 +1,139 @@ +// Eryn Wells + +use crate::{ + piece::{Piece, PlacedPiece}, + r#move::Castle, + Color, File, Position, Rank, Square, +}; +use std::fmt::Write; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum FenError { + FmtError(std::fmt::Error), +} + +pub trait ToFen { + fn to_fen(&self) -> Result; +} + +impl ToFen for Position { + fn to_fen(&self) -> Result { + let mut fen_string = String::new(); + + let mut empty_squares: u8 = 0; + for rank in Rank::ALL.iter().rev() { + for file in File::ALL.iter() { + let square = Square::from_file_rank(*file, *rank); + match self.piece_on_square(square) { + Some(piece) => { + if empty_squares > 0 { + write!(fen_string, "{}", empty_squares) + .map_err(|err| FenError::FmtError(err))?; + empty_squares = 0; + } + write!(fen_string, "{}", piece.to_fen()?) + .map_err(|err| FenError::FmtError(err))?; + } + None => empty_squares += 1, + } + } + + if empty_squares > 0 { + write!(fen_string, "{}", empty_squares).map_err(|err| FenError::FmtError(err))?; + empty_squares = 0; + } + if rank != &Rank::One { + write!(fen_string, "/").map_err(|err| FenError::FmtError(err))?; + } + } + + write!(fen_string, " {}", self.player_to_move().to_fen()?) + .map_err(|err| FenError::FmtError(err))?; + + let castling = [ + (Color::White, Castle::KingSide), + (Color::White, Castle::QueenSide), + (Color::Black, Castle::KingSide), + (Color::Black, Castle::QueenSide), + ] + .map(|(color, castle)| { + let can_castle = self.player_has_right_to_castle(color, castle); + if !can_castle { + "" + } else { + match (color, castle) { + (Color::White, Castle::KingSide) => "K", + (Color::White, Castle::QueenSide) => "Q", + (Color::Black, Castle::KingSide) => "k", + (Color::Black, Castle::QueenSide) => "q", + } + } + }) + .concat(); + + write!( + fen_string, + " {}", + if castling.len() > 0 { &castling } else { "-" } + ) + .map_err(|err| FenError::FmtError(err))?; + + write!( + fen_string, + " {}", + if let Some(en_passant_square) = self.en_passant_square() { + en_passant_square.to_string() + } else { + "-".to_string() + } + ) + .map_err(|err| FenError::FmtError(err))?; + + write!(fen_string, " {}", self.ply_counter()).map_err(|err| FenError::FmtError(err))?; + write!(fen_string, " {}", self.move_number()).map_err(|err| FenError::FmtError(err))?; + + Ok(fen_string) + } +} + +impl ToFen for Color { + fn to_fen(&self) -> Result { + match self { + Color::White => Ok("w".to_string()), + Color::Black => Ok("b".to_string()), + } + } +} + +impl ToFen for Piece { + fn to_fen(&self) -> Result { + let ascii: char = self.to_ascii(); + Ok(String::from(match self.color() { + Color::White => ascii.to_ascii_uppercase(), + Color::Black => ascii.to_ascii_lowercase(), + })) + } +} + +impl ToFen for PlacedPiece { + fn to_fen(&self) -> Result { + Ok(self.piece().to_fen()?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn starting_position() { + let pos = Position::starting(); + println!("{pos:#?}"); + assert_eq!( + pos.to_fen(), + Ok(String::from( + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" + )) + ); + } +} diff --git a/board/src/lib.rs b/board/src/lib.rs index b267517..ad3abf0 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -3,6 +3,7 @@ #[macro_use] mod bitboard; mod display; +mod fen; mod r#move; #[macro_use] mod macros; From c642c1158ab710aa5cf467b64b225aca139149d3 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 22 Jan 2024 08:10:40 -0800 Subject: [PATCH 056/423] [board] Implement TryFrom on string types for Color and Shape --- board/src/piece.rs | 51 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/board/src/piece.rs b/board/src/piece.rs index 45ed4b0..7dc2e8f 100644 --- a/board/src/piece.rs +++ b/board/src/piece.rs @@ -7,6 +7,12 @@ use crate::{ use std::fmt; use std::slice::Iter; +#[derive(Debug, Eq, PartialEq)] +pub enum TryFromError { + InvalidCharacter, + ZeroLengthString, +} + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum Color { White = 0, @@ -32,6 +38,37 @@ impl Default for Color { } } +impl TryFrom for Color { + type Error = TryFromError; + + fn try_from(value: char) -> Result { + match value { + 'w' | 'W' => Ok(Color::White), + 'b' | 'B' => Ok(Color::Black), + _ => Err(TryFromError::InvalidCharacter), + } + } +} + +macro_rules! try_from_string { + ($type:ty) => { + try_from_string!($type, &str); + try_from_string!($type, &String); + }; + ($type:ty, $from_type:ty) => { + impl TryFrom<$from_type> for $type { + type Error = TryFromError; + + fn try_from(value: $from_type) -> Result { + let first_char = value.chars().nth(0).ok_or(TryFromError::ZeroLengthString)?; + Self::try_from(first_char) + } + } + }; +} + +try_from_string!(Color); + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum Shape { Pawn = 0, @@ -75,9 +112,6 @@ impl Shape { } } -#[derive(Debug, Eq, PartialEq)] -pub struct TryFromError; - impl TryFrom for Shape { type Error = TryFromError; @@ -89,19 +123,12 @@ impl TryFrom for Shape { 'R' | 'r' => Ok(Shape::Rook), 'Q' | 'q' => Ok(Shape::Queen), 'K' | 'k' => Ok(Shape::King), - _ => Err(TryFromError), + _ => Err(TryFromError::InvalidCharacter), } } } -impl TryFrom<&str> for Shape { - type Error = TryFromError; - - fn try_from(value: &str) -> Result { - let first_char = value.chars().nth(0).ok_or(TryFromError)?; - Shape::try_from(first_char) - } -} +try_from_string!(Shape); impl Into for &Shape { fn into(self) -> char { From 8dbf44c741987ba051e278734cf530c9d6bdcd65 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 22 Jan 2024 08:11:02 -0800 Subject: [PATCH 057/423] =?UTF-8?q?[board]=20Rename=20position::MoveBuilde?= =?UTF-8?q?r=20=E2=86=92=20MakeMoveBuilder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- board/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/board/src/lib.rs b/board/src/lib.rs index ad3abf0..024c6a8 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -14,7 +14,7 @@ mod sight; mod square; pub use piece::{Color, Piece}; -pub use position::{Position, PositionBuilder}; +pub use position::{MoveBuilder as MakeMoveBuilder, Position, PositionBuilder}; pub use r#move::{Castle, MakeMoveError, Move, MoveBuilder}; pub use square::{File, Rank, Square}; From c1008ef672bff5ca9e9392763cfc67d4fb3125a0 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 22 Jan 2024 08:12:28 -0800 Subject: [PATCH 058/423] [board] Put a cached Moves object into a OnceCell on Position Cache move generation so we don't have to remake it every time. --- board/src/position/position.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/board/src/position/position.rs b/board/src/position/position.rs index abdfbe1..484293e 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -18,6 +18,7 @@ pub struct Position { pieces: PieceBitBoards, en_passant_square: Option, sight: [OnceCell; 2], + moves: OnceCell, half_move_counter: u16, full_move_number: u16, @@ -31,6 +32,7 @@ impl Position { pieces: PieceBitBoards::default(), en_passant_square: None, sight: [OnceCell::new(), OnceCell::new()], + moves: OnceCell::new(), half_move_counter: 0, full_move_number: 1, } @@ -62,6 +64,7 @@ impl Position { pieces: PieceBitBoards::new([white_pieces, black_pieces]), en_passant_square: None, sight: [OnceCell::new(), OnceCell::new()], + moves: OnceCell::new(), half_move_counter: 0, full_move_number: 1, } @@ -121,8 +124,9 @@ impl Position { self.piece_on_square(square) } - pub fn moves(&self) -> Moves { - Moves::new(self, self.color_to_move) + pub fn moves(&self) -> &Moves { + self.moves + .get_or_init(|| Moves::new(self, self.color_to_move)) } /// Return a BitBoard representing the set of squares containing a piece. @@ -215,6 +219,7 @@ impl Position { en_passant_square, pieces, sight: [OnceCell::new(), OnceCell::new()], + moves: OnceCell::new(), half_move_counter: 0, full_move_number: 1, } From bf2bccbc7d0fe3dee3c16753fa5373670b65285f Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 22 Jan 2024 08:12:55 -0800 Subject: [PATCH 059/423] [board] Implement PositionBuilder::from_position Create a PositionBuilder from a Position. --- .../src/position/builders/position_builder.rs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/board/src/position/builders/position_builder.rs b/board/src/position/builders/position_builder.rs index 1d8c3fb..7c68413 100644 --- a/board/src/position/builders/position_builder.rs +++ b/board/src/position/builders/position_builder.rs @@ -25,6 +25,27 @@ impl Builder { Self::default() } + pub fn from_position(position: &Position) -> Self { + let pieces = BTreeMap::from_iter( + position + .pieces(Color::White) + .chain(position.pieces(Color::Black)) + .map(|placed_piece| (placed_piece.square(), *placed_piece.piece())), + ); + + let white_king = position.king_square(Color::White); + let black_king = position.king_square(Color::Black); + + Self { + player_to_move: position.player_to_move(), + flags: position.flags(), + pieces, + kings: [white_king, black_king], + ply_counter: position.ply_counter(), + move_number: position.move_number(), + } + } + pub fn to_move(&mut self, player: Color) -> &mut Self { self.player_to_move = player; self From 9a8380573b110fe1abbe77f832c4a77ecd7d057b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 22 Jan 2024 08:18:19 -0800 Subject: [PATCH 060/423] [board] Move MakeMoveBuilder::new() to an impl that specifies NoMove I can build these now without having to specify a type for M. --- board/src/position/builders/move_builder.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/board/src/position/builders/move_builder.rs b/board/src/position/builders/move_builder.rs index d27e373..5d12b19 100644 --- a/board/src/position/builders/move_builder.rs +++ b/board/src/position/builders/move_builder.rs @@ -41,17 +41,19 @@ pub enum ValidatedMove { impl MoveToMake for NoMove {} impl MoveToMake for ValidatedMove {} -impl<'p, M> Builder<'p, M> -where - M: MoveToMake, -{ +impl<'p> Builder<'p, NoMove> { pub fn new(position: &'p Position) -> Builder<'p, NoMove> { Builder { position, move_to_make: NoMove, } } +} +impl<'p, M> Builder<'p, M> +where + M: MoveToMake, +{ pub fn make(self, mv: &Move) -> Result, MakeMoveError> { let from_square = mv.from_square(); From 7424236f1dfa11ffb425f85bc94d746b302aff9f Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 22 Jan 2024 08:18:37 -0800 Subject: [PATCH 061/423] [board] Add MakeMoveError::IllegalSquare(Square) --- board/src/move.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/board/src/move.rs b/board/src/move.rs index ade1ff8..08cf988 100644 --- a/board/src/move.rs +++ b/board/src/move.rs @@ -16,6 +16,7 @@ pub enum MakeMoveError { NoPiece, NoCapturedPiece, IllegalCastle, + IllegalSquare(Square), } mod castle { From c2a115cee94d098e1749d6e4a25fe4b5c522623f Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 22 Jan 2024 08:18:49 -0800 Subject: [PATCH 062/423] [board] Implement Display for Color --- board/src/piece.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/board/src/piece.rs b/board/src/piece.rs index 7dc2e8f..f65a7b8 100644 --- a/board/src/piece.rs +++ b/board/src/piece.rs @@ -38,6 +38,19 @@ impl Default for Color { } } +impl fmt::Display for Color { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + Color::White => "White", + Color::Black => "Black", + }, + ) + } +} + impl TryFrom for Color { type Error = TryFromError; From 3244bfc211de7c487d25b5a5d2d7f698728cbe01 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 22 Jan 2024 08:19:09 -0800 Subject: [PATCH 063/423] [board] Make the fen module public so clients can access ToFen --- board/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/board/src/lib.rs b/board/src/lib.rs index 024c6a8..3db0753 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -1,9 +1,10 @@ // Eryn Wells +pub mod fen; + #[macro_use] mod bitboard; mod display; -mod fen; mod r#move; #[macro_use] mod macros; From 90266f2dd009312acc3139bed32fd21190858e9f Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 22 Jan 2024 08:20:38 -0800 Subject: [PATCH 064/423] [board] Make the MoveSet struct (and its internal structs) public for the crate --- board/src/move_generator/mod.rs | 3 +-- board/src/move_generator/move_set.rs | 11 ++++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/board/src/move_generator/mod.rs b/board/src/move_generator/mod.rs index eb5aee8..eb080c3 100644 --- a/board/src/move_generator/mod.rs +++ b/board/src/move_generator/mod.rs @@ -10,8 +10,7 @@ mod queen; mod rook; pub use move_generator::Moves; - -pub(self) use move_set::MoveSet; +pub(crate) use move_set::MoveSet; use crate::{ piece::{Color, Piece, PlacedPiece}, diff --git a/board/src/move_generator/move_set.rs b/board/src/move_generator/move_set.rs index f03d177..1c44766 100644 --- a/board/src/move_generator/move_set.rs +++ b/board/src/move_generator/move_set.rs @@ -1,17 +1,26 @@ use crate::{piece::PlacedPiece, BitBoard, Move}; +#[derive(Clone, Debug, Eq, PartialEq)] struct BitBoardSet { quiet: BitBoard, captures: BitBoard, } +#[derive(Clone, Debug, Eq, PartialEq)] struct MoveListSet { quiet: Vec, captures: Vec, } +impl MoveListSet { + pub fn contains(&self, mv: &Move) -> bool { + self.quiet.contains(mv) || self.captures.contains(mv) + } +} + /// A set of moves for a piece on the board. -pub(super) struct MoveSet { +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct MoveSet { piece: PlacedPiece, bitboards: BitBoardSet, move_lists: MoveListSet, From 1ebe7d10ded75953279e6a15256734fd3c3d0ffc Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 22 Jan 2024 19:27:08 -0800 Subject: [PATCH 065/423] Move Notes and ChessPieces docs to a doc directory --- chess_pieces.txt => doc/ChessPieces.txt | 0 Notes.md => doc/Notes.md | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename chess_pieces.txt => doc/ChessPieces.txt (100%) rename Notes.md => doc/Notes.md (100%) diff --git a/chess_pieces.txt b/doc/ChessPieces.txt similarity index 100% rename from chess_pieces.txt rename to doc/ChessPieces.txt diff --git a/Notes.md b/doc/Notes.md similarity index 100% rename from Notes.md rename to doc/Notes.md From e522e268cff8497227f99e32be5c69ea55d46774 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 22 Jan 2024 19:33:59 -0800 Subject: [PATCH 066/423] Make a Cargo Workspace! --- Cargo.lock | 428 +++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 6 + 2 files changed, 434 insertions(+) create mode 100644 Cargo.lock create mode 100644 Cargo.toml diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..e49153a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,428 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anstream" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + +[[package]] +name = "board" +version = "0.1.0" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "clipboard-win" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c57002a5d9be777c1ef967e33674dac9ebd310d8893e4e3437b14d5f0f6372cc" +dependencies = [ + "error-code", +] + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "error-code" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "281e452d3bad4005426416cdba5ccfd4f5c1280e10099e21db27f7c1c28347fc" + +[[package]] +name = "explorer" +version = "0.1.0" +dependencies = [ + "board", + "clap", + "rustyline", + "shlex", +] + +[[package]] +name = "fd-lock" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "libc" +version = "0.2.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "rustix" +version = "0.38.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustyline" +version = "13.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02a2d683a4ac90aeef5b1013933f6d977bd37d51ff3f4dad829d4931a7e6be86" +dependencies = [ + "bitflags", + "cfg-if", + "clipboard-win", + "fd-lock", + "home", + "libc", + "log", + "memchr", + "nix", + "radix_trie", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "winapi", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..655f6ac --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,6 @@ +[workspace] + +members = [ + "board", + "explorer", +] From 025ceb2694886149668de6cec0d76da4ef958aa5 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 23 Jan 2024 17:18:48 -0800 Subject: [PATCH 067/423] [core] Move the contents of board::square to core::cordinates Export Square, Direction, Rank, and File from the core crate. --- Cargo.toml | 1 + core/Cargo.toml | 8 + core/src/coordinates.rs | 417 ++++++++++++++++++++++++++++++++++++++++ core/src/lib.rs | 3 + 4 files changed, 429 insertions(+) create mode 100644 core/Cargo.toml create mode 100644 core/src/coordinates.rs create mode 100644 core/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 655f6ac..d595fa5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,5 +2,6 @@ members = [ "board", + "core", "explorer", ] diff --git a/core/Cargo.toml b/core/Cargo.toml new file mode 100644 index 0000000..900733d --- /dev/null +++ b/core/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "core" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/core/src/coordinates.rs b/core/src/coordinates.rs new file mode 100644 index 0000000..fd0d25e --- /dev/null +++ b/core/src/coordinates.rs @@ -0,0 +1,417 @@ +// Eryn Wells + +use std::fmt; +use std::str::FromStr; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u8)] +pub enum Direction { + North, + NorthWest, + West, + SouthWest, + South, + SouthEast, + East, + NorthEast, +} + +impl Direction { + pub fn to_offset(&self) -> i8 { + const OFFSETS: [i8; 8] = [8, 7, -1, -9, -8, -7, 1, 9]; + OFFSETS[*self as usize] + } +} + +macro_rules! try_from_integer { + ($type:ident, $int_type:ident) => { + impl TryFrom<$int_type> for $type { + type Error = (); + + fn try_from(value: $int_type) -> Result { + Square::try_from(value as u8) + } + } + }; +} + +macro_rules! coordinate_enum { + ($name: ident, $($variant:ident),*) => { + #[repr(u8)] + #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] + pub enum $name { + $($variant), * + } + + impl $name { + pub const NUM: usize = [$(Self::$variant), *].len(); + pub const ALL: [Self; Self::NUM] = [$(Self::$variant), *]; + } + + impl TryFrom for $name { + type Error = (); + + fn try_from(value: u8) -> Result { + let value_usize = value as usize; + if value_usize < Self::NUM { + Ok($name::ALL[value_usize]) + } else { + Err(()) + } + } + } + + try_from_integer!($name, u16); + try_from_integer!($name, u32); + try_from_integer!($name, u64); + } +} + +macro_rules! range_bound_struct { + ($vis:vis, $type:ident, $repr:ty, $max:expr) => { + #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] + $vis struct $type($repr); + + #[allow(dead_code)] + impl $type { + $vis const FIRST: $type = $type(0); + $vis const LAST: $type = $type($max - 1); + } + + impl $type { + $vis fn new(x: $repr) -> Option { + if x < $max { + Some(Self(x)) + } else { + None + } + } + + $vis unsafe fn new_unchecked(x: $repr) -> Self { + Self(x) + } + } + + impl Into<$repr> for $type { + fn into(self) -> $repr { + self.0 + } + } + + impl TryFrom<$repr> for $type { + type Error = (); + + fn try_from(value: $repr) -> Result { + Self::new(value).ok_or(()) + } + } + } +} + +range_bound_struct!(pub, File, u8, 8); + +impl File { + pub const A: File = File(0); + pub const B: File = File(1); + pub const C: File = File(2); + pub const D: File = File(3); + pub const E: File = File(4); + pub const F: File = File(5); + pub const G: File = File(6); + pub const H: File = File(7); + + pub const ALL: [File; 8] = [ + File::A, + File::B, + File::C, + File::D, + File::E, + File::F, + File::G, + File::H, + ]; +} + +range_bound_struct!(pub, Rank, u8, 8); + +#[allow(dead_code)] +impl Rank { + pub const ONE: Rank = Rank(0); + pub const TWO: Rank = Rank(1); + pub const THREE: Rank = Rank(2); + pub const FOUR: Rank = Rank(3); + pub const FIVE: Rank = Rank(4); + pub const SIX: Rank = Rank(5); + pub const SEVEN: Rank = Rank(6); + pub const EIGHT: Rank = Rank(7); + + pub const ALL: [Rank; 8] = [ + Rank::ONE, + Rank::TWO, + Rank::THREE, + Rank::FOUR, + Rank::FIVE, + Rank::SIX, + Rank::SEVEN, + Rank::EIGHT, + ]; +} + +#[rustfmt::skip] +coordinate_enum!(Square, + A1, B1, C1, D1, E1, F1, G1, H1, + A2, B2, C2, D2, E2, F2, G2, H2, + A3, B3, C3, D3, E3, F3, G3, H3, + A4, B4, C4, D4, E4, F4, G4, H4, + A5, B5, C5, D5, E5, F5, G5, H5, + A6, B6, C6, D6, E6, F6, G6, H6, + A7, B7, C7, D7, E7, F7, G7, H7, + A8, B8, C8, D8, E8, F8, G8, H8 +); + +impl Square { + pub unsafe fn from_index(x: u8) -> Square { + Self::try_from(x).unwrap_unchecked() + } + + #[inline] + pub fn from_file_rank(file: File, rank: Rank) -> Square { + let file_int: u8 = file.into(); + let rank_int: u8 = rank.into(); + unsafe { Self::from_index(rank_int << 3 | file_int) } + } + + pub fn from_algebraic_str(s: &str) -> Result { + s.parse() + } + + #[inline] + pub fn file(self) -> File { + unsafe { File::new_unchecked((self as u8) & 0b000111) } + } + + #[inline] + pub fn rank(self) -> Rank { + unsafe { Rank::new_unchecked((self as u8) >> 3) } + } + + pub fn neighbor(self, direction: Direction) -> Option { + let index: u8 = self as u8; + let dir: i8 = direction.to_offset(); + match direction { + Direction::North => Square::try_from(index.wrapping_add_signed(dir)).ok(), + Direction::NorthWest => { + if self.rank() != Rank::EIGHT { + Square::try_from(index.wrapping_add_signed(dir)).ok() + } else { + None + } + } + Direction::West => { + if self.file() != File::A { + Square::try_from(index.wrapping_add_signed(dir)).ok() + } else { + None + } + } + Direction::SouthWest => { + if self.rank() != Rank::ONE { + Square::try_from(index.wrapping_add_signed(dir)).ok() + } else { + None + } + } + Direction::South => { + if self.rank() != Rank::ONE { + Square::try_from(index.wrapping_add_signed(dir)).ok() + } else { + None + } + } + Direction::SouthEast => { + if self.rank() != Rank::ONE { + Square::try_from(index.wrapping_add_signed(dir)).ok() + } else { + None + } + } + Direction::East => { + if self.file() != File::H { + Square::try_from(index.wrapping_add_signed(dir)).ok() + } else { + None + } + } + Direction::NorthEast => Square::try_from(index.wrapping_add_signed(dir)).ok(), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ParseSquareError; + +impl FromStr for Square { + type Err = ParseSquareError; + + fn from_str(s: &str) -> Result { + let mut chars = s.chars(); + + let file: File = chars + .next() + .and_then(|c| c.try_into().ok()) + .ok_or(ParseSquareError)?; + + let rank: Rank = chars + .next() + .and_then(|c| c.try_into().ok()) + .ok_or(ParseSquareError)?; + + if !chars.next().is_none() { + return Err(ParseSquareError); + } + + Ok(Square::from_file_rank(file, rank)) + } +} + +impl fmt::Display for File { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", Into::::into(*self)) + } +} + +impl fmt::Display for Rank { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", Into::::into(*self)) + } +} + +impl fmt::Display for Square { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.file().fmt(f)?; + self.rank().fmt(f)?; + Ok(()) + } +} + +impl Into for File { + fn into(self) -> char { + let value: u8 = self.into(); + (value + 'a' as u8) as char + } +} + +impl Into for Rank { + fn into(self) -> char { + let value: u8 = self.into(); + (value + '1' as u8) as char + } +} + +impl TryFrom for File { + type Error = (); + + fn try_from(value: char) -> Result { + File::try_from(value.to_ascii_lowercase() as u8 - 'a' as u8) + } +} + +impl TryFrom for Rank { + type Error = (); + + fn try_from(value: char) -> Result { + let result = (value as u8).checked_sub('1' as u8).ok_or(())?; + Self::try_from(result) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn good_algebraic_input() { + let sq = Square::from_algebraic_str("a4").expect("Failed to parse 'a4' square"); + assert_eq!(sq.file(), File(0)); + assert_eq!(sq.rank(), Rank(3)); + + let sq = Square::from_algebraic_str("B8").expect("Failed to parse 'B8' square"); + assert_eq!(sq.file(), File(1)); + assert_eq!(sq.rank(), Rank(7)); + + let sq = Square::from_algebraic_str("e4").expect("Failed to parse 'B8' square"); + assert_eq!(sq.file(), File(4)); + assert_eq!(sq.rank(), Rank(3)); + } + + #[test] + fn bad_algebraic_input() { + Square::from_algebraic_str("a0").expect_err("Got valid Square for 'a0'"); + Square::from_algebraic_str("j3").expect_err("Got valid Square for 'j3'"); + Square::from_algebraic_str("a11").expect_err("Got valid Square for 'a11'"); + Square::from_algebraic_str("b-1").expect_err("Got valid Square for 'b-1'"); + Square::from_algebraic_str("a 1").expect_err("Got valid Square for 'a 1'"); + Square::from_algebraic_str("").expect_err("Got valid Square for ''"); + } + + #[test] + fn from_index() { + let sq = Square::try_from(4u32).expect("Unable to get Square from index"); + assert_eq!(sq.file(), File(4)); + assert_eq!(sq.rank(), Rank(0)); + + let sq = Square::try_from(28u32).expect("Unable to get Square from index"); + assert_eq!(sq.file(), File(4)); + assert_eq!(sq.rank(), Rank(3)); + } + + #[test] + fn to_index() { + assert_eq!(Square::A1 as usize, 0); + assert_eq!(Square::H8 as usize, 63); + } + + #[test] + fn valid_neighbors() { + let sq = Square::E4; + + assert_eq!(sq.neighbor(Direction::North), Some(Square::E5)); + assert_eq!(sq.neighbor(Direction::NorthEast), Some(Square::F5)); + assert_eq!(sq.neighbor(Direction::East), Some(Square::F4)); + assert_eq!(sq.neighbor(Direction::SouthEast), Some(Square::F3)); + assert_eq!(sq.neighbor(Direction::South), Some(Square::E3)); + assert_eq!(sq.neighbor(Direction::SouthWest), Some(Square::D3)); + assert_eq!(sq.neighbor(Direction::West), Some(Square::D4)); + assert_eq!(sq.neighbor(Direction::NorthWest), Some(Square::D5)); + } + + #[test] + fn invalid_neighbors() { + let sq = Square::A1; + assert!(sq.neighbor(Direction::West).is_none()); + assert!(sq.neighbor(Direction::SouthWest).is_none()); + assert!(sq.neighbor(Direction::South).is_none()); + + let sq = Square::H1; + assert!(sq.neighbor(Direction::East).is_none()); + assert!(sq.neighbor(Direction::SouthEast).is_none()); + assert!(sq.neighbor(Direction::South).is_none()); + + let sq = Square::A8; + assert!(sq.neighbor(Direction::North).is_none()); + assert!(sq.neighbor(Direction::NorthWest).is_none()); + assert!(sq.neighbor(Direction::West).is_none()); + + let sq = Square::H8; + assert!(sq.neighbor(Direction::North).is_none()); + assert!(sq.neighbor(Direction::NorthEast).is_none()); + assert!(sq.neighbor(Direction::East).is_none()); + } + + #[test] + fn display() { + assert_eq!(format!("{}", Square::C5), "c5"); + assert_eq!(format!("{}", Square::A1), "a1"); + assert_eq!(format!("{}", Square::H8), "h8"); + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs new file mode 100644 index 0000000..2fe191d --- /dev/null +++ b/core/src/lib.rs @@ -0,0 +1,3 @@ +mod coordinates; + +pub use coordinates::{Direction, File, Rank, Square}; From 7e08a9adc44eba7ffc85e893b23cff66c81dba2f Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 23 Jan 2024 17:18:48 -0800 Subject: [PATCH 068/423] [core] Create a core crate --- Cargo.toml | 1 + core/Cargo.toml | 8 ++++++++ core/src/lib.rs | 0 3 files changed, 9 insertions(+) create mode 100644 core/Cargo.toml create mode 100644 core/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 655f6ac..d595fa5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,5 +2,6 @@ members = [ "board", + "core", "explorer", ] diff --git a/core/Cargo.toml b/core/Cargo.toml new file mode 100644 index 0000000..900733d --- /dev/null +++ b/core/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "core" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/core/src/lib.rs b/core/src/lib.rs new file mode 100644 index 0000000..e69de29 From 406631b617646a43967668ae94125d94e62fcf2b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 24 Jan 2024 08:25:56 -0800 Subject: [PATCH 069/423] [core] Move the contents of board::square to core::coordinates Export Square, Rank, and File from the core crate. --- board/src/lib.rs | 1 - .../src/square.rs => core/src/coordinates.rs | 361 ++++++++++-------- core/src/lib.rs | 3 + 3 files changed, 212 insertions(+), 153 deletions(-) rename board/src/square.rs => core/src/coordinates.rs (55%) diff --git a/board/src/lib.rs b/board/src/lib.rs index 3db0753..8c68fad 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -12,7 +12,6 @@ mod move_generator; pub mod piece; mod position; mod sight; -mod square; pub use piece::{Color, Piece}; pub use position::{MoveBuilder as MakeMoveBuilder, Position, PositionBuilder}; diff --git a/board/src/square.rs b/core/src/coordinates.rs similarity index 55% rename from board/src/square.rs rename to core/src/coordinates.rs index 23565cf..fd0d25e 100644 --- a/board/src/square.rs +++ b/core/src/coordinates.rs @@ -1,8 +1,10 @@ // Eryn Wells -use crate::Color; -use std::{fmt, str::FromStr}; +use std::fmt; +use std::str::FromStr; +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u8)] pub enum Direction { North, NorthWest, @@ -14,14 +16,24 @@ pub enum Direction { NorthEast, } -#[derive(Debug)] -pub struct ParseFileError; +impl Direction { + pub fn to_offset(&self) -> i8 { + const OFFSETS: [i8; 8] = [8, 7, -1, -9, -8, -7, 1, 9]; + OFFSETS[*self as usize] + } +} -#[derive(Debug)] -pub struct ParseSquareError; +macro_rules! try_from_integer { + ($type:ident, $int_type:ident) => { + impl TryFrom<$int_type> for $type { + type Error = (); -#[derive(Debug)] -pub struct SquareOutOfBoundsError; + fn try_from(value: $int_type) -> Result { + Square::try_from(value as u8) + } + } + }; +} macro_rules! coordinate_enum { ($name: ident, $($variant:ident),*) => { @@ -34,43 +46,116 @@ macro_rules! coordinate_enum { impl $name { pub const NUM: usize = [$(Self::$variant), *].len(); pub const ALL: [Self; Self::NUM] = [$(Self::$variant), *]; + } - #[inline] - pub(crate) fn from_index(index: usize) -> Self { - assert!( - index < Self::NUM, - "Index {} out of bounds for {}.", - index, - stringify!($name) - ); - Self::try_index(index).unwrap() + impl TryFrom for $name { + type Error = (); + + fn try_from(value: u8) -> Result { + let value_usize = value as usize; + if value_usize < Self::NUM { + Ok($name::ALL[value_usize]) + } else { + Err(()) + } + } + } + + try_from_integer!($name, u16); + try_from_integer!($name, u32); + try_from_integer!($name, u64); + } +} + +macro_rules! range_bound_struct { + ($vis:vis, $type:ident, $repr:ty, $max:expr) => { + #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] + $vis struct $type($repr); + + #[allow(dead_code)] + impl $type { + $vis const FIRST: $type = $type(0); + $vis const LAST: $type = $type($max - 1); + } + + impl $type { + $vis fn new(x: $repr) -> Option { + if x < $max { + Some(Self(x)) + } else { + None + } } - pub fn try_index(index: usize) -> Option { - $( - #[allow(non_upper_case_globals)] - const $variant: usize = $name::$variant as usize; - )* + $vis unsafe fn new_unchecked(x: $repr) -> Self { + Self(x) + } + } - #[allow(non_upper_case_globals)] - match index { - $($variant => Some($name::$variant),)* - _ => None, - } + impl Into<$repr> for $type { + fn into(self) -> $repr { + self.0 + } + } + + impl TryFrom<$repr> for $type { + type Error = (); + + fn try_from(value: $repr) -> Result { + Self::new(value).ok_or(()) } } } } -#[rustfmt::skip] -coordinate_enum!(Rank, - One, Two, Three, Four, Five, Six, Seven, Eight -); +range_bound_struct!(pub, File, u8, 8); -#[rustfmt::skip] -coordinate_enum!(File, - A, B, C, D, E, F, G, H -); +impl File { + pub const A: File = File(0); + pub const B: File = File(1); + pub const C: File = File(2); + pub const D: File = File(3); + pub const E: File = File(4); + pub const F: File = File(5); + pub const G: File = File(6); + pub const H: File = File(7); + + pub const ALL: [File; 8] = [ + File::A, + File::B, + File::C, + File::D, + File::E, + File::F, + File::G, + File::H, + ]; +} + +range_bound_struct!(pub, Rank, u8, 8); + +#[allow(dead_code)] +impl Rank { + pub const ONE: Rank = Rank(0); + pub const TWO: Rank = Rank(1); + pub const THREE: Rank = Rank(2); + pub const FOUR: Rank = Rank(3); + pub const FIVE: Rank = Rank(4); + pub const SIX: Rank = Rank(5); + pub const SEVEN: Rank = Rank(6); + pub const EIGHT: Rank = Rank(7); + + pub const ALL: [Rank; 8] = [ + Rank::ONE, + Rank::TWO, + Rank::THREE, + Rank::FOUR, + Rank::FIVE, + Rank::SIX, + Rank::SEVEN, + Rank::EIGHT, + ]; +} #[rustfmt::skip] coordinate_enum!(Square, @@ -84,138 +169,87 @@ coordinate_enum!(Square, A8, B8, C8, D8, E8, F8, G8, H8 ); -impl Into for File { - fn into(self) -> char { - ('a' as u8 + self as u8) as char - } -} - -impl TryFrom for File { - type Error = ParseFileError; - - fn try_from(value: char) -> Result { - let lowercase_value = value.to_ascii_lowercase(); - for file in File::ALL.iter() { - if lowercase_value == (*file).into() { - return Ok(*file); - } - } - - Err(ParseFileError) - } -} - -impl fmt::Display for File { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", Into::::into(*self).to_uppercase()) - } -} - -impl Into for Rank { - fn into(self) -> char { - ('1' as u8 + self as u8) as char - } -} - -impl TryFrom for Rank { - type Error = ParseFileError; - - fn try_from(value: char) -> Result { - let lowercase_value = value.to_ascii_lowercase(); - for rank in Self::ALL.iter().cloned() { - if lowercase_value == rank.into() { - return Ok(rank); - } - } - - Err(ParseFileError) - } -} - -impl fmt::Display for Rank { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", Into::::into(*self)) - } -} - impl Square { + pub unsafe fn from_index(x: u8) -> Square { + Self::try_from(x).unwrap_unchecked() + } + #[inline] pub fn from_file_rank(file: File, rank: Rank) -> Square { - Self::from_index((rank as usize) << 3 | file as usize) - } - - #[inline] - pub fn file(self) -> File { - File::from_index(self as usize & 0b000111) - } - - #[inline] - pub fn rank(self) -> Rank { - Rank::from_index(self as usize >> 3) - } -} - -impl Square { - const KING_STARTING_SQUARES: [Square; 2] = [Square::E1, Square::E8]; - - pub fn king_starting_square(color: Color) -> Square { - Square::KING_STARTING_SQUARES[color as usize] + let file_int: u8 = file.into(); + let rank_int: u8 = rank.into(); + unsafe { Self::from_index(rank_int << 3 | file_int) } } pub fn from_algebraic_str(s: &str) -> Result { s.parse() } + #[inline] + pub fn file(self) -> File { + unsafe { File::new_unchecked((self as u8) & 0b000111) } + } + + #[inline] + pub fn rank(self) -> Rank { + unsafe { Rank::new_unchecked((self as u8) >> 3) } + } + pub fn neighbor(self, direction: Direction) -> Option { + let index: u8 = self as u8; + let dir: i8 = direction.to_offset(); match direction { - Direction::North => Square::try_index(self as usize + 8), + Direction::North => Square::try_from(index.wrapping_add_signed(dir)).ok(), Direction::NorthWest => { - if self.rank() != Rank::Eight { - Square::try_index(self as usize + 7) + if self.rank() != Rank::EIGHT { + Square::try_from(index.wrapping_add_signed(dir)).ok() } else { None } } Direction::West => { if self.file() != File::A { - Square::try_index(self as usize - 1) + Square::try_from(index.wrapping_add_signed(dir)).ok() } else { None } } Direction::SouthWest => { - if self.rank() != Rank::One { - Square::try_index(self as usize - 9) + if self.rank() != Rank::ONE { + Square::try_from(index.wrapping_add_signed(dir)).ok() } else { None } } Direction::South => { - if self.rank() != Rank::One { - Square::try_index(self as usize - 8) + if self.rank() != Rank::ONE { + Square::try_from(index.wrapping_add_signed(dir)).ok() } else { None } } Direction::SouthEast => { - if self.rank() != Rank::One { - Square::try_index(self as usize - 7) + if self.rank() != Rank::ONE { + Square::try_from(index.wrapping_add_signed(dir)).ok() } else { None } } Direction::East => { if self.file() != File::H { - Square::try_index(self as usize + 1) + Square::try_from(index.wrapping_add_signed(dir)).ok() } else { None } } - Direction::NorthEast => Square::try_index(self as usize + 9), + Direction::NorthEast => Square::try_from(index.wrapping_add_signed(dir)).ok(), } } } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ParseSquareError; + impl FromStr for Square { type Err = ParseSquareError; @@ -240,31 +274,54 @@ impl FromStr for Square { } } -macro_rules! try_from_integer { - ($int_type:ident) => { - impl TryFrom<$int_type> for Square { - type Error = SquareOutOfBoundsError; - - fn try_from(value: $int_type) -> Result { - Square::try_index(value as usize).ok_or(SquareOutOfBoundsError) - } - } - }; +impl fmt::Display for File { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", Into::::into(*self)) + } } -try_from_integer!(u8); -try_from_integer!(u16); -try_from_integer!(u32); -try_from_integer!(u64); +impl fmt::Display for Rank { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", Into::::into(*self)) + } +} impl fmt::Display for Square { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}{}", - ('a' as u8 + self.file() as u8) as char, - self.rank() as usize + 1 - ) + self.file().fmt(f)?; + self.rank().fmt(f)?; + Ok(()) + } +} + +impl Into for File { + fn into(self) -> char { + let value: u8 = self.into(); + (value + 'a' as u8) as char + } +} + +impl Into for Rank { + fn into(self) -> char { + let value: u8 = self.into(); + (value + '1' as u8) as char + } +} + +impl TryFrom for File { + type Error = (); + + fn try_from(value: char) -> Result { + File::try_from(value.to_ascii_lowercase() as u8 - 'a' as u8) + } +} + +impl TryFrom for Rank { + type Error = (); + + fn try_from(value: char) -> Result { + let result = (value as u8).checked_sub('1' as u8).ok_or(())?; + Self::try_from(result) } } @@ -275,16 +332,16 @@ mod tests { #[test] fn good_algebraic_input() { let sq = Square::from_algebraic_str("a4").expect("Failed to parse 'a4' square"); - assert_eq!(sq.file(), File::A); - assert_eq!(sq.rank(), Rank::Four); + assert_eq!(sq.file(), File(0)); + assert_eq!(sq.rank(), Rank(3)); let sq = Square::from_algebraic_str("B8").expect("Failed to parse 'B8' square"); - assert_eq!(sq.file(), File::B); - assert_eq!(sq.rank(), Rank::Eight); + assert_eq!(sq.file(), File(1)); + assert_eq!(sq.rank(), Rank(7)); let sq = Square::from_algebraic_str("e4").expect("Failed to parse 'B8' square"); - assert_eq!(sq.file(), File::E); - assert_eq!(sq.rank(), Rank::Four); + assert_eq!(sq.file(), File(4)); + assert_eq!(sq.rank(), Rank(3)); } #[test] @@ -299,13 +356,13 @@ mod tests { #[test] fn from_index() { - let sq = Square::try_index(4).expect("Unable to get Square from index"); - assert_eq!(sq.file(), File::E); - assert_eq!(sq.rank(), Rank::One); + let sq = Square::try_from(4u32).expect("Unable to get Square from index"); + assert_eq!(sq.file(), File(4)); + assert_eq!(sq.rank(), Rank(0)); - let sq = Square::try_index(28).expect("Unable to get Square from index"); - assert_eq!(sq.file(), File::E); - assert_eq!(sq.rank(), Rank::Four); + let sq = Square::try_from(28u32).expect("Unable to get Square from index"); + assert_eq!(sq.file(), File(4)); + assert_eq!(sq.rank(), Rank(3)); } #[test] diff --git a/core/src/lib.rs b/core/src/lib.rs index e69de29..2fe191d 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -0,0 +1,3 @@ +mod coordinates; + +pub use coordinates::{Direction, File, Rank, Square}; From 3c3a62345d2ec67983a01bdff6867fc702d9b37b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 24 Jan 2024 08:29:16 -0800 Subject: [PATCH 070/423] =?UTF-8?q?[core]=20Rename=20core=20=E2=86=92=20ch?= =?UTF-8?q?ess=5Fcore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 2 +- {core => chess_core}/Cargo.toml | 2 +- {core => chess_core}/src/coordinates.rs | 0 {core => chess_core}/src/lib.rs | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename {core => chess_core}/Cargo.toml (88%) rename {core => chess_core}/src/coordinates.rs (100%) rename {core => chess_core}/src/lib.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index d595fa5..0ecbba5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,6 @@ members = [ "board", - "core", + "chess_core", "explorer", ] diff --git a/core/Cargo.toml b/chess_core/Cargo.toml similarity index 88% rename from core/Cargo.toml rename to chess_core/Cargo.toml index 900733d..9b8c272 100644 --- a/core/Cargo.toml +++ b/chess_core/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "core" +name = "chess_core" version = "0.1.0" edition = "2021" diff --git a/core/src/coordinates.rs b/chess_core/src/coordinates.rs similarity index 100% rename from core/src/coordinates.rs rename to chess_core/src/coordinates.rs diff --git a/core/src/lib.rs b/chess_core/src/lib.rs similarity index 100% rename from core/src/lib.rs rename to chess_core/src/lib.rs From 106800bcb353f57d6ceaf9f816d7ce96026dbd55 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 24 Jan 2024 08:32:09 -0800 Subject: [PATCH 071/423] [core,board] Update all use imports referring to Square, Rank, and File --- board/Cargo.toml | 1 + board/src/fen.rs | 5 ++-- board/src/lib.rs | 1 - board/src/macros.rs | 2 +- board/src/move.rs | 24 +++++++++---------- board/src/move_generator/bishop.rs | 2 +- board/src/move_generator/queen.rs | 2 +- board/src/move_generator/rook.rs | 7 +++--- board/src/piece.rs | 6 ++--- board/src/position/builders/move_builder.rs | 4 ++-- .../src/position/builders/position_builder.rs | 6 ++--- board/src/position/diagram_formatter.rs | 3 ++- board/src/position/piece_sets.rs | 3 ++- board/src/position/pieces.rs | 3 +-- board/src/position/position.rs | 6 +++-- board/src/sight.rs | 5 ++-- 16 files changed, 40 insertions(+), 40 deletions(-) diff --git a/board/Cargo.toml b/board/Cargo.toml index 6b600a9..da7df3c 100644 --- a/board/Cargo.toml +++ b/board/Cargo.toml @@ -6,3 +6,4 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +chess_core = { path = "../chess_core" } diff --git a/board/src/fen.rs b/board/src/fen.rs index b212c94..be63610 100644 --- a/board/src/fen.rs +++ b/board/src/fen.rs @@ -3,8 +3,9 @@ use crate::{ piece::{Piece, PlacedPiece}, r#move::Castle, - Color, File, Position, Rank, Square, + Color, Position, }; +use chess_core::{File, Rank, Square}; use std::fmt::Write; #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -42,7 +43,7 @@ impl ToFen for Position { write!(fen_string, "{}", empty_squares).map_err(|err| FenError::FmtError(err))?; empty_squares = 0; } - if rank != &Rank::One { + if rank != &Rank::ONE { write!(fen_string, "/").map_err(|err| FenError::FmtError(err))?; } } diff --git a/board/src/lib.rs b/board/src/lib.rs index 8c68fad..670368f 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -16,6 +16,5 @@ mod sight; pub use piece::{Color, Piece}; pub use position::{MoveBuilder as MakeMoveBuilder, Position, PositionBuilder}; pub use r#move::{Castle, MakeMoveError, Move, MoveBuilder}; -pub use square::{File, Rank, Square}; pub(crate) use bitboard::BitBoard; diff --git a/board/src/macros.rs b/board/src/macros.rs index 9dd3323..ad69496 100644 --- a/board/src/macros.rs +++ b/board/src/macros.rs @@ -6,7 +6,7 @@ macro_rules! piece { $crate::piece::Piece::new($crate::piece::Color::$color, $crate::piece::Shape::$shape) }; ($color:ident $shape:ident on $square:ident) => { - $crate::piece::PlacedPiece::new(piece!($color $shape), $crate::square::Square::$square) + $crate::piece::PlacedPiece::new(piece!($color $shape), chess_core::Square::$square) } } diff --git a/board/src/move.rs b/board/src/move.rs index 08cf988..c593b56 100644 --- a/board/src/move.rs +++ b/board/src/move.rs @@ -1,10 +1,7 @@ // Eryn Wells -use crate::{ - piece::{Piece, PlacedPiece, Shape}, - square::Rank, - Square, -}; +use crate::piece::{Piece, PlacedPiece, Shape}; +use chess_core::{Rank, Square}; use std::fmt; pub use castle::Castle; @@ -20,7 +17,8 @@ pub enum MakeMoveError { } mod castle { - use crate::{Color, Square}; + use crate::Color; + use chess_core::Square; #[repr(u16)] #[derive(Copy, Clone, Debug, Eq, PartialEq)] @@ -248,8 +246,8 @@ impl MoveBuilder { Shape::Pawn => { let from_rank = from.rank(); let to_rank = to.rank(); - let is_white_double_push = from_rank == Rank::Two && to_rank == Rank::Four; - let is_black_double_push = from_rank == Rank::Seven && to_rank == Rank::Five; + let is_white_double_push = from_rank == Rank::TWO && to_rank == Rank::FOUR; + let is_black_double_push = from_rank == Rank::SEVEN && to_rank == Rank::FIVE; if is_white_double_push || is_black_double_push { Kind::DoublePush } else { @@ -421,8 +419,8 @@ mod move_formatter { $crate::piece::Color::$color, $crate::piece::Shape::$shape, ), - $crate::Square::$from_square, - $crate::Square::$to_square, + chess_core::Square::$from_square, + chess_core::Square::$to_square, ) .build() }; @@ -432,15 +430,15 @@ mod move_formatter { $crate::piece::Color::$color, $crate::piece::Shape::$shape, ), - $crate::Square::$from_square, - $crate::Square::$to_square, + chess_core::Square::$from_square, + chess_core::Square::$to_square, ) .capturing($crate::piece::PlacedPiece::new( $crate::piece::Piece::new( $crate::piece::Color::$captured_color, $crate::piece::Shape::$captured_shape, ), - $crate::Square::$to_square, + chess_core::Square::$to_square, )) .build() }; diff --git a/board/src/move_generator/bishop.rs b/board/src/move_generator/bishop.rs index 7b07d54..7bad274 100644 --- a/board/src/move_generator/bishop.rs +++ b/board/src/move_generator/bishop.rs @@ -3,9 +3,9 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::{ piece::{Color, Piece, PlacedPiece}, - square::Direction, BitBoard, MoveBuilder, Position, }; +use chess_core::Direction; move_generator_declaration!(ClassicalMoveGenerator); diff --git a/board/src/move_generator/queen.rs b/board/src/move_generator/queen.rs index 6694bf6..10ad498 100644 --- a/board/src/move_generator/queen.rs +++ b/board/src/move_generator/queen.rs @@ -3,9 +3,9 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::{ piece::{Color, Piece, PlacedPiece}, - square::Direction, BitBoard, MoveBuilder, Position, }; +use chess_core::Direction; move_generator_declaration!(ClassicalMoveGenerator); diff --git a/board/src/move_generator/rook.rs b/board/src/move_generator/rook.rs index bd48711..8ee6acd 100644 --- a/board/src/move_generator/rook.rs +++ b/board/src/move_generator/rook.rs @@ -3,9 +3,9 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::{ piece::{Color, Piece, PlacedPiece}, - square::Direction, BitBoard, MoveBuilder, Position, }; +use chess_core::Direction; move_generator_declaration!(ClassicalMoveGenerator); @@ -62,9 +62,8 @@ impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> { #[cfg(test)] mod tests { use super::*; - use crate::{ - piece::Piece, position, position::DiagramFormatter, BitBoard, Color, Position, Square, - }; + use crate::{piece::Piece, position, position::DiagramFormatter, BitBoard, Color, Position}; + use chess_core::Square; #[test] fn classical_single_rook_bitboard() { diff --git a/board/src/piece.rs b/board/src/piece.rs index f65a7b8..cc91765 100644 --- a/board/src/piece.rs +++ b/board/src/piece.rs @@ -1,9 +1,7 @@ // Eryn Wells -use crate::{ - display::{ASCIIDisplay, FENDisplay, UnicodeDisplay}, - Square, -}; +use crate::display::{ASCIIDisplay, FENDisplay, UnicodeDisplay}; +use chess_core::Square; use std::fmt; use std::slice::Iter; diff --git a/board/src/position/builders/move_builder.rs b/board/src/position/builders/move_builder.rs index 5d12b19..2146e9d 100644 --- a/board/src/position/builders/move_builder.rs +++ b/board/src/position/builders/move_builder.rs @@ -4,9 +4,9 @@ use crate::{ piece::{PlacedPiece, Shape}, position::flags::Flags, r#move::Castle, - square::Direction, - BitBoard, Color, MakeMoveError, Move, Piece, Position, Square, + BitBoard, Color, MakeMoveError, Move, Piece, Position, }; +use chess_core::{Direction, Square}; /// A position builder that builds a new position by making a move. #[derive(Clone)] diff --git a/board/src/position/builders/position_builder.rs b/board/src/position/builders/position_builder.rs index 7c68413..0410ca3 100644 --- a/board/src/position/builders/position_builder.rs +++ b/board/src/position/builders/position_builder.rs @@ -5,9 +5,9 @@ use crate::{ piece::{PlacedPiece, Shape}, position::{flags::Flags, piece_sets::PieceBitBoards}, r#move::Castle, - square::{Direction, Rank}, - BitBoard, Color, MakeMoveError, Move, Piece, Position, Square, + BitBoard, Color, MakeMoveError, Move, Piece, Position, }; +use chess_core::{Rank, Square}; use std::collections::BTreeMap; #[derive(Clone)] @@ -100,7 +100,7 @@ impl Builder { // Pawns cannot be placed on the first (back) rank of their side, // and cannot be placed on the final rank without a promotion. let rank = piece.square().rank(); - return rank != Rank::One && rank != Rank::Eight; + return rank != Rank::ONE && rank != Rank::EIGHT; } true diff --git a/board/src/position/diagram_formatter.rs b/board/src/position/diagram_formatter.rs index b007d20..ff4c3cf 100644 --- a/board/src/position/diagram_formatter.rs +++ b/board/src/position/diagram_formatter.rs @@ -1,6 +1,7 @@ // Eryn Wells -use crate::{File, Position, Rank, Square}; +use crate::Position; +use chess_core::{File, Rank, Square}; use std::fmt; pub struct DiagramFormatter<'a>(&'a Position); diff --git a/board/src/position/piece_sets.rs b/board/src/position/piece_sets.rs index 58921b9..fa08c84 100644 --- a/board/src/position/piece_sets.rs +++ b/board/src/position/piece_sets.rs @@ -2,8 +2,9 @@ use crate::{ piece::{Piece, PlacedPiece}, - BitBoard, Color, Square, + BitBoard, Color, }; +use chess_core::Square; #[derive(Debug, Eq, PartialEq)] pub enum PlacePieceStrategy { diff --git a/board/src/position/pieces.rs b/board/src/position/pieces.rs index d1cb6c7..4e4495d 100644 --- a/board/src/position/pieces.rs +++ b/board/src/position/pieces.rs @@ -3,7 +3,7 @@ use super::Position; use crate::piece::{Color, Piece, PlacedPiece, Shape}; use crate::BitBoard; -use crate::Square; +use chess_core::Square; pub struct Pieces<'a> { color: Color, @@ -76,7 +76,6 @@ impl<'a> Iterator for Pieces<'a> { mod tests { use super::*; use crate::piece::{Color, Piece, Shape}; - use crate::Square; use crate::{Position, PositionBuilder}; use std::collections::HashSet; diff --git a/board/src/position/position.rs b/board/src/position/position.rs index 484293e..ebc2948 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -7,8 +7,9 @@ use crate::{ position::DiagramFormatter, r#move::Castle, sight::Sight, - BitBoard, Move, Square, + BitBoard, Move, }; +use chess_core::Square; use std::{cell::OnceCell, fmt}; #[derive(Clone, Debug, Eq, PartialEq)] @@ -266,7 +267,8 @@ impl fmt::Display for Position { #[cfg(test)] mod tests { - use crate::{position, Castle, Color, Position, Square}; + use crate::{position, Castle, Color, Position}; + use chess_core::Square; #[test] fn piece_on_square() { diff --git a/board/src/sight.rs b/board/src/sight.rs index 33bd2d8..7763bac 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -2,9 +2,9 @@ use crate::{ piece::{Color, PlacedPiece, Shape}, - square::Direction, BitBoard, Position, }; +use chess_core::Direction; pub(crate) trait Sight { fn sight_in_position(&self, position: &Position) -> BitBoard; @@ -174,7 +174,8 @@ mod tests { } mod pawn { - use crate::{sight::Sight, BitBoard, Square}; + use crate::{sight::Sight, BitBoard}; + use chess_core::Square; sight_test!(e4_pawn, piece!(White Pawn on E4), bitboard!(D5, F5)); From eab30cc33baea919991450a5a3d16c65b6731091 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 24 Jan 2024 08:33:07 -0800 Subject: [PATCH 072/423] =?UTF-8?q?[core]=20Rename=20the=20directory=20che?= =?UTF-8?q?ss=5Fcore=20=E2=86=92=20core?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I think this is sufficient. --- {chess_core => core}/Cargo.toml | 0 {chess_core => core}/src/coordinates.rs | 0 {chess_core => core}/src/lib.rs | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename {chess_core => core}/Cargo.toml (100%) rename {chess_core => core}/src/coordinates.rs (100%) rename {chess_core => core}/src/lib.rs (100%) diff --git a/chess_core/Cargo.toml b/core/Cargo.toml similarity index 100% rename from chess_core/Cargo.toml rename to core/Cargo.toml diff --git a/chess_core/src/coordinates.rs b/core/src/coordinates.rs similarity index 100% rename from chess_core/src/coordinates.rs rename to core/src/coordinates.rs diff --git a/chess_core/src/lib.rs b/core/src/lib.rs similarity index 100% rename from chess_core/src/lib.rs rename to core/src/lib.rs From 32100b9553169c8553bfa360221f7fe4845fcb0b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 24 Jan 2024 08:34:23 -0800 Subject: [PATCH 073/423] [bitboard] Make an empty chess_bitboard crate This crate lives in bitboard/ --- Cargo.toml | 1 + bitboard/Cargo.toml | 9 +++++++++ bitboard/src/lib.rs | 1 + 3 files changed, 11 insertions(+) create mode 100644 bitboard/Cargo.toml create mode 100644 bitboard/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 0ecbba5..a5661d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "board", + "chess_bitboard", "chess_core", "explorer", ] diff --git a/bitboard/Cargo.toml b/bitboard/Cargo.toml new file mode 100644 index 0000000..0670e94 --- /dev/null +++ b/bitboard/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "chess_bitboard" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +core = { path = "../core" } diff --git a/bitboard/src/lib.rs b/bitboard/src/lib.rs new file mode 100644 index 0000000..8aa971d --- /dev/null +++ b/bitboard/src/lib.rs @@ -0,0 +1 @@ +// Eryn Wells From 625bfb2446de4e3a7502714523e82fe6b224fd85 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 24 Jan 2024 08:35:22 -0800 Subject: [PATCH 074/423] [bitboard] Move everything in board::bitboard to the bitboard crate --- .../bitboard => bitboard/src}/bit_scanner.rs | 0 .../src/bitboard => bitboard/src}/bitboard.rs | 13 +++++++------ bitboard/src/lib.rs | 17 +++++++++++++++++ {board/src/bitboard => bitboard/src}/library.rs | 2 +- {board/src/bitboard => bitboard/src}/shifts.rs | 0 board/src/bitboard/mod.rs | 16 ---------------- board/src/lib.rs | 2 -- 7 files changed, 25 insertions(+), 25 deletions(-) rename {board/src/bitboard => bitboard/src}/bit_scanner.rs (100%) rename {board/src/bitboard => bitboard/src}/bitboard.rs (96%) rename {board/src/bitboard => bitboard/src}/library.rs (99%) rename {board/src/bitboard => bitboard/src}/shifts.rs (100%) delete mode 100644 board/src/bitboard/mod.rs diff --git a/board/src/bitboard/bit_scanner.rs b/bitboard/src/bit_scanner.rs similarity index 100% rename from board/src/bitboard/bit_scanner.rs rename to bitboard/src/bit_scanner.rs diff --git a/board/src/bitboard/bitboard.rs b/bitboard/src/bitboard.rs similarity index 96% rename from board/src/bitboard/bitboard.rs rename to bitboard/src/bitboard.rs index 5c35ca8..730c7c2 100644 --- a/board/src/bitboard/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -1,8 +1,8 @@ // Eryn Wells -use super::library::{library, FILES, RANKS}; -use super::LeadingBitScanner; -use crate::{square::Direction, Square}; +use crate::library::{library, FILES, RANKS}; +use crate::LeadingBitScanner; +use chess_core::{Direction, Square}; use std::fmt; use std::ops::Not; @@ -72,13 +72,13 @@ impl BitBoard { /// Return an Iterator over the occupied squares, starting from the leading /// (most-significant bit) end of the field. pub(crate) fn occupied_squares(&self) -> impl Iterator { - LeadingBitScanner::new(self.0).map(Square::from_index) + LeadingBitScanner::new(self.0).map(|idx| unsafe { Square::from_index(idx as u8) }) } /// Return an Iterator over the occupied squares, starting from the trailing /// (least-significant bit) end of the field. pub(crate) fn occupied_squares_trailing(&self) -> impl Iterator { - LeadingBitScanner::new(self.0).map(Square::from_index) + LeadingBitScanner::new(self.0).map(|idx| unsafe { Square::from_index(idx as u8) }) } } @@ -243,7 +243,8 @@ impl BitBoardBuilder { #[cfg(test)] mod tests { use super::*; - use crate::{bitboard, Square}; + use crate::bitboard; + use chess_core::Square; #[test] fn display_and_debug() { diff --git a/bitboard/src/lib.rs b/bitboard/src/lib.rs index 8aa971d..d3c0c4b 100644 --- a/bitboard/src/lib.rs +++ b/bitboard/src/lib.rs @@ -1 +1,18 @@ // Eryn Wells + +mod bit_scanner; +mod bitboard; +mod library; +mod shifts; + +pub(crate) use bit_scanner::{LeadingBitScanner, TrailingBitScanner}; +pub(crate) use bitboard::{BitBoard, BitBoardBuilder}; + +#[macro_export] +macro_rules! bitboard { + ($($sq:ident),* $(,)?) => { + $crate::bitboard::BitBoardBuilder::empty() + $(.square(chess_core::Square::$sq))* + .build() + }; +} diff --git a/board/src/bitboard/library.rs b/bitboard/src/library.rs similarity index 99% rename from board/src/bitboard/library.rs rename to bitboard/src/library.rs index fe2daa0..1e6a84d 100644 --- a/board/src/bitboard/library.rs +++ b/bitboard/src/library.rs @@ -1,7 +1,7 @@ // Eryn Wells use super::BitBoard; -use crate::{square::Direction, Square}; +use chess_core::{Direction, Square}; use std::sync::Once; pub(super) const RANKS: [BitBoard; 8] = [ diff --git a/board/src/bitboard/shifts.rs b/bitboard/src/shifts.rs similarity index 100% rename from board/src/bitboard/shifts.rs rename to bitboard/src/shifts.rs diff --git a/board/src/bitboard/mod.rs b/board/src/bitboard/mod.rs deleted file mode 100644 index c417abd..0000000 --- a/board/src/bitboard/mod.rs +++ /dev/null @@ -1,16 +0,0 @@ -mod bit_scanner; -mod bitboard; -mod library; -mod shifts; - -pub(crate) use bit_scanner::{LeadingBitScanner, TrailingBitScanner}; -pub(crate) use bitboard::{BitBoard, BitBoardBuilder}; - -#[macro_export] -macro_rules! bitboard { - ($($sq:ident),* $(,)?) => { - $crate::bitboard::BitBoardBuilder::empty() - $(.square($crate::Square::$sq))* - .build() - }; -} diff --git a/board/src/lib.rs b/board/src/lib.rs index 670368f..2ed3495 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -2,8 +2,6 @@ pub mod fen; -#[macro_use] -mod bitboard; mod display; mod r#move; #[macro_use] From b0b22048a8efdd120faaf7d350318162cccc19d7 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 24 Jan 2024 08:48:19 -0800 Subject: [PATCH 075/423] =?UTF-8?q?[core]=20Rename=20(once=20again)=20ches?= =?UTF-8?q?s=5Fcore=20=E2=86=92=20chessfriend=5Fcore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 2 +- bitboard/Cargo.toml | 2 +- bitboard/src/bitboard.rs | 2 +- bitboard/src/lib.rs | 2 +- bitboard/src/library.rs | 2 +- board/Cargo.toml | 2 +- board/src/fen.rs | 2 +- board/src/macros.rs | 2 +- board/src/move.rs | 14 +++++++------- board/src/move_generator/bishop.rs | 2 +- board/src/move_generator/queen.rs | 2 +- board/src/move_generator/rook.rs | 4 ++-- board/src/piece.rs | 2 +- board/src/position/builders/move_builder.rs | 2 +- board/src/position/builders/position_builder.rs | 2 +- board/src/position/diagram_formatter.rs | 2 +- board/src/position/piece_sets.rs | 2 +- board/src/position/pieces.rs | 2 +- board/src/position/position.rs | 4 ++-- board/src/sight.rs | 4 ++-- core/Cargo.toml | 2 +- 21 files changed, 30 insertions(+), 30 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a5661d8..7542226 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,6 @@ members = [ "board", "chess_bitboard", - "chess_core", + "chessfriend_core", "explorer", ] diff --git a/bitboard/Cargo.toml b/bitboard/Cargo.toml index 0670e94..e565de3 100644 --- a/bitboard/Cargo.toml +++ b/bitboard/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -core = { path = "../core" } +chessfriend_core = { path = "../core" } diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 730c7c2..5be905a 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -2,7 +2,7 @@ use crate::library::{library, FILES, RANKS}; use crate::LeadingBitScanner; -use chess_core::{Direction, Square}; +use chessfriend_core::{Direction, Square}; use std::fmt; use std::ops::Not; diff --git a/bitboard/src/lib.rs b/bitboard/src/lib.rs index d3c0c4b..6b0703e 100644 --- a/bitboard/src/lib.rs +++ b/bitboard/src/lib.rs @@ -12,7 +12,7 @@ pub(crate) use bitboard::{BitBoard, BitBoardBuilder}; macro_rules! bitboard { ($($sq:ident),* $(,)?) => { $crate::bitboard::BitBoardBuilder::empty() - $(.square(chess_core::Square::$sq))* + $(.square(chessfriend_core::Square::$sq))* .build() }; } diff --git a/bitboard/src/library.rs b/bitboard/src/library.rs index 1e6a84d..506d6cd 100644 --- a/bitboard/src/library.rs +++ b/bitboard/src/library.rs @@ -1,7 +1,7 @@ // Eryn Wells use super::BitBoard; -use chess_core::{Direction, Square}; +use chessfriend_core::{Direction, Square}; use std::sync::Once; pub(super) const RANKS: [BitBoard; 8] = [ diff --git a/board/Cargo.toml b/board/Cargo.toml index da7df3c..2f8727d 100644 --- a/board/Cargo.toml +++ b/board/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -chess_core = { path = "../chess_core" } +chessfriend_core = { path = "../core" } diff --git a/board/src/fen.rs b/board/src/fen.rs index be63610..8610f00 100644 --- a/board/src/fen.rs +++ b/board/src/fen.rs @@ -5,7 +5,7 @@ use crate::{ r#move::Castle, Color, Position, }; -use chess_core::{File, Rank, Square}; +use chessfriend_core::{File, Rank, Square}; use std::fmt::Write; #[derive(Clone, Copy, Debug, Eq, PartialEq)] diff --git a/board/src/macros.rs b/board/src/macros.rs index ad69496..c6c374c 100644 --- a/board/src/macros.rs +++ b/board/src/macros.rs @@ -6,7 +6,7 @@ macro_rules! piece { $crate::piece::Piece::new($crate::piece::Color::$color, $crate::piece::Shape::$shape) }; ($color:ident $shape:ident on $square:ident) => { - $crate::piece::PlacedPiece::new(piece!($color $shape), chess_core::Square::$square) + $crate::piece::PlacedPiece::new(piece!($color $shape), chessfriend_core::Square::$square) } } diff --git a/board/src/move.rs b/board/src/move.rs index c593b56..7b3e55c 100644 --- a/board/src/move.rs +++ b/board/src/move.rs @@ -1,7 +1,7 @@ // Eryn Wells use crate::piece::{Piece, PlacedPiece, Shape}; -use chess_core::{Rank, Square}; +use chessfriend_core::{Rank, Square}; use std::fmt; pub use castle::Castle; @@ -18,7 +18,7 @@ pub enum MakeMoveError { mod castle { use crate::Color; - use chess_core::Square; + use chessfriend_core::Square; #[repr(u16)] #[derive(Copy, Clone, Debug, Eq, PartialEq)] @@ -419,8 +419,8 @@ mod move_formatter { $crate::piece::Color::$color, $crate::piece::Shape::$shape, ), - chess_core::Square::$from_square, - chess_core::Square::$to_square, + chessfriend_core::Square::$from_square, + chessfriend_core::Square::$to_square, ) .build() }; @@ -430,15 +430,15 @@ mod move_formatter { $crate::piece::Color::$color, $crate::piece::Shape::$shape, ), - chess_core::Square::$from_square, - chess_core::Square::$to_square, + chessfriend_core::Square::$from_square, + chessfriend_core::Square::$to_square, ) .capturing($crate::piece::PlacedPiece::new( $crate::piece::Piece::new( $crate::piece::Color::$captured_color, $crate::piece::Shape::$captured_shape, ), - chess_core::Square::$to_square, + chessfriend_core::Square::$to_square, )) .build() }; diff --git a/board/src/move_generator/bishop.rs b/board/src/move_generator/bishop.rs index 7bad274..cd259f7 100644 --- a/board/src/move_generator/bishop.rs +++ b/board/src/move_generator/bishop.rs @@ -5,7 +5,7 @@ use crate::{ piece::{Color, Piece, PlacedPiece}, BitBoard, MoveBuilder, Position, }; -use chess_core::Direction; +use chessfriend_core::Direction; move_generator_declaration!(ClassicalMoveGenerator); diff --git a/board/src/move_generator/queen.rs b/board/src/move_generator/queen.rs index 10ad498..f012d8c 100644 --- a/board/src/move_generator/queen.rs +++ b/board/src/move_generator/queen.rs @@ -5,7 +5,7 @@ use crate::{ piece::{Color, Piece, PlacedPiece}, BitBoard, MoveBuilder, Position, }; -use chess_core::Direction; +use chessfriend_core::Direction; move_generator_declaration!(ClassicalMoveGenerator); diff --git a/board/src/move_generator/rook.rs b/board/src/move_generator/rook.rs index 8ee6acd..c6dbb7b 100644 --- a/board/src/move_generator/rook.rs +++ b/board/src/move_generator/rook.rs @@ -5,7 +5,7 @@ use crate::{ piece::{Color, Piece, PlacedPiece}, BitBoard, MoveBuilder, Position, }; -use chess_core::Direction; +use chessfriend_core::Direction; move_generator_declaration!(ClassicalMoveGenerator); @@ -63,7 +63,7 @@ impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> { mod tests { use super::*; use crate::{piece::Piece, position, position::DiagramFormatter, BitBoard, Color, Position}; - use chess_core::Square; + use chessfriend_core::Square; #[test] fn classical_single_rook_bitboard() { diff --git a/board/src/piece.rs b/board/src/piece.rs index cc91765..b80d1da 100644 --- a/board/src/piece.rs +++ b/board/src/piece.rs @@ -1,7 +1,7 @@ // Eryn Wells use crate::display::{ASCIIDisplay, FENDisplay, UnicodeDisplay}; -use chess_core::Square; +use chessfriend_core::Square; use std::fmt; use std::slice::Iter; diff --git a/board/src/position/builders/move_builder.rs b/board/src/position/builders/move_builder.rs index 2146e9d..17eb36d 100644 --- a/board/src/position/builders/move_builder.rs +++ b/board/src/position/builders/move_builder.rs @@ -6,7 +6,7 @@ use crate::{ r#move::Castle, BitBoard, Color, MakeMoveError, Move, Piece, Position, }; -use chess_core::{Direction, Square}; +use chessfriend_core::{Direction, Square}; /// A position builder that builds a new position by making a move. #[derive(Clone)] diff --git a/board/src/position/builders/position_builder.rs b/board/src/position/builders/position_builder.rs index 0410ca3..0866094 100644 --- a/board/src/position/builders/position_builder.rs +++ b/board/src/position/builders/position_builder.rs @@ -7,8 +7,8 @@ use crate::{ r#move::Castle, BitBoard, Color, MakeMoveError, Move, Piece, Position, }; -use chess_core::{Rank, Square}; use std::collections::BTreeMap; +use chessfriend_core::Square; #[derive(Clone)] pub struct Builder { diff --git a/board/src/position/diagram_formatter.rs b/board/src/position/diagram_formatter.rs index ff4c3cf..0c46de4 100644 --- a/board/src/position/diagram_formatter.rs +++ b/board/src/position/diagram_formatter.rs @@ -1,7 +1,7 @@ // Eryn Wells use crate::Position; -use chess_core::{File, Rank, Square}; +use chessfriend_core::{File, Rank, Square}; use std::fmt; pub struct DiagramFormatter<'a>(&'a Position); diff --git a/board/src/position/piece_sets.rs b/board/src/position/piece_sets.rs index fa08c84..856a3e8 100644 --- a/board/src/position/piece_sets.rs +++ b/board/src/position/piece_sets.rs @@ -4,7 +4,7 @@ use crate::{ piece::{Piece, PlacedPiece}, BitBoard, Color, }; -use chess_core::Square; +use chessfriend_core::Square; #[derive(Debug, Eq, PartialEq)] pub enum PlacePieceStrategy { diff --git a/board/src/position/pieces.rs b/board/src/position/pieces.rs index 4e4495d..959a9da 100644 --- a/board/src/position/pieces.rs +++ b/board/src/position/pieces.rs @@ -3,7 +3,7 @@ use super::Position; use crate::piece::{Color, Piece, PlacedPiece, Shape}; use crate::BitBoard; -use chess_core::Square; +use chessfriend_core::Square; pub struct Pieces<'a> { color: Color, diff --git a/board/src/position/position.rs b/board/src/position/position.rs index ebc2948..e1f2f92 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -9,7 +9,7 @@ use crate::{ sight::Sight, BitBoard, Move, }; -use chess_core::Square; +use chessfriend_core::Square; use std::{cell::OnceCell, fmt}; #[derive(Clone, Debug, Eq, PartialEq)] @@ -268,7 +268,7 @@ impl fmt::Display for Position { #[cfg(test)] mod tests { use crate::{position, Castle, Color, Position}; - use chess_core::Square; + use chessfriend_core::Square; #[test] fn piece_on_square() { diff --git a/board/src/sight.rs b/board/src/sight.rs index 7763bac..92b295f 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -4,7 +4,7 @@ use crate::{ piece::{Color, PlacedPiece, Shape}, BitBoard, Position, }; -use chess_core::Direction; +use chessfriend_core::Direction; pub(crate) trait Sight { fn sight_in_position(&self, position: &Position) -> BitBoard; @@ -175,7 +175,7 @@ mod tests { mod pawn { use crate::{sight::Sight, BitBoard}; - use chess_core::Square; + use chessfriend_core::Square; sight_test!(e4_pawn, piece!(White Pawn on E4), bitboard!(D5, F5)); diff --git a/core/Cargo.toml b/core/Cargo.toml index 9b8c272..9b33128 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "chess_core" +name = "chessfriend_core" version = "0.1.0" edition = "2021" From 7738f30e5e51c487d6e1fca0d829e14684d9748c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 24 Jan 2024 09:14:59 -0800 Subject: [PATCH 076/423] Fix the workspace members The items in the members array are paths, not crate names. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7542226..cfa9c81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ members = [ "board", - "chess_bitboard", - "chessfriend_core", + "bitboard", + "core", "explorer", ] From d901be53d2ff45173e8be11319fa69740eb1c724 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 24 Jan 2024 09:15:18 -0800 Subject: [PATCH 077/423] [explorer] Depends on core --- explorer/Cargo.toml | 1 + explorer/src/main.rs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/explorer/Cargo.toml b/explorer/Cargo.toml index 5d49281..3f2513b 100644 --- a/explorer/Cargo.toml +++ b/explorer/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +chessfriend_core = { path = "../core" } board = { path = "../board" } clap = { version = "4.4.12", features = ["derive"] } rustyline = "13.0.0" diff --git a/explorer/src/main.rs b/explorer/src/main.rs index 9da6a64..e8fca2f 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -1,5 +1,6 @@ use board::piece::{Color, Piece, Shape}; -use board::{Position, Square}; +use board::Position; +use chessfriend_core::Square; use clap::{Arg, Command}; use rustyline::error::ReadlineError; use rustyline::DefaultEditor; From 3cec64d6861d0b36fc88182e8ab1aa5d0c28a58d Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 24 Jan 2024 09:16:21 -0800 Subject: [PATCH 078/423] [bitboard] Make the bitboard crate more crate-like Export symbols needed to use BitBoard and BitBoardBuilder. Fix build errors. --- bitboard/Cargo.toml | 2 +- bitboard/src/bitboard.rs | 8 ++++---- bitboard/src/lib.rs | 5 +++-- bitboard/src/library.rs | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/bitboard/Cargo.toml b/bitboard/Cargo.toml index e565de3..df608b5 100644 --- a/bitboard/Cargo.toml +++ b/bitboard/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "chess_bitboard" +name = "chessfriend_bitboard" version = "0.1.0" edition = "2021" diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 5be905a..6aa0df2 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -7,7 +7,7 @@ use std::fmt; use std::ops::Not; #[derive(Clone, Copy, Eq, Hash, PartialEq)] -pub(crate) struct BitBoard(pub(super) u64); +pub struct BitBoard(pub(crate) u64); macro_rules! moves_getter { ($getter_name:ident) => { @@ -71,13 +71,13 @@ impl BitBoard { impl BitBoard { /// Return an Iterator over the occupied squares, starting from the leading /// (most-significant bit) end of the field. - pub(crate) fn occupied_squares(&self) -> impl Iterator { + pub fn occupied_squares(&self) -> impl Iterator { LeadingBitScanner::new(self.0).map(|idx| unsafe { Square::from_index(idx as u8) }) } /// Return an Iterator over the occupied squares, starting from the trailing /// (least-significant bit) end of the field. - pub(crate) fn occupied_squares_trailing(&self) -> impl Iterator { + pub fn occupied_squares_trailing(&self) -> impl Iterator { LeadingBitScanner::new(self.0).map(|idx| unsafe { Square::from_index(idx as u8) }) } } @@ -244,7 +244,7 @@ impl BitBoardBuilder { mod tests { use super::*; use crate::bitboard; - use chess_core::Square; + use chessfriend_core::Square; #[test] fn display_and_debug() { diff --git a/bitboard/src/lib.rs b/bitboard/src/lib.rs index 6b0703e..30a35f2 100644 --- a/bitboard/src/lib.rs +++ b/bitboard/src/lib.rs @@ -5,13 +5,14 @@ mod bitboard; mod library; mod shifts; +pub use bitboard::{BitBoard, BitBoardBuilder}; + pub(crate) use bit_scanner::{LeadingBitScanner, TrailingBitScanner}; -pub(crate) use bitboard::{BitBoard, BitBoardBuilder}; #[macro_export] macro_rules! bitboard { ($($sq:ident),* $(,)?) => { - $crate::bitboard::BitBoardBuilder::empty() + $crate::BitBoardBuilder::empty() $(.square(chessfriend_core::Square::$sq))* .build() }; diff --git a/bitboard/src/library.rs b/bitboard/src/library.rs index 506d6cd..6c59291 100644 --- a/bitboard/src/library.rs +++ b/bitboard/src/library.rs @@ -1,6 +1,6 @@ // Eryn Wells -use super::BitBoard; +use crate::BitBoard; use chessfriend_core::{Direction, Square}; use std::sync::Once; From 6f85305912d6b8ca4cc290f6f6e7e2290ba9e145 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 24 Jan 2024 09:18:12 -0800 Subject: [PATCH 079/423] [board] Clean up a bunch of build errors Fix imports to refer to core and bitboard crates. Fix some API use errors. --- board/Cargo.toml | 1 + board/src/lib.rs | 2 -- board/src/move_generator/bishop.rs | 6 ++++-- board/src/move_generator/king.rs | 6 ++++-- board/src/move_generator/knight.rs | 6 ++++-- board/src/move_generator/mod.rs | 19 +++++++++++-------- board/src/move_generator/move_generator.rs | 3 ++- board/src/move_generator/move_set.rs | 3 ++- board/src/move_generator/pawn.rs | 6 ++++-- board/src/move_generator/queen.rs | 6 ++++-- board/src/move_generator/rook.rs | 6 ++++-- board/src/position/builders/move_builder.rs | 3 ++- .../src/position/builders/position_builder.rs | 9 ++++----- board/src/position/piece_sets.rs | 5 +++-- board/src/position/pieces.rs | 2 +- board/src/position/position.rs | 3 ++- board/src/sight.rs | 9 +++++++-- 17 files changed, 59 insertions(+), 36 deletions(-) diff --git a/board/Cargo.toml b/board/Cargo.toml index 2f8727d..70584c2 100644 --- a/board/Cargo.toml +++ b/board/Cargo.toml @@ -7,3 +7,4 @@ edition = "2021" [dependencies] chessfriend_core = { path = "../core" } +chessfriend_bitboard = { path = "../bitboard" } diff --git a/board/src/lib.rs b/board/src/lib.rs index 2ed3495..243686e 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -14,5 +14,3 @@ mod sight; pub use piece::{Color, Piece}; pub use position::{MoveBuilder as MakeMoveBuilder, Position, PositionBuilder}; pub use r#move::{Castle, MakeMoveError, Move, MoveBuilder}; - -pub(crate) use bitboard::BitBoard; diff --git a/board/src/move_generator/bishop.rs b/board/src/move_generator/bishop.rs index cd259f7..404d68b 100644 --- a/board/src/move_generator/bishop.rs +++ b/board/src/move_generator/bishop.rs @@ -3,8 +3,9 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::{ piece::{Color, Piece, PlacedPiece}, - BitBoard, MoveBuilder, Position, + MoveBuilder, Position, }; +use chessfriend_bitboard::BitBoard; use chessfriend_core::Direction; move_generator_declaration!(ClassicalMoveGenerator); @@ -60,7 +61,8 @@ impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> { #[cfg(test)] mod tests { use super::*; - use crate::{piece, piece::Color, position, position::DiagramFormatter, BitBoard}; + use crate::{piece, piece::Color, position, position::DiagramFormatter}; + use chessfriend_bitboard::BitBoard; #[test] fn classical_single_bishop_bitboard() { diff --git a/board/src/move_generator/king.rs b/board/src/move_generator/king.rs index 06ae76e..643a987 100644 --- a/board/src/move_generator/king.rs +++ b/board/src/move_generator/king.rs @@ -7,8 +7,9 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::{ piece::{Color, Piece, PlacedPiece}, r#move::Castle, - BitBoard, Move, MoveBuilder, Position, + Move, MoveBuilder, Position, }; +use chessfriend_bitboard::BitBoard; move_generator_declaration!(KingMoveGenerator, struct); move_generator_declaration!(KingMoveGenerator, new); @@ -76,7 +77,8 @@ impl<'pos> MoveGeneratorInternal for KingMoveGenerator<'pos> { #[cfg(test)] mod tests { use super::*; - use crate::Square; + use chessfriend_bitboard::bitboard; + use chessfriend_core::Square; use std::collections::HashSet; #[test] diff --git a/board/src/move_generator/knight.rs b/board/src/move_generator/knight.rs index 50a7ad0..64b360f 100644 --- a/board/src/move_generator/knight.rs +++ b/board/src/move_generator/knight.rs @@ -3,8 +3,9 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::{ piece::{Color, Piece, PlacedPiece}, - BitBoard, MoveBuilder, Position, + MoveBuilder, Position, }; +use chessfriend_bitboard::BitBoard; move_generator_declaration!(KnightMoveGenerator); @@ -40,7 +41,8 @@ impl<'pos> MoveGeneratorInternal for KnightMoveGenerator<'pos> { #[cfg(test)] mod tests { use super::*; - use crate::{piece, position, Move, Square}; + use crate::{piece, position, Move}; + use chessfriend_core::Square; use std::collections::HashSet; #[test] diff --git a/board/src/move_generator/mod.rs b/board/src/move_generator/mod.rs index eb080c3..ed5ac74 100644 --- a/board/src/move_generator/mod.rs +++ b/board/src/move_generator/mod.rs @@ -14,8 +14,9 @@ pub(crate) use move_set::MoveSet; use crate::{ piece::{Color, Piece, PlacedPiece}, - Move, Position, Square, + Move, Position, }; +use chessfriend_core::Square; use std::collections::BTreeMap; trait MoveGenerator { @@ -34,7 +35,10 @@ macro_rules! move_generator_declaration { pub(super) struct $name<'pos> { position: &'pos $crate::Position, color: $crate::piece::Color, - move_sets: std::collections::BTreeMap<$crate::Square, $crate::move_generator::MoveSet>, + move_sets: std::collections::BTreeMap< + chessfriend_core::Square, + $crate::move_generator::MoveSet, + >, } }; ($name:ident, new) => { @@ -54,12 +58,11 @@ macro_rules! move_generator_declaration { self.move_sets.values().map(|set| set.moves()).flatten() } - fn bitboard(&self) -> $crate::BitBoard { - self.move_sets - .values() - .fold($crate::BitBoard::empty(), |partial, mv_set| { - partial | mv_set.bitboard() - }) + fn bitboard(&self) -> chessfriend_bitboard::BitBoard { + self.move_sets.values().fold( + chessfriend_bitboard::BitBoard::empty(), + |partial, mv_set| partial | mv_set.bitboard(), + ) } } }; diff --git a/board/src/move_generator/move_generator.rs b/board/src/move_generator/move_generator.rs index 689e47d..2ef4922 100644 --- a/board/src/move_generator/move_generator.rs +++ b/board/src/move_generator/move_generator.rs @@ -43,7 +43,8 @@ impl<'a> Moves<'a> { #[cfg(test)] mod tests { - use crate::{piece, position, r#move::AlgebraicMoveFormatter, Move, MoveBuilder, Square}; + use crate::{piece, position, r#move::AlgebraicMoveFormatter, Move, MoveBuilder}; + use chessfriend_core::Square; use std::collections::HashSet; #[test] diff --git a/board/src/move_generator/move_set.rs b/board/src/move_generator/move_set.rs index 1c44766..1c22eac 100644 --- a/board/src/move_generator/move_set.rs +++ b/board/src/move_generator/move_set.rs @@ -1,4 +1,5 @@ -use crate::{piece::PlacedPiece, BitBoard, Move}; +use crate::{piece::PlacedPiece, Move}; +use chessfriend_bitboard::BitBoard; #[derive(Clone, Debug, Eq, PartialEq)] struct BitBoardSet { diff --git a/board/src/move_generator/pawn.rs b/board/src/move_generator/pawn.rs index 7f77e15..d366405 100644 --- a/board/src/move_generator/pawn.rs +++ b/board/src/move_generator/pawn.rs @@ -2,8 +2,9 @@ use crate::{ piece::{Color, Piece, Shape}, - BitBoard, Move, MoveBuilder, Position, + Move, MoveBuilder, Position, }; +use chessfriend_bitboard::BitBoard; enum MoveList { Quiet = 0, @@ -246,7 +247,8 @@ impl<'pos> Iterator for PawnMoveGenerator<'pos> { mod tests { use super::*; use crate::position::DiagramFormatter; - use crate::{piece, Position, Square}; + use crate::{piece, Position}; + use chessfriend_core::Square; use std::collections::HashSet; #[test] diff --git a/board/src/move_generator/queen.rs b/board/src/move_generator/queen.rs index f012d8c..fb96dd7 100644 --- a/board/src/move_generator/queen.rs +++ b/board/src/move_generator/queen.rs @@ -3,8 +3,9 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::{ piece::{Color, Piece, PlacedPiece}, - BitBoard, MoveBuilder, Position, + MoveBuilder, Position, }; +use chessfriend_bitboard::BitBoard; use chessfriend_core::Direction; move_generator_declaration!(ClassicalMoveGenerator); @@ -66,7 +67,8 @@ impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> { #[cfg(test)] mod tests { use super::*; - use crate::{piece, position, position::DiagramFormatter, BitBoard, Color}; + use crate::{piece, position, position::DiagramFormatter, Color}; + use chessfriend_bitboard::{bitboard, BitBoard}; #[test] fn classical_single_queen_bitboard() { diff --git a/board/src/move_generator/rook.rs b/board/src/move_generator/rook.rs index c6dbb7b..570255a 100644 --- a/board/src/move_generator/rook.rs +++ b/board/src/move_generator/rook.rs @@ -3,8 +3,9 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::{ piece::{Color, Piece, PlacedPiece}, - BitBoard, MoveBuilder, Position, + MoveBuilder, Position, }; +use chessfriend_bitboard::BitBoard; use chessfriend_core::Direction; move_generator_declaration!(ClassicalMoveGenerator); @@ -62,7 +63,8 @@ impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> { #[cfg(test)] mod tests { use super::*; - use crate::{piece::Piece, position, position::DiagramFormatter, BitBoard, Color, Position}; + use crate::{piece::Piece, position, position::DiagramFormatter, Color, Position}; + use chessfriend_bitboard::{bitboard, BitBoard}; use chessfriend_core::Square; #[test] diff --git a/board/src/position/builders/move_builder.rs b/board/src/position/builders/move_builder.rs index 17eb36d..4530543 100644 --- a/board/src/position/builders/move_builder.rs +++ b/board/src/position/builders/move_builder.rs @@ -4,8 +4,9 @@ use crate::{ piece::{PlacedPiece, Shape}, position::flags::Flags, r#move::Castle, - BitBoard, Color, MakeMoveError, Move, Piece, Position, + Color, MakeMoveError, Move, Piece, Position, }; +use chessfriend_bitboard::BitBoard; use chessfriend_core::{Direction, Square}; /// A position builder that builds a new position by making a move. diff --git a/board/src/position/builders/position_builder.rs b/board/src/position/builders/position_builder.rs index 0866094..88838b7 100644 --- a/board/src/position/builders/position_builder.rs +++ b/board/src/position/builders/position_builder.rs @@ -1,14 +1,13 @@ // Eryn Wells use crate::{ - bitboard::BitBoardBuilder, piece::{PlacedPiece, Shape}, position::{flags::Flags, piece_sets::PieceBitBoards}, r#move::Castle, - BitBoard, Color, MakeMoveError, Move, Piece, Position, + Color, MakeMoveError, Move, Piece, Position, }; +use chessfriend_core::{Rank, Square}; use std::collections::BTreeMap; -use chessfriend_core::Square; #[derive(Clone)] pub struct Builder { @@ -109,8 +108,8 @@ impl Builder { impl Default for Builder { fn default() -> Self { - let white_king_square = Square::king_starting_square(Color::White); - let black_king_square = Square::king_starting_square(Color::Black); + let white_king_square = Square::E1; + let black_king_square = Square::E8; let pieces = BTreeMap::from_iter([ (white_king_square, piece!(White King)), diff --git a/board/src/position/piece_sets.rs b/board/src/position/piece_sets.rs index 856a3e8..d07eb2a 100644 --- a/board/src/position/piece_sets.rs +++ b/board/src/position/piece_sets.rs @@ -2,8 +2,9 @@ use crate::{ piece::{Piece, PlacedPiece}, - BitBoard, Color, + Color, }; +use chessfriend_bitboard::BitBoard; use chessfriend_core::Square; #[derive(Debug, Eq, PartialEq)] @@ -125,7 +126,7 @@ impl PieceBitBoards { impl FromIterator for PieceBitBoards { fn from_iter>(iter: T) -> Self { - let mut pieces = Self::default(); + let mut pieces: Self = Default::default(); for piece in iter { let _ = pieces.place_piece(&piece); diff --git a/board/src/position/pieces.rs b/board/src/position/pieces.rs index 959a9da..8530869 100644 --- a/board/src/position/pieces.rs +++ b/board/src/position/pieces.rs @@ -2,7 +2,7 @@ use super::Position; use crate::piece::{Color, Piece, PlacedPiece, Shape}; -use crate::BitBoard; +use chessfriend_bitboard::BitBoard; use chessfriend_core::Square; pub struct Pieces<'a> { diff --git a/board/src/position/position.rs b/board/src/position/position.rs index e1f2f92..82ef1bd 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -7,8 +7,9 @@ use crate::{ position::DiagramFormatter, r#move::Castle, sight::Sight, - BitBoard, Move, + Move, }; +use chessfriend_bitboard::BitBoard; use chessfriend_core::Square; use std::{cell::OnceCell, fmt}; diff --git a/board/src/sight.rs b/board/src/sight.rs index 92b295f..ffed3f7 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -2,8 +2,9 @@ use crate::{ piece::{Color, PlacedPiece, Shape}, - BitBoard, Position, + Position, }; +use chessfriend_bitboard::BitBoard; use chessfriend_core::Direction; pub(crate) trait Sight { @@ -174,7 +175,8 @@ mod tests { } mod pawn { - use crate::{sight::Sight, BitBoard}; + use crate::sight::Sight; + use chessfriend_bitboard::{bitboard, BitBoard}; use chessfriend_core::Square; sight_test!(e4_pawn, piece!(White Pawn on E4), bitboard!(D5, F5)); @@ -234,6 +236,7 @@ mod tests { #[macro_use] mod knight { use crate::sight::Sight; + use chessfriend_bitboard::bitboard; sight_test!( f6_knight, @@ -244,6 +247,7 @@ mod tests { mod bishop { use crate::sight::Sight; + use chessfriend_bitboard::bitboard; sight_test!( c2_bishop, @@ -254,6 +258,7 @@ mod tests { mod rook { use crate::sight::Sight; + use chessfriend_bitboard::bitboard; sight_test!( g3_rook, From 8b2a3926b35f0912cb0e308fb6df8ece49d3714a Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 24 Jan 2024 17:08:27 -0800 Subject: [PATCH 080/423] [core,board] Move board::piece to core Break up types in core into finer grained modules. Update all the imports. --- board/src/lib.rs | 7 +- board/src/macros.rs | 10 -- board/src/move.rs | 34 +++--- board/src/move_generator/bishop.rs | 10 +- board/src/move_generator/king.rs | 10 +- board/src/move_generator/knight.rs | 10 +- board/src/move_generator/mod.rs | 22 ++-- board/src/move_generator/move_generator.rs | 21 +++- board/src/move_generator/move_set.rs | 5 +- board/src/move_generator/pawn.rs | 7 +- board/src/move_generator/queen.rs | 10 +- board/src/move_generator/rook.rs | 11 +- board/src/position/builders/move_builder.rs | 10 +- .../src/position/builders/position_builder.rs | 6 +- board/src/position/diagram_formatter.rs | 3 +- board/src/position/flags.rs | 6 +- board/src/position/piece_sets.rs | 6 +- board/src/position/pieces.rs | 6 +- board/src/position/position.rs | 11 +- board/src/sight.rs | 34 +++--- core/src/colors.rs | 62 ++++++++++ core/src/errors.rs | 7 ++ core/src/lib.rs | 11 +- core/src/macros.rs | 32 +++++ board/src/piece.rs => core/src/pieces.rs | 111 +----------------- 25 files changed, 235 insertions(+), 227 deletions(-) create mode 100644 core/src/colors.rs create mode 100644 core/src/errors.rs create mode 100644 core/src/macros.rs rename board/src/piece.rs => core/src/pieces.rs (69%) diff --git a/board/src/lib.rs b/board/src/lib.rs index 243686e..015b10b 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -4,13 +4,12 @@ pub mod fen; mod display; mod r#move; -#[macro_use] -mod macros; mod move_generator; -pub mod piece; mod position; mod sight; -pub use piece::{Color, Piece}; +#[macro_use] +mod macros; + pub use position::{MoveBuilder as MakeMoveBuilder, Position, PositionBuilder}; pub use r#move::{Castle, MakeMoveError, Move, MoveBuilder}; diff --git a/board/src/macros.rs b/board/src/macros.rs index c6c374c..26827b6 100644 --- a/board/src/macros.rs +++ b/board/src/macros.rs @@ -1,15 +1,5 @@ // Eryn Wells -#[macro_export] -macro_rules! piece { - ($color:ident $shape:ident) => { - $crate::piece::Piece::new($crate::piece::Color::$color, $crate::piece::Shape::$shape) - }; - ($color:ident $shape:ident on $square:ident) => { - $crate::piece::PlacedPiece::new(piece!($color $shape), chessfriend_core::Square::$square) - } -} - #[macro_export] macro_rules! position { [$($color:ident $shape:ident on $square:ident),* $(,)?] => { diff --git a/board/src/move.rs b/board/src/move.rs index 7b3e55c..0469e82 100644 --- a/board/src/move.rs +++ b/board/src/move.rs @@ -1,7 +1,6 @@ // Eryn Wells -use crate::piece::{Piece, PlacedPiece, Shape}; -use chessfriend_core::{Rank, Square}; +use chessfriend_core::{Piece, PlacedPiece, Rank, Shape, Square}; use std::fmt; pub use castle::Castle; @@ -17,8 +16,7 @@ pub enum MakeMoveError { } mod castle { - use crate::Color; - use chessfriend_core::Square; + use chessfriend_core::{Color, Square}; #[repr(u16)] #[derive(Copy, Clone, Debug, Eq, PartialEq)] @@ -305,7 +303,8 @@ impl MoveBuilder { mod move_formatter { use super::{Castle, Move}; - use crate::{piece::Shape, Position}; + use crate::Position; + use chessfriend_core::Shape; use std::fmt; enum Style { @@ -410,14 +409,15 @@ mod move_formatter { #[cfg(test)] mod tests { use super::{AlgebraicMoveFormatter, Style}; - use crate::{piece, position}; + use crate::position; + use chessfriend_core::piece; macro_rules! chess_move { ($color:ident $shape:ident $from_square:ident - $to_square:ident) => { $crate::MoveBuilder::new( - $crate::piece::Piece::new( - $crate::piece::Color::$color, - $crate::piece::Shape::$shape, + chessfriend_core::Piece::new( + chessfriend_core::Color::$color, + chessfriend_core::Shape::$shape, ), chessfriend_core::Square::$from_square, chessfriend_core::Square::$to_square, @@ -426,17 +426,17 @@ mod move_formatter { }; ($color:ident $shape:ident $from_square:ident x $to_square:ident, $captured_color:ident $captured_shape:ident) => { $crate::MoveBuilder::new( - $crate::piece::Piece::new( - $crate::piece::Color::$color, - $crate::piece::Shape::$shape, + chessfriend_core::Piece::new( + chessfriend_core::Color::$color, + chessfriend_core::Shape::$shape, ), chessfriend_core::Square::$from_square, chessfriend_core::Square::$to_square, ) - .capturing($crate::piece::PlacedPiece::new( - $crate::piece::Piece::new( - $crate::piece::Color::$captured_color, - $crate::piece::Shape::$captured_shape, + .capturing(chessfriend_core::PlacedPiece::new( + chessfriend_core::Piece::new( + chessfriend_core::Color::$captured_color, + chessfriend_core::Shape::$captured_shape, ), chessfriend_core::Square::$to_square, )) @@ -488,7 +488,7 @@ mod move_formatter { #[cfg(test)] mod tests { use super::*; - use crate::piece; + use chessfriend_core::piece; macro_rules! assert_flag { ($move:expr, $left:expr, $right:expr) => { diff --git a/board/src/move_generator/bishop.rs b/board/src/move_generator/bishop.rs index 404d68b..d740c21 100644 --- a/board/src/move_generator/bishop.rs +++ b/board/src/move_generator/bishop.rs @@ -1,12 +1,9 @@ // Eryn Wells use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use crate::{ - piece::{Color, Piece, PlacedPiece}, - MoveBuilder, Position, -}; +use crate::{MoveBuilder, Position}; use chessfriend_bitboard::BitBoard; -use chessfriend_core::Direction; +use chessfriend_core::{Color, Direction, Piece, PlacedPiece}; move_generator_declaration!(ClassicalMoveGenerator); @@ -61,8 +58,9 @@ impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> { #[cfg(test)] mod tests { use super::*; - use crate::{piece, piece::Color, position, position::DiagramFormatter}; + use crate::{position, position::DiagramFormatter}; use chessfriend_bitboard::BitBoard; + use chessfriend_core::{piece, Color}; #[test] fn classical_single_bishop_bitboard() { diff --git a/board/src/move_generator/king.rs b/board/src/move_generator/king.rs index 643a987..f401de9 100644 --- a/board/src/move_generator/king.rs +++ b/board/src/move_generator/king.rs @@ -4,12 +4,9 @@ //! generating the possible moves for the king in the given position. use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use crate::{ - piece::{Color, Piece, PlacedPiece}, - r#move::Castle, - Move, MoveBuilder, Position, -}; +use crate::{ r#move::Castle, Move, MoveBuilder, Position}; use chessfriend_bitboard::BitBoard; +use chessfriend_core::{Color, Piece, PlacedPiece}; move_generator_declaration!(KingMoveGenerator, struct); move_generator_declaration!(KingMoveGenerator, new); @@ -77,8 +74,9 @@ impl<'pos> MoveGeneratorInternal for KingMoveGenerator<'pos> { #[cfg(test)] mod tests { use super::*; + use crate::position; use chessfriend_bitboard::bitboard; - use chessfriend_core::Square; + use chessfriend_core::{piece, Square}; use std::collections::HashSet; #[test] diff --git a/board/src/move_generator/knight.rs b/board/src/move_generator/knight.rs index 64b360f..f334b64 100644 --- a/board/src/move_generator/knight.rs +++ b/board/src/move_generator/knight.rs @@ -1,11 +1,9 @@ // Eryn Wells use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use crate::{ - piece::{Color, Piece, PlacedPiece}, - MoveBuilder, Position, -}; +use crate::{MoveBuilder, Position}; use chessfriend_bitboard::BitBoard; +use chessfriend_core::{Color, Piece, PlacedPiece}; move_generator_declaration!(KnightMoveGenerator); @@ -41,8 +39,8 @@ impl<'pos> MoveGeneratorInternal for KnightMoveGenerator<'pos> { #[cfg(test)] mod tests { use super::*; - use crate::{piece, position, Move}; - use chessfriend_core::Square; + use crate::{position, Move}; + use chessfriend_core::{piece, Square}; use std::collections::HashSet; #[test] diff --git a/board/src/move_generator/mod.rs b/board/src/move_generator/mod.rs index ed5ac74..c5b275b 100644 --- a/board/src/move_generator/mod.rs +++ b/board/src/move_generator/mod.rs @@ -12,11 +12,8 @@ mod rook; pub use move_generator::Moves; pub(crate) use move_set::MoveSet; -use crate::{ - piece::{Color, Piece, PlacedPiece}, - Move, Position, -}; -use chessfriend_core::Square; +use crate::{ Move, Position}; +use chessfriend_core::{Color, Piece, PlacedPiece, Square}; use std::collections::BTreeMap; trait MoveGenerator { @@ -32,9 +29,10 @@ macro_rules! move_generator_declaration { move_generator_declaration!($name, getters); }; ($name:ident, struct) => { + #[derive(Clone, Debug)] pub(super) struct $name<'pos> { position: &'pos $crate::Position, - color: $crate::piece::Color, + color: chessfriend_core::Color, move_sets: std::collections::BTreeMap< chessfriend_core::Square, $crate::move_generator::MoveSet, @@ -43,7 +41,10 @@ macro_rules! move_generator_declaration { }; ($name:ident, new) => { impl<'pos> $name<'pos> { - pub(super) fn new(position: &$crate::Position, color: $crate::piece::Color) -> $name { + pub(super) fn new( + position: &$crate::Position, + color: chessfriend_core::Color, + ) -> $name { $name { position, color, @@ -58,6 +59,13 @@ macro_rules! move_generator_declaration { self.move_sets.values().map(|set| set.moves()).flatten() } + pub(crate) fn moves_for_piece( + &self, + piece: &chessfriend_core::PlacedPiece, + ) -> Option<&$crate::move_generator::move_set::MoveSet> { + self.move_sets.get(&piece.square()) + } + fn bitboard(&self) -> chessfriend_bitboard::BitBoard { self.move_sets.values().fold( chessfriend_bitboard::BitBoard::empty(), diff --git a/board/src/move_generator/move_generator.rs b/board/src/move_generator/move_generator.rs index 2ef4922..4647b72 100644 --- a/board/src/move_generator/move_generator.rs +++ b/board/src/move_generator/move_generator.rs @@ -2,12 +2,14 @@ use super::{ bishop::ClassicalMoveGenerator as BishopMoveGenerator, king::KingMoveGenerator, - knight::KnightMoveGenerator, pawn::PawnMoveGenerator, + knight::KnightMoveGenerator, move_set::MoveSet, pawn::PawnMoveGenerator, queen::ClassicalMoveGenerator as QueenMoveGenerator, rook::ClassicalMoveGenerator as RookMoveGenerator, }; -use crate::{piece::Color, Move, Position}; +use crate::{Move, Position}; +use chessfriend_core::{Color, PlacedPiece, Shape}; +#[derive(Clone, Debug)] pub struct Moves<'pos> { pawn_moves: PawnMoveGenerator<'pos>, knight_moves: KnightMoveGenerator<'pos>, @@ -29,6 +31,17 @@ impl<'a> Moves<'a> { } } + pub(crate) fn moves_for_piece(&self, piece: &PlacedPiece) -> Option<&MoveSet> { + match piece.shape() { + Shape::Pawn => self.pawn_moves.moves_for_piece(piece), + Shape::Knight => self.knight_moves.moves_for_piece(piece), + Shape::Bishop => self.bishop_moves.moves_for_piece(piece), + Shape::Rook => self.rook_moves.moves_for_piece(piece), + Shape::Queen => self.queen_moves.moves_for_piece(piece), + Shape::King => self.king_moves.moves_for_piece(piece), + } + } + fn iter(&self) -> impl Iterator + '_ { self.pawn_moves .iter() @@ -43,8 +56,8 @@ impl<'a> Moves<'a> { #[cfg(test)] mod tests { - use crate::{piece, position, r#move::AlgebraicMoveFormatter, Move, MoveBuilder}; - use chessfriend_core::Square; + use crate::{position, r#move::AlgebraicMoveFormatter, Move, MoveBuilder}; + use chessfriend_core::{piece, Square}; use std::collections::HashSet; #[test] diff --git a/board/src/move_generator/move_set.rs b/board/src/move_generator/move_set.rs index 1c22eac..bfd9f75 100644 --- a/board/src/move_generator/move_set.rs +++ b/board/src/move_generator/move_set.rs @@ -1,5 +1,8 @@ -use crate::{piece::PlacedPiece, Move}; +// Eryn Wells + +use crate::Move; use chessfriend_bitboard::BitBoard; +use chessfriend_core::PlacedPiece; #[derive(Clone, Debug, Eq, PartialEq)] struct BitBoardSet { diff --git a/board/src/move_generator/pawn.rs b/board/src/move_generator/pawn.rs index d366405..c7899b0 100644 --- a/board/src/move_generator/pawn.rs +++ b/board/src/move_generator/pawn.rs @@ -1,10 +1,9 @@ // Eryn Wells -use crate::{ - piece::{Color, Piece, Shape}, - Move, MoveBuilder, Position, -}; +use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; +use crate::{Move, MoveBuilder, Position}; use chessfriend_bitboard::BitBoard; +use chessfriend_core::{Color, Piece, PlacedPiece, Shape}; enum MoveList { Quiet = 0, diff --git a/board/src/move_generator/queen.rs b/board/src/move_generator/queen.rs index fb96dd7..e2c90ad 100644 --- a/board/src/move_generator/queen.rs +++ b/board/src/move_generator/queen.rs @@ -1,12 +1,9 @@ // Eryn Wells use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use crate::{ - piece::{Color, Piece, PlacedPiece}, - MoveBuilder, Position, -}; +use crate::{MoveBuilder, Position}; use chessfriend_bitboard::BitBoard; -use chessfriend_core::Direction; +use chessfriend_core::{Color, Direction, Piece, PlacedPiece}; move_generator_declaration!(ClassicalMoveGenerator); @@ -67,8 +64,9 @@ impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> { #[cfg(test)] mod tests { use super::*; - use crate::{piece, position, position::DiagramFormatter, Color}; + use crate::{position, position::DiagramFormatter}; use chessfriend_bitboard::{bitboard, BitBoard}; + use chessfriend_core::{piece, Color}; #[test] fn classical_single_queen_bitboard() { diff --git a/board/src/move_generator/rook.rs b/board/src/move_generator/rook.rs index 570255a..8cee244 100644 --- a/board/src/move_generator/rook.rs +++ b/board/src/move_generator/rook.rs @@ -1,12 +1,9 @@ // Eryn Wells use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use crate::{ - piece::{Color, Piece, PlacedPiece}, - MoveBuilder, Position, -}; +use crate::{ MoveBuilder, Position}; use chessfriend_bitboard::BitBoard; -use chessfriend_core::Direction; +use chessfriend_core::{Color, Direction, Piece, PlacedPiece}; move_generator_declaration!(ClassicalMoveGenerator); @@ -63,9 +60,9 @@ impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> { #[cfg(test)] mod tests { use super::*; - use crate::{piece::Piece, position, position::DiagramFormatter, Color, Position}; + use crate::{position, position::DiagramFormatter}; use chessfriend_bitboard::{bitboard, BitBoard}; - use chessfriend_core::Square; + use chessfriend_core::Color; #[test] fn classical_single_rook_bitboard() { diff --git a/board/src/position/builders/move_builder.rs b/board/src/position/builders/move_builder.rs index 4530543..80cae08 100644 --- a/board/src/position/builders/move_builder.rs +++ b/board/src/position/builders/move_builder.rs @@ -1,13 +1,10 @@ // Eryn Wells use crate::{ - piece::{PlacedPiece, Shape}, - position::flags::Flags, - r#move::Castle, - Color, MakeMoveError, Move, Piece, Position, + position::flags::Flags, r#move::Castle, sight::SightExt, MakeMoveError, Move, Position, }; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Direction, Square}; +use chessfriend_core::{Color, Direction, Piece, PlacedPiece, Shape, Square}; /// A position builder that builds a new position by making a move. #[derive(Clone)] @@ -244,7 +241,8 @@ impl<'p> From<&'p Position> for Builder<'p, NoMove> { #[cfg(test)] mod tests { use super::*; - use crate::{r#move::Castle, MoveBuilder, PositionBuilder}; + use crate::{position, r#move::Castle, MoveBuilder, PositionBuilder}; + use chessfriend_core::piece; #[test] fn move_white_pawn_one_square() -> Result<(), MakeMoveError> { diff --git a/board/src/position/builders/position_builder.rs b/board/src/position/builders/position_builder.rs index 88838b7..369014c 100644 --- a/board/src/position/builders/position_builder.rs +++ b/board/src/position/builders/position_builder.rs @@ -1,12 +1,11 @@ // Eryn Wells use crate::{ - piece::{PlacedPiece, Shape}, position::{flags::Flags, piece_sets::PieceBitBoards}, r#move::Castle, - Color, MakeMoveError, Move, Piece, Position, + MakeMoveError, Move, Position, }; -use chessfriend_core::{Rank, Square}; +use chessfriend_core::{piece, Color, Piece, PlacedPiece, Rank, Shape, Square}; use std::collections::BTreeMap; #[derive(Clone)] @@ -130,6 +129,7 @@ impl Default for Builder { #[cfg(test)] mod tests { use crate::PositionBuilder; + use chessfriend_core::piece; #[test] fn place_piece() { diff --git a/board/src/position/diagram_formatter.rs b/board/src/position/diagram_formatter.rs index 0c46de4..80bbbe0 100644 --- a/board/src/position/diagram_formatter.rs +++ b/board/src/position/diagram_formatter.rs @@ -40,7 +40,8 @@ impl<'a> fmt::Display for DiagramFormatter<'a> { #[cfg(test)] mod tests { use super::*; - use crate::Position; + use crate::{position, Position}; + use chessfriend_core::piece; #[test] #[ignore] diff --git a/board/src/position/flags.rs b/board/src/position/flags.rs index e92b9f6..699a63e 100644 --- a/board/src/position/flags.rs +++ b/board/src/position/flags.rs @@ -1,6 +1,7 @@ // Eryn Wells -use crate::{r#move::Castle, Color}; +use crate::r#move::Castle; +use chessfriend_core::Color; use std::fmt; #[derive(Clone, Copy, Eq, Hash, PartialEq)] @@ -40,7 +41,8 @@ impl Default for Flags { #[cfg(test)] mod tests { use super::*; - use crate::{r#move::Castle, Color}; + use crate::r#move::Castle; + use chessfriend_core::Color; #[test] fn castle_flags() { diff --git a/board/src/position/piece_sets.rs b/board/src/position/piece_sets.rs index d07eb2a..9382156 100644 --- a/board/src/position/piece_sets.rs +++ b/board/src/position/piece_sets.rs @@ -1,11 +1,7 @@ // Eryn Wells -use crate::{ - piece::{Piece, PlacedPiece}, - Color, -}; use chessfriend_bitboard::BitBoard; -use chessfriend_core::Square; +use chessfriend_core::{Color, Piece, PlacedPiece, Square}; #[derive(Debug, Eq, PartialEq)] pub enum PlacePieceStrategy { diff --git a/board/src/position/pieces.rs b/board/src/position/pieces.rs index 8530869..655aca2 100644 --- a/board/src/position/pieces.rs +++ b/board/src/position/pieces.rs @@ -1,9 +1,8 @@ // Eryn Wells use super::Position; -use crate::piece::{Color, Piece, PlacedPiece, Shape}; use chessfriend_bitboard::BitBoard; -use chessfriend_core::Square; +use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; pub struct Pieces<'a> { color: Color, @@ -74,9 +73,8 @@ impl<'a> Iterator for Pieces<'a> { #[cfg(test)] mod tests { - use super::*; - use crate::piece::{Color, Piece, Shape}; use crate::{Position, PositionBuilder}; + use chessfriend_core::{piece, Color}; use std::collections::HashSet; #[test] diff --git a/board/src/position/position.rs b/board/src/position/position.rs index 82ef1bd..6d31343 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -2,15 +2,14 @@ use super::{flags::Flags, piece_sets::PieceBitBoards, Pieces}; use crate::{ - move_generator::Moves, - piece::{Color, Piece, PlacedPiece, Shape}, + move_generator::{MoveSet, Moves}, position::DiagramFormatter, r#move::Castle, - sight::Sight, + sight::SightExt, Move, }; use chessfriend_bitboard::BitBoard; -use chessfriend_core::Square; +use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; use std::{cell::OnceCell, fmt}; #[derive(Clone, Debug, Eq, PartialEq)] @@ -268,8 +267,8 @@ impl fmt::Display for Position { #[cfg(test)] mod tests { - use crate::{position, Castle, Color, Position}; - use chessfriend_core::Square; + use crate::{position, test_position, Castle, Position}; + use chessfriend_core::{piece, Color, Square}; #[test] fn piece_on_square() { diff --git a/board/src/sight.rs b/board/src/sight.rs index ffed3f7..0e1dc8b 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -1,17 +1,22 @@ // Eryn Wells -use crate::{ - piece::{Color, PlacedPiece, Shape}, - Position, -}; +use crate::Position; use chessfriend_bitboard::BitBoard; -use chessfriend_core::Direction; +use chessfriend_core::{Color, Direction, PlacedPiece, Shape}; -pub(crate) trait Sight { +pub(crate) trait SightExt { fn sight_in_position(&self, position: &Position) -> BitBoard; + + fn white_pawn_sight_in_position(&self, position: &Position) -> BitBoard; + fn black_pawn_sight_in_position(&self, position: &Position) -> BitBoard; + fn knight_sight_in_position(&self, position: &Position) -> BitBoard; + fn bishop_sight_in_position(&self, position: &Position) -> BitBoard; + fn rook_sight_in_position(&self, position: &Position) -> BitBoard; + fn queen_sight_in_position(&self, position: &Position) -> BitBoard; + fn king_sight_in_position(&self, position: &Position) -> BitBoard; } -impl Sight for PlacedPiece { +impl SightExt for PlacedPiece { fn sight_in_position(&self, position: &Position) -> BitBoard { match self.shape() { Shape::Pawn => match self.color() { @@ -25,9 +30,7 @@ impl Sight for PlacedPiece { Shape::King => self.king_sight_in_position(position), } } -} -impl PlacedPiece { fn white_pawn_sight_in_position(&self, position: &Position) -> BitBoard { let pawn: BitBoard = self.square().into(); let pawn = pawn.shift_north_west_one() | pawn.shift_north_east_one(); @@ -175,9 +178,9 @@ mod tests { } mod pawn { - use crate::sight::Sight; + use crate::{sight::SightExt, test_position}; use chessfriend_bitboard::{bitboard, BitBoard}; - use chessfriend_core::Square; + use chessfriend_core::{piece, Square}; sight_test!(e4_pawn, piece!(White Pawn on E4), bitboard!(D5, F5)); @@ -235,8 +238,9 @@ mod tests { #[macro_use] mod knight { - use crate::sight::Sight; + use crate::sight::SightExt; use chessfriend_bitboard::bitboard; + use chessfriend_core::piece; sight_test!( f6_knight, @@ -246,8 +250,9 @@ mod tests { } mod bishop { - use crate::sight::Sight; + use crate::sight::SightExt; use chessfriend_bitboard::bitboard; + use chessfriend_core::piece; sight_test!( c2_bishop, @@ -257,8 +262,9 @@ mod tests { } mod rook { - use crate::sight::Sight; + use crate::sight::SightExt; use chessfriend_bitboard::bitboard; + use chessfriend_core::piece; sight_test!( g3_rook, diff --git a/core/src/colors.rs b/core/src/colors.rs new file mode 100644 index 0000000..dd0bf12 --- /dev/null +++ b/core/src/colors.rs @@ -0,0 +1,62 @@ +// Eryn Wells + +use crate::try_from_string; +use std::fmt; + +#[derive(Debug, Eq, PartialEq)] +pub enum TryFromError { + InvalidCharacter, + ZeroLengthString, +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum Color { + White = 0, + Black = 1, +} + +impl Color { + pub fn iter() -> impl Iterator { + [Color::White, Color::Black].into_iter() + } + + pub fn other(&self) -> Color { + match self { + Color::White => Color::Black, + Color::Black => Color::White, + } + } +} + +impl Default for Color { + fn default() -> Self { + Color::White + } +} + +impl fmt::Display for Color { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + Color::White => "White", + Color::Black => "Black", + }, + ) + } +} + +impl TryFrom for Color { + type Error = TryFromError; + + fn try_from(value: char) -> Result { + match value { + 'w' | 'W' => Ok(Color::White), + 'b' | 'B' => Ok(Color::Black), + _ => Err(TryFromError::InvalidCharacter), + } + } +} + +try_from_string!(Color); diff --git a/core/src/errors.rs b/core/src/errors.rs new file mode 100644 index 0000000..c897997 --- /dev/null +++ b/core/src/errors.rs @@ -0,0 +1,7 @@ +// Eryn Wells + +#[derive(Debug, Eq, PartialEq)] +pub struct TryFromCharError; + +#[derive(Debug, Eq, PartialEq)] +pub struct TryFromStrError; diff --git a/core/src/lib.rs b/core/src/lib.rs index 2fe191d..fc3dfbc 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,3 +1,12 @@ -mod coordinates; +// Eryn Wells +pub mod colors; +pub mod coordinates; +pub mod errors; +pub mod pieces; + +mod macros; + +pub use colors::Color; pub use coordinates::{Direction, File, Rank, Square}; +pub use pieces::{Piece, PlacedPiece, Shape}; diff --git a/core/src/macros.rs b/core/src/macros.rs new file mode 100644 index 0000000..9cce129 --- /dev/null +++ b/core/src/macros.rs @@ -0,0 +1,32 @@ +// Eryn Wells + +#[macro_export] +macro_rules! try_from_string { + ($type:ty) => { + try_from_string!($type, &str); + try_from_string!($type, &String); + }; + ($type:ty, $from_type:ty) => { + impl TryFrom<$from_type> for $type { + type Error = $crate::errors::TryFromStrError; + + fn try_from(value: $from_type) -> Result { + let first_char = value + .chars() + .nth(0) + .ok_or($crate::errors::TryFromStrError)?; + Self::try_from(first_char).map_err(|_| $crate::errors::TryFromStrError) + } + } + }; +} + +#[macro_export] +macro_rules! piece { + ($color:ident $shape:ident) => { + $crate::Piece::new($crate::Color::$color, $crate::Shape::$shape) + }; + ($color:ident $shape:ident on $square:ident) => { + $crate::PlacedPiece::new(piece!($color $shape), $crate::Square::$square) + } +} diff --git a/board/src/piece.rs b/core/src/pieces.rs similarity index 69% rename from board/src/piece.rs rename to core/src/pieces.rs index b80d1da..782fc47 100644 --- a/board/src/piece.rs +++ b/core/src/pieces.rs @@ -1,84 +1,7 @@ // Eryn Wells -use crate::display::{ASCIIDisplay, FENDisplay, UnicodeDisplay}; -use chessfriend_core::Square; -use std::fmt; -use std::slice::Iter; - -#[derive(Debug, Eq, PartialEq)] -pub enum TryFromError { - InvalidCharacter, - ZeroLengthString, -} - -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum Color { - White = 0, - Black = 1, -} - -impl Color { - pub fn iter() -> impl Iterator { - [Color::White, Color::Black].into_iter() - } - - pub fn other(&self) -> Color { - match self { - Color::White => Color::Black, - Color::Black => Color::White, - } - } -} - -impl Default for Color { - fn default() -> Self { - Color::White - } -} - -impl fmt::Display for Color { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match self { - Color::White => "White", - Color::Black => "Black", - }, - ) - } -} - -impl TryFrom for Color { - type Error = TryFromError; - - fn try_from(value: char) -> Result { - match value { - 'w' | 'W' => Ok(Color::White), - 'b' | 'B' => Ok(Color::Black), - _ => Err(TryFromError::InvalidCharacter), - } - } -} - -macro_rules! try_from_string { - ($type:ty) => { - try_from_string!($type, &str); - try_from_string!($type, &String); - }; - ($type:ty, $from_type:ty) => { - impl TryFrom<$from_type> for $type { - type Error = TryFromError; - - fn try_from(value: $from_type) -> Result { - let first_char = value.chars().nth(0).ok_or(TryFromError::ZeroLengthString)?; - Self::try_from(first_char) - } - } - }; -} - -try_from_string!(Color); +use crate::{errors::TryFromCharError, try_from_string, Color, Square}; +use std::{fmt, slice::Iter}; #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum Shape { @@ -124,7 +47,7 @@ impl Shape { } impl TryFrom for Shape { - type Error = TryFromError; + type Error = TryFromCharError; fn try_from(value: char) -> Result { match value { @@ -134,7 +57,7 @@ impl TryFrom for Shape { 'R' | 'r' => Ok(Shape::Rook), 'Q' | 'q' => Ok(Shape::Queen), 'K' | 'k' => Ok(Shape::King), - _ => Err(TryFromError::InvalidCharacter), + _ => Err(TryFromCharError), } } } @@ -219,18 +142,6 @@ impl Piece { impl fmt::Display for Piece { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - UnicodeDisplay::fmt(self, f) - } -} - -impl ASCIIDisplay for Piece { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", Into::::into(self.shape())) - } -} - -impl UnicodeDisplay for Piece { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, "{}", @@ -252,20 +163,6 @@ impl UnicodeDisplay for Piece { } } -impl FENDisplay for Piece { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let ascii = self.shape().to_ascii(); - write!( - f, - "{}", - match self.color { - Color::White => ascii.to_ascii_uppercase(), - Color::Black => ascii.to_ascii_lowercase(), - } - ) - } -} - #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct PlacedPiece { piece: Piece, From 1b5319c702b93f5614892caaf45bdd48b64322c5 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 24 Jan 2024 17:09:15 -0800 Subject: [PATCH 081/423] [board] Pass a Color to Position::king_square Now it can return either color's king. --- board/src/position/position.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/board/src/position/position.rs b/board/src/position/position.rs index 82ef1bd..b72becb 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -189,12 +189,12 @@ impl Position { pub(crate) fn is_king_in_check(&self) -> bool { let sight_of_opposing_player = self.sight_of_player(self.color_to_move.other()); - sight_of_opposing_player.is_set(self.king_square()) + sight_of_opposing_player.is_set(self.king_square(self.color_to_move)) } - fn king_square(&self) -> Square { + pub(crate) fn king_square(&self, player: Color) -> Square { self.pieces - .bitboard_for_piece(&Piece::king(self.color_to_move)) + .bitboard_for_piece(&Piece::king(player)) .occupied_squares() .next() .unwrap() From 88c0638d83ae1da6c640943ac7614d7d3d2ae5bc Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 24 Jan 2024 17:10:10 -0800 Subject: [PATCH 082/423] [board] Check that move is in sight of piece If it's not, return an error from MoveBuilder::make() --- board/src/position/builders/move_builder.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/board/src/position/builders/move_builder.rs b/board/src/position/builders/move_builder.rs index 4530543..ae1c0fd 100644 --- a/board/src/position/builders/move_builder.rs +++ b/board/src/position/builders/move_builder.rs @@ -62,9 +62,14 @@ where .position .piece_on_square(from_square) .ok_or(MakeMoveError::NoPiece)?; - println!("{}", &piece); let to_square = mv.to_square(); + + let sight = piece.sight_in_position(self.position); + if !sight.is_set(to_square) { + return Err(MakeMoveError::IllegalSquare(to_square)); + } + let player = self.position.player_to_move(); let captured_piece: Option = if mv.is_en_passant() { From 3e1675f87e1fe40fa47125fba442e94c2bdf7ee7 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 24 Jan 2024 17:10:44 -0800 Subject: [PATCH 083/423] Specify resolver=2 in the Cargo workspace --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index cfa9c81..64d2a06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [workspace] - members = [ "board", "bitboard", "core", "explorer", ] +resolver = "2" From a73355c7694298642c271966e6017ebeaa364143 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 24 Jan 2024 17:16:33 -0800 Subject: [PATCH 084/423] [explorer] Track result of command with a CommandResult type, and overall state with a State type --- explorer/src/main.rs | 51 +++++++++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/explorer/src/main.rs b/explorer/src/main.rs index e8fca2f..fedd11c 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -5,10 +5,23 @@ use clap::{Arg, Command}; use rustyline::error::ReadlineError; use rustyline::DefaultEditor; -#[derive(Eq, PartialEq)] -enum ShouldContinue { - Yes, - No, +struct CommandResult { + should_continue: bool, + should_print_position: bool, +} + +impl Default for CommandResult { + fn default() -> Self { + CommandResult { + should_continue: true, + should_print_position: true, + } + } +} + +struct State { + builder: PositionBuilder, + position: Position, } fn command_line() -> Command { @@ -41,16 +54,19 @@ fn command_line() -> Command { .subcommand(Command::new("quit").about("Quit the program")) } -fn respond(line: &str, pos: &mut Position) -> Result { +fn respond(line: &str, state: &mut State) -> Result { let args = shlex::split(line).ok_or("error: Invalid quoting")?; let matches = command_line() .try_get_matches_from(args) .map_err(|e| e.to_string())?; + let mut result = CommandResult::default(); + match matches.subcommand() { Some(("print", _matches)) => {} Some(("quit", _matches)) => { - return Ok(ShouldContinue::No); + result.should_continue = false; + result.should_print_position = false; } Some(("place", _matches)) => { let piece_arg = _matches.get_one::("piece").unwrap(); @@ -71,16 +87,26 @@ fn respond(line: &str, pos: &mut Position) -> Result { None => unreachable!("Subcommand required"), } - Ok(ShouldContinue::Yes) + Ok(result) } fn main() -> Result<(), String> { let mut editor = DefaultEditor::new().map_err(|err| format!("Error: {}", err.to_string()))?; - let mut pos = Position::empty(); + let starting_position = Position::starting(); + let builder = PositionBuilder::from_position(&starting_position); + let mut state = State { + builder, + position: starting_position, + }; + + let mut should_print_position = true; loop { - println!("{}", &pos); + if should_print_position { + println!("{}", &state.position); + println!("{} to move.", state.position.player_to_move()); + } let readline = editor.readline("? "); match readline { @@ -90,9 +116,10 @@ fn main() -> Result<(), String> { continue; } - match respond(line, &mut pos) { - Ok(should_continue) => { - if should_continue == ShouldContinue::No { + match respond(line, &mut state) { + Ok(result) => { + should_print_position = result.should_print_position; + if !result.should_continue { break; } } From 54c94a93aab1e6f2b8d316548299c3571881a726 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 26 Jan 2024 08:38:01 -0800 Subject: [PATCH 085/423] [board] Rewrite the pawn move generator I wrote it before I made the MoveGeneratorInternal trait. Found a few bugs in there too. --- board/src/move_generator/pawn.rs | 261 ++++++++----------------------- 1 file changed, 62 insertions(+), 199 deletions(-) diff --git a/board/src/move_generator/pawn.rs b/board/src/move_generator/pawn.rs index c7899b0..61b2246 100644 --- a/board/src/move_generator/pawn.rs +++ b/board/src/move_generator/pawn.rs @@ -3,7 +3,7 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::{Move, MoveBuilder, Position}; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece, PlacedPiece, Shape}; +use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; enum MoveList { Quiet = 0, @@ -22,43 +22,41 @@ struct MoveGenerationParameters { right_capture_shift: fn(&BitBoard) -> BitBoard, } -pub(super) struct PawnMoveGenerator<'pos> { - color: Color, - position: &'pos Position, +move_generator_declaration!(PawnMoveGenerator); - did_populate_move_lists: bool, +impl<'pos> MoveGeneratorInternal for PawnMoveGenerator<'pos> { + fn piece(color: Color) -> Piece { + Piece::pawn(color) + } - pushes: BitBoard, - attacks: BitBoard, + fn move_set_for_piece(position: &Position, placed_piece: PlacedPiece) -> MoveSet { + let from_square = placed_piece.square(); + let parameters = Self::move_generation_parameters(placed_piece.color()); - move_lists: [Vec; 3], - move_iterator: MoveIterator, + let opposing_pieces = position.opposing_pieces(); + + let captures_bitboard = Self::attacks(position, placed_piece, ¶meters); + let quiet_moves_bitboard = Self::pushes(position, placed_piece, ¶meters); + + let quiet_moves = quiet_moves_bitboard.occupied_squares().map(|to_square| { + MoveBuilder::new(*placed_piece.piece(), from_square, to_square).build() + }); + let capture_moves = captures_bitboard.occupied_squares().map(|to_square| { + let captured_piece = position.piece_on_square(to_square).unwrap(); + MoveBuilder::new(*placed_piece.piece(), from_square, to_square) + .capturing(captured_piece) + .build() + }); + + MoveSet::new(placed_piece) + .quiet_moves(quiet_moves_bitboard, quiet_moves) + .capture_moves(captures_bitboard, capture_moves) + } } impl<'pos> PawnMoveGenerator<'pos> { - pub(super) fn new(position: &Position, color: Color) -> PawnMoveGenerator { - PawnMoveGenerator { - position, - color, - did_populate_move_lists: false, - pushes: BitBoard::empty(), - attacks: BitBoard::empty(), - move_lists: [Vec::new(), Vec::new(), Vec::new()], - move_iterator: MoveIterator(0, 0), - } - } - - pub(super) fn iter(&self) -> impl Iterator + '_ { - self.move_lists.iter().flatten() - } - - fn generate_moves(&mut self) { - self.generate_move_bitboards(); - self.populate_move_lists(); - } - - fn move_generation_parameters(&self) -> MoveGenerationParameters { - match self.color { + fn move_generation_parameters(color: Color) -> MoveGenerationParameters { + match color { Color::White => MoveGenerationParameters { starting_rank: BitBoard::rank(1), promotion_rank: BitBoard::rank(7), @@ -76,178 +74,43 @@ impl<'pos> PawnMoveGenerator<'pos> { } } - #[inline] - fn quiet_move_list(&mut self) -> &mut Vec { - &mut self.move_lists[MoveList::Quiet as usize] + fn pushes( + position: &Position, + piece: PlacedPiece, + parameters: &MoveGenerationParameters, + ) -> BitBoard { + let empty_squares = position.empty_squares(); + let from_square: BitBoard = piece.square().into(); + + (parameters.push_shift)(&from_square) & empty_squares } - #[inline] - fn promotion_move_list(&mut self) -> &mut Vec { - &mut self.move_lists[MoveList::Promotions as usize] - } + fn attacks( + position: &Position, + piece: PlacedPiece, + parameters: &MoveGenerationParameters, + ) -> BitBoard { + let color = piece.color(); - #[inline] - fn capture_move_list(&mut self) -> &mut Vec { - &mut self.move_lists[MoveList::Captures as usize] - } -} + let opponent_pieces = position.bitboard_for_color(color.other()); + let en_passant_square = position + .en_passant_square() + .map(|square| >::into(square)) + .unwrap_or(BitBoard::empty()); -impl<'pos> PawnMoveGenerator<'pos> { - fn generate_move_bitboards(&mut self) { - let parameters = self.move_generation_parameters(); - self.generate_pushes_bitboard(¶meters); - self.generate_attacks_bitboard(¶meters); - } + let from_square: BitBoard = piece.square().into(); - fn generate_pushes_bitboard(&mut self, parameters: &MoveGenerationParameters) { - let empty_squares = self.position.empty_squares(); - let bb = self.position.bitboard_for_piece(Piece::pawn(self.color)); - - let legal_1square_pushes = (parameters.push_shift)(bb) & empty_squares; - let legal_2square_pushes = - (parameters.push_shift)(&(legal_1square_pushes & BitBoard::rank(2))) & empty_squares; - - self.pushes = legal_1square_pushes | legal_2square_pushes; - } - - fn generate_attacks_bitboard(&mut self, parameters: &MoveGenerationParameters) { - let opponent_pieces = self.position.bitboard_for_color(self.color.other()); - let bb = self.position.bitboard_for_piece(Piece::pawn(Color::White)); - - self.attacks = ((parameters.left_capture_shift)(bb) | (parameters.right_capture_shift)(bb)) - & opponent_pieces; - - #[allow(unused_variables)] - if let Some(en_passant) = self.en_passant() { - // TODO: Add en passant move to the attacks bitboard. - } - } -} - -impl<'pos> PawnMoveGenerator<'pos> { - fn populate_move_lists(&mut self) { - let parameters = self.move_generation_parameters(); - - self._populate_move_lists(¶meters); - self.did_populate_move_lists = true; - } - - fn _populate_move_lists(&mut self, parameters: &MoveGenerationParameters) { - let piece = Piece::pawn(self.color); - - let bb = self.position.bitboard_for_piece(piece); - if bb.is_empty() { - return; - } - - let empty_squares = self.position.empty_squares(); - let black_pieces = self.position.bitboard_for_color(self.color.other()); - - for from_sq in bb.occupied_squares() { - let pawn: BitBoard = from_sq.into(); - - let push = (parameters.push_shift)(&pawn); - if !(push & empty_squares).is_empty() { - let to_sq = push.occupied_squares().next().unwrap(); - - let builder = MoveBuilder::new(piece, from_sq, to_sq); - if !(push & parameters.promotion_rank).is_empty() { - for shape in Shape::promotable() { - self.promotion_move_list() - .push(builder.clone().promoting_to(*shape).build()); - } - } else { - self.quiet_move_list().push(builder.build()); - } - - if !(pawn & parameters.starting_rank).is_empty() { - let push = (parameters.push_shift)(&push); - if !(push & empty_squares).is_empty() { - let to_sq = push.occupied_squares().next().unwrap(); - self.quiet_move_list() - .push(MoveBuilder::new(piece, from_sq, to_sq).build()); - } - } - } - - for attack in [ - (parameters.left_capture_shift)(&pawn), - (parameters.right_capture_shift)(&pawn), - ] { - if !(attack & black_pieces).is_empty() { - let to_sq = attack.occupied_squares().next().unwrap(); - let captured_piece = self.position.piece_on_square(to_sq).unwrap(); - - let builder = MoveBuilder::new(piece, from_sq, to_sq).capturing(captured_piece); - if !(attack & parameters.promotion_rank).is_empty() { - for shape in Shape::promotable() { - self.capture_move_list() - .push(builder.clone().promoting_to(*shape).build()); - } - } else { - self.capture_move_list().push(builder.build()); - } - } - } - - if let Some(en_passant) = self.en_passant() { - self.capture_move_list().push(en_passant); - } - } - } - - fn en_passant(&self) -> Option { - // TODO: En passant. I think the way to do this is to have the position mark - // an en passant square when the conditions are correct, i.e. when the - // opposing player has pushed a pawn two squares off the initial rank, and - // then check in these routines if a pawn of this color attacks that square. - - None - } -} - -impl<'pos> Iterator for PawnMoveGenerator<'pos> { - type Item = Move; - - fn next(&mut self) -> Option { - if !self.did_populate_move_lists { - self.generate_moves(); - } - - let iter = &mut self.move_iterator; - - // Find the next non-empty list. - loop { - if iter.0 >= self.move_lists.len() || !self.move_lists[iter.0].is_empty() { - break; - } - - iter.0 += 1; - } - - if let Some(move_list) = self.move_lists.get(iter.0) { - let next_move = move_list[iter.1].clone(); - - iter.1 += 1; - if iter.1 >= move_list.len() { - // Increment the list index here. On the next iteration, find the next non-empty list. - iter.0 += 1; - iter.1 = 0; - } - - Some(next_move) - } else { - None - } + ((parameters.left_capture_shift)(&from_square) + | (parameters.right_capture_shift)(&from_square)) + & (opponent_pieces | en_passant_square) } } #[cfg(test)] mod tests { use super::*; - use crate::position::DiagramFormatter; - use crate::{piece, Position}; - use chessfriend_core::Square; + use crate::{position, position::DiagramFormatter}; + use chessfriend_core::{piece, Square}; use std::collections::HashSet; #[test] @@ -261,7 +124,7 @@ mod tests { MoveBuilder::new(piece!(White Pawn), Square::E2, Square::E4).build(), ]); - let generated_moves: HashSet = generator.collect(); + let generated_moves: HashSet = generator.iter().cloned().collect(); assert_eq!(generated_moves, expected_moves); } @@ -279,7 +142,7 @@ mod tests { ) .build()]); - let generated_moves: HashSet = generator.collect(); + let generated_moves: HashSet = generator.iter().cloned().collect(); assert_eq!(generated_moves, expected_moves); } @@ -302,7 +165,7 @@ mod tests { ) .build()]); - let generated_moves: HashSet = generator.collect(); + let generated_moves: HashSet = generator.iter().cloned().collect(); assert_eq!(generated_moves, expected_moves); } @@ -317,7 +180,7 @@ mod tests { let generator = PawnMoveGenerator::new(&pos, Color::White); - let generated_moves: HashSet = generator.collect(); + let generated_moves: HashSet = generator.iter().cloned().collect(); assert_eq!(generated_moves, HashSet::new()); } @@ -340,7 +203,7 @@ mod tests { .build()], ); - let generated_moves: HashSet = generator.collect(); + let generated_moves: HashSet = generator.iter().cloned().collect(); assert_eq!(generated_moves, expected_moves); } @@ -366,7 +229,7 @@ mod tests { .build(), ]); - let generated_moves: HashSet = generator.collect(); + let generated_moves: HashSet = generator.iter().cloned().collect(); assert_eq!( generated_moves, expected_moves, From f08a4c66a1d8474be4b50b490258943d0048d5bb Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 26 Jan 2024 12:58:51 -0800 Subject: [PATCH 086/423] [core] Use TryFromCharError in TryFrom for Color --- core/src/colors.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/core/src/colors.rs b/core/src/colors.rs index dd0bf12..4e94fed 100644 --- a/core/src/colors.rs +++ b/core/src/colors.rs @@ -1,14 +1,8 @@ // Eryn Wells -use crate::try_from_string; +use crate::{errors::TryFromCharError, try_from_string}; use std::fmt; -#[derive(Debug, Eq, PartialEq)] -pub enum TryFromError { - InvalidCharacter, - ZeroLengthString, -} - #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum Color { White = 0, @@ -48,13 +42,13 @@ impl fmt::Display for Color { } impl TryFrom for Color { - type Error = TryFromError; + type Error = TryFromCharError; fn try_from(value: char) -> Result { match value { 'w' | 'W' => Ok(Color::White), 'b' | 'B' => Ok(Color::Black), - _ => Err(TryFromError::InvalidCharacter), + _ => Err(TryFromCharError), } } } From 6292421b1cdfa665f4a9d5e16b5e9d71d3e231ee Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 26 Jan 2024 12:59:05 -0800 Subject: [PATCH 087/423] [board] Clean up imports in fen.rs --- board/src/fen.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/board/src/fen.rs b/board/src/fen.rs index 8610f00..26cdd0c 100644 --- a/board/src/fen.rs +++ b/board/src/fen.rs @@ -1,11 +1,7 @@ // Eryn Wells -use crate::{ - piece::{Piece, PlacedPiece}, - r#move::Castle, - Color, Position, -}; -use chessfriend_core::{File, Rank, Square}; +use crate::{r#move::Castle, Position}; +use chessfriend_core::{Color, File, Piece, PlacedPiece, Rank, Square}; use std::fmt::Write; #[derive(Clone, Copy, Debug, Eq, PartialEq)] From d3ab47779582c69ea904eabd699dcb95e26e637c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 26 Jan 2024 12:59:23 -0800 Subject: [PATCH 088/423] [board] Remove a stray space in move_generator/mod.rs --- board/src/move_generator/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/board/src/move_generator/mod.rs b/board/src/move_generator/mod.rs index c5b275b..730d8b2 100644 --- a/board/src/move_generator/mod.rs +++ b/board/src/move_generator/mod.rs @@ -12,7 +12,7 @@ mod rook; pub use move_generator::Moves; pub(crate) use move_set::MoveSet; -use crate::{ Move, Position}; +use crate::{Move, Position}; use chessfriend_core::{Color, Piece, PlacedPiece, Square}; use std::collections::BTreeMap; From bea6dd67c81592545918bac62dcea44a3813c1a5 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 27 Jan 2024 13:02:43 -0800 Subject: [PATCH 089/423] [board] Remove the position reference from move generator structs Simplifies the lifetime calculations, and makes it possible to cache a Moves object on Position. --- board/src/move_generator/bishop.rs | 4 ++-- board/src/move_generator/king.rs | 6 +++--- board/src/move_generator/knight.rs | 2 +- board/src/move_generator/mod.rs | 10 ++++------ board/src/move_generator/move_generator.rs | 18 +++++++++--------- board/src/move_generator/pawn.rs | 4 ++-- board/src/move_generator/queen.rs | 4 ++-- board/src/move_generator/rook.rs | 4 ++-- board/src/position/position.rs | 1 - 9 files changed, 25 insertions(+), 28 deletions(-) diff --git a/board/src/move_generator/bishop.rs b/board/src/move_generator/bishop.rs index d740c21..48fd833 100644 --- a/board/src/move_generator/bishop.rs +++ b/board/src/move_generator/bishop.rs @@ -7,7 +7,7 @@ use chessfriend_core::{Color, Direction, Piece, PlacedPiece}; move_generator_declaration!(ClassicalMoveGenerator); -impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> { +impl MoveGeneratorInternal for ClassicalMoveGenerator { fn piece(color: Color) -> Piece { Piece::bishop(color) } @@ -60,7 +60,7 @@ mod tests { use super::*; use crate::{position, position::DiagramFormatter}; use chessfriend_bitboard::BitBoard; - use chessfriend_core::{piece, Color}; + use chessfriend_core::Color; #[test] fn classical_single_bishop_bitboard() { diff --git a/board/src/move_generator/king.rs b/board/src/move_generator/king.rs index f401de9..1d0dda2 100644 --- a/board/src/move_generator/king.rs +++ b/board/src/move_generator/king.rs @@ -4,7 +4,7 @@ //! generating the possible moves for the king in the given position. use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use crate::{ r#move::Castle, Move, MoveBuilder, Position}; +use crate::{r#move::Castle, Move, MoveBuilder, Position}; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece, PlacedPiece}; @@ -12,7 +12,7 @@ move_generator_declaration!(KingMoveGenerator, struct); move_generator_declaration!(KingMoveGenerator, new); move_generator_declaration!(KingMoveGenerator, getters); -impl<'a> KingMoveGenerator<'a> { +impl KingMoveGenerator { #[allow(unused_variables)] fn king_side_castle(position: &Position, color: Color) -> Option { if !position.player_has_right_to_castle(color, Castle::KingSide) { @@ -34,7 +34,7 @@ impl<'a> KingMoveGenerator<'a> { } } -impl<'pos> MoveGeneratorInternal for KingMoveGenerator<'pos> { +impl MoveGeneratorInternal for KingMoveGenerator { fn piece(color: Color) -> Piece { Piece::king(color) } diff --git a/board/src/move_generator/knight.rs b/board/src/move_generator/knight.rs index f334b64..751217a 100644 --- a/board/src/move_generator/knight.rs +++ b/board/src/move_generator/knight.rs @@ -7,7 +7,7 @@ use chessfriend_core::{Color, Piece, PlacedPiece}; move_generator_declaration!(KnightMoveGenerator); -impl<'pos> MoveGeneratorInternal for KnightMoveGenerator<'pos> { +impl MoveGeneratorInternal for KnightMoveGenerator { fn piece(color: Color) -> Piece { Piece::knight(color) } diff --git a/board/src/move_generator/mod.rs b/board/src/move_generator/mod.rs index 730d8b2..b15d2f9 100644 --- a/board/src/move_generator/mod.rs +++ b/board/src/move_generator/mod.rs @@ -29,9 +29,8 @@ macro_rules! move_generator_declaration { move_generator_declaration!($name, getters); }; ($name:ident, struct) => { - #[derive(Clone, Debug)] - pub(super) struct $name<'pos> { - position: &'pos $crate::Position, + #[derive(Clone, Debug, Eq, PartialEq)] + pub(super) struct $name { color: chessfriend_core::Color, move_sets: std::collections::BTreeMap< chessfriend_core::Square, @@ -40,13 +39,12 @@ macro_rules! move_generator_declaration { } }; ($name:ident, new) => { - impl<'pos> $name<'pos> { + impl $name { pub(super) fn new( position: &$crate::Position, color: chessfriend_core::Color, ) -> $name { $name { - position, color, move_sets: Self::move_sets(position, color), } @@ -54,7 +52,7 @@ macro_rules! move_generator_declaration { } }; ($name:ident, getters) => { - impl<'pos> $name<'pos> { + impl $name { pub(super) fn iter(&self) -> impl Iterator + '_ { self.move_sets.values().map(|set| set.moves()).flatten() } diff --git a/board/src/move_generator/move_generator.rs b/board/src/move_generator/move_generator.rs index 4647b72..35292a1 100644 --- a/board/src/move_generator/move_generator.rs +++ b/board/src/move_generator/move_generator.rs @@ -9,17 +9,17 @@ use super::{ use crate::{Move, Position}; use chessfriend_core::{Color, PlacedPiece, Shape}; -#[derive(Clone, Debug)] -pub struct Moves<'pos> { - pawn_moves: PawnMoveGenerator<'pos>, - knight_moves: KnightMoveGenerator<'pos>, - bishop_moves: BishopMoveGenerator<'pos>, - rook_moves: RookMoveGenerator<'pos>, - queen_moves: QueenMoveGenerator<'pos>, - king_moves: KingMoveGenerator<'pos>, +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Moves { + pawn_moves: PawnMoveGenerator, + knight_moves: KnightMoveGenerator, + bishop_moves: BishopMoveGenerator, + rook_moves: RookMoveGenerator, + queen_moves: QueenMoveGenerator, + king_moves: KingMoveGenerator, } -impl<'a> Moves<'a> { +impl Moves { pub fn new(position: &Position, color: Color) -> Moves { Moves { pawn_moves: PawnMoveGenerator::new(position, color), diff --git a/board/src/move_generator/pawn.rs b/board/src/move_generator/pawn.rs index 61b2246..860c564 100644 --- a/board/src/move_generator/pawn.rs +++ b/board/src/move_generator/pawn.rs @@ -24,7 +24,7 @@ struct MoveGenerationParameters { move_generator_declaration!(PawnMoveGenerator); -impl<'pos> MoveGeneratorInternal for PawnMoveGenerator<'pos> { +impl MoveGeneratorInternal for PawnMoveGenerator { fn piece(color: Color) -> Piece { Piece::pawn(color) } @@ -54,7 +54,7 @@ impl<'pos> MoveGeneratorInternal for PawnMoveGenerator<'pos> { } } -impl<'pos> PawnMoveGenerator<'pos> { +impl PawnMoveGenerator { fn move_generation_parameters(color: Color) -> MoveGenerationParameters { match color { Color::White => MoveGenerationParameters { diff --git a/board/src/move_generator/queen.rs b/board/src/move_generator/queen.rs index e2c90ad..f38da4a 100644 --- a/board/src/move_generator/queen.rs +++ b/board/src/move_generator/queen.rs @@ -7,7 +7,7 @@ use chessfriend_core::{Color, Direction, Piece, PlacedPiece}; move_generator_declaration!(ClassicalMoveGenerator); -impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> { +impl MoveGeneratorInternal for ClassicalMoveGenerator { fn piece(color: Color) -> Piece { Piece::queen(color) } @@ -66,7 +66,7 @@ mod tests { use super::*; use crate::{position, position::DiagramFormatter}; use chessfriend_bitboard::{bitboard, BitBoard}; - use chessfriend_core::{piece, Color}; + use chessfriend_core::Color; #[test] fn classical_single_queen_bitboard() { diff --git a/board/src/move_generator/rook.rs b/board/src/move_generator/rook.rs index 8cee244..dae6daa 100644 --- a/board/src/move_generator/rook.rs +++ b/board/src/move_generator/rook.rs @@ -1,13 +1,13 @@ // Eryn Wells use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use crate::{ MoveBuilder, Position}; +use crate::{MoveBuilder, Position}; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Direction, Piece, PlacedPiece}; move_generator_declaration!(ClassicalMoveGenerator); -impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> { +impl MoveGeneratorInternal for ClassicalMoveGenerator { fn piece(color: Color) -> Piece { Piece::rook(color) } diff --git a/board/src/position/position.rs b/board/src/position/position.rs index 4cb6c6e..326371f 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -20,7 +20,6 @@ pub struct Position { en_passant_square: Option, sight: [OnceCell; 2], moves: OnceCell, - half_move_counter: u16, full_move_number: u16, } From e8d7f15a7f99e2a241c675a02ee430fc78a0cb1d Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 27 Jan 2024 13:03:18 -0800 Subject: [PATCH 090/423] [board] Simplify the piece placement strategy logic Check whether the strategy is PreserveExisting before checking if the piece is already placed. --- board/src/position/piece_sets.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/board/src/position/piece_sets.rs b/board/src/position/piece_sets.rs index 9382156..0ebbd90 100644 --- a/board/src/position/piece_sets.rs +++ b/board/src/position/piece_sets.rs @@ -87,13 +87,10 @@ impl PieceBitBoards { let color = piece.color(); let square = piece.square(); - if self.by_color.bitboard(color).is_set(piece.square()) { - match strategy { - PlacePieceStrategy::Replace => todo!(), - PlacePieceStrategy::PreserveExisting => { - return Err(PlacePieceError::ExisitingPiece) - } - } + if strategy == PlacePieceStrategy::PreserveExisting + && self.by_color.bitboard(color).is_set(piece.square()) + { + return Err(PlacePieceError::ExisitingPiece); } self.by_color_and_shape.set_square(square, piece.piece()); From 9d25414b978a88025eb7f28648d50c607c1cd5e1 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 27 Jan 2024 13:04:22 -0800 Subject: [PATCH 091/423] [board] Directly use PlacedPiece in the position! macro Avoids having to also import the piece! macro to use position!. --- board/src/macros.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/board/src/macros.rs b/board/src/macros.rs index 26827b6..0ee322b 100644 --- a/board/src/macros.rs +++ b/board/src/macros.rs @@ -4,7 +4,14 @@ macro_rules! position { [$($color:ident $shape:ident on $square:ident),* $(,)?] => { $crate::PositionBuilder::new() - $(.place_piece(piece!($color $shape on $square)))* + $(.place_piece( + chessfriend_core::PlacedPiece::new( + chessfriend_core::Piece::new( + chessfriend_core::Color::$color, + chessfriend_core::Shape::$shape), + chessfriend_core::Square::$square + ) + ))* .build() }; } From 654e4094d9056bcaa648eae7255b3cdcaf066357 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 27 Jan 2024 14:25:38 -0800 Subject: [PATCH 092/423] [board] Restructure the move_generator module Move mod.rs to ../move_generator.rs. This is the new style. Move the contents of move_generator::move_generator to the top of move_generator. --- .../{move_generator => }/move_generator.rs | 97 ++++++++++++++++++- board/src/move_generator/mod.rs | 97 ------------------- 2 files changed, 94 insertions(+), 100 deletions(-) rename board/src/{move_generator => }/move_generator.rs (53%) delete mode 100644 board/src/move_generator/mod.rs diff --git a/board/src/move_generator/move_generator.rs b/board/src/move_generator.rs similarity index 53% rename from board/src/move_generator/move_generator.rs rename to board/src/move_generator.rs index 35292a1..2202104 100644 --- a/board/src/move_generator/move_generator.rs +++ b/board/src/move_generator.rs @@ -1,13 +1,104 @@ // Eryn Wells -use super::{ +mod bishop; +mod king; +mod knight; +mod move_set; +mod pawn; +mod queen; +mod rook; + +pub(crate) use move_set::MoveSet; + +use self::{ bishop::ClassicalMoveGenerator as BishopMoveGenerator, king::KingMoveGenerator, - knight::KnightMoveGenerator, move_set::MoveSet, pawn::PawnMoveGenerator, + knight::KnightMoveGenerator, pawn::PawnMoveGenerator, queen::ClassicalMoveGenerator as QueenMoveGenerator, rook::ClassicalMoveGenerator as RookMoveGenerator, }; use crate::{Move, Position}; -use chessfriend_core::{Color, PlacedPiece, Shape}; +use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; +use std::collections::BTreeMap; + +trait MoveGenerator { + fn iter(&self) -> dyn Iterator; + fn moves(&self, color: Color) -> dyn Iterator; + fn attacks(&self, color: Color) -> dyn Iterator; +} + +macro_rules! move_generator_declaration { + ($name:ident) => { + move_generator_declaration!($name, struct); + move_generator_declaration!($name, new); + move_generator_declaration!($name, getters); + }; + ($name:ident, struct) => { + #[derive(Clone, Debug, Eq, PartialEq)] + pub(super) struct $name { + color: chessfriend_core::Color, + move_sets: std::collections::BTreeMap< + chessfriend_core::Square, + $crate::move_generator::MoveSet, + >, + } + }; + ($name:ident, new) => { + impl $name { + pub(super) fn new( + position: &$crate::Position, + color: chessfriend_core::Color, + ) -> $name { + $name { + color, + move_sets: Self::move_sets(position, color), + } + } + } + }; + ($name:ident, getters) => { + impl $name { + pub(super) fn iter(&self) -> impl Iterator + '_ { + self.move_sets.values().map(|set| set.moves()).flatten() + } + + pub(crate) fn moves_for_piece( + &self, + piece: &chessfriend_core::PlacedPiece, + ) -> Option<&$crate::move_generator::move_set::MoveSet> { + self.move_sets.get(&piece.square()) + } + + fn bitboard(&self) -> chessfriend_bitboard::BitBoard { + self.move_sets.values().fold( + chessfriend_bitboard::BitBoard::empty(), + |partial, mv_set| partial | mv_set.bitboard(), + ) + } + } + }; +} + +pub(self) use move_generator_declaration; + +trait MoveGeneratorInternal { + fn piece(color: Color) -> Piece; + + fn move_sets(position: &Position, color: Color) -> BTreeMap { + let piece = Self::piece(color); + BTreeMap::from_iter( + position + .bitboard_for_piece(piece) + .occupied_squares() + .map(|sq| { + let placed_piece = PlacedPiece::new(piece, sq); + let move_set = Self::move_set_for_piece(position, placed_piece); + (sq, move_set) + }), + ) + } + + fn move_set_for_piece(position: &Position, placed_piece: PlacedPiece) -> MoveSet; +} #[derive(Clone, Debug, Eq, PartialEq)] pub struct Moves { diff --git a/board/src/move_generator/mod.rs b/board/src/move_generator/mod.rs deleted file mode 100644 index b15d2f9..0000000 --- a/board/src/move_generator/mod.rs +++ /dev/null @@ -1,97 +0,0 @@ -// Eryn Wells - -mod bishop; -mod king; -mod knight; -mod move_generator; -mod move_set; -mod pawn; -mod queen; -mod rook; - -pub use move_generator::Moves; -pub(crate) use move_set::MoveSet; - -use crate::{Move, Position}; -use chessfriend_core::{Color, Piece, PlacedPiece, Square}; -use std::collections::BTreeMap; - -trait MoveGenerator { - fn iter(&self) -> dyn Iterator; - fn moves(&self, color: Color) -> dyn Iterator; - fn attacks(&self, color: Color) -> dyn Iterator; -} - -macro_rules! move_generator_declaration { - ($name:ident) => { - move_generator_declaration!($name, struct); - move_generator_declaration!($name, new); - move_generator_declaration!($name, getters); - }; - ($name:ident, struct) => { - #[derive(Clone, Debug, Eq, PartialEq)] - pub(super) struct $name { - color: chessfriend_core::Color, - move_sets: std::collections::BTreeMap< - chessfriend_core::Square, - $crate::move_generator::MoveSet, - >, - } - }; - ($name:ident, new) => { - impl $name { - pub(super) fn new( - position: &$crate::Position, - color: chessfriend_core::Color, - ) -> $name { - $name { - color, - move_sets: Self::move_sets(position, color), - } - } - } - }; - ($name:ident, getters) => { - impl $name { - pub(super) fn iter(&self) -> impl Iterator + '_ { - self.move_sets.values().map(|set| set.moves()).flatten() - } - - pub(crate) fn moves_for_piece( - &self, - piece: &chessfriend_core::PlacedPiece, - ) -> Option<&$crate::move_generator::move_set::MoveSet> { - self.move_sets.get(&piece.square()) - } - - fn bitboard(&self) -> chessfriend_bitboard::BitBoard { - self.move_sets.values().fold( - chessfriend_bitboard::BitBoard::empty(), - |partial, mv_set| partial | mv_set.bitboard(), - ) - } - } - }; -} - -pub(self) use move_generator_declaration; - -trait MoveGeneratorInternal { - fn piece(color: Color) -> Piece; - - fn move_sets(position: &Position, color: Color) -> BTreeMap { - let piece = Self::piece(color); - BTreeMap::from_iter( - position - .bitboard_for_piece(piece) - .occupied_squares() - .map(|sq| { - let placed_piece = PlacedPiece::new(piece, sq); - let move_set = Self::move_set_for_piece(position, placed_piece); - (sq, move_set) - }), - ) - } - - fn move_set_for_piece(position: &Position, placed_piece: PlacedPiece) -> MoveSet; -} From 164fe94bc0ea1b0208c1ace353184c347f51a90f Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 00:25:53 -0800 Subject: [PATCH 093/423] [board] Implement danger squares for the current player This concept comes from [1]. Danger squares are the squares a king cannot move to because it would permit the opposing player to capture the king on their next turn. [1]: https://peterellisjones.com/posts/generating-legal-chess-moves-efficiently/ --- board/src/position/builders/move_builder.rs | 2 +- board/src/position/mod.rs | 13 +- board/src/position/piece_sets.rs | 17 ++- board/src/position/position.rs | 72 +++++++++- board/src/sight.rs | 139 +++++++++++++------- core/src/pieces.rs | 20 +-- 6 files changed, 190 insertions(+), 73 deletions(-) diff --git a/board/src/position/builders/move_builder.rs b/board/src/position/builders/move_builder.rs index ff1ce5b..23229e7 100644 --- a/board/src/position/builders/move_builder.rs +++ b/board/src/position/builders/move_builder.rs @@ -62,7 +62,7 @@ where let to_square = mv.to_square(); - let sight = piece.sight_in_position(self.position); + let sight = self.position.sight_of_piece(&piece); if !sight.is_set(to_square) { return Err(MakeMoveError::IllegalSquare(to_square)); } diff --git a/board/src/position/mod.rs b/board/src/position/mod.rs index a03e392..8ece091 100644 --- a/board/src/position/mod.rs +++ b/board/src/position/mod.rs @@ -1,13 +1,16 @@ // Eryn Wells +pub mod piece_sets; + mod builders; mod diagram_formatter; mod flags; -mod piece_sets; mod pieces; mod position; -pub use builders::{MoveBuilder, PositionBuilder}; -pub use diagram_formatter::DiagramFormatter; -pub use pieces::Pieces; -pub use position::Position; +pub use { + builders::{MoveBuilder, PositionBuilder}, + diagram_formatter::DiagramFormatter, + pieces::Pieces, + position::Position, +}; diff --git a/board/src/position/piece_sets.rs b/board/src/position/piece_sets.rs index 0ebbd90..095d184 100644 --- a/board/src/position/piece_sets.rs +++ b/board/src/position/piece_sets.rs @@ -1,7 +1,7 @@ // Eryn Wells use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece, PlacedPiece, Square}; +use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; #[derive(Debug, Eq, PartialEq)] pub enum PlacePieceStrategy { @@ -21,7 +21,7 @@ impl Default for PlacePieceStrategy { } #[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] -pub(super) struct PieceBitBoards { +pub(crate) struct PieceBitBoards { by_color: ByColor, by_color_and_shape: ByColorAndShape, } @@ -51,11 +51,20 @@ impl PieceBitBoards { } } - pub(super) fn all_pieces(&self) -> &BitBoard { + pub(super) fn king(&self, color: Color) -> &BitBoard { + self.by_color_and_shape + .bitboard_for_piece(&Piece::new(color, Shape::King)) + } + + pub(crate) fn all_pieces(&self) -> &BitBoard { self.by_color.all() } - pub(super) fn all_pieces_of_color(&self, color: Color) -> &BitBoard { + pub(crate) fn empty_squares(&self) -> BitBoard { + !self.by_color.all() + } + + pub(crate) fn all_pieces_of_color(&self, color: Color) -> &BitBoard { self.by_color.bitboard(color) } diff --git a/board/src/position/position.rs b/board/src/position/position.rs index 326371f..6f3af39 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -177,12 +177,54 @@ impl Position { self.en_passant_square } + /// A bitboard representing the squares the pieces of the given color can + /// see. This is synonymous with the squares attacked by the player's + /// pieces. pub(crate) fn sight_of_player(&self, color: Color) -> BitBoard { - *self.sight[color as usize].get_or_init(|| { - self.pieces(color).fold(BitBoard::empty(), |acc, pp| { - acc | pp.sight_in_position(&self) + *self.sight[color as usize].get_or_init(|| self._sight_of_player(color, &self.pieces)) + } + + fn _sight_of_player(&self, player: Color, pieces: &PieceBitBoards) -> BitBoard { + let en_passant_square = self.en_passant_square; + + Shape::ALL + .iter() + .filter_map(|&shape| { + let piece = Piece::new(player, shape); + let bitboard = pieces.bitboard_for_piece(&piece); + if !bitboard.is_empty() { + Some((piece, bitboard)) + } else { + None + } }) - }) + .flat_map(|(piece, &bitboard)| { + bitboard.occupied_squares().map(move |square| { + PlacedPiece::new(piece, square).sight(pieces, en_passant_square) + }) + }) + .fold(BitBoard::empty(), |acc, sight| acc | sight) + } + + pub(crate) fn sight_of_piece(&self, piece: &PlacedPiece) -> BitBoard { + piece.sight(&self.pieces, self.en_passant_square) + } + + /// A bitboard representing the squares where a king of the given color will + /// be in danger. The king cannot move to these squares. + pub(crate) fn king_danger(&self) -> BitBoard { + let pieces_without_king = { + let mut cloned_pieces = self.pieces.clone(); + let placed_king = PlacedPiece::new( + Piece::king(self.color_to_move), + self.king_square(self.color_to_move), + ); + cloned_pieces.remove_piece(&placed_king); + + cloned_pieces + }; + + self._sight_of_player(self.color_to_move.other(), &pieces_without_king) } pub(crate) fn is_king_in_check(&self) -> bool { @@ -266,7 +308,8 @@ impl fmt::Display for Position { #[cfg(test)] mod tests { - use crate::{position, test_position, Castle, Position}; + use crate::{position, test_position, Castle, Position, PositionBuilder}; + use chessfriend_bitboard::bitboard; use chessfriend_core::{piece, Color, Square}; #[test] @@ -329,4 +372,23 @@ mod tests { Some(piece!(White Rook on A1)) ); } + + #[test] + fn danger_squares() { + let pos = PositionBuilder::new() + .place_piece(piece!(White King on E1)) + .place_piece(piece!(Black King on E7)) + .place_piece(piece!(White Rook on E4)) + .to_move(Color::Black) + .build(); + + let danger_squares = pos.king_danger(); + let expected = + bitboard![D1, F1, D2, E2, F2, E3, A4, B4, C4, D4, F4, G4, H4, E5, E6, E7, E8]; + assert_eq!( + danger_squares, expected, + "Actual:\n{}\n\nExpected:\n{}", + danger_squares, expected + ); + } } diff --git a/board/src/sight.rs b/board/src/sight.rs index 0e1dc8b..eead56e 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -1,42 +1,56 @@ // Eryn Wells +use crate::position::piece_sets::PieceBitBoards; use crate::Position; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Direction, PlacedPiece, Shape}; +use chessfriend_core::{Color, Direction, Piece, PlacedPiece, Shape, Square}; pub(crate) trait SightExt { - fn sight_in_position(&self, position: &Position) -> BitBoard; + fn sight(&self, pieces: &PieceBitBoards, en_passant_square: Option) -> BitBoard; - fn white_pawn_sight_in_position(&self, position: &Position) -> BitBoard; - fn black_pawn_sight_in_position(&self, position: &Position) -> BitBoard; - fn knight_sight_in_position(&self, position: &Position) -> BitBoard; - fn bishop_sight_in_position(&self, position: &Position) -> BitBoard; - fn rook_sight_in_position(&self, position: &Position) -> BitBoard; - fn queen_sight_in_position(&self, position: &Position) -> BitBoard; - fn king_sight_in_position(&self, position: &Position) -> BitBoard; + fn white_pawn_sight( + &self, + pieces: &PieceBitBoards, + en_passant_square: Option, + ) -> BitBoard; + fn black_pawn_sight( + &self, + pieces: &PieceBitBoards, + en_passant_square: Option, + ) -> BitBoard; + fn knight_sight(&self, pieces: &PieceBitBoards) -> BitBoard; + fn bishop_sight(&self, pieces: &PieceBitBoards) -> BitBoard; + fn rook_sight(&self, pieces: &PieceBitBoards) -> BitBoard; + fn queen_sight(&self, pieces: &PieceBitBoards) -> BitBoard; + fn king_sight(&self, pieces: &PieceBitBoards) -> BitBoard; } impl SightExt for PlacedPiece { - fn sight_in_position(&self, position: &Position) -> BitBoard { + fn sight(&self, pieces: &PieceBitBoards, en_passant_square: Option) -> BitBoard { match self.shape() { Shape::Pawn => match self.color() { - Color::White => self.white_pawn_sight_in_position(position), - Color::Black => self.black_pawn_sight_in_position(position), + Color::White => self.white_pawn_sight(pieces, en_passant_square), + Color::Black => self.black_pawn_sight(pieces, en_passant_square), }, - Shape::Knight => self.knight_sight_in_position(position), - Shape::Bishop => self.bishop_sight_in_position(position), - Shape::Rook => self.rook_sight_in_position(position), - Shape::Queen => self.queen_sight_in_position(position), - Shape::King => self.king_sight_in_position(position), + Shape::Knight => self.knight_sight(pieces), + Shape::Bishop => self.bishop_sight(pieces), + Shape::Rook => self.rook_sight(pieces), + Shape::Queen => self.queen_sight(pieces), + Shape::King => self.king_sight(pieces), } } - fn white_pawn_sight_in_position(&self, position: &Position) -> BitBoard { + fn white_pawn_sight( + &self, + pieces: &PieceBitBoards, + en_passant_square: Option, + ) -> BitBoard { + let opponent = self.color().other(); let pawn: BitBoard = self.square().into(); let pawn = pawn.shift_north_west_one() | pawn.shift_north_east_one(); - let mut possible_squares = position.empty_squares() | position.opposing_pieces(); - if let Some(en_passant) = position.en_passant_square() { + let mut possible_squares = pieces.empty_squares() | pieces.all_pieces_of_color(opponent); + if let Some(en_passant) = en_passant_square { let en_passant_bitboard: BitBoard = en_passant.into(); possible_squares |= en_passant_bitboard; } @@ -44,28 +58,34 @@ impl SightExt for PlacedPiece { pawn & possible_squares } - fn black_pawn_sight_in_position(&self, position: &Position) -> BitBoard { + fn black_pawn_sight( + &self, + pieces: &PieceBitBoards, + en_passant_square: Option, + ) -> BitBoard { + let opponent = self.color().other(); + let pawn: BitBoard = self.square().into(); let pawn = pawn.shift_south_west_one() | pawn.shift_south_east_one(); - let mut possible_squares = position.empty_squares() | position.opposing_pieces(); - if let Some(en_passant) = position.en_passant_square() { + let mut possible_squares = pieces.empty_squares() | pieces.all_pieces_of_color(opponent); + if let Some(en_passant) = en_passant_square { possible_squares |= &en_passant.into(); } pawn & possible_squares } - fn knight_sight_in_position(&self, position: &Position) -> BitBoard { - BitBoard::knight_moves(self.square()) & !position.friendly_pieces() + fn knight_sight(&self, pieces: &PieceBitBoards) -> BitBoard { + BitBoard::knight_moves(self.square()) & !pieces.all_pieces_of_color(self.color()) } - fn bishop_sight_in_position(&self, position: &Position) -> BitBoard { + fn bishop_sight(&self, pieces: &PieceBitBoards) -> BitBoard { let square = self.square(); let mut sight = BitBoard::empty(); - let blockers = position.occupied_squares(); + let blockers = pieces.all_pieces(); macro_rules! update_moves_with_ray { ($direction:ident, $occupied_squares:tt) => { @@ -87,15 +107,16 @@ impl SightExt for PlacedPiece { update_moves_with_ray!(SouthEast, occupied_squares); update_moves_with_ray!(SouthWest, occupied_squares); - sight + let friendly_pieces = pieces.all_pieces_of_color(self.color()); + sight & !friendly_pieces } - fn rook_sight_in_position(&self, position: &Position) -> BitBoard { + fn rook_sight(&self, pieces: &PieceBitBoards) -> BitBoard { let square = self.square(); let mut sight = BitBoard::empty(); - let blockers = position.occupied_squares(); + let blockers = pieces.all_pieces(); macro_rules! update_moves_with_ray { ($direction:ident, $occupied_squares:tt) => { @@ -117,15 +138,16 @@ impl SightExt for PlacedPiece { update_moves_with_ray!(South, occupied_squares); update_moves_with_ray!(West, occupied_squares); - sight + let friendly_pieces = pieces.all_pieces_of_color(self.color()); + sight & !friendly_pieces } - fn queen_sight_in_position(&self, position: &Position) -> BitBoard { + fn queen_sight(&self, pieces: &PieceBitBoards) -> BitBoard { let square = self.square(); let mut sight = BitBoard::empty(); - let blockers = position.occupied_squares(); + let blockers = pieces.all_pieces(); macro_rules! update_moves_with_ray { ($direction:ident, $occupied_squares:tt) => { @@ -151,11 +173,12 @@ impl SightExt for PlacedPiece { update_moves_with_ray!(SouthWest, occupied_squares); update_moves_with_ray!(West, occupied_squares); - sight + let friendly_pieces = pieces.all_pieces_of_color(self.color()); + sight & !friendly_pieces } - fn king_sight_in_position(&self, position: &Position) -> BitBoard { - BitBoard::king_moves(self.square()) & !position.friendly_pieces() + fn king_sight(&self, pieces: &PieceBitBoards) -> BitBoard { + BitBoard::king_moves(self.square()) & !pieces.all_pieces_of_color(self.color()) } } @@ -166,8 +189,8 @@ mod tests { #[test] fn $test_name() { let pos = $position; - let pp = $piece; - let sight = pp.sight_in_position(&pos); + let piece = $piece; + let sight = pos.sight_of_piece(&piece); assert_eq!(sight, $bitboard); } @@ -178,7 +201,7 @@ mod tests { } mod pawn { - use crate::{sight::SightExt, test_position}; + use crate::test_position; use chessfriend_bitboard::{bitboard, BitBoard}; use chessfriend_core::{piece, Square}; @@ -202,8 +225,8 @@ mod tests { White Pawn on E4, ); - let pp = piece!(White Pawn on E4); - let sight = pp.sight_in_position(&pos); + let piece = piece!(White Pawn on E4); + let sight = pos.sight_of_piece(&piece); assert_eq!(sight, BitBoard::empty()); } @@ -216,8 +239,8 @@ mod tests { White Pawn on E4, ); - let pp = piece!(White Pawn on E4); - let sight = pp.sight_in_position(&pos); + let piece = piece!(White Pawn on E4); + let sight = pos.sight_of_piece(&piece); assert_eq!(sight, bitboard!(D5)); } @@ -229,8 +252,8 @@ mod tests { Black Pawn on D5, ); pos.test_set_en_passant_square(Square::D6); - let pp = piece!(White Pawn on E5); - let sight = pp.sight_in_position(&pos); + let piece = piece!(White Pawn on E5); + let sight = pos.sight_of_piece(&piece); assert_eq!(sight, bitboard!(D6, F6)); } @@ -238,7 +261,6 @@ mod tests { #[macro_use] mod knight { - use crate::sight::SightExt; use chessfriend_bitboard::bitboard; use chessfriend_core::piece; @@ -250,7 +272,6 @@ mod tests { } mod bishop { - use crate::sight::SightExt; use chessfriend_bitboard::bitboard; use chessfriend_core::piece; @@ -262,7 +283,7 @@ mod tests { } mod rook { - use crate::sight::SightExt; + use crate::test_position; use chessfriend_bitboard::bitboard; use chessfriend_core::piece; @@ -271,5 +292,27 @@ mod tests { piece!(White Rook on G3), bitboard!(G1, G2, G4, G5, G6, G7, G8, A3, B3, C3, D3, E3, F3, H3) ); + + sight_test!( + e4_rook_with_e1_white_king_e7_black_king, + test_position![ + White Rook on E4, + White King on E1, + Black King on E7, + ], + piece!(White Rook on E4), + bitboard!(A4, B4, C4, D4, F4, G4, H4, E2, E3, E5, E6, E7) + ); + } + + mod king { + use chessfriend_bitboard::bitboard; + use chessfriend_core::piece; + + sight_test!( + e1_king, + piece!(White King on E1), + bitboard![D1, D2, E2, F2, F1] + ); } } diff --git a/core/src/pieces.rs b/core/src/pieces.rs index 782fc47..b49db6d 100644 --- a/core/src/pieces.rs +++ b/core/src/pieces.rs @@ -14,17 +14,17 @@ pub enum Shape { } impl Shape { - pub fn iter() -> Iter<'static, Shape> { - const ALL_SHAPES: [Shape; 6] = [ - Shape::Pawn, - Shape::Knight, - Shape::Bishop, - Shape::Rook, - Shape::Queen, - Shape::King, - ]; + pub const ALL: [Shape; 6] = [ + Shape::Pawn, + Shape::Knight, + Shape::Bishop, + Shape::Rook, + Shape::Queen, + Shape::King, + ]; - ALL_SHAPES.iter() + pub fn iter() -> Iter<'static, Shape> { + Shape::ALL.iter() } pub fn promotable() -> Iter<'static, Shape> { From 1f873879bb29ec12a4fc3d14ea5fe23f4c78d86d Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 09:08:57 -0800 Subject: [PATCH 094/423] [bitboard] Add pawn attacks bitboards to the Library --- bitboard/src/bitboard.rs | 4 ++++ bitboard/src/library.rs | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 6aa0df2..3858ebb 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -40,6 +40,10 @@ impl BitBoard { library().ray(sq, dir) } + pub fn pawn_attacks(sq: Square, color: Color) -> BitBoard { + library().pawn_attacks(sq, color) + } + moves_getter!(knight_moves); moves_getter!(bishop_moves); moves_getter!(rook_moves); diff --git a/bitboard/src/library.rs b/bitboard/src/library.rs index 6c59291..cc37100 100644 --- a/bitboard/src/library.rs +++ b/bitboard/src/library.rs @@ -1,7 +1,7 @@ // Eryn Wells use crate::BitBoard; -use chessfriend_core::{Direction, Square}; +use chessfriend_core::{Color, Direction, Square}; use std::sync::Once; pub(super) const RANKS: [BitBoard; 8] = [ @@ -57,6 +57,7 @@ pub(super) struct MoveLibrary { rays: [[BitBoard; 8]; Square::NUM], // Piecewise move tables + pawn_attacks: [[BitBoard; 64]; 2], knight_moves: [BitBoard; 64], bishop_moves: [BitBoard; 64], rook_moves: [BitBoard; 64], @@ -68,6 +69,7 @@ impl MoveLibrary { const fn new() -> MoveLibrary { MoveLibrary { rays: [[BitBoard::empty(); 8]; Square::NUM], + pawn_attacks: [[BitBoard::empty(); 64]; 2], knight_moves: [BitBoard::empty(); 64], bishop_moves: [BitBoard::empty(); 64], rook_moves: [BitBoard::empty(); 64], @@ -78,6 +80,7 @@ impl MoveLibrary { fn init(&mut self) { for sq in Square::ALL { + self.init_pawn_moves(sq); self.init_orthogonal_rays(sq); self.init_diagonal_rays(sq); self.init_knight_moves(sq as usize); @@ -164,6 +167,14 @@ impl MoveLibrary { self.queen_moves[sq as usize] = rook_moves | bishop_moves; } + fn init_pawn_moves(&mut self, sq: Square) { + let bitboard: BitBoard = sq.into(); + self.pawn_attacks[Color::White as usize][sq as usize] = + bitboard.shift_north_west_one() | bitboard.shift_north_east_one(); + self.pawn_attacks[Color::Black as usize][sq as usize] = + bitboard.shift_south_west_one() | bitboard.shift_south_east_one(); + } + #[inline] fn generate_ray(sq: BitBoard, shift: fn(&BitBoard) -> BitBoard) -> BitBoard { let mut ray = BitBoard::empty(); @@ -181,6 +192,10 @@ impl MoveLibrary { self.rays[sq as usize][dir as usize] } + pub(super) fn pawn_attacks(&self, sq: Square, color: Color) -> BitBoard { + self.pawn_attacks[color as usize][sq as usize] + } + library_getter!(knight_moves); library_getter!(bishop_moves); library_getter!(rook_moves); From b002442b42397c7883898d2e712e94c2208da295 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 09:16:22 -0800 Subject: [PATCH 095/423] [board] Implement a custom PartialEq on Position We don't need to compare cached data. The load-bearing parts of a Position are: player to move, position of all pieces, flags, and en passant status. Inspiration for this implementation came from the FEN spec. It specifies all these in the string. --- board/src/position/position.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/board/src/position/position.rs b/board/src/position/position.rs index 6f3af39..acb6efa 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -12,7 +12,7 @@ use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; use std::{cell::OnceCell, fmt}; -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq)] pub struct Position { color_to_move: Color, flags: Flags, @@ -300,6 +300,15 @@ impl Default for Position { } } +impl PartialEq for Position { + fn eq(&self, other: &Self) -> bool { + self.pieces == other.pieces + && self.color_to_move == other.color_to_move + && self.flags == other.flags + && self.en_passant_square == other.en_passant_square + } +} + impl fmt::Display for Position { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", DiagramFormatter::new(self)) From 76ac7194180b041d9fc01d9a55ab677f4c467732 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 09:40:45 -0800 Subject: [PATCH 096/423] =?UTF-8?q?[position]=20Rename=20the=20board=20cra?= =?UTF-8?q?te=20=E2=86=92=20chessfriend=5Fposition=20in=20Cargo.toml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename the crate, but don't move any files. --- board/Cargo.toml | 2 +- explorer/Cargo.toml | 2 +- explorer/src/main.rs | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/board/Cargo.toml b/board/Cargo.toml index 70584c2..db11c0d 100644 --- a/board/Cargo.toml +++ b/board/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "board" +name = "chessfriend_position" version = "0.1.0" edition = "2021" diff --git a/explorer/Cargo.toml b/explorer/Cargo.toml index 3f2513b..3281cf4 100644 --- a/explorer/Cargo.toml +++ b/explorer/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] chessfriend_core = { path = "../core" } -board = { path = "../board" } +chessfriend_position = { path = "../position" } clap = { version = "4.4.12", features = ["derive"] } rustyline = "13.0.0" shlex = "1.2.0" diff --git a/explorer/src/main.rs b/explorer/src/main.rs index fedd11c..9bf7000 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -1,6 +1,5 @@ -use board::piece::{Color, Piece, Shape}; -use board::Position; use chessfriend_core::Square; +use chessfriend_position::{fen::ToFen, MakeMoveBuilder, MoveBuilder, Position, PositionBuilder}; use clap::{Arg, Command}; use rustyline::error::ReadlineError; use rustyline::DefaultEditor; From 1e77bc5ebb53e23c1e6e188ea30b256b4a1573bc Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 09:45:13 -0800 Subject: [PATCH 097/423] [board] Test to verify the king can't move into or stay in check Write a test on the king move generator to verify the king can't move to a square where it would still be in check. --- board/src/move_generator/king.rs | 38 +++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/board/src/move_generator/king.rs b/board/src/move_generator/king.rs index 1d0dda2..1f35af5 100644 --- a/board/src/move_generator/king.rs +++ b/board/src/move_generator/king.rs @@ -74,7 +74,7 @@ impl MoveGeneratorInternal for KingMoveGenerator { #[cfg(test)] mod tests { use super::*; - use crate::position; + use crate::{position, r#move::AlgebraicMoveFormatter, PositionBuilder}; use chessfriend_bitboard::bitboard; use chessfriend_core::{piece, Square}; use std::collections::HashSet; @@ -154,4 +154,40 @@ mod tests { generated_moves ); } + + #[test] + fn black_king_in_check_by_rook() { + let pos = PositionBuilder::new() + .place_piece(piece!(White King on E1)) + .place_piece(piece!(White Rook on E4)) + .place_piece(piece!(Black King on E7)) + .to_move(Color::Black) + .build(); + + assert!(pos.is_king_in_check()); + + let generator = KingMoveGenerator::new(&pos, Color::Black); + let generated_moves: HashSet = generator.iter().cloned().collect(); + + let king = piece!(Black King); + let from_square = Square::E7; + let expected_moves = HashSet::from_iter([ + MoveBuilder::new(king, from_square, Square::D6).build(), + MoveBuilder::new(king, from_square, Square::D7).build(), + MoveBuilder::new(king, from_square, Square::D8).build(), + MoveBuilder::new(king, from_square, Square::F6).build(), + MoveBuilder::new(king, from_square, Square::F7).build(), + MoveBuilder::new(king, from_square, Square::F8).build(), + ]); + + assert_eq!( + generated_moves, + expected_moves, + "Difference: {:?}", + generated_moves + .symmetric_difference(&expected_moves) + .map(|mv| format!("{}", AlgebraicMoveFormatter::new(mv, &pos))) + .collect::>() + ); + } } From 66d03d35144ab0201aaf2ad2b25c905ddb460322 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 09:46:38 -0800 Subject: [PATCH 098/423] [board] Clean up a bunch of imports --- bitboard/src/bitboard.rs | 2 +- board/src/position/diagram_formatter.rs | 1 - board/src/sight.rs | 5 +++-- explorer/src/main.rs | 5 ++--- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 3858ebb..f9babd4 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -2,7 +2,7 @@ use crate::library::{library, FILES, RANKS}; use crate::LeadingBitScanner; -use chessfriend_core::{Direction, Square}; +use chessfriend_core::{Color, Direction, Square}; use std::fmt; use std::ops::Not; diff --git a/board/src/position/diagram_formatter.rs b/board/src/position/diagram_formatter.rs index 80bbbe0..9dfb5c7 100644 --- a/board/src/position/diagram_formatter.rs +++ b/board/src/position/diagram_formatter.rs @@ -41,7 +41,6 @@ impl<'a> fmt::Display for DiagramFormatter<'a> { mod tests { use super::*; use crate::{position, Position}; - use chessfriend_core::piece; #[test] #[ignore] diff --git a/board/src/sight.rs b/board/src/sight.rs index eead56e..76005a2 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -1,9 +1,8 @@ // Eryn Wells use crate::position::piece_sets::PieceBitBoards; -use crate::Position; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Direction, Piece, PlacedPiece, Shape, Square}; +use chessfriend_core::{Color, Direction, PlacedPiece, Shape, Square}; pub(crate) trait SightExt { fn sight(&self, pieces: &PieceBitBoards, en_passant_square: Option) -> BitBoard; @@ -13,11 +12,13 @@ pub(crate) trait SightExt { pieces: &PieceBitBoards, en_passant_square: Option, ) -> BitBoard; + fn black_pawn_sight( &self, pieces: &PieceBitBoards, en_passant_square: Option, ) -> BitBoard; + fn knight_sight(&self, pieces: &PieceBitBoards) -> BitBoard; fn bishop_sight(&self, pieces: &PieceBitBoards) -> BitBoard; fn rook_sight(&self, pieces: &PieceBitBoards) -> BitBoard; diff --git a/explorer/src/main.rs b/explorer/src/main.rs index fedd11c..54b9fdd 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -1,6 +1,5 @@ -use board::piece::{Color, Piece, Shape}; -use board::Position; -use chessfriend_core::Square; +use board::{fen::ToFen, MakeMoveBuilder, MoveBuilder, Position, PositionBuilder}; +use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; use clap::{Arg, Command}; use rustyline::error::ReadlineError; use rustyline::DefaultEditor; From 6bd3787a24cf4e6dfc435850038e9fc834ba412b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 09:47:25 -0800 Subject: [PATCH 099/423] [board] Write test_position!({starting,empty}) macros These produce starting and empty positions, respectively, and then print the position. --- board/src/macros.rs | 14 ++++++++++++++ board/src/position/position.rs | 3 +-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/board/src/macros.rs b/board/src/macros.rs index 0ee322b..13d2c38 100644 --- a/board/src/macros.rs +++ b/board/src/macros.rs @@ -28,4 +28,18 @@ macro_rules! test_position { pos } }; + (empty) => { + { + let pos = Position::empty(); + println!("{pos}"); + pos + } + }; + (starting) => { + { + let pos = Position::starting(); + println!("{pos}"); + pos + } + }; } diff --git a/board/src/position/position.rs b/board/src/position/position.rs index acb6efa..6a09226 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -333,8 +333,7 @@ mod tests { #[test] fn piece_in_starting_position() { - let pos = Position::starting(); - println!("{pos}"); + let pos = test_position!(starting); assert_eq!( pos.piece_on_square(Square::H1), From 8eb180df6714d1c9ba65f5db0b47db8a4efcdbd4 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 09:50:39 -0800 Subject: [PATCH 100/423] [explorer] Add fen and make commands Clean up the implementation of the place command. Track state with a State struct that contains a position and a builder. The place command will place a new piece and then regenerate the position. The make command makes a move. The syntax is: make [color:w|b] [shape] [from square] [to square] The fen command prints a FEN string representing the position. --- explorer/src/main.rs | 105 ++++++++++++++++++++++++++++++++----------- 1 file changed, 80 insertions(+), 25 deletions(-) diff --git a/explorer/src/main.rs b/explorer/src/main.rs index 54b9fdd..0dfe2ad 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -29,28 +29,31 @@ fn command_line() -> Command { {all-args} "; - // strip out name/version - const APPLET_TEMPLATE: &str = "\ - {about-with-newline}\n\ - {usage-heading}\n {usage}\n\ - \n\ - {all-args}{after-help}\ - "; - Command::new("explorer") .multicall(true) .arg_required_else_help(true) .subcommand_required(true) - .subcommand_value_name("APPLET") - .subcommand_help_heading("APPLETS") + .subcommand_value_name("CMD") + .subcommand_help_heading("COMMANDS") .help_template(PARSER_TEMPLATE) - .subcommand(Command::new("print").about("Print the board")) + .subcommand(Command::new("fen").about("Print the current position as a FEN string")) + .subcommand( + Command::new("make") + .arg(Arg::new("piece").required(true)) + .arg(Arg::new("from").required(true)) + .arg(Arg::new("to").required(true)) + .about("Make a move"), + ) .subcommand( Command::new("place") + .arg(Arg::new("color").required(true)) .arg(Arg::new("piece").required(true)) - .arg(Arg::new("square").required(true)), + .arg(Arg::new("square").required(true)) + .about("Place a piece on the board"), ) - .subcommand(Command::new("quit").about("Quit the program")) + .subcommand(Command::new("print").about("Print the board")) + .subcommand(Command::new("quit").alias("exit").about("Quit the program")) + .subcommand(Command::new("starting").about("Reset the board to the starting position")) } fn respond(line: &str, state: &mut State) -> Result { @@ -67,20 +70,72 @@ fn respond(line: &str, state: &mut State) -> Result { result.should_continue = false; result.should_print_position = false; } - Some(("place", _matches)) => { - let piece_arg = _matches.get_one::("piece").unwrap(); - let shape = match piece_arg.chars().nth(0) { - Some(c) => Shape::try_from(c).map_err(|_| ()), - None => Err(()), - } - .map_err(|_| "Error: invalid piece specifier")?; + Some(("fen", _matches)) => { + println!( + "{}", + state + .position + .to_fen() + .map_err(|_| "error: Unable to generate FEN for current position")? + ); - let square_arg = _matches.get_one::("square").unwrap(); - let square = Square::from_algebraic_str(square_arg) + result.should_print_position = false; + } + Some(("make", matches)) => { + let shape = matches + .get_one::("piece") + .ok_or("Missing piece descriptor")?; + let shape = Shape::try_from(shape).map_err(|_| "Invalid piece descriptor")?; + + let from_square = Square::from_algebraic_str( + matches.get_one::("from").ok_or("Missing square")?, + ) + .map_err(|_| "Error: invalid square specifier")?; + + let to_square = Square::from_algebraic_str( + matches.get_one::("to").ok_or("Missing square")?, + ) + .map_err(|_| "Error: invalid square specifier")?; + + let mv = MoveBuilder::new( + Piece::new(state.position.player_to_move(), shape), + from_square, + to_square, + ) + .build(); + + state.position = MakeMoveBuilder::new(&state.position) + .make(&mv) + .map_err(|err| format!("error: Cannot make move: {:?}", err))? + .build(); + state.builder = PositionBuilder::from_position(&state.position); + } + Some(("place", matches)) => { + let color = matches + .get_one::("color") + .ok_or("Missing color descriptor")?; + let color = Color::try_from(color).map_err(|_| "Invalid color descriptor")?; + + let shape = matches + .get_one::("piece") + .ok_or("Missing piece descriptor")?; + let shape = Shape::try_from(shape).map_err(|_| "Invalid piece descriptor")?; + + let square = matches + .get_one::("square") + .ok_or("Missing square")?; + let square = Square::from_algebraic_str(square) .map_err(|_| "Error: invalid square specifier")?; - pos.place_piece(&Piece::new(Color::White, shape), &square) - .map_err(|_| "Error: Unable to place piece")?; + let piece = PlacedPiece::new(Piece::new(color, shape), square); + + state.builder.place_piece(piece); + state.position = state.builder.build(); + } + Some(("starting", _matches)) => { + let starting_position = Position::starting(); + state.builder = PositionBuilder::from_position(&starting_position); + state.position = starting_position; } Some((name, _matches)) => unimplemented!("{name}"), None => unreachable!("Subcommand required"), @@ -107,7 +162,7 @@ fn main() -> Result<(), String> { println!("{} to move.", state.position.player_to_move()); } - let readline = editor.readline("? "); + let readline = editor.readline("\n? "); match readline { Ok(line) => { let line = line.trim(); From 220da087274ac9c45d3bd438de329b70e3050580 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 09:56:57 -0800 Subject: [PATCH 101/423] Directly rename board -> position --- {board => position}/Cargo.lock | 0 {board => position}/Cargo.toml | 0 {board => position}/src/display.rs | 0 {board => position}/src/fen.rs | 0 {board => position}/src/lib.rs | 0 {board => position}/src/macros.rs | 0 {board => position}/src/move.rs | 0 {board => position}/src/move_generator.rs | 0 {board => position}/src/move_generator/bishop.rs | 0 {board => position}/src/move_generator/king.rs | 0 {board => position}/src/move_generator/knight.rs | 0 {board => position}/src/move_generator/move_set.rs | 0 {board => position}/src/move_generator/pawn.rs | 0 {board => position}/src/move_generator/queen.rs | 0 {board => position}/src/move_generator/rook.rs | 0 {board => position}/src/position/builders/mod.rs | 0 {board => position}/src/position/builders/move_builder.rs | 0 {board => position}/src/position/builders/position_builder.rs | 0 {board => position}/src/position/diagram_formatter.rs | 0 {board => position}/src/position/flags.rs | 0 {board => position}/src/position/mod.rs | 0 {board => position}/src/position/piece_sets.rs | 0 {board => position}/src/position/pieces.rs | 0 {board => position}/src/position/position.rs | 0 {board => position}/src/sight.rs | 0 25 files changed, 0 insertions(+), 0 deletions(-) rename {board => position}/Cargo.lock (100%) rename {board => position}/Cargo.toml (100%) rename {board => position}/src/display.rs (100%) rename {board => position}/src/fen.rs (100%) rename {board => position}/src/lib.rs (100%) rename {board => position}/src/macros.rs (100%) rename {board => position}/src/move.rs (100%) rename {board => position}/src/move_generator.rs (100%) rename {board => position}/src/move_generator/bishop.rs (100%) rename {board => position}/src/move_generator/king.rs (100%) rename {board => position}/src/move_generator/knight.rs (100%) rename {board => position}/src/move_generator/move_set.rs (100%) rename {board => position}/src/move_generator/pawn.rs (100%) rename {board => position}/src/move_generator/queen.rs (100%) rename {board => position}/src/move_generator/rook.rs (100%) rename {board => position}/src/position/builders/mod.rs (100%) rename {board => position}/src/position/builders/move_builder.rs (100%) rename {board => position}/src/position/builders/position_builder.rs (100%) rename {board => position}/src/position/diagram_formatter.rs (100%) rename {board => position}/src/position/flags.rs (100%) rename {board => position}/src/position/mod.rs (100%) rename {board => position}/src/position/piece_sets.rs (100%) rename {board => position}/src/position/pieces.rs (100%) rename {board => position}/src/position/position.rs (100%) rename {board => position}/src/sight.rs (100%) diff --git a/board/Cargo.lock b/position/Cargo.lock similarity index 100% rename from board/Cargo.lock rename to position/Cargo.lock diff --git a/board/Cargo.toml b/position/Cargo.toml similarity index 100% rename from board/Cargo.toml rename to position/Cargo.toml diff --git a/board/src/display.rs b/position/src/display.rs similarity index 100% rename from board/src/display.rs rename to position/src/display.rs diff --git a/board/src/fen.rs b/position/src/fen.rs similarity index 100% rename from board/src/fen.rs rename to position/src/fen.rs diff --git a/board/src/lib.rs b/position/src/lib.rs similarity index 100% rename from board/src/lib.rs rename to position/src/lib.rs diff --git a/board/src/macros.rs b/position/src/macros.rs similarity index 100% rename from board/src/macros.rs rename to position/src/macros.rs diff --git a/board/src/move.rs b/position/src/move.rs similarity index 100% rename from board/src/move.rs rename to position/src/move.rs diff --git a/board/src/move_generator.rs b/position/src/move_generator.rs similarity index 100% rename from board/src/move_generator.rs rename to position/src/move_generator.rs diff --git a/board/src/move_generator/bishop.rs b/position/src/move_generator/bishop.rs similarity index 100% rename from board/src/move_generator/bishop.rs rename to position/src/move_generator/bishop.rs diff --git a/board/src/move_generator/king.rs b/position/src/move_generator/king.rs similarity index 100% rename from board/src/move_generator/king.rs rename to position/src/move_generator/king.rs diff --git a/board/src/move_generator/knight.rs b/position/src/move_generator/knight.rs similarity index 100% rename from board/src/move_generator/knight.rs rename to position/src/move_generator/knight.rs diff --git a/board/src/move_generator/move_set.rs b/position/src/move_generator/move_set.rs similarity index 100% rename from board/src/move_generator/move_set.rs rename to position/src/move_generator/move_set.rs diff --git a/board/src/move_generator/pawn.rs b/position/src/move_generator/pawn.rs similarity index 100% rename from board/src/move_generator/pawn.rs rename to position/src/move_generator/pawn.rs diff --git a/board/src/move_generator/queen.rs b/position/src/move_generator/queen.rs similarity index 100% rename from board/src/move_generator/queen.rs rename to position/src/move_generator/queen.rs diff --git a/board/src/move_generator/rook.rs b/position/src/move_generator/rook.rs similarity index 100% rename from board/src/move_generator/rook.rs rename to position/src/move_generator/rook.rs diff --git a/board/src/position/builders/mod.rs b/position/src/position/builders/mod.rs similarity index 100% rename from board/src/position/builders/mod.rs rename to position/src/position/builders/mod.rs diff --git a/board/src/position/builders/move_builder.rs b/position/src/position/builders/move_builder.rs similarity index 100% rename from board/src/position/builders/move_builder.rs rename to position/src/position/builders/move_builder.rs diff --git a/board/src/position/builders/position_builder.rs b/position/src/position/builders/position_builder.rs similarity index 100% rename from board/src/position/builders/position_builder.rs rename to position/src/position/builders/position_builder.rs diff --git a/board/src/position/diagram_formatter.rs b/position/src/position/diagram_formatter.rs similarity index 100% rename from board/src/position/diagram_formatter.rs rename to position/src/position/diagram_formatter.rs diff --git a/board/src/position/flags.rs b/position/src/position/flags.rs similarity index 100% rename from board/src/position/flags.rs rename to position/src/position/flags.rs diff --git a/board/src/position/mod.rs b/position/src/position/mod.rs similarity index 100% rename from board/src/position/mod.rs rename to position/src/position/mod.rs diff --git a/board/src/position/piece_sets.rs b/position/src/position/piece_sets.rs similarity index 100% rename from board/src/position/piece_sets.rs rename to position/src/position/piece_sets.rs diff --git a/board/src/position/pieces.rs b/position/src/position/pieces.rs similarity index 100% rename from board/src/position/pieces.rs rename to position/src/position/pieces.rs diff --git a/board/src/position/position.rs b/position/src/position/position.rs similarity index 100% rename from board/src/position/position.rs rename to position/src/position/position.rs diff --git a/board/src/sight.rs b/position/src/sight.rs similarity index 100% rename from board/src/sight.rs rename to position/src/sight.rs From ed55eda901d5f8a27a6c2754290e5d80b8af42fa Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 09:58:50 -0800 Subject: [PATCH 102/423] Update the Cargo.toml files --- Cargo.toml | 3 ++- explorer/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 64d2a06..16969d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,9 @@ [workspace] members = [ - "board", "bitboard", "core", "explorer", + "move_generator", + "position", ] resolver = "2" diff --git a/explorer/Cargo.toml b/explorer/Cargo.toml index aabf2e3..3281cf4 100644 --- a/explorer/Cargo.toml +++ b/explorer/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] chessfriend_core = { path = "../core" } -chessfriend_position = { path = "../board" } +chessfriend_position = { path = "../position" } clap = { version = "4.4.12", features = ["derive"] } rustyline = "13.0.0" shlex = "1.2.0" From 77f419ad3bf90520f2e55f51a685869aea7e7b29 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 10:02:53 -0800 Subject: [PATCH 103/423] =?UTF-8?q?[position]=20Rename=20FenError=20?= =?UTF-8?q?=E2=86=92=20ToFenError?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an associated type called Error to the ToFen trait. This mirrors the try_from any try_into traits. --- position/src/fen.rs | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/position/src/fen.rs b/position/src/fen.rs index 26cdd0c..f21de15 100644 --- a/position/src/fen.rs +++ b/position/src/fen.rs @@ -5,16 +5,19 @@ use chessfriend_core::{Color, File, Piece, PlacedPiece, Rank, Square}; use std::fmt::Write; #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum FenError { +pub enum ToFenError { FmtError(std::fmt::Error), } pub trait ToFen { - fn to_fen(&self) -> Result; + type Error; + fn to_fen(&self) -> Result; } impl ToFen for Position { - fn to_fen(&self) -> Result { + type Error = ToFenError; + + fn to_fen(&self) -> Result { let mut fen_string = String::new(); let mut empty_squares: u8 = 0; @@ -25,27 +28,27 @@ impl ToFen for Position { Some(piece) => { if empty_squares > 0 { write!(fen_string, "{}", empty_squares) - .map_err(|err| FenError::FmtError(err))?; + .map_err(|err| ToFenError::FmtError(err))?; empty_squares = 0; } write!(fen_string, "{}", piece.to_fen()?) - .map_err(|err| FenError::FmtError(err))?; + .map_err(|err| ToFenError::FmtError(err))?; } None => empty_squares += 1, } } if empty_squares > 0 { - write!(fen_string, "{}", empty_squares).map_err(|err| FenError::FmtError(err))?; + write!(fen_string, "{}", empty_squares).map_err(|err| ToFenError::FmtError(err))?; empty_squares = 0; } if rank != &Rank::ONE { - write!(fen_string, "/").map_err(|err| FenError::FmtError(err))?; + write!(fen_string, "/").map_err(|err| ToFenError::FmtError(err))?; } } write!(fen_string, " {}", self.player_to_move().to_fen()?) - .map_err(|err| FenError::FmtError(err))?; + .map_err(|err| ToFenError::FmtError(err))?; let castling = [ (Color::White, Castle::KingSide), @@ -73,7 +76,7 @@ impl ToFen for Position { " {}", if castling.len() > 0 { &castling } else { "-" } ) - .map_err(|err| FenError::FmtError(err))?; + .map_err(|err| ToFenError::FmtError(err))?; write!( fen_string, @@ -84,17 +87,19 @@ impl ToFen for Position { "-".to_string() } ) - .map_err(|err| FenError::FmtError(err))?; + .map_err(|err| ToFenError::FmtError(err))?; - write!(fen_string, " {}", self.ply_counter()).map_err(|err| FenError::FmtError(err))?; - write!(fen_string, " {}", self.move_number()).map_err(|err| FenError::FmtError(err))?; + write!(fen_string, " {}", self.ply_counter()).map_err(|err| ToFenError::FmtError(err))?; + write!(fen_string, " {}", self.move_number()).map_err(|err| ToFenError::FmtError(err))?; Ok(fen_string) } } impl ToFen for Color { - fn to_fen(&self) -> Result { + type Error = ToFenError; + + fn to_fen(&self) -> Result { match self { Color::White => Ok("w".to_string()), Color::Black => Ok("b".to_string()), @@ -103,7 +108,9 @@ impl ToFen for Color { } impl ToFen for Piece { - fn to_fen(&self) -> Result { + type Error = ToFenError; + + fn to_fen(&self) -> Result { let ascii: char = self.to_ascii(); Ok(String::from(match self.color() { Color::White => ascii.to_ascii_uppercase(), @@ -113,7 +120,9 @@ impl ToFen for Piece { } impl ToFen for PlacedPiece { - fn to_fen(&self) -> Result { + type Error = ToFenError; + + fn to_fen(&self) -> Result { Ok(self.piece().to_fen()?) } } From ea74b214da1f1e73d63c6db199a5e686faf237e9 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 10:25:01 -0800 Subject: [PATCH 104/423] [position] Implement generating pawn moves by looking up bitboards in the Library This enables a bunch of clean up! Remove the MoveGenerationParameters and MoveList types from move_generator::pawn. Implement BitBoard::pawn_pushes to look up pawn pushes by square and color. --- bitboard/src/bitboard.rs | 4 ++ bitboard/src/library.rs | 25 ++++++++ position/src/move_generator/pawn.rs | 91 ++++++----------------------- 3 files changed, 48 insertions(+), 72 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index f9babd4..fa91c16 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -44,6 +44,10 @@ impl BitBoard { library().pawn_attacks(sq, color) } + pub fn pawn_pushes(sq: Square, color: Color) -> BitBoard { + library().pawn_pushes(sq, color) + } + moves_getter!(knight_moves); moves_getter!(bishop_moves); moves_getter!(rook_moves); diff --git a/bitboard/src/library.rs b/bitboard/src/library.rs index cc37100..6e51f24 100644 --- a/bitboard/src/library.rs +++ b/bitboard/src/library.rs @@ -58,6 +58,7 @@ pub(super) struct MoveLibrary { // Piecewise move tables pawn_attacks: [[BitBoard; 64]; 2], + pawn_pushes: [[BitBoard; 64]; 2], knight_moves: [BitBoard; 64], bishop_moves: [BitBoard; 64], rook_moves: [BitBoard; 64], @@ -70,6 +71,7 @@ impl MoveLibrary { MoveLibrary { rays: [[BitBoard::empty(); 8]; Square::NUM], pawn_attacks: [[BitBoard::empty(); 64]; 2], + pawn_pushes: [[BitBoard::empty(); 64]; 2], knight_moves: [BitBoard::empty(); 64], bishop_moves: [BitBoard::empty(); 64], rook_moves: [BitBoard::empty(); 64], @@ -169,10 +171,29 @@ impl MoveLibrary { fn init_pawn_moves(&mut self, sq: Square) { let bitboard: BitBoard = sq.into(); + self.pawn_attacks[Color::White as usize][sq as usize] = bitboard.shift_north_west_one() | bitboard.shift_north_east_one(); self.pawn_attacks[Color::Black as usize][sq as usize] = bitboard.shift_south_west_one() | bitboard.shift_south_east_one(); + + self.pawn_pushes[Color::White as usize][sq as usize] = { + let mut push = bitboard.shift_north_one(); + if !(bitboard & RANKS[1]).is_empty() { + push |= push.shift_north_one(); + } + + push + }; + + self.pawn_pushes[Color::Black as usize][sq as usize] = { + let mut push = bitboard.shift_south_one(); + if !(bitboard & RANKS[6]).is_empty() { + push |= push.shift_south_one(); + } + + push + }; } #[inline] @@ -192,6 +213,10 @@ impl MoveLibrary { self.rays[sq as usize][dir as usize] } + pub(super) fn pawn_pushes(&self, sq: Square, color: Color) -> BitBoard { + self.pawn_pushes[color as usize][sq as usize] + } + pub(super) fn pawn_attacks(&self, sq: Square, color: Color) -> BitBoard { self.pawn_attacks[color as usize][sq as usize] } diff --git a/position/src/move_generator/pawn.rs b/position/src/move_generator/pawn.rs index 860c564..3d6d5f4 100644 --- a/position/src/move_generator/pawn.rs +++ b/position/src/move_generator/pawn.rs @@ -1,27 +1,13 @@ // Eryn Wells use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use crate::{Move, MoveBuilder, Position}; +use crate::{MoveBuilder, Position}; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; - -enum MoveList { - Quiet = 0, - Promotions = 1, - Captures = 2, -} +use chessfriend_core::{Color, Piece, PlacedPiece, Square}; #[derive(Debug)] struct MoveIterator(usize, usize); -struct MoveGenerationParameters { - starting_rank: BitBoard, - promotion_rank: BitBoard, - push_shift: fn(&BitBoard) -> BitBoard, - left_capture_shift: fn(&BitBoard) -> BitBoard, - right_capture_shift: fn(&BitBoard) -> BitBoard, -} - move_generator_declaration!(PawnMoveGenerator); impl MoveGeneratorInternal for PawnMoveGenerator { @@ -31,12 +17,9 @@ impl MoveGeneratorInternal for PawnMoveGenerator { fn move_set_for_piece(position: &Position, placed_piece: PlacedPiece) -> MoveSet { let from_square = placed_piece.square(); - let parameters = Self::move_generation_parameters(placed_piece.color()); - let opposing_pieces = position.opposing_pieces(); - - let captures_bitboard = Self::attacks(position, placed_piece, ¶meters); - let quiet_moves_bitboard = Self::pushes(position, placed_piece, ¶meters); + let captures_bitboard = Self::attacks(position, placed_piece); + let quiet_moves_bitboard = Self::pushes(position, placed_piece); let quiet_moves = quiet_moves_bitboard.occupied_squares().map(|to_square| { MoveBuilder::new(*placed_piece.piece(), from_square, to_square).build() @@ -55,67 +38,34 @@ impl MoveGeneratorInternal for PawnMoveGenerator { } impl PawnMoveGenerator { - fn move_generation_parameters(color: Color) -> MoveGenerationParameters { - match color { - Color::White => MoveGenerationParameters { - starting_rank: BitBoard::rank(1), - promotion_rank: BitBoard::rank(7), - push_shift: BitBoard::shift_north_one, - left_capture_shift: BitBoard::shift_north_west_one, - right_capture_shift: BitBoard::shift_north_east_one, - }, - Color::Black => MoveGenerationParameters { - starting_rank: BitBoard::rank(6), - promotion_rank: BitBoard::rank(0), - push_shift: BitBoard::shift_south_one, - left_capture_shift: BitBoard::shift_south_east_one, - right_capture_shift: BitBoard::shift_south_west_one, - }, - } - } - - fn pushes( - position: &Position, - piece: PlacedPiece, - parameters: &MoveGenerationParameters, - ) -> BitBoard { + fn pushes(position: &Position, piece: PlacedPiece) -> BitBoard { let empty_squares = position.empty_squares(); - let from_square: BitBoard = piece.square().into(); - - (parameters.push_shift)(&from_square) & empty_squares + BitBoard::pawn_pushes(piece.square(), piece.color()) & empty_squares } - fn attacks( - position: &Position, - piece: PlacedPiece, - parameters: &MoveGenerationParameters, - ) -> BitBoard { + fn attacks(position: &Position, piece: PlacedPiece) -> BitBoard { let color = piece.color(); let opponent_pieces = position.bitboard_for_color(color.other()); - let en_passant_square = position - .en_passant_square() - .map(|square| >::into(square)) - .unwrap_or(BitBoard::empty()); + let en_passant_bitboard = match position.en_passant_square() { + Some(square) => >::into(square), + None => BitBoard::empty(), + }; - let from_square: BitBoard = piece.square().into(); - - ((parameters.left_capture_shift)(&from_square) - | (parameters.right_capture_shift)(&from_square)) - & (opponent_pieces | en_passant_square) + BitBoard::pawn_attacks(piece.square(), color) & (opponent_pieces | en_passant_bitboard) } } #[cfg(test)] mod tests { use super::*; - use crate::{position, position::DiagramFormatter}; + use crate::{position::DiagramFormatter, test_position, Move}; use chessfriend_core::{piece, Square}; use std::collections::HashSet; #[test] fn one_2square_push() { - let pos = position![White Pawn on E2]; + let pos = test_position![White Pawn on E2]; let generator = PawnMoveGenerator::new(&pos, Color::White); @@ -131,7 +81,7 @@ mod tests { #[test] fn one_1square_push() { - let mut pos = position![White Pawn on E3]; + let pos = test_position![White Pawn on E3]; let generator = PawnMoveGenerator::new(&pos, Color::White); @@ -149,7 +99,7 @@ mod tests { #[test] fn one_obstructed_2square_push() { - let pos = position![ + let pos = test_position![ White Pawn on E2, White Knight on E4, ]; @@ -172,11 +122,10 @@ mod tests { #[test] fn one_obstructed_1square_push() { - let mut pos = position![ + let pos = test_position![ White Pawn on E2, White Knight on E3, ]; - println!("{}", DiagramFormatter::new(&pos)); let generator = PawnMoveGenerator::new(&pos, Color::White); @@ -187,12 +136,11 @@ mod tests { #[test] fn one_attack() { - let pos = position![ + let pos = test_position![ White Pawn on E4, White Bishop on E5, Black Knight on D5, ]; - println!("{}", DiagramFormatter::new(&pos)); let generator = PawnMoveGenerator::new(&pos, Color::White); @@ -210,13 +158,12 @@ mod tests { #[test] fn one_double_attack() { - let pos = position![ + let pos = test_position![ White Pawn on E4, White Bishop on E5, Black Knight on D5, Black Queen on F5, ]; - println!("{}", DiagramFormatter::new(&pos)); let generator = PawnMoveGenerator::new(&pos, Color::White); From dab787170cd7f26f56c255cb3e92827083bd3288 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 10:28:01 -0800 Subject: [PATCH 105/423] [position] Clean up rook unit tests Use test_position! instead of position! Spell out the PlacedPiece constructor in the test_position! macro. --- position/src/macros.rs | 10 +++++++++- position/src/move_generator/rook.rs | 11 +++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/position/src/macros.rs b/position/src/macros.rs index 13d2c38..178b4c4 100644 --- a/position/src/macros.rs +++ b/position/src/macros.rs @@ -22,7 +22,15 @@ macro_rules! test_position { [$($color:ident $shape:ident on $square:ident),* $(,)?] => { { let pos = $crate::PositionBuilder::new() - $(.place_piece(piece!($color $shape on $square)))* + $(.place_piece( + chessfriend_core::PlacedPiece::new( + chessfriend_core::Piece::new( + chessfriend_core::Color::$color, + chessfriend_core::Shape::$shape + ), + chessfriend_core::Square::$square + )) + )* .build(); println!("{pos}"); pos diff --git a/position/src/move_generator/rook.rs b/position/src/move_generator/rook.rs index dae6daa..71e17e7 100644 --- a/position/src/move_generator/rook.rs +++ b/position/src/move_generator/rook.rs @@ -60,14 +60,13 @@ impl MoveGeneratorInternal for ClassicalMoveGenerator { #[cfg(test)] mod tests { use super::*; - use crate::{position, position::DiagramFormatter}; + use crate::{position::DiagramFormatter, test_position}; use chessfriend_bitboard::{bitboard, BitBoard}; use chessfriend_core::Color; #[test] fn classical_single_rook_bitboard() { - let pos = position![White Rook on A2]; - println!("{}", DiagramFormatter::new(&pos)); + let pos = test_position![White Rook on A2]; let generator = ClassicalMoveGenerator::new(&pos, Color::White); @@ -80,7 +79,7 @@ mod tests { /// Test that a rook can move up to, but not onto, a friendly piece. #[test] fn classical_single_rook_with_same_color_blocker_bitboard() { - let mut pos = position![ + let pos = test_position![ White Rook on A1, White Knight on E1, ]; @@ -100,7 +99,7 @@ mod tests { /// Test that a rook can move up to, and then capture, an enemy piece. #[test] fn classical_single_rook_with_opposing_color_blocker_bitboard() { - let pos = position![ + let pos = test_position![ White Rook on A1, Black Knight on E1, ]; @@ -115,7 +114,7 @@ mod tests { #[test] fn classical_single_rook_in_center() { - let pos = position![White Rook on D4]; + let pos = test_position![White Rook on D4]; let generator = ClassicalMoveGenerator::new(&pos, Color::White); From 21c81f237a4f93b64b11971f26c97982938b1d14 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 15:50:31 -0800 Subject: [PATCH 106/423] [core] Implement as_index() for range_bound_struct --- core/src/coordinates.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/src/coordinates.rs b/core/src/coordinates.rs index fd0d25e..7e4be53 100644 --- a/core/src/coordinates.rs +++ b/core/src/coordinates.rs @@ -90,6 +90,10 @@ macro_rules! range_bound_struct { $vis unsafe fn new_unchecked(x: $repr) -> Self { Self(x) } + + $vis fn as_index(&self) -> &$repr { + &self.0 + } } impl Into<$repr> for $type { From 0f664f6c801058a45bf7e6750d91f1cb69b1a502 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 15:51:24 -0800 Subject: [PATCH 107/423] [bitboard] Implement BitBoard::as_bits; let rank and file take &u8 instead of usize --- bitboard/src/bitboard.rs | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index fa91c16..6bc3f38 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -22,18 +22,18 @@ impl BitBoard { BitBoard(0) } - pub fn new(bits: u64) -> BitBoard { + pub const fn new(bits: u64) -> BitBoard { BitBoard(bits) } - pub fn rank(rank: usize) -> BitBoard { - assert!(rank < 8); - RANKS[rank] + pub fn rank(rank: &u8) -> BitBoard { + debug_assert!(*rank < 8); + RANKS[*rank as usize] } - pub fn file(file: usize) -> BitBoard { - assert!(file < 8); - FILES[file] + pub fn file(file: &u8) -> BitBoard { + debug_assert!(*file < 8); + FILES[*file as usize] } pub fn ray(sq: Square, dir: Direction) -> BitBoard { @@ -56,6 +56,10 @@ impl BitBoard { } impl BitBoard { + pub fn as_bits(&self) -> &u64 { + &self.0 + } + pub fn is_empty(&self) -> bool { self.0 == 0 } @@ -255,21 +259,22 @@ mod tests { use chessfriend_core::Square; #[test] + #[ignore] fn display_and_debug() { - let bb = BitBoard::file(0) | BitBoard::file(3) | BitBoard::rank(7) | BitBoard::rank(4); + let bb = BitBoard::file(&0) | BitBoard::file(&3) | BitBoard::rank(&7) | BitBoard::rank(&4); println!("{}", &bb); } #[test] fn rank() { - assert_eq!(BitBoard::rank(0).0, 0xFF, "Rank 1"); - assert_eq!(BitBoard::rank(1).0, 0xFF00, "Rank 2"); - assert_eq!(BitBoard::rank(2).0, 0xFF0000, "Rank 3"); - assert_eq!(BitBoard::rank(3).0, 0xFF000000, "Rank 4"); - assert_eq!(BitBoard::rank(4).0, 0xFF00000000, "Rank 5"); - assert_eq!(BitBoard::rank(5).0, 0xFF0000000000, "Rank 6"); - assert_eq!(BitBoard::rank(6).0, 0xFF000000000000, "Rank 7"); - assert_eq!(BitBoard::rank(7).0, 0xFF00000000000000, "Rank 8"); + assert_eq!(BitBoard::rank(&0).0, 0xFF, "Rank 1"); + assert_eq!(BitBoard::rank(&1).0, 0xFF00, "Rank 2"); + assert_eq!(BitBoard::rank(&2).0, 0xFF0000, "Rank 3"); + assert_eq!(BitBoard::rank(&3).0, 0xFF000000, "Rank 4"); + assert_eq!(BitBoard::rank(&4).0, 0xFF00000000, "Rank 5"); + assert_eq!(BitBoard::rank(&5).0, 0xFF0000000000, "Rank 6"); + assert_eq!(BitBoard::rank(&6).0, 0xFF000000000000, "Rank 7"); + assert_eq!(BitBoard::rank(&7).0, 0xFF00000000000000, "Rank 8"); } #[test] From cb48413ce7da82592d6f0b67f495f4bbcd997e7b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 15:51:57 -0800 Subject: [PATCH 108/423] [bitboard] Implement BitBoard::is_single_square() Returns true if there's only one square set in the bitboard. --- bitboard/src/bitboard.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 6bc3f38..10620e5 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -78,6 +78,10 @@ impl BitBoard { let sq_bb: BitBoard = sq.into(); *self &= !sq_bb } + + pub fn is_single_square(&self) -> bool { + self.0.is_power_of_two() + } } impl BitBoard { From 1f78d4811acff510db02a662203b52b686c3d7a8 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 15:52:41 -0800 Subject: [PATCH 109/423] [core] Declare Rank::PAWN_STARTING_RANKS This is a slice that declares the pawn starting ranks for each color. --- core/src/coordinates.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/src/coordinates.rs b/core/src/coordinates.rs index 7e4be53..f40ccb1 100644 --- a/core/src/coordinates.rs +++ b/core/src/coordinates.rs @@ -159,6 +159,15 @@ impl Rank { Rank::SEVEN, Rank::EIGHT, ]; + + /// Ranks on which pawns start, by color. + /// + /// ``` + /// use chessfriend_core::{Color, Rank}; + /// assert_eq!(Rank::PAWN_STARTING_RANKS[Color::White as usize], Rank::TWO); + /// assert_eq!(Rank::PAWN_STARTING_RANKS[Color::Black as usize], Rank::SEVEN); + /// ``` + pub const PAWN_STARTING_RANKS: [Rank; 2] = [Rank::TWO, Rank::SEVEN]; } #[rustfmt::skip] From c558800385c39345731697be8cb987d5f616aedb Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 15:53:19 -0800 Subject: [PATCH 110/423] [position] Fix a bug in the pawn pushes move generator --- position/src/move_generator/pawn.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/position/src/move_generator/pawn.rs b/position/src/move_generator/pawn.rs index 3d6d5f4..8b9b4cf 100644 --- a/position/src/move_generator/pawn.rs +++ b/position/src/move_generator/pawn.rs @@ -3,7 +3,7 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::{MoveBuilder, Position}; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece, PlacedPiece, Square}; +use chessfriend_core::{Color, Piece, PlacedPiece, Rank, Square}; #[derive(Debug)] struct MoveIterator(usize, usize); @@ -39,8 +39,20 @@ impl MoveGeneratorInternal for PawnMoveGenerator { impl PawnMoveGenerator { fn pushes(position: &Position, piece: PlacedPiece) -> BitBoard { + let square = piece.square(); + let bitboard: BitBoard = square.into(); + let empty_squares = position.empty_squares(); - BitBoard::pawn_pushes(piece.square(), piece.color()) & empty_squares + + let mut moves = bitboard.shift_north_one() & empty_squares; + if !(bitboard + & BitBoard::rank(Rank::PAWN_STARTING_RANKS[piece.color() as usize].as_index())) + .is_empty() + { + moves |= moves.shift_north_one() & empty_squares; + } + + moves } fn attacks(position: &Position, piece: PlacedPiece) -> BitBoard { From d910ff708e17c91302391c3ff2b78c295a24fd85 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 15:43:47 -0800 Subject: [PATCH 111/423] Remove the move list argument from MoveList::quiet_moves and capture_moves Produce an iterator of Moves in MoveList::moves --- position/src/move_generator/move_set.rs | 53 +++++++++++++------------ 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/position/src/move_generator/move_set.rs b/position/src/move_generator/move_set.rs index bfd9f75..fd62e85 100644 --- a/position/src/move_generator/move_set.rs +++ b/position/src/move_generator/move_set.rs @@ -1,10 +1,10 @@ // Eryn Wells -use crate::Move; +use crate::{Move, MoveBuilder}; use chessfriend_bitboard::BitBoard; -use chessfriend_core::PlacedPiece; +use chessfriend_core::{Color, Piece, PlacedPiece, Shape}; -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Default, Eq, PartialEq)] struct BitBoardSet { quiet: BitBoard, captures: BitBoard, @@ -34,10 +34,7 @@ impl MoveSet { pub(super) fn new(piece: PlacedPiece) -> MoveSet { MoveSet { piece, - bitboards: BitBoardSet { - quiet: BitBoard::empty(), - captures: BitBoard::empty(), - }, + bitboards: BitBoardSet::default(), move_lists: MoveListSet { quiet: Vec::new(), captures: Vec::new(), @@ -45,25 +42,13 @@ impl MoveSet { } } - pub(super) fn quiet_moves( - mut self, - bitboard: BitBoard, - move_list: impl Iterator, - ) -> MoveSet { + pub(super) fn quiet_moves(mut self, bitboard: BitBoard) -> MoveSet { self.bitboards.quiet = bitboard; - self.move_lists.quiet = move_list.collect(); - self } - pub(super) fn capture_moves( - mut self, - bitboard: BitBoard, - move_list: impl Iterator, - ) -> MoveSet { + pub(super) fn capture_moves(mut self, bitboard: BitBoard) -> MoveSet { self.bitboards.captures = bitboard; - self.move_lists.captures = move_list.collect(); - self } @@ -72,10 +57,26 @@ impl MoveSet { self.bitboards.captures | self.bitboards.quiet } - pub(super) fn moves(&self) -> impl Iterator { - self.move_lists - .captures - .iter() - .chain(self.move_lists.quiet.iter()) + pub(super) fn moves(&self) -> impl Iterator + '_ { + let piece = self.piece.piece(); + let from_square = self.piece.square(); + + self.bitboards + .quiet + .occupied_squares() + .map(move |to_square| MoveBuilder::new(*piece, from_square, to_square).build()) + .chain( + self.bitboards + .captures + .occupied_squares() + .map(move |to_square| { + MoveBuilder::new(*piece, from_square, to_square) + .capturing(PlacedPiece::new( + Piece::new(Color::White, Shape::Pawn), + to_square, + )) + .build() + }), + ) } } From cd3cb82192568ae3b83747e7398bbc8c1a4b78e8 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 15:47:32 -0800 Subject: [PATCH 112/423] Add an assert_move_list! macro to help with verifying move lists --- position/src/lib.rs | 4 ++++ position/src/tests.rs | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 position/src/tests.rs diff --git a/position/src/lib.rs b/position/src/lib.rs index 015b10b..9c58540 100644 --- a/position/src/lib.rs +++ b/position/src/lib.rs @@ -11,5 +11,9 @@ mod sight; #[macro_use] mod macros; +#[cfg(test)] +#[macro_use] +mod tests; + pub use position::{MoveBuilder as MakeMoveBuilder, Position, PositionBuilder}; pub use r#move::{Castle, MakeMoveError, Move, MoveBuilder}; diff --git a/position/src/tests.rs b/position/src/tests.rs new file mode 100644 index 0000000..8663afa --- /dev/null +++ b/position/src/tests.rs @@ -0,0 +1,19 @@ +// Eryn Wells + +#[macro_export] +macro_rules! assert_move_list { + ($generated:expr, $expected:expr, $position:expr) => { + assert_eq!( + $generated, + $expected, + "Difference: {:?}", + $generated + .symmetric_difference(&$expected) + .map(|mv| format!( + "{}", + $crate::r#move::AlgebraicMoveFormatter::new(mv, &$position) + )) + .collect::>() + ) + }; +} From 5e3ef9d21e52577e38a68fc57bee1dc7fbc0ef4c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 15:48:11 -0800 Subject: [PATCH 113/423] Remove the move lists from bishop, knight, queen, and rook move set construction These are the easy ones. --- position/src/move_generator/bishop.rs | 12 ++++-------- position/src/move_generator/knight.rs | 20 +++++--------------- position/src/move_generator/queen.rs | 14 +++++--------- position/src/move_generator/rook.rs | 12 ++++-------- 4 files changed, 18 insertions(+), 40 deletions(-) diff --git a/position/src/move_generator/bishop.rs b/position/src/move_generator/bishop.rs index 48fd833..3eb1405 100644 --- a/position/src/move_generator/bishop.rs +++ b/position/src/move_generator/bishop.rs @@ -42,16 +42,12 @@ impl MoveGeneratorInternal for ClassicalMoveGenerator { update_moves_with_ray!(SouthEast, occupied_squares); update_moves_with_ray!(SouthWest, occupied_squares); - let quiet_moves_bb = all_moves & (empty_squares | !friendly_pieces); - let capture_moves_bb = all_moves & opposing_pieces; - - let map_to_move = |sq| MoveBuilder::new(*piece, square, sq).build(); - let quiet_moves = quiet_moves_bb.occupied_squares().map(map_to_move); - let capture_moves = capture_moves_bb.occupied_squares().map(map_to_move); + let quiet_moves = all_moves & (empty_squares | !friendly_pieces); + let capture_moves = all_moves & opposing_pieces; MoveSet::new(placed_piece) - .quiet_moves(quiet_moves_bb, quiet_moves) - .capture_moves(capture_moves_bb, capture_moves) + .quiet_moves(quiet_moves) + .capture_moves(capture_moves) } } diff --git a/position/src/move_generator/knight.rs b/position/src/move_generator/knight.rs index 751217a..f2e3fcd 100644 --- a/position/src/move_generator/knight.rs +++ b/position/src/move_generator/knight.rs @@ -17,22 +17,12 @@ impl MoveGeneratorInternal for KnightMoveGenerator { let empty_squares = position.empty_squares(); let knight_moves = BitBoard::knight_moves(placed_piece.square()); - let quiet_moves_bb = knight_moves & empty_squares; - let capture_moves_bb = knight_moves & opposing_pieces; - - let quiet_moves = quiet_moves_bb.occupied_squares().map(|to_sq| { - MoveBuilder::new(*placed_piece.piece(), placed_piece.square(), to_sq).build() - }); - let capture_moves = capture_moves_bb.occupied_squares().map(|to_sq| { - let captured_piece = position.piece_on_square(to_sq).unwrap(); - MoveBuilder::new(*placed_piece.piece(), placed_piece.square(), to_sq) - .capturing(captured_piece) - .build() - }); + let quiet_moves = knight_moves & empty_squares; + let capture_moves = knight_moves & opposing_pieces; MoveSet::new(placed_piece) - .quiet_moves(quiet_moves_bb, quiet_moves) - .capture_moves(capture_moves_bb, capture_moves) + .quiet_moves(quiet_moves) + .capture_moves(capture_moves) } } @@ -72,7 +62,7 @@ mod tests { MoveBuilder::new(piece!(White Knight), Square::E4, Square::F6).build(), ]; - let mut generated_moves: HashSet = generator.iter().cloned().collect(); + let mut generated_moves: HashSet = generator.iter().collect(); for ex_move in expected_moves { assert!( diff --git a/position/src/move_generator/queen.rs b/position/src/move_generator/queen.rs index f38da4a..737686c 100644 --- a/position/src/move_generator/queen.rs +++ b/position/src/move_generator/queen.rs @@ -1,7 +1,7 @@ // Eryn Wells use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use crate::{MoveBuilder, Position}; +use crate::Position; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Direction, Piece, PlacedPiece}; @@ -48,16 +48,12 @@ impl MoveGeneratorInternal for ClassicalMoveGenerator { update_moves_with_ray!(SouthWest, occupied_squares); update_moves_with_ray!(West, occupied_squares); - let quiet_moves_bb = all_moves & (empty_squares | !friendly_pieces); - let capture_moves_bb = all_moves & opposing_pieces; - - let map_to_move = |sq| MoveBuilder::new(*piece, square, sq).build(); - let quiet_moves = quiet_moves_bb.occupied_squares().map(map_to_move); - let capture_moves = capture_moves_bb.occupied_squares().map(map_to_move); + let quiet_moves = all_moves & (empty_squares | !friendly_pieces); + let capture_moves = all_moves & opposing_pieces; MoveSet::new(placed_piece) - .quiet_moves(quiet_moves_bb, quiet_moves) - .capture_moves(capture_moves_bb, capture_moves) + .quiet_moves(quiet_moves) + .capture_moves(capture_moves) } } diff --git a/position/src/move_generator/rook.rs b/position/src/move_generator/rook.rs index 71e17e7..4d74c11 100644 --- a/position/src/move_generator/rook.rs +++ b/position/src/move_generator/rook.rs @@ -44,16 +44,12 @@ impl MoveGeneratorInternal for ClassicalMoveGenerator { update_moves_with_ray!(South, occupied_squares); update_moves_with_ray!(West, occupied_squares); - let quiet_moves_bb = all_moves & (empty_squares | !friendly_pieces); - let capture_moves_bb = all_moves & opposing_pieces; - - let map_to_move = |sq| MoveBuilder::new(*piece, square, sq).build(); - let quiet_moves = quiet_moves_bb.occupied_squares().map(map_to_move); - let capture_moves = capture_moves_bb.occupied_squares().map(map_to_move); + let quiet_moves = all_moves & (empty_squares | !friendly_pieces); + let capture_moves = all_moves & opposing_pieces; MoveSet::new(placed_piece) - .quiet_moves(quiet_moves_bb, quiet_moves) - .capture_moves(capture_moves_bb, capture_moves) + .quiet_moves(quiet_moves) + .capture_moves(capture_moves) } } From 296a57d7ac586783d8d9b052dabafeb0343f18dc Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 15:50:00 -0800 Subject: [PATCH 114/423] Remove move list arguments from king and pawn move set constuction These are harder. --- position/src/move_generator/king.rs | 66 +++++++++-------------------- position/src/move_generator/pawn.rs | 46 +++++++++----------- 2 files changed, 40 insertions(+), 72 deletions(-) diff --git a/position/src/move_generator/king.rs b/position/src/move_generator/king.rs index 1f35af5..bd65151 100644 --- a/position/src/move_generator/king.rs +++ b/position/src/move_generator/king.rs @@ -44,12 +44,17 @@ impl MoveGeneratorInternal for KingMoveGenerator { let color = piece.color(); let square = placed_piece.square(); - let empty_squares = position.empty_squares(); - let opposing_pieces = position.bitboard_for_color(color.other()); + let safe_squares = !position.king_danger(); + let all_king_moves = BitBoard::king_moves(square); - let all_moves = BitBoard::king_moves(square); - let quiet_moves_bb = all_moves & empty_squares; - let capture_moves_bb = all_moves & opposing_pieces; + let empty_squares = position.empty_squares(); + let safe_empty_squares = empty_squares & safe_squares; + + let opposing_pieces = position.bitboard_for_color(color.other()); + let opposing_pieces_on_safe_squares = opposing_pieces & safe_squares; + + let quiet_moves = all_king_moves & safe_empty_squares; + let capture_moves = all_king_moves & opposing_pieces_on_safe_squares; // TODO: Handle checks. Prevent moving a king to a square attacked by a // piece of the opposite color. @@ -66,15 +71,15 @@ impl MoveGeneratorInternal for KingMoveGenerator { let capture_moves = capture_moves_bb.occupied_squares().map(map_to_move); MoveSet::new(placed_piece) - .quiet_moves(quiet_moves_bb, quiet_moves) - .capture_moves(capture_moves_bb, capture_moves) + .quiet_moves(quiet_moves) + .capture_moves(capture_moves) } } #[cfg(test)] mod tests { use super::*; - use crate::{position, r#move::AlgebraicMoveFormatter, PositionBuilder}; + use crate::{assert_move_list, position, r#move::AlgebraicMoveFormatter, PositionBuilder}; use chessfriend_bitboard::bitboard; use chessfriend_core::{piece, Square}; use std::collections::HashSet; @@ -90,7 +95,7 @@ mod tests { bitboard![E5, F5, F4, F3, E3, D3, D4, D5] ); - let expected_moves = [ + let expected_moves: HashSet = HashSet::from_iter([ MoveBuilder::new(piece!(White King), Square::E4, Square::D5).build(), MoveBuilder::new(piece!(White King), Square::E4, Square::E5).build(), MoveBuilder::new(piece!(White King), Square::E4, Square::F5).build(), @@ -99,23 +104,11 @@ mod tests { MoveBuilder::new(piece!(White King), Square::E4, Square::E3).build(), MoveBuilder::new(piece!(White King), Square::E4, Square::D3).build(), MoveBuilder::new(piece!(White King), Square::E4, Square::D4).build(), - ]; + ]); - let mut generated_moves: HashSet = generator.iter().cloned().collect(); + let generated_moves: HashSet = generator.iter().collect(); - for ex_move in expected_moves { - assert!( - generated_moves.remove(&ex_move), - "{:#?} was not generated", - &ex_move - ); - } - - assert!( - generated_moves.is_empty(), - "Moves unexpectedly present: {:#?}", - generated_moves - ); + assert_move_list!(generated_moves, expected_moves, pos); } #[test] @@ -138,7 +131,7 @@ mod tests { MoveBuilder::new(piece!(White King), Square::A1, Square::B2).build(), ]; - let mut generated_moves: HashSet = generator.iter().cloned().collect(); + let mut generated_moves: HashSet = generator.iter().collect(); for ex_move in expected_moves { assert!( @@ -167,27 +160,10 @@ mod tests { assert!(pos.is_king_in_check()); let generator = KingMoveGenerator::new(&pos, Color::Black); - let generated_moves: HashSet = generator.iter().cloned().collect(); + let generated_moves = generator.bitboard(); - let king = piece!(Black King); - let from_square = Square::E7; - let expected_moves = HashSet::from_iter([ - MoveBuilder::new(king, from_square, Square::D6).build(), - MoveBuilder::new(king, from_square, Square::D7).build(), - MoveBuilder::new(king, from_square, Square::D8).build(), - MoveBuilder::new(king, from_square, Square::F6).build(), - MoveBuilder::new(king, from_square, Square::F7).build(), - MoveBuilder::new(king, from_square, Square::F8).build(), - ]); + let expected_moves = bitboard![F8, F7, F6, D6, D7, D8]; - assert_eq!( - generated_moves, - expected_moves, - "Difference: {:?}", - generated_moves - .symmetric_difference(&expected_moves) - .map(|mv| format!("{}", AlgebraicMoveFormatter::new(mv, &pos))) - .collect::>() - ); + assert_eq!(generated_moves, expected_moves); } } diff --git a/position/src/move_generator/pawn.rs b/position/src/move_generator/pawn.rs index 8b9b4cf..401366e 100644 --- a/position/src/move_generator/pawn.rs +++ b/position/src/move_generator/pawn.rs @@ -1,7 +1,7 @@ // Eryn Wells use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use crate::{MoveBuilder, Position}; +use crate::Position; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece, PlacedPiece, Rank, Square}; @@ -16,24 +16,12 @@ impl MoveGeneratorInternal for PawnMoveGenerator { } fn move_set_for_piece(position: &Position, placed_piece: PlacedPiece) -> MoveSet { - let from_square = placed_piece.square(); - - let captures_bitboard = Self::attacks(position, placed_piece); - let quiet_moves_bitboard = Self::pushes(position, placed_piece); - - let quiet_moves = quiet_moves_bitboard.occupied_squares().map(|to_square| { - MoveBuilder::new(*placed_piece.piece(), from_square, to_square).build() - }); - let capture_moves = captures_bitboard.occupied_squares().map(|to_square| { - let captured_piece = position.piece_on_square(to_square).unwrap(); - MoveBuilder::new(*placed_piece.piece(), from_square, to_square) - .capturing(captured_piece) - .build() - }); + let capture_moves = Self::attacks(position, placed_piece); + let quiet_moves = Self::pushes(position, placed_piece); MoveSet::new(placed_piece) - .quiet_moves(quiet_moves_bitboard, quiet_moves) - .capture_moves(captures_bitboard, capture_moves) + .quiet_moves(quiet_moves) + .capture_moves(capture_moves) } } @@ -71,7 +59,10 @@ impl PawnMoveGenerator { #[cfg(test)] mod tests { use super::*; - use crate::{position::DiagramFormatter, test_position, Move}; + use crate::{ + assert_move_list, position::DiagramFormatter, r#move::AlgebraicMoveFormatter, + test_position, Move, MoveBuilder, + }; use chessfriend_core::{piece, Square}; use std::collections::HashSet; @@ -86,7 +77,7 @@ mod tests { MoveBuilder::new(piece!(White Pawn), Square::E2, Square::E4).build(), ]); - let generated_moves: HashSet = generator.iter().cloned().collect(); + let generated_moves: HashSet = generator.iter().collect(); assert_eq!(generated_moves, expected_moves); } @@ -104,9 +95,9 @@ mod tests { ) .build()]); - let generated_moves: HashSet = generator.iter().cloned().collect(); + let generated_moves: HashSet = generator.iter().collect(); - assert_eq!(generated_moves, expected_moves); + assert_move_list!(generated_moves, expected_moves, pos); } #[test] @@ -127,9 +118,9 @@ mod tests { ) .build()]); - let generated_moves: HashSet = generator.iter().cloned().collect(); + let generated_moves: HashSet = generator.iter().collect(); - assert_eq!(generated_moves, expected_moves); + assert_move_list!(generated_moves, expected_moves, pos); } #[test] @@ -141,9 +132,10 @@ mod tests { let generator = PawnMoveGenerator::new(&pos, Color::White); - let generated_moves: HashSet = generator.iter().cloned().collect(); + let generated_moves: HashSet = generator.iter().collect(); + let expected_moves: HashSet = HashSet::new(); - assert_eq!(generated_moves, HashSet::new()); + assert_move_list!(generated_moves, expected_moves, pos); } #[test] @@ -163,7 +155,7 @@ mod tests { .build()], ); - let generated_moves: HashSet = generator.iter().cloned().collect(); + let generated_moves: HashSet = generator.iter().collect(); assert_eq!(generated_moves, expected_moves); } @@ -188,7 +180,7 @@ mod tests { .build(), ]); - let generated_moves: HashSet = generator.iter().cloned().collect(); + let generated_moves: HashSet = generator.iter().collect(); assert_eq!( generated_moves, expected_moves, From 2d5710ccb10c80f61cc3240c99e7e965dfafde04 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 15:58:36 -0800 Subject: [PATCH 115/423] Clean up Pawn::pushes a little bit --- position/src/move_generator/pawn.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/position/src/move_generator/pawn.rs b/position/src/move_generator/pawn.rs index 401366e..1797603 100644 --- a/position/src/move_generator/pawn.rs +++ b/position/src/move_generator/pawn.rs @@ -30,13 +30,11 @@ impl PawnMoveGenerator { let square = piece.square(); let bitboard: BitBoard = square.into(); - let empty_squares = position.empty_squares(); + let starting_rank = Rank::PAWN_STARTING_RANKS[piece.color() as usize]; + let empty_squares = position.empty_squares(); let mut moves = bitboard.shift_north_one() & empty_squares; - if !(bitboard - & BitBoard::rank(Rank::PAWN_STARTING_RANKS[piece.color() as usize].as_index())) - .is_empty() - { + if !(bitboard & BitBoard::rank(starting_rank.as_index())).is_empty() { moves |= moves.shift_north_one() & empty_squares; } From ea22f7c5c722ded57808e050c1273265e91b4002 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 15:58:47 -0800 Subject: [PATCH 116/423] Clean up some test imports --- position/src/move_generator/pawn.rs | 5 +---- position/src/move_generator/rook.rs | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/position/src/move_generator/pawn.rs b/position/src/move_generator/pawn.rs index 1797603..728ba05 100644 --- a/position/src/move_generator/pawn.rs +++ b/position/src/move_generator/pawn.rs @@ -57,10 +57,7 @@ impl PawnMoveGenerator { #[cfg(test)] mod tests { use super::*; - use crate::{ - assert_move_list, position::DiagramFormatter, r#move::AlgebraicMoveFormatter, - test_position, Move, MoveBuilder, - }; + use crate::{assert_move_list, position::DiagramFormatter, test_position, Move, MoveBuilder}; use chessfriend_core::{piece, Square}; use std::collections::HashSet; diff --git a/position/src/move_generator/rook.rs b/position/src/move_generator/rook.rs index 4d74c11..72f237b 100644 --- a/position/src/move_generator/rook.rs +++ b/position/src/move_generator/rook.rs @@ -1,7 +1,7 @@ // Eryn Wells use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use crate::{MoveBuilder, Position}; +use crate::Position; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Direction, Piece, PlacedPiece}; From 83a4e47e56452b0fc1b0faf8fc6f817c13cdaaad Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 15:59:16 -0800 Subject: [PATCH 117/423] MoveGenerator::iter() returns an iterator of moves-by-value --- position/src/move_generator.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/position/src/move_generator.rs b/position/src/move_generator.rs index 2202104..eef5fcf 100644 --- a/position/src/move_generator.rs +++ b/position/src/move_generator.rs @@ -57,7 +57,7 @@ macro_rules! move_generator_declaration { }; ($name:ident, getters) => { impl $name { - pub(super) fn iter(&self) -> impl Iterator + '_ { + pub(super) fn iter(&self) -> impl Iterator + '_ { self.move_sets.values().map(|set| set.moves()).flatten() } @@ -141,7 +141,6 @@ impl Moves { .chain(self.rook_moves.iter()) .chain(self.queen_moves.iter()) .chain(self.king_moves.iter()) - .cloned() } } From 1d7dada9878cd298e7a283a3a79c4c6bb0cc0971 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 29 Jan 2024 14:44:48 -0800 Subject: [PATCH 118/423] [bitboard, core, position] Implement proper castle move generation Add a field to MoveSet called special that flags special moves that should be generated in the iter() method. This field is a u8. It only tracks castles in the first and second bits (kingside and queenside, respectively). The move iterator chains two maps over Option<()> that produce the kingside and queenside castle moves. With that done, finish the implementation of Position::player_can_castle by adding checks for whether the squares between the rook and king are clear, and that the king would not pass through a check. This is done with BitBoards! Finally, implement some logic in PositionBuilder that updates the position's castling flags based on the positions of king and rooks. Supporting changes: - Add Color:ALL and iterate on that slice - Add Castle::ALL and iterator on that slice - Add a CastlingParameters struct that contains BitBoard properties that describe squares that should be clear of pieces and squares that should not be attacked. --- core/src/colors.rs | 6 +- position/src/move.rs | 31 ++++ position/src/move_generator/king.rs | 136 ++++++++++++------ position/src/move_generator/move_set.rs | 51 ++++++- .../src/position/builders/position_builder.rs | 26 +++- position/src/position/position.rs | 44 ++++-- 6 files changed, 234 insertions(+), 60 deletions(-) diff --git a/core/src/colors.rs b/core/src/colors.rs index 4e94fed..55757c1 100644 --- a/core/src/colors.rs +++ b/core/src/colors.rs @@ -10,8 +10,10 @@ pub enum Color { } impl Color { - pub fn iter() -> impl Iterator { - [Color::White, Color::Black].into_iter() + pub const ALL: [Color; 2] = [Color::White, Color::Black]; + + pub fn iter() -> impl Iterator { + Color::ALL.iter() } pub fn other(&self) -> Color { diff --git a/position/src/move.rs b/position/src/move.rs index 0469e82..7bd7052 100644 --- a/position/src/move.rs +++ b/position/src/move.rs @@ -16,6 +16,7 @@ pub enum MakeMoveError { } mod castle { + use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Square}; #[repr(u16)] @@ -25,12 +26,19 @@ mod castle { QueenSide = 0b11, } + pub(crate) struct CastlingParameters { + clear_squares: BitBoard, + check_squares: BitBoard, + } + pub(crate) struct Squares { pub king: Square, pub rook: Square, } impl Castle { + pub const ALL: [Castle; 2] = [Castle::KingSide, Castle::QueenSide]; + const STARTING_SQUARES: [[Squares; 2]; 2] = [ [ Squares { @@ -91,6 +99,29 @@ mod castle { Castle::QueenSide => 1, } } + + pub(crate) fn parameters(&self) -> CastlingParameters { + match self { + Castle::KingSide => CastlingParameters { + clear_squares: BitBoard::new(0b01100000), + check_squares: BitBoard::new(0b01110000), + }, + Castle::QueenSide => CastlingParameters { + clear_squares: BitBoard::new(0b00001110), + check_squares: BitBoard::new(0b00011100), + }, + } + } + } + + impl CastlingParameters { + pub fn clear_squares(&self) -> &BitBoard { + &self.clear_squares + } + + pub fn check_squares(&self) -> &BitBoard { + &self.check_squares + } } } diff --git a/position/src/move_generator/king.rs b/position/src/move_generator/king.rs index bd65151..307cbc4 100644 --- a/position/src/move_generator/king.rs +++ b/position/src/move_generator/king.rs @@ -12,28 +12,6 @@ move_generator_declaration!(KingMoveGenerator, struct); move_generator_declaration!(KingMoveGenerator, new); move_generator_declaration!(KingMoveGenerator, getters); -impl KingMoveGenerator { - #[allow(unused_variables)] - fn king_side_castle(position: &Position, color: Color) -> Option { - if !position.player_has_right_to_castle(color, Castle::KingSide) { - return None; - } - - // TODO: Implement king side castle. - None - } - - #[allow(unused_variables)] - fn queen_side_castle(position: &Position, color: Color) -> Option { - if !position.player_has_right_to_castle(color, Castle::QueenSide) { - return None; - } - - // TODO: Implement queen side castle. - None - } -} - impl MoveGeneratorInternal for KingMoveGenerator { fn piece(color: Color) -> Piece { Piece::king(color) @@ -44,7 +22,7 @@ impl MoveGeneratorInternal for KingMoveGenerator { let color = piece.color(); let square = placed_piece.square(); - let safe_squares = !position.king_danger(); + let safe_squares = !position.king_danger(color); let all_king_moves = BitBoard::king_moves(square); let empty_squares = position.empty_squares(); @@ -56,30 +34,25 @@ impl MoveGeneratorInternal for KingMoveGenerator { let quiet_moves = all_king_moves & safe_empty_squares; let capture_moves = all_king_moves & opposing_pieces_on_safe_squares; - // TODO: Handle checks. Prevent moving a king to a square attacked by a - // piece of the opposite color. - - let map_to_move = |sq| MoveBuilder::new(*piece, square, sq).build(); - - let king_side_castle = Self::king_side_castle(position, color); - let queen_side_castle = Self::queen_side_castle(position, color); - let quiet_moves = quiet_moves_bb - .occupied_squares() - .map(map_to_move) - .chain(king_side_castle.iter().cloned()) - .chain(queen_side_castle.iter().cloned()); - let capture_moves = capture_moves_bb.occupied_squares().map(map_to_move); - - MoveSet::new(placed_piece) + let mut move_set = MoveSet::new(placed_piece) .quiet_moves(quiet_moves) - .capture_moves(capture_moves) + .capture_moves(capture_moves); + + if position.player_can_castle(color, Castle::KingSide) { + move_set.kingside_castle(); + } + if position.player_can_castle(color, Castle::QueenSide) { + move_set.queenside_castle(); + } + + move_set } } #[cfg(test)] mod tests { use super::*; - use crate::{assert_move_list, position, r#move::AlgebraicMoveFormatter, PositionBuilder}; + use crate::{assert_move_list, position, test_position, PositionBuilder}; use chessfriend_bitboard::bitboard; use chessfriend_core::{piece, Square}; use std::collections::HashSet; @@ -166,4 +139,87 @@ mod tests { assert_eq!(generated_moves, expected_moves); } + + #[test] + fn white_king_unobstructed_castles() { + let pos = test_position!( + White King on E1, + White Rook on A1, + White Rook on H1, + ); + + assert!(pos.player_can_castle(Color::White, Castle::KingSide)); + assert!(pos.player_can_castle(Color::White, Castle::QueenSide)); + + let generator = KingMoveGenerator::new(&pos, Color::White); + let generated_moves: HashSet = generator.iter().collect(); + + let king = piece!(White King); + assert!(generated_moves.contains( + &MoveBuilder::new(king, Square::E1, Square::G1) + .castle(Castle::KingSide) + .build() + )); + assert!(generated_moves.contains( + &MoveBuilder::new(king, Square::E1, Square::C1) + .castle(Castle::QueenSide) + .build() + )); + } + + #[test] + fn white_king_obstructed_queenside_castle() { + let pos = test_position!( + White King on E1, + White Knight on B1, + White Rook on A1, + White Rook on H1, + ); + + assert!(pos.player_can_castle(Color::White, Castle::KingSide)); + assert!(!pos.player_can_castle(Color::White, Castle::QueenSide)); + + let generator = KingMoveGenerator::new(&pos, Color::White); + let generated_moves: HashSet = generator.iter().collect(); + + let king = piece!(White King); + assert!(generated_moves.contains( + &MoveBuilder::new(king, Square::E1, Square::G1) + .castle(Castle::KingSide) + .build() + )); + assert!(!generated_moves.contains( + &MoveBuilder::new(king, Square::E1, Square::C1) + .castle(Castle::QueenSide) + .build() + )); + } + + #[test] + fn white_king_obstructed_kingside_castle() { + let pos = test_position!( + White King on E1, + White Rook on A1, + White Knight on G1, + White Rook on H1, + ); + + assert!(!pos.player_can_castle(Color::White, Castle::KingSide)); + assert!(pos.player_can_castle(Color::White, Castle::QueenSide)); + + let generator = KingMoveGenerator::new(&pos, Color::White); + let generated_moves: HashSet = generator.iter().collect(); + + let king = piece!(White King); + assert!(!generated_moves.contains( + &MoveBuilder::new(king, Square::E1, Square::G1) + .castle(Castle::KingSide) + .build() + )); + assert!(generated_moves.contains( + &MoveBuilder::new(king, Square::E1, Square::C1) + .castle(Castle::QueenSide) + .build() + )); + } } diff --git a/position/src/move_generator/move_set.rs b/position/src/move_generator/move_set.rs index fd62e85..cf405f1 100644 --- a/position/src/move_generator/move_set.rs +++ b/position/src/move_generator/move_set.rs @@ -1,6 +1,6 @@ // Eryn Wells -use crate::{Move, MoveBuilder}; +use crate::{r#move::Castle, Move, MoveBuilder}; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece, PlacedPiece, Shape}; @@ -27,7 +27,7 @@ impl MoveListSet { pub(crate) struct MoveSet { piece: PlacedPiece, bitboards: BitBoardSet, - move_lists: MoveListSet, + special: u8, } impl MoveSet { @@ -35,10 +35,7 @@ impl MoveSet { MoveSet { piece, bitboards: BitBoardSet::default(), - move_lists: MoveListSet { - quiet: Vec::new(), - captures: Vec::new(), - }, + special: 0, } } @@ -52,6 +49,16 @@ impl MoveSet { self } + pub(super) fn kingside_castle(&mut self) -> &mut MoveSet { + self.special |= 0b1; + self + } + + pub(super) fn queenside_castle(&mut self) -> &mut MoveSet { + self.special |= 0b10; + self + } + /// Return a BitBoard representing all possible moves. pub(super) fn bitboard(&self) -> BitBoard { self.bitboards.captures | self.bitboards.quiet @@ -78,5 +85,37 @@ impl MoveSet { .build() }), ) + .chain( + if (self.special & 0b1) != 0 { + Some(()) + } else { + None + } + .map(|()| { + MoveBuilder::new( + *piece, + from_square, + Castle::KingSide.target_squares(piece.color()).king, + ) + .castle(Castle::KingSide) + .build() + }), + ) + .chain( + if (self.special & 0b10) != 0 { + Some(()) + } else { + None + } + .map(|()| { + MoveBuilder::new( + *piece, + from_square, + Castle::QueenSide.target_squares(piece.color()).king, + ) + .castle(Castle::QueenSide) + .build() + }), + ) } } diff --git a/position/src/position/builders/position_builder.rs b/position/src/position/builders/position_builder.rs index 369014c..5b0a4e0 100644 --- a/position/src/position/builders/position_builder.rs +++ b/position/src/position/builders/position_builder.rs @@ -61,9 +61,11 @@ impl Builder { pub fn place_piece(&mut self, piece: PlacedPiece) -> &mut Self { let square = piece.square(); + let shape = piece.shape(); - if piece.shape() == Shape::King { - let color_index: usize = piece.color() as usize; + if shape == Shape::King { + let color = piece.color(); + let color_index: usize = color as usize; self.pieces.remove(&self.kings[color_index]); self.kings[color_index] = square; } @@ -81,9 +83,27 @@ impl Builder { .filter(Self::is_piece_placement_valid), ); + let mut flags = self.flags; + + for color in Color::ALL { + for castle in Castle::ALL { + let starting_squares = castle.starting_squares(color); + let has_rook_on_starting_square = self + .pieces + .get(&starting_squares.rook) + .is_some_and(|piece| piece.shape() == Shape::Rook); + let king_is_on_starting_square = + self.kings[color as usize] == starting_squares.king; + + if !king_is_on_starting_square || !has_rook_on_starting_square { + flags.clear_player_has_right_to_castle_flag(color, castle); + } + } + } + Position::new( self.player_to_move, - self.flags, + flags, pieces, None, self.ply_counter, diff --git a/position/src/position/position.rs b/position/src/position/position.rs index 6a09226..3ee09f1 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -107,7 +107,17 @@ impl Position { return false; } - // TODO: Perform a real check that the player can castle. + let castling_parameters = castle.parameters(); + + let all_pieces = self.occupied_squares(); + if !(all_pieces & castling_parameters.clear_squares()).is_empty() { + return false; + } + + let danger_squares = self.king_danger(player); + if !(danger_squares & castling_parameters.check_squares()).is_empty() { + return false; + } true } @@ -159,7 +169,7 @@ impl Position { pub fn piece_on_square(&self, sq: Square) -> Option { for color in Color::iter() { for shape in Shape::iter() { - let piece = Piece::new(color, *shape); + let piece = Piece::new(*color, *shape); if self.pieces.bitboard_for_piece(&piece).is_set(sq) { return Some(PlacedPiece::new(piece, sq)); } @@ -212,19 +222,16 @@ impl Position { /// A bitboard representing the squares where a king of the given color will /// be in danger. The king cannot move to these squares. - pub(crate) fn king_danger(&self) -> BitBoard { + pub(crate) fn king_danger(&self, color: Color) -> BitBoard { let pieces_without_king = { let mut cloned_pieces = self.pieces.clone(); - let placed_king = PlacedPiece::new( - Piece::king(self.color_to_move), - self.king_square(self.color_to_move), - ); + let placed_king = PlacedPiece::new(Piece::king(color), self.king_square(color)); cloned_pieces.remove_piece(&placed_king); cloned_pieces }; - self._sight_of_player(self.color_to_move.other(), &pieces_without_king) + self._sight_of_player(color.other(), &pieces_without_king) } pub(crate) fn is_king_in_check(&self) -> bool { @@ -363,6 +370,25 @@ mod tests { assert!(!pos.is_king_in_check()); } + #[test] + fn king_not_on_starting_square_cannot_castle() { + let pos = test_position!(White King on E4); + assert!(!pos.player_can_castle(Color::White, Castle::KingSide)); + assert!(!pos.player_can_castle(Color::White, Castle::QueenSide)); + } + + #[test] + fn king_on_starting_square_can_castle() { + let pos = test_position!( + White King on E1, + White Rook on A1, + White Rook on H1 + ); + + assert!(pos.player_can_castle(Color::White, Castle::KingSide)); + assert!(pos.player_can_castle(Color::White, Castle::QueenSide)); + } + #[test] fn rook_for_castle() { let pos = position![ @@ -390,7 +416,7 @@ mod tests { .to_move(Color::Black) .build(); - let danger_squares = pos.king_danger(); + let danger_squares = pos.king_danger(Color::Black); let expected = bitboard![D1, F1, D2, E2, F2, E3, A4, B4, C4, D4, F4, G4, H4, E5, E6, E7, E8]; assert_eq!( From 3239f288d773047d6b18b7b787e39fe858b757cc Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 29 Jan 2024 15:00:53 -0800 Subject: [PATCH 119/423] [bitboard] Bitboards for kingside and queenside per color Add two small BitBoard slices that represent kingside and queenside squares per color. Add doc comments to DARK_SQUARES and LIGHT_SQUARES. Add getters on BitBoard for getting a boardside bitboard. Clean up imports. Import the whole library module and refer to library things in BitBoard by path. --- bitboard/src/bitboard.rs | 22 +++++++++++++++------- bitboard/src/library.rs | 15 +++++++++++++-- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 10620e5..b2e1a4c 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -1,6 +1,6 @@ // Eryn Wells -use crate::library::{library, FILES, RANKS}; +use crate::library; use crate::LeadingBitScanner; use chessfriend_core::{Color, Direction, Square}; use std::fmt; @@ -12,7 +12,7 @@ pub struct BitBoard(pub(crate) u64); macro_rules! moves_getter { ($getter_name:ident) => { pub fn $getter_name(sq: Square) -> BitBoard { - library().$getter_name(sq) + library::library().$getter_name(sq) } }; } @@ -28,24 +28,24 @@ impl BitBoard { pub fn rank(rank: &u8) -> BitBoard { debug_assert!(*rank < 8); - RANKS[*rank as usize] + library::RANKS[*rank as usize] } pub fn file(file: &u8) -> BitBoard { debug_assert!(*file < 8); - FILES[*file as usize] + library::FILES[*file as usize] } pub fn ray(sq: Square, dir: Direction) -> BitBoard { - library().ray(sq, dir) + library::library().ray(sq, dir) } pub fn pawn_attacks(sq: Square, color: Color) -> BitBoard { - library().pawn_attacks(sq, color) + library::library().pawn_attacks(sq, color) } pub fn pawn_pushes(sq: Square, color: Color) -> BitBoard { - library().pawn_pushes(sq, color) + library::library().pawn_pushes(sq, color) } moves_getter!(knight_moves); @@ -53,6 +53,14 @@ impl BitBoard { moves_getter!(rook_moves); moves_getter!(queen_moves); moves_getter!(king_moves); + + pub const fn kingside(color: Color) -> &'static BitBoard { + &library::KINGSIDES[color as usize] + } + + pub const fn queenside(color: Color) -> &'static BitBoard { + &library::QUEENSIDES[color as usize] + } } impl BitBoard { diff --git a/bitboard/src/library.rs b/bitboard/src/library.rs index 6e51f24..41951c7 100644 --- a/bitboard/src/library.rs +++ b/bitboard/src/library.rs @@ -26,9 +26,20 @@ pub(super) const FILES: [BitBoard; 8] = [ BitBoard(0x0101010101010101 << 7), ]; -pub(super) const LIGHT_SQUARES: BitBoard = +/// Bitboards representing the kingside of the board, per color. +pub(crate) const KINGSIDES: [BitBoard; 2] = + [BitBoard(0xF0F0F0F0F0F0F0F0), BitBoard(0x0F0F0F0F0F0F0F0F)]; + +/// Bitboards representing the queenside of the board, per color. +pub(crate) const QUEENSIDES: [BitBoard; 2] = + [BitBoard(0x0F0F0F0F0F0F0F0F), BitBoard(0xF0F0F0F0F0F0F0F0)]; + +/// A bitboard representing the light squares. +pub(crate) const LIGHT_SQUARES: BitBoard = BitBoard(0x5555 | 0x5555 << 16 | 0x5555 << 32 | 0x5555 << 48); -pub(super) const DARK_SQUARES: BitBoard = BitBoard(!LIGHT_SQUARES.0); + +/// A bitboad representing the dark squares +pub(crate) const DARK_SQUARES: BitBoard = BitBoard(!LIGHT_SQUARES.0); pub(super) fn library() -> &'static MoveLibrary { static MOVE_LIBRARY_INIT: Once = Once::new(); From 52b19b87d8d9f6c3a0a460535e5b28726ff8cf67 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 29 Jan 2024 16:10:08 -0800 Subject: [PATCH 120/423] [position] Implement FromFen for Position, Piece, and Color I can now create Positions from FEN strings! --- position/src/fen.rs | 143 +++++++++++++++++- position/src/move.rs | 1 + .../src/position/builders/position_builder.rs | 48 +++++- position/src/position/flags.rs | 4 + position/src/position/position.rs | 4 +- 5 files changed, 187 insertions(+), 13 deletions(-) diff --git a/position/src/fen.rs b/position/src/fen.rs index f21de15..fd7b530 100644 --- a/position/src/fen.rs +++ b/position/src/fen.rs @@ -1,7 +1,7 @@ // Eryn Wells -use crate::{r#move::Castle, Position}; -use chessfriend_core::{Color, File, Piece, PlacedPiece, Rank, Square}; +use crate::{r#move::Castle, Position, PositionBuilder}; +use chessfriend_core::{piece, Color, File, Piece, PlacedPiece, Rank, Square}; use std::fmt::Write; #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -9,11 +9,19 @@ pub enum ToFenError { FmtError(std::fmt::Error), } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct FromFenError; + pub trait ToFen { type Error; fn to_fen(&self) -> Result; } +pub trait FromFen: Sized { + type Error; + fn from_fen_str(string: &str) -> Result; +} + impl ToFen for Position { type Error = ToFenError; @@ -127,14 +135,131 @@ impl ToFen for PlacedPiece { } } +impl FromFen for Position { + type Error = FromFenError; + + fn from_fen_str(string: &str) -> Result { + let mut builder = PositionBuilder::empty(); + + let mut fields = string.split(" "); + + let placements = fields.next().ok_or(FromFenError)?; + let ranks = placements.split("/"); + + for (rank, pieces) in Rank::ALL.iter().rev().zip(ranks) { + let mut files = File::ALL.iter(); + for ch in pieces.chars() { + if let Some(skip) = ch.to_digit(10) { + // TODO: Use advance_by() when it's available. + for _ in 0..skip { + files.next(); + } + + continue; + } + + let file = files.next().ok_or(FromFenError)?; + let piece = Piece::from_fen_str(&ch.to_string())?; + + builder.place_piece(PlacedPiece::new( + piece, + Square::from_file_rank(*file, *rank), + )); + } + + debug_assert_eq!(files.next(), None); + } + + let player_to_move = Color::from_fen_str(fields.next().ok_or(FromFenError)?)?; + builder.to_move(player_to_move); + + let castling_rights = fields.next().ok_or(FromFenError)?; + if castling_rights == "-" { + builder.no_castling_rights(); + } else { + for ch in castling_rights.chars() { + match ch { + 'K' => builder.player_can_castle(Color::White, Castle::KingSide), + 'Q' => builder.player_can_castle(Color::White, Castle::QueenSide), + 'k' => builder.player_can_castle(Color::Black, Castle::KingSide), + 'q' => builder.player_can_castle(Color::Black, Castle::QueenSide), + _ => return Err(FromFenError), + }; + } + } + + let en_passant_square = fields.next().ok_or(FromFenError)?; + if en_passant_square != "-" { + let square = Square::from_algebraic_str(en_passant_square).map_err(|_| FromFenError)?; + builder.en_passant_square(Some(square)); + } + + let half_move_clock = fields.next().ok_or(FromFenError)?; + let half_move_clock: u16 = half_move_clock.parse().map_err(|_| FromFenError)?; + builder.ply_counter(half_move_clock); + + let full_move_counter = fields.next().ok_or(FromFenError)?; + let full_move_counter: u16 = full_move_counter.parse().map_err(|_| FromFenError)?; + builder.move_number(full_move_counter); + + debug_assert_eq!(fields.next(), None); + + Ok(builder.build()) + } +} + +impl FromFen for Color { + type Error = FromFenError; + + fn from_fen_str(string: &str) -> Result { + if string.len() != 1 { + return Err(FromFenError); + } + + match string.chars().take(1).next().unwrap() { + 'w' => Ok(Color::White), + 'b' => Ok(Color::Black), + _ => Err(FromFenError), + } + } +} + +impl FromFen for Piece { + type Error = FromFenError; + + fn from_fen_str(string: &str) -> Result { + if string.len() != 1 { + return Err(FromFenError); + } + + match string.chars().take(1).next().unwrap() { + 'P' => Ok(piece!(White Pawn)), + 'N' => Ok(piece!(White Knight)), + 'B' => Ok(piece!(White Bishop)), + 'R' => Ok(piece!(White Rook)), + 'Q' => Ok(piece!(White Queen)), + 'K' => Ok(piece!(White King)), + 'p' => Ok(piece!(Black Pawn)), + 'n' => Ok(piece!(Black Knight)), + 'b' => Ok(piece!(Black Bishop)), + 'r' => Ok(piece!(Black Rook)), + 'q' => Ok(piece!(Black Queen)), + 'k' => Ok(piece!(Black King)), + _ => Err(FromFenError), + } + } +} + #[cfg(test)] mod tests { + use crate::test_position; + use super::*; #[test] fn starting_position() { - let pos = Position::starting(); - println!("{pos:#?}"); + let pos = test_position!(starting); + assert_eq!( pos.to_fen(), Ok(String::from( @@ -142,4 +267,14 @@ mod tests { )) ); } + + #[test] + fn from_starting_fen() { + let pos = + Position::from_fen_str("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") + .unwrap(); + let expected = Position::starting(); + + assert_eq!(pos, expected, "{pos:#?}\n{expected:#?}"); + } } diff --git a/position/src/move.rs b/position/src/move.rs index 7bd7052..bf6dc3c 100644 --- a/position/src/move.rs +++ b/position/src/move.rs @@ -31,6 +31,7 @@ mod castle { check_squares: BitBoard, } + #[derive(Debug)] pub(crate) struct Squares { pub king: Square, pub rook: Square, diff --git a/position/src/position/builders/position_builder.rs b/position/src/position/builders/position_builder.rs index 5b0a4e0..f81ac01 100644 --- a/position/src/position/builders/position_builder.rs +++ b/position/src/position/builders/position_builder.rs @@ -13,7 +13,8 @@ pub struct Builder { player_to_move: Color, flags: Flags, pieces: BTreeMap, - kings: [Square; 2], + kings: [Option; 2], + en_passant_square: Option, ply_counter: u16, move_number: u16, } @@ -23,6 +24,18 @@ impl Builder { Self::default() } + pub(crate) fn empty() -> Self { + Self { + player_to_move: Color::default(), + flags: Flags::default(), + pieces: BTreeMap::default(), + kings: [None, None], + en_passant_square: None, + ply_counter: 0, + move_number: 1, + } + } + pub fn from_position(position: &Position) -> Self { let pieces = BTreeMap::from_iter( position @@ -38,7 +51,8 @@ impl Builder { player_to_move: position.player_to_move(), flags: position.flags(), pieces, - kings: [white_king, black_king], + kings: [Some(white_king), Some(black_king)], + en_passant_square: position.en_passant_square(), ply_counter: position.ply_counter(), move_number: position.move_number(), } @@ -59,6 +73,11 @@ impl Builder { self } + pub fn en_passant_square(&mut self, square: Option) -> &mut Self { + self.en_passant_square = square; + self + } + pub fn place_piece(&mut self, piece: PlacedPiece) -> &mut Self { let square = piece.square(); let shape = piece.shape(); @@ -66,8 +85,11 @@ impl Builder { if shape == Shape::King { let color = piece.color(); let color_index: usize = color as usize; - self.pieces.remove(&self.kings[color_index]); - self.kings[color_index] = square; + + if let Some(king_square) = self.kings[color_index] { + self.pieces.remove(&king_square); + } + self.kings[color_index] = Some(square); } self.pieces.insert(square, *piece.piece()); @@ -75,6 +97,17 @@ impl Builder { self } + pub fn player_can_castle(&mut self, color: Color, castle: Castle) -> &mut Self { + self.flags + .set_player_has_right_to_castle_flag(color, castle); + self + } + + pub fn no_castling_rights(&mut self) -> &mut Self { + self.flags.clear_all_castling_rights(); + self + } + pub fn build(&self) -> Position { let pieces = PieceBitBoards::from_iter( self.pieces @@ -93,7 +126,7 @@ impl Builder { .get(&starting_squares.rook) .is_some_and(|piece| piece.shape() == Shape::Rook); let king_is_on_starting_square = - self.kings[color as usize] == starting_squares.king; + self.kings[color as usize] == Some(starting_squares.king); if !king_is_on_starting_square || !has_rook_on_starting_square { flags.clear_player_has_right_to_castle_flag(color, castle); @@ -105,7 +138,7 @@ impl Builder { self.player_to_move, flags, pieces, - None, + self.en_passant_square, self.ply_counter, self.move_number, ) @@ -139,7 +172,8 @@ impl Default for Builder { player_to_move: Color::White, flags: Flags::default(), pieces: pieces, - kings: [white_king_square, black_king_square], + kings: [Some(white_king_square), Some(black_king_square)], + en_passant_square: None, ply_counter: 0, move_number: 1, } diff --git a/position/src/position/flags.rs b/position/src/position/flags.rs index 699a63e..c1fbfbf 100644 --- a/position/src/position/flags.rs +++ b/position/src/position/flags.rs @@ -24,6 +24,10 @@ impl Flags { pub(super) fn clear_player_has_right_to_castle_flag(&mut self, color: Color, castle: Castle) { self.0 &= !(1 << Self::player_has_right_to_castle_flag_offset(color, castle)); } + + pub(super) fn clear_all_castling_rights(&mut self) { + self.0 &= 0b11111100; + } } impl fmt::Debug for Flags { diff --git a/position/src/position/position.rs b/position/src/position/position.rs index 3ee09f1..0443929 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -269,8 +269,8 @@ impl Position { pieces, sight: [OnceCell::new(), OnceCell::new()], moves: OnceCell::new(), - half_move_counter: 0, - full_move_number: 1, + half_move_counter, + full_move_number, } } From 8aa44e56f26af293f9074822c55c73fb6831754e Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 29 Jan 2024 17:12:08 -0800 Subject: [PATCH 121/423] [position] Fix a few warnings related to imports; make position::Flags public --- position/src/move_generator/king.rs | 4 ++-- position/src/move_generator/knight.rs | 4 ++-- position/src/position/builders/move_builder.rs | 4 +--- position/src/position/builders/position_builder.rs | 2 +- position/src/position/flags.rs | 2 +- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/position/src/move_generator/king.rs b/position/src/move_generator/king.rs index 307cbc4..5867876 100644 --- a/position/src/move_generator/king.rs +++ b/position/src/move_generator/king.rs @@ -4,7 +4,7 @@ //! generating the possible moves for the king in the given position. use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use crate::{r#move::Castle, Move, MoveBuilder, Position}; +use crate::{r#move::Castle, Position}; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece, PlacedPiece}; @@ -52,7 +52,7 @@ impl MoveGeneratorInternal for KingMoveGenerator { #[cfg(test)] mod tests { use super::*; - use crate::{assert_move_list, position, test_position, PositionBuilder}; + use crate::{assert_move_list, position, test_position, Move, MoveBuilder, PositionBuilder}; use chessfriend_bitboard::bitboard; use chessfriend_core::{piece, Square}; use std::collections::HashSet; diff --git a/position/src/move_generator/knight.rs b/position/src/move_generator/knight.rs index f2e3fcd..aa2a7af 100644 --- a/position/src/move_generator/knight.rs +++ b/position/src/move_generator/knight.rs @@ -1,7 +1,7 @@ // Eryn Wells use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use crate::{MoveBuilder, Position}; +use crate::Position; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece, PlacedPiece}; @@ -29,7 +29,7 @@ impl MoveGeneratorInternal for KnightMoveGenerator { #[cfg(test)] mod tests { use super::*; - use crate::{position, Move}; + use crate::{position, Move, MoveBuilder}; use chessfriend_core::{piece, Square}; use std::collections::HashSet; diff --git a/position/src/position/builders/move_builder.rs b/position/src/position/builders/move_builder.rs index 23229e7..add0bb1 100644 --- a/position/src/position/builders/move_builder.rs +++ b/position/src/position/builders/move_builder.rs @@ -1,8 +1,6 @@ // Eryn Wells -use crate::{ - position::flags::Flags, r#move::Castle, sight::SightExt, MakeMoveError, Move, Position, -}; +use crate::{position::flags::Flags, r#move::Castle, MakeMoveError, Move, Position}; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Direction, Piece, PlacedPiece, Shape, Square}; diff --git a/position/src/position/builders/position_builder.rs b/position/src/position/builders/position_builder.rs index f81ac01..5011f21 100644 --- a/position/src/position/builders/position_builder.rs +++ b/position/src/position/builders/position_builder.rs @@ -3,7 +3,7 @@ use crate::{ position::{flags::Flags, piece_sets::PieceBitBoards}, r#move::Castle, - MakeMoveError, Move, Position, + Position, }; use chessfriend_core::{piece, Color, Piece, PlacedPiece, Rank, Shape, Square}; use std::collections::BTreeMap; diff --git a/position/src/position/flags.rs b/position/src/position/flags.rs index c1fbfbf..47e49bf 100644 --- a/position/src/position/flags.rs +++ b/position/src/position/flags.rs @@ -5,7 +5,7 @@ use chessfriend_core::Color; use std::fmt; #[derive(Clone, Copy, Eq, Hash, PartialEq)] -pub(super) struct Flags(u8); +pub struct Flags(u8); impl Flags { #[inline] From 9c4360c88684f888ae754f666b8023832930888a Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 29 Jan 2024 19:02:02 -0800 Subject: [PATCH 122/423] [position] Remove move_generator::MoveListSet --- position/src/move_generator/move_set.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/position/src/move_generator/move_set.rs b/position/src/move_generator/move_set.rs index cf405f1..f8f3ca1 100644 --- a/position/src/move_generator/move_set.rs +++ b/position/src/move_generator/move_set.rs @@ -10,18 +10,6 @@ struct BitBoardSet { captures: BitBoard, } -#[derive(Clone, Debug, Eq, PartialEq)] -struct MoveListSet { - quiet: Vec, - captures: Vec, -} - -impl MoveListSet { - pub fn contains(&self, mv: &Move) -> bool { - self.quiet.contains(mv) || self.captures.contains(mv) - } -} - /// A set of moves for a piece on the board. #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct MoveSet { From b93f8684fa97ec15e74241d9555aead5471fff5b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 29 Jan 2024 20:11:34 -0800 Subject: [PATCH 123/423] [position] Plumb capture mask and push mask arguments through all move generators These masks will help when generating moves that address checks. Create BitBoard::EMPTY and BitBoard::FULL. --- bitboard/src/bitboard.rs | 3 ++ position/src/move_generator.rs | 47 +++++++++++++++++++-------- position/src/move_generator/bishop.rs | 19 ++++++++--- position/src/move_generator/king.rs | 19 +++++++---- position/src/move_generator/knight.rs | 10 ++++-- position/src/move_generator/pawn.rs | 19 +++++++---- position/src/move_generator/queen.rs | 19 ++++++++--- position/src/move_generator/rook.rs | 19 ++++++++--- position/src/position/position.rs | 2 +- 9 files changed, 111 insertions(+), 46 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index b2e1a4c..2f9740d 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -18,6 +18,9 @@ macro_rules! moves_getter { } impl BitBoard { + pub const EMPTY: BitBoard = BitBoard(0); + pub const FULL: BitBoard = BitBoard(0xFFFFFFFFFFFFFFFF); + pub const fn empty() -> BitBoard { BitBoard(0) } diff --git a/position/src/move_generator.rs b/position/src/move_generator.rs index eef5fcf..7e193c1 100644 --- a/position/src/move_generator.rs +++ b/position/src/move_generator.rs @@ -17,6 +17,7 @@ use self::{ rook::ClassicalMoveGenerator as RookMoveGenerator, }; use crate::{Move, Position}; +use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; use std::collections::BTreeMap; @@ -47,10 +48,12 @@ macro_rules! move_generator_declaration { pub(super) fn new( position: &$crate::Position, color: chessfriend_core::Color, + capture_mask: chessfriend_bitboard::BitBoard, + push_mask: chessfriend_bitboard::BitBoard, ) -> $name { $name { color, - move_sets: Self::move_sets(position, color), + move_sets: Self::move_sets(position, color, capture_mask, push_mask), } } } @@ -83,21 +86,32 @@ pub(self) use move_generator_declaration; trait MoveGeneratorInternal { fn piece(color: Color) -> Piece; - fn move_sets(position: &Position, color: Color) -> BTreeMap { + fn move_sets( + position: &Position, + color: Color, + capture_mask: BitBoard, + push_mask: BitBoard, + ) -> BTreeMap { let piece = Self::piece(color); BTreeMap::from_iter( position .bitboard_for_piece(piece) .occupied_squares() - .map(|sq| { - let placed_piece = PlacedPiece::new(piece, sq); - let move_set = Self::move_set_for_piece(position, placed_piece); - (sq, move_set) + .map(|square| { + let piece = PlacedPiece::new(piece, square); + let move_set = + Self::move_set_for_piece(position, piece, capture_mask, push_mask); + (square, move_set) }), ) } - fn move_set_for_piece(position: &Position, placed_piece: PlacedPiece) -> MoveSet; + fn move_set_for_piece( + position: &Position, + piece: PlacedPiece, + capture_mask: BitBoard, + push_mask: BitBoard, + ) -> MoveSet; } #[derive(Clone, Debug, Eq, PartialEq)] @@ -111,14 +125,19 @@ pub struct Moves { } impl Moves { - pub fn new(position: &Position, color: Color) -> Moves { + pub fn new( + position: &Position, + color: Color, + capture_mask: BitBoard, + push_mask: BitBoard, + ) -> Moves { Moves { - pawn_moves: PawnMoveGenerator::new(position, color), - knight_moves: KnightMoveGenerator::new(position, color), - bishop_moves: BishopMoveGenerator::new(position, color), - rook_moves: RookMoveGenerator::new(position, color), - queen_moves: QueenMoveGenerator::new(position, color), - king_moves: KingMoveGenerator::new(position, color), + pawn_moves: PawnMoveGenerator::new(position, color, capture_mask, push_mask), + knight_moves: KnightMoveGenerator::new(position, color, capture_mask, push_mask), + bishop_moves: BishopMoveGenerator::new(position, color, capture_mask, push_mask), + rook_moves: RookMoveGenerator::new(position, color, capture_mask, push_mask), + queen_moves: QueenMoveGenerator::new(position, color, capture_mask, push_mask), + king_moves: KingMoveGenerator::new(position, color, capture_mask, push_mask), } } diff --git a/position/src/move_generator/bishop.rs b/position/src/move_generator/bishop.rs index 3eb1405..ec6a96a 100644 --- a/position/src/move_generator/bishop.rs +++ b/position/src/move_generator/bishop.rs @@ -12,7 +12,12 @@ impl MoveGeneratorInternal for ClassicalMoveGenerator { Piece::bishop(color) } - fn move_set_for_piece(position: &Position, placed_piece: PlacedPiece) -> MoveSet { + fn move_set_for_piece( + position: &Position, + placed_piece: PlacedPiece, + capture_mask: BitBoard, + push_mask: BitBoard, + ) -> MoveSet { let piece = placed_piece.piece(); let square = placed_piece.square(); @@ -64,7 +69,8 @@ mod tests { White Bishop on A1, ]; - let generator = ClassicalMoveGenerator::new(&pos, Color::White); + let generator = + ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( generator.bitboard(), @@ -84,7 +90,8 @@ mod tests { println!("{}", DiagramFormatter::new(&pos)); - let generator = ClassicalMoveGenerator::new(&pos, Color::White); + let generator = + ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( generator.bitboard(), @@ -102,7 +109,8 @@ mod tests { Black Knight on C3, ]; - let generator = ClassicalMoveGenerator::new(&pos, Color::White); + let generator = + ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( generator.bitboard(), @@ -120,7 +128,8 @@ mod tests { println!("{}", DiagramFormatter::new(&pos)); - let generator = ClassicalMoveGenerator::new(&pos, Color::White); + let generator = + ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); let bitboard = generator.bitboard(); let expected = BitBoard::new( 0b00000001_10000010_01000100_00101000_00000000_00101000_01000100_10000010, diff --git a/position/src/move_generator/king.rs b/position/src/move_generator/king.rs index 5867876..7941bf3 100644 --- a/position/src/move_generator/king.rs +++ b/position/src/move_generator/king.rs @@ -17,7 +17,12 @@ impl MoveGeneratorInternal for KingMoveGenerator { Piece::king(color) } - fn move_set_for_piece(position: &Position, placed_piece: PlacedPiece) -> MoveSet { + fn move_set_for_piece( + position: &Position, + placed_piece: PlacedPiece, + capture_mask: BitBoard, + push_mask: BitBoard, + ) -> MoveSet { let piece = placed_piece.piece(); let color = piece.color(); let square = placed_piece.square(); @@ -61,7 +66,7 @@ mod tests { fn one_king() { let pos = position![White King on E4]; - let generator = KingMoveGenerator::new(&pos, Color::White); + let generator = KingMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( generator.bitboard(), @@ -88,7 +93,7 @@ mod tests { fn one_king_corner() { let pos = position![White King on A1]; - let generator = KingMoveGenerator::new(&pos, Color::White); + let generator = KingMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); let generated_bitboard = generator.bitboard(); let expected_bitboard = bitboard![A2, B2, B1]; @@ -132,7 +137,7 @@ mod tests { assert!(pos.is_king_in_check()); - let generator = KingMoveGenerator::new(&pos, Color::Black); + let generator = KingMoveGenerator::new(&pos, Color::Black, BitBoard::FULL, BitBoard::FULL); let generated_moves = generator.bitboard(); let expected_moves = bitboard![F8, F7, F6, D6, D7, D8]; @@ -151,7 +156,7 @@ mod tests { assert!(pos.player_can_castle(Color::White, Castle::KingSide)); assert!(pos.player_can_castle(Color::White, Castle::QueenSide)); - let generator = KingMoveGenerator::new(&pos, Color::White); + let generator = KingMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); let generated_moves: HashSet = generator.iter().collect(); let king = piece!(White King); @@ -179,7 +184,7 @@ mod tests { assert!(pos.player_can_castle(Color::White, Castle::KingSide)); assert!(!pos.player_can_castle(Color::White, Castle::QueenSide)); - let generator = KingMoveGenerator::new(&pos, Color::White); + let generator = KingMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); let generated_moves: HashSet = generator.iter().collect(); let king = piece!(White King); @@ -207,7 +212,7 @@ mod tests { assert!(!pos.player_can_castle(Color::White, Castle::KingSide)); assert!(pos.player_can_castle(Color::White, Castle::QueenSide)); - let generator = KingMoveGenerator::new(&pos, Color::White); + let generator = KingMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); let generated_moves: HashSet = generator.iter().collect(); let king = piece!(White King); diff --git a/position/src/move_generator/knight.rs b/position/src/move_generator/knight.rs index aa2a7af..0f5bca3 100644 --- a/position/src/move_generator/knight.rs +++ b/position/src/move_generator/knight.rs @@ -12,7 +12,12 @@ impl MoveGeneratorInternal for KnightMoveGenerator { Piece::knight(color) } - fn move_set_for_piece(position: &Position, placed_piece: PlacedPiece) -> MoveSet { + fn move_set_for_piece( + position: &Position, + placed_piece: PlacedPiece, + capture_mask: BitBoard, + push_mask: BitBoard, + ) -> MoveSet { let opposing_pieces = position.bitboard_for_color(placed_piece.piece().color().other()); let empty_squares = position.empty_squares(); let knight_moves = BitBoard::knight_moves(placed_piece.square()); @@ -39,7 +44,8 @@ mod tests { White Knight on E4, ]; - let generator = KnightMoveGenerator::new(&pos, Color::White); + let generator = + KnightMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); /* let bb = generator.bitboard(); diff --git a/position/src/move_generator/pawn.rs b/position/src/move_generator/pawn.rs index 728ba05..9bfbbd5 100644 --- a/position/src/move_generator/pawn.rs +++ b/position/src/move_generator/pawn.rs @@ -15,7 +15,12 @@ impl MoveGeneratorInternal for PawnMoveGenerator { Piece::pawn(color) } - fn move_set_for_piece(position: &Position, placed_piece: PlacedPiece) -> MoveSet { + fn move_set_for_piece( + position: &Position, + placed_piece: PlacedPiece, + capture_mask: BitBoard, + push_mask: BitBoard, + ) -> MoveSet { let capture_moves = Self::attacks(position, placed_piece); let quiet_moves = Self::pushes(position, placed_piece); @@ -65,7 +70,7 @@ mod tests { fn one_2square_push() { let pos = test_position![White Pawn on E2]; - let generator = PawnMoveGenerator::new(&pos, Color::White); + let generator = PawnMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); let expected_moves = HashSet::from_iter([ MoveBuilder::new(piece!(White Pawn), Square::E2, Square::E3).build(), @@ -81,7 +86,7 @@ mod tests { fn one_1square_push() { let pos = test_position![White Pawn on E3]; - let generator = PawnMoveGenerator::new(&pos, Color::White); + let generator = PawnMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); let expected_moves = HashSet::from_iter([MoveBuilder::new( Piece::pawn(Color::White), @@ -104,7 +109,7 @@ mod tests { println!("{}", DiagramFormatter::new(&pos)); - let generator = PawnMoveGenerator::new(&pos, Color::White); + let generator = PawnMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); let expected_moves = HashSet::from_iter([MoveBuilder::new( Piece::pawn(Color::White), @@ -125,7 +130,7 @@ mod tests { White Knight on E3, ]; - let generator = PawnMoveGenerator::new(&pos, Color::White); + let generator = PawnMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); let generated_moves: HashSet = generator.iter().collect(); let expected_moves: HashSet = HashSet::new(); @@ -141,7 +146,7 @@ mod tests { Black Knight on D5, ]; - let generator = PawnMoveGenerator::new(&pos, Color::White); + let generator = PawnMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); let expected_moves = HashSet::from_iter( @@ -164,7 +169,7 @@ mod tests { Black Queen on F5, ]; - let generator = PawnMoveGenerator::new(&pos, Color::White); + let generator = PawnMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); let expected_moves = HashSet::from_iter([ MoveBuilder::new(piece!(White Pawn), Square::E4, Square::D5) diff --git a/position/src/move_generator/queen.rs b/position/src/move_generator/queen.rs index 737686c..943b46d 100644 --- a/position/src/move_generator/queen.rs +++ b/position/src/move_generator/queen.rs @@ -12,7 +12,12 @@ impl MoveGeneratorInternal for ClassicalMoveGenerator { Piece::queen(color) } - fn move_set_for_piece(position: &Position, placed_piece: PlacedPiece) -> MoveSet { + fn move_set_for_piece( + position: &Position, + placed_piece: PlacedPiece, + capture_mask: BitBoard, + push_mask: BitBoard, + ) -> MoveSet { let piece = placed_piece.piece(); let color = piece.color(); let square = placed_piece.square(); @@ -68,7 +73,8 @@ mod tests { fn classical_single_queen_bitboard() { let pos = position![White Queen on B2]; - let generator = ClassicalMoveGenerator::new(&pos, Color::White); + let generator = + ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); let bitboard = generator.bitboard(); let expected = bitboard![ A2, C2, D2, E2, F2, G2, H2, // Rank @@ -94,7 +100,8 @@ mod tests { println!("{}", DiagramFormatter::new(&pos)); - let generator = ClassicalMoveGenerator::new(&pos, Color::White); + let generator = + ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); let bitboard = generator.bitboard(); let expected = BitBoard::new( 0b10000001_01000001_00100001_00010001_00001001_00000101_00000011_00001110, @@ -116,7 +123,8 @@ mod tests { ]; println!("{}", DiagramFormatter::new(&pos)); - let generator = ClassicalMoveGenerator::new(&pos, Color::White); + let generator = + ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( generator.bitboard(), @@ -134,7 +142,8 @@ mod tests { let pos = position![White Queen on D3]; println!("{}", DiagramFormatter::new(&pos)); - let generator = ClassicalMoveGenerator::new(&pos, Color::White); + let generator = + ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( generator.bitboard(), diff --git a/position/src/move_generator/rook.rs b/position/src/move_generator/rook.rs index 72f237b..1389b11 100644 --- a/position/src/move_generator/rook.rs +++ b/position/src/move_generator/rook.rs @@ -12,7 +12,12 @@ impl MoveGeneratorInternal for ClassicalMoveGenerator { Piece::rook(color) } - fn move_set_for_piece(position: &Position, placed_piece: PlacedPiece) -> MoveSet { + fn move_set_for_piece( + position: &Position, + placed_piece: PlacedPiece, + capture_mask: BitBoard, + push_mask: BitBoard, + ) -> MoveSet { let piece = placed_piece.piece(); let color = piece.color(); let square = placed_piece.square(); @@ -64,7 +69,8 @@ mod tests { fn classical_single_rook_bitboard() { let pos = test_position![White Rook on A2]; - let generator = ClassicalMoveGenerator::new(&pos, Color::White); + let generator = + ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( generator.bitboard(), @@ -82,7 +88,8 @@ mod tests { println!("{}", DiagramFormatter::new(&pos)); - let generator = ClassicalMoveGenerator::new(&pos, Color::White); + let generator = + ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( generator.bitboard(), @@ -100,7 +107,8 @@ mod tests { Black Knight on E1, ]; - let generator = ClassicalMoveGenerator::new(&pos, Color::White); + let generator = + ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( generator.bitboard(), @@ -112,7 +120,8 @@ mod tests { fn classical_single_rook_in_center() { let pos = test_position![White Rook on D4]; - let generator = ClassicalMoveGenerator::new(&pos, Color::White); + let generator = + ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( generator.bitboard(), diff --git a/position/src/position/position.rs b/position/src/position/position.rs index 0443929..31e772d 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -136,7 +136,7 @@ impl Position { pub fn moves(&self) -> &Moves { self.moves - .get_or_init(|| Moves::new(self, self.color_to_move)) + .get_or_init(|| Moves::new(self, self.color_to_move, BitBoard::FULL, BitBoard::FULL)) } /// Return a BitBoard representing the set of squares containing a piece. From 13e166e059fd6218d8d6b1719ddb81368eb6b8f0 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 29 Jan 2024 20:12:26 -0800 Subject: [PATCH 124/423] [position] Remove an unnecessary type annotation --- position/src/position/builders/move_builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/position/src/position/builders/move_builder.rs b/position/src/position/builders/move_builder.rs index add0bb1..85310d8 100644 --- a/position/src/position/builders/move_builder.rs +++ b/position/src/position/builders/move_builder.rs @@ -67,7 +67,7 @@ where let player = self.position.player_to_move(); - let captured_piece: Option = if mv.is_en_passant() { + let captured_piece = if mv.is_en_passant() { // En passant captures the pawn directly ahead (in the player's direction) of the en passant square. let capture_square = match player { Color::White => to_square.neighbor(Direction::South), From 98c8ef6e242fb36c82f817d4f0134eb1fe4f19b1 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 29 Jan 2024 20:12:41 -0800 Subject: [PATCH 125/423] [position] Remove the unused Position::move_is_legal --- position/src/position/position.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/position/src/position/position.rs b/position/src/position/position.rs index 31e772d..23e25d6 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -246,10 +246,6 @@ impl Position { .next() .unwrap() } - - pub(crate) fn move_is_legal(&self, mv: Move) -> bool { - true - } } // crate::position methods From 8cdb9f13b4f106d79b40e0c0eab2f16c647fd173 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 30 Jan 2024 08:34:29 -0800 Subject: [PATCH 126/423] [position] Apply capture and push masks to move generator results --- position/src/move_generator/bishop.rs | 13 ++++++------- position/src/move_generator/knight.rs | 4 ++-- position/src/move_generator/pawn.rs | 4 ++-- position/src/move_generator/queen.rs | 4 ++-- position/src/move_generator/rook.rs | 4 ++-- 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/position/src/move_generator/bishop.rs b/position/src/move_generator/bishop.rs index ec6a96a..5b72f78 100644 --- a/position/src/move_generator/bishop.rs +++ b/position/src/move_generator/bishop.rs @@ -1,7 +1,7 @@ // Eryn Wells use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use crate::{MoveBuilder, Position}; +use crate::Position; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Direction, Piece, PlacedPiece}; @@ -14,12 +14,11 @@ impl MoveGeneratorInternal for ClassicalMoveGenerator { fn move_set_for_piece( position: &Position, - placed_piece: PlacedPiece, + piece: PlacedPiece, capture_mask: BitBoard, push_mask: BitBoard, ) -> MoveSet { - let piece = placed_piece.piece(); - let square = placed_piece.square(); + let square = piece.square(); let blockers = position.occupied_squares(); let empty_squares = !blockers; @@ -47,10 +46,10 @@ impl MoveGeneratorInternal for ClassicalMoveGenerator { update_moves_with_ray!(SouthEast, occupied_squares); update_moves_with_ray!(SouthWest, occupied_squares); - let quiet_moves = all_moves & (empty_squares | !friendly_pieces); - let capture_moves = all_moves & opposing_pieces; + let quiet_moves = all_moves & (empty_squares | !friendly_pieces) & push_mask; + let capture_moves = all_moves & opposing_pieces & capture_mask; - MoveSet::new(placed_piece) + MoveSet::new(piece) .quiet_moves(quiet_moves) .capture_moves(capture_moves) } diff --git a/position/src/move_generator/knight.rs b/position/src/move_generator/knight.rs index 0f5bca3..66ee8ba 100644 --- a/position/src/move_generator/knight.rs +++ b/position/src/move_generator/knight.rs @@ -22,8 +22,8 @@ impl MoveGeneratorInternal for KnightMoveGenerator { let empty_squares = position.empty_squares(); let knight_moves = BitBoard::knight_moves(placed_piece.square()); - let quiet_moves = knight_moves & empty_squares; - let capture_moves = knight_moves & opposing_pieces; + let quiet_moves = knight_moves & empty_squares & push_mask; + let capture_moves = knight_moves & opposing_pieces & capture_mask; MoveSet::new(placed_piece) .quiet_moves(quiet_moves) diff --git a/position/src/move_generator/pawn.rs b/position/src/move_generator/pawn.rs index 9bfbbd5..f68baf4 100644 --- a/position/src/move_generator/pawn.rs +++ b/position/src/move_generator/pawn.rs @@ -21,8 +21,8 @@ impl MoveGeneratorInternal for PawnMoveGenerator { capture_mask: BitBoard, push_mask: BitBoard, ) -> MoveSet { - let capture_moves = Self::attacks(position, placed_piece); - let quiet_moves = Self::pushes(position, placed_piece); + let capture_moves = Self::attacks(position, placed_piece) & capture_mask; + let quiet_moves = Self::pushes(position, placed_piece) & push_mask; MoveSet::new(placed_piece) .quiet_moves(quiet_moves) diff --git a/position/src/move_generator/queen.rs b/position/src/move_generator/queen.rs index 943b46d..ad913fe 100644 --- a/position/src/move_generator/queen.rs +++ b/position/src/move_generator/queen.rs @@ -53,8 +53,8 @@ impl MoveGeneratorInternal for ClassicalMoveGenerator { update_moves_with_ray!(SouthWest, occupied_squares); update_moves_with_ray!(West, occupied_squares); - let quiet_moves = all_moves & (empty_squares | !friendly_pieces); - let capture_moves = all_moves & opposing_pieces; + let quiet_moves = all_moves & (empty_squares | !friendly_pieces) & push_mask; + let capture_moves = all_moves & opposing_pieces & capture_mask; MoveSet::new(placed_piece) .quiet_moves(quiet_moves) diff --git a/position/src/move_generator/rook.rs b/position/src/move_generator/rook.rs index 1389b11..b440273 100644 --- a/position/src/move_generator/rook.rs +++ b/position/src/move_generator/rook.rs @@ -49,8 +49,8 @@ impl MoveGeneratorInternal for ClassicalMoveGenerator { update_moves_with_ray!(South, occupied_squares); update_moves_with_ray!(West, occupied_squares); - let quiet_moves = all_moves & (empty_squares | !friendly_pieces); - let capture_moves = all_moves & opposing_pieces; + let quiet_moves = all_moves & (empty_squares | !friendly_pieces) & capture_mask; + let capture_moves = all_moves & opposing_pieces & push_mask; MoveSet::new(placed_piece) .quiet_moves(quiet_moves) From 357b81151893269b9df75fbc907e7a1b8506cec9 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 30 Jan 2024 08:35:02 -0800 Subject: [PATCH 127/423] [position] Add a fen! macro --- position/src/fen.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/position/src/fen.rs b/position/src/fen.rs index fd7b530..d6c3568 100644 --- a/position/src/fen.rs +++ b/position/src/fen.rs @@ -4,6 +4,12 @@ use crate::{r#move::Castle, Position, PositionBuilder}; use chessfriend_core::{piece, Color, File, Piece, PlacedPiece, Rank, Square}; use std::fmt::Write; +macro_rules! fen { + ($fen_string:literal) => { + Position::from_fen_str($fen_string) + }; +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ToFenError { FmtError(std::fmt::Error), @@ -270,11 +276,8 @@ mod tests { #[test] fn from_starting_fen() { - let pos = - Position::from_fen_str("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") - .unwrap(); + let pos = fen!("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1").unwrap(); let expected = Position::starting(); - assert_eq!(pos, expected, "{pos:#?}\n{expected:#?}"); } } From 26aedd88997acb2df7aeb2c0015ca440d86e155a Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 30 Jan 2024 08:36:03 -0800 Subject: [PATCH 128/423] [position] Implement Position::king_bitboard to easily get the king bitboard for a player Rewrite king_square() and is_king_in_check() in terms of king_bitboard(). --- position/src/position/position.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/position/src/position/position.rs b/position/src/position/position.rs index 23e25d6..f2b6de6 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -235,13 +235,16 @@ impl Position { } pub(crate) fn is_king_in_check(&self) -> bool { - let sight_of_opposing_player = self.sight_of_player(self.color_to_move.other()); - sight_of_opposing_player.is_set(self.king_square(self.color_to_move)) + let danger_squares = self.king_danger(self.color_to_move); + (danger_squares & self.king_bitboard(self.color_to_move)).is_empty() + } + + fn king_bitboard(&self, player: Color) -> &BitBoard { + self.pieces.bitboard_for_piece(&Piece::king(player)) } pub(crate) fn king_square(&self, player: Color) -> Square { - self.pieces - .bitboard_for_piece(&Piece::king(player)) + self.king_bitboard(player) .occupied_squares() .next() .unwrap() From 8aa5dacfc8cacd1cd80a07618504d528ef779d1b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 09:51:58 -0800 Subject: [PATCH 129/423] Initial implementation of CheckingPieces and Position::checking_pieces() --- position/src/check.rs | 29 +++++++++++++++++++++++ position/src/lib.rs | 1 + position/src/position/position.rs | 38 +++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 position/src/check.rs diff --git a/position/src/check.rs b/position/src/check.rs new file mode 100644 index 0000000..d9d5172 --- /dev/null +++ b/position/src/check.rs @@ -0,0 +1,29 @@ +// Eryn Wells + +use chessfriend_bitboard::BitBoard; + +pub struct CheckingPieces { + pawn: BitBoard, + knight: BitBoard, + bishop: BitBoard, + rook: BitBoard, + queen: BitBoard, +} + +impl CheckingPieces { + pub(crate) fn new( + pawn: BitBoard, + knight: BitBoard, + bishop: BitBoard, + rook: BitBoard, + queen: BitBoard, + ) -> CheckingPieces { + CheckingPieces { + pawn, + knight, + bishop, + rook, + queen, + } + } +} diff --git a/position/src/lib.rs b/position/src/lib.rs index 9c58540..dde3e5d 100644 --- a/position/src/lib.rs +++ b/position/src/lib.rs @@ -2,6 +2,7 @@ pub mod fen; +mod check; mod display; mod r#move; mod move_generator; diff --git a/position/src/position/position.rs b/position/src/position/position.rs index f2b6de6..7e211b4 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -249,6 +249,44 @@ impl Position { .next() .unwrap() } + + pub(crate) fn checking_pieces(&self) -> CheckingPieces { + let opponent = self.color_to_move.other(); + let king_square = self.king_square(self.color_to_move); + + let checking_pawns = { + // The current player's pawn attack moves *from* this square are the + // same as the pawn moves for the opposing player attacking this square. + let pawn_moves_to_king_square = BitBoard::pawn_attacks(king_square, self.color_to_move); + let opposing_pawn = Piece::pawn(opponent); + let opposing_pawns = self.pieces.bitboard_for_piece(&opposing_pawn); + + pawn_moves_to_king_square & opposing_pawns + }; + + macro_rules! checking_piece { + ($moves_bb_fn:path, $piece_fn:path) => {{ + let moves_from_opposing_square = $moves_bb_fn(king_square); + let piece = $piece_fn(opponent); + let opposing_pieces = self.pieces.bitboard_for_piece(&piece); + + moves_from_opposing_square & opposing_pieces + }}; + } + + let checking_knights = checking_piece!(BitBoard::knight_moves, Piece::knight); + let checking_bishops = checking_piece!(BitBoard::bishop_moves, Piece::bishop); + let checking_rooks = checking_piece!(BitBoard::rook_moves, Piece::rook); + let checking_queens = checking_piece!(BitBoard::queen_moves, Piece::queen); + + CheckingPieces::new( + checking_pawns, + checking_knights, + checking_bishops, + checking_rooks, + checking_queens, + ) + } } // crate::position methods From cac13b4bc753f0bdbc8a4a5a7be3e4c5aae88cbc Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 30 Jan 2024 08:41:23 -0800 Subject: [PATCH 130/423] Cargo.lock changes --- Cargo.lock | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3ebe93a..6e580bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,16 +56,40 @@ version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" -[[package]] -name = "board" -version = "0.1.0" - [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chessfriend_bitboard" +version = "0.1.0" +dependencies = [ + "chessfriend_core", +] + +[[package]] +name = "chessfriend_core" +version = "0.1.0" + +[[package]] +name = "chessfriend_move_generator" +version = "0.1.0" +dependencies = [ + "chessfriend_bitboard", + "chessfriend_core", + "chessfriend_position", +] + +[[package]] +name = "chessfriend_position" +version = "0.1.0" +dependencies = [ + "chessfriend_bitboard", + "chessfriend_core", +] + [[package]] name = "clap" version = "4.4.18" @@ -121,10 +145,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" -[[package]] -name = "core" -version = "0.1.0" - [[package]] name = "endian-type" version = "0.1.2" @@ -151,7 +171,8 @@ checksum = "281e452d3bad4005426416cdba5ccfd4f5c1280e10099e21db27f7c1c28347fc" name = "explorer" version = "0.1.0" dependencies = [ - "board", + "chessfriend_core", + "chessfriend_position", "clap", "rustyline", "shlex", From 722a90b860afb5ad69da32670db2399c39566246 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 1 Feb 2024 08:42:19 -0800 Subject: [PATCH 131/423] [position] Implement a SliderRayToSquare trait This trait declares ray_to_square() which should return a BitBoard representing a ray from a Square to another Square. The ray should include the target Square. PlacedPiece implements this trait. --- bitboard/src/bitboard.rs | 30 ++++++++++- bitboard/src/library.rs | 4 +- position/src/sight.rs | 114 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 141 insertions(+), 7 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 2f9740d..4904c3e 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -39,7 +39,7 @@ impl BitBoard { library::FILES[*file as usize] } - pub fn ray(sq: Square, dir: Direction) -> BitBoard { + pub fn ray(sq: Square, dir: Direction) -> &'static BitBoard { library::library().ray(sq, dir) } @@ -107,6 +107,24 @@ impl BitBoard { pub fn occupied_squares_trailing(&self) -> impl Iterator { LeadingBitScanner::new(self.0).map(|idx| unsafe { Square::from_index(idx as u8) }) } + + pub fn first_occupied_square(&self) -> Option { + let leading_zeros = self.0.leading_zeros() as u8; + if leading_zeros < Square::NUM as u8 { + unsafe { Some(Square::from_index(Square::NUM as u8 - leading_zeros - 1)) } + } else { + None + } + } + + pub fn first_occupied_square_trailing(&self) -> Option { + let trailing_zeros = self.0.trailing_zeros() as u8; + if trailing_zeros < Square::NUM as u8 { + unsafe { Some(Square::from_index(trailing_zeros)) } + } else { + None + } + } } impl Default for BitBoard { @@ -377,4 +395,14 @@ mod tests { assert_eq!(BitBoard::from(Square::A1), BitBoard(0b1)); assert_eq!(BitBoard::from(Square::H8), BitBoard(1 << 63)); } + + #[test] + fn first_occupied_squares() { + let bb = bitboard![A8, E1]; + assert_eq!(bb.first_occupied_square(), Some(Square::A8)); + assert_eq!(bb.first_occupied_square_trailing(), Some(Square::E1)); + + let bb = bitboard![D6, E7, F8]; + assert_eq!(bb.first_occupied_square_trailing(), Some(Square::D6)); + } } diff --git a/bitboard/src/library.rs b/bitboard/src/library.rs index 41951c7..4615caa 100644 --- a/bitboard/src/library.rs +++ b/bitboard/src/library.rs @@ -220,8 +220,8 @@ impl MoveLibrary { ray } - pub(super) fn ray(&self, sq: Square, dir: Direction) -> BitBoard { - self.rays[sq as usize][dir as usize] + pub(super) fn ray(&self, sq: Square, dir: Direction) -> &BitBoard { + &self.rays[sq as usize][dir as usize] } pub(super) fn pawn_pushes(&self, sq: Square, color: Color) -> BitBoard { diff --git a/position/src/sight.rs b/position/src/sight.rs index 76005a2..6e87d95 100644 --- a/position/src/sight.rs +++ b/position/src/sight.rs @@ -26,6 +26,10 @@ pub(crate) trait SightExt { fn king_sight(&self, pieces: &PieceBitBoards) -> BitBoard; } +pub(crate) trait SliderRayToSquareExt { + fn ray_to_square(&self, square: Square) -> Option; +} + impl SightExt for PlacedPiece { fn sight(&self, pieces: &PieceBitBoards, en_passant_square: Option) -> BitBoard { match self.shape() { @@ -183,8 +187,80 @@ impl SightExt for PlacedPiece { } } +impl SliderRayToSquareExt for PlacedPiece { + fn ray_to_square(&self, target: Square) -> Option { + macro_rules! ray { + ($square:expr, $direction:ident) => { + ( + BitBoard::ray($square, Direction::$direction), + Direction::$direction, + ) + }; + } + + let square = self.square(); + let target_bitboard: BitBoard = target.into(); + + let ray_and_direction = match self.shape() { + Shape::Bishop => [ + ray!(square, NorthEast), + ray!(square, SouthEast), + ray!(square, SouthWest), + ray!(square, NorthWest), + ] + .into_iter() + .find(|(&ray, _)| !(target_bitboard & ray).is_empty()), + Shape::Rook => [ + ray!(square, North), + ray!(square, East), + ray!(square, South), + ray!(square, West), + ] + .into_iter() + .find(|(&ray, _)| !(target_bitboard & ray).is_empty()), + Shape::Queen => [ + ray!(square, North), + ray!(square, NorthEast), + ray!(square, East), + ray!(square, SouthEast), + ray!(square, South), + ray!(square, SouthWest), + ray!(square, West), + ray!(square, NorthWest), + ] + .into_iter() + .find(|(&ray, _)| !(target_bitboard & ray).is_empty()), + _ => None, + }; + + if let Some((ray, direction)) = ray_and_direction { + let first_occupied_square = match direction { + Direction::East + | Direction::NorthWest + | Direction::NorthEast + | Direction::North => ray.first_occupied_square_trailing(), + Direction::West + | Direction::SouthWest + | Direction::SouthEast + | Direction::South => ray.first_occupied_square(), + }; + + if let Some(occupied_square) = first_occupied_square { + let remainder = BitBoard::ray(target, direction); + return Some(ray & !remainder); + } + } + + None + } +} + #[cfg(test)] mod tests { + use super::*; + use chessfriend_bitboard::bitboard; + use chessfriend_core::{piece, Square}; + macro_rules! sight_test { ($test_name:ident, $position:expr, $piece:expr, $bitboard:expr) => { #[test] @@ -201,6 +277,12 @@ mod tests { }; } + #[test] + fn pawns_and_knights_cannot_make_rays() { + assert_eq!(piece!(White Pawn on F7).ray_to_square(Square::E8), None); + assert_eq!(piece!(White Knight on F6).ray_to_square(Square::E8), None); + } + mod pawn { use crate::test_position; use chessfriend_bitboard::{bitboard, BitBoard}; @@ -273,20 +355,25 @@ mod tests { } mod bishop { - use chessfriend_bitboard::bitboard; - use chessfriend_core::piece; + use super::*; sight_test!( c2_bishop, piece!(Black Bishop on C2), bitboard!(D1, B3, A4, B1, D3, E4, F5, G6, H7) ); + + #[test] + fn ray_to_square() { + let generated_ray = piece!(White Bishop on C5).ray_to_square(Square::E7); + let expected_ray = bitboard![D6, E7]; + assert_eq!(generated_ray, Some(expected_ray)); + } } mod rook { + use super::*; use crate::test_position; - use chessfriend_bitboard::bitboard; - use chessfriend_core::piece; sight_test!( g3_rook, @@ -304,6 +391,25 @@ mod tests { piece!(White Rook on E4), bitboard!(A4, B4, C4, D4, F4, G4, H4, E2, E3, E5, E6, E7) ); + + #[test] + fn ray_to_square() { + let generated_ray = piece!(White Rook on C2).ray_to_square(Square::C6); + let expected_ray = bitboard![C3, C4, C5, C6]; + assert_eq!(generated_ray, Some(expected_ray)); + + let generated_ray = piece!(White Rook on D2).ray_to_square(Square::H2); + let expected_ray = bitboard![E2, F2, G2, H2]; + assert_eq!(generated_ray, Some(expected_ray)); + + let generated_ray = piece!(White Rook on G6).ray_to_square(Square::B6); + let expected_ray = bitboard![B6, C6, D6, E6, F6]; + assert_eq!(generated_ray, Some(expected_ray)); + + let generated_ray = piece!(White Rook on A6).ray_to_square(Square::A3); + let expected_ray = bitboard![A5, A4, A3]; + assert_eq!(generated_ray, Some(expected_ray)); + } } mod king { From f1a05f33c96a25ec8328467245bdfff5de33de0c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 1 Feb 2024 08:43:03 -0800 Subject: [PATCH 132/423] [position] Factor out the update_moves_with_ray! as ray_in_direction! This macro is implemented in every sight method. Factor it out, rename it, and use it in all these macros. --- position/src/sight.rs | 91 ++++++++++++++----------------------------- 1 file changed, 30 insertions(+), 61 deletions(-) diff --git a/position/src/sight.rs b/position/src/sight.rs index 6e87d95..27cb53d 100644 --- a/position/src/sight.rs +++ b/position/src/sight.rs @@ -4,6 +4,20 @@ use crate::position::piece_sets::PieceBitBoards; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Direction, PlacedPiece, Shape, Square}; +macro_rules! ray_in_direction { + ($square:expr, $blockers:expr, $direction:ident, $occupied_squares:tt) => {{ + let ray = BitBoard::ray($square, Direction::$direction); + if let Some(first_occupied_square) = BitBoard::$occupied_squares(&(ray & $blockers)).next() + { + let remainder = BitBoard::ray(first_occupied_square, Direction::$direction); + let attack_ray = ray & !remainder; + attack_ray + } else { + *ray + } + }}; +} + pub(crate) trait SightExt { fn sight(&self, pieces: &PieceBitBoards, en_passant_square: Option) -> BitBoard; @@ -92,25 +106,10 @@ impl SightExt for PlacedPiece { let blockers = pieces.all_pieces(); - macro_rules! update_moves_with_ray { - ($direction:ident, $occupied_squares:tt) => { - let ray = BitBoard::ray(square, Direction::$direction); - if let Some(first_occupied_square) = - BitBoard::$occupied_squares(&(ray & blockers)).next() - { - let remainder = BitBoard::ray(first_occupied_square, Direction::$direction); - let attack_ray = ray & !remainder; - sight |= attack_ray; - } else { - sight |= ray; - } - }; - } - - update_moves_with_ray!(NorthEast, occupied_squares_trailing); - update_moves_with_ray!(NorthWest, occupied_squares_trailing); - update_moves_with_ray!(SouthEast, occupied_squares); - update_moves_with_ray!(SouthWest, occupied_squares); + sight |= ray_in_direction!(square, blockers, NorthEast, occupied_squares_trailing); + sight |= ray_in_direction!(square, blockers, NorthWest, occupied_squares_trailing); + sight |= ray_in_direction!(square, blockers, SouthEast, occupied_squares); + sight |= ray_in_direction!(square, blockers, SouthWest, occupied_squares); let friendly_pieces = pieces.all_pieces_of_color(self.color()); sight & !friendly_pieces @@ -123,25 +122,10 @@ impl SightExt for PlacedPiece { let blockers = pieces.all_pieces(); - macro_rules! update_moves_with_ray { - ($direction:ident, $occupied_squares:tt) => { - let ray = BitBoard::ray(square, Direction::$direction); - if let Some(first_occupied_square) = - BitBoard::$occupied_squares(&(ray & blockers)).next() - { - let remainder = BitBoard::ray(first_occupied_square, Direction::$direction); - let attack_ray = ray & !remainder; - sight |= attack_ray; - } else { - sight |= ray; - } - }; - } - - update_moves_with_ray!(North, occupied_squares_trailing); - update_moves_with_ray!(East, occupied_squares_trailing); - update_moves_with_ray!(South, occupied_squares); - update_moves_with_ray!(West, occupied_squares); + sight |= ray_in_direction!(square, blockers, North, occupied_squares_trailing); + sight |= ray_in_direction!(square, blockers, East, occupied_squares_trailing); + sight |= ray_in_direction!(square, blockers, South, occupied_squares); + sight |= ray_in_direction!(square, blockers, West, occupied_squares); let friendly_pieces = pieces.all_pieces_of_color(self.color()); sight & !friendly_pieces @@ -154,29 +138,14 @@ impl SightExt for PlacedPiece { let blockers = pieces.all_pieces(); - macro_rules! update_moves_with_ray { - ($direction:ident, $occupied_squares:tt) => { - let ray = BitBoard::ray(square, Direction::$direction); - if let Some(first_occupied_square) = - BitBoard::$occupied_squares(&(ray & blockers)).next() - { - let remainder = BitBoard::ray(first_occupied_square, Direction::$direction); - let attack_ray = ray & !remainder; - sight |= attack_ray; - } else { - sight |= ray; - } - }; - } - - update_moves_with_ray!(NorthWest, occupied_squares_trailing); - update_moves_with_ray!(North, occupied_squares_trailing); - update_moves_with_ray!(NorthEast, occupied_squares_trailing); - update_moves_with_ray!(East, occupied_squares_trailing); - update_moves_with_ray!(SouthEast, occupied_squares); - update_moves_with_ray!(South, occupied_squares); - update_moves_with_ray!(SouthWest, occupied_squares); - update_moves_with_ray!(West, occupied_squares); + sight |= ray_in_direction!(square, blockers, NorthWest, occupied_squares_trailing); + sight |= ray_in_direction!(square, blockers, North, occupied_squares_trailing); + sight |= ray_in_direction!(square, blockers, NorthEast, occupied_squares_trailing); + sight |= ray_in_direction!(square, blockers, East, occupied_squares_trailing); + sight |= ray_in_direction!(square, blockers, SouthEast, occupied_squares); + sight |= ray_in_direction!(square, blockers, South, occupied_squares); + sight |= ray_in_direction!(square, blockers, SouthWest, occupied_squares); + sight |= ray_in_direction!(square, blockers, West, occupied_squares); let friendly_pieces = pieces.all_pieces_of_color(self.color()); sight & !friendly_pieces From 7b97060ba205f76c1a1166380349b59fa0b3126a Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 1 Feb 2024 08:43:49 -0800 Subject: [PATCH 133/423] [position] The KingMoveGenerator doesn't use push or capture masks Mark these arguments with an _ so the linter stops complaining about it. --- position/src/move_generator/king.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/position/src/move_generator/king.rs b/position/src/move_generator/king.rs index 7941bf3..b2d0d50 100644 --- a/position/src/move_generator/king.rs +++ b/position/src/move_generator/king.rs @@ -20,8 +20,8 @@ impl MoveGeneratorInternal for KingMoveGenerator { fn move_set_for_piece( position: &Position, placed_piece: PlacedPiece, - capture_mask: BitBoard, - push_mask: BitBoard, + _capture_mask: BitBoard, + _push_mask: BitBoard, ) -> MoveSet { let piece = placed_piece.piece(); let color = piece.color(); From deea23352bcbb6c4bad1bd9ecd678aa3a3e96c5d Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 1 Feb 2024 08:44:25 -0800 Subject: [PATCH 134/423] [bitboard] Implement BitBoard::population_count() Uses u64::count_ones() to return the population count of the BitBoard. --- bitboard/src/bitboard.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 4904c3e..b8a56ef 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -80,6 +80,20 @@ impl BitBoard { !(self & square_bitboard).is_empty() } + /// The number of 1 bits in the BitBoard. + /// + /// ## Examples + /// + /// ``` + /// use chessfriend_bitboard::BitBoard; + /// assert_eq!(BitBoard::EMPTY.population_count(), 0); + /// assert_eq!(BitBoard::new(0b01011110010).population_count(), 6); + /// assert_eq!(BitBoard::FULL.population_count(), 64); + /// ``` + pub fn population_count(&self) -> u32 { + self.0.count_ones() + } + pub fn set_square(&mut self, sq: Square) { let sq_bb: BitBoard = sq.into(); *self |= sq_bb From ca861df9c4d1d73b3e4a7e35375fcaa52270236f Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 1 Feb 2024 08:44:53 -0800 Subject: [PATCH 135/423] [bitboard] Use TrailingBitScanner in occupied_squares_trailing This was an oversight. --- bitboard/src/bitboard.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index b8a56ef..4ed4719 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -1,7 +1,7 @@ // Eryn Wells use crate::library; -use crate::LeadingBitScanner; +use crate::{LeadingBitScanner, TrailingBitScanner}; use chessfriend_core::{Color, Direction, Square}; use std::fmt; use std::ops::Not; @@ -119,7 +119,7 @@ impl BitBoard { /// Return an Iterator over the occupied squares, starting from the trailing /// (least-significant bit) end of the field. pub fn occupied_squares_trailing(&self) -> impl Iterator { - LeadingBitScanner::new(self.0).map(|idx| unsafe { Square::from_index(idx as u8) }) + TrailingBitScanner::new(self.0).map(|idx| unsafe { Square::from_index(idx as u8) }) } pub fn first_occupied_square(&self) -> Option { From 032fabe072bb85c2fd6fd1a7c027d72939f81e5f Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 1 Feb 2024 08:45:10 -0800 Subject: [PATCH 136/423] [bitboard] Return BitBoard::EMPTY from BitBoard's Default impl --- bitboard/src/bitboard.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 4ed4719..2453cce 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -143,7 +143,7 @@ impl BitBoard { impl Default for BitBoard { fn default() -> Self { - BitBoard::empty() + BitBoard::EMPTY } } From f09376f5dc6de86eb2c525606011393a04729378 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 2 Feb 2024 07:25:59 -0800 Subject: [PATCH 137/423] [position] Make the checking_piece! macro handle specifying the path to the piece constructor --- position/src/position/position.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/position/src/position/position.rs b/position/src/position/position.rs index 7e211b4..75b4b79 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -265,19 +265,19 @@ impl Position { }; macro_rules! checking_piece { - ($moves_bb_fn:path, $piece_fn:path) => {{ + ($moves_bb_fn:path, $piece_fn:ident) => {{ let moves_from_opposing_square = $moves_bb_fn(king_square); - let piece = $piece_fn(opponent); + let piece = Piece::$piece_fn(opponent); let opposing_pieces = self.pieces.bitboard_for_piece(&piece); moves_from_opposing_square & opposing_pieces }}; } - let checking_knights = checking_piece!(BitBoard::knight_moves, Piece::knight); - let checking_bishops = checking_piece!(BitBoard::bishop_moves, Piece::bishop); - let checking_rooks = checking_piece!(BitBoard::rook_moves, Piece::rook); - let checking_queens = checking_piece!(BitBoard::queen_moves, Piece::queen); + let checking_knights = checking_piece!(BitBoard::knight_moves, knight); + let checking_bishops = checking_piece!(BitBoard::bishop_moves, bishop); + let checking_rooks = checking_piece!(BitBoard::rook_moves, rook); + let checking_queens = checking_piece!(BitBoard::queen_moves, queen); CheckingPieces::new( checking_pawns, From c8faad799e64af616539b698ad8ddb300039a062 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 2 Feb 2024 07:28:12 -0800 Subject: [PATCH 138/423] [position] Clean up Position's construction scheme Position::default specifies the defaults for all field. Then, ::new() and ::starting() can use ..Default::default() in their implementations to avoid having to specify empty values for all the internal structures. --- position/src/position/position.rs | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/position/src/position/position.rs b/position/src/position/position.rs index 75b4b79..774fc26 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -26,16 +26,7 @@ pub struct Position { impl Position { pub fn empty() -> Position { - Position { - color_to_move: Color::White, - flags: Default::default(), - pieces: PieceBitBoards::default(), - en_passant_square: None, - sight: [OnceCell::new(), OnceCell::new()], - moves: OnceCell::new(), - half_move_counter: 0, - full_move_number: 1, - } + Default::default() } /// Return a starting position. @@ -60,13 +51,8 @@ impl Position { Self { color_to_move: Color::White, - flags: Flags::default(), pieces: PieceBitBoards::new([white_pieces, black_pieces]), - en_passant_square: None, - sight: [OnceCell::new(), OnceCell::new()], - moves: OnceCell::new(), - half_move_counter: 0, - full_move_number: 1, + ..Default::default() } } @@ -340,7 +326,16 @@ impl Position { impl Default for Position { fn default() -> Self { - Self::empty() + Self { + color_to_move: Color::White, + flags: Flags::default(), + pieces: PieceBitBoards::default(), + en_passant_square: None, + sight: [OnceCell::new(), OnceCell::new()], + moves: OnceCell::new(), + half_move_counter: 0, + full_move_number: 1, + } } } From ac07a8d6cfa6bca72f9581abe460a964197f4015 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 2 Feb 2024 07:29:43 -0800 Subject: [PATCH 139/423] [position] Implement SliderRayToSquareExt on Shape instead of PlacedPiece Add an origin square argument to its one method ::ray_to_square(). Pushing this implementation down a layer means I don't have to care about the color of the piece. --- position/src/sight.rs | 72 +++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 43 deletions(-) diff --git a/position/src/sight.rs b/position/src/sight.rs index 27cb53d..056ae45 100644 --- a/position/src/sight.rs +++ b/position/src/sight.rs @@ -41,7 +41,7 @@ pub(crate) trait SightExt { } pub(crate) trait SliderRayToSquareExt { - fn ray_to_square(&self, square: Square) -> Option; + fn ray_to_square(&self, origin: Square, target: Square) -> Option; } impl SightExt for PlacedPiece { @@ -156,8 +156,8 @@ impl SightExt for PlacedPiece { } } -impl SliderRayToSquareExt for PlacedPiece { - fn ray_to_square(&self, target: Square) -> Option { +impl SliderRayToSquareExt for Shape { + fn ray_to_square(&self, origin: Square, target: Square) -> Option { macro_rules! ray { ($square:expr, $direction:ident) => { ( @@ -167,35 +167,34 @@ impl SliderRayToSquareExt for PlacedPiece { }; } - let square = self.square(); let target_bitboard: BitBoard = target.into(); - let ray_and_direction = match self.shape() { + let ray_and_direction = match self { Shape::Bishop => [ - ray!(square, NorthEast), - ray!(square, SouthEast), - ray!(square, SouthWest), - ray!(square, NorthWest), + ray!(origin, NorthEast), + ray!(origin, SouthEast), + ray!(origin, SouthWest), + ray!(origin, NorthWest), ] .into_iter() .find(|(&ray, _)| !(target_bitboard & ray).is_empty()), Shape::Rook => [ - ray!(square, North), - ray!(square, East), - ray!(square, South), - ray!(square, West), + ray!(origin, North), + ray!(origin, East), + ray!(origin, South), + ray!(origin, West), ] .into_iter() .find(|(&ray, _)| !(target_bitboard & ray).is_empty()), Shape::Queen => [ - ray!(square, North), - ray!(square, NorthEast), - ray!(square, East), - ray!(square, SouthEast), - ray!(square, South), - ray!(square, SouthWest), - ray!(square, West), - ray!(square, NorthWest), + ray!(origin, North), + ray!(origin, NorthEast), + ray!(origin, East), + ray!(origin, SouthEast), + ray!(origin, South), + ray!(origin, SouthWest), + ray!(origin, West), + ray!(origin, NorthWest), ] .into_iter() .find(|(&ray, _)| !(target_bitboard & ray).is_empty()), @@ -203,21 +202,8 @@ impl SliderRayToSquareExt for PlacedPiece { }; if let Some((ray, direction)) = ray_and_direction { - let first_occupied_square = match direction { - Direction::East - | Direction::NorthWest - | Direction::NorthEast - | Direction::North => ray.first_occupied_square_trailing(), - Direction::West - | Direction::SouthWest - | Direction::SouthEast - | Direction::South => ray.first_occupied_square(), - }; - - if let Some(occupied_square) = first_occupied_square { - let remainder = BitBoard::ray(target, direction); - return Some(ray & !remainder); - } + let remainder = BitBoard::ray(target, direction); + return Some(ray & !remainder); } None @@ -248,8 +234,8 @@ mod tests { #[test] fn pawns_and_knights_cannot_make_rays() { - assert_eq!(piece!(White Pawn on F7).ray_to_square(Square::E8), None); - assert_eq!(piece!(White Knight on F6).ray_to_square(Square::E8), None); + assert_eq!(Shape::Pawn.ray_to_square(Square::F7, Square::E8), None); + assert_eq!(Shape::Knight.ray_to_square(Square::F6, Square::E8), None); } mod pawn { @@ -334,7 +320,7 @@ mod tests { #[test] fn ray_to_square() { - let generated_ray = piece!(White Bishop on C5).ray_to_square(Square::E7); + let generated_ray = Shape::Bishop.ray_to_square(Square::C5, Square::E7); let expected_ray = bitboard![D6, E7]; assert_eq!(generated_ray, Some(expected_ray)); } @@ -363,19 +349,19 @@ mod tests { #[test] fn ray_to_square() { - let generated_ray = piece!(White Rook on C2).ray_to_square(Square::C6); + let generated_ray = Shape::Rook.ray_to_square(Square::C2, Square::C6); let expected_ray = bitboard![C3, C4, C5, C6]; assert_eq!(generated_ray, Some(expected_ray)); - let generated_ray = piece!(White Rook on D2).ray_to_square(Square::H2); + let generated_ray = Shape::Rook.ray_to_square(Square::D2, Square::H2); let expected_ray = bitboard![E2, F2, G2, H2]; assert_eq!(generated_ray, Some(expected_ray)); - let generated_ray = piece!(White Rook on G6).ray_to_square(Square::B6); + let generated_ray = Shape::Rook.ray_to_square(Square::G6, Square::B6); let expected_ray = bitboard![B6, C6, D6, E6, F6]; assert_eq!(generated_ray, Some(expected_ray)); - let generated_ray = piece!(White Rook on A6).ray_to_square(Square::A3); + let generated_ray = Shape::Rook.ray_to_square(Square::A6, Square::A3); let expected_ray = bitboard![A5, A4, A3]; assert_eq!(generated_ray, Some(expected_ray)); } From 31903cb514cce5d890bd0ae682569bc9ab53d13f Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 2 Feb 2024 07:53:50 -0800 Subject: [PATCH 140/423] [position] Calculate push and capture masks on CheckingPieces --- position/src/check.rs | 88 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 78 insertions(+), 10 deletions(-) diff --git a/position/src/check.rs b/position/src/check.rs index d9d5172..df30749 100644 --- a/position/src/check.rs +++ b/position/src/check.rs @@ -1,13 +1,12 @@ // Eryn Wells use chessfriend_bitboard::BitBoard; +use chessfriend_core::{Color, Direction, Shape, Square}; + +use crate::sight::SliderRayToSquareExt; pub struct CheckingPieces { - pawn: BitBoard, - knight: BitBoard, - bishop: BitBoard, - rook: BitBoard, - queen: BitBoard, + bitboards: [BitBoard; 5], } impl CheckingPieces { @@ -19,11 +18,80 @@ impl CheckingPieces { queen: BitBoard, ) -> CheckingPieces { CheckingPieces { - pawn, - knight, - bishop, - rook, - queen, + bitboards: [pawn, knight, bishop, rook, queen], } } + + pub fn count(&self) -> u32 { + self.bitboards.iter().map(|b| b.population_count()).sum() + } + + /// A BitBoard representing the set of pieces that must be captured to + /// resolve check. + pub fn capture_mask(&self) -> BitBoard { + if self.count() == 0 { + BitBoard::FULL + } else { + self.bitboards + .iter() + .fold(BitBoard::EMPTY, std::ops::BitOr::bitor) + } + } + + /// A BitBoard representing the set of squares to which a player can move a + /// piece to block a checking piece. + pub fn push_mask(&self, king: &BitBoard) -> BitBoard { + let target = king.first_occupied_square().unwrap(); + + macro_rules! push_mask_for_shape { + ($push_mask:expr, $shape:ident, $king:expr) => {{ + let checking_pieces = self.bitboard_for_shape(Shape::$shape); + if !checking_pieces.is_empty() { + if let Some(checking_ray) = checking_pieces + .occupied_squares() + .flat_map(|sq| Shape::$shape.ray_to_square(sq, target).into_iter()) + .find(|bb| !(bb & $king).is_empty()) + { + $push_mask |= checking_ray & !$king + } + } + }}; + } + + let mut push_mask = BitBoard::EMPTY; + + push_mask_for_shape!(push_mask, Bishop, king); + push_mask_for_shape!(push_mask, Rook, king); + push_mask_for_shape!(push_mask, Queen, king); + + push_mask + } + + fn bitboard_for_shape(&self, shape: Shape) -> &BitBoard { + &self.bitboards[shape as usize] + } +} + +#[cfg(test)] +mod tests { + use super::*; + use chessfriend_bitboard::{bitboard, BitBoard}; + + /// This is a test position from [this execellent blog post][1] about how to + /// efficiently generate legal chess moves. + /// + /// [1]: https://peterellisjones.com/posts/generating-legal-chess-moves-efficiently/ + #[test] + fn rook_push_mask() { + let checks = CheckingPieces::new( + BitBoard::EMPTY, + BitBoard::EMPTY, + BitBoard::EMPTY, + bitboard![E5], + BitBoard::EMPTY, + ); + + let push_mask = checks.push_mask(&bitboard![E8]); + assert_eq!(push_mask, bitboard![E6, E7]); + } } From 986758d0110de8c00d985f21452bf926ce4281e9 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 2 Feb 2024 08:03:51 -0800 Subject: [PATCH 141/423] [position] For non-King move generators, only generate moves if the push and captures masks aren't empty Always generate King moves. --- position/src/move_generator.rs | 19 ++++++++++++++----- position/src/move_generator/bishop.rs | 6 +++--- position/src/move_generator/king.rs | 8 ++++---- position/src/move_generator/knight.rs | 8 ++++---- position/src/move_generator/pawn.rs | 8 ++++---- position/src/move_generator/queen.rs | 6 +++--- position/src/move_generator/rook.rs | 6 +++--- 7 files changed, 35 insertions(+), 26 deletions(-) diff --git a/position/src/move_generator.rs b/position/src/move_generator.rs index 7e193c1..7f7d481 100644 --- a/position/src/move_generator.rs +++ b/position/src/move_generator.rs @@ -51,10 +51,15 @@ macro_rules! move_generator_declaration { capture_mask: chessfriend_bitboard::BitBoard, push_mask: chessfriend_bitboard::BitBoard, ) -> $name { - $name { - color, - move_sets: Self::move_sets(position, color, capture_mask, push_mask), - } + let move_sets = if Self::shape() == chessfriend_core::Shape::King + || (!capture_mask.is_empty() && !push_mask.is_empty()) + { + Self::move_sets(position, color, capture_mask, push_mask) + } else { + std::collections::BTreeMap::new() + }; + + $name { color, move_sets } } } }; @@ -84,7 +89,11 @@ macro_rules! move_generator_declaration { pub(self) use move_generator_declaration; trait MoveGeneratorInternal { - fn piece(color: Color) -> Piece; + fn shape() -> Shape; + + fn piece(color: Color) -> Piece { + Piece::new(color, Self::shape()) + } fn move_sets( position: &Position, diff --git a/position/src/move_generator/bishop.rs b/position/src/move_generator/bishop.rs index 5b72f78..2fae36f 100644 --- a/position/src/move_generator/bishop.rs +++ b/position/src/move_generator/bishop.rs @@ -3,13 +3,13 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::Position; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Direction, Piece, PlacedPiece}; +use chessfriend_core::{Direction, PlacedPiece, Shape}; move_generator_declaration!(ClassicalMoveGenerator); impl MoveGeneratorInternal for ClassicalMoveGenerator { - fn piece(color: Color) -> Piece { - Piece::bishop(color) + fn shape() -> Shape { + Shape::Bishop } fn move_set_for_piece( diff --git a/position/src/move_generator/king.rs b/position/src/move_generator/king.rs index b2d0d50..c18692a 100644 --- a/position/src/move_generator/king.rs +++ b/position/src/move_generator/king.rs @@ -6,15 +6,15 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::{r#move::Castle, Position}; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece, PlacedPiece}; +use chessfriend_core::{PlacedPiece, Shape}; move_generator_declaration!(KingMoveGenerator, struct); move_generator_declaration!(KingMoveGenerator, new); move_generator_declaration!(KingMoveGenerator, getters); impl MoveGeneratorInternal for KingMoveGenerator { - fn piece(color: Color) -> Piece { - Piece::king(color) + fn shape() -> Shape { + Shape::King } fn move_set_for_piece( @@ -59,7 +59,7 @@ mod tests { use super::*; use crate::{assert_move_list, position, test_position, Move, MoveBuilder, PositionBuilder}; use chessfriend_bitboard::bitboard; - use chessfriend_core::{piece, Square}; + use chessfriend_core::{piece, Color, Square}; use std::collections::HashSet; #[test] diff --git a/position/src/move_generator/knight.rs b/position/src/move_generator/knight.rs index 66ee8ba..b4dc1bc 100644 --- a/position/src/move_generator/knight.rs +++ b/position/src/move_generator/knight.rs @@ -3,13 +3,13 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::Position; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece, PlacedPiece}; +use chessfriend_core::{PlacedPiece, Shape}; move_generator_declaration!(KnightMoveGenerator); impl MoveGeneratorInternal for KnightMoveGenerator { - fn piece(color: Color) -> Piece { - Piece::knight(color) + fn shape() -> Shape { + Shape::Knight } fn move_set_for_piece( @@ -35,7 +35,7 @@ impl MoveGeneratorInternal for KnightMoveGenerator { mod tests { use super::*; use crate::{position, Move, MoveBuilder}; - use chessfriend_core::{piece, Square}; + use chessfriend_core::{piece, Color, Square}; use std::collections::HashSet; #[test] diff --git a/position/src/move_generator/pawn.rs b/position/src/move_generator/pawn.rs index f68baf4..c8b2e6c 100644 --- a/position/src/move_generator/pawn.rs +++ b/position/src/move_generator/pawn.rs @@ -3,7 +3,7 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::Position; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece, PlacedPiece, Rank, Square}; +use chessfriend_core::{Piece, PlacedPiece, Rank, Shape, Square}; #[derive(Debug)] struct MoveIterator(usize, usize); @@ -11,8 +11,8 @@ struct MoveIterator(usize, usize); move_generator_declaration!(PawnMoveGenerator); impl MoveGeneratorInternal for PawnMoveGenerator { - fn piece(color: Color) -> Piece { - Piece::pawn(color) + fn shape() -> Shape { + Shape::Pawn } fn move_set_for_piece( @@ -63,7 +63,7 @@ impl PawnMoveGenerator { mod tests { use super::*; use crate::{assert_move_list, position::DiagramFormatter, test_position, Move, MoveBuilder}; - use chessfriend_core::{piece, Square}; + use chessfriend_core::{piece, Color, Square}; use std::collections::HashSet; #[test] diff --git a/position/src/move_generator/queen.rs b/position/src/move_generator/queen.rs index ad913fe..dc41769 100644 --- a/position/src/move_generator/queen.rs +++ b/position/src/move_generator/queen.rs @@ -3,13 +3,13 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::Position; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Direction, Piece, PlacedPiece}; +use chessfriend_core::{Direction, PlacedPiece, Shape}; move_generator_declaration!(ClassicalMoveGenerator); impl MoveGeneratorInternal for ClassicalMoveGenerator { - fn piece(color: Color) -> Piece { - Piece::queen(color) + fn shape() -> Shape { + Shape::Queen } fn move_set_for_piece( diff --git a/position/src/move_generator/rook.rs b/position/src/move_generator/rook.rs index b440273..18129c8 100644 --- a/position/src/move_generator/rook.rs +++ b/position/src/move_generator/rook.rs @@ -3,13 +3,13 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::Position; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Direction, Piece, PlacedPiece}; +use chessfriend_core::{Direction, PlacedPiece, Shape}; move_generator_declaration!(ClassicalMoveGenerator); impl MoveGeneratorInternal for ClassicalMoveGenerator { - fn piece(color: Color) -> Piece { - Piece::rook(color) + fn shape() -> Shape { + Shape::Rook } fn move_set_for_piece( From 942c758e151450576d98f362ad29a7075974c8d5 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 2 Feb 2024 08:05:37 -0800 Subject: [PATCH 142/423] [position] Calculate legal moves based on whether the king is in check Use CheckingPieces to determine which and how many pieces check the king. - If there are no checks, proceed with move generation as normal. - If there is one checking piece, calculate push and capture masks and use those to generate legal moves. - If there are more than one checking pieces, the only legal moves are king moves. Indicate this by setting the push and capture masks to EMPTY. --- position/src/position/position.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/position/src/position/position.rs b/position/src/position/position.rs index 774fc26..f0f8414 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -2,11 +2,11 @@ use super::{flags::Flags, piece_sets::PieceBitBoards, Pieces}; use crate::{ - move_generator::{MoveSet, Moves}, + check::{self, CheckingPieces}, + move_generator::Moves, position::DiagramFormatter, r#move::Castle, sight::SightExt, - Move, }; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; @@ -121,8 +121,22 @@ impl Position { } pub fn moves(&self) -> &Moves { - self.moves - .get_or_init(|| Moves::new(self, self.color_to_move, BitBoard::FULL, BitBoard::FULL)) + self.moves.get_or_init(|| { + let checking_pieces = self.checking_pieces(); + match checking_pieces.count() { + // Normal, unrestricted move generation + 0 => Moves::new(self, self.color_to_move, BitBoard::FULL, BitBoard::FULL), + 1 => { + // Calculate push and capture masks for checking piece. Moves are restricted to those that intersect those masks. + let capture_mask = checking_pieces.capture_mask(); + let push_mask = + checking_pieces.push_mask(self.king_bitboard(self.color_to_move)); + Moves::new(self, self.color_to_move, capture_mask, push_mask) + } + // With more than one checking piece, the only legal moves are king moves. + _ => Moves::new(self, self.color_to_move, BitBoard::EMPTY, BitBoard::EMPTY), + } + }) } /// Return a BitBoard representing the set of squares containing a piece. From 63758a2edd72ddab79f795d8594a65f60ef5056e Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 2 Feb 2024 08:07:13 -0800 Subject: [PATCH 143/423] Remove the move_generator crate from the Cargo workspace --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 16969d5..b490c8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,6 @@ members = [ "bitboard", "core", "explorer", - "move_generator", "position", ] resolver = "2" From 4a601c2b81304b6afa34c71d1fcfd3af99e21770 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 3 Feb 2024 10:04:41 -0800 Subject: [PATCH 144/423] [position] Fix all the unit tests The pawn move generator only generated pushes for white pawns. The is_king_in_check returned an inverted flag. MoveSet needed a couple more validation methods: can_move_to_square and can_castle. The MakeMoveBuilder also needed a little more move validation using the above methods. --- position/src/move.rs | 23 ++++++++------ position/src/move_generator.rs | 4 +-- position/src/move_generator/move_set.rs | 18 +++++++++-- position/src/move_generator/pawn.rs | 31 +++++++++++++------ .../src/position/builders/move_builder.rs | 27 +++++++++++++--- position/src/position/position.rs | 10 ++++-- 6 files changed, 82 insertions(+), 31 deletions(-) diff --git a/position/src/move.rs b/position/src/move.rs index bf6dc3c..32bc399 100644 --- a/position/src/move.rs +++ b/position/src/move.rs @@ -11,6 +11,7 @@ pub enum MakeMoveError { PlayerOutOfTurn, NoPiece, NoCapturedPiece, + NoLegalMoves, IllegalCastle, IllegalSquare(Square), } @@ -523,20 +524,24 @@ mod tests { use chessfriend_core::piece; macro_rules! assert_flag { - ($move:expr, $left:expr, $right:expr) => { - assert_eq!($left, $right, "{:?}", $move) + ($move:expr, $left:expr, $right:expr, $desc:expr) => { + assert_eq!($left, $right, "{:?} -> {}", $move, stringify!($desc)) }; } macro_rules! assert_flags { ($move:expr, $quiet:expr, $double_push:expr, $en_passant:expr, $capture:expr, $castle:expr, $promotion:expr) => { - assert_flag!($move, $move.is_quiet(), $quiet); - assert_flag!($move, $move.is_quiet(), $quiet); - assert_flag!($move, $move.is_double_push(), $double_push); - assert_flag!($move, $move.is_en_passant(), $en_passant); - assert_flag!($move, $move.is_capture(), $capture); - assert_flag!($move, $move.is_castle(), $castle); - assert_flag!($move, $move.is_promotion(), $promotion); + assert_flag!($move, $move.is_quiet(), $quiet, "is_quiet"); + assert_flag!( + $move, + $move.is_double_push(), + $double_push, + "is_double_push" + ); + assert_flag!($move, $move.is_en_passant(), $en_passant, "is_en_passant"); + assert_flag!($move, $move.is_capture(), $capture, "is_capture"); + assert_flag!($move, $move.is_castle(), $castle, "is_castle"); + assert_flag!($move, $move.is_promotion(), $promotion, "is_promotion"); }; } diff --git a/position/src/move_generator.rs b/position/src/move_generator.rs index 7f7d481..d2b7920 100644 --- a/position/src/move_generator.rs +++ b/position/src/move_generator.rs @@ -66,7 +66,7 @@ macro_rules! move_generator_declaration { ($name:ident, getters) => { impl $name { pub(super) fn iter(&self) -> impl Iterator + '_ { - self.move_sets.values().map(|set| set.moves()).flatten() + self.move_sets.values().flat_map(|set| set.moves()) } pub(crate) fn moves_for_piece( @@ -161,7 +161,7 @@ impl Moves { } } - fn iter(&self) -> impl Iterator + '_ { + pub fn iter(&self) -> impl Iterator + '_ { self.pawn_moves .iter() .chain(self.knight_moves.iter()) diff --git a/position/src/move_generator/move_set.rs b/position/src/move_generator/move_set.rs index f8f3ca1..6cefb7a 100644 --- a/position/src/move_generator/move_set.rs +++ b/position/src/move_generator/move_set.rs @@ -2,15 +2,16 @@ use crate::{r#move::Castle, Move, MoveBuilder}; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece, PlacedPiece, Shape}; +use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; +/// A set of bitboards defining the moves for a single piece on the board. #[derive(Clone, Debug, Default, Eq, PartialEq)] struct BitBoardSet { quiet: BitBoard, captures: BitBoard, } -/// A set of moves for a piece on the board. +/// A set of moves for a single piece on the board. #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct MoveSet { piece: PlacedPiece, @@ -27,6 +28,17 @@ impl MoveSet { } } + pub(crate) fn can_move_to_square(&self, to: Square) -> bool { + self.bitboard().is_set(to) + } + + pub(crate) fn can_castle(&self, castle: Castle) -> bool { + match castle { + Castle::KingSide => (self.special & 0b1) != 0, + Castle::QueenSide => (self.special & 0b10) != 0, + } + } + pub(super) fn quiet_moves(mut self, bitboard: BitBoard) -> MoveSet { self.bitboards.quiet = bitboard; self @@ -52,7 +64,7 @@ impl MoveSet { self.bitboards.captures | self.bitboards.quiet } - pub(super) fn moves(&self) -> impl Iterator + '_ { + pub(crate) fn moves(&self) -> impl Iterator + '_ { let piece = self.piece.piece(); let from_square = self.piece.square(); diff --git a/position/src/move_generator/pawn.rs b/position/src/move_generator/pawn.rs index c8b2e6c..8fef802 100644 --- a/position/src/move_generator/pawn.rs +++ b/position/src/move_generator/pawn.rs @@ -3,7 +3,7 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::Position; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Piece, PlacedPiece, Rank, Shape, Square}; +use chessfriend_core::{Color, PlacedPiece, Rank, Shape, Square}; #[derive(Debug)] struct MoveIterator(usize, usize); @@ -32,18 +32,31 @@ impl MoveGeneratorInternal for PawnMoveGenerator { impl PawnMoveGenerator { fn pushes(position: &Position, piece: PlacedPiece) -> BitBoard { + let color = piece.color(); let square = piece.square(); let bitboard: BitBoard = square.into(); - let starting_rank = Rank::PAWN_STARTING_RANKS[piece.color() as usize]; - + let starting_rank = Rank::PAWN_STARTING_RANKS[color as usize]; let empty_squares = position.empty_squares(); - let mut moves = bitboard.shift_north_one() & empty_squares; - if !(bitboard & BitBoard::rank(starting_rank.as_index())).is_empty() { - moves |= moves.shift_north_one() & empty_squares; - } - moves + match color { + Color::White => { + let mut moves = bitboard.shift_north_one() & empty_squares; + if !(bitboard & BitBoard::rank(starting_rank.as_index())).is_empty() { + moves |= moves.shift_north_one() & empty_squares; + } + + moves + } + Color::Black => { + let mut moves = bitboard.shift_south_one() & empty_squares; + if !(bitboard & BitBoard::rank(starting_rank.as_index())).is_empty() { + moves |= moves.shift_south_one() & empty_squares; + } + + moves + } + } } fn attacks(position: &Position, piece: PlacedPiece) -> BitBoard { @@ -63,7 +76,7 @@ impl PawnMoveGenerator { mod tests { use super::*; use crate::{assert_move_list, position::DiagramFormatter, test_position, Move, MoveBuilder}; - use chessfriend_core::{piece, Color, Square}; + use chessfriend_core::{piece, Color, Piece, Square}; use std::collections::HashSet; #[test] diff --git a/position/src/position/builders/move_builder.rs b/position/src/position/builders/move_builder.rs index 85310d8..c926c08 100644 --- a/position/src/position/builders/move_builder.rs +++ b/position/src/position/builders/move_builder.rs @@ -1,6 +1,10 @@ // Eryn Wells -use crate::{position::flags::Flags, r#move::Castle, MakeMoveError, Move, Position}; +use crate::{ + position::flags::Flags, + r#move::{AlgebraicMoveFormatter, Castle}, + MakeMoveError, Move, Position, +}; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Direction, Piece, PlacedPiece, Shape, Square}; @@ -60,9 +64,22 @@ where let to_square = mv.to_square(); - let sight = self.position.sight_of_piece(&piece); - if !sight.is_set(to_square) { - return Err(MakeMoveError::IllegalSquare(to_square)); + let moves = self + .position + .moves_for_piece(&piece) + .ok_or(MakeMoveError::NoLegalMoves)?; + + match mv.castle() { + Some(castle) => { + if !moves.can_castle(castle) { + return Err(MakeMoveError::IllegalCastle); + } + } + None => { + if !moves.can_move_to_square(to_square) { + return Err(MakeMoveError::IllegalSquare(to_square)); + } + } } let player = self.position.player_to_move(); @@ -324,7 +341,7 @@ mod tests { assert!(black_pawn_move.is_double_push()); assert!(!black_pawn_move.is_en_passant()); - let en_passant_position = Builder::::new(&pos).make(&black_pawn_move)?.build(); + let en_passant_position = Builder::new(&pos).make(&black_pawn_move)?.build(); println!("{en_passant_position}"); assert_eq!( diff --git a/position/src/position/position.rs b/position/src/position/position.rs index f0f8414..1785c04 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -2,8 +2,8 @@ use super::{flags::Flags, piece_sets::PieceBitBoards, Pieces}; use crate::{ - check::{self, CheckingPieces}, - move_generator::Moves, + check::CheckingPieces, + move_generator::{MoveSet, Moves}, position::DiagramFormatter, r#move::Castle, sight::SightExt, @@ -216,6 +216,10 @@ impl Position { .fold(BitBoard::empty(), |acc, sight| acc | sight) } + pub(crate) fn moves_for_piece(&self, piece: &PlacedPiece) -> Option<&MoveSet> { + self.moves().moves_for_piece(piece) + } + pub(crate) fn sight_of_piece(&self, piece: &PlacedPiece) -> BitBoard { piece.sight(&self.pieces, self.en_passant_square) } @@ -236,7 +240,7 @@ impl Position { pub(crate) fn is_king_in_check(&self) -> bool { let danger_squares = self.king_danger(self.color_to_move); - (danger_squares & self.king_bitboard(self.color_to_move)).is_empty() + !(danger_squares & self.king_bitboard(self.color_to_move)).is_empty() } fn king_bitboard(&self, player: Color) -> &BitBoard { From 6c14851806c5a78a040a54a5b33fe9579ce18623 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 3 Feb 2024 10:05:11 -0800 Subject: [PATCH 145/423] [position] Move whole-module move_generator tests to a tests/ module --- position/src/move_generator.rs | 39 +------------------ position/src/move_generator/tests.rs | 3 ++ .../src/move_generator/tests/single_pieces.rs | 36 +++++++++++++++++ 3 files changed, 40 insertions(+), 38 deletions(-) create mode 100644 position/src/move_generator/tests.rs create mode 100644 position/src/move_generator/tests/single_pieces.rs diff --git a/position/src/move_generator.rs b/position/src/move_generator.rs index d2b7920..5a79fb0 100644 --- a/position/src/move_generator.rs +++ b/position/src/move_generator.rs @@ -7,6 +7,7 @@ mod move_set; mod pawn; mod queen; mod rook; +mod tests; pub(crate) use move_set::MoveSet; @@ -171,41 +172,3 @@ impl Moves { .chain(self.king_moves.iter()) } } - -#[cfg(test)] -mod tests { - use crate::{position, r#move::AlgebraicMoveFormatter, Move, MoveBuilder}; - use chessfriend_core::{piece, Square}; - use std::collections::HashSet; - - #[test] - fn one_king() { - let pos = position![ - White King on D3, - Black King on H6, - ]; - - let expected_moves = HashSet::from_iter([ - MoveBuilder::new(piece!(White King), Square::D3, Square::D4).build(), - MoveBuilder::new(piece!(White King), Square::D3, Square::E4).build(), - MoveBuilder::new(piece!(White King), Square::D3, Square::E3).build(), - MoveBuilder::new(piece!(White King), Square::D3, Square::E2).build(), - MoveBuilder::new(piece!(White King), Square::D3, Square::D2).build(), - MoveBuilder::new(piece!(White King), Square::D3, Square::C2).build(), - MoveBuilder::new(piece!(White King), Square::D3, Square::C3).build(), - MoveBuilder::new(piece!(White King), Square::D3, Square::C4).build(), - ]); - - let generated_moves: HashSet = pos.moves().iter().collect(); - - assert_eq!( - generated_moves, - expected_moves, - "{:?}", - generated_moves - .symmetric_difference(&expected_moves) - .map(|m| format!("{}", AlgebraicMoveFormatter::new(&m, &pos))) - .collect::>() - ); - } -} diff --git a/position/src/move_generator/tests.rs b/position/src/move_generator/tests.rs new file mode 100644 index 0000000..777faf8 --- /dev/null +++ b/position/src/move_generator/tests.rs @@ -0,0 +1,3 @@ +// Eryn Wells + +mod single_pieces; diff --git a/position/src/move_generator/tests/single_pieces.rs b/position/src/move_generator/tests/single_pieces.rs new file mode 100644 index 0000000..01a3e34 --- /dev/null +++ b/position/src/move_generator/tests/single_pieces.rs @@ -0,0 +1,36 @@ +// Eryn Wells + +use crate::{position, r#move::AlgebraicMoveFormatter, Move, MoveBuilder}; +use chessfriend_core::{piece, Square}; +use std::collections::HashSet; + +#[test] +fn one_king() { + let pos = position![ + White King on D3, + Black King on H6, + ]; + + let expected_moves = HashSet::from_iter([ + MoveBuilder::new(piece!(White King), Square::D3, Square::D4).build(), + MoveBuilder::new(piece!(White King), Square::D3, Square::E4).build(), + MoveBuilder::new(piece!(White King), Square::D3, Square::E3).build(), + MoveBuilder::new(piece!(White King), Square::D3, Square::E2).build(), + MoveBuilder::new(piece!(White King), Square::D3, Square::D2).build(), + MoveBuilder::new(piece!(White King), Square::D3, Square::C2).build(), + MoveBuilder::new(piece!(White King), Square::D3, Square::C3).build(), + MoveBuilder::new(piece!(White King), Square::D3, Square::C4).build(), + ]); + + let generated_moves: HashSet = pos.moves().iter().collect(); + + assert_eq!( + generated_moves, + expected_moves, + "{:?}", + generated_moves + .symmetric_difference(&expected_moves) + .map(|m| format!("{}", AlgebraicMoveFormatter::new(&m, &pos))) + .collect::>() + ); +} From f4e57d7d6c5e086ae73895157630c6978e53b284 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 3 Feb 2024 15:16:00 -0800 Subject: [PATCH 146/423] [position] Implement all the example positions from Peter Ellis Jones' blog post https://peterellisjones.com/posts/generating-legal-chess-moves-efficiently/ --- position/src/lib.rs | 1 - position/src/macros.rs | 42 +++- position/src/move_generator/tests.rs | 1 + .../move_generator/tests/peterellisjones.rs | 226 ++++++++++++++++++ position/src/tests.rs | 15 ++ 5 files changed, 282 insertions(+), 3 deletions(-) create mode 100644 position/src/move_generator/tests/peterellisjones.rs diff --git a/position/src/lib.rs b/position/src/lib.rs index dde3e5d..5776e05 100644 --- a/position/src/lib.rs +++ b/position/src/lib.rs @@ -12,7 +12,6 @@ mod sight; #[macro_use] mod macros; -#[cfg(test)] #[macro_use] mod tests; diff --git a/position/src/macros.rs b/position/src/macros.rs index 178b4c4..c7998d7 100644 --- a/position/src/macros.rs +++ b/position/src/macros.rs @@ -16,10 +16,48 @@ macro_rules! position { }; } -#[cfg(test)] #[macro_export] macro_rules! test_position { - [$($color:ident $shape:ident on $square:ident),* $(,)?] => { + ($to_move:ident, [ $($color:ident $shape:ident on $square:ident),* $(,)? ], $en_passant:ident) => { + { + let pos = $crate::PositionBuilder::new() + $(.place_piece( + chessfriend_core::PlacedPiece::new( + chessfriend_core::Piece::new( + chessfriend_core::Color::$color, + chessfriend_core::Shape::$shape + ), + chessfriend_core::Square::$square + )) + )* + .to_move(chessfriend_core::Color::$to_move) + .en_passant_square(Some(chessfriend_core::Square::$en_passant)) + .build(); + println!("{pos}"); + + pos + } + }; + ($to_move:ident, [ $($color:ident $shape:ident on $square:ident),* $(,)? ]) => { + { + let pos = $crate::PositionBuilder::new() + $(.place_piece( + chessfriend_core::PlacedPiece::new( + chessfriend_core::Piece::new( + chessfriend_core::Color::$color, + chessfriend_core::Shape::$shape + ), + chessfriend_core::Square::$square + )) + )* + .to_move(chessfriend_core::Color::$to_move) + .build(); + println!("{pos}"); + + pos + } + }; + ($($color:ident $shape:ident on $square:ident),* $(,)?) => { { let pos = $crate::PositionBuilder::new() $(.place_piece( diff --git a/position/src/move_generator/tests.rs b/position/src/move_generator/tests.rs index 777faf8..e6674d3 100644 --- a/position/src/move_generator/tests.rs +++ b/position/src/move_generator/tests.rs @@ -1,3 +1,4 @@ // Eryn Wells +mod peterellisjones; mod single_pieces; diff --git a/position/src/move_generator/tests/peterellisjones.rs b/position/src/move_generator/tests/peterellisjones.rs new file mode 100644 index 0000000..52b0269 --- /dev/null +++ b/position/src/move_generator/tests/peterellisjones.rs @@ -0,0 +1,226 @@ +// Eryn Wells + +//! Move generator tests based on board positions described in [Peter Ellis +//! Jones][1]' excellent [blog post][2] on generated legal chess moves. +//! +//! [1]: https://peterellisjones.com +//! [2]: https://peterellisjones.com/posts/generating-legal-chess-moves-efficiently/ + +use crate::{ + assert_move_list, formatted_move_list, move_generator::Moves, r#move::AlgebraicMoveFormatter, + test_position, Move, MoveBuilder, +}; +use chessfriend_core::{piece, Square}; +use std::collections::HashSet; + +#[test] +fn pseudo_legal_move_generation() -> Result<(), String> { + let pos = test_position!(Black, [ + Black King on E8, + White King on E1, + White Rook on F5, + ]); + + let generated_moves = pos.moves(); + let king_moves = generated_moves + .moves_for_piece(&piece!(Black King on E8)) + .ok_or("No valid king moves")?; + + assert!(!king_moves.can_move_to_square(Square::F8)); + assert!(!king_moves.can_move_to_square(Square::F7)); + + Ok(()) +} + +#[test] +fn gotcha_king_moves_away_from_a_checking_slider() -> Result<(), String> { + let pos = test_position!(Black, [ + Black King on E7, + White King on E1, + White Rook on E4, + ]); + + let generated_moves = pos.moves(); + let king_moves = generated_moves + .moves_for_piece(&piece!(Black King on E7)) + .ok_or("No valid king moves")?; + + assert!(!king_moves.can_move_to_square(Square::E8)); + + Ok(()) +} + +#[test] +fn check_evasions_1() { + let pos = test_position!(Black, [ + Black King on E8, + White King on E1, + White Knight on F6, + ]); + + let generated_moves = pos.moves(); + + let expected_moves = HashSet::from_iter([ + MoveBuilder::new(piece!(Black King), Square::E8, Square::D8).build(), + MoveBuilder::new(piece!(Black King), Square::E8, Square::E7).build(), + MoveBuilder::new(piece!(Black King), Square::E8, Square::F7).build(), + MoveBuilder::new(piece!(Black King), Square::E8, Square::F8).build(), + ]); + + assert_move_list!( + generated_moves.iter().collect::>(), + expected_moves, + pos + ); +} + +#[test] +fn check_evasions_double_check() { + let pos = test_position!(Black, [ + Black King on E8, + Black Bishop on F6, + White King on E1, + White Knight on G7, + White Rook on E5, + ]); + + let generated_moves = pos.moves(); + + let expected_moves = HashSet::from_iter([ + MoveBuilder::new(piece!(Black King), Square::E8, Square::D8).build(), + MoveBuilder::new(piece!(Black King), Square::E8, Square::D7).build(), + MoveBuilder::new(piece!(Black King), Square::E8, Square::F7).build(), + MoveBuilder::new(piece!(Black King), Square::E8, Square::F8).build(), + ]); + + assert_move_list!( + generated_moves.iter().collect::>(), + expected_moves, + pos + ); +} + +#[test] +fn single_check_with_blocker() { + let pos = test_position!(Black, [ + Black King on E8, + Black Knight on G6, + White King on E1, + White Rook on E5, + ]); + + let generated_moves = pos.moves(); + + let expected_moves = HashSet::from_iter([ + MoveBuilder::new(piece!(Black King), Square::E8, Square::D8).build(), + MoveBuilder::new(piece!(Black King), Square::E8, Square::D7).build(), + MoveBuilder::new(piece!(Black King), Square::E8, Square::F7).build(), + MoveBuilder::new(piece!(Black King), Square::E8, Square::F8).build(), + MoveBuilder::new(piece!(Black Knight), Square::G6, Square::E7).build(), + MoveBuilder::new(piece!(Black Knight), Square::G6, Square::E5) + .capturing(piece!(White Rook on E5)) + .build(), + ]); + + assert_move_list!( + generated_moves.iter().collect::>(), + expected_moves, + pos + ); +} + +#[test] +fn en_passant_check_capture() { + let pos = test_position!(Black, [ + Black King on C5, + Black Pawn on E4, + White Pawn on D4, + ], D3); + + assert!(pos.is_king_in_check()); + + let generated_moves = pos.moves().iter().collect::>(); + + assert!( + generated_moves.contains( + &MoveBuilder::new(piece!(Black Pawn), Square::E4, Square::D3) + .capturing_en_passant(piece!(White Pawn on D4)) + .build() + ), + "Valid moves: {:?}", + formatted_move_list!(generated_moves, pos) + ); +} + +#[test] +fn en_passant_check_block() { + let pos = test_position!(Black, [ + Black King on B5, + Black Pawn on E4, + White Pawn on D4, + White Queen on F1, + ], D3); + + assert!(pos.is_king_in_check()); + + let generated_moves = pos.moves().iter().collect::>(); + + assert!( + generated_moves.contains( + &MoveBuilder::new(piece!(Black Pawn), Square::E4, Square::D3) + .capturing_en_passant(piece!(White Pawn on D4)) + .build() + ), + "Valid moves: {:?}", + formatted_move_list!(generated_moves, pos) + ); +} + +#[test] +fn pinned_pieces_rook_cannot_move_out_of_pin() -> Result<(), String> { + let pos = test_position!(Black, [ + Black King on E8, + Black Rook on E6, + White Queen on E3, + White King on C1, + ]); + + assert!(!pos.is_king_in_check()); + + let generated_moves = pos.moves(); + let rook_moves = generated_moves + .moves_for_piece(&piece!(Black Rook on E6)) + .ok_or("No valid rook moves")?; + + assert!(!rook_moves.can_move_to_square(Square::D6)); + assert!(!rook_moves.can_move_to_square(Square::F6)); + + assert!(rook_moves.can_move_to_square(Square::E7)); + assert!(rook_moves.can_move_to_square(Square::E5)); + assert!(rook_moves.can_move_to_square(Square::E4)); + assert!(rook_moves.can_move_to_square(Square::E3)); + + Ok(()) +} + +#[test] +fn en_passant_discovered_check() { + let pos = test_position!(Black, [ + Black King on A4, + Black Pawn on E4, + White Pawn on D4, + White Queen on H4, + ], D3); + + let generated_moves = pos.moves().iter().collect::>(); + + assert!( + generated_moves.contains( + &MoveBuilder::new(piece!(Black Pawn), Square::E4, Square::D3) + .capturing_en_passant(piece!(White Pawn on D4)) + .build() + ), + "Valid moves: {:?}", + formatted_move_list!(generated_moves, pos) + ); +} diff --git a/position/src/tests.rs b/position/src/tests.rs index 8663afa..962b9bc 100644 --- a/position/src/tests.rs +++ b/position/src/tests.rs @@ -17,3 +17,18 @@ macro_rules! assert_move_list { ) }; } + +#[macro_export] +macro_rules! formatted_move_list { + ($move_list:expr, $position:expr) => { + $move_list + .iter() + .map(|mv| { + format!( + "{}", + $crate::r#move::AlgebraicMoveFormatter::new(mv, &$position) + ) + }) + .collect::>() + }; +} From 17410936abdcd72d7305023a17ba87a70ca93dc9 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 4 Feb 2024 19:26:11 -0800 Subject: [PATCH 147/423] [position] Remove the rook_push_mask test from check.rs This test now lives in move_generator::tests::peterellisjones --- position/src/check.rs | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/position/src/check.rs b/position/src/check.rs index df30749..218ef58 100644 --- a/position/src/check.rs +++ b/position/src/check.rs @@ -71,27 +71,3 @@ impl CheckingPieces { &self.bitboards[shape as usize] } } - -#[cfg(test)] -mod tests { - use super::*; - use chessfriend_bitboard::{bitboard, BitBoard}; - - /// This is a test position from [this execellent blog post][1] about how to - /// efficiently generate legal chess moves. - /// - /// [1]: https://peterellisjones.com/posts/generating-legal-chess-moves-efficiently/ - #[test] - fn rook_push_mask() { - let checks = CheckingPieces::new( - BitBoard::EMPTY, - BitBoard::EMPTY, - BitBoard::EMPTY, - bitboard![E5], - BitBoard::EMPTY, - ); - - let push_mask = checks.push_mask(&bitboard![E8]); - assert_eq!(push_mask, bitboard![E6, E7]); - } -} From 4b35051deb99bbe1b0d1220a658d89f87c1b3a05 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 4 Feb 2024 19:26:41 -0800 Subject: [PATCH 148/423] [position] Derive several traits for CheckingPieces Clone, Debug, Eq, and PartialEq. --- position/src/check.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/position/src/check.rs b/position/src/check.rs index 218ef58..05e34b7 100644 --- a/position/src/check.rs +++ b/position/src/check.rs @@ -5,6 +5,7 @@ use chessfriend_core::{Color, Direction, Shape, Square}; use crate::sight::SliderRayToSquareExt; +#[derive(Clone, Debug, Eq, PartialEq)] pub struct CheckingPieces { bitboards: [BitBoard; 5], } @@ -23,7 +24,7 @@ impl CheckingPieces { } pub fn count(&self) -> u32 { - self.bitboards.iter().map(|b| b.population_count()).sum() + self.bitboards.iter().map(BitBoard::population_count).sum() } /// A BitBoard representing the set of pieces that must be captured to From a8034c79aa5cecb08953a9733e385b30e157eb16 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 5 Feb 2024 13:59:26 -0800 Subject: [PATCH 149/423] [bitboard] Use a OnceLock to hold the global BitBoard library instance Thread safe and a single object instead of two! --- bitboard/src/library.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bitboard/src/library.rs b/bitboard/src/library.rs index 4615caa..afc4240 100644 --- a/bitboard/src/library.rs +++ b/bitboard/src/library.rs @@ -2,7 +2,7 @@ use crate::BitBoard; use chessfriend_core::{Color, Direction, Square}; -use std::sync::Once; +use std::sync::OnceLock; pub(super) const RANKS: [BitBoard; 8] = [ BitBoard(0xFF << 0 * 8), @@ -42,15 +42,15 @@ pub(crate) const LIGHT_SQUARES: BitBoard = pub(crate) const DARK_SQUARES: BitBoard = BitBoard(!LIGHT_SQUARES.0); pub(super) fn library() -> &'static MoveLibrary { - static MOVE_LIBRARY_INIT: Once = Once::new(); - static mut MOVE_LIBRARY: MoveLibrary = MoveLibrary::new(); + static mut MOVE_LIBRARY: OnceLock = OnceLock::new(); unsafe { - MOVE_LIBRARY_INIT.call_once(|| { - MOVE_LIBRARY.init(); - }); + MOVE_LIBRARY.get_or_init(|| { + let mut library = MoveLibrary::new(); + library.init(); - &MOVE_LIBRARY + library + }) } } From a5e8f33afeb054ea6e72bf19f6f3656266c074d8 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 5 Feb 2024 13:59:44 -0800 Subject: [PATCH 150/423] [core] Implement Square::file_rank() Returns a tuple of the square's file and rank. --- core/src/coordinates.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/src/coordinates.rs b/core/src/coordinates.rs index f40ccb1..68ff3fe 100644 --- a/core/src/coordinates.rs +++ b/core/src/coordinates.rs @@ -208,6 +208,10 @@ impl Square { unsafe { Rank::new_unchecked((self as u8) >> 3) } } + pub fn file_rank(&self) -> (File, Rank) { + (self.file(), self.rank()) + } + pub fn neighbor(self, direction: Direction) -> Option { let index: u8 = self as u8; let dir: i8 = direction.to_offset(); From 891b3ddbb9a098b4050cf7c34a234078d7d0456b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 5 Feb 2024 14:00:23 -0800 Subject: [PATCH 151/423] [position] Remove the sight data from Position --- position/src/position/position.rs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/position/src/position/position.rs b/position/src/position/position.rs index 1785c04..ab5a71a 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -18,7 +18,6 @@ pub struct Position { flags: Flags, pieces: PieceBitBoards, en_passant_square: Option, - sight: [OnceCell; 2], moves: OnceCell, half_move_counter: u16, full_move_number: u16, @@ -187,13 +186,6 @@ impl Position { self.en_passant_square } - /// A bitboard representing the squares the pieces of the given color can - /// see. This is synonymous with the squares attacked by the player's - /// pieces. - pub(crate) fn sight_of_player(&self, color: Color) -> BitBoard { - *self.sight[color as usize].get_or_init(|| self._sight_of_player(color, &self.pieces)) - } - fn _sight_of_player(&self, player: Color, pieces: &PieceBitBoards) -> BitBoard { let en_passant_square = self.en_passant_square; @@ -308,10 +300,9 @@ impl Position { flags, en_passant_square, pieces, - sight: [OnceCell::new(), OnceCell::new()], - moves: OnceCell::new(), half_move_counter, full_move_number, + ..Default::default() } } @@ -349,7 +340,6 @@ impl Default for Position { flags: Flags::default(), pieces: PieceBitBoards::default(), en_passant_square: None, - sight: [OnceCell::new(), OnceCell::new()], moves: OnceCell::new(), half_move_counter: 0, full_move_number: 1, From 997621eea73e0058653e7159da904e31cb1c84cb Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 11 Feb 2024 08:44:11 -0700 Subject: [PATCH 152/423] [bitboard] Make a few tweaks to some definitions in BitBoard Use u64::MIN and u64::MAX to define the empty and full bitboards Write From as `1u64 << sq as u32` Write a doc test for BitBoard::is_single_square() Make library::RANKS and library::FILES pub(crate) instead of pub(super) --- bitboard/src/bitboard.rs | 17 ++++++++++++++--- bitboard/src/library.rs | 4 ++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 2453cce..66a5336 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -18,8 +18,8 @@ macro_rules! moves_getter { } impl BitBoard { - pub const EMPTY: BitBoard = BitBoard(0); - pub const FULL: BitBoard = BitBoard(0xFFFFFFFFFFFFFFFF); + pub const EMPTY: BitBoard = BitBoard(u64::MIN); + pub const FULL: BitBoard = BitBoard(u64::MAX); pub const fn empty() -> BitBoard { BitBoard(0) @@ -104,6 +104,17 @@ impl BitBoard { *self &= !sq_bb } + /// Returns `true` if this BitBoard represents a single square. + /// + /// ## Examples + /// + /// ``` + /// use chessfriend_bitboard::BitBoard; + /// assert!(!BitBoard::EMPTY.is_single_square(), "Empty bitboards represent no squares"); + /// assert!(!BitBoard::FULL.is_single_square(), "Full bitboards represent all the squares"); + /// assert!(!BitBoard::new(0b010011110101101100).is_single_square(), "This bitboard represents a bunch of squares"); + /// assert!(BitBoard::new(0b10000000000000).is_single_square()); + /// ``` pub fn is_single_square(&self) -> bool { self.0.is_power_of_two() } @@ -149,7 +160,7 @@ impl Default for BitBoard { impl From for BitBoard { fn from(value: Square) -> Self { - BitBoard(1 << value as u64) + BitBoard(1u64 << value as u32) } } diff --git a/bitboard/src/library.rs b/bitboard/src/library.rs index afc4240..5e74259 100644 --- a/bitboard/src/library.rs +++ b/bitboard/src/library.rs @@ -4,7 +4,7 @@ use crate::BitBoard; use chessfriend_core::{Color, Direction, Square}; use std::sync::OnceLock; -pub(super) const RANKS: [BitBoard; 8] = [ +pub(crate) const RANKS: [BitBoard; 8] = [ BitBoard(0xFF << 0 * 8), BitBoard(0xFF << 1 * 8), BitBoard(0xFF << 2 * 8), @@ -15,7 +15,7 @@ pub(super) const RANKS: [BitBoard; 8] = [ BitBoard(0xFF << 7 * 8), ]; -pub(super) const FILES: [BitBoard; 8] = [ +pub(crate) const FILES: [BitBoard; 8] = [ BitBoard(0x0101010101010101 << 0), BitBoard(0x0101010101010101 << 1), BitBoard(0x0101010101010101 << 2), From 742b00119a07ff0ac9aad814663839aed7534274 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 11 Feb 2024 08:44:46 -0700 Subject: [PATCH 153/423] [bitboard] Implement From, From for BitBoard and TryFrom for Square --- bitboard/src/bitboard.rs | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 66a5336..a8e2b3c 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -2,7 +2,7 @@ use crate::library; use crate::{LeadingBitScanner, TrailingBitScanner}; -use chessfriend_core::{Color, Direction, Square}; +use chessfriend_core::{Color, Direction, File, Rank, Square}; use std::fmt; use std::ops::Not; @@ -158,6 +158,18 @@ impl Default for BitBoard { } } +impl From for BitBoard { + fn from(value: File) -> Self { + library::FILES[*value.as_index() as usize] + } +} + +impl From for BitBoard { + fn from(value: Rank) -> Self { + library::FILES[*value.as_index() as usize] + } +} + impl From for BitBoard { fn from(value: Square) -> Self { BitBoard(1u64 << value as u32) @@ -176,6 +188,23 @@ impl FromIterator for BitBoard { } } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum TryFromBitBoardError { + NotSingleSquare, +} + +impl TryFrom for Square { + type Error = TryFromBitBoardError; + + fn try_from(value: BitBoard) -> Result { + if !value.is_single_square() { + return Err(TryFromBitBoardError::NotSingleSquare); + } + + unsafe { Ok(Square::from_index(value.0.trailing_zeros() as u8)) } + } +} + impl fmt::Binary for BitBoard { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // Delegate to u64's implementation of Binary. From 0201668563cd49935dd91b0853dcf19a872a10e5 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 11 Feb 2024 08:45:19 -0700 Subject: [PATCH 154/423] [core] Move the Unicode piece table to a helper to_unicode() method on Piece --- core/src/pieces.rs | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/core/src/pieces.rs b/core/src/pieces.rs index b49db6d..148f05f 100644 --- a/core/src/pieces.rs +++ b/core/src/pieces.rs @@ -136,30 +136,34 @@ impl Piece { is_shape!(is_king, King); pub fn to_ascii(&self) -> char { - self.shape.to_ascii() + let ch = self.shape.to_ascii(); + match self.color { + Color::White => ch, + Color::Black => ch.to_ascii_lowercase(), + } + } + + fn to_unicode(&self) -> char { + match (self.color, self.shape) { + (Color::Black, Shape::Pawn) => '♟', + (Color::Black, Shape::Knight) => '♞', + (Color::Black, Shape::Bishop) => '♝', + (Color::Black, Shape::Rook) => '♜', + (Color::Black, Shape::Queen) => '♛', + (Color::Black, Shape::King) => '♚', + (Color::White, Shape::Pawn) => '♙', + (Color::White, Shape::Knight) => '♘', + (Color::White, Shape::Bishop) => '♗', + (Color::White, Shape::Rook) => '♖', + (Color::White, Shape::Queen) => '♕', + (Color::White, Shape::King) => '♔', + } } } impl fmt::Display for Piece { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match (self.color, self.shape) { - (Color::Black, Shape::Pawn) => '♟', - (Color::Black, Shape::Knight) => '♞', - (Color::Black, Shape::Bishop) => '♝', - (Color::Black, Shape::Rook) => '♜', - (Color::Black, Shape::Queen) => '♛', - (Color::Black, Shape::King) => '♚', - (Color::White, Shape::Pawn) => '♙', - (Color::White, Shape::Knight) => '♘', - (Color::White, Shape::Bishop) => '♗', - (Color::White, Shape::Rook) => '♖', - (Color::White, Shape::Queen) => '♕', - (Color::White, Shape::King) => '♔', - } - ) + write!(f, "{}", self.to_unicode()) } } From eb192e6dc41518a0d493fa598323464f8d1223d6 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 11 Feb 2024 09:56:24 -0700 Subject: [PATCH 155/423] [position] Fix the peterellisjones::en_passant_discovered_check test This test was asserting that an e.p. move is included in the list of generated moves. Actually, the position does NOT allow an e.p. move for black. --- position/src/move_generator/tests/peterellisjones.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/position/src/move_generator/tests/peterellisjones.rs b/position/src/move_generator/tests/peterellisjones.rs index 52b0269..3828970 100644 --- a/position/src/move_generator/tests/peterellisjones.rs +++ b/position/src/move_generator/tests/peterellisjones.rs @@ -214,12 +214,12 @@ fn en_passant_discovered_check() { let generated_moves = pos.moves().iter().collect::>(); + let unexpected_move = MoveBuilder::new(piece!(Black Pawn), Square::E4, Square::D3) + .capturing_en_passant(piece!(White Pawn on D4)) + .build(); + assert!( - generated_moves.contains( - &MoveBuilder::new(piece!(Black Pawn), Square::E4, Square::D3) - .capturing_en_passant(piece!(White Pawn on D4)) - .build() - ), + !generated_moves.contains(&unexpected_move), "Valid moves: {:?}", formatted_move_list!(generated_moves, pos) ); From 29eb56a5d705dcd1fde3fe68f99e58593cbf13e8 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 11 Feb 2024 09:57:02 -0700 Subject: [PATCH 156/423] [position] Remove some dead code from position::piece_sets --- position/src/position/piece_sets.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/position/src/position/piece_sets.rs b/position/src/position/piece_sets.rs index 095d184..7bde2b1 100644 --- a/position/src/position/piece_sets.rs +++ b/position/src/position/piece_sets.rs @@ -1,7 +1,7 @@ // Eryn Wells use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; +use chessfriend_core::{Color, Piece, PlacedPiece, Square}; #[derive(Debug, Eq, PartialEq)] pub enum PlacePieceStrategy { @@ -51,11 +51,6 @@ impl PieceBitBoards { } } - pub(super) fn king(&self, color: Color) -> &BitBoard { - self.by_color_and_shape - .bitboard_for_piece(&Piece::new(color, Shape::King)) - } - pub(crate) fn all_pieces(&self) -> &BitBoard { self.by_color.all() } From 41c3a2075ca5537c7d6d2a037d458f322ae40710 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 11 Feb 2024 10:17:09 -0700 Subject: [PATCH 157/423] [position] Pass the PlacedPiece in move_set_for_piece() by reference to all the MoveGenerators --- position/src/move_generator.rs | 4 ++-- position/src/move_generator/bishop.rs | 4 ++-- position/src/move_generator/king.rs | 4 ++-- position/src/move_generator/knight.rs | 4 ++-- position/src/move_generator/pawn.rs | 8 ++++---- position/src/move_generator/queen.rs | 4 ++-- position/src/move_generator/rook.rs | 4 ++-- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/position/src/move_generator.rs b/position/src/move_generator.rs index 5a79fb0..e6884d4 100644 --- a/position/src/move_generator.rs +++ b/position/src/move_generator.rs @@ -110,7 +110,7 @@ trait MoveGeneratorInternal { .map(|square| { let piece = PlacedPiece::new(piece, square); let move_set = - Self::move_set_for_piece(position, piece, capture_mask, push_mask); + Self::move_set_for_piece(position, &piece, capture_mask, push_mask); (square, move_set) }), ) @@ -118,7 +118,7 @@ trait MoveGeneratorInternal { fn move_set_for_piece( position: &Position, - piece: PlacedPiece, + piece: &PlacedPiece, capture_mask: BitBoard, push_mask: BitBoard, ) -> MoveSet; diff --git a/position/src/move_generator/bishop.rs b/position/src/move_generator/bishop.rs index 2fae36f..5630246 100644 --- a/position/src/move_generator/bishop.rs +++ b/position/src/move_generator/bishop.rs @@ -14,7 +14,7 @@ impl MoveGeneratorInternal for ClassicalMoveGenerator { fn move_set_for_piece( position: &Position, - piece: PlacedPiece, + piece: &PlacedPiece, capture_mask: BitBoard, push_mask: BitBoard, ) -> MoveSet { @@ -49,7 +49,7 @@ impl MoveGeneratorInternal for ClassicalMoveGenerator { let quiet_moves = all_moves & (empty_squares | !friendly_pieces) & push_mask; let capture_moves = all_moves & opposing_pieces & capture_mask; - MoveSet::new(piece) + MoveSet::new(*piece) .quiet_moves(quiet_moves) .capture_moves(capture_moves) } diff --git a/position/src/move_generator/king.rs b/position/src/move_generator/king.rs index c18692a..befd0b2 100644 --- a/position/src/move_generator/king.rs +++ b/position/src/move_generator/king.rs @@ -19,7 +19,7 @@ impl MoveGeneratorInternal for KingMoveGenerator { fn move_set_for_piece( position: &Position, - placed_piece: PlacedPiece, + placed_piece: &PlacedPiece, _capture_mask: BitBoard, _push_mask: BitBoard, ) -> MoveSet { @@ -39,7 +39,7 @@ impl MoveGeneratorInternal for KingMoveGenerator { let quiet_moves = all_king_moves & safe_empty_squares; let capture_moves = all_king_moves & opposing_pieces_on_safe_squares; - let mut move_set = MoveSet::new(placed_piece) + let mut move_set = MoveSet::new(*placed_piece) .quiet_moves(quiet_moves) .capture_moves(capture_moves); diff --git a/position/src/move_generator/knight.rs b/position/src/move_generator/knight.rs index b4dc1bc..6366808 100644 --- a/position/src/move_generator/knight.rs +++ b/position/src/move_generator/knight.rs @@ -14,7 +14,7 @@ impl MoveGeneratorInternal for KnightMoveGenerator { fn move_set_for_piece( position: &Position, - placed_piece: PlacedPiece, + placed_piece: &PlacedPiece, capture_mask: BitBoard, push_mask: BitBoard, ) -> MoveSet { @@ -25,7 +25,7 @@ impl MoveGeneratorInternal for KnightMoveGenerator { let quiet_moves = knight_moves & empty_squares & push_mask; let capture_moves = knight_moves & opposing_pieces & capture_mask; - MoveSet::new(placed_piece) + MoveSet::new(*placed_piece) .quiet_moves(quiet_moves) .capture_moves(capture_moves) } diff --git a/position/src/move_generator/pawn.rs b/position/src/move_generator/pawn.rs index 8fef802..c6e7a5a 100644 --- a/position/src/move_generator/pawn.rs +++ b/position/src/move_generator/pawn.rs @@ -17,14 +17,14 @@ impl MoveGeneratorInternal for PawnMoveGenerator { fn move_set_for_piece( position: &Position, - placed_piece: PlacedPiece, + placed_piece: &PlacedPiece, capture_mask: BitBoard, push_mask: BitBoard, ) -> MoveSet { - let capture_moves = Self::attacks(position, placed_piece) & capture_mask; - let quiet_moves = Self::pushes(position, placed_piece) & push_mask; + let capture_moves = Self::attacks(position, &placed_piece) & capture_mask; + let quiet_moves = Self::pushes(position, &placed_piece) & push_mask; - MoveSet::new(placed_piece) + MoveSet::new(*placed_piece) .quiet_moves(quiet_moves) .capture_moves(capture_moves) } diff --git a/position/src/move_generator/queen.rs b/position/src/move_generator/queen.rs index dc41769..0b8a571 100644 --- a/position/src/move_generator/queen.rs +++ b/position/src/move_generator/queen.rs @@ -14,7 +14,7 @@ impl MoveGeneratorInternal for ClassicalMoveGenerator { fn move_set_for_piece( position: &Position, - placed_piece: PlacedPiece, + placed_piece: &PlacedPiece, capture_mask: BitBoard, push_mask: BitBoard, ) -> MoveSet { @@ -56,7 +56,7 @@ impl MoveGeneratorInternal for ClassicalMoveGenerator { let quiet_moves = all_moves & (empty_squares | !friendly_pieces) & push_mask; let capture_moves = all_moves & opposing_pieces & capture_mask; - MoveSet::new(placed_piece) + MoveSet::new(*placed_piece) .quiet_moves(quiet_moves) .capture_moves(capture_moves) } diff --git a/position/src/move_generator/rook.rs b/position/src/move_generator/rook.rs index 18129c8..8e9037a 100644 --- a/position/src/move_generator/rook.rs +++ b/position/src/move_generator/rook.rs @@ -14,7 +14,7 @@ impl MoveGeneratorInternal for ClassicalMoveGenerator { fn move_set_for_piece( position: &Position, - placed_piece: PlacedPiece, + placed_piece: &PlacedPiece, capture_mask: BitBoard, push_mask: BitBoard, ) -> MoveSet { @@ -52,7 +52,7 @@ impl MoveGeneratorInternal for ClassicalMoveGenerator { let quiet_moves = all_moves & (empty_squares | !friendly_pieces) & capture_mask; let capture_moves = all_moves & opposing_pieces & push_mask; - MoveSet::new(placed_piece) + MoveSet::new(*placed_piece) .quiet_moves(quiet_moves) .capture_moves(capture_moves) } From e94819c79a679474b486bbb1e589c4e821d3d257 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 11 Feb 2024 10:20:54 -0700 Subject: [PATCH 158/423] Visual Studio Code workspace --- ChessFriend.code-workspace | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 ChessFriend.code-workspace diff --git a/ChessFriend.code-workspace b/ChessFriend.code-workspace new file mode 100644 index 0000000..b51044b --- /dev/null +++ b/ChessFriend.code-workspace @@ -0,0 +1,7 @@ +{ + "folders": [ + { + "path": "." + } + ] +} From 1958c1a50e43af112637e50986bf0179d9cf012b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 11 Feb 2024 10:22:10 -0700 Subject: [PATCH 159/423] [position] Address a few warnings in Position Build Position::sight_of_piece() and ::is_king_in_check() for cfg(test) only. Expand the doc comment for ::king_danger() slightly. Remove an unused Rank import. # Conflicts: # position/src/position/position.rs --- position/src/position/position.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/position/src/position/position.rs b/position/src/position/position.rs index ab5a71a..01de793 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -212,12 +212,15 @@ impl Position { self.moves().moves_for_piece(piece) } + #[cfg(test)] pub(crate) fn sight_of_piece(&self, piece: &PlacedPiece) -> BitBoard { piece.sight(&self.pieces, self.en_passant_square) } /// A bitboard representing the squares where a king of the given color will - /// be in danger. The king cannot move to these squares. + /// be in danger of being captured by the opposing player. If the king is on + /// one of these squares, it is in check. The king cannot move to these + /// squares. pub(crate) fn king_danger(&self, color: Color) -> BitBoard { let pieces_without_king = { let mut cloned_pieces = self.pieces.clone(); @@ -230,6 +233,7 @@ impl Position { self._sight_of_player(color.other(), &pieces_without_king) } + #[cfg(test)] pub(crate) fn is_king_in_check(&self) -> bool { let danger_squares = self.king_danger(self.color_to_move); !(danger_squares & self.king_bitboard(self.color_to_move)).is_empty() From f69c7d4c9626535c38a414e72817eb652da7fb32 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 3 Feb 2024 15:17:02 -0800 Subject: [PATCH 160/423] Move a bunch of stuff from the position::move module over to a new chessfriend_moves crate --- Cargo.lock | 3 +- Cargo.toml | 1 + moves/Cargo.toml | 10 ++++ moves/src/castle.rs | 110 ++++++++++++++++++++++++++++++++++++++ moves/src/lib.rs | 8 +++ moves/src/moves.rs | 127 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 257 insertions(+), 2 deletions(-) create mode 100644 moves/Cargo.toml create mode 100644 moves/src/castle.rs create mode 100644 moves/src/lib.rs create mode 100644 moves/src/moves.rs diff --git a/Cargo.lock b/Cargo.lock index 6e580bf..7251e3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,12 +74,11 @@ name = "chessfriend_core" version = "0.1.0" [[package]] -name = "chessfriend_move_generator" +name = "chessfriend_moves" version = "0.1.0" dependencies = [ "chessfriend_bitboard", "chessfriend_core", - "chessfriend_position", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b490c8e..490ec43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "bitboard", "core", "explorer", + "moves", "position", ] resolver = "2" diff --git a/moves/Cargo.toml b/moves/Cargo.toml new file mode 100644 index 0000000..3c9e6cf --- /dev/null +++ b/moves/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "chessfriend_moves" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +chessfriend_core = { path = "../core" } +chessfriend_bitboard = { path = "../bitboard" } diff --git a/moves/src/castle.rs b/moves/src/castle.rs new file mode 100644 index 0000000..a8e95d1 --- /dev/null +++ b/moves/src/castle.rs @@ -0,0 +1,110 @@ +// Eryn Wells + +use chessfriend_bitboard::BitBoard; +use chessfriend_core::{Color, Square}; + +#[repr(u16)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Castle { + KingSide = 0b10, + QueenSide = 0b11, +} + +pub(crate) struct CastlingParameters { + clear_squares: BitBoard, + check_squares: BitBoard, +} + +#[derive(Debug)] +pub(crate) struct Squares { + pub king: Square, + pub rook: Square, +} + +impl Castle { + pub const ALL: [Castle; 2] = [Castle::KingSide, Castle::QueenSide]; + + const STARTING_SQUARES: [[Squares; 2]; 2] = [ + [ + Squares { + king: Square::E1, + rook: Square::H1, + }, + Squares { + king: Square::E1, + rook: Square::A1, + }, + ], + [ + Squares { + king: Square::E8, + rook: Square::H8, + }, + Squares { + king: Square::E8, + rook: Square::A8, + }, + ], + ]; + + const TARGET_SQUARES: [[Squares; 2]; 2] = [ + [ + Squares { + king: Square::G1, + rook: Square::F1, + }, + Squares { + king: Square::C1, + rook: Square::D1, + }, + ], + [ + Squares { + king: Square::G8, + rook: Square::F8, + }, + Squares { + king: Square::C8, + rook: Square::D8, + }, + ], + ]; + + pub(crate) fn starting_squares(&self, color: Color) -> &'static Squares { + &Castle::STARTING_SQUARES[color as usize][self.into_index()] + } + + pub(crate) fn target_squares(&self, color: Color) -> &'static Squares { + &Castle::TARGET_SQUARES[color as usize][self.into_index()] + } + + pub(crate) fn into_index(&self) -> usize { + match self { + Castle::KingSide => 0, + Castle::QueenSide => 1, + } + } + + pub(crate) fn parameters(&self) -> CastlingParameters { + match self { + Castle::KingSide => CastlingParameters { + clear_squares: BitBoard::new(0b01100000), + check_squares: BitBoard::new(0b01110000), + }, + Castle::QueenSide => CastlingParameters { + clear_squares: BitBoard::new(0b00001110), + check_squares: BitBoard::new(0b00011100), + }, + } + } +} + +impl CastlingParameters { + pub fn clear_squares(&self) -> &BitBoard { + &self.clear_squares + } + + pub fn check_squares(&self) -> &BitBoard { + &self.check_squares + } +} diff --git a/moves/src/lib.rs b/moves/src/lib.rs new file mode 100644 index 0000000..564c897 --- /dev/null +++ b/moves/src/lib.rs @@ -0,0 +1,8 @@ +// Eryn Wells + +mod builder; +mod castle; +mod moves; + +pub use builder::Builder; +pub use moves::Move; diff --git a/moves/src/moves.rs b/moves/src/moves.rs new file mode 100644 index 0000000..8d33b04 --- /dev/null +++ b/moves/src/moves.rs @@ -0,0 +1,127 @@ +// Eryn Wells + +use crate::castle::Castle; +use chessfriend_core::{PlacedPiece, Shape, Square}; +use std::fmt; + +#[repr(u16)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum PromotableShape { + Knight = 0b00, + Bishop = 0b01, + Rook = 0b10, + Queen = 0b11, +} + +#[repr(u16)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +enum Kind { + Quiet = 0b00, + DoublePush = 0b01, + Castle(Castle), + Capture(PlacedPiece) = 0b0100, + EnPassantCapture(PlacedPiece) = 0b0101, + Promotion(PromotableShape) = 0b1000, + CapturePromotion(PlacedPiece, PromotableShape) = 0b1100, +} + +impl Kind { + fn bits(&self) -> u16 { + match self { + Self::Promotion(shape) => self.discriminant() | *shape as u16, + Self::CapturePromotion(_, shape) => self.discriminant() | *shape as u16, + Self::Castle(castle) => *castle as u16, + _ => self.discriminant(), + } + } + + /// Return the discriminant value. This implementation is copied from the Rust docs. + /// See https://doc.rust-lang.org/std/mem/fn.discriminant.html + fn discriminant(&self) -> u16 { + unsafe { *<*const _>::from(self).cast::() } + } +} + +impl Default for Kind { + fn default() -> Self { + Self::Quiet + } +} + +/// A single player's move. In chess parlance, this is a "ply". +#[derive(Clone, Copy, Eq, Hash, PartialEq)] +pub struct Move(pub(crate) u16); + +impl Move { + pub fn from_square(&self) -> Square { + ((self.0 >> 4) & 0b111111).try_into().unwrap() + } + + pub fn to_square(&self) -> Square { + (self.0 >> 10).try_into().unwrap() + } + + pub fn is_quiet(&self) -> bool { + self.flags() == Kind::Quiet.discriminant() + } + + pub fn is_double_push(&self) -> bool { + self.flags() == Kind::DoublePush.discriminant() + } + + pub fn is_castle(&self) -> bool { + self.castle().is_some() + } + + pub fn castle(&self) -> Option { + match self.flags() { + 0b0010 => Some(Castle::KingSide), + 0b0011 => Some(Castle::QueenSide), + _ => None, + } + } + + pub fn is_capture(&self) -> bool { + (self.0 & 0b0100) != 0 + } + + pub fn is_en_passant(&self) -> bool { + self.flags() == 0b0101 + } + + pub fn is_promotion(&self) -> bool { + (self.0 & 0b1000) != 0 + } + + pub fn promotion(&self) -> Option { + if !self.is_promotion() { + return None; + } + + Some(match self.special() { + 0b00 => Shape::Knight, + 0b01 => Shape::Bishop, + 0b10 => Shape::Rook, + 0b11 => Shape::Queen, + _ => unreachable!(), + }) + } + + #[inline] + fn flags(&self) -> u16 { + self.0 & 0b1111 + } + + #[inline] + fn special(&self) -> u16 { + self.0 & 0b11 + } +} + +impl fmt::Debug for Move { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("Move") + .field(&format_args!("{:08b}", &self.0)) + .finish() + } +} From 0bedf2aa9f490c430e799b61d9629264e52f7ba8 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 3 Feb 2024 15:17:40 -0800 Subject: [PATCH 161/423] Implement part of a new Builder using the type state pattern The API for this is much easier to use. --- moves/src/builder.rs | 105 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 moves/src/builder.rs diff --git a/moves/src/builder.rs b/moves/src/builder.rs new file mode 100644 index 0000000..9340497 --- /dev/null +++ b/moves/src/builder.rs @@ -0,0 +1,105 @@ +// Eryn Wells + +use crate::{castle, Move}; +use chessfriend_core::{PlacedPiece, Square}; + +pub trait Style {} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Builder { + style: S, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Null; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Push { + from: Option, + to: Option, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Capture { + push: Push, + capture: Option, +} + +pub struct Castle { + castle: castle::Castle, +} + +impl Style for Null {} +impl Style for Push {} +impl Style for Capture {} +impl Style for Castle {} + +impl Builder { + pub fn new() -> Self { + Self { style: Null } + } + + pub fn piece(piece: &PlacedPiece) -> Builder { + Builder { + style: Push { + from: Some(piece.square()), + to: None, + }, + } + } + + pub fn castling(castle: castle::Castle) -> Builder { + Builder { + style: Castle { castle }, + } + } + + pub fn build(&self) -> Move { + Move(0) + } +} + +impl Builder { + pub fn from(mut self, square: Square) -> Self { + self.style.from = Some(square); + self + } + + pub fn to(mut self, square: Square) -> Self { + self.style.to = Some(square); + self + } + + pub fn capturing(self, square: Square) -> Builder { + Builder { + style: Capture { + push: self.style, + capture: Some(square), + }, + } + } + + pub fn capturing_piece(self, piece: PlacedPiece) -> Builder { + Builder { + style: Capture { + push: self.style, + capture: Some(piece.square()), + }, + } + } +} + +impl Builder { + pub fn build(&self) -> Move { + Move(self.style.into_bits()) + } +} + +impl Castle { + fn into_bits(&self) -> u16 { + match self.castle { + castle::Castle::KingSide => 0b10, + castle::Castle::QueenSide => 0b11, + } + } +} From c55b7c4877e12a828fc54cf5f372564074548602 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 9 Feb 2024 20:00:47 -0800 Subject: [PATCH 162/423] Implement a whole new move crate --- moves/src/builder.rs | 288 +++++++++++++++++++++++++++++++++++++++--- moves/src/castle.rs | 141 +++++++++------------ moves/src/defs.rs | 34 +++++ moves/src/lib.rs | 5 +- moves/src/moves.rs | 73 ++++------- moves/tests/flags.rs | 104 +++++++++++++++ moves/tests/pushes.rs | 15 +++ 7 files changed, 512 insertions(+), 148 deletions(-) create mode 100644 moves/src/defs.rs create mode 100644 moves/tests/flags.rs create mode 100644 moves/tests/pushes.rs diff --git a/moves/src/builder.rs b/moves/src/builder.rs index 9340497..780ed73 100644 --- a/moves/src/builder.rs +++ b/moves/src/builder.rs @@ -1,9 +1,35 @@ // Eryn Wells -use crate::{castle, Move}; -use chessfriend_core::{PlacedPiece, Square}; +use crate::{castle, defs::Kind, Move, PromotionShape}; +use chessfriend_core::{Color, File, PlacedPiece, Rank, Square}; +use std::result::Result as StdResult; -pub trait Style {} +pub type Result = std::result::Result; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Error { + MissingOriginSquare, + MissingTargetSquare, + MissingCaptureSquare, + InvalidEnPassantSquare, +} + +pub trait Style { + fn origin_square(&self) -> Option { + None + } + + fn target_square(&self) -> Option { + None + } + + fn into_move_bits(&self) -> StdResult { + let origin_square = self.origin_square().ok_or(Error::MissingOriginSquare)? as u16; + let target_square = self.target_square().ok_or(Error::MissingTargetSquare)? as u16; + + Ok((origin_square & 0b111111) << 4 | (target_square & 0b111111) << 10) + } +} #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct Builder { @@ -19,67 +45,241 @@ pub struct Push { to: Option, } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct DoublePush { + from: Square, + to: Square, +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct Capture { push: Push, capture: Option, } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EnPassantCapture { + push: Push, + capture: Option, +} + +pub struct Promotion { + style: S, + promotion: PromotionShape, +} + pub struct Castle { castle: castle::Castle, } impl Style for Null {} -impl Style for Push {} -impl Style for Capture {} + +impl Style for Push { + fn origin_square(&self) -> Option { + self.from + } + + fn target_square(&self) -> Option { + self.to + } +} + +impl Style for Capture { + fn origin_square(&self) -> Option { + self.push.from + } + + fn target_square(&self) -> Option { + self.push.to + } +} + impl Style for Castle {} +impl Style for DoublePush { + fn origin_square(&self) -> Option { + Some(self.from) + } + + fn target_square(&self) -> Option { + Some(self.to) + } + + fn into_move_bits(&self) -> StdResult { + Ok(Kind::DoublePush as u16 + | (self.from as u16 & 0b111111) << 4 + | (self.to as u16 & 0b111111) << 10) + } +} + +impl Style for EnPassantCapture { + fn origin_square(&self) -> Option { + self.push.from + } + + fn target_square(&self) -> Option { + self.push.to + } + + fn into_move_bits(&self) -> StdResult { + let origin_square = self.origin_square().ok_or(Error::MissingOriginSquare)? as u16; + let target_square = self.target_square().ok_or(Error::MissingTargetSquare)? as u16; + + Ok((origin_square & 0b111111) << 4 | (target_square & 0b111111) << 10) + } +} + +impl Style for Promotion { + fn origin_square(&self) -> Option { + self.style.from + } + + fn target_square(&self) -> Option { + self.style.to + } +} + +impl Style for Promotion { + fn origin_square(&self) -> Option { + self.style.push.from + } + + fn target_square(&self) -> Option { + self.style.push.to + } +} + +impl Promotion { + fn into_move_bits(&self) -> StdResult { + let origin_square = self + .style + .origin_square() + .ok_or(Error::MissingOriginSquare)? as u16; + let target_square = self + .style + .target_square() + .ok_or(Error::MissingTargetSquare)? as u16; + + Ok(Kind::Promotion as u16 + | self.promotion as u16 + | (origin_square & 0b111111) << 4 + | (target_square & 0b111111) << 10) + } +} + +impl Promotion { + fn into_move_bits(&self) -> StdResult { + let origin_square = self + .style + .origin_square() + .ok_or(Error::MissingOriginSquare)? as u16; + let target_square = self + .style + .target_square() + .ok_or(Error::MissingTargetSquare)? as u16; + + Ok(Kind::CapturePromotion as u16 + | self.promotion as u16 + | (origin_square & 0b111111) << 4 + | (target_square & 0b111111) << 10) + } +} + impl Builder { pub fn new() -> Self { Self { style: Null } } - pub fn piece(piece: &PlacedPiece) -> Builder { + pub fn push(piece: &PlacedPiece, to: Square) -> Builder { Builder { style: Push { from: Some(piece.square()), - to: None, + to: Some(to), }, } } + pub fn double_push(file: File, color: Color) -> Builder { + let (from, to) = match color { + Color::White => ( + Square::from_file_rank(file, Rank::TWO), + Square::from_file_rank(file, Rank::FOUR), + ), + Color::Black => ( + Square::from_file_rank(file, Rank::SEVEN), + Square::from_file_rank(file, Rank::FIVE), + ), + }; + + Builder { + style: DoublePush { from, to }, + } + } + pub fn castling(castle: castle::Castle) -> Builder { Builder { style: Castle { castle }, } } + pub fn capturing_on(piece: &PlacedPiece, to: Square) -> Builder { + Self::push(piece, to).capturing_on(to) + } + + pub fn capturing_piece(piece: &PlacedPiece, capturing: &PlacedPiece) -> Builder { + Self::push(piece, capturing.square()).capturing_piece(&capturing) + } + + pub fn from(self, square: Square) -> Builder { + Builder { + style: Push { + from: Some(square), + to: None, + }, + } + } + pub fn build(&self) -> Move { Move(0) } } impl Builder { - pub fn from(mut self, square: Square) -> Self { + pub fn from(&mut self, square: Square) -> &mut Self { self.style.from = Some(square); self } - pub fn to(mut self, square: Square) -> Self { + pub fn to(&mut self, square: Square) -> &mut Self { self.style.to = Some(square); self } - pub fn capturing(self, square: Square) -> Builder { + pub fn capturing_on(self, square: Square) -> Builder { + let mut style = self.style; + style.to = Some(square); + Builder { style: Capture { + push: style, + capture: Some(square), + }, + } + } + + pub fn capturing_en_passant_on(self, square: Square) -> Builder { + let mut style = self.style; + style.to = Some(square); + + Builder { + style: EnPassantCapture { push: self.style, capture: Some(square), }, } } - pub fn capturing_piece(self, piece: PlacedPiece) -> Builder { + pub fn capturing_piece(self, piece: &PlacedPiece) -> Builder { Builder { style: Capture { push: self.style, @@ -87,19 +287,73 @@ impl Builder { }, } } + + pub fn promoting_to(self, shape: PromotionShape) -> Builder> { + Builder { + style: Promotion { + style: self.style, + promotion: shape, + }, + } + } + + pub fn build(&self) -> Result { + Ok(Move(Kind::Quiet as u16 | self.style.into_move_bits()?)) + } } impl Builder { + fn bits(&self) -> u16 { + let bits = match self.style.castle { + castle::Castle::KingSide => Kind::KingSideCastle, + castle::Castle::QueenSide => Kind::QueenSideCastle, + }; + + bits as u16 + } + pub fn build(&self) -> Move { - Move(self.style.into_bits()) + Move(self.bits()) } } -impl Castle { - fn into_bits(&self) -> u16 { - match self.castle { - castle::Castle::KingSide => 0b10, - castle::Castle::QueenSide => 0b11, +impl Builder { + pub fn promoting_to(self, shape: PromotionShape) -> Builder> { + Builder { + style: Promotion { + style: self.style, + promotion: shape, + }, } } + + pub fn build(&self) -> Result { + Ok(Move(Kind::Capture as u16 | self.style.into_move_bits()?)) + } +} + +impl Builder { + pub fn build(&self) -> Result { + Ok(Move(Kind::DoublePush as u16 | self.style.into_move_bits()?)) + } +} + +impl Builder { + pub unsafe fn build_unchecked(&self) -> Result { + Ok(Move( + Kind::EnPassantCapture as u16 | self.style.into_move_bits()?, + )) + } +} + +impl Builder> { + pub fn build(&self) -> Result { + Ok(Move(self.style.into_move_bits()?)) + } +} + +impl Builder> { + pub fn build(&self) -> Result { + Ok(Move(self.style.into_move_bits()?)) + } } diff --git a/moves/src/castle.rs b/moves/src/castle.rs index a8e95d1..5459ff4 100644 --- a/moves/src/castle.rs +++ b/moves/src/castle.rs @@ -1,17 +1,26 @@ // Eryn Wells use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Square}; +use chessfriend_core::Square; -#[repr(u16)] +#[repr(u8)] #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum Castle { - KingSide = 0b10, - QueenSide = 0b11, + KingSide = 0, + QueenSide = 1, } pub(crate) struct CastlingParameters { + /// Origin squares of the king and rook. + origin_squares: Squares, + + /// Target or destination squares for the king and rook. + target_squares: Squares, + + /// The set of squares that must be clear of any pieces in order to perform this castle. clear_squares: BitBoard, + + /// The set of squares that must not be attacked in order to perform this castle. check_squares: BitBoard, } @@ -24,87 +33,59 @@ pub(crate) struct Squares { impl Castle { pub const ALL: [Castle; 2] = [Castle::KingSide, Castle::QueenSide]; - const STARTING_SQUARES: [[Squares; 2]; 2] = [ + /// Parameters for each castling move, organized by color and board-side. + const PARAMETERS: [[CastlingParameters; 2]; 2] = [ [ - Squares { - king: Square::E1, - rook: Square::H1, - }, - Squares { - king: Square::E1, - rook: Square::A1, - }, - ], - [ - Squares { - king: Square::E8, - rook: Square::H8, - }, - Squares { - king: Square::E8, - rook: Square::A8, - }, - ], - ]; - - const TARGET_SQUARES: [[Squares; 2]; 2] = [ - [ - Squares { - king: Square::G1, - rook: Square::F1, - }, - Squares { - king: Square::C1, - rook: Square::D1, - }, - ], - [ - Squares { - king: Square::G8, - rook: Square::F8, - }, - Squares { - king: Square::C8, - rook: Square::D8, - }, - ], - ]; - - pub(crate) fn starting_squares(&self, color: Color) -> &'static Squares { - &Castle::STARTING_SQUARES[color as usize][self.into_index()] - } - - pub(crate) fn target_squares(&self, color: Color) -> &'static Squares { - &Castle::TARGET_SQUARES[color as usize][self.into_index()] - } - - pub(crate) fn into_index(&self) -> usize { - match self { - Castle::KingSide => 0, - Castle::QueenSide => 1, - } - } - - pub(crate) fn parameters(&self) -> CastlingParameters { - match self { - Castle::KingSide => CastlingParameters { + CastlingParameters { + origin_squares: Squares { + king: Square::E1, + rook: Square::H1, + }, + target_squares: Squares { + king: Square::G1, + rook: Square::F1, + }, clear_squares: BitBoard::new(0b01100000), check_squares: BitBoard::new(0b01110000), }, - Castle::QueenSide => CastlingParameters { + CastlingParameters { + origin_squares: Squares { + king: Square::E1, + rook: Square::A1, + }, + target_squares: Squares { + king: Square::C1, + rook: Square::D1, + }, clear_squares: BitBoard::new(0b00001110), check_squares: BitBoard::new(0b00011100), }, - } - } -} - -impl CastlingParameters { - pub fn clear_squares(&self) -> &BitBoard { - &self.clear_squares - } - - pub fn check_squares(&self) -> &BitBoard { - &self.check_squares - } + ], + [ + CastlingParameters { + origin_squares: Squares { + king: Square::E8, + rook: Square::H8, + }, + target_squares: Squares { + king: Square::G8, + rook: Square::F8, + }, + clear_squares: BitBoard::new(0b01100000 << 8 * 7), + check_squares: BitBoard::new(0b01110000 << 8 * 7), + }, + CastlingParameters { + origin_squares: Squares { + king: Square::E8, + rook: Square::A8, + }, + target_squares: Squares { + king: Square::C8, + rook: Square::D8, + }, + clear_squares: BitBoard::new(0b00001110 << 8 * 7), + check_squares: BitBoard::new(0b00011100 << 8 * 7), + }, + ], + ]; } diff --git a/moves/src/defs.rs b/moves/src/defs.rs new file mode 100644 index 0000000..642b82e --- /dev/null +++ b/moves/src/defs.rs @@ -0,0 +1,34 @@ +// Eryn Wells + +use chessfriend_core::Shape; + +pub(crate) enum Kind { + Quiet = 0b00, + DoublePush = 0b01, + KingSideCastle = 0b10, + QueenSideCastle = 0b11, + Capture = 0b0100, + EnPassantCapture = 0b0101, + Promotion = 0b1000, + CapturePromotion = 0b1100, +} + +#[repr(u16)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum PromotionShape { + Knight = 0b00, + Bishop = 0b01, + Rook = 0b10, + Queen = 0b11, +} + +impl From for Shape { + fn from(value: PromotionShape) -> Self { + match value { + PromotionShape::Knight => Shape::Knight, + PromotionShape::Bishop => Shape::Bishop, + PromotionShape::Rook => Shape::Rook, + PromotionShape::Queen => Shape::Queen, + } + } +} diff --git a/moves/src/lib.rs b/moves/src/lib.rs index 564c897..0ad0fc1 100644 --- a/moves/src/lib.rs +++ b/moves/src/lib.rs @@ -2,7 +2,10 @@ mod builder; mod castle; +mod defs; mod moves; -pub use builder::Builder; +pub use builder::{Builder, Error as BuilderError}; +pub use castle::Castle; +pub use defs::PromotionShape; pub use moves::Move; diff --git a/moves/src/moves.rs b/moves/src/moves.rs index 8d33b04..ab0c0da 100644 --- a/moves/src/moves.rs +++ b/moves/src/moves.rs @@ -1,72 +1,45 @@ // Eryn Wells -use crate::castle::Castle; -use chessfriend_core::{PlacedPiece, Shape, Square}; +use crate::{castle::Castle, defs::Kind}; +use chessfriend_core::{Rank, Shape, Square}; use std::fmt; -#[repr(u16)] -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum PromotableShape { - Knight = 0b00, - Bishop = 0b01, - Rook = 0b10, - Queen = 0b11, -} - -#[repr(u16)] -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -enum Kind { - Quiet = 0b00, - DoublePush = 0b01, - Castle(Castle), - Capture(PlacedPiece) = 0b0100, - EnPassantCapture(PlacedPiece) = 0b0101, - Promotion(PromotableShape) = 0b1000, - CapturePromotion(PlacedPiece, PromotableShape) = 0b1100, -} - -impl Kind { - fn bits(&self) -> u16 { - match self { - Self::Promotion(shape) => self.discriminant() | *shape as u16, - Self::CapturePromotion(_, shape) => self.discriminant() | *shape as u16, - Self::Castle(castle) => *castle as u16, - _ => self.discriminant(), - } - } - - /// Return the discriminant value. This implementation is copied from the Rust docs. - /// See https://doc.rust-lang.org/std/mem/fn.discriminant.html - fn discriminant(&self) -> u16 { - unsafe { *<*const _>::from(self).cast::() } - } -} - -impl Default for Kind { - fn default() -> Self { - Self::Quiet - } -} - /// A single player's move. In chess parlance, this is a "ply". #[derive(Clone, Copy, Eq, Hash, PartialEq)] pub struct Move(pub(crate) u16); impl Move { - pub fn from_square(&self) -> Square { + pub fn origin_square(&self) -> Square { ((self.0 >> 4) & 0b111111).try_into().unwrap() } - pub fn to_square(&self) -> Square { + pub fn target_square(&self) -> Square { (self.0 >> 10).try_into().unwrap() } + pub fn capture_square(&self) -> Option { + if self.is_capture() { + return Some(self.target_square()); + } + + if self.is_en_passant() { + let target_square = self.target_square(); + return Some(match target_square.rank() { + Rank::THREE => Square::from_file_rank(target_square.file(), Rank::FOUR), + Rank::SIX => Square::from_file_rank(target_square.file(), Rank::FIVE), + _ => unreachable!(), + }); + } + + None + } + pub fn is_quiet(&self) -> bool { - self.flags() == Kind::Quiet.discriminant() + self.flags() == Kind::Quiet as u16 } pub fn is_double_push(&self) -> bool { - self.flags() == Kind::DoublePush.discriminant() + self.flags() == Kind::DoublePush as u16 } pub fn is_castle(&self) -> bool { diff --git a/moves/tests/flags.rs b/moves/tests/flags.rs new file mode 100644 index 0000000..cff0841 --- /dev/null +++ b/moves/tests/flags.rs @@ -0,0 +1,104 @@ +// Eryn Wells + +use chessfriend_core::{piece, Color, File, Shape, Square}; +use chessfriend_moves::{Builder, BuilderError, Castle, PromotionShape}; + +macro_rules! assert_flag { + ($move:expr, $left:expr, $right:expr, $desc:expr) => { + assert_eq!($left, $right, "{:?} -> {}", $move, stringify!($desc)) + }; +} + +macro_rules! assert_flags { + ($move:expr, $quiet:expr, $double_push:expr, $en_passant:expr, $capture:expr, $castle:expr, $promotion:expr) => { + assert_flag!($move, $move.is_quiet(), $quiet, "is_quiet"); + assert_flag!( + $move, + $move.is_double_push(), + $double_push, + "is_double_push" + ); + assert_flag!($move, $move.is_en_passant(), $en_passant, "is_en_passant"); + assert_flag!($move, $move.is_capture(), $capture, "is_capture"); + assert_flag!($move, $move.is_castle(), $castle, "is_castle"); + assert_flag!($move, $move.is_promotion(), $promotion, "is_promotion"); + }; +} + +#[test] +fn move_flags_quiet() -> Result<(), BuilderError> { + let mv = Builder::push(&piece!(White Pawn on A4), Square::A5).build()?; + assert_flags!(mv, true, false, false, false, false, false); + + Ok(()) +} + +#[test] +fn move_flags_double_push() -> Result<(), BuilderError> { + let mv = Builder::double_push(File::C, Color::White).build()?; + assert_flags!(mv, false, true, false, false, false, false); + + Ok(()) +} + +#[test] +fn move_flags_capture() -> Result<(), BuilderError> { + let mv = Builder::new() + .from(Square::A4) + .capturing_on(Square::B5) + .build()?; + + assert_flags!(mv, false, false, false, true, false, false); + + Ok(()) +} + +#[test] +fn move_flags_en_passant_capture() -> Result<(), BuilderError> { + let mv = unsafe { + Builder::new() + .from(Square::A5) + .capturing_en_passant_on(Square::B4) + .build_unchecked()? + }; + + assert_flags!(mv, false, false, true, true, false, false); + assert_eq!(mv.origin_square(), Square::A5); + assert_eq!(mv.target_square(), Square::B4); + + Ok(()) +} + +#[test] +fn move_flags_promotion() -> Result<(), BuilderError> { + let mv = Builder::push(&piece!(White Pawn on H7), Square::H8) + .promoting_to(PromotionShape::Queen) + .build()?; + + assert_flags!(mv, false, false, false, false, false, true); + assert_eq!(mv.promotion(), Some(Shape::Queen)); + + Ok(()) +} + +#[test] +fn move_flags_capture_promotion() -> Result<(), BuilderError> { + let mv = Builder::push(&piece!(White Pawn on H7), Square::H8) + .capturing_piece(&piece!(Black Knight on G8)) + .promoting_to(PromotionShape::Queen) + .build()?; + + assert_flags!(mv, false, false, false, true, false, true); + assert_eq!(mv.promotion(), Some(Shape::Queen)); + + Ok(()) +} + +#[test] +fn move_flags_castle() -> Result<(), BuilderError> { + let mv = Builder::castling(Castle::KingSide).build(); + + assert_flags!(mv, false, false, false, false, true, false); + + Ok(()) +} diff --git a/moves/tests/pushes.rs b/moves/tests/pushes.rs new file mode 100644 index 0000000..0ef2a2f --- /dev/null +++ b/moves/tests/pushes.rs @@ -0,0 +1,15 @@ +// Eryn Wells + +use chessfriend_core::{piece, Square}; +use chessfriend_moves::{Builder, BuilderError}; + +#[test] +fn pawn_push() -> Result<(), BuilderError> { + let mv = Builder::push(&piece!(White Pawn on A3), Square::A4).build()?; + + assert!(mv.is_quiet()); + assert_eq!(mv.origin_square(), Square::A3); + assert_eq!(mv.target_square(), Square::A4); + + Ok(()) +} From cc23ee2d90148d3c32f6fcd84d6091220b0b7cf6 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 10 Feb 2024 11:40:27 -0700 Subject: [PATCH 163/423] Rename en passant square method on Position and implement a getter for the capture square MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Position::en_passant_square → en_passant_target_square Position::en_passant_capture_square --- position/src/fen.rs | 2 +- .../src/position/builders/move_builder.rs | 2 +- .../src/position/builders/position_builder.rs | 2 +- position/src/position/position.rs | 19 +++++++++++++++++-- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/position/src/fen.rs b/position/src/fen.rs index d6c3568..9a8369a 100644 --- a/position/src/fen.rs +++ b/position/src/fen.rs @@ -95,7 +95,7 @@ impl ToFen for Position { write!( fen_string, " {}", - if let Some(en_passant_square) = self.en_passant_square() { + if let Some(en_passant_square) = self.en_passant_target_square() { en_passant_square.to_string() } else { "-".to_string() diff --git a/position/src/position/builders/move_builder.rs b/position/src/position/builders/move_builder.rs index c926c08..2de56d4 100644 --- a/position/src/position/builders/move_builder.rs +++ b/position/src/position/builders/move_builder.rs @@ -292,7 +292,7 @@ mod tests { new_position.piece_on_square(Square::E4), Some(piece!(White Pawn on E4)) ); - assert_eq!(new_position.en_passant_square(), Some(Square::E3)); + assert_eq!(new_position.en_passant_target_square(), Some(Square::E3)); Ok(()) } diff --git a/position/src/position/builders/position_builder.rs b/position/src/position/builders/position_builder.rs index 5011f21..cba432f 100644 --- a/position/src/position/builders/position_builder.rs +++ b/position/src/position/builders/position_builder.rs @@ -52,7 +52,7 @@ impl Builder { flags: position.flags(), pieces, kings: [Some(white_king), Some(black_king)], - en_passant_square: position.en_passant_square(), + en_passant_square: position.en_passant_target_square(), ply_counter: position.ply_counter(), move_number: position.move_number(), } diff --git a/position/src/position/position.rs b/position/src/position/position.rs index 01de793..b8c193a 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -9,7 +9,7 @@ use crate::{ sight::SightExt, }; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; +use chessfriend_core::{Color, Piece, PlacedPiece, Rank, Shape, Square}; use std::{cell::OnceCell, fmt}; #[derive(Clone, Debug, Eq)] @@ -182,10 +182,25 @@ impl Position { Pieces::new(&self, color) } - pub fn en_passant_square(&self) -> Option { + /// If en passant is available in this position, the square a pawn will move + /// to if it captures en passant. + pub fn en_passant_target_square(&self) -> Option { self.en_passant_square } + /// If en passant is available in this position, the square on which the + /// captured pawn is sitting. + pub fn en_passant_capture_square(&self) -> Option { + let target_square = self.en_passant_square?; + + let file = target_square.file(); + Some(match target_square.rank() { + Rank::THREE => Square::from_file_rank(file, Rank::FOUR), + Rank::SIX => Square::from_file_rank(file, Rank::SEVEN), + _ => unreachable!(), + }) + } + fn _sight_of_player(&self, player: Color, pieces: &PieceBitBoards) -> BitBoard { let en_passant_square = self.en_passant_square; From e6a9b7f8c4865848cef5022b1580e12ba9868d27 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 10 Feb 2024 18:30:11 -0700 Subject: [PATCH 164/423] Return a chessfriend_moves::EnPassant from a new Position::en_passant() Replace Position's en_passant_target_square() and en_passant_capture_square() with a single en_passant() method that returns a new EnPassant struct that has the target and capture squares for the en passant move. --- Cargo.lock | 1 + moves/src/en_passant.rs | 49 +++++++++++++++++++ moves/src/lib.rs | 2 + position/Cargo.toml | 1 + position/src/fen.rs | 7 +-- .../src/position/builders/move_builder.rs | 11 ++--- .../src/position/builders/position_builder.rs | 2 +- position/src/position/position.rs | 20 ++------ 8 files changed, 66 insertions(+), 27 deletions(-) create mode 100644 moves/src/en_passant.rs diff --git a/Cargo.lock b/Cargo.lock index 7251e3e..90cedc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -87,6 +87,7 @@ version = "0.1.0" dependencies = [ "chessfriend_bitboard", "chessfriend_core", + "chessfriend_moves", ] [[package]] diff --git a/moves/src/en_passant.rs b/moves/src/en_passant.rs new file mode 100644 index 0000000..ac57835 --- /dev/null +++ b/moves/src/en_passant.rs @@ -0,0 +1,49 @@ +// Eryn Wells + +use chessfriend_core::{Rank, Square}; + +/// En passant information. +#[derive(Clone, Copy, Debug)] +pub struct EnPassant { + target: Square, + capture: Square, +} + +impl EnPassant { + fn _capture_square(target: Square) -> Option { + let (file, rank) = target.file_rank(); + match rank { + Rank::THREE => Some(Square::from_file_rank(file, Rank::FOUR)), + Rank::SIX => Some(Square::from_file_rank(file, Rank::FIVE)), + _ => None, + } + } + + /// Return en passant information for a particular target square. The target + /// square is the square a pawn capturing en passant will move to. + /// + /// Return `None` if the square is not eligible for en passant. + /// + /// ## Examples + /// + /// ``` + /// use chessfriend_core::Square; + /// use chessfriend_moves::EnPassant; + /// assert!(EnPassant::from_target_square(Square::E3).is_some()); + /// assert!(EnPassant::from_target_square(Square::B4).is_none()); + /// ``` + pub fn from_target_square(target: Square) -> Option { + match Self::_capture_square(target) { + Some(capture) => Some(Self { target, capture }), + None => None, + } + } + + pub fn target_square(&self) -> Square { + self.target + } + + pub fn capture_square(&self) -> Square { + self.capture + } +} diff --git a/moves/src/lib.rs b/moves/src/lib.rs index 0ad0fc1..75e26ce 100644 --- a/moves/src/lib.rs +++ b/moves/src/lib.rs @@ -3,9 +3,11 @@ mod builder; mod castle; mod defs; +mod en_passant; mod moves; pub use builder::{Builder, Error as BuilderError}; pub use castle::Castle; pub use defs::PromotionShape; +pub use en_passant::EnPassant; pub use moves::Move; diff --git a/position/Cargo.toml b/position/Cargo.toml index db11c0d..07157eb 100644 --- a/position/Cargo.toml +++ b/position/Cargo.toml @@ -8,3 +8,4 @@ edition = "2021" [dependencies] chessfriend_core = { path = "../core" } chessfriend_bitboard = { path = "../bitboard" } +chessfriend_moves = { path = "../moves" } diff --git a/position/src/fen.rs b/position/src/fen.rs index 9a8369a..dd9fa29 100644 --- a/position/src/fen.rs +++ b/position/src/fen.rs @@ -95,11 +95,8 @@ impl ToFen for Position { write!( fen_string, " {}", - if let Some(en_passant_square) = self.en_passant_target_square() { - en_passant_square.to_string() - } else { - "-".to_string() - } + self.en_passant() + .map_or("-".to_string(), |ep| ep.target_square().to_string()) ) .map_err(|err| ToFenError::FmtError(err))?; diff --git a/position/src/position/builders/move_builder.rs b/position/src/position/builders/move_builder.rs index 2de56d4..cb7f0f7 100644 --- a/position/src/position/builders/move_builder.rs +++ b/position/src/position/builders/move_builder.rs @@ -1,10 +1,6 @@ // Eryn Wells -use crate::{ - position::flags::Flags, - r#move::{AlgebraicMoveFormatter, Castle}, - MakeMoveError, Move, Position, -}; +use crate::{position::flags::Flags, r#move::Castle, MakeMoveError, Move, Position}; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Direction, Piece, PlacedPiece, Shape, Square}; @@ -292,7 +288,10 @@ mod tests { new_position.piece_on_square(Square::E4), Some(piece!(White Pawn on E4)) ); - assert_eq!(new_position.en_passant_target_square(), Some(Square::E3)); + assert_eq!( + new_position.en_passant().map(|ep| ep.target_square()), + Some(Square::E3) + ); Ok(()) } diff --git a/position/src/position/builders/position_builder.rs b/position/src/position/builders/position_builder.rs index cba432f..96757ce 100644 --- a/position/src/position/builders/position_builder.rs +++ b/position/src/position/builders/position_builder.rs @@ -52,7 +52,7 @@ impl Builder { flags: position.flags(), pieces, kings: [Some(white_king), Some(black_king)], - en_passant_square: position.en_passant_target_square(), + en_passant_square: position.en_passant().map(|ep| ep.target_square()), ply_counter: position.ply_counter(), move_number: position.move_number(), } diff --git a/position/src/position/position.rs b/position/src/position/position.rs index b8c193a..15da1c7 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -10,6 +10,7 @@ use crate::{ }; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece, PlacedPiece, Rank, Shape, Square}; +use chessfriend_moves::EnPassant; use std::{cell::OnceCell, fmt}; #[derive(Clone, Debug, Eq)] @@ -182,23 +183,12 @@ impl Position { Pieces::new(&self, color) } - /// If en passant is available in this position, the square a pawn will move - /// to if it captures en passant. - pub fn en_passant_target_square(&self) -> Option { - self.en_passant_square + pub fn has_en_passant_square(&self) -> bool { + self.en_passant_square.is_some() } - /// If en passant is available in this position, the square on which the - /// captured pawn is sitting. - pub fn en_passant_capture_square(&self) -> Option { - let target_square = self.en_passant_square?; - - let file = target_square.file(); - Some(match target_square.rank() { - Rank::THREE => Square::from_file_rank(file, Rank::FOUR), - Rank::SIX => Square::from_file_rank(file, Rank::SEVEN), - _ => unreachable!(), - }) + pub fn en_passant(&self) -> Option { + EnPassant::from_target_square(self.en_passant_square?) } fn _sight_of_player(&self, player: Color, pieces: &PieceBitBoards) -> BitBoard { From 8724c3cdce51d940967a2dd3c4b256bac70d7d8b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 10 Feb 2024 18:33:13 -0700 Subject: [PATCH 165/423] Pad out the discriminants of Kind variants --- moves/src/defs.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/moves/src/defs.rs b/moves/src/defs.rs index 642b82e..337f30e 100644 --- a/moves/src/defs.rs +++ b/moves/src/defs.rs @@ -3,10 +3,10 @@ use chessfriend_core::Shape; pub(crate) enum Kind { - Quiet = 0b00, - DoublePush = 0b01, - KingSideCastle = 0b10, - QueenSideCastle = 0b11, + Quiet = 0b0000, + DoublePush = 0b0001, + KingSideCastle = 0b0010, + QueenSideCastle = 0b0011, Capture = 0b0100, EnPassantCapture = 0b0101, Promotion = 0b1000, From 3b8b6b36e37b5b042ea5f4507c44ddcd2b236694 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 10 Feb 2024 18:33:29 -0700 Subject: [PATCH 166/423] Implement a basic Display for chessfriend_moves::Move --- moves/src/moves.rs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/moves/src/moves.rs b/moves/src/moves.rs index ab0c0da..f4f9b21 100644 --- a/moves/src/moves.rs +++ b/moves/src/moves.rs @@ -89,6 +89,41 @@ impl Move { fn special(&self) -> u16 { self.0 & 0b11 } + + fn _transfer_char(&self) -> char { + if self.is_capture() || self.is_en_passant() { + 'x' + } else { + '-' + } + } +} + +impl fmt::Display for Move { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(castle) = self.castle() { + match castle { + Castle::KingSide => return write!(f, "0-0"), + Castle::QueenSide => return write!(f, "0-0-0"), + } + } + + write!( + f, + "{}{}{}", + self.origin_square(), + self._transfer_char(), + self.target_square() + )?; + + if let Some(promotion) = self.promotion() { + write!(f, "={}", promotion)?; + } else if self.is_en_passant() { + write!(f, " e.p.")?; + } + + Ok(()) + } } impl fmt::Debug for Move { From c03a804c792ad291f262ec52507e4c78313506f2 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 11 Feb 2024 09:57:29 -0700 Subject: [PATCH 167/423] Rework the Pawn move generator to correctly compute en passant moves --- position/src/move_generator/pawn.rs | 93 ++++++++++++++++++++++++++--- 1 file changed, 84 insertions(+), 9 deletions(-) diff --git a/position/src/move_generator/pawn.rs b/position/src/move_generator/pawn.rs index c6e7a5a..1aa7a4d 100644 --- a/position/src/move_generator/pawn.rs +++ b/position/src/move_generator/pawn.rs @@ -1,14 +1,22 @@ // Eryn Wells use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use crate::Position; +use crate::{r#move::Move, MoveBuilder, Position}; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, PlacedPiece, Rank, Shape, Square}; +use std::collections::BTreeMap; #[derive(Debug)] struct MoveIterator(usize, usize); -move_generator_declaration!(PawnMoveGenerator); +#[derive(Clone, Debug, Eq, PartialEq)] +pub(super) struct PawnMoveGenerator { + color: chessfriend_core::Color, + move_sets: BTreeMap, + en_passant_captures: Vec, +} + +move_generator_declaration!(PawnMoveGenerator, getters); impl MoveGeneratorInternal for PawnMoveGenerator { fn shape() -> Shape { @@ -31,7 +39,50 @@ impl MoveGeneratorInternal for PawnMoveGenerator { } impl PawnMoveGenerator { - fn pushes(position: &Position, piece: PlacedPiece) -> BitBoard { + pub(super) fn new( + position: &Position, + color: Color, + capture_mask: BitBoard, + push_mask: BitBoard, + ) -> Self { + let move_sets = if !capture_mask.is_empty() && !push_mask.is_empty() { + Self::move_sets(position, color, capture_mask, push_mask) + } else { + std::collections::BTreeMap::new() + }; + + Self { + color, + move_sets, + en_passant_captures: Vec::new(), + } + } + + fn move_sets( + position: &Position, + color: Color, + capture_mask: BitBoard, + push_mask: BitBoard, + ) -> BTreeMap { + let piece = Self::piece(color); + let mut moves_for_pieces = + BTreeMap::from_iter(position.bitboard_for_piece(piece).occupied_squares().map( + |square| { + let piece = PlacedPiece::new(piece, square); + let move_set = + Self::move_set_for_piece(position, &piece, capture_mask, push_mask); + (square, move_set) + }, + )); + + if position.has_en_passant_square() { + + } + + moves_for_pieces + } + + fn pushes(position: &Position, piece: &PlacedPiece) -> BitBoard { let color = piece.color(); let square = piece.square(); let bitboard: BitBoard = square.into(); @@ -59,16 +110,40 @@ impl PawnMoveGenerator { } } - fn attacks(position: &Position, piece: PlacedPiece) -> BitBoard { + fn attacks(position: &Position, piece: &PlacedPiece) -> BitBoard { let color = piece.color(); let opponent_pieces = position.bitboard_for_color(color.other()); - let en_passant_bitboard = match position.en_passant_square() { - Some(square) => >::into(square), - None => BitBoard::empty(), - }; - BitBoard::pawn_attacks(piece.square(), color) & (opponent_pieces | en_passant_bitboard) + BitBoard::pawn_attacks(piece.square(), color) & opponent_pieces + } + + fn en_passant_attack(position: &Position, piece: &PlacedPiece) -> Option { + match position.en_passant() { + Some(en_passant) => { + let en_passant_bitboard: BitBoard = en_passant.target_square().into(); + let capture = + BitBoard::pawn_attacks(piece.square(), piece.color()) & en_passant_bitboard; + + if capture.is_empty() { + return None; + } + + match position.piece_on_square(en_passant.capture_square()) { + Some(captured_piece) => Some( + MoveBuilder::new( + *piece.piece(), + piece.square(), + en_passant.target_square(), + ) + .capturing_en_passant(captured_piece) + .build(), + ), + None => None, + } + } + None => None, + } } } From a2865c87b02e8b61b23727ecaa7db381eba77900 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 11 Feb 2024 10:22:10 -0700 Subject: [PATCH 168/423] Remove an unused Rank import from position.rs --- position/src/position/position.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/position/src/position/position.rs b/position/src/position/position.rs index 15da1c7..9b913c2 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -9,7 +9,7 @@ use crate::{ sight::SightExt, }; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece, PlacedPiece, Rank, Shape, Square}; +use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; use chessfriend_moves::EnPassant; use std::{cell::OnceCell, fmt}; From f23967f4f3096596fd7d34b690e72646c089405c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 11 Feb 2024 10:29:09 -0700 Subject: [PATCH 169/423] =?UTF-8?q?Rename=20MoveGenerator::bitboard=20?= =?UTF-8?q?=E2=86=92=20=5Ftest=5Fbitboard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This method is only used by tests. Mark it with cfg(test) and prefix it with _test to indicate that fact. --- position/src/move_generator.rs | 3 ++- position/src/move_generator/bishop.rs | 8 ++++---- position/src/move_generator/king.rs | 8 ++++---- position/src/move_generator/queen.rs | 8 ++++---- position/src/move_generator/rook.rs | 8 ++++---- 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/position/src/move_generator.rs b/position/src/move_generator.rs index e6884d4..c9d4b76 100644 --- a/position/src/move_generator.rs +++ b/position/src/move_generator.rs @@ -77,7 +77,8 @@ macro_rules! move_generator_declaration { self.move_sets.get(&piece.square()) } - fn bitboard(&self) -> chessfriend_bitboard::BitBoard { + #[cfg(test)] + fn _test_bitboard(&self) -> chessfriend_bitboard::BitBoard { self.move_sets.values().fold( chessfriend_bitboard::BitBoard::empty(), |partial, mv_set| partial | mv_set.bitboard(), diff --git a/position/src/move_generator/bishop.rs b/position/src/move_generator/bishop.rs index 5630246..a57b1a4 100644 --- a/position/src/move_generator/bishop.rs +++ b/position/src/move_generator/bishop.rs @@ -72,7 +72,7 @@ mod tests { ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( - generator.bitboard(), + generator._test_bitboard(), BitBoard::new( 0b10000000_01000000_00100000_00010000_00001000_00000100_00000010_00000000 ) @@ -93,7 +93,7 @@ mod tests { ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( - generator.bitboard(), + generator._test_bitboard(), BitBoard::new( 0b00000000_00000000_00000000_00000000_00001000_00000100_00000010_00000000 ) @@ -112,7 +112,7 @@ mod tests { ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( - generator.bitboard(), + generator._test_bitboard(), BitBoard::new( 0b00000000_00000000_00000000_00000000_00000000_00000100_00000010_00000000 ) @@ -129,7 +129,7 @@ mod tests { let generator = ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); - let bitboard = generator.bitboard(); + let bitboard = generator._test_bitboard(); let expected = BitBoard::new( 0b00000001_10000010_01000100_00101000_00000000_00101000_01000100_10000010, ); diff --git a/position/src/move_generator/king.rs b/position/src/move_generator/king.rs index befd0b2..121905a 100644 --- a/position/src/move_generator/king.rs +++ b/position/src/move_generator/king.rs @@ -69,7 +69,7 @@ mod tests { let generator = KingMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( - generator.bitboard(), + generator._test_bitboard(), bitboard![E5, F5, F4, F3, E3, D3, D4, D5] ); @@ -95,10 +95,10 @@ mod tests { let generator = KingMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); - let generated_bitboard = generator.bitboard(); + let generated_bitboard = generator._test_bitboard(); let expected_bitboard = bitboard![A2, B2, B1]; assert_eq!( - generator.bitboard(), + generator._test_bitboard(), bitboard![A2, B2, B1], "Generated:\n{generated_bitboard}\nExpected:\n{expected_bitboard}" ); @@ -138,7 +138,7 @@ mod tests { assert!(pos.is_king_in_check()); let generator = KingMoveGenerator::new(&pos, Color::Black, BitBoard::FULL, BitBoard::FULL); - let generated_moves = generator.bitboard(); + let generated_moves = generator._test_bitboard(); let expected_moves = bitboard![F8, F7, F6, D6, D7, D8]; diff --git a/position/src/move_generator/queen.rs b/position/src/move_generator/queen.rs index 0b8a571..e0817cb 100644 --- a/position/src/move_generator/queen.rs +++ b/position/src/move_generator/queen.rs @@ -75,7 +75,7 @@ mod tests { let generator = ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); - let bitboard = generator.bitboard(); + let bitboard = generator._test_bitboard(); let expected = bitboard![ A2, C2, D2, E2, F2, G2, H2, // Rank B1, B3, B4, B5, B6, B7, B8, // File @@ -102,7 +102,7 @@ mod tests { let generator = ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); - let bitboard = generator.bitboard(); + let bitboard = generator._test_bitboard(); let expected = BitBoard::new( 0b10000001_01000001_00100001_00010001_00001001_00000101_00000011_00001110, ); @@ -127,7 +127,7 @@ mod tests { ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( - generator.bitboard(), + generator._test_bitboard(), bitboard![ A2, C2, D2, E2, F2, G2, H2, // Rank B1, B3, B4, B5, B6, B7, B8, // File @@ -146,7 +146,7 @@ mod tests { ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( - generator.bitboard(), + generator._test_bitboard(), bitboard![ A3, B3, C3, E3, F3, G3, H3, // Rank D1, D2, D4, D5, D6, D7, D8, // File diff --git a/position/src/move_generator/rook.rs b/position/src/move_generator/rook.rs index 8e9037a..f692da5 100644 --- a/position/src/move_generator/rook.rs +++ b/position/src/move_generator/rook.rs @@ -73,7 +73,7 @@ mod tests { ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( - generator.bitboard(), + generator._test_bitboard(), bitboard![A1, A3, A4, A5, A6, A7, A8, B2, C2, D2, E2, F2, G2, H2] ); } @@ -92,7 +92,7 @@ mod tests { ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( - generator.bitboard(), + generator._test_bitboard(), BitBoard::new( 0b00000001_00000001_00000001_00000001_00000001_00000001_00000001_00001110 ) @@ -111,7 +111,7 @@ mod tests { ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( - generator.bitboard(), + generator._test_bitboard(), bitboard![A2, A3, A4, A5, A6, A7, A8, B1, C1, D1, E1] ); } @@ -124,7 +124,7 @@ mod tests { ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( - generator.bitboard(), + generator._test_bitboard(), bitboard![A4, B4, C4, E4, F4, G4, H4, D1, D2, D3, D5, D6, D7, D8] ); } From e172bfb5dd9ef87d5760b8f09a9e7e3b72a7681b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 13 Feb 2024 11:03:28 -0700 Subject: [PATCH 170/423] Remove the Copy trait from most move Styles and add Clone, Debug, Eq, and PartialEq to Promotion and Castle --- moves/src/builder.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/moves/src/builder.rs b/moves/src/builder.rs index 780ed73..e8144a1 100644 --- a/moves/src/builder.rs +++ b/moves/src/builder.rs @@ -31,43 +31,45 @@ pub trait Style { } } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Builder { style: S, } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Null; -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Push { from: Option, to: Option, } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct DoublePush { from: Square, to: Square, } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Capture { push: Push, capture: Option, } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct EnPassantCapture { push: Push, capture: Option, } +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Promotion { style: S, promotion: PromotionShape, } +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Castle { castle: castle::Castle, } From 5d1ad73be6d1021747ec02f27d89aa9cccc08544 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 13 Feb 2024 11:04:21 -0700 Subject: [PATCH 171/423] In Move::capture_square, move the en passant check above the simple capture check --- moves/src/moves.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/moves/src/moves.rs b/moves/src/moves.rs index f4f9b21..5348306 100644 --- a/moves/src/moves.rs +++ b/moves/src/moves.rs @@ -18,10 +18,6 @@ impl Move { } pub fn capture_square(&self) -> Option { - if self.is_capture() { - return Some(self.target_square()); - } - if self.is_en_passant() { let target_square = self.target_square(); return Some(match target_square.rank() { @@ -31,6 +27,10 @@ impl Move { }); } + if self.is_capture() { + return Some(self.target_square()); + } + None } From 047eb4fd77cbd3999b18dfc5d8b4e5bc89bf8ae3 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 13 Feb 2024 11:05:02 -0700 Subject: [PATCH 172/423] Get en passant move building working (again?) --- moves/src/builder.rs | 19 ++++++++++++------- moves/tests/flags.rs | 21 +++++++++++++-------- moves/tests/pushes.rs | 4 +++- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/moves/src/builder.rs b/moves/src/builder.rs index e8144a1..f18e5d1 100644 --- a/moves/src/builder.rs +++ b/moves/src/builder.rs @@ -270,14 +270,19 @@ impl Builder { } pub fn capturing_en_passant_on(self, square: Square) -> Builder { - let mut style = self.style; - style.to = Some(square); + match EnPassant::from_target_square(square) { + Some(en_passant) => { + let mut style = self.style; + style.to = Some(en_passant.target_square()); - Builder { - style: EnPassantCapture { - push: self.style, - capture: Some(square), - }, + Builder { + style: EnPassantCapture { + push: style, + capture: Some(en_passant.capture_square()), + }, + } + } + None => todo!(), } } diff --git a/moves/tests/flags.rs b/moves/tests/flags.rs index cff0841..2e56389 100644 --- a/moves/tests/flags.rs +++ b/moves/tests/flags.rs @@ -27,7 +27,9 @@ macro_rules! assert_flags { #[test] fn move_flags_quiet() -> Result<(), BuilderError> { - let mv = Builder::push(&piece!(White Pawn on A4), Square::A5).build()?; + let mv = Builder::push(&piece!(White Pawn on A4)) + .to(Square::A5) + .build()?; assert_flags!(mv, true, false, false, false, false, false); Ok(()) @@ -57,21 +59,23 @@ fn move_flags_capture() -> Result<(), BuilderError> { fn move_flags_en_passant_capture() -> Result<(), BuilderError> { let mv = unsafe { Builder::new() - .from(Square::A5) - .capturing_en_passant_on(Square::B4) - .build_unchecked()? + .from(Square::A4) + .capturing_en_passant_on(Square::B3) + .build_unchecked() }; assert_flags!(mv, false, false, true, true, false, false); - assert_eq!(mv.origin_square(), Square::A5); - assert_eq!(mv.target_square(), Square::B4); + assert_eq!(mv.origin_square(), Square::A4); + assert_eq!(mv.target_square(), Square::B3); + assert_eq!(mv.capture_square(), Some(Square::B4)); Ok(()) } #[test] fn move_flags_promotion() -> Result<(), BuilderError> { - let mv = Builder::push(&piece!(White Pawn on H7), Square::H8) + let mv = Builder::push(&piece!(White Pawn on H7)) + .to(Square::H8) .promoting_to(PromotionShape::Queen) .build()?; @@ -83,7 +87,8 @@ fn move_flags_promotion() -> Result<(), BuilderError> { #[test] fn move_flags_capture_promotion() -> Result<(), BuilderError> { - let mv = Builder::push(&piece!(White Pawn on H7), Square::H8) + let mv = Builder::push(&piece!(White Pawn on H7)) + .to(Square::H8) .capturing_piece(&piece!(Black Knight on G8)) .promoting_to(PromotionShape::Queen) .build()?; diff --git a/moves/tests/pushes.rs b/moves/tests/pushes.rs index 0ef2a2f..ad66152 100644 --- a/moves/tests/pushes.rs +++ b/moves/tests/pushes.rs @@ -5,7 +5,9 @@ use chessfriend_moves::{Builder, BuilderError}; #[test] fn pawn_push() -> Result<(), BuilderError> { - let mv = Builder::push(&piece!(White Pawn on A3), Square::A4).build()?; + let mv = Builder::push(&piece!(White Pawn on A3)) + .to(Square::A4) + .build()?; assert!(mv.is_quiet()); assert_eq!(mv.origin_square(), Square::A3); From b3e55f6dcdfd45daf68c9e87942941e626fa5c7c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 13 Feb 2024 11:07:49 -0700 Subject: [PATCH 173/423] Split out some unchecked and check move build() methods Unchecked are unsafe. Checked are safe. --- moves/src/builder.rs | 54 +++++++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/moves/src/builder.rs b/moves/src/builder.rs index f18e5d1..b71c432 100644 --- a/moves/src/builder.rs +++ b/moves/src/builder.rs @@ -1,10 +1,11 @@ // Eryn Wells -use crate::{castle, defs::Kind, Move, PromotionShape}; +use crate::{castle, defs::Kind, EnPassant, Move, PromotionShape}; use chessfriend_core::{Color, File, PlacedPiece, Rank, Square}; use std::result::Result as StdResult; pub type Result = std::result::Result; +type EncodedMoveResult = std::result::Result; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Error { @@ -23,11 +24,22 @@ pub trait Style { None } - fn into_move_bits(&self) -> StdResult { - let origin_square = self.origin_square().ok_or(Error::MissingOriginSquare)? as u16; - let target_square = self.target_square().ok_or(Error::MissingTargetSquare)? as u16; + fn into_move_bits(&self) -> EncodedMoveResult { + let origin_square = self.origin_square().ok_or(Error::MissingOriginSquare)?; + let target_square = self.target_square().ok_or(Error::MissingTargetSquare)?; - Ok((origin_square & 0b111111) << 4 | (target_square & 0b111111) << 10) + Ok(self._build_move_bits(origin_square, target_square)) + } + + unsafe fn into_move_bits_unchecked(&self) -> u16 { + let origin_square = self.origin_square().unwrap(); + let target_square = self.target_square().unwrap(); + + self._build_move_bits(origin_square, target_square) + } + + fn _build_move_bits(&self, origin_square: Square, target_square: Square) -> u16 { + (origin_square as u16 & 0b111111) << 4 | (target_square as u16 & 0b111111) << 10 } } @@ -123,11 +135,17 @@ impl Style for EnPassantCapture { self.push.to } - fn into_move_bits(&self) -> StdResult { - let origin_square = self.origin_square().ok_or(Error::MissingOriginSquare)? as u16; - let target_square = self.target_square().ok_or(Error::MissingTargetSquare)? as u16; + fn into_move_bits(&self) -> EncodedMoveResult { + let origin_square = self.origin_square().ok_or(Error::MissingOriginSquare)?; + let target_square = self.target_square().ok_or(Error::MissingTargetSquare)?; - Ok((origin_square & 0b111111) << 4 | (target_square & 0b111111) << 10) + Ok(self._build_move_bits(origin_square, target_square)) + } +} + +impl EnPassantCapture { + fn _build_move_bits(&self, origin_square: Square, target_square: Square) -> u16 { + (origin_square as u16 & 0b111111) << 4 | (target_square as u16 & 0b111111) << 10 } } @@ -192,11 +210,11 @@ impl Builder { Self { style: Null } } - pub fn push(piece: &PlacedPiece, to: Square) -> Builder { + pub fn push(piece: &PlacedPiece) -> Builder { Builder { style: Push { from: Some(piece.square()), - to: Some(to), + to: None, }, } } @@ -224,12 +242,8 @@ impl Builder { } } - pub fn capturing_on(piece: &PlacedPiece, to: Square) -> Builder { - Self::push(piece, to).capturing_on(to) - } - pub fn capturing_piece(piece: &PlacedPiece, capturing: &PlacedPiece) -> Builder { - Self::push(piece, capturing.square()).capturing_piece(&capturing) + Self::push(piece).capturing_piece(&capturing) } pub fn from(self, square: Square) -> Builder { @@ -252,7 +266,7 @@ impl Builder { self } - pub fn to(&mut self, square: Square) -> &mut Self { + pub fn to(mut self, square: Square) -> Self { self.style.to = Some(square); self } @@ -346,7 +360,11 @@ impl Builder { } impl Builder { - pub unsafe fn build_unchecked(&self) -> Result { + pub unsafe fn build_unchecked(&self) -> Move { + Move(Kind::EnPassantCapture as u16 | self.style.into_move_bits_unchecked()) + } + + pub fn build(&self) -> Result { Ok(Move( Kind::EnPassantCapture as u16 | self.style.into_move_bits()?, )) From b5d4069751426baa3e32d1b126d34010663d4e11 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 25 Feb 2024 08:51:23 -0800 Subject: [PATCH 174/423] Make moves::castle::Parameters public MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename this struct CastlingParameters → Parameters Implement getter methods for private properties --- moves/src/castle.rs | 50 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/moves/src/castle.rs b/moves/src/castle.rs index 5459ff4..731f25f 100644 --- a/moves/src/castle.rs +++ b/moves/src/castle.rs @@ -1,7 +1,7 @@ // Eryn Wells use chessfriend_bitboard::BitBoard; -use chessfriend_core::Square; +use chessfriend_core::{Color, Square}; #[repr(u8)] #[derive(Copy, Clone, Debug, Eq, PartialEq)] @@ -10,7 +10,7 @@ pub enum Castle { QueenSide = 1, } -pub(crate) struct CastlingParameters { +pub struct Parameters { /// Origin squares of the king and rook. origin_squares: Squares, @@ -24,19 +24,45 @@ pub(crate) struct CastlingParameters { check_squares: BitBoard, } +impl Parameters { + pub fn king_origin_square(&self) -> Square { + self.origin_squares.king + } + + pub fn rook_origin_square(&self) -> Square { + self.origin_squares.rook + } + + pub fn king_target_square(&self) -> Square { + self.target_squares.king + } + + pub fn rook_target_square(&self) -> Square { + self.target_squares.rook + } + + pub fn clear_squares(&self) -> &BitBoard { + &self.clear_squares + } + + pub fn check_squares(&self) -> &BitBoard { + &self.check_squares + } +} + #[derive(Debug)] -pub(crate) struct Squares { - pub king: Square, - pub rook: Square, +struct Squares { + king: Square, + rook: Square, } impl Castle { pub const ALL: [Castle; 2] = [Castle::KingSide, Castle::QueenSide]; /// Parameters for each castling move, organized by color and board-side. - const PARAMETERS: [[CastlingParameters; 2]; 2] = [ + const PARAMETERS: [[Parameters; 2]; 2] = [ [ - CastlingParameters { + Parameters { origin_squares: Squares { king: Square::E1, rook: Square::H1, @@ -48,7 +74,7 @@ impl Castle { clear_squares: BitBoard::new(0b01100000), check_squares: BitBoard::new(0b01110000), }, - CastlingParameters { + Parameters { origin_squares: Squares { king: Square::E1, rook: Square::A1, @@ -62,7 +88,7 @@ impl Castle { }, ], [ - CastlingParameters { + Parameters { origin_squares: Squares { king: Square::E8, rook: Square::H8, @@ -74,7 +100,7 @@ impl Castle { clear_squares: BitBoard::new(0b01100000 << 8 * 7), check_squares: BitBoard::new(0b01110000 << 8 * 7), }, - CastlingParameters { + Parameters { origin_squares: Squares { king: Square::E8, rook: Square::A8, @@ -88,4 +114,8 @@ impl Castle { }, ], ]; + + pub fn parameters(&self, color: Color) -> &'static Parameters { + &Castle::PARAMETERS[color as usize][*self as usize] + } } From d714374f35ee63a9a8976f547fe7fadc2cae835a Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 25 Feb 2024 08:57:16 -0800 Subject: [PATCH 175/423] Pass self by reference to move builder methods where possible --- moves/src/builder.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/moves/src/builder.rs b/moves/src/builder.rs index b71c432..a1ab78e 100644 --- a/moves/src/builder.rs +++ b/moves/src/builder.rs @@ -246,7 +246,7 @@ impl Builder { Self::push(piece).capturing_piece(&capturing) } - pub fn from(self, square: Square) -> Builder { + pub fn from(&self, square: Square) -> Builder { Builder { style: Push { from: Some(square), @@ -266,13 +266,13 @@ impl Builder { self } - pub fn to(mut self, square: Square) -> Self { + pub fn to(&mut self, square: Square) -> &mut Self { self.style.to = Some(square); self } - pub fn capturing_on(self, square: Square) -> Builder { - let mut style = self.style; + pub fn capturing_on(&self, square: Square) -> Builder { + let mut style = self.style.clone(); style.to = Some(square); Builder { @@ -283,10 +283,10 @@ impl Builder { } } - pub fn capturing_en_passant_on(self, square: Square) -> Builder { + pub fn capturing_en_passant_on(&self, square: Square) -> Builder { match EnPassant::from_target_square(square) { Some(en_passant) => { - let mut style = self.style; + let mut style = self.style.clone(); style.to = Some(en_passant.target_square()); Builder { @@ -300,19 +300,19 @@ impl Builder { } } - pub fn capturing_piece(self, piece: &PlacedPiece) -> Builder { + pub fn capturing_piece(&self, piece: &PlacedPiece) -> Builder { Builder { style: Capture { - push: self.style, + push: self.style.clone(), capture: Some(piece.square()), }, } } - pub fn promoting_to(self, shape: PromotionShape) -> Builder> { + pub fn promoting_to(&self, shape: PromotionShape) -> Builder> { Builder { style: Promotion { - style: self.style, + style: self.style.clone(), promotion: shape, }, } From 9b7bf3a212d643947d670e75cd9a8717f03129dd Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 25 Feb 2024 09:15:07 -0800 Subject: [PATCH 176/423] Implement some helpful testing types and traits in the moves package --- moves/src/lib.rs | 4 +++- moves/src/testing.rs | 17 +++++++++++++++++ moves/tests/flags.rs | 16 ++++++++-------- moves/tests/pushes.rs | 4 ++-- 4 files changed, 30 insertions(+), 11 deletions(-) create mode 100644 moves/src/testing.rs diff --git a/moves/src/lib.rs b/moves/src/lib.rs index 75e26ce..297f82e 100644 --- a/moves/src/lib.rs +++ b/moves/src/lib.rs @@ -1,12 +1,14 @@ // Eryn Wells +pub mod testing; + mod builder; mod castle; mod defs; mod en_passant; mod moves; -pub use builder::{Builder, Error as BuilderError}; +pub use builder::{Builder, Error as BuildMoveError, Result as BuildMoveResult}; pub use castle::Castle; pub use defs::PromotionShape; pub use en_passant::EnPassant; diff --git a/moves/src/testing.rs b/moves/src/testing.rs new file mode 100644 index 0000000..b298d5d --- /dev/null +++ b/moves/src/testing.rs @@ -0,0 +1,17 @@ +// Eryn Wells + +use crate::BuildMoveError; + +pub type TestResult = Result<(), TestError>; + +#[derive(Debug, Eq, PartialEq)] +pub enum TestError { + BuildMove(BuildMoveError), + NoLegalMoves, +} + +impl From for TestError { + fn from(value: BuildMoveError) -> Self { + TestError::BuildMove(value) + } +} diff --git a/moves/tests/flags.rs b/moves/tests/flags.rs index 2e56389..cdcdd57 100644 --- a/moves/tests/flags.rs +++ b/moves/tests/flags.rs @@ -1,7 +1,7 @@ // Eryn Wells use chessfriend_core::{piece, Color, File, Shape, Square}; -use chessfriend_moves::{Builder, BuilderError, Castle, PromotionShape}; +use chessfriend_moves::{testing::*, Builder, Castle, PromotionShape}; macro_rules! assert_flag { ($move:expr, $left:expr, $right:expr, $desc:expr) => { @@ -26,7 +26,7 @@ macro_rules! assert_flags { } #[test] -fn move_flags_quiet() -> Result<(), BuilderError> { +fn move_flags_quiet() -> TestResult { let mv = Builder::push(&piece!(White Pawn on A4)) .to(Square::A5) .build()?; @@ -36,7 +36,7 @@ fn move_flags_quiet() -> Result<(), BuilderError> { } #[test] -fn move_flags_double_push() -> Result<(), BuilderError> { +fn move_flags_double_push() -> TestResult { let mv = Builder::double_push(File::C, Color::White).build()?; assert_flags!(mv, false, true, false, false, false, false); @@ -44,7 +44,7 @@ fn move_flags_double_push() -> Result<(), BuilderError> { } #[test] -fn move_flags_capture() -> Result<(), BuilderError> { +fn move_flags_capture() -> TestResult { let mv = Builder::new() .from(Square::A4) .capturing_on(Square::B5) @@ -56,7 +56,7 @@ fn move_flags_capture() -> Result<(), BuilderError> { } #[test] -fn move_flags_en_passant_capture() -> Result<(), BuilderError> { +fn move_flags_en_passant_capture() -> TestResult { let mv = unsafe { Builder::new() .from(Square::A4) @@ -73,7 +73,7 @@ fn move_flags_en_passant_capture() -> Result<(), BuilderError> { } #[test] -fn move_flags_promotion() -> Result<(), BuilderError> { +fn move_flags_promotion() -> TestResult { let mv = Builder::push(&piece!(White Pawn on H7)) .to(Square::H8) .promoting_to(PromotionShape::Queen) @@ -86,7 +86,7 @@ fn move_flags_promotion() -> Result<(), BuilderError> { } #[test] -fn move_flags_capture_promotion() -> Result<(), BuilderError> { +fn move_flags_capture_promotion() -> TestResult { let mv = Builder::push(&piece!(White Pawn on H7)) .to(Square::H8) .capturing_piece(&piece!(Black Knight on G8)) @@ -100,7 +100,7 @@ fn move_flags_capture_promotion() -> Result<(), BuilderError> { } #[test] -fn move_flags_castle() -> Result<(), BuilderError> { +fn move_flags_castle() -> TestResult { let mv = Builder::castling(Castle::KingSide).build(); assert_flags!(mv, false, false, false, false, true, false); diff --git a/moves/tests/pushes.rs b/moves/tests/pushes.rs index ad66152..7406eb0 100644 --- a/moves/tests/pushes.rs +++ b/moves/tests/pushes.rs @@ -1,10 +1,10 @@ // Eryn Wells use chessfriend_core::{piece, Square}; -use chessfriend_moves::{Builder, BuilderError}; +use chessfriend_moves::{testing::*, Builder}; #[test] -fn pawn_push() -> Result<(), BuilderError> { +fn pawn_push() -> TestResult { let mv = Builder::push(&piece!(White Pawn on A3)) .to(Square::A4) .build()?; From 36db46ac18cfe864c25299c99641b01f692e5253 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 25 Feb 2024 09:20:45 -0800 Subject: [PATCH 177/423] =?UTF-8?q?Move=20position::tests=20=E2=86=92=20te?= =?UTF-8?q?sting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expand information printed in assert_move_list --- position/src/lib.rs | 2 +- position/src/testing.rs | 58 +++++++++++++++++++++++++++++++++++++++++ position/src/tests.rs | 34 ------------------------ 3 files changed, 59 insertions(+), 35 deletions(-) create mode 100644 position/src/testing.rs delete mode 100644 position/src/tests.rs diff --git a/position/src/lib.rs b/position/src/lib.rs index 5776e05..d313231 100644 --- a/position/src/lib.rs +++ b/position/src/lib.rs @@ -13,7 +13,7 @@ mod sight; mod macros; #[macro_use] -mod tests; +mod testing; pub use position::{MoveBuilder as MakeMoveBuilder, Position, PositionBuilder}; pub use r#move::{Castle, MakeMoveError, Move, MoveBuilder}; diff --git a/position/src/testing.rs b/position/src/testing.rs new file mode 100644 index 0000000..ba4bd69 --- /dev/null +++ b/position/src/testing.rs @@ -0,0 +1,58 @@ +// Eryn Wells + +use crate::MakeMoveError; +use chessfriend_moves::{BuildMoveError, BuildMoveResult, Move}; + +#[macro_export] +macro_rules! assert_move_list { + ($generated:expr, $expected:expr, $position:expr) => { + assert_eq!( + $generated, + $expected, + "\n\tMatching: {:?}\n\tGenerated, not expected: {:?}\n\tExpected, not generated: {:?}", + $generated + .intersection(&$expected) + .map(|mv| format!("{}", mv)) + .collect::>(), + $generated + .difference(&$expected) + .map(|mv| format!("{}", mv)) + .collect::>(), + $expected + .difference(&$generated) + .map(|mv| format!("{}", mv)) + .collect::>(), + ) + }; +} + +#[macro_export] +macro_rules! formatted_move_list { + ($move_list:expr, $position:expr) => { + $move_list + .iter() + .map(|mv| format!("{}", mv)) + .collect::>() + }; +} + +pub type TestResult = Result<(), TestError>; + +#[derive(Debug, Eq, PartialEq)] +pub enum TestError { + BuildMove(BuildMoveError), + MakeMove(MakeMoveError), + NoLegalMoves, +} + +impl From for TestError { + fn from(value: BuildMoveError) -> Self { + TestError::BuildMove(value) + } +} + +impl From for TestError { + fn from(value: MakeMoveError) -> Self { + TestError::MakeMove(value) + } +} diff --git a/position/src/tests.rs b/position/src/tests.rs deleted file mode 100644 index 962b9bc..0000000 --- a/position/src/tests.rs +++ /dev/null @@ -1,34 +0,0 @@ -// Eryn Wells - -#[macro_export] -macro_rules! assert_move_list { - ($generated:expr, $expected:expr, $position:expr) => { - assert_eq!( - $generated, - $expected, - "Difference: {:?}", - $generated - .symmetric_difference(&$expected) - .map(|mv| format!( - "{}", - $crate::r#move::AlgebraicMoveFormatter::new(mv, &$position) - )) - .collect::>() - ) - }; -} - -#[macro_export] -macro_rules! formatted_move_list { - ($move_list:expr, $position:expr) => { - $move_list - .iter() - .map(|mv| { - format!( - "{}", - $crate::r#move::AlgebraicMoveFormatter::new(mv, &$position) - ) - }) - .collect::>() - }; -} From aaad9918998b7e015c163b867dd5b2b3a2594a0c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 25 Feb 2024 09:44:25 -0800 Subject: [PATCH 178/423] Replace crate::r#move::castle::Castle with moves::Castle --- position/src/move_generator/king.rs | 3 ++- position/src/move_generator/move_set.rs | 2 +- position/src/position/builders/position_builder.rs | 2 +- position/src/position/flags.rs | 4 ++-- position/src/position/position.rs | 5 ++--- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/position/src/move_generator/king.rs b/position/src/move_generator/king.rs index 121905a..1813339 100644 --- a/position/src/move_generator/king.rs +++ b/position/src/move_generator/king.rs @@ -4,9 +4,10 @@ //! generating the possible moves for the king in the given position. use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use crate::{r#move::Castle, Position}; +use crate::Position; use chessfriend_bitboard::BitBoard; use chessfriend_core::{PlacedPiece, Shape}; +use chessfriend_moves::Castle; move_generator_declaration!(KingMoveGenerator, struct); move_generator_declaration!(KingMoveGenerator, new); diff --git a/position/src/move_generator/move_set.rs b/position/src/move_generator/move_set.rs index 6cefb7a..d03eaef 100644 --- a/position/src/move_generator/move_set.rs +++ b/position/src/move_generator/move_set.rs @@ -1,8 +1,8 @@ // Eryn Wells -use crate::{r#move::Castle, Move, MoveBuilder}; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; +use chessfriend_moves::{Builder as MoveBuilder, Castle, Move}; /// A set of bitboards defining the moves for a single piece on the board. #[derive(Clone, Debug, Default, Eq, PartialEq)] diff --git a/position/src/position/builders/position_builder.rs b/position/src/position/builders/position_builder.rs index 96757ce..28652ed 100644 --- a/position/src/position/builders/position_builder.rs +++ b/position/src/position/builders/position_builder.rs @@ -2,10 +2,10 @@ use crate::{ position::{flags::Flags, piece_sets::PieceBitBoards}, - r#move::Castle, Position, }; use chessfriend_core::{piece, Color, Piece, PlacedPiece, Rank, Shape, Square}; +use chessfriend_moves::Castle; use std::collections::BTreeMap; #[derive(Clone)] diff --git a/position/src/position/flags.rs b/position/src/position/flags.rs index 47e49bf..7c722a9 100644 --- a/position/src/position/flags.rs +++ b/position/src/position/flags.rs @@ -1,7 +1,7 @@ // Eryn Wells -use crate::r#move::Castle; use chessfriend_core::Color; +use chessfriend_moves::Castle; use std::fmt; #[derive(Clone, Copy, Eq, Hash, PartialEq)] @@ -10,7 +10,7 @@ pub struct Flags(u8); impl Flags { #[inline] pub(super) fn player_has_right_to_castle_flag_offset(color: Color, castle: Castle) -> usize { - ((color as usize) << 1) + castle.into_index() + ((color as usize) << 1) + castle as usize } pub(super) fn player_has_right_to_castle(&self, color: Color, castle: Castle) -> bool { diff --git a/position/src/position/position.rs b/position/src/position/position.rs index 9b913c2..bbdcb9e 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -5,12 +5,11 @@ use crate::{ check::CheckingPieces, move_generator::{MoveSet, Moves}, position::DiagramFormatter, - r#move::Castle, sight::SightExt, }; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; -use chessfriend_moves::EnPassant; +use chessfriend_moves::{Castle, EnPassant}; use std::{cell::OnceCell, fmt}; #[derive(Clone, Debug, Eq)] @@ -93,7 +92,7 @@ impl Position { return false; } - let castling_parameters = castle.parameters(); + let castling_parameters = castle.parameters(player); let all_pieces = self.occupied_squares(); if !(all_pieces & castling_parameters.clear_squares()).is_empty() { From d668091d0dadd2dede4260cbf68dc15305b55d5c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 25 Feb 2024 09:52:40 -0800 Subject: [PATCH 179/423] Replace uses of types in r#move with types from the moves package types --- position/src/fen.rs | 3 +- position/src/lib.rs | 5 +- position/src/move_generator.rs | 7 +- position/src/move_generator/king.rs | 73 ++++------ position/src/move_generator/knight.rs | 53 +++---- position/src/move_generator/move_set.rs | 41 ++---- position/src/move_generator/pawn.rs | 134 ++++++++++-------- .../move_generator/tests/peterellisjones.rs | 90 +++++++----- .../src/move_generator/tests/single_pieces.rs | 38 +++-- position/src/position/builders/mod.rs | 2 +- .../src/position/builders/move_builder.rs | 90 +++++++----- .../src/position/builders/position_builder.rs | 6 +- position/src/position/flags.rs | 2 - position/src/position/mod.rs | 2 +- position/src/position/position.rs | 5 +- position/src/testing.rs | 2 +- 16 files changed, 273 insertions(+), 280 deletions(-) diff --git a/position/src/fen.rs b/position/src/fen.rs index dd9fa29..fab45e1 100644 --- a/position/src/fen.rs +++ b/position/src/fen.rs @@ -1,7 +1,8 @@ // Eryn Wells -use crate::{r#move::Castle, Position, PositionBuilder}; +use crate::{Position, PositionBuilder}; use chessfriend_core::{piece, Color, File, Piece, PlacedPiece, Rank, Square}; +use chessfriend_moves::Castle; use std::fmt::Write; macro_rules! fen { diff --git a/position/src/lib.rs b/position/src/lib.rs index d313231..1995be0 100644 --- a/position/src/lib.rs +++ b/position/src/lib.rs @@ -4,7 +4,6 @@ pub mod fen; mod check; mod display; -mod r#move; mod move_generator; mod position; mod sight; @@ -12,8 +11,8 @@ mod sight; #[macro_use] mod macros; +#[cfg(test)] #[macro_use] mod testing; -pub use position::{MoveBuilder as MakeMoveBuilder, Position, PositionBuilder}; -pub use r#move::{Castle, MakeMoveError, Move, MoveBuilder}; +pub use position::{MakeMoveError, MoveBuilder as MakeMoveBuilder, Position, PositionBuilder}; diff --git a/position/src/move_generator.rs b/position/src/move_generator.rs index c9d4b76..4d14ece 100644 --- a/position/src/move_generator.rs +++ b/position/src/move_generator.rs @@ -7,6 +7,8 @@ mod move_set; mod pawn; mod queen; mod rook; + +#[cfg(test)] mod tests; pub(crate) use move_set::MoveSet; @@ -17,9 +19,10 @@ use self::{ queen::ClassicalMoveGenerator as QueenMoveGenerator, rook::ClassicalMoveGenerator as RookMoveGenerator, }; -use crate::{Move, Position}; +use crate::Position; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; +use chessfriend_moves::Move; use std::collections::BTreeMap; trait MoveGenerator { @@ -66,7 +69,7 @@ macro_rules! move_generator_declaration { }; ($name:ident, getters) => { impl $name { - pub(super) fn iter(&self) -> impl Iterator + '_ { + pub(super) fn iter(&self) -> impl Iterator + '_ { self.move_sets.values().flat_map(|set| set.moves()) } diff --git a/position/src/move_generator/king.rs b/position/src/move_generator/king.rs index 1813339..45c928e 100644 --- a/position/src/move_generator/king.rs +++ b/position/src/move_generator/king.rs @@ -58,13 +58,14 @@ impl MoveGeneratorInternal for KingMoveGenerator { #[cfg(test)] mod tests { use super::*; - use crate::{assert_move_list, position, test_position, Move, MoveBuilder, PositionBuilder}; + use crate::{assert_move_list, position, test_position, testing::*, PositionBuilder}; use chessfriend_bitboard::bitboard; use chessfriend_core::{piece, Color, Square}; + use chessfriend_moves::{Builder as MoveBuilder, Castle, Move}; use std::collections::HashSet; #[test] - fn one_king() { + fn one_king() -> TestResult { let pos = position![White King on E4]; let generator = KingMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); @@ -74,24 +75,27 @@ mod tests { bitboard![E5, F5, F4, F3, E3, D3, D4, D5] ); + let builder = MoveBuilder::push(&piece!(White King on E4)); let expected_moves: HashSet = HashSet::from_iter([ - MoveBuilder::new(piece!(White King), Square::E4, Square::D5).build(), - MoveBuilder::new(piece!(White King), Square::E4, Square::E5).build(), - MoveBuilder::new(piece!(White King), Square::E4, Square::F5).build(), - MoveBuilder::new(piece!(White King), Square::E4, Square::F4).build(), - MoveBuilder::new(piece!(White King), Square::E4, Square::F3).build(), - MoveBuilder::new(piece!(White King), Square::E4, Square::E3).build(), - MoveBuilder::new(piece!(White King), Square::E4, Square::D3).build(), - MoveBuilder::new(piece!(White King), Square::E4, Square::D4).build(), + builder.clone().to(Square::D5).build()?, + builder.clone().to(Square::E5).build()?, + builder.clone().to(Square::F5).build()?, + builder.clone().to(Square::F4).build()?, + builder.clone().to(Square::F3).build()?, + builder.clone().to(Square::E3).build()?, + builder.clone().to(Square::D3).build()?, + builder.clone().to(Square::D4).build()?, ]); let generated_moves: HashSet = generator.iter().collect(); assert_move_list!(generated_moves, expected_moves, pos); + + Ok(()) } #[test] - fn one_king_corner() { + fn one_king_corner() -> TestResult { let pos = position![White King on A1]; let generator = KingMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); @@ -104,13 +108,14 @@ mod tests { "Generated:\n{generated_bitboard}\nExpected:\n{expected_bitboard}" ); + let builder = MoveBuilder::push(&piece!(White King on A1)); let expected_moves = [ - MoveBuilder::new(piece!(White King), Square::A1, Square::A2).build(), - MoveBuilder::new(piece!(White King), Square::A1, Square::B1).build(), - MoveBuilder::new(piece!(White King), Square::A1, Square::B2).build(), + builder.clone().to(Square::A2).build()?, + builder.clone().to(Square::B1).build()?, + builder.clone().to(Square::B2).build()?, ]; - let mut generated_moves: HashSet = generator.iter().collect(); + let mut generated_moves: HashSet<_> = generator.iter().collect(); for ex_move in expected_moves { assert!( @@ -125,6 +130,8 @@ mod tests { "Moves unexpectedly present: {:#?}", generated_moves ); + + Ok(()) } #[test] @@ -161,16 +168,8 @@ mod tests { let generated_moves: HashSet = generator.iter().collect(); let king = piece!(White King); - assert!(generated_moves.contains( - &MoveBuilder::new(king, Square::E1, Square::G1) - .castle(Castle::KingSide) - .build() - )); - assert!(generated_moves.contains( - &MoveBuilder::new(king, Square::E1, Square::C1) - .castle(Castle::QueenSide) - .build() - )); + assert!(generated_moves.contains(&MoveBuilder::castling(Castle::KingSide).build())); + assert!(generated_moves.contains(&MoveBuilder::castling(Castle::QueenSide).build())); } #[test] @@ -189,16 +188,8 @@ mod tests { let generated_moves: HashSet = generator.iter().collect(); let king = piece!(White King); - assert!(generated_moves.contains( - &MoveBuilder::new(king, Square::E1, Square::G1) - .castle(Castle::KingSide) - .build() - )); - assert!(!generated_moves.contains( - &MoveBuilder::new(king, Square::E1, Square::C1) - .castle(Castle::QueenSide) - .build() - )); + assert!(generated_moves.contains(&MoveBuilder::castling(Castle::KingSide).build())); + assert!(!generated_moves.contains(&MoveBuilder::castling(Castle::QueenSide).build())); } #[test] @@ -217,15 +208,7 @@ mod tests { let generated_moves: HashSet = generator.iter().collect(); let king = piece!(White King); - assert!(!generated_moves.contains( - &MoveBuilder::new(king, Square::E1, Square::G1) - .castle(Castle::KingSide) - .build() - )); - assert!(generated_moves.contains( - &MoveBuilder::new(king, Square::E1, Square::C1) - .castle(Castle::QueenSide) - .build() - )); + assert!(!generated_moves.contains(&MoveBuilder::castling(Castle::KingSide).build())); + assert!(generated_moves.contains(&MoveBuilder::castling(Castle::QueenSide).build())); } } diff --git a/position/src/move_generator/knight.rs b/position/src/move_generator/knight.rs index 6366808..2b385ec 100644 --- a/position/src/move_generator/knight.rs +++ b/position/src/move_generator/knight.rs @@ -34,54 +34,35 @@ impl MoveGeneratorInternal for KnightMoveGenerator { #[cfg(test)] mod tests { use super::*; - use crate::{position, Move, MoveBuilder}; + use crate::{assert_move_list, position, testing::*}; use chessfriend_core::{piece, Color, Square}; + use chessfriend_moves::Builder as MoveBuilder; use std::collections::HashSet; #[test] - fn one_knight() { + fn one_knight() -> TestResult { let pos = position![ White Knight on E4, ]; let generator = KnightMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + let generated_moves: HashSet<_> = generator.iter().collect(); - /* - let bb = generator.bitboard(); - assert_eq!( - bb, - BitBoard::new( - 0b00000000_00000000_00000000_00111000_00101000_00111000_00000000_00000000 - ) - ); - */ + let piece = piece!(White Knight on E4); + let expected_moves = HashSet::from_iter([ + MoveBuilder::push(&piece).to(Square::C3).build()?, + MoveBuilder::push(&piece).to(Square::D2).build()?, + MoveBuilder::push(&piece).to(Square::F2).build()?, + MoveBuilder::push(&piece).to(Square::G3).build()?, + MoveBuilder::push(&piece).to(Square::C5).build()?, + MoveBuilder::push(&piece).to(Square::D6).build()?, + MoveBuilder::push(&piece).to(Square::G5).build()?, + MoveBuilder::push(&piece).to(Square::F6).build()?, + ]); - let expected_moves = [ - MoveBuilder::new(piece!(White Knight), Square::E4, Square::C3).build(), - MoveBuilder::new(piece!(White Knight), Square::E4, Square::D2).build(), - MoveBuilder::new(piece!(White Knight), Square::E4, Square::F2).build(), - MoveBuilder::new(piece!(White Knight), Square::E4, Square::G3).build(), - MoveBuilder::new(piece!(White Knight), Square::E4, Square::C5).build(), - MoveBuilder::new(piece!(White Knight), Square::E4, Square::D6).build(), - MoveBuilder::new(piece!(White Knight), Square::E4, Square::G5).build(), - MoveBuilder::new(piece!(White Knight), Square::E4, Square::F6).build(), - ]; + assert_move_list!(generated_moves, expected_moves, pos); - let mut generated_moves: HashSet = generator.iter().collect(); - - for ex_move in expected_moves { - assert!( - generated_moves.remove(&ex_move), - "{:#?} was not generated", - &ex_move - ); - } - - assert!( - generated_moves.is_empty(), - "Moves unexpectedly present: {:#?}", - generated_moves - ); + Ok(()) } } diff --git a/position/src/move_generator/move_set.rs b/position/src/move_generator/move_set.rs index d03eaef..f1042c9 100644 --- a/position/src/move_generator/move_set.rs +++ b/position/src/move_generator/move_set.rs @@ -1,7 +1,7 @@ // Eryn Wells use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; +use chessfriend_core::{PlacedPiece, Square}; use chessfriend_moves::{Builder as MoveBuilder, Castle, Move}; /// A set of bitboards defining the moves for a single piece on the board. @@ -65,57 +65,34 @@ impl MoveSet { } pub(crate) fn moves(&self) -> impl Iterator + '_ { - let piece = self.piece.piece(); - let from_square = self.piece.square(); + let piece = &self.piece; self.bitboards .quiet .occupied_squares() - .map(move |to_square| MoveBuilder::new(*piece, from_square, to_square).build()) + .filter_map(|to_square| MoveBuilder::push(&self.piece).to(to_square).build().ok()) .chain( self.bitboards .captures .occupied_squares() - .map(move |to_square| { - MoveBuilder::new(*piece, from_square, to_square) - .capturing(PlacedPiece::new( - Piece::new(Color::White, Shape::Pawn), - to_square, - )) - .build() - }), + .filter_map(|to_square| MoveBuilder::push(piece).to(to_square).build().ok()), ) .chain( if (self.special & 0b1) != 0 { - Some(()) + let mv = MoveBuilder::castling(Castle::KingSide).build(); + Some(mv) } else { None } - .map(|()| { - MoveBuilder::new( - *piece, - from_square, - Castle::KingSide.target_squares(piece.color()).king, - ) - .castle(Castle::KingSide) - .build() - }), + .into_iter(), ) .chain( if (self.special & 0b10) != 0 { - Some(()) + Some(MoveBuilder::castling(Castle::QueenSide).build()) } else { None } - .map(|()| { - MoveBuilder::new( - *piece, - from_square, - Castle::QueenSide.target_squares(piece.color()).king, - ) - .castle(Castle::QueenSide) - .build() - }), + .into_iter(), ) } } diff --git a/position/src/move_generator/pawn.rs b/position/src/move_generator/pawn.rs index 1aa7a4d..67fefb3 100644 --- a/position/src/move_generator/pawn.rs +++ b/position/src/move_generator/pawn.rs @@ -1,9 +1,10 @@ // Eryn Wells use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use crate::{r#move::Move, MoveBuilder, Position}; +use crate::Position; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, PlacedPiece, Rank, Shape, Square}; +use chessfriend_moves::{Builder as MoveBuilder, Move}; use std::collections::BTreeMap; #[derive(Debug)] @@ -65,7 +66,7 @@ impl PawnMoveGenerator { push_mask: BitBoard, ) -> BTreeMap { let piece = Self::piece(color); - let mut moves_for_pieces = + let moves_for_pieces = BTreeMap::from_iter(position.bitboard_for_piece(piece).occupied_squares().map( |square| { let piece = PlacedPiece::new(piece, square); @@ -75,10 +76,6 @@ impl PawnMoveGenerator { }, )); - if position.has_en_passant_square() { - - } - moves_for_pieces } @@ -121,23 +118,21 @@ impl PawnMoveGenerator { fn en_passant_attack(position: &Position, piece: &PlacedPiece) -> Option { match position.en_passant() { Some(en_passant) => { - let en_passant_bitboard: BitBoard = en_passant.target_square().into(); + let target_square = en_passant.target_square(); + + let en_passant_bitboard: BitBoard = target_square.into(); let capture = BitBoard::pawn_attacks(piece.square(), piece.color()) & en_passant_bitboard; - if capture.is_empty() { return None; } match position.piece_on_square(en_passant.capture_square()) { - Some(captured_piece) => Some( - MoveBuilder::new( - *piece.piece(), - piece.square(), - en_passant.target_square(), - ) - .capturing_en_passant(captured_piece) - .build(), + Some(_) => Some( + MoveBuilder::push(piece) + .capturing_en_passant_on(target_square) + .build() + .ok()?, ), None => None, } @@ -150,46 +145,48 @@ impl PawnMoveGenerator { #[cfg(test)] mod tests { use super::*; - use crate::{assert_move_list, position::DiagramFormatter, test_position, Move, MoveBuilder}; - use chessfriend_core::{piece, Color, Piece, Square}; + use crate::{assert_move_list, position::DiagramFormatter, test_position, testing::*}; + use chessfriend_core::{piece, Color, Square}; + use chessfriend_moves::{Builder as MoveBuilder, Move}; use std::collections::HashSet; #[test] - fn one_2square_push() { + fn one_double_push() -> TestResult { let pos = test_position![White Pawn on E2]; let generator = PawnMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + let pawn = piece!(White Pawn on E2); let expected_moves = HashSet::from_iter([ - MoveBuilder::new(piece!(White Pawn), Square::E2, Square::E3).build(), - MoveBuilder::new(piece!(White Pawn), Square::E2, Square::E4).build(), + MoveBuilder::push(&pawn).to(Square::E3).build()?, + MoveBuilder::double_push(pawn.square().file(), pawn.color()).build()?, ]); - let generated_moves: HashSet = generator.iter().collect(); + let generated_moves: HashSet<_> = generator.iter().collect(); assert_eq!(generated_moves, expected_moves); + + Ok(()) } #[test] - fn one_1square_push() { + fn one_single_push() -> TestResult { let pos = test_position![White Pawn on E3]; let generator = PawnMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + let generated_moves: HashSet<_> = generator.iter().collect(); - let expected_moves = HashSet::from_iter([MoveBuilder::new( - Piece::pawn(Color::White), - Square::E3, - Square::E4, - ) - .build()]); - - let generated_moves: HashSet = generator.iter().collect(); + let expected_moves = HashSet::from_iter([MoveBuilder::push(&piece!(White Pawn on E3)) + .to(Square::E4) + .build()?]); assert_move_list!(generated_moves, expected_moves, pos); + + Ok(()) } #[test] - fn one_obstructed_2square_push() { + fn one_obstructed_2square_push() -> TestResult { let pos = test_position![ White Pawn on E2, White Knight on E4, @@ -199,16 +196,15 @@ mod tests { let generator = PawnMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); - let expected_moves = HashSet::from_iter([MoveBuilder::new( - Piece::pawn(Color::White), - Square::E2, - Square::E3, - ) - .build()]); + let expected_moves = HashSet::from_iter([MoveBuilder::push(&piece!(White Pawn on E2)) + .to(Square::E3) + .build()?]); - let generated_moves: HashSet = generator.iter().collect(); + let generated_moves: HashSet<_> = generator.iter().collect(); assert_move_list!(generated_moves, expected_moves, pos); + + Ok(()) } #[test] @@ -220,14 +216,14 @@ mod tests { let generator = PawnMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); - let generated_moves: HashSet = generator.iter().collect(); - let expected_moves: HashSet = HashSet::new(); + let generated_moves: HashSet<_> = generator.iter().collect(); + let expected_moves: HashSet<_> = HashSet::new(); assert_move_list!(generated_moves, expected_moves, pos); } #[test] - fn one_attack() { + fn one_attack() -> TestResult { let pos = test_position![ White Pawn on E4, White Bishop on E5, @@ -236,20 +232,19 @@ mod tests { let generator = PawnMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); - let expected_moves = - HashSet::from_iter( - [MoveBuilder::new(piece!(White Pawn), Square::E4, Square::D5) - .capturing(piece!(Black Knight on D5)) - .build()], - ); + let expected_moves = HashSet::from_iter([MoveBuilder::push(&piece!(White Pawn on E4)) + .capturing_on(Square::D5) + .build()?]); - let generated_moves: HashSet = generator.iter().collect(); + let generated_moves: HashSet<_> = generator.iter().collect(); - assert_eq!(generated_moves, expected_moves); + assert_move_list!(generated_moves, expected_moves, pos); + + Ok(()) } #[test] - fn one_double_attack() { + fn one_double_attack() -> TestResult { let pos = test_position![ White Pawn on E4, White Bishop on E5, @@ -259,21 +254,44 @@ mod tests { let generator = PawnMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + let builder = MoveBuilder::push(&piece!(White Pawn on E4)); let expected_moves = HashSet::from_iter([ - MoveBuilder::new(piece!(White Pawn), Square::E4, Square::D5) - .capturing(piece!(Black Knight on D5)) - .build(), - MoveBuilder::new(piece!(White Pawn), Square::E4, Square::F5) - .capturing(piece!(Black Queen on F5)) - .build(), + builder.clone().capturing_on(Square::D5).build()?, + builder.clone().capturing_on(Square::F5).build()?, ]); - let generated_moves: HashSet = generator.iter().collect(); + let generated_moves: HashSet<_> = generator.iter().collect(); assert_eq!( generated_moves, expected_moves, "generated: {:#?}\nexpected: {:#?}", generated_moves, expected_moves ); + + Ok(()) + } + + #[test] + fn one_en_passant_attack() -> TestResult { + let pos = test_position!(Black, [ + White Pawn on D4, + Black Pawn on E4, + ], D3); + + let generator = PawnMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + let generated_moves: HashSet = generator.iter().collect(); + + let builder = MoveBuilder::push(&piece!(Black Pawn on E4)); + let expected_moves = HashSet::from_iter([ + builder + .clone() + .capturing_en_passant_on(Square::D3) + .build()?, + builder.clone().to(Square::E3).build()?, + ]); + + assert_move_list!(generated_moves, expected_moves, pos); + + Ok(()) } } diff --git a/position/src/move_generator/tests/peterellisjones.rs b/position/src/move_generator/tests/peterellisjones.rs index 3828970..31a878a 100644 --- a/position/src/move_generator/tests/peterellisjones.rs +++ b/position/src/move_generator/tests/peterellisjones.rs @@ -6,15 +6,13 @@ //! [1]: https://peterellisjones.com //! [2]: https://peterellisjones.com/posts/generating-legal-chess-moves-efficiently/ -use crate::{ - assert_move_list, formatted_move_list, move_generator::Moves, r#move::AlgebraicMoveFormatter, - test_position, Move, MoveBuilder, -}; +use crate::{assert_move_list, formatted_move_list, test_position, testing::*}; use chessfriend_core::{piece, Square}; +use chessfriend_moves::Builder as MoveBuilder; use std::collections::HashSet; #[test] -fn pseudo_legal_move_generation() -> Result<(), String> { +fn pseudo_legal_move_generation() -> TestResult { let pos = test_position!(Black, [ Black King on E8, White King on E1, @@ -24,7 +22,7 @@ fn pseudo_legal_move_generation() -> Result<(), String> { let generated_moves = pos.moves(); let king_moves = generated_moves .moves_for_piece(&piece!(Black King on E8)) - .ok_or("No valid king moves")?; + .ok_or(TestError::NoLegalMoves)?; assert!(!king_moves.can_move_to_square(Square::F8)); assert!(!king_moves.can_move_to_square(Square::F7)); @@ -33,7 +31,7 @@ fn pseudo_legal_move_generation() -> Result<(), String> { } #[test] -fn gotcha_king_moves_away_from_a_checking_slider() -> Result<(), String> { +fn gotcha_king_moves_away_from_a_checking_slider() -> TestResult { let pos = test_position!(Black, [ Black King on E7, White King on E1, @@ -43,7 +41,7 @@ fn gotcha_king_moves_away_from_a_checking_slider() -> Result<(), String> { let generated_moves = pos.moves(); let king_moves = generated_moves .moves_for_piece(&piece!(Black King on E7)) - .ok_or("No valid king moves")?; + .ok_or(TestError::NoLegalMoves)?; assert!(!king_moves.can_move_to_square(Square::E8)); @@ -51,7 +49,7 @@ fn gotcha_king_moves_away_from_a_checking_slider() -> Result<(), String> { } #[test] -fn check_evasions_1() { +fn check_evasions_1() -> TestResult { let pos = test_position!(Black, [ Black King on E8, White King on E1, @@ -60,11 +58,12 @@ fn check_evasions_1() { let generated_moves = pos.moves(); + let builder = MoveBuilder::push(&piece!(Black King on E8)); let expected_moves = HashSet::from_iter([ - MoveBuilder::new(piece!(Black King), Square::E8, Square::D8).build(), - MoveBuilder::new(piece!(Black King), Square::E8, Square::E7).build(), - MoveBuilder::new(piece!(Black King), Square::E8, Square::F7).build(), - MoveBuilder::new(piece!(Black King), Square::E8, Square::F8).build(), + builder.clone().to(Square::D8).build()?, + builder.clone().to(Square::E7).build()?, + builder.clone().to(Square::F7).build()?, + builder.clone().to(Square::F8).build()?, ]); assert_move_list!( @@ -72,10 +71,12 @@ fn check_evasions_1() { expected_moves, pos ); + + Ok(()) } #[test] -fn check_evasions_double_check() { +fn check_evasions_double_check() -> TestResult { let pos = test_position!(Black, [ Black King on E8, Black Bishop on F6, @@ -86,11 +87,12 @@ fn check_evasions_double_check() { let generated_moves = pos.moves(); + let builder = MoveBuilder::push(&piece!(Black King on E8)); let expected_moves = HashSet::from_iter([ - MoveBuilder::new(piece!(Black King), Square::E8, Square::D8).build(), - MoveBuilder::new(piece!(Black King), Square::E8, Square::D7).build(), - MoveBuilder::new(piece!(Black King), Square::E8, Square::F7).build(), - MoveBuilder::new(piece!(Black King), Square::E8, Square::F8).build(), + builder.clone().to(Square::D8).build()?, + builder.clone().to(Square::D7).build()?, + builder.clone().to(Square::F7).build()?, + builder.clone().to(Square::F8).build()?, ]); assert_move_list!( @@ -98,10 +100,12 @@ fn check_evasions_double_check() { expected_moves, pos ); + + Ok(()) } #[test] -fn single_check_with_blocker() { +fn single_check_with_blocker() -> TestResult { let pos = test_position!(Black, [ Black King on E8, Black Knight on G6, @@ -111,15 +115,15 @@ fn single_check_with_blocker() { let generated_moves = pos.moves(); + let king_builder = MoveBuilder::push(&piece!(Black King on E8)); + let knight_builder = MoveBuilder::push(&piece!(Black Knight on G6)); let expected_moves = HashSet::from_iter([ - MoveBuilder::new(piece!(Black King), Square::E8, Square::D8).build(), - MoveBuilder::new(piece!(Black King), Square::E8, Square::D7).build(), - MoveBuilder::new(piece!(Black King), Square::E8, Square::F7).build(), - MoveBuilder::new(piece!(Black King), Square::E8, Square::F8).build(), - MoveBuilder::new(piece!(Black Knight), Square::G6, Square::E7).build(), - MoveBuilder::new(piece!(Black Knight), Square::G6, Square::E5) - .capturing(piece!(White Rook on E5)) - .build(), + king_builder.clone().to(Square::D8).build()?, + king_builder.clone().to(Square::D7).build()?, + king_builder.clone().to(Square::F7).build()?, + king_builder.clone().to(Square::F8).build()?, + knight_builder.clone().to(Square::E7).build()?, + knight_builder.clone().capturing_on(Square::E5).build()?, ]); assert_move_list!( @@ -127,10 +131,12 @@ fn single_check_with_blocker() { expected_moves, pos ); + + Ok(()) } #[test] -fn en_passant_check_capture() { +fn en_passant_check_capture() -> TestResult { let pos = test_position!(Black, [ Black King on C5, Black Pawn on E4, @@ -143,17 +149,19 @@ fn en_passant_check_capture() { assert!( generated_moves.contains( - &MoveBuilder::new(piece!(Black Pawn), Square::E4, Square::D3) - .capturing_en_passant(piece!(White Pawn on D4)) - .build() + &MoveBuilder::push(&piece!(Black Pawn on E4)) + .capturing_en_passant_on(Square::D4) + .build()? ), "Valid moves: {:?}", formatted_move_list!(generated_moves, pos) ); + + Ok(()) } #[test] -fn en_passant_check_block() { +fn en_passant_check_block() -> TestResult { let pos = test_position!(Black, [ Black King on B5, Black Pawn on E4, @@ -167,13 +175,15 @@ fn en_passant_check_block() { assert!( generated_moves.contains( - &MoveBuilder::new(piece!(Black Pawn), Square::E4, Square::D3) - .capturing_en_passant(piece!(White Pawn on D4)) - .build() + &MoveBuilder::push(&piece!(Black Pawn on E4)) + .capturing_en_passant_on(Square::D4) + .build()? ), "Valid moves: {:?}", formatted_move_list!(generated_moves, pos) ); + + Ok(()) } #[test] @@ -204,7 +214,7 @@ fn pinned_pieces_rook_cannot_move_out_of_pin() -> Result<(), String> { } #[test] -fn en_passant_discovered_check() { +fn en_passant_discovered_check() -> TestResult { let pos = test_position!(Black, [ Black King on A4, Black Pawn on E4, @@ -214,13 +224,15 @@ fn en_passant_discovered_check() { let generated_moves = pos.moves().iter().collect::>(); - let unexpected_move = MoveBuilder::new(piece!(Black Pawn), Square::E4, Square::D3) - .capturing_en_passant(piece!(White Pawn on D4)) - .build(); + let unexpected_move = MoveBuilder::push(&piece!(Black Pawn on E4)) + .capturing_en_passant_on(Square::D4) + .build()?; assert!( !generated_moves.contains(&unexpected_move), "Valid moves: {:?}", formatted_move_list!(generated_moves, pos) ); + + Ok(()) } diff --git a/position/src/move_generator/tests/single_pieces.rs b/position/src/move_generator/tests/single_pieces.rs index 01a3e34..f8d06fb 100644 --- a/position/src/move_generator/tests/single_pieces.rs +++ b/position/src/move_generator/tests/single_pieces.rs @@ -1,36 +1,32 @@ // Eryn Wells -use crate::{position, r#move::AlgebraicMoveFormatter, Move, MoveBuilder}; +use crate::{assert_move_list, test_position, testing::*}; use chessfriend_core::{piece, Square}; +use chessfriend_moves::Builder as MoveBuilder; use std::collections::HashSet; #[test] -fn one_king() { - let pos = position![ +fn one_king() -> TestResult { + let pos = test_position![ White King on D3, Black King on H6, ]; + let builder = MoveBuilder::push(&piece!(White King on D3)); let expected_moves = HashSet::from_iter([ - MoveBuilder::new(piece!(White King), Square::D3, Square::D4).build(), - MoveBuilder::new(piece!(White King), Square::D3, Square::E4).build(), - MoveBuilder::new(piece!(White King), Square::D3, Square::E3).build(), - MoveBuilder::new(piece!(White King), Square::D3, Square::E2).build(), - MoveBuilder::new(piece!(White King), Square::D3, Square::D2).build(), - MoveBuilder::new(piece!(White King), Square::D3, Square::C2).build(), - MoveBuilder::new(piece!(White King), Square::D3, Square::C3).build(), - MoveBuilder::new(piece!(White King), Square::D3, Square::C4).build(), + builder.clone().to(Square::D4).build()?, + builder.clone().to(Square::E4).build()?, + builder.clone().to(Square::E3).build()?, + builder.clone().to(Square::E2).build()?, + builder.clone().to(Square::D2).build()?, + builder.clone().to(Square::C2).build()?, + builder.clone().to(Square::C3).build()?, + builder.clone().to(Square::C4).build()?, ]); - let generated_moves: HashSet = pos.moves().iter().collect(); + let generated_moves: HashSet<_> = pos.moves().iter().collect(); - assert_eq!( - generated_moves, - expected_moves, - "{:?}", - generated_moves - .symmetric_difference(&expected_moves) - .map(|m| format!("{}", AlgebraicMoveFormatter::new(&m, &pos))) - .collect::>() - ); + assert_move_list!(generated_moves, expected_moves, pos); + + Ok(()) } diff --git a/position/src/position/builders/mod.rs b/position/src/position/builders/mod.rs index 9324249..ea1f60d 100644 --- a/position/src/position/builders/mod.rs +++ b/position/src/position/builders/mod.rs @@ -3,5 +3,5 @@ mod move_builder; mod position_builder; -pub use move_builder::Builder as MoveBuilder; +pub use move_builder::{Builder as MoveBuilder, MakeMoveError}; pub use position_builder::Builder as PositionBuilder; diff --git a/position/src/position/builders/move_builder.rs b/position/src/position/builders/move_builder.rs index cb7f0f7..900c322 100644 --- a/position/src/position/builders/move_builder.rs +++ b/position/src/position/builders/move_builder.rs @@ -1,8 +1,19 @@ // Eryn Wells -use crate::{position::flags::Flags, r#move::Castle, MakeMoveError, Move, Position}; +use crate::{position::flags::Flags, Position}; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Direction, Piece, PlacedPiece, Shape, Square}; +use chessfriend_moves::{Castle, Move}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum MakeMoveError { + PlayerOutOfTurn, + NoPiece, + NoCapturedPiece, + NoLegalMoves, + IllegalCastle, + IllegalSquare(Square), +} /// A position builder that builds a new position by making a move. #[derive(Clone)] @@ -51,14 +62,14 @@ where M: MoveToMake, { pub fn make(self, mv: &Move) -> Result, MakeMoveError> { - let from_square = mv.from_square(); + let origin_square = mv.origin_square(); let piece = self .position - .piece_on_square(from_square) + .piece_on_square(origin_square) .ok_or(MakeMoveError::NoPiece)?; - let to_square = mv.to_square(); + let target_square = mv.target_square(); let moves = self .position @@ -72,8 +83,8 @@ where } } None => { - if !moves.can_move_to_square(to_square) { - return Err(MakeMoveError::IllegalSquare(to_square)); + if !moves.can_move_to_square(target_square) { + return Err(MakeMoveError::IllegalSquare(target_square)); } } } @@ -83,8 +94,8 @@ where let captured_piece = if mv.is_en_passant() { // En passant captures the pawn directly ahead (in the player's direction) of the en passant square. let capture_square = match player { - Color::White => to_square.neighbor(Direction::South), - Color::Black => to_square.neighbor(Direction::North), + Color::White => target_square.neighbor(Direction::South), + Color::Black => target_square.neighbor(Direction::North), } .ok_or(MakeMoveError::NoCapturedPiece)?; @@ -96,7 +107,7 @@ where } else if mv.is_capture() { Some( self.position - .piece_on_square(to_square) + .piece_on_square(target_square) .ok_or(MakeMoveError::NoCapturedPiece)?, ) } else { @@ -140,8 +151,8 @@ where } else { let en_passant_square: Option = if mv.is_double_push() { match piece.color() { - Color::White => to_square.neighbor(Direction::South), - Color::Black => to_square.neighbor(Direction::North), + Color::White => target_square.neighbor(Direction::South), + Color::Black => target_square.neighbor(Direction::North), } } else { None @@ -150,8 +161,8 @@ where Ok(Builder { position: self.position, move_to_make: ValidatedMove::RegularMove { - from_square, - to_square, + from_square: origin_square, + to_square: target_square, moving_piece: piece, captured_piece, promotion: mv.promotion(), @@ -219,18 +230,19 @@ impl<'p> Builder<'p, ValidatedMove> { } => { let mut pieces = self.position.piece_bitboards().clone(); - let target_squares = castle.target_squares(player); + let parameters = castle.parameters(player); - let king_from: BitBoard = king.square().into(); - let king_to: BitBoard = target_squares.king.into(); - *pieces.bitboard_for_piece_mut(king.piece()) ^= king_from | king_to; + let king_origin_square: BitBoard = king.square().into(); + let king_target_square: BitBoard = parameters.king_target_square().into(); + *pieces.bitboard_for_piece_mut(king.piece()) ^= + king_origin_square | king_target_square; let rook_from: BitBoard = rook.square().into(); - let rook_to: BitBoard = target_squares.rook.into(); + let rook_to: BitBoard = parameters.rook_target_square().into(); *pieces.bitboard_for_piece_mut(rook.piece()) ^= rook_from | rook_to; *pieces.bitboard_for_color_mut(player) &= - !(king_from | rook_from) | (king_to | rook_to); + !(king_origin_square | rook_from) | (king_target_square | rook_to); Position::new( player.other(), @@ -257,15 +269,24 @@ impl<'p> From<&'p Position> for Builder<'p, NoMove> { #[cfg(test)] mod tests { use super::*; - use crate::{position, r#move::Castle, MoveBuilder, PositionBuilder}; + use crate::testing::*; + use crate::{position, PositionBuilder}; use chessfriend_core::piece; + use chessfriend_moves::Builder as MoveBuilder; #[test] - fn move_white_pawn_one_square() -> Result<(), MakeMoveError> { + fn move_white_pawn_one_square() -> TestResult { let pos = position![White Pawn on E2]; - let mv = MoveBuilder::new(piece!(White Pawn), Square::E2, Square::E3).build(); + let mv = MoveBuilder::new() + .from(Square::E2) + .to(Square::E3) + .build() + .map_err(TestError::BuildMove)?; - let new_position = Builder::::new(&pos).make(&mv)?.build(); + let new_position = Builder::::new(&pos) + .make(&mv) + .map_err(|err| TestError::MakeMove(err))? + .build(); println!("{}", &new_position); assert_eq!( @@ -277,9 +298,13 @@ mod tests { } #[test] - fn move_white_pawn_two_squares() -> Result<(), MakeMoveError> { + fn move_white_pawn_two_squares() -> TestResult { let pos = position![White Pawn on E2]; - let mv = MoveBuilder::new(piece!(White Pawn), Square::E2, Square::E4).build(); + let mv = MoveBuilder::new() + .from(Square::E2) + .to(Square::E4) + .build() + .map_err(TestError::BuildMove)?; let new_position = Builder::::new(&pos).make(&mv)?.build(); println!("{}", &new_position); @@ -308,9 +333,7 @@ mod tests { ]; println!("{}", &pos); - let mv = MoveBuilder::new(piece!(White King), Square::E1, Square::G1) - .castle(Castle::KingSide) - .build(); + let mv = MoveBuilder::castling(Castle::KingSide).build(); let new_position = Builder::::new(&pos).make(&mv)?.build(); println!("{}", &new_position); @@ -328,7 +351,7 @@ mod tests { } #[test] - fn en_passant_capture() -> Result<(), MakeMoveError> { + fn en_passant_capture() -> TestResult { let pos = PositionBuilder::new() .place_piece(piece!(White Pawn on B5)) .place_piece(piece!(Black Pawn on A7)) @@ -336,7 +359,8 @@ mod tests { .build(); println!("{pos}"); - let black_pawn_move = MoveBuilder::new(piece!(Black Pawn), Square::A7, Square::A5).build(); + let black_pawn_move = MoveBuilder::new().from(Square::A7).to(Square::A5).build()?; + assert!(black_pawn_move.is_double_push()); assert!(!black_pawn_move.is_en_passant()); @@ -352,9 +376,9 @@ mod tests { Some(piece!(White Pawn on B5)) ); - let white_pawn_capture = MoveBuilder::new(piece!(White Pawn), Square::B5, Square::A6) - .capturing_en_passant(piece!(Black Pawn on A5)) - .build(); + let white_pawn_capture = MoveBuilder::push(&piece!(White Pawn on B5)) + .capturing_en_passant_on(Square::A5) + .build()?; let en_passant_capture = Builder::::new(&en_passant_position) .make(&white_pawn_capture)? .build(); diff --git a/position/src/position/builders/position_builder.rs b/position/src/position/builders/position_builder.rs index 28652ed..a4f8d2f 100644 --- a/position/src/position/builders/position_builder.rs +++ b/position/src/position/builders/position_builder.rs @@ -120,13 +120,13 @@ impl Builder { for color in Color::ALL { for castle in Castle::ALL { - let starting_squares = castle.starting_squares(color); + let parameters = castle.parameters(color); let has_rook_on_starting_square = self .pieces - .get(&starting_squares.rook) + .get(¶meters.rook_origin_square()) .is_some_and(|piece| piece.shape() == Shape::Rook); let king_is_on_starting_square = - self.kings[color as usize] == Some(starting_squares.king); + self.kings[color as usize] == Some(parameters.king_origin_square()); if !king_is_on_starting_square || !has_rook_on_starting_square { flags.clear_player_has_right_to_castle_flag(color, castle); diff --git a/position/src/position/flags.rs b/position/src/position/flags.rs index 7c722a9..19d3165 100644 --- a/position/src/position/flags.rs +++ b/position/src/position/flags.rs @@ -45,8 +45,6 @@ impl Default for Flags { #[cfg(test)] mod tests { use super::*; - use crate::r#move::Castle; - use chessfriend_core::Color; #[test] fn castle_flags() { diff --git a/position/src/position/mod.rs b/position/src/position/mod.rs index 8ece091..644ccc1 100644 --- a/position/src/position/mod.rs +++ b/position/src/position/mod.rs @@ -9,7 +9,7 @@ mod pieces; mod position; pub use { - builders::{MoveBuilder, PositionBuilder}, + builders::{MakeMoveError, MoveBuilder, PositionBuilder}, diagram_formatter::DiagramFormatter, pieces::Pieces, position::Position, diff --git a/position/src/position/position.rs b/position/src/position/position.rs index bbdcb9e..d336703 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -372,9 +372,10 @@ impl fmt::Display for Position { #[cfg(test)] mod tests { - use crate::{position, test_position, Castle, Position, PositionBuilder}; + use super::*; + use crate::{position, test_position, Position, PositionBuilder}; use chessfriend_bitboard::bitboard; - use chessfriend_core::{piece, Color, Square}; + use chessfriend_core::piece; #[test] fn piece_on_square() { diff --git a/position/src/testing.rs b/position/src/testing.rs index ba4bd69..1693110 100644 --- a/position/src/testing.rs +++ b/position/src/testing.rs @@ -1,7 +1,7 @@ // Eryn Wells use crate::MakeMoveError; -use chessfriend_moves::{BuildMoveError, BuildMoveResult, Move}; +use chessfriend_moves::BuildMoveError; #[macro_export] macro_rules! assert_move_list { From f1cd36952b5f2f5d3043ececbebcd29e76fc1890 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 25 Feb 2024 09:52:49 -0800 Subject: [PATCH 180/423] Fix build errors in explorer --- explorer/Cargo.toml | 1 + explorer/src/main.rs | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/explorer/Cargo.toml b/explorer/Cargo.toml index 3281cf4..9689332 100644 --- a/explorer/Cargo.toml +++ b/explorer/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] chessfriend_core = { path = "../core" } +chessfriend_moves = { path = "../moves" } chessfriend_position = { path = "../position" } clap = { version = "4.4.12", features = ["derive"] } rustyline = "13.0.0" diff --git a/explorer/src/main.rs b/explorer/src/main.rs index 8edb00e..5b8a17d 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -1,5 +1,8 @@ +// Eryn Wells + use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; -use chessfriend_position::{fen::ToFen, MakeMoveBuilder, MoveBuilder, Position, PositionBuilder}; +use chessfriend_moves::Builder as MoveBuilder; +use chessfriend_position::{fen::ToFen, MakeMoveBuilder, Position, PositionBuilder}; use clap::{Arg, Command}; use rustyline::error::ReadlineError; use rustyline::DefaultEditor; @@ -97,16 +100,15 @@ fn respond(line: &str, state: &mut State) -> Result { ) .map_err(|_| "Error: invalid square specifier")?; - let mv = MoveBuilder::new( - Piece::new(state.position.player_to_move(), shape), - from_square, - to_square, - ) - .build(); + let mv = MoveBuilder::new() + .from(from_square) + .to(to_square) + .build() + .map_err(|err| format!("Error: cannot build move: {:?}", err))?; state.position = MakeMoveBuilder::new(&state.position) .make(&mv) - .map_err(|err| format!("error: Cannot make move: {:?}", err))? + .map_err(|err| format!("Error: cannot make move: {:?}", err))? .build(); state.builder = PositionBuilder::from_position(&state.position); } From d77345901cdfff11bfaacbc540725e931cff745f Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 25 Feb 2024 09:52:59 -0800 Subject: [PATCH 181/423] Add chessfriend_moves to the workspace --- Cargo.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.lock b/Cargo.lock index 90cedc0..53be692 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,6 +172,7 @@ name = "explorer" version = "0.1.0" dependencies = [ "chessfriend_core", + "chessfriend_moves", "chessfriend_position", "clap", "rustyline", From 63c03fb879e774d74ac7591e26861c82d7238344 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 25 Feb 2024 09:54:03 -0800 Subject: [PATCH 182/423] Delete the position::r#move module --- position/src/move.rs | 602 ------------------------------------------- 1 file changed, 602 deletions(-) delete mode 100644 position/src/move.rs diff --git a/position/src/move.rs b/position/src/move.rs deleted file mode 100644 index 32bc399..0000000 --- a/position/src/move.rs +++ /dev/null @@ -1,602 +0,0 @@ -// Eryn Wells - -use chessfriend_core::{Piece, PlacedPiece, Rank, Shape, Square}; -use std::fmt; - -pub use castle::Castle; -pub(crate) use move_formatter::AlgebraicMoveFormatter; - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum MakeMoveError { - PlayerOutOfTurn, - NoPiece, - NoCapturedPiece, - NoLegalMoves, - IllegalCastle, - IllegalSquare(Square), -} - -mod castle { - use chessfriend_bitboard::BitBoard; - use chessfriend_core::{Color, Square}; - - #[repr(u16)] - #[derive(Copy, Clone, Debug, Eq, PartialEq)] - pub enum Castle { - KingSide = 0b10, - QueenSide = 0b11, - } - - pub(crate) struct CastlingParameters { - clear_squares: BitBoard, - check_squares: BitBoard, - } - - #[derive(Debug)] - pub(crate) struct Squares { - pub king: Square, - pub rook: Square, - } - - impl Castle { - pub const ALL: [Castle; 2] = [Castle::KingSide, Castle::QueenSide]; - - const STARTING_SQUARES: [[Squares; 2]; 2] = [ - [ - Squares { - king: Square::E1, - rook: Square::H1, - }, - Squares { - king: Square::E1, - rook: Square::A1, - }, - ], - [ - Squares { - king: Square::E8, - rook: Square::H8, - }, - Squares { - king: Square::E8, - rook: Square::A8, - }, - ], - ]; - - const TARGET_SQUARES: [[Squares; 2]; 2] = [ - [ - Squares { - king: Square::G1, - rook: Square::F1, - }, - Squares { - king: Square::C1, - rook: Square::D1, - }, - ], - [ - Squares { - king: Square::G8, - rook: Square::F8, - }, - Squares { - king: Square::C8, - rook: Square::D8, - }, - ], - ]; - - pub(crate) fn starting_squares(&self, color: Color) -> &'static Squares { - &Castle::STARTING_SQUARES[color as usize][self.into_index()] - } - - pub(crate) fn target_squares(&self, color: Color) -> &'static Squares { - &Castle::TARGET_SQUARES[color as usize][self.into_index()] - } - - pub(crate) fn into_index(&self) -> usize { - match self { - Castle::KingSide => 0, - Castle::QueenSide => 1, - } - } - - pub(crate) fn parameters(&self) -> CastlingParameters { - match self { - Castle::KingSide => CastlingParameters { - clear_squares: BitBoard::new(0b01100000), - check_squares: BitBoard::new(0b01110000), - }, - Castle::QueenSide => CastlingParameters { - clear_squares: BitBoard::new(0b00001110), - check_squares: BitBoard::new(0b00011100), - }, - } - } - } - - impl CastlingParameters { - pub fn clear_squares(&self) -> &BitBoard { - &self.clear_squares - } - - pub fn check_squares(&self) -> &BitBoard { - &self.check_squares - } - } -} - -#[repr(u16)] -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum PromotableShape { - Knight = 0b00, - Bishop = 0b01, - Rook = 0b10, - Queen = 0b11, -} - -impl TryFrom for PromotableShape { - type Error = (); - - fn try_from(value: Shape) -> Result { - match value { - Shape::Knight => Ok(PromotableShape::Knight), - Shape::Bishop => Ok(PromotableShape::Bishop), - Shape::Rook => Ok(PromotableShape::Rook), - Shape::Queen => Ok(PromotableShape::Queen), - _ => Err(()), - } - } -} - -#[repr(u16)] -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -enum Kind { - Quiet = 0b00, - DoublePush = 0b01, - Castle(Castle), - Capture(PlacedPiece) = 0b0100, - EnPassantCapture(PlacedPiece) = 0b0101, - Promotion(PromotableShape) = 0b1000, - CapturePromotion(PlacedPiece, PromotableShape) = 0b1100, -} - -impl Kind { - fn bits(&self) -> u16 { - match self { - Self::Promotion(shape) => self.discriminant() | *shape as u16, - Self::CapturePromotion(_, shape) => self.discriminant() | *shape as u16, - Self::Castle(castle) => *castle as u16, - _ => self.discriminant(), - } - } - - /// Return the discriminant value. This implementation is copied from the Rust docs. - /// See https://doc.rust-lang.org/std/mem/fn.discriminant.html - fn discriminant(&self) -> u16 { - unsafe { *<*const _>::from(self).cast::() } - } -} - -impl Default for Kind { - fn default() -> Self { - Self::Quiet - } -} - -/// A single player's move. In chess parlance, this is a "ply". -#[derive(Clone, Copy, Eq, Hash, PartialEq)] -pub struct Move(u16); - -impl Move { - pub fn from_square(&self) -> Square { - ((self.0 >> 4) & 0b111111).try_into().unwrap() - } - - pub fn to_square(&self) -> Square { - (self.0 >> 10).try_into().unwrap() - } - - pub fn is_quiet(&self) -> bool { - self.flags() == Kind::Quiet.discriminant() - } - - pub fn is_double_push(&self) -> bool { - self.flags() == Kind::DoublePush.discriminant() - } - - pub fn is_castle(&self) -> bool { - self.castle().is_some() - } - - pub fn castle(&self) -> Option { - match self.flags() { - 0b0010 => Some(Castle::KingSide), - 0b0011 => Some(Castle::QueenSide), - _ => None, - } - } - - pub fn is_capture(&self) -> bool { - (self.0 & 0b0100) != 0 - } - - pub fn is_en_passant(&self) -> bool { - self.flags() == 0b0101 - } - - pub fn is_promotion(&self) -> bool { - (self.0 & 0b1000) != 0 - } - - pub fn promotion(&self) -> Option { - if !self.is_promotion() { - return None; - } - - Some(match self.special() { - 0b00 => Shape::Knight, - 0b01 => Shape::Bishop, - 0b10 => Shape::Rook, - 0b11 => Shape::Queen, - _ => unreachable!(), - }) - } - - #[inline] - fn flags(&self) -> u16 { - self.0 & 0b1111 - } - - #[inline] - fn special(&self) -> u16 { - self.0 & 0b11 - } -} - -impl fmt::Debug for Move { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("Move") - .field(&format_args!("{:08b}", &self.0)) - .finish() - } -} - -#[derive(Clone, Debug)] -pub struct MoveBuilder { - piece: Piece, - from: Square, - to: Square, - kind: Kind, -} - -impl MoveBuilder { - pub fn new(piece: Piece, from: Square, to: Square) -> Self { - let kind = match piece.shape() { - Shape::Pawn => { - let from_rank = from.rank(); - let to_rank = to.rank(); - let is_white_double_push = from_rank == Rank::TWO && to_rank == Rank::FOUR; - let is_black_double_push = from_rank == Rank::SEVEN && to_rank == Rank::FIVE; - if is_white_double_push || is_black_double_push { - Kind::DoublePush - } else { - Kind::Quiet - } - } - _ => Kind::Quiet, - }; - - Self { - piece, - from, - to, - kind, - } - } - - pub fn castle(mut self, castle: Castle) -> Self { - self.kind = Kind::Castle(castle); - self - } - - pub fn capturing(mut self, captured_piece: PlacedPiece) -> Self { - self.kind = match self.kind { - Kind::Promotion(shape) => Kind::CapturePromotion(captured_piece, shape), - _ => Kind::Capture(captured_piece), - }; - self - } - - pub fn capturing_en_passant(mut self, captured_piece: PlacedPiece) -> Self { - self.kind = Kind::EnPassantCapture(captured_piece); - self - } - - pub fn promoting_to(mut self, shape: Shape) -> Self { - if let Some(shape) = PromotableShape::try_from(shape).ok() { - self.kind = match self.kind { - Kind::Capture(piece) => Kind::CapturePromotion(piece, shape), - Kind::CapturePromotion(piece, _) => Kind::CapturePromotion(piece, shape), - _ => Kind::Promotion(shape), - }; - } - self - } - - pub fn build(&self) -> Move { - Move( - self.kind.bits() - | ((self.from as u16 & 0b111111) << 4) - | ((self.to as u16 & 0b111111) << 10), - ) - } -} - -mod move_formatter { - use super::{Castle, Move}; - use crate::Position; - use chessfriend_core::Shape; - use std::fmt; - - enum Style { - Short, - Long, - } - - impl Default for Style { - fn default() -> Self { - Style::Long - } - } - - pub(crate) struct AlgebraicMoveFormatter<'m, 'pos> { - position: &'pos Position, - r#move: &'m Move, - style: Style, - } - - impl<'pos, 'm> AlgebraicMoveFormatter<'m, 'pos> { - pub(crate) fn new( - mv: &'m Move, - position: &'pos Position, - ) -> AlgebraicMoveFormatter<'m, 'pos> { - AlgebraicMoveFormatter { - position, - r#move: mv, - style: Style::default(), - } - } - - fn style(mut self, style: Style) -> Self { - self.style = style; - self - } - - fn fmt_kingside_castle(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "0-0") - } - - fn fmt_queenside_castle(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "0-0-0") - } - - fn fmt_short(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - unimplemented!() - } - - fn fmt_long(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // TODO: Figure out how to write the short algebraic form, where a - // disambiguating coordiate is specified when two of the same piece - // cam move to the same square. - - // TODO: Write better pawn moves. - - let mv = self.r#move; - let from_square = mv.from_square(); - let to_square = mv.to_square(); - - let piece = self - .position - .piece_on_square(from_square) - .expect(&format!("No piece on {}", from_square)); - if piece.shape() != Shape::Pawn { - write!(f, "{}", piece.shape())?; - } - - write!( - f, - "{}{}{}", - from_square, - if mv.is_capture() { 'x' } else { '-' }, - to_square, - )?; - - if let Some(promotion) = mv.promotion() { - write!(f, "={}", promotion)?; - } - - // TODO: Write check (+) and checkmate (#) symbols - - Ok(()) - } - } - - impl<'pos, 'mv> fmt::Display for AlgebraicMoveFormatter<'mv, 'pos> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mv = self.r#move; - match mv.castle() { - Some(Castle::KingSide) => return self.fmt_kingside_castle(f), - Some(Castle::QueenSide) => return self.fmt_queenside_castle(f), - _ => {} - } - - match self.style { - Style::Short => self.fmt_short(f), - Style::Long => self.fmt_long(f), - } - } - } - - #[cfg(test)] - mod tests { - use super::{AlgebraicMoveFormatter, Style}; - use crate::position; - use chessfriend_core::piece; - - macro_rules! chess_move { - ($color:ident $shape:ident $from_square:ident - $to_square:ident) => { - $crate::MoveBuilder::new( - chessfriend_core::Piece::new( - chessfriend_core::Color::$color, - chessfriend_core::Shape::$shape, - ), - chessfriend_core::Square::$from_square, - chessfriend_core::Square::$to_square, - ) - .build() - }; - ($color:ident $shape:ident $from_square:ident x $to_square:ident, $captured_color:ident $captured_shape:ident) => { - $crate::MoveBuilder::new( - chessfriend_core::Piece::new( - chessfriend_core::Color::$color, - chessfriend_core::Shape::$shape, - ), - chessfriend_core::Square::$from_square, - chessfriend_core::Square::$to_square, - ) - .capturing(chessfriend_core::PlacedPiece::new( - chessfriend_core::Piece::new( - chessfriend_core::Color::$captured_color, - chessfriend_core::Shape::$captured_shape, - ), - chessfriend_core::Square::$to_square, - )) - .build() - }; - } - - macro_rules! test_algebraic_formatter { - ($test_name:ident, $style:ident, $color:ident $shape:ident $from_square:ident x $to_square:ident, $captured_color:ident $captured_shape:ident, $output:expr) => { - #[test] - fn $test_name() { - let pos = position![ - $color $shape on $from_square, - $captured_color $captured_shape on $to_square, - ]; - let mv = chess_move!( - $color $shape $from_square x $to_square, - $captured_color $captured_shape - ); - - println!("{:?}", &mv); - - let formatter = AlgebraicMoveFormatter::new(&mv, &pos).style(Style::$style); - assert_eq!(format!("{}", formatter), $output); - } - }; - ($test_name:ident, $style:ident, $color:ident $shape:ident $from_square:ident - $to_square:ident, $output:expr) => { - #[test] - fn $test_name() { - let pos = position![ - $color $shape on $from_square, - ]; - - let mv = chess_move!($color $shape $from_square-$to_square); - println!("{:?}", &mv); - - let formatter = AlgebraicMoveFormatter::new(&mv, &pos).style(Style::$style); - assert_eq!(format!("{}", formatter), $output); - } - }; - } - - test_algebraic_formatter!(long_pawn_move, Long, White Pawn E4 - E5, "e4-e5"); - test_algebraic_formatter!(long_bishop_move, Long, White Bishop A4 - D7, "Ba4-d7"); - test_algebraic_formatter!(long_bishop_capture, Long, White Bishop A2 x E6, Black Knight, "Ba2xe6"); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use chessfriend_core::piece; - - macro_rules! assert_flag { - ($move:expr, $left:expr, $right:expr, $desc:expr) => { - assert_eq!($left, $right, "{:?} -> {}", $move, stringify!($desc)) - }; - } - - macro_rules! assert_flags { - ($move:expr, $quiet:expr, $double_push:expr, $en_passant:expr, $capture:expr, $castle:expr, $promotion:expr) => { - assert_flag!($move, $move.is_quiet(), $quiet, "is_quiet"); - assert_flag!( - $move, - $move.is_double_push(), - $double_push, - "is_double_push" - ); - assert_flag!($move, $move.is_en_passant(), $en_passant, "is_en_passant"); - assert_flag!($move, $move.is_capture(), $capture, "is_capture"); - assert_flag!($move, $move.is_castle(), $castle, "is_castle"); - assert_flag!($move, $move.is_promotion(), $promotion, "is_promotion"); - }; - } - - #[test] - fn move_flags_quiet() { - let mv = MoveBuilder::new(piece!(White Pawn), Square::A4, Square::A5).build(); - assert_flags!(mv, true, false, false, false, false, false); - } - - #[test] - fn move_flags_double_push() { - let mv = MoveBuilder::new(piece!(White Pawn), Square::C2, Square::C4).build(); - assert_flags!(mv, false, true, false, false, false, false); - } - - #[test] - fn move_flags_capture() { - let mv = MoveBuilder::new(piece!(White Pawn), Square::A4, Square::B5) - .capturing(piece!(Black Bishop on B5)) - .build(); - assert_flags!(mv, false, false, false, true, false, false); - } - - #[test] - fn move_flags_en_passant_capture() { - let mv = MoveBuilder::new(piece!(White Pawn), Square::A5, Square::B6) - .capturing_en_passant(piece!(Black Pawn on B5)) - .build(); - assert_flags!(mv, false, false, true, true, false, false); - } - - #[test] - fn move_flags_promotion() { - let mv = MoveBuilder::new(piece!(White Pawn), Square::H7, Square::H8) - .promoting_to(Shape::Queen) - .build(); - assert_flags!(mv, false, false, false, false, false, true); - assert_eq!(mv.promotion(), Some(Shape::Queen)); - } - - #[test] - fn move_flags_capture_promotion() { - let mv = MoveBuilder::new(piece!(White Pawn), Square::H7, Square::G8) - .capturing(piece!(Black Knight on G8)) - .promoting_to(Shape::Queen) - .build(); - assert_flags!(mv, false, false, false, true, false, true); - assert_eq!(mv.promotion(), Some(Shape::Queen)); - } - - #[test] - fn move_flags_castle() { - let mv = MoveBuilder::new(piece!(Black King), Square::E8, Square::G8) - .castle(Castle::KingSide) - .build(); - assert_flags!(mv, false, false, false, false, true, false); - } -} From 2a6b098cb8fd3fd3e21a75f409c8590983586469 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 25 Feb 2024 10:51:27 -0800 Subject: [PATCH 183/423] Fix the pawn unit tests --- core/src/coordinates.rs | 11 +++ moves/src/en_passant.rs | 4 +- position/src/move_generator/move_set.rs | 106 ++++++++++++++++++------ position/src/move_generator/pawn.rs | 36 ++++---- 4 files changed, 112 insertions(+), 45 deletions(-) diff --git a/core/src/coordinates.rs b/core/src/coordinates.rs index 68ff3fe..54aeb1a 100644 --- a/core/src/coordinates.rs +++ b/core/src/coordinates.rs @@ -1,5 +1,6 @@ // Eryn Wells +use crate::Color; use std::fmt; use std::str::FromStr; @@ -168,6 +169,16 @@ impl Rank { /// assert_eq!(Rank::PAWN_STARTING_RANKS[Color::Black as usize], Rank::SEVEN); /// ``` pub const PAWN_STARTING_RANKS: [Rank; 2] = [Rank::TWO, Rank::SEVEN]; + + pub const PAWN_DOUBLE_PUSH_TARGET_RANKS: [Rank; 2] = [Rank::FOUR, Rank::FIVE]; + + pub fn is_pawn_starting_rank(&self, color: Color) -> bool { + self == &Self::PAWN_STARTING_RANKS[color as usize] + } + + pub fn is_pawn_double_push_target_rank(&self, color: Color) -> bool { + self == &Self::PAWN_DOUBLE_PUSH_TARGET_RANKS[color as usize] + } } #[rustfmt::skip] diff --git a/moves/src/en_passant.rs b/moves/src/en_passant.rs index ac57835..49e88a6 100644 --- a/moves/src/en_passant.rs +++ b/moves/src/en_passant.rs @@ -3,7 +3,7 @@ use chessfriend_core::{Rank, Square}; /// En passant information. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct EnPassant { target: Square, capture: Square, @@ -39,10 +39,12 @@ impl EnPassant { } } + /// The square the capturing piece will move to. pub fn target_square(&self) -> Square { self.target } + /// The square on which the captured pawn sits. pub fn capture_square(&self) -> Square { self.capture } diff --git a/position/src/move_generator/move_set.rs b/position/src/move_generator/move_set.rs index f1042c9..5ba9467 100644 --- a/position/src/move_generator/move_set.rs +++ b/position/src/move_generator/move_set.rs @@ -2,7 +2,7 @@ use chessfriend_bitboard::BitBoard; use chessfriend_core::{PlacedPiece, Square}; -use chessfriend_moves::{Builder as MoveBuilder, Castle, Move}; +use chessfriend_moves::{Builder as MoveBuilder, Castle, EnPassant, Move}; /// A set of bitboards defining the moves for a single piece on the board. #[derive(Clone, Debug, Default, Eq, PartialEq)] @@ -11,12 +11,18 @@ struct BitBoardSet { captures: BitBoard, } +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) enum Special { + Pawn { en_passant: EnPassant }, + King { castles: u8 }, +} + /// A set of moves for a single piece on the board. #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct MoveSet { piece: PlacedPiece, bitboards: BitBoardSet, - special: u8, + special: Option, } impl MoveSet { @@ -24,7 +30,7 @@ impl MoveSet { MoveSet { piece, bitboards: BitBoardSet::default(), - special: 0, + special: None, } } @@ -33,9 +39,9 @@ impl MoveSet { } pub(crate) fn can_castle(&self, castle: Castle) -> bool { - match castle { - Castle::KingSide => (self.special & 0b1) != 0, - Castle::QueenSide => (self.special & 0b10) != 0, + match self.special { + Some(Special::King { castles }) => (castles & 1 << castle as u8) != 0, + _ => false, } } @@ -50,49 +56,99 @@ impl MoveSet { } pub(super) fn kingside_castle(&mut self) -> &mut MoveSet { - self.special |= 0b1; + match self.special { + Some(Special::King { ref mut castles }) => *castles |= 1 << Castle::KingSide as u8, + _ => { + self.special = Some(Special::King { + castles: 1 << Castle::KingSide as u8, + }) + } + } + self } pub(super) fn queenside_castle(&mut self) -> &mut MoveSet { - self.special |= 0b10; + match self.special { + Some(Special::King { ref mut castles }) => *castles |= 1 << Castle::QueenSide as u8, + _ => { + self.special = Some(Special::King { + castles: 1 << Castle::QueenSide as u8, + }) + } + } + self } - /// Return a BitBoard representing all possible moves. + pub(super) fn en_passant(&mut self, en_passant: EnPassant) -> &mut MoveSet { + self.special = Some(Special::Pawn { en_passant }); + self + } + + /// A `BitBoard` representing all possible moves. pub(super) fn bitboard(&self) -> BitBoard { self.bitboards.captures | self.bitboards.quiet } pub(crate) fn moves(&self) -> impl Iterator + '_ { let piece = &self.piece; + let color = piece.color(); + + let is_pawn_on_starting_rank = + piece.is_pawn() && piece.square().rank().is_pawn_starting_rank(color); self.bitboards .quiet .occupied_squares() - .filter_map(|to_square| MoveBuilder::push(&self.piece).to(to_square).build().ok()) + .filter_map(move |to_square| { + if is_pawn_on_starting_rank + && to_square.rank().is_pawn_double_push_target_rank(color) + { + MoveBuilder::double_push(piece.square().file(), color) + .build() + .ok() + } else { + MoveBuilder::push(piece).to(to_square).build().ok() + } + }) .chain( self.bitboards .captures .occupied_squares() - .filter_map(|to_square| MoveBuilder::push(piece).to(to_square).build().ok()), + .filter_map(|to_square| { + MoveBuilder::push(piece) + .capturing_on(to_square) + .build() + .ok() + }), ) - .chain( - if (self.special & 0b1) != 0 { - let mv = MoveBuilder::castling(Castle::KingSide).build(); - Some(mv) + .chain(self.castle_move(Castle::KingSide)) + .chain(self.castle_move(Castle::QueenSide)) + .chain(self.en_passant_move()) + } + + fn castle_move(&self, castle: Castle) -> Option { + match self.special { + Some(Special::King { castles }) => { + if (castles & 1 << castle as u8) != 0 { + Some(MoveBuilder::castling(castle).build()) } else { None } - .into_iter(), - ) - .chain( - if (self.special & 0b10) != 0 { - Some(MoveBuilder::castling(Castle::QueenSide).build()) - } else { - None - } - .into_iter(), - ) + } + _ => None, + } + } + + fn en_passant_move(&self) -> Option { + match self.special { + Some(Special::Pawn { en_passant }) => Some(unsafe { + MoveBuilder::push(&self.piece) + .capturing_en_passant_on(en_passant.target_square()) + .build_unchecked() + }), + _ => None, + } } } diff --git a/position/src/move_generator/pawn.rs b/position/src/move_generator/pawn.rs index 67fefb3..e5db4cf 100644 --- a/position/src/move_generator/pawn.rs +++ b/position/src/move_generator/pawn.rs @@ -4,7 +4,7 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::Position; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, PlacedPiece, Rank, Shape, Square}; -use chessfriend_moves::{Builder as MoveBuilder, Move}; +use chessfriend_moves::{EnPassant, Move}; use std::collections::BTreeMap; #[derive(Debug)] @@ -33,27 +33,33 @@ impl MoveGeneratorInternal for PawnMoveGenerator { let capture_moves = Self::attacks(position, &placed_piece) & capture_mask; let quiet_moves = Self::pushes(position, &placed_piece) & push_mask; - MoveSet::new(*placed_piece) + let mut move_set = MoveSet::new(*placed_piece) .quiet_moves(quiet_moves) - .capture_moves(capture_moves) + .capture_moves(capture_moves); + + if let Some(en_passant) = Self::en_passant(position, placed_piece) { + move_set.en_passant(en_passant); + } + + move_set } } impl PawnMoveGenerator { pub(super) fn new( position: &Position, - color: Color, + player_to_move: Color, capture_mask: BitBoard, push_mask: BitBoard, ) -> Self { let move_sets = if !capture_mask.is_empty() && !push_mask.is_empty() { - Self::move_sets(position, color, capture_mask, push_mask) + Self::move_sets(position, player_to_move, capture_mask, push_mask) } else { std::collections::BTreeMap::new() }; Self { - color, + color: player_to_move, move_sets, en_passant_captures: Vec::new(), } @@ -115,7 +121,7 @@ impl PawnMoveGenerator { BitBoard::pawn_attacks(piece.square(), color) & opponent_pieces } - fn en_passant_attack(position: &Position, piece: &PlacedPiece) -> Option { + fn en_passant(position: &Position, piece: &PlacedPiece) -> Option { match position.en_passant() { Some(en_passant) => { let target_square = en_passant.target_square(); @@ -128,12 +134,7 @@ impl PawnMoveGenerator { } match position.piece_on_square(en_passant.capture_square()) { - Some(_) => Some( - MoveBuilder::push(piece) - .capturing_en_passant_on(target_square) - .build() - .ok()?, - ), + Some(_) => Some(en_passant), None => None, } } @@ -164,7 +165,7 @@ mod tests { let generated_moves: HashSet<_> = generator.iter().collect(); - assert_eq!(generated_moves, expected_moves); + assert_move_list!(generated_moves, expected_moves, pos); Ok(()) } @@ -278,15 +279,12 @@ mod tests { Black Pawn on E4, ], D3); - let generator = PawnMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + let generator = PawnMoveGenerator::new(&pos, Color::Black, BitBoard::FULL, BitBoard::FULL); let generated_moves: HashSet = generator.iter().collect(); let builder = MoveBuilder::push(&piece!(Black Pawn on E4)); let expected_moves = HashSet::from_iter([ - builder - .clone() - .capturing_en_passant_on(Square::D3) - .build()?, + builder.capturing_en_passant_on(Square::D3).build()?, builder.clone().to(Square::E3).build()?, ]); From d2fe546824bf18a773386090ed472bc1751d9602 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 25 Feb 2024 10:52:10 -0800 Subject: [PATCH 184/423] Remove some unused variables from tests in the king move generator --- position/src/move_generator/king.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/position/src/move_generator/king.rs b/position/src/move_generator/king.rs index 45c928e..33b9860 100644 --- a/position/src/move_generator/king.rs +++ b/position/src/move_generator/king.rs @@ -167,7 +167,6 @@ mod tests { let generator = KingMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); let generated_moves: HashSet = generator.iter().collect(); - let king = piece!(White King); assert!(generated_moves.contains(&MoveBuilder::castling(Castle::KingSide).build())); assert!(generated_moves.contains(&MoveBuilder::castling(Castle::QueenSide).build())); } @@ -187,7 +186,6 @@ mod tests { let generator = KingMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); let generated_moves: HashSet = generator.iter().collect(); - let king = piece!(White King); assert!(generated_moves.contains(&MoveBuilder::castling(Castle::KingSide).build())); assert!(!generated_moves.contains(&MoveBuilder::castling(Castle::QueenSide).build())); } @@ -207,7 +205,6 @@ mod tests { let generator = KingMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); let generated_moves: HashSet = generator.iter().collect(); - let king = piece!(White King); assert!(!generated_moves.contains(&MoveBuilder::castling(Castle::KingSide).build())); assert!(generated_moves.contains(&MoveBuilder::castling(Castle::QueenSide).build())); } From 5f1fce6cc2d451d266662202a8de1a84cee610c7 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 25 Feb 2024 12:38:55 -0800 Subject: [PATCH 185/423] Fix the remaining tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Well… all the tests except the Peter Ellis Jones tests. Pass around whole EnPassant types instead of pulling out just the e.p. square. Make sure that Castling moves have their target and origin squares populated. Add a color field to the Castle move style to make this possible. --- moves/src/builder.rs | 41 ++++++++----- moves/tests/flags.rs | 2 +- position/src/fen.rs | 4 +- position/src/macros.rs | 3 +- position/src/move_generator/king.rs | 30 +++++++--- position/src/move_generator/move_set.rs | 44 ++++++++++++-- .../src/position/builders/move_builder.rs | 58 +++++++++---------- .../src/position/builders/position_builder.rs | 16 ++--- position/src/position/position.rs | 24 ++++---- position/src/sight.rs | 3 +- 10 files changed, 142 insertions(+), 83 deletions(-) diff --git a/moves/src/builder.rs b/moves/src/builder.rs index a1ab78e..5f02364 100644 --- a/moves/src/builder.rs +++ b/moves/src/builder.rs @@ -72,7 +72,7 @@ pub struct Capture { #[derive(Clone, Debug, Eq, PartialEq)] pub struct EnPassantCapture { push: Push, - capture: Option, + capture: Option, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -83,9 +83,16 @@ pub struct Promotion { #[derive(Clone, Debug, Eq, PartialEq)] pub struct Castle { + color: Color, castle: castle::Castle, } +impl EnPassantCapture { + fn _build_move_bits(&self, origin_square: Square, target_square: Square) -> u16 { + (origin_square as u16 & 0b111111) << 4 | (target_square as u16 & 0b111111) << 10 + } +} + impl Style for Null {} impl Style for Push { @@ -108,7 +115,17 @@ impl Style for Capture { } } -impl Style for Castle {} +impl Style for Castle { + fn origin_square(&self) -> Option { + let parameters = self.castle.parameters(self.color); + Some(parameters.king_origin_square()) + } + + fn target_square(&self) -> Option { + let parameters = self.castle.parameters(self.color); + Some(parameters.king_target_square()) + } +} impl Style for DoublePush { fn origin_square(&self) -> Option { @@ -143,12 +160,6 @@ impl Style for EnPassantCapture { } } -impl EnPassantCapture { - fn _build_move_bits(&self, origin_square: Square, target_square: Square) -> u16 { - (origin_square as u16 & 0b111111) << 4 | (target_square as u16 & 0b111111) << 10 - } -} - impl Style for Promotion { fn origin_square(&self) -> Option { self.style.from @@ -236,9 +247,9 @@ impl Builder { } } - pub fn castling(castle: castle::Castle) -> Builder { + pub fn castling(color: Color, castle: castle::Castle) -> Builder { Builder { - style: Castle { castle }, + style: Castle { color, castle }, } } @@ -283,8 +294,8 @@ impl Builder { } } - pub fn capturing_en_passant_on(&self, square: Square) -> Builder { - match EnPassant::from_target_square(square) { + pub fn capturing_en_passant_on(&self, target_square: Square) -> Builder { + match EnPassant::from_target_square(target_square) { Some(en_passant) => { let mut style = self.style.clone(); style.to = Some(en_passant.target_square()); @@ -292,7 +303,7 @@ impl Builder { Builder { style: EnPassantCapture { push: style, - capture: Some(en_passant.capture_square()), + capture: Some(en_passant), }, } } @@ -333,8 +344,8 @@ impl Builder { bits as u16 } - pub fn build(&self) -> Move { - Move(self.bits()) + pub fn build(&self) -> Result { + Ok(Move(self.bits() | self.style.into_move_bits()?)) } } diff --git a/moves/tests/flags.rs b/moves/tests/flags.rs index cdcdd57..6d001cb 100644 --- a/moves/tests/flags.rs +++ b/moves/tests/flags.rs @@ -101,7 +101,7 @@ fn move_flags_capture_promotion() -> TestResult { #[test] fn move_flags_castle() -> TestResult { - let mv = Builder::castling(Castle::KingSide).build(); + let mv = Builder::castling(Color::White, Castle::KingSide).build()?; assert_flags!(mv, false, false, false, false, true, false); diff --git a/position/src/fen.rs b/position/src/fen.rs index fab45e1..8859324 100644 --- a/position/src/fen.rs +++ b/position/src/fen.rs @@ -2,7 +2,7 @@ use crate::{Position, PositionBuilder}; use chessfriend_core::{piece, Color, File, Piece, PlacedPiece, Rank, Square}; -use chessfriend_moves::Castle; +use chessfriend_moves::{Castle, EnPassant}; use std::fmt::Write; macro_rules! fen { @@ -195,7 +195,7 @@ impl FromFen for Position { let en_passant_square = fields.next().ok_or(FromFenError)?; if en_passant_square != "-" { let square = Square::from_algebraic_str(en_passant_square).map_err(|_| FromFenError)?; - builder.en_passant_square(Some(square)); + builder.en_passant(Some(EnPassant::from_target_square(square).unwrap())); } let half_move_clock = fields.next().ok_or(FromFenError)?; diff --git a/position/src/macros.rs b/position/src/macros.rs index c7998d7..260a502 100644 --- a/position/src/macros.rs +++ b/position/src/macros.rs @@ -16,6 +16,7 @@ macro_rules! position { }; } +#[cfg(test)] #[macro_export] macro_rules! test_position { ($to_move:ident, [ $($color:ident $shape:ident on $square:ident),* $(,)? ], $en_passant:ident) => { @@ -31,7 +32,7 @@ macro_rules! test_position { )) )* .to_move(chessfriend_core::Color::$to_move) - .en_passant_square(Some(chessfriend_core::Square::$en_passant)) + .en_passant(Some(chessfriend_moves::EnPassant::from_target_square(chessfriend_core::Square::$en_passant)).unwrap()) .build(); println!("{pos}"); diff --git a/position/src/move_generator/king.rs b/position/src/move_generator/king.rs index 33b9860..3443d46 100644 --- a/position/src/move_generator/king.rs +++ b/position/src/move_generator/king.rs @@ -154,7 +154,7 @@ mod tests { } #[test] - fn white_king_unobstructed_castles() { + fn white_king_unobstructed_castles() -> TestResult { let pos = test_position!( White King on E1, White Rook on A1, @@ -167,12 +167,16 @@ mod tests { let generator = KingMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); let generated_moves: HashSet = generator.iter().collect(); - assert!(generated_moves.contains(&MoveBuilder::castling(Castle::KingSide).build())); - assert!(generated_moves.contains(&MoveBuilder::castling(Castle::QueenSide).build())); + assert!(generated_moves + .contains(&MoveBuilder::castling(Color::White, Castle::KingSide).build()?)); + assert!(generated_moves + .contains(&MoveBuilder::castling(Color::White, Castle::QueenSide).build()?)); + + Ok(()) } #[test] - fn white_king_obstructed_queenside_castle() { + fn white_king_obstructed_queenside_castle() -> TestResult { let pos = test_position!( White King on E1, White Knight on B1, @@ -186,12 +190,16 @@ mod tests { let generator = KingMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); let generated_moves: HashSet = generator.iter().collect(); - assert!(generated_moves.contains(&MoveBuilder::castling(Castle::KingSide).build())); - assert!(!generated_moves.contains(&MoveBuilder::castling(Castle::QueenSide).build())); + assert!(generated_moves + .contains(&MoveBuilder::castling(Color::White, Castle::KingSide).build()?)); + assert!(!generated_moves + .contains(&MoveBuilder::castling(Color::White, Castle::QueenSide).build()?)); + + Ok(()) } #[test] - fn white_king_obstructed_kingside_castle() { + fn white_king_obstructed_kingside_castle() -> TestResult { let pos = test_position!( White King on E1, White Rook on A1, @@ -205,7 +213,11 @@ mod tests { let generator = KingMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); let generated_moves: HashSet = generator.iter().collect(); - assert!(!generated_moves.contains(&MoveBuilder::castling(Castle::KingSide).build())); - assert!(generated_moves.contains(&MoveBuilder::castling(Castle::QueenSide).build())); + assert!(!generated_moves + .contains(&MoveBuilder::castling(Color::White, Castle::KingSide).build()?)); + assert!(generated_moves + .contains(&MoveBuilder::castling(Color::White, Castle::QueenSide).build()?)); + + Ok(()) } } diff --git a/position/src/move_generator/move_set.rs b/position/src/move_generator/move_set.rs index 5ba9467..61adb03 100644 --- a/position/src/move_generator/move_set.rs +++ b/position/src/move_generator/move_set.rs @@ -34,17 +34,49 @@ impl MoveSet { } } - pub(crate) fn can_move_to_square(&self, to: Square) -> bool { - self.bitboard().is_set(to) + pub(crate) fn can_move_to_square(&self, target_square: Square) -> bool { + match self.special { + Some(Special::King { castles }) => { + if self.check_castle_field(castles, Castle::KingSide) + && target_square + == Castle::KingSide + .parameters(self.piece.color()) + .king_target_square() + { + return true; + } + + if self.check_castle_field(castles, Castle::KingSide) + && target_square + == Castle::QueenSide + .parameters(self.piece.color()) + .king_target_square() + { + return true; + } + } + Some(Special::Pawn { en_passant }) => { + if target_square == en_passant.target_square() { + return true; + } + } + None => {} + } + + self.bitboard().is_set(target_square) } pub(crate) fn can_castle(&self, castle: Castle) -> bool { match self.special { - Some(Special::King { castles }) => (castles & 1 << castle as u8) != 0, + Some(Special::King { castles }) => self.check_castle_field(castles, castle), _ => false, } } + fn check_castle_field(&self, castle_field: u8, castle: Castle) -> bool { + (castle_field & 1 << castle as u8) != 0 + } + pub(super) fn quiet_moves(mut self, bitboard: BitBoard) -> MoveSet { self.bitboards.quiet = bitboard; self @@ -132,7 +164,11 @@ impl MoveSet { match self.special { Some(Special::King { castles }) => { if (castles & 1 << castle as u8) != 0 { - Some(MoveBuilder::castling(castle).build()) + Some( + MoveBuilder::castling(self.piece.color(), castle) + .build() + .ok()?, + ) } else { None } diff --git a/position/src/position/builders/move_builder.rs b/position/src/position/builders/move_builder.rs index 900c322..307bae6 100644 --- a/position/src/position/builders/move_builder.rs +++ b/position/src/position/builders/move_builder.rs @@ -3,7 +3,7 @@ use crate::{position::flags::Flags, Position}; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Direction, Piece, PlacedPiece, Shape, Square}; -use chessfriend_moves::{Castle, Move}; +use chessfriend_moves::{Castle, EnPassant, Move}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum MakeMoveError { @@ -34,7 +34,7 @@ pub enum ValidatedMove { captured_piece: Option, promotion: Option, flags: Flags, - en_passant_square: Option, + en_passant: Option, increment_ply: bool, }, Castle { @@ -149,11 +149,12 @@ where }, }) } else { - let en_passant_square: Option = if mv.is_double_push() { + let en_passant = if mv.is_double_push() { match piece.color() { Color::White => target_square.neighbor(Direction::South), Color::Black => target_square.neighbor(Direction::North), } + .and_then(EnPassant::from_target_square) } else { None }; @@ -167,7 +168,7 @@ where captured_piece, promotion: mv.promotion(), flags, - en_passant_square, + en_passant, increment_ply: !(mv.is_capture() || piece.is_pawn()), }, }) @@ -190,7 +191,7 @@ impl<'p> Builder<'p, ValidatedMove> { captured_piece, promotion, flags, - en_passant_square, + en_passant, increment_ply, } => { let mut pieces = self.position.piece_bitboards().clone(); @@ -217,7 +218,7 @@ impl<'p> Builder<'p, ValidatedMove> { self.position.player_to_move().other(), flags, pieces, - en_passant_square, + en_passant, ply, updated_move_number, ) @@ -270,8 +271,8 @@ impl<'p> From<&'p Position> for Builder<'p, NoMove> { mod tests { use super::*; use crate::testing::*; - use crate::{position, PositionBuilder}; - use chessfriend_core::piece; + use crate::{position, test_position}; + use chessfriend_core::{piece, File}; use chessfriend_moves::Builder as MoveBuilder; #[test] @@ -299,22 +300,22 @@ mod tests { #[test] fn move_white_pawn_two_squares() -> TestResult { - let pos = position![White Pawn on E2]; - let mv = MoveBuilder::new() - .from(Square::E2) - .to(Square::E4) - .build() - .map_err(TestError::BuildMove)?; + let pos = test_position![White Pawn on E2]; - let new_position = Builder::::new(&pos).make(&mv)?.build(); + let mv = MoveBuilder::double_push(File::E, Color::White).build()?; + + let new_position = Builder::new(&pos).make(&mv)?.build(); println!("{}", &new_position); assert_eq!( new_position.piece_on_square(Square::E4), Some(piece!(White Pawn on E4)) ); + + let en_passant = new_position.en_passant(); + assert!(en_passant.is_some()); assert_eq!( - new_position.en_passant().map(|ep| ep.target_square()), + en_passant.as_ref().map(EnPassant::target_square), Some(Square::E3) ); @@ -322,8 +323,8 @@ mod tests { } #[test] - fn white_kingside_castle() -> Result<(), MakeMoveError> { - let pos = position![ + fn white_kingside_castle() -> TestResult { + let pos = test_position![ White King on E1, White Rook on H1, White Pawn on E2, @@ -331,11 +332,10 @@ mod tests { White Pawn on G2, White Pawn on H2 ]; - println!("{}", &pos); - let mv = MoveBuilder::castling(Castle::KingSide).build(); + let mv = MoveBuilder::castling(Color::White, Castle::KingSide).build()?; - let new_position = Builder::::new(&pos).make(&mv)?.build(); + let new_position = Builder::new(&pos).make(&mv)?.build(); println!("{}", &new_position); assert_eq!( @@ -352,14 +352,12 @@ mod tests { #[test] fn en_passant_capture() -> TestResult { - let pos = PositionBuilder::new() - .place_piece(piece!(White Pawn on B5)) - .place_piece(piece!(Black Pawn on A7)) - .to_move(Color::Black) - .build(); - println!("{pos}"); + let pos = test_position!(Black, [ + White Pawn on B5, + Black Pawn on A7, + ]); - let black_pawn_move = MoveBuilder::new().from(Square::A7).to(Square::A5).build()?; + let black_pawn_move = MoveBuilder::double_push(File::A, Color::Black).build()?; assert!(black_pawn_move.is_double_push()); assert!(!black_pawn_move.is_en_passant()); @@ -377,9 +375,9 @@ mod tests { ); let white_pawn_capture = MoveBuilder::push(&piece!(White Pawn on B5)) - .capturing_en_passant_on(Square::A5) + .capturing_en_passant_on(Square::A6) .build()?; - let en_passant_capture = Builder::::new(&en_passant_position) + let en_passant_capture = Builder::new(&en_passant_position) .make(&white_pawn_capture)? .build(); println!("{en_passant_capture}"); diff --git a/position/src/position/builders/position_builder.rs b/position/src/position/builders/position_builder.rs index a4f8d2f..08fc193 100644 --- a/position/src/position/builders/position_builder.rs +++ b/position/src/position/builders/position_builder.rs @@ -5,7 +5,7 @@ use crate::{ Position, }; use chessfriend_core::{piece, Color, Piece, PlacedPiece, Rank, Shape, Square}; -use chessfriend_moves::Castle; +use chessfriend_moves::{Castle, EnPassant}; use std::collections::BTreeMap; #[derive(Clone)] @@ -14,7 +14,7 @@ pub struct Builder { flags: Flags, pieces: BTreeMap, kings: [Option; 2], - en_passant_square: Option, + en_passant: Option, ply_counter: u16, move_number: u16, } @@ -30,7 +30,7 @@ impl Builder { flags: Flags::default(), pieces: BTreeMap::default(), kings: [None, None], - en_passant_square: None, + en_passant: None, ply_counter: 0, move_number: 1, } @@ -52,7 +52,7 @@ impl Builder { flags: position.flags(), pieces, kings: [Some(white_king), Some(black_king)], - en_passant_square: position.en_passant().map(|ep| ep.target_square()), + en_passant: position.en_passant(), ply_counter: position.ply_counter(), move_number: position.move_number(), } @@ -73,8 +73,8 @@ impl Builder { self } - pub fn en_passant_square(&mut self, square: Option) -> &mut Self { - self.en_passant_square = square; + pub fn en_passant(&mut self, en_passant: Option) -> &mut Self { + self.en_passant = en_passant; self } @@ -138,7 +138,7 @@ impl Builder { self.player_to_move, flags, pieces, - self.en_passant_square, + self.en_passant, self.ply_counter, self.move_number, ) @@ -173,7 +173,7 @@ impl Default for Builder { flags: Flags::default(), pieces: pieces, kings: [Some(white_king_square), Some(black_king_square)], - en_passant_square: None, + en_passant: None, ply_counter: 0, move_number: 1, } diff --git a/position/src/position/position.rs b/position/src/position/position.rs index d336703..a6fc369 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -17,7 +17,7 @@ pub struct Position { color_to_move: Color, flags: Flags, pieces: PieceBitBoards, - en_passant_square: Option, + en_passant: Option, moves: OnceCell, half_move_counter: u16, full_move_number: u16, @@ -183,15 +183,15 @@ impl Position { } pub fn has_en_passant_square(&self) -> bool { - self.en_passant_square.is_some() + self.en_passant.is_some() } pub fn en_passant(&self) -> Option { - EnPassant::from_target_square(self.en_passant_square?) + self.en_passant } fn _sight_of_player(&self, player: Color, pieces: &PieceBitBoards) -> BitBoard { - let en_passant_square = self.en_passant_square; + let en_passant_target_square = self.en_passant.map(|ep| ep.target_square()); Shape::ALL .iter() @@ -206,7 +206,7 @@ impl Position { }) .flat_map(|(piece, &bitboard)| { bitboard.occupied_squares().map(move |square| { - PlacedPiece::new(piece, square).sight(pieces, en_passant_square) + PlacedPiece::new(piece, square).sight(pieces, en_passant_target_square) }) }) .fold(BitBoard::empty(), |acc, sight| acc | sight) @@ -218,7 +218,7 @@ impl Position { #[cfg(test)] pub(crate) fn sight_of_piece(&self, piece: &PlacedPiece) -> BitBoard { - piece.sight(&self.pieces, self.en_passant_square) + piece.sight(&self.pieces, self.en_passant.map(|ep| ep.target_square())) } /// A bitboard representing the squares where a king of the given color will @@ -299,14 +299,14 @@ impl Position { player_to_move: Color, flags: Flags, pieces: PieceBitBoards, - en_passant_square: Option, + en_passant: Option, half_move_counter: u16, full_move_number: u16, ) -> Self { Self { color_to_move: player_to_move, flags, - en_passant_square, + en_passant, pieces, half_move_counter, full_move_number, @@ -336,8 +336,8 @@ impl Position { #[cfg(test)] impl Position { - pub(crate) fn test_set_en_passant_square(&mut self, square: Square) { - self.en_passant_square = Some(square); + pub(crate) fn test_set_en_passant(&mut self, en_passant: EnPassant) { + self.en_passant = Some(en_passant); } } @@ -347,7 +347,7 @@ impl Default for Position { color_to_move: Color::White, flags: Flags::default(), pieces: PieceBitBoards::default(), - en_passant_square: None, + en_passant: None, moves: OnceCell::new(), half_move_counter: 0, full_move_number: 1, @@ -360,7 +360,7 @@ impl PartialEq for Position { self.pieces == other.pieces && self.color_to_move == other.color_to_move && self.flags == other.flags - && self.en_passant_square == other.en_passant_square + && self.en_passant == other.en_passant } } diff --git a/position/src/sight.rs b/position/src/sight.rs index 056ae45..29cbf91 100644 --- a/position/src/sight.rs +++ b/position/src/sight.rs @@ -242,6 +242,7 @@ mod tests { use crate::test_position; use chessfriend_bitboard::{bitboard, BitBoard}; use chessfriend_core::{piece, Square}; + use chessfriend_moves::EnPassant; sight_test!(e4_pawn, piece!(White Pawn on E4), bitboard!(D5, F5)); @@ -289,7 +290,7 @@ mod tests { White Pawn on E5, Black Pawn on D5, ); - pos.test_set_en_passant_square(Square::D6); + pos.test_set_en_passant(EnPassant::from_target_square(Square::D6).unwrap()); let piece = piece!(White Pawn on E5); let sight = pos.sight_of_piece(&piece); From 673d57c02e29998e7b1cfd1de8894c6e1c359277 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 25 Feb 2024 14:12:51 -0800 Subject: [PATCH 186/423] Remove some unused imports from position::check --- position/src/check.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/position/src/check.rs b/position/src/check.rs index 05e34b7..a3f2804 100644 --- a/position/src/check.rs +++ b/position/src/check.rs @@ -1,7 +1,7 @@ // Eryn Wells use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Direction, Shape, Square}; +use chessfriend_core::Shape; use crate::sight::SliderRayToSquareExt; From 8f07e08500d7ac67493a69e4a2bd5a6b85ec8857 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 25 Feb 2024 14:51:25 -0800 Subject: [PATCH 187/423] Fix a couple of the obscure en passant cases from PEJ --- position/src/move_generator.rs | 2 +- position/src/move_generator/pawn.rs | 73 +++++++++++++++++-- .../move_generator/tests/peterellisjones.rs | 22 +++--- 3 files changed, 77 insertions(+), 20 deletions(-) diff --git a/position/src/move_generator.rs b/position/src/move_generator.rs index 4d14ece..c4271ce 100644 --- a/position/src/move_generator.rs +++ b/position/src/move_generator.rs @@ -56,7 +56,7 @@ macro_rules! move_generator_declaration { push_mask: chessfriend_bitboard::BitBoard, ) -> $name { let move_sets = if Self::shape() == chessfriend_core::Shape::King - || (!capture_mask.is_empty() && !push_mask.is_empty()) + || !(capture_mask.is_empty() && push_mask.is_empty()) { Self::move_sets(position, color, capture_mask, push_mask) } else { diff --git a/position/src/move_generator/pawn.rs b/position/src/move_generator/pawn.rs index e5db4cf..1a07c4c 100644 --- a/position/src/move_generator/pawn.rs +++ b/position/src/move_generator/pawn.rs @@ -37,7 +37,9 @@ impl MoveGeneratorInternal for PawnMoveGenerator { .quiet_moves(quiet_moves) .capture_moves(capture_moves); - if let Some(en_passant) = Self::en_passant(position, placed_piece) { + if let Some(en_passant) = + Self::en_passant(position, placed_piece, &push_mask, &capture_mask) + { move_set.en_passant(en_passant); } @@ -52,7 +54,7 @@ impl PawnMoveGenerator { capture_mask: BitBoard, push_mask: BitBoard, ) -> Self { - let move_sets = if !capture_mask.is_empty() && !push_mask.is_empty() { + let move_sets = if !(capture_mask.is_empty() && push_mask.is_empty()) { Self::move_sets(position, player_to_move, capture_mask, push_mask) } else { std::collections::BTreeMap::new() @@ -121,14 +123,26 @@ impl PawnMoveGenerator { BitBoard::pawn_attacks(piece.square(), color) & opponent_pieces } - fn en_passant(position: &Position, piece: &PlacedPiece) -> Option { + fn en_passant( + position: &Position, + piece: &PlacedPiece, + push_mask: &BitBoard, + capture_mask: &BitBoard, + ) -> Option { match position.en_passant() { Some(en_passant) => { - let target_square = en_passant.target_square(); + let target_square: BitBoard = en_passant.target_square().into(); + let capture_square: BitBoard = en_passant.capture_square().into(); - let en_passant_bitboard: BitBoard = target_square.into(); - let capture = - BitBoard::pawn_attacks(piece.square(), piece.color()) & en_passant_bitboard; + if (target_square & push_mask).is_empty() + && (capture_square & capture_mask).is_empty() + { + // Do not allow en passant if capturing would not either + // block an active check, or capture a checking pawn. + return None; + } + + let capture = BitBoard::pawn_attacks(piece.square(), piece.color()) & target_square; if capture.is_empty() { return None; } @@ -141,12 +155,28 @@ impl PawnMoveGenerator { None => None, } } + + #[cfg(none)] + fn does_en_passant_reveal_check(&self, position: &Position) -> bool { + let player_to_move = position.player_to_move(); + let opposing_player = player_to_move.other(); + + if position.king_square(opposing_player).rank() + != Rank::PAWN_DOUBLE_PUSH_TARGET_RANKS[player_to_move as usize] + { + return false; + } + false + } } #[cfg(test)] mod tests { use super::*; - use crate::{assert_move_list, position::DiagramFormatter, test_position, testing::*}; + use crate::{ + assert_move_list, formatted_move_list, position::DiagramFormatter, test_position, + testing::*, + }; use chessfriend_core::{piece, Color, Square}; use chessfriend_moves::{Builder as MoveBuilder, Move}; use std::collections::HashSet; @@ -292,4 +322,31 @@ mod tests { Ok(()) } + + /// Make sure the player cannot capture en passant if doing so would not resolve the check. + #[test] + fn cannot_capture_en_passant_while_in_check() -> TestResult { + let pos = test_position!(Black, [ + Black King on B5, + Black Pawn on E4, + White Pawn on D4, + White Rook on B1, + ], D3); + + assert!(pos.is_king_in_check()); + + let generated_moves: HashSet<_> = pos.moves().iter().collect(); + + assert!( + !generated_moves.contains( + &MoveBuilder::push(&piece!(Black Pawn on E4)) + .capturing_en_passant_on(Square::D3) + .build()? + ), + "Valid moves: {:?}", + formatted_move_list!(generated_moves, pos) + ); + + Ok(()) + } } diff --git a/position/src/move_generator/tests/peterellisjones.rs b/position/src/move_generator/tests/peterellisjones.rs index 31a878a..4a12b92 100644 --- a/position/src/move_generator/tests/peterellisjones.rs +++ b/position/src/move_generator/tests/peterellisjones.rs @@ -145,12 +145,12 @@ fn en_passant_check_capture() -> TestResult { assert!(pos.is_king_in_check()); - let generated_moves = pos.moves().iter().collect::>(); + let generated_moves: HashSet<_> = pos.moves().iter().collect(); assert!( generated_moves.contains( &MoveBuilder::push(&piece!(Black Pawn on E4)) - .capturing_en_passant_on(Square::D4) + .capturing_en_passant_on(Square::D3) .build()? ), "Valid moves: {:?}", @@ -171,12 +171,12 @@ fn en_passant_check_block() -> TestResult { assert!(pos.is_king_in_check()); - let generated_moves = pos.moves().iter().collect::>(); + let generated_moves: HashSet<_> = pos.moves().iter().collect(); assert!( generated_moves.contains( &MoveBuilder::push(&piece!(Black Pawn on E4)) - .capturing_en_passant_on(Square::D4) + .capturing_en_passant_on(Square::D3) .build()? ), "Valid moves: {:?}", @@ -187,7 +187,7 @@ fn en_passant_check_block() -> TestResult { } #[test] -fn pinned_pieces_rook_cannot_move_out_of_pin() -> Result<(), String> { +fn pinned_pieces_rook_cannot_move_out_of_pin() -> TestResult { let pos = test_position!(Black, [ Black King on E8, Black Rook on E6, @@ -200,15 +200,15 @@ fn pinned_pieces_rook_cannot_move_out_of_pin() -> Result<(), String> { let generated_moves = pos.moves(); let rook_moves = generated_moves .moves_for_piece(&piece!(Black Rook on E6)) - .ok_or("No valid rook moves")?; + .ok_or(TestError::NoLegalMoves)?; assert!(!rook_moves.can_move_to_square(Square::D6)); assert!(!rook_moves.can_move_to_square(Square::F6)); - assert!(rook_moves.can_move_to_square(Square::E7)); - assert!(rook_moves.can_move_to_square(Square::E5)); - assert!(rook_moves.can_move_to_square(Square::E4)); assert!(rook_moves.can_move_to_square(Square::E3)); + assert!(rook_moves.can_move_to_square(Square::E4)); + assert!(rook_moves.can_move_to_square(Square::E5)); + assert!(rook_moves.can_move_to_square(Square::E7)); Ok(()) } @@ -222,10 +222,10 @@ fn en_passant_discovered_check() -> TestResult { White Queen on H4, ], D3); - let generated_moves = pos.moves().iter().collect::>(); + let generated_moves: HashSet<_> = pos.moves().iter().collect(); let unexpected_move = MoveBuilder::push(&piece!(Black Pawn on E4)) - .capturing_en_passant_on(Square::D4) + .capturing_en_passant_on(Square::D3) .build()?; assert!( From 8a1b16d553c0ed2a36db6583c73e9e7ee457eb5e Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 25 Feb 2024 14:51:32 -0800 Subject: [PATCH 188/423] Export the fen! macro --- position/src/fen.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/position/src/fen.rs b/position/src/fen.rs index 8859324..fde920c 100644 --- a/position/src/fen.rs +++ b/position/src/fen.rs @@ -5,6 +5,7 @@ use chessfriend_core::{piece, Color, File, Piece, PlacedPiece, Rank, Square}; use chessfriend_moves::{Castle, EnPassant}; use std::fmt::Write; +#[macro_export] macro_rules! fen { ($fen_string:literal) => { Position::from_fen_str($fen_string) From a65fcd6000a188cd844112fd75622de131dab5ad Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 1 Mar 2024 15:24:20 -0800 Subject: [PATCH 189/423] [position] Rename fields of EnPassant struct Append _square to each one. --- moves/src/en_passant.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/moves/src/en_passant.rs b/moves/src/en_passant.rs index 49e88a6..9a0f972 100644 --- a/moves/src/en_passant.rs +++ b/moves/src/en_passant.rs @@ -5,8 +5,8 @@ use chessfriend_core::{Rank, Square}; /// En passant information. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct EnPassant { - target: Square, - capture: Square, + target_square: Square, + capture_square: Square, } impl EnPassant { @@ -34,18 +34,21 @@ impl EnPassant { /// ``` pub fn from_target_square(target: Square) -> Option { match Self::_capture_square(target) { - Some(capture) => Some(Self { target, capture }), + Some(capture) => Some(Self { + target_square: target, + capture_square: capture, + }), None => None, } } /// The square the capturing piece will move to. pub fn target_square(&self) -> Square { - self.target + self.target_square } /// The square on which the captured pawn sits. pub fn capture_square(&self) -> Square { - self.capture + self.capture_square } } From d20119dfe3eb97081e754901def144eb9a806d73 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 1 Mar 2024 15:24:43 -0800 Subject: [PATCH 190/423] [position] Make Shape::to_ascii() const --- core/src/pieces.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/pieces.rs b/core/src/pieces.rs index 148f05f..51ae727 100644 --- a/core/src/pieces.rs +++ b/core/src/pieces.rs @@ -34,7 +34,7 @@ impl Shape { PROMOTABLE_SHAPES.iter() } - fn to_ascii(&self) -> char { + const fn to_ascii(&self) -> char { match self { Shape::Pawn => 'P', Shape::Knight => 'N', From 349d82304d853e7ba1a2908181a705b30f79e4b7 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 1 Mar 2024 15:25:33 -0800 Subject: [PATCH 191/423] [bitboard] Implement From> for BitBoard Return an empty BitBoard if the input is None. --- bitboard/src/bitboard.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index a8e2b3c..8d8d8dc 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -164,6 +164,12 @@ impl From for BitBoard { } } +impl From> for BitBoard { + fn from(value: Option) -> Self { + value.map_or(BitBoard::EMPTY, Into::::into) + } +} + impl From for BitBoard { fn from(value: Rank) -> Self { library::FILES[*value.as_index() as usize] From f0b9681ceffd1cbb1fdcec67abdb8d6aa9053e54 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 1 Mar 2024 15:26:01 -0800 Subject: [PATCH 192/423] [position] Add a doc comment to PieceBitBoards::all_pieces; remove unused empty_squares --- position/src/position/piece_sets.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/position/src/position/piece_sets.rs b/position/src/position/piece_sets.rs index 7bde2b1..b998600 100644 --- a/position/src/position/piece_sets.rs +++ b/position/src/position/piece_sets.rs @@ -51,14 +51,12 @@ impl PieceBitBoards { } } + /// A BitBoard representing all the pieces currently on the board. Other + /// engines might refer to this concept as 'occupancy'. pub(crate) fn all_pieces(&self) -> &BitBoard { self.by_color.all() } - pub(crate) fn empty_squares(&self) -> BitBoard { - !self.by_color.all() - } - pub(crate) fn all_pieces_of_color(&self, color: Color) -> &BitBoard { self.by_color.bitboard(color) } From 069f94e8c222fdcf4e43a3b4775fc0931ebb56ab Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 8 Mar 2024 08:04:21 -0800 Subject: [PATCH 193/423] [position] Refactor sight routines - Break out the actual routines into file-private helper functions - Declare traits for each piece type that call the corresponding helper function - Implement these traits on PlacedPiece The sight routines changed slightly such that they include the player's own pieces in the resulting BitBoard. The tests neeeded to be updated to account for this. --- position/src/sight.rs | 260 +++++++++++++++++++++++++++--------------- 1 file changed, 169 insertions(+), 91 deletions(-) diff --git a/position/src/sight.rs b/position/src/sight.rs index 29cbf91..3b9170d 100644 --- a/position/src/sight.rs +++ b/position/src/sight.rs @@ -1,5 +1,9 @@ // Eryn Wells +//! Defines routines for computing sight of a piece. Sight is the set of squares +//! that a piece can see. In other words, it's the set of squares attacked or +//! controled by a piece. + use crate::position::piece_sets::PieceBitBoards; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Direction, PlacedPiece, Shape, Square}; @@ -18,8 +22,84 @@ macro_rules! ray_in_direction { }}; } -pub(crate) trait SightExt { - fn sight(&self, pieces: &PieceBitBoards, en_passant_square: Option) -> BitBoard; +/// Compute sight of a white pawn. +fn _white_pawn_sight( + pawn: &BitBoard, + occupancy: &BitBoard, + blockers: &BitBoard, + en_passant_square: &BitBoard, +) -> BitBoard { + let possible_squares = !occupancy | blockers | en_passant_square; + let pawn = pawn.shift_north_west_one() | pawn.shift_north_east_one(); + pawn & possible_squares +} + +fn _black_pawn_sight( + pawn: &BitBoard, + occupancy: &BitBoard, + blockers: &BitBoard, + en_passant_square: &BitBoard, +) -> BitBoard { + let possible_squares = !occupancy | blockers | en_passant_square; + let pawn = pawn.shift_south_west_one() | pawn.shift_south_east_one(); + pawn & possible_squares +} + +fn _knight_sight(knight_square: Square, blockers: &BitBoard) -> BitBoard { + BitBoard::knight_moves(knight_square) & !blockers +} + +fn _bishop_sight(bishop_square: Square, occupancy: &BitBoard) -> BitBoard { + #[rustfmt::skip] + let sight = ray_in_direction!(bishop_square, occupancy, NorthEast, occupied_squares_trailing) + | ray_in_direction!(bishop_square, occupancy, NorthWest, occupied_squares_trailing) + | ray_in_direction!(bishop_square, occupancy, SouthEast, occupied_squares) + | ray_in_direction!(bishop_square, occupancy, SouthWest, occupied_squares); + + sight +} + +fn _rook_sight(rook_square: Square, occupancy: &BitBoard) -> BitBoard { + #[rustfmt::skip] + let sight = ray_in_direction!(rook_square, occupancy, North, occupied_squares_trailing) + | ray_in_direction!(rook_square, occupancy, East, occupied_squares_trailing) + | ray_in_direction!(rook_square, occupancy, South, occupied_squares) + | ray_in_direction!(rook_square, occupancy, West, occupied_squares); + + sight +} + +fn _queen_sight(queen_square: Square, occupancy: &BitBoard) -> BitBoard { + #[rustfmt::skip] + let sight = ray_in_direction!(queen_square, occupancy, NorthWest, occupied_squares_trailing) + | ray_in_direction!(queen_square, occupancy, North, occupied_squares_trailing) + | ray_in_direction!(queen_square, occupancy, NorthEast, occupied_squares_trailing) + | ray_in_direction!(queen_square, occupancy, East, occupied_squares_trailing) + | ray_in_direction!(queen_square, occupancy, SouthEast, occupied_squares) + | ray_in_direction!(queen_square, occupancy, South, occupied_squares) + | ray_in_direction!(queen_square, occupancy, SouthWest, occupied_squares) + | ray_in_direction!(queen_square, occupancy, West, occupied_squares); + + sight +} + +fn _king_sight(king_square: Square, blockers: &BitBoard) -> BitBoard { + BitBoard::king_moves(king_square) & !blockers +} +pub(crate) trait BishopSightExt { + fn bishop_sight(&self, occupancy: &BitBoard) -> BitBoard; +} + +pub(crate) trait KingSightExt { + fn king_sight(&self, pieces: &PieceBitBoards) -> BitBoard; +} + +pub(crate) trait KnightSightExt { + fn knight_sight(&self, pieces: &PieceBitBoards) -> BitBoard; +} + +pub(crate) trait PawnSightExt { + fn pawn_sight(&self, pieces: &PieceBitBoards, en_passant_square: Option) -> BitBoard; fn white_pawn_sight( &self, @@ -32,12 +112,19 @@ pub(crate) trait SightExt { pieces: &PieceBitBoards, en_passant_square: Option, ) -> BitBoard; +} +pub(crate) trait QueenSightExt { + fn queen_sight(&self, occupancy: &BitBoard) -> BitBoard; +} - fn knight_sight(&self, pieces: &PieceBitBoards) -> BitBoard; - fn bishop_sight(&self, pieces: &PieceBitBoards) -> BitBoard; - fn rook_sight(&self, pieces: &PieceBitBoards) -> BitBoard; - fn queen_sight(&self, pieces: &PieceBitBoards) -> BitBoard; - fn king_sight(&self, pieces: &PieceBitBoards) -> BitBoard; +pub(crate) trait RookSightExt { + fn rook_sight(&self, occupancy: &BitBoard) -> BitBoard; +} + +pub(crate) trait SliderSightExt: BishopSightExt + QueenSightExt + RookSightExt {} + +pub(crate) trait SightExt { + fn sight(&self, pieces: &PieceBitBoards, en_passant_square: Option) -> BitBoard; } pub(crate) trait SliderRayToSquareExt { @@ -52,12 +139,33 @@ impl SightExt for PlacedPiece { Color::Black => self.black_pawn_sight(pieces, en_passant_square), }, Shape::Knight => self.knight_sight(pieces), - Shape::Bishop => self.bishop_sight(pieces), - Shape::Rook => self.rook_sight(pieces), - Shape::Queen => self.queen_sight(pieces), + Shape::Bishop => self.bishop_sight(pieces.all_pieces()), + Shape::Rook => self.rook_sight(pieces.all_pieces()), + Shape::Queen => self.queen_sight(pieces.all_pieces()), Shape::King => self.king_sight(pieces), } } +} + +impl KingSightExt for PlacedPiece { + fn king_sight(&self, pieces: &PieceBitBoards) -> BitBoard { + _king_sight(self.square(), pieces.all_pieces_of_color(self.color())) + } +} + +impl KnightSightExt for PlacedPiece { + fn knight_sight(&self, pieces: &PieceBitBoards) -> BitBoard { + _knight_sight(self.square(), pieces.all_pieces_of_color(self.color())) + } +} + +impl PawnSightExt for PlacedPiece { + fn pawn_sight(&self, pieces: &PieceBitBoards, en_passant_square: Option) -> BitBoard { + match self.color() { + Color::White => self.white_pawn_sight(pieces, en_passant_square), + Color::Black => self.black_pawn_sight(pieces, en_passant_square), + } + } fn white_pawn_sight( &self, @@ -65,16 +173,12 @@ impl SightExt for PlacedPiece { en_passant_square: Option, ) -> BitBoard { let opponent = self.color().other(); - let pawn: BitBoard = self.square().into(); - let pawn = pawn.shift_north_west_one() | pawn.shift_north_east_one(); - - let mut possible_squares = pieces.empty_squares() | pieces.all_pieces_of_color(opponent); - if let Some(en_passant) = en_passant_square { - let en_passant_bitboard: BitBoard = en_passant.into(); - possible_squares |= en_passant_bitboard; - } - - pawn & possible_squares + _white_pawn_sight( + &self.square().into(), + pieces.all_pieces(), + pieces.all_pieces_of_color(opponent), + &en_passant_square.into(), + ) } fn black_pawn_sight( @@ -83,79 +187,35 @@ impl SightExt for PlacedPiece { en_passant_square: Option, ) -> BitBoard { let opponent = self.color().other(); - - let pawn: BitBoard = self.square().into(); - let pawn = pawn.shift_south_west_one() | pawn.shift_south_east_one(); - - let mut possible_squares = pieces.empty_squares() | pieces.all_pieces_of_color(opponent); - if let Some(en_passant) = en_passant_square { - possible_squares |= &en_passant.into(); - } - - pawn & possible_squares - } - - fn knight_sight(&self, pieces: &PieceBitBoards) -> BitBoard { - BitBoard::knight_moves(self.square()) & !pieces.all_pieces_of_color(self.color()) - } - - fn bishop_sight(&self, pieces: &PieceBitBoards) -> BitBoard { - let square = self.square(); - - let mut sight = BitBoard::empty(); - - let blockers = pieces.all_pieces(); - - sight |= ray_in_direction!(square, blockers, NorthEast, occupied_squares_trailing); - sight |= ray_in_direction!(square, blockers, NorthWest, occupied_squares_trailing); - sight |= ray_in_direction!(square, blockers, SouthEast, occupied_squares); - sight |= ray_in_direction!(square, blockers, SouthWest, occupied_squares); - - let friendly_pieces = pieces.all_pieces_of_color(self.color()); - sight & !friendly_pieces - } - - fn rook_sight(&self, pieces: &PieceBitBoards) -> BitBoard { - let square = self.square(); - - let mut sight = BitBoard::empty(); - - let blockers = pieces.all_pieces(); - - sight |= ray_in_direction!(square, blockers, North, occupied_squares_trailing); - sight |= ray_in_direction!(square, blockers, East, occupied_squares_trailing); - sight |= ray_in_direction!(square, blockers, South, occupied_squares); - sight |= ray_in_direction!(square, blockers, West, occupied_squares); - - let friendly_pieces = pieces.all_pieces_of_color(self.color()); - sight & !friendly_pieces - } - - fn queen_sight(&self, pieces: &PieceBitBoards) -> BitBoard { - let square = self.square(); - - let mut sight = BitBoard::empty(); - - let blockers = pieces.all_pieces(); - - sight |= ray_in_direction!(square, blockers, NorthWest, occupied_squares_trailing); - sight |= ray_in_direction!(square, blockers, North, occupied_squares_trailing); - sight |= ray_in_direction!(square, blockers, NorthEast, occupied_squares_trailing); - sight |= ray_in_direction!(square, blockers, East, occupied_squares_trailing); - sight |= ray_in_direction!(square, blockers, SouthEast, occupied_squares); - sight |= ray_in_direction!(square, blockers, South, occupied_squares); - sight |= ray_in_direction!(square, blockers, SouthWest, occupied_squares); - sight |= ray_in_direction!(square, blockers, West, occupied_squares); - - let friendly_pieces = pieces.all_pieces_of_color(self.color()); - sight & !friendly_pieces - } - - fn king_sight(&self, pieces: &PieceBitBoards) -> BitBoard { - BitBoard::king_moves(self.square()) & !pieces.all_pieces_of_color(self.color()) + _black_pawn_sight( + &self.square().into(), + pieces.all_pieces(), + pieces.all_pieces_of_color(opponent), + &en_passant_square.into(), + ) } } +impl BishopSightExt for PlacedPiece { + fn bishop_sight(&self, occupancy: &BitBoard) -> BitBoard { + _bishop_sight(self.square(), occupancy) + } +} + +impl RookSightExt for PlacedPiece { + fn rook_sight(&self, occupancy: &BitBoard) -> BitBoard { + _rook_sight(self.square(), occupancy) + } +} + +impl QueenSightExt for PlacedPiece { + fn queen_sight(&self, occupancy: &BitBoard) -> BitBoard { + _queen_sight(self.square(), occupancy) + } +} + +impl SliderSightExt for PlacedPiece {} + impl SliderRayToSquareExt for Shape { fn ray_to_square(&self, origin: Square, target: Square) -> Option { macro_rules! ray { @@ -210,6 +270,24 @@ impl SliderRayToSquareExt for Shape { } } +impl BishopSightExt for Square { + fn bishop_sight(&self, occupancy: &BitBoard) -> BitBoard { + _bishop_sight(*self, occupancy) + } +} + +impl QueenSightExt for Square { + fn queen_sight(&self, occupancy: &BitBoard) -> BitBoard { + _queen_sight(*self, occupancy) + } +} + +impl RookSightExt for Square { + fn rook_sight(&self, occupancy: &BitBoard) -> BitBoard { + _rook_sight(*self, occupancy) + } +} + #[cfg(test)] mod tests { use super::*; @@ -345,7 +423,7 @@ mod tests { Black King on E7, ], piece!(White Rook on E4), - bitboard!(A4, B4, C4, D4, F4, G4, H4, E2, E3, E5, E6, E7) + bitboard!(A4, B4, C4, D4, F4, G4, H4, E2, E3, E5, E6, E7, E1) ); #[test] From 20182d4035e23a93f3ba7e3c125f75ef6a7b6280 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 8 Mar 2024 08:08:52 -0800 Subject: [PATCH 194/423] [position] Implement an assert_eq_bitboards! macro This one helps with printing BitBoards if the assertion fails. --- position/src/testing.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/position/src/testing.rs b/position/src/testing.rs index 1693110..12400bc 100644 --- a/position/src/testing.rs +++ b/position/src/testing.rs @@ -36,6 +36,19 @@ macro_rules! formatted_move_list { }; } +#[macro_export] +macro_rules! assert_eq_bitboards { + ($result:expr, $expected:expr) => {{ + let result = $result; + let expected = $expected; + assert_eq!( + result, expected, + "Result:\n{}\nExpected:\n{}", + result, expected + ); + }}; +} + pub type TestResult = Result<(), TestError>; #[derive(Debug, Eq, PartialEq)] From 3f6ffef9f375f8b27d1325fa5dd13f19fb07de90 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 8 Mar 2024 08:15:45 -0800 Subject: [PATCH 195/423] [bitboard] Write some documentation; mark some methods const - Implement BitBoard::is_populated(), the opposite of ::is_empty() - Write a bit of documentation for the BitBoard Library and for some methods on BitBoard - Mark a few methods as const --- bitboard/src/bitboard.rs | 35 ++++++++++++++++++++++++++++++----- bitboard/src/library.rs | 8 ++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 8d8d8dc..5efbed0 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -67,16 +67,41 @@ impl BitBoard { } impl BitBoard { - pub fn as_bits(&self) -> &u64 { + pub const fn as_bits(&self) -> &u64 { &self.0 } - pub fn is_empty(&self) -> bool { + /// Returns `true` if the [`BitBoard`] has no bits set. + /// + /// ## Examples + /// + /// ``` + /// use chessfriend_bitboard::BitBoard; + /// assert!(BitBoard::EMPTY.is_populated()); + /// assert!(!BitBoard::FULL.is_populated()); + /// assert!(!BitBoard::new(0b1000).is_populated()); + /// ``` + pub const fn is_empty(&self) -> bool { self.0 == 0 } - pub fn is_set(self, sq: Square) -> bool { - let square_bitboard: BitBoard = sq.into(); + /// Returns `true` if the [`BitBoard`] has at least one bit set. + /// + /// ## Examples + /// + /// ``` + /// use chessfriend_bitboard::BitBoard; + /// assert!(!BitBoard::EMPTY.is_populated()); + /// assert!(BitBoard::FULL.is_populated()); + /// assert!(BitBoard::new(0b1).is_populated()); + /// ``` + pub const fn is_populated(&self) -> bool { + self.0 != 0 + } + + /// Returns `true` if this [`BitBoard`] has the bit corresponding to `square` set. + pub fn is_set(self, square: Square) -> bool { + let square_bitboard: BitBoard = square.into(); !(self & square_bitboard).is_empty() } @@ -90,7 +115,7 @@ impl BitBoard { /// assert_eq!(BitBoard::new(0b01011110010).population_count(), 6); /// assert_eq!(BitBoard::FULL.population_count(), 64); /// ``` - pub fn population_count(&self) -> u32 { + pub const fn population_count(&self) -> u32 { self.0.count_ones() } diff --git a/bitboard/src/library.rs b/bitboard/src/library.rs index 5e74259..f87084b 100644 --- a/bitboard/src/library.rs +++ b/bitboard/src/library.rs @@ -1,5 +1,13 @@ // Eryn Wells +//! # The BitBoard Library +//! +//! This module implements a collection of commonly used BitBoards that can be +//! looked up efficiently as needed. +//! +//! The `library()` method returns a static instance of a `Library`, which +//! provides getters for all available BitBoards. + use crate::BitBoard; use chessfriend_core::{Color, Direction, Square}; use std::sync::OnceLock; From 82aa7a2b0127deff344c2c876988593c9f7ccbab Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 8 Mar 2024 08:17:54 -0800 Subject: [PATCH 196/423] [core] Rewrite Direction as a coordinate_enum! - Order the values of Direction in a clockwise fashion - Implement Direction::opposite() to return the opposing direction - Make some small changes to the macros in this file to improve readability, maybe. --- core/src/coordinates.rs | 76 +++++++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 29 deletions(-) diff --git a/core/src/coordinates.rs b/core/src/coordinates.rs index 54aeb1a..f165a9a 100644 --- a/core/src/coordinates.rs +++ b/core/src/coordinates.rs @@ -1,28 +1,7 @@ // Eryn Wells use crate::Color; -use std::fmt; -use std::str::FromStr; - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[repr(u8)] -pub enum Direction { - North, - NorthWest, - West, - SouthWest, - South, - SouthEast, - East, - NorthEast, -} - -impl Direction { - pub fn to_offset(&self) -> i8 { - const OFFSETS: [i8; 8] = [8, 7, -1, -9, -8, -7, 1, 9]; - OFFSETS[*self as usize] - } -} +use std::{fmt, str::FromStr}; macro_rules! try_from_integer { ($type:ident, $int_type:ident) => { @@ -30,14 +9,14 @@ macro_rules! try_from_integer { type Error = (); fn try_from(value: $int_type) -> Result { - Square::try_from(value as u8) + Self::try_from(value as u8) } } }; } macro_rules! coordinate_enum { - ($name: ident, $($variant:ident),*) => { + ($name: ident, [ $($variant:ident),* ]) => { #[repr(u8)] #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum $name { @@ -69,7 +48,7 @@ macro_rules! coordinate_enum { } macro_rules! range_bound_struct { - ($vis:vis, $type:ident, $repr:ty, $max:expr) => { + ($vis:vis $type:ident, $repr:ty, $max:expr) => { #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] $vis struct $type($repr); @@ -113,7 +92,34 @@ macro_rules! range_bound_struct { } } -range_bound_struct!(pub, File, u8, 8); +coordinate_enum!( + Direction, + [North, NorthEast, East, SouthEast, South, SouthWest, West, NorthWest] +); + +impl Direction { + pub fn to_offset(&self) -> i8 { + const OFFSETS: [i8; 8] = [8, 9, 1, -7, -8, -9, -1, 7]; + OFFSETS[*self as usize] + } + + pub fn opposite(&self) -> Direction { + const OPPOSITES: [Direction; 8] = [ + Direction::South, + Direction::SouthEast, + Direction::East, + Direction::NorthEast, + Direction::North, + Direction::NorthWest, + Direction::West, + Direction::SouthWest, + ]; + + OPPOSITES[*self as usize] + } +} + +range_bound_struct!(pub File, u8, 8); impl File { pub const A: File = File(0); @@ -137,7 +143,7 @@ impl File { ]; } -range_bound_struct!(pub, Rank, u8, 8); +range_bound_struct!(pub Rank, u8, 8); #[allow(dead_code)] impl Rank { @@ -182,7 +188,7 @@ impl Rank { } #[rustfmt::skip] -coordinate_enum!(Square, +coordinate_enum!(Square, [ A1, B1, C1, D1, E1, F1, G1, H1, A2, B2, C2, D2, E2, F2, G2, H2, A3, B3, C3, D3, E3, F3, G3, H3, @@ -191,7 +197,7 @@ coordinate_enum!(Square, A6, B6, C6, D6, E6, F6, G6, H6, A7, B7, C7, D7, E7, F7, G7, H7, A8, B8, C8, D8, E8, F8, G8, H8 -); +]); impl Square { pub unsafe fn from_index(x: u8) -> Square { @@ -357,6 +363,18 @@ impl TryFrom for Rank { mod tests { use super::*; + #[test] + fn direction_offsets() { + assert_eq!(Direction::North.to_offset(), 8); + assert_eq!(Direction::NorthEast.to_offset(), 9); + assert_eq!(Direction::East.to_offset(), 1); + assert_eq!(Direction::SouthEast.to_offset(), -7); + assert_eq!(Direction::South.to_offset(), -8); + assert_eq!(Direction::SouthWest.to_offset(), -9); + assert_eq!(Direction::West.to_offset(), -1); + assert_eq!(Direction::NorthWest.to_offset(), 7); + } + #[test] fn good_algebraic_input() { let sq = Square::from_algebraic_str("a4").expect("Failed to parse 'a4' square"); From a65c0c8ef1ff4d791fc4db546e4fb5826e86ec72 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 8 Mar 2024 08:18:49 -0800 Subject: [PATCH 197/423] [position] Make the Position initializer methods const Position::empty() and Position::starting can both be const. Neat! --- position/src/position/position.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/position/src/position/position.rs b/position/src/position/position.rs index a6fc369..4b75758 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -24,13 +24,13 @@ pub struct Position { } impl Position { - pub fn empty() -> Position { + pub const fn empty() -> Position { Default::default() } /// Return a starting position. - pub fn starting() -> Self { - let black_pieces = [ + pub const fn starting() -> Self { + const BLACK_PIECES: [BitBoard; 6] = [ BitBoard::new(0b0000000011111111 << 48), BitBoard::new(0b0100001000000000 << 48), BitBoard::new(0b0010010000000000 << 48), @@ -39,7 +39,7 @@ impl Position { BitBoard::new(0b0001000000000000 << 48), ]; - let white_pieces = [ + const WHITE_PIECES: [BitBoard; 6] = [ BitBoard::new(0b1111111100000000), BitBoard::new(0b0000000001000010), BitBoard::new(0b0000000000100100), @@ -50,7 +50,7 @@ impl Position { Self { color_to_move: Color::White, - pieces: PieceBitBoards::new([white_pieces, black_pieces]), + pieces: PieceBitBoards::new([WHITE_PIECES, BLACK_PIECES]), ..Default::default() } } From 89802be53d475d34f762640c7fe2a69c4257ab48 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 10 Mar 2024 09:16:21 -0700 Subject: [PATCH 198/423] [core] Address some clippy linter errors in core/coordinate.rs --- core/src/coordinates.rs | 74 ++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 38 deletions(-) diff --git a/core/src/coordinates.rs b/core/src/coordinates.rs index f165a9a..b0c67ee 100644 --- a/core/src/coordinates.rs +++ b/core/src/coordinates.rs @@ -67,7 +67,14 @@ macro_rules! range_bound_struct { } } + /// Create a new `Self` + /// + /// # Safety + /// + /// This function does not perform any bounds checking. It should only be called when + /// the input is already known to be within bounds, i.e. when `x >= Self::FIRST && x < Self::LAST`. $vis unsafe fn new_unchecked(x: $repr) -> Self { + debug_assert!((Self::FIRST.0..=Self::LAST.0).contains(&x)); Self(x) } @@ -76,9 +83,9 @@ macro_rules! range_bound_struct { } } - impl Into<$repr> for $type { - fn into(self) -> $repr { - self.0 + impl From<$type> for $repr { + fn from(x: $type) -> Self { + x.0 } } @@ -200,7 +207,11 @@ coordinate_enum!(Square, [ ]); impl Square { + /// # Safety + /// + /// This function does not do any bounds checking on the input. pub unsafe fn from_index(x: u8) -> Square { + debug_assert!((x as usize) < Self::NUM); Self::try_from(x).unwrap_unchecked() } @@ -233,50 +244,37 @@ impl Square { let index: u8 = self as u8; let dir: i8 = direction.to_offset(); match direction { - Direction::North => Square::try_from(index.wrapping_add_signed(dir)).ok(), + Direction::North | Direction::NorthEast => { + Square::try_from(index.wrapping_add_signed(dir)).ok() + } Direction::NorthWest => { - if self.rank() != Rank::EIGHT { - Square::try_from(index.wrapping_add_signed(dir)).ok() - } else { + if self.rank() == Rank::EIGHT { None + } else { + Square::try_from(index.wrapping_add_signed(dir)).ok() } } Direction::West => { - if self.file() != File::A { - Square::try_from(index.wrapping_add_signed(dir)).ok() - } else { + if self.file() == File::A { None + } else { + Square::try_from(index.wrapping_add_signed(dir)).ok() } } - Direction::SouthWest => { - if self.rank() != Rank::ONE { - Square::try_from(index.wrapping_add_signed(dir)).ok() - } else { + Direction::SouthEast | Direction::South | Direction::SouthWest => { + if self.rank() == Rank::ONE { None - } - } - Direction::South => { - if self.rank() != Rank::ONE { - Square::try_from(index.wrapping_add_signed(dir)).ok() } else { - None - } - } - Direction::SouthEast => { - if self.rank() != Rank::ONE { Square::try_from(index.wrapping_add_signed(dir)).ok() - } else { - None } } Direction::East => { - if self.file() != File::H { - Square::try_from(index.wrapping_add_signed(dir)).ok() - } else { + if self.file() == File::H { None + } else { + Square::try_from(index.wrapping_add_signed(dir)).ok() } } - Direction::NorthEast => Square::try_from(index.wrapping_add_signed(dir)).ok(), } } } @@ -300,7 +298,7 @@ impl FromStr for Square { .and_then(|c| c.try_into().ok()) .ok_or(ParseSquareError)?; - if !chars.next().is_none() { + if chars.next().is_some() { return Err(ParseSquareError); } @@ -328,17 +326,17 @@ impl fmt::Display for Square { } } -impl Into for File { - fn into(self) -> char { - let value: u8 = self.into(); - (value + 'a' as u8) as char +impl From for char { + fn from(value: File) -> Self { + let u8value: u8 = value.into(); + (u8value + b'a') as char } } impl Into for Rank { fn into(self) -> char { let value: u8 = self.into(); - (value + '1' as u8) as char + (value + b'1') as char } } @@ -346,7 +344,7 @@ impl TryFrom for File { type Error = (); fn try_from(value: char) -> Result { - File::try_from(value.to_ascii_lowercase() as u8 - 'a' as u8) + File::try_from(value.to_ascii_lowercase() as u8 - b'a') } } @@ -354,7 +352,7 @@ impl TryFrom for Rank { type Error = (); fn try_from(value: char) -> Result { - let result = (value as u8).checked_sub('1' as u8).ok_or(())?; + let result = (value as u8).checked_sub(b'1').ok_or(())?; Self::try_from(result) } } From d0abbd8f931f314da803ade128aae66c9adbd0ff Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 10 Mar 2024 09:18:08 -0700 Subject: [PATCH 199/423] [position] Rever the const-ness of Position's initializers Apparently you can't actually do that. :( You can't call trait methods in const contexts. --- position/src/position/position.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/position/src/position/position.rs b/position/src/position/position.rs index 4b75758..383e998 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -24,12 +24,12 @@ pub struct Position { } impl Position { - pub const fn empty() -> Position { + pub fn empty() -> Position { Default::default() } /// Return a starting position. - pub const fn starting() -> Self { + pub fn starting() -> Self { const BLACK_PIECES: [BitBoard; 6] = [ BitBoard::new(0b0000000011111111 << 48), BitBoard::new(0b0100001000000000 << 48), From 27a36565f3b950a350f629b613920b9b71bf25bb Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 14 Mar 2024 17:00:46 -0700 Subject: [PATCH 200/423] [bitboard,core,position] Address a bunch of clippy warnings --- bitboard/src/bit_scanner.rs | 4 ++-- bitboard/src/bitboard.rs | 10 ++++++++-- bitboard/src/library.rs | 20 ++++++++++++-------- core/src/colors.rs | 13 +++++-------- core/src/pieces.rs | 27 +++++++++++++++++++++++---- position/src/check.rs | 11 +++++------ position/src/position/piece_sets.rs | 2 +- position/src/position/position.rs | 8 +++++++- 8 files changed, 63 insertions(+), 32 deletions(-) diff --git a/bitboard/src/bit_scanner.rs b/bitboard/src/bit_scanner.rs index bf90394..c9d5190 100644 --- a/bitboard/src/bit_scanner.rs +++ b/bitboard/src/bit_scanner.rs @@ -89,7 +89,7 @@ mod tests { #[test] fn leading_complex() { - let mut scanner = LeadingBitScanner::new(0b11000101); + let mut scanner = LeadingBitScanner::new(0b_1100_0101); assert_eq!(scanner.next(), Some(7)); assert_eq!(scanner.next(), Some(6)); assert_eq!(scanner.next(), Some(2)); @@ -112,7 +112,7 @@ mod tests { #[test] fn trailing_complex() { - let mut scanner = TrailingBitScanner::new(0b11000101); + let mut scanner = TrailingBitScanner::new(0b_1100_0101); assert_eq!(scanner.next(), Some(0)); assert_eq!(scanner.next(), Some(2)); assert_eq!(scanner.next(), Some(6)); diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 5efbed0..9b893b6 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -146,18 +146,23 @@ impl BitBoard { } impl BitBoard { - /// Return an Iterator over the occupied squares, starting from the leading - /// (most-significant bit) end of the field. + /// Returns an Iterator over the occupied squares. + /// + /// The Iterator yields squares starting from the leading (most-significant bit) end of the + /// board to the trailing (least-significant bit) end. + #[must_use] pub fn occupied_squares(&self) -> impl Iterator { LeadingBitScanner::new(self.0).map(|idx| unsafe { Square::from_index(idx as u8) }) } /// Return an Iterator over the occupied squares, starting from the trailing /// (least-significant bit) end of the field. + #[must_use] pub fn occupied_squares_trailing(&self) -> impl Iterator { TrailingBitScanner::new(self.0).map(|idx| unsafe { Square::from_index(idx as u8) }) } + #[must_use] pub fn first_occupied_square(&self) -> Option { let leading_zeros = self.0.leading_zeros() as u8; if leading_zeros < Square::NUM as u8 { @@ -167,6 +172,7 @@ impl BitBoard { } } + #[must_use] pub fn first_occupied_square_trailing(&self) -> Option { let trailing_zeros = self.0.trailing_zeros() as u8; if trailing_zeros < Square::NUM as u8 { diff --git a/bitboard/src/library.rs b/bitboard/src/library.rs index f87084b..7435435 100644 --- a/bitboard/src/library.rs +++ b/bitboard/src/library.rs @@ -12,17 +12,21 @@ use crate::BitBoard; use chessfriend_core::{Color, Direction, Square}; use std::sync::OnceLock; +#[allow(clippy::identity_op)] +#[allow(clippy::erasing_op)] pub(crate) const RANKS: [BitBoard; 8] = [ - BitBoard(0xFF << 0 * 8), - BitBoard(0xFF << 1 * 8), - BitBoard(0xFF << 2 * 8), - BitBoard(0xFF << 3 * 8), - BitBoard(0xFF << 4 * 8), - BitBoard(0xFF << 5 * 8), - BitBoard(0xFF << 6 * 8), - BitBoard(0xFF << 7 * 8), + BitBoard(0xFF << (0 * 8)), + BitBoard(0xFF << (1 * 8)), + BitBoard(0xFF << (2 * 8)), + BitBoard(0xFF << (3 * 8)), + BitBoard(0xFF << (4 * 8)), + BitBoard(0xFF << (5 * 8)), + BitBoard(0xFF << (6 * 8)), + BitBoard(0xFF << (7 * 8)), ]; +#[allow(clippy::identity_op)] +#[allow(clippy::erasing_op)] pub(crate) const FILES: [BitBoard; 8] = [ BitBoard(0x0101010101010101 << 0), BitBoard(0x0101010101010101 << 1), diff --git a/core/src/colors.rs b/core/src/colors.rs index 55757c1..62d7603 100644 --- a/core/src/colors.rs +++ b/core/src/colors.rs @@ -3,8 +3,9 @@ use crate::{errors::TryFromCharError, try_from_string}; use std::fmt; -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] pub enum Color { + #[default] White = 0, Black = 1, } @@ -16,7 +17,9 @@ impl Color { Color::ALL.iter() } - pub fn other(&self) -> Color { + /// The other color + #[must_use] + pub const fn other(&self) -> Color { match self { Color::White => Color::Black, Color::Black => Color::White, @@ -24,12 +27,6 @@ impl Color { } } -impl Default for Color { - fn default() -> Self { - Color::White - } -} - impl fmt::Display for Color { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( diff --git a/core/src/pieces.rs b/core/src/pieces.rs index 51ae727..d980105 100644 --- a/core/src/pieces.rs +++ b/core/src/pieces.rs @@ -34,7 +34,7 @@ impl Shape { PROMOTABLE_SHAPES.iter() } - const fn to_ascii(&self) -> char { + const fn to_ascii(self) -> char { match self { Shape::Pawn => 'P', Shape::Knight => 'N', @@ -79,7 +79,7 @@ impl Into for Shape { impl fmt::Display for Shape { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let self_char: char = self.into(); - write!(f, "{}", self_char) + write!(f, "{self_char}") } } @@ -91,6 +91,7 @@ pub struct Piece { macro_rules! piece_constructor { ($func_name:ident, $type:tt) => { + #[must_use] pub fn $func_name(color: Color) -> Piece { Piece { color, @@ -102,6 +103,7 @@ macro_rules! piece_constructor { macro_rules! is_shape { ($func_name:ident, $shape:ident) => { + #[must_use] pub fn $func_name(&self) -> bool { self.shape == Shape::$shape } @@ -109,6 +111,7 @@ macro_rules! is_shape { } impl Piece { + #[must_use] pub fn new(color: Color, shape: Shape) -> Piece { Piece { color, shape } } @@ -120,10 +123,12 @@ impl Piece { piece_constructor!(queen, Queen); piece_constructor!(king, King); + #[must_use] pub fn color(&self) -> Color { self.color } + #[must_use] pub fn shape(&self) -> Shape { self.shape } @@ -135,7 +140,8 @@ impl Piece { is_shape!(is_queen, Queen); is_shape!(is_king, King); - pub fn to_ascii(&self) -> char { + #[must_use] + pub fn to_ascii(self) -> char { let ch = self.shape.to_ascii(); match self.color { Color::White => ch, @@ -143,7 +149,8 @@ impl Piece { } } - fn to_unicode(&self) -> char { + #[must_use] + fn to_unicode(self) -> char { match (self.color, self.shape) { (Color::Black, Shape::Pawn) => '♟', (Color::Black, Shape::Knight) => '♞', @@ -175,6 +182,7 @@ pub struct PlacedPiece { macro_rules! is_shape { ($func_name:ident, $shape:ident) => { + #[must_use] pub fn $func_name(&self) -> bool { self.piece().shape == Shape::$shape } @@ -182,26 +190,35 @@ macro_rules! is_shape { } impl PlacedPiece { + #[must_use] pub const fn new(piece: Piece, square: Square) -> PlacedPiece { PlacedPiece { piece, square } } + /// The [Piece] itself #[inline] + #[must_use] pub fn piece(&self) -> &Piece { &self.piece } + /// The square the piece is on #[inline] + #[must_use] pub fn square(&self) -> Square { self.square } + /// The piece's [Color] #[inline] + #[must_use] pub fn color(&self) -> Color { self.piece.color } + /// The piece's [Shape] #[inline] + #[must_use] pub fn shape(&self) -> Shape { self.piece.shape } @@ -213,6 +230,7 @@ impl PlacedPiece { is_shape!(is_queen, Queen); is_shape!(is_king, King); + #[must_use] pub fn is_kingside_rook(&self) -> bool { self.is_rook() && match self.color() { @@ -221,6 +239,7 @@ impl PlacedPiece { } } + #[must_use] pub fn is_queenside_rook(&self) -> bool { self.is_rook() && match self.color() { diff --git a/position/src/check.rs b/position/src/check.rs index a3f2804..06baf74 100644 --- a/position/src/check.rs +++ b/position/src/check.rs @@ -1,10 +1,9 @@ // Eryn Wells +use crate::sight::SliderRayToSquareExt; use chessfriend_bitboard::BitBoard; use chessfriend_core::Shape; -use crate::sight::SliderRayToSquareExt; - #[derive(Clone, Debug, Eq, PartialEq)] pub struct CheckingPieces { bitboards: [BitBoard; 5], @@ -23,12 +22,12 @@ impl CheckingPieces { } } + /// The number of checking pieces. pub fn count(&self) -> u32 { self.bitboards.iter().map(BitBoard::population_count).sum() } - /// A BitBoard representing the set of pieces that must be captured to - /// resolve check. + /// A BitBoard representing the set of pieces that must be captured to resolve check. pub fn capture_mask(&self) -> BitBoard { if self.count() == 0 { BitBoard::FULL @@ -39,8 +38,8 @@ impl CheckingPieces { } } - /// A BitBoard representing the set of squares to which a player can move a - /// piece to block a checking piece. + /// A BitBoard representing the set of squares to which a player can move a piece to block a + /// checking piece. pub fn push_mask(&self, king: &BitBoard) -> BitBoard { let target = king.first_occupied_square().unwrap(); diff --git a/position/src/position/piece_sets.rs b/position/src/position/piece_sets.rs index b998600..fee1a3f 100644 --- a/position/src/position/piece_sets.rs +++ b/position/src/position/piece_sets.rs @@ -78,7 +78,7 @@ impl PieceBitBoards { } pub(super) fn place_piece(&mut self, piece: &PlacedPiece) -> Result<(), PlacePieceError> { - self.place_piece_with_strategy(piece, Default::default()) + self.place_piece_with_strategy(piece, PlacePieceStrategy::default()) } pub(super) fn place_piece_with_strategy( diff --git a/position/src/position/position.rs b/position/src/position/position.rs index 383e998..cad7b2a 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -138,22 +138,27 @@ impl Position { }) } - /// Return a BitBoard representing the set of squares containing a piece. + /// A [BitBoard] representing the set of squares containing a piece. #[inline] + #[must_use] pub(crate) fn occupied_squares(&self) -> &BitBoard { &self.pieces.all_pieces() } #[inline] + #[must_use] pub(crate) fn friendly_pieces(&self) -> &BitBoard { self.pieces.all_pieces_of_color(self.color_to_move) } #[inline] + #[must_use] pub(crate) fn opposing_pieces(&self) -> &BitBoard { self.pieces.all_pieces_of_color(self.color_to_move.other()) } + #[inline] + #[must_use] pub(crate) fn all_pieces(&self) -> (&BitBoard, &BitBoard) { (self.friendly_pieces(), self.opposing_pieces()) } @@ -161,6 +166,7 @@ impl Position { /// Return a BitBoard representing the set of squares containing a piece. /// This set is the inverse of `occupied_squares`. #[inline] + #[must_use] pub(crate) fn empty_squares(&self) -> BitBoard { !self.occupied_squares() } From 21b58a6422a73d73b49f9622fffa3ebb0e8e87b1 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 14 Mar 2024 17:01:28 -0700 Subject: [PATCH 201/423] [position] Update the danger_squares unit test to use assert_eq_bitboards!() --- position/src/position/position.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/position/src/position/position.rs b/position/src/position/position.rs index cad7b2a..f7e05d7 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -379,7 +379,7 @@ impl fmt::Display for Position { #[cfg(test)] mod tests { use super::*; - use crate::{position, test_position, Position, PositionBuilder}; + use crate::{assert_eq_bitboards, position, test_position, Position, PositionBuilder}; use chessfriend_bitboard::bitboard; use chessfriend_core::piece; @@ -474,10 +474,6 @@ mod tests { let danger_squares = pos.king_danger(Color::Black); let expected = bitboard![D1, F1, D2, E2, F2, E3, A4, B4, C4, D4, F4, G4, H4, E5, E6, E7, E8]; - assert_eq!( - danger_squares, expected, - "Actual:\n{}\n\nExpected:\n{}", - danger_squares, expected - ); + assert_eq_bitboards!(danger_squares, expected); } } From 1f3c90ff35320973201ec565720d1a5d9bfe258f Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 25 Mar 2024 10:38:02 -0700 Subject: [PATCH 202/423] [position] Print the chess board diagram with box drawing characters --- position/src/position/diagram_formatter.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/position/src/position/diagram_formatter.rs b/position/src/position/diagram_formatter.rs index 9dfb5c7..64eb507 100644 --- a/position/src/position/diagram_formatter.rs +++ b/position/src/position/diagram_formatter.rs @@ -14,23 +14,23 @@ impl<'a> DiagramFormatter<'a> { impl<'a> fmt::Display for DiagramFormatter<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, " +-----------------+\n")?; + write!(f, " ╔═════════════════╗\n")?; for rank in Rank::ALL.iter().rev() { - write!(f, "{rank} | ")?; + write!(f, "{rank} ║ ")?; for file in File::ALL.iter() { let square = Square::from_file_rank(*file, *rank); match self.0.piece_on_square(square) { Some(placed_piece) => write!(f, "{} ", placed_piece.piece())?, - None => write!(f, ". ")?, + None => write!(f, "· ")?, } } - write!(f, "|\n")?; + write!(f, "║\n")?; } - write!(f, " +-----------------+\n")?; + write!(f, " ╚═════════════════╝\n")?; write!(f, " a b c d e f g h\n")?; Ok(()) From cad040e4545758fb139da7b10d8c67c0cbb660b2 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 25 Apr 2024 07:23:59 -0700 Subject: [PATCH 203/423] [bitboard] Fix some warnings and clippy suggestions in library.rs - Add allow(dead_code) to LIGHT_SQUARES and DARK_SQUARES. These aren't used yet but I have a feeling they'll come in handy. - Add some separators to long numeric literals. - Lightly reformat BitBoard -> Bitboard in the module documentation --- bitboard/src/library.rs | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/bitboard/src/library.rs b/bitboard/src/library.rs index 7435435..c92388a 100644 --- a/bitboard/src/library.rs +++ b/bitboard/src/library.rs @@ -1,12 +1,12 @@ // Eryn Wells -//! # The BitBoard Library +//! # The Bitboard Library //! -//! This module implements a collection of commonly used BitBoards that can be +//! This module implements a collection of commonly used bitboards that can be //! looked up efficiently as needed. //! //! The `library()` method returns a static instance of a `Library`, which -//! provides getters for all available BitBoards. +//! provides getters for all available bitboards. use crate::BitBoard; use chessfriend_core::{Color, Direction, Square}; @@ -28,29 +28,35 @@ pub(crate) const RANKS: [BitBoard; 8] = [ #[allow(clippy::identity_op)] #[allow(clippy::erasing_op)] pub(crate) const FILES: [BitBoard; 8] = [ - BitBoard(0x0101010101010101 << 0), - BitBoard(0x0101010101010101 << 1), - BitBoard(0x0101010101010101 << 2), - BitBoard(0x0101010101010101 << 3), - BitBoard(0x0101010101010101 << 4), - BitBoard(0x0101010101010101 << 5), - BitBoard(0x0101010101010101 << 6), - BitBoard(0x0101010101010101 << 7), + BitBoard(0x0101_0101_0101_0101 << 0), + BitBoard(0x0101_0101_0101_0101 << 1), + BitBoard(0x0101_0101_0101_0101 << 2), + BitBoard(0x0101_0101_0101_0101 << 3), + BitBoard(0x0101_0101_0101_0101 << 4), + BitBoard(0x0101_0101_0101_0101 << 5), + BitBoard(0x0101_0101_0101_0101 << 6), + BitBoard(0x0101_0101_0101_0101 << 7), ]; /// Bitboards representing the kingside of the board, per color. -pub(crate) const KINGSIDES: [BitBoard; 2] = - [BitBoard(0xF0F0F0F0F0F0F0F0), BitBoard(0x0F0F0F0F0F0F0F0F)]; +pub(crate) const KINGSIDES: [BitBoard; 2] = [ + BitBoard(0xF0F0_F0F0_F0F0_F0F0), + BitBoard(0x0F0F_0F0F_0F0F_0F0F), +]; /// Bitboards representing the queenside of the board, per color. -pub(crate) const QUEENSIDES: [BitBoard; 2] = - [BitBoard(0x0F0F0F0F0F0F0F0F), BitBoard(0xF0F0F0F0F0F0F0F0)]; +pub(crate) const QUEENSIDES: [BitBoard; 2] = [ + BitBoard(0x0F0F_0F0F_0F0F_0F0F), + BitBoard(0xF0F0_F0F0_F0F0_F0F0), +]; /// A bitboard representing the light squares. +#[allow(dead_code)] pub(crate) const LIGHT_SQUARES: BitBoard = BitBoard(0x5555 | 0x5555 << 16 | 0x5555 << 32 | 0x5555 << 48); /// A bitboad representing the dark squares +#[allow(dead_code)] pub(crate) const DARK_SQUARES: BitBoard = BitBoard(!LIGHT_SQUARES.0); pub(super) fn library() -> &'static MoveLibrary { From 1b63f56042cff25273b33cb8a091e155781ce504 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 25 Apr 2024 07:56:53 -0700 Subject: [PATCH 204/423] [bitboard, core] Make library getters const; parameterize Library attributes - Add chessfriend_core::Color::NUM - All the library getters can be const. - Use the constants from the core library to define the length of the slices in the Library struct. --- bitboard/src/library.rs | 59 ++++++++++++++++++++--------------------- core/src/colors.rs | 6 ++++- 2 files changed, 34 insertions(+), 31 deletions(-) diff --git a/bitboard/src/library.rs b/bitboard/src/library.rs index c92388a..7a45906 100644 --- a/bitboard/src/library.rs +++ b/bitboard/src/library.rs @@ -74,7 +74,7 @@ pub(super) fn library() -> &'static MoveLibrary { macro_rules! library_getter { ($name:ident) => { - pub(super) fn $name(&self, sq: Square) -> BitBoard { + pub(super) const fn $name(&self, sq: Square) -> BitBoard { self.$name[sq as usize] } }; @@ -83,29 +83,29 @@ macro_rules! library_getter { #[derive(Debug)] pub(super) struct MoveLibrary { // Rays - rays: [[BitBoard; 8]; Square::NUM], + rays: [[BitBoard; Direction::NUM]; Square::NUM], // Piecewise move tables - pawn_attacks: [[BitBoard; 64]; 2], - pawn_pushes: [[BitBoard; 64]; 2], - knight_moves: [BitBoard; 64], - bishop_moves: [BitBoard; 64], - rook_moves: [BitBoard; 64], - queen_moves: [BitBoard; 64], - king_moves: [BitBoard; 64], + pawn_attacks: [[BitBoard; Square::NUM]; Color::NUM], + pawn_pushes: [[BitBoard; Square::NUM]; Color::NUM], + knight_moves: [BitBoard; Square::NUM], + bishop_moves: [BitBoard; Square::NUM], + rook_moves: [BitBoard; Square::NUM], + queen_moves: [BitBoard; Square::NUM], + king_moves: [BitBoard; Square::NUM], } impl MoveLibrary { const fn new() -> MoveLibrary { MoveLibrary { - rays: [[BitBoard::empty(); 8]; Square::NUM], - pawn_attacks: [[BitBoard::empty(); 64]; 2], - pawn_pushes: [[BitBoard::empty(); 64]; 2], - knight_moves: [BitBoard::empty(); 64], - bishop_moves: [BitBoard::empty(); 64], - rook_moves: [BitBoard::empty(); 64], - queen_moves: [BitBoard::empty(); 64], - king_moves: [BitBoard::empty(); 64], + rays: [[BitBoard::empty(); Direction::NUM]; Square::NUM], + pawn_attacks: [[BitBoard::empty(); Square::NUM]; Color::NUM], + pawn_pushes: [[BitBoard::empty(); Square::NUM]; Color::NUM], + knight_moves: [BitBoard::empty(); Square::NUM], + bishop_moves: [BitBoard::empty(); Square::NUM], + rook_moves: [BitBoard::empty(); Square::NUM], + queen_moves: [BitBoard::empty(); Square::NUM], + king_moves: [BitBoard::empty(); Square::NUM], } } @@ -125,23 +125,23 @@ impl MoveLibrary { fn init_orthogonal_rays(&mut self, sq: Square) { let sq_bb: BitBoard = sq.into(); let rays = &mut self.rays[sq as usize]; - rays[Direction::North as usize] = Self::generate_ray(sq_bb, BitBoard::shift_north_one); - rays[Direction::South as usize] = Self::generate_ray(sq_bb, BitBoard::shift_south_one); - rays[Direction::East as usize] = Self::generate_ray(sq_bb, BitBoard::shift_east_one); - rays[Direction::West as usize] = Self::generate_ray(sq_bb, BitBoard::shift_west_one); + rays[Direction::North as usize] = Self::_generate_ray(sq_bb, BitBoard::shift_north_one); + rays[Direction::South as usize] = Self::_generate_ray(sq_bb, BitBoard::shift_south_one); + rays[Direction::East as usize] = Self::_generate_ray(sq_bb, BitBoard::shift_east_one); + rays[Direction::West as usize] = Self::_generate_ray(sq_bb, BitBoard::shift_west_one); } fn init_diagonal_rays(&mut self, sq: Square) { let sq_bb: BitBoard = sq.into(); let rays = &mut self.rays[sq as usize]; rays[Direction::NorthEast as usize] = - Self::generate_ray(sq_bb, BitBoard::shift_north_east_one); + Self::_generate_ray(sq_bb, BitBoard::shift_north_east_one); rays[Direction::NorthWest as usize] = - Self::generate_ray(sq_bb, BitBoard::shift_north_west_one); + Self::_generate_ray(sq_bb, BitBoard::shift_north_west_one); rays[Direction::SouthWest as usize] = - Self::generate_ray(sq_bb, BitBoard::shift_south_west_one); + Self::_generate_ray(sq_bb, BitBoard::shift_south_west_one); rays[Direction::SouthEast as usize] = - Self::generate_ray(sq_bb, BitBoard::shift_south_east_one); + Self::_generate_ray(sq_bb, BitBoard::shift_south_east_one); } fn init_king_moves(&mut self, idx: usize) { @@ -225,8 +225,7 @@ impl MoveLibrary { }; } - #[inline] - fn generate_ray(sq: BitBoard, shift: fn(&BitBoard) -> BitBoard) -> BitBoard { + fn _generate_ray(sq: BitBoard, shift: fn(&BitBoard) -> BitBoard) -> BitBoard { let mut ray = BitBoard::empty(); let mut iter = shift(&sq); @@ -238,15 +237,15 @@ impl MoveLibrary { ray } - pub(super) fn ray(&self, sq: Square, dir: Direction) -> &BitBoard { + pub(super) const fn ray(&self, sq: Square, dir: Direction) -> &BitBoard { &self.rays[sq as usize][dir as usize] } - pub(super) fn pawn_pushes(&self, sq: Square, color: Color) -> BitBoard { + pub(super) const fn pawn_pushes(&self, sq: Square, color: Color) -> BitBoard { self.pawn_pushes[color as usize][sq as usize] } - pub(super) fn pawn_attacks(&self, sq: Square, color: Color) -> BitBoard { + pub(super) const fn pawn_attacks(&self, sq: Square, color: Color) -> BitBoard { self.pawn_attacks[color as usize][sq as usize] } diff --git a/core/src/colors.rs b/core/src/colors.rs index 62d7603..b60531b 100644 --- a/core/src/colors.rs +++ b/core/src/colors.rs @@ -11,7 +11,11 @@ pub enum Color { } impl Color { - pub const ALL: [Color; 2] = [Color::White, Color::Black]; + /// Number of colors + pub const NUM: usize = 2; + + /// Slice of all possible colors + pub const ALL: [Color; Color::NUM] = [Color::White, Color::Black]; pub fn iter() -> impl Iterator { Color::ALL.iter() From a2d0c638d024f7cd9f60b3053a5e132be8a406ca Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 25 Apr 2024 08:05:07 -0700 Subject: [PATCH 205/423] [core] Address clippy suggestions; clean up unit tests In coordinates.rs: - Add some [must_use] decorators to some getters - Rewrite some unit tests to remove the .expect() and use ? instead --- core/src/coordinates.rs | 46 +++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/core/src/coordinates.rs b/core/src/coordinates.rs index b0c67ee..4805a87 100644 --- a/core/src/coordinates.rs +++ b/core/src/coordinates.rs @@ -59,6 +59,7 @@ macro_rules! range_bound_struct { } impl $type { + #[must_use] $vis fn new(x: $repr) -> Option { if x < $max { Some(Self(x)) @@ -73,14 +74,21 @@ macro_rules! range_bound_struct { /// /// This function does not perform any bounds checking. It should only be called when /// the input is already known to be within bounds, i.e. when `x >= Self::FIRST && x < Self::LAST`. + #[must_use] $vis unsafe fn new_unchecked(x: $repr) -> Self { debug_assert!((Self::FIRST.0..=Self::LAST.0).contains(&x)); Self(x) } + #[must_use] $vis fn as_index(&self) -> &$repr { &self.0 } + + #[must_use] + $vis fn iter(&self) -> impl Iterator { + (Self::FIRST.0..=Self::LAST.0).map(Self) + } } impl From<$type> for $repr { @@ -105,11 +113,13 @@ coordinate_enum!( ); impl Direction { + #[must_use] pub fn to_offset(&self) -> i8 { const OFFSETS: [i8; 8] = [8, 9, 1, -7, -8, -9, -1, 7]; OFFSETS[*self as usize] } + #[must_use] pub fn opposite(&self) -> Direction { const OPPOSITES: [Direction; 8] = [ Direction::South, @@ -228,7 +238,7 @@ impl Square { #[inline] pub fn file(self) -> File { - unsafe { File::new_unchecked((self as u8) & 0b000111) } + unsafe { File::new_unchecked((self as u8) & 0b000_00111) } } #[inline] @@ -374,39 +384,45 @@ mod tests { } #[test] - fn good_algebraic_input() { - let sq = Square::from_algebraic_str("a4").expect("Failed to parse 'a4' square"); + fn good_algebraic_input() -> Result<(), ParseSquareError> { + let sq = Square::from_algebraic_str("a4")?; assert_eq!(sq.file(), File(0)); assert_eq!(sq.rank(), Rank(3)); - let sq = Square::from_algebraic_str("B8").expect("Failed to parse 'B8' square"); + let sq = Square::from_algebraic_str("B8")?; assert_eq!(sq.file(), File(1)); assert_eq!(sq.rank(), Rank(7)); - let sq = Square::from_algebraic_str("e4").expect("Failed to parse 'B8' square"); + let sq = Square::from_algebraic_str("e4")?; assert_eq!(sq.file(), File(4)); assert_eq!(sq.rank(), Rank(3)); + + Ok(()) } #[test] - fn bad_algebraic_input() { - Square::from_algebraic_str("a0").expect_err("Got valid Square for 'a0'"); - Square::from_algebraic_str("j3").expect_err("Got valid Square for 'j3'"); - Square::from_algebraic_str("a11").expect_err("Got valid Square for 'a11'"); - Square::from_algebraic_str("b-1").expect_err("Got valid Square for 'b-1'"); - Square::from_algebraic_str("a 1").expect_err("Got valid Square for 'a 1'"); - Square::from_algebraic_str("").expect_err("Got valid Square for ''"); + fn bad_algebraic_input() -> Result<(), ParseSquareError> { + Square::from_algebraic_str("a0")?; + Square::from_algebraic_str("j3")?; + Square::from_algebraic_str("a11")?; + Square::from_algebraic_str("b-1")?; + Square::from_algebraic_str("a 1")?; + Square::from_algebraic_str("")?; + + Ok(()) } #[test] - fn from_index() { - let sq = Square::try_from(4u32).expect("Unable to get Square from index"); + fn from_index() -> Result<(), ()> { + let sq = Square::try_from(4u32)?; assert_eq!(sq.file(), File(4)); assert_eq!(sq.rank(), Rank(0)); - let sq = Square::try_from(28u32).expect("Unable to get Square from index"); + let sq = Square::try_from(28u32)?; assert_eq!(sq.file(), File(4)); assert_eq!(sq.rank(), Rank(3)); + + Ok(()) } #[test] From 797606785e7c28115c77638ff961ae0beeeda76e Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 25 Apr 2024 09:32:27 -0700 Subject: [PATCH 206/423] Empty board package --- Cargo.toml | 1 + board/Cargo.toml | 8 ++++++++ board/src/lib.rs | 14 ++++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 board/Cargo.toml create mode 100644 board/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 490ec43..70558c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "bitboard", + "board", "core", "explorer", "moves", diff --git a/board/Cargo.toml b/board/Cargo.toml new file mode 100644 index 0000000..6b600a9 --- /dev/null +++ b/board/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "board" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/board/src/lib.rs b/board/src/lib.rs new file mode 100644 index 0000000..7d12d9a --- /dev/null +++ b/board/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: usize, right: usize) -> usize { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} From 1d82d27f842fd39ffe6e7ba4722f0e118a075800 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 25 Apr 2024 13:28:24 -0700 Subject: [PATCH 207/423] Move a whole bunch of stuff to the new chessfriend_board package --- Cargo.lock | 8 + board/Cargo.toml | 4 +- board/src/board.rs | 290 +++++++++++++++++++++++++++++++++ board/src/builder.rs | 179 ++++++++++++++++++++ {moves => board}/src/castle.rs | 56 +++---- board/src/display.rs | 68 ++++++++ board/src/en_passant.rs | 48 ++++++ board/src/flags.rs | 90 ++++++++++ board/src/lib.rs | 31 ++-- board/src/macros.rs | 96 +++++++++++ board/src/piece_sets.rs | 174 ++++++++++++++++++++ board/src/pieces.rs | 127 +++++++++++++++ 12 files changed, 1130 insertions(+), 41 deletions(-) create mode 100644 board/src/board.rs create mode 100644 board/src/builder.rs rename {moves => board}/src/castle.rs (62%) create mode 100644 board/src/display.rs create mode 100644 board/src/en_passant.rs create mode 100644 board/src/flags.rs create mode 100644 board/src/macros.rs create mode 100644 board/src/piece_sets.rs create mode 100644 board/src/pieces.rs diff --git a/Cargo.lock b/Cargo.lock index 53be692..50995a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -69,6 +69,14 @@ dependencies = [ "chessfriend_core", ] +[[package]] +name = "chessfriend_board" +version = "0.1.0" +dependencies = [ + "chessfriend_bitboard", + "chessfriend_core", +] + [[package]] name = "chessfriend_core" version = "0.1.0" diff --git a/board/Cargo.toml b/board/Cargo.toml index 6b600a9..54a0dd4 100644 --- a/board/Cargo.toml +++ b/board/Cargo.toml @@ -1,8 +1,10 @@ [package] -name = "board" +name = "chessfriend_board" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +chessfriend_bitboard = { path = "../bitboard" } +chessfriend_core = { path = "../core" } diff --git a/board/src/board.rs b/board/src/board.rs new file mode 100644 index 0000000..94c2d97 --- /dev/null +++ b/board/src/board.rs @@ -0,0 +1,290 @@ +// Eryn Wells + +use crate::{display::DiagramFormatter, Castle, EnPassant, Flags, PieceBitBoards, Pieces}; +use chessfriend_bitboard::BitBoard; +use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; + +#[derive(Clone, Debug, Eq)] +pub struct Board { + player_to_move: Color, + flags: Flags, + pieces: PieceBitBoards, + en_passant: Option, + half_move_counter: u16, + full_move_number: u16, +} + +impl Board { + /// An empty board + #[must_use] + pub fn empty() -> Self { + Board::default() + } + + /// The starting position + #[must_use] + pub fn starting() -> Self { + const BLACK_PIECES: [BitBoard; Shape::NUM] = [ + BitBoard::new(0b0000_0000_1111_1111 << 48), + BitBoard::new(0b0100_0010_0000_0000 << 48), + BitBoard::new(0b0010_0100_0000_0000 << 48), + BitBoard::new(0b1000_0001_0000_0000 << 48), + BitBoard::new(0b0000_1000_0000_0000 << 48), + BitBoard::new(0b0001_0000_0000_0000 << 48), + ]; + + const WHITE_PIECES: [BitBoard; Shape::NUM] = [ + BitBoard::new(0b1111_1111_0000_0000), + BitBoard::new(0b0000_0000_0100_0010), + BitBoard::new(0b0000_0000_0010_0100), + BitBoard::new(0b0000_0000_1000_0001), + BitBoard::new(0b0000_0000_0000_1000), + BitBoard::new(0b0000_0000_0001_0000), + ]; + + Self { + player_to_move: Color::White, + pieces: PieceBitBoards::new([WHITE_PIECES, BLACK_PIECES]), + ..Default::default() + } + } + + pub(crate) fn new( + player_to_move: Color, + flags: Flags, + pieces: PieceBitBoards, + en_passant: Option, + half_move_counter: u16, + full_move_number: u16, + ) -> Self { + Self { + player_to_move, + flags, + pieces, + en_passant, + half_move_counter, + full_move_number, + } + } + + #[must_use] + pub fn player_to_move(&self) -> Color { + self.player_to_move + } + + #[must_use] + pub fn move_number(&self) -> u16 { + self.full_move_number + } + + #[must_use] + pub fn ply_counter(&self) -> u16 { + self.half_move_counter + } + + #[must_use] + pub(crate) fn flags(&self) -> &Flags { + &self.flags + } + + /// Returns `true` if the player has the right to castle on the given side of the board. + /// + /// The right to castle on a particular side of the board is retained as long as the player has + /// not moved their king, or the rook on that side of the board. + #[must_use] + pub fn player_has_right_to_castle(&self, color: Color, castle: Castle) -> bool { + self.flags.player_has_right_to_castle(color, castle) + } + + /// The rook to use for a castling move. + #[must_use] + pub fn rook_for_castle(&self, player: Color, castle: Castle) -> Option { + let square = castle.parameters(player).rook_origin_square(); + self.piece_on_square(square) + } + + /// A [`BitBoard`] representing the set of squares containing a piece. + #[inline] + #[must_use] + pub fn occupied_squares(&self) -> &BitBoard { + self.pieces.all_pieces() + } + + #[inline] + #[must_use] + pub fn friendly_pieces(&self) -> &BitBoard { + self.pieces.all_pieces_of_color(self.player_to_move) + } + + #[inline] + #[must_use] + pub fn opposing_pieces(&self) -> &BitBoard { + self.pieces.all_pieces_of_color(self.player_to_move.other()) + } + + #[inline] + #[must_use] + pub fn all_pieces(&self) -> (&BitBoard, &BitBoard) { + (self.friendly_pieces(), self.opposing_pieces()) + } + + /// A [`BitBoard`] representing the set of squares containing a piece. This set is the inverse of + /// `Board::occupied_squares`. + #[inline] + #[must_use] + pub fn empty_squares(&self) -> BitBoard { + !self.occupied_squares() + } + + #[must_use] + pub fn piece_on_square(&self, sq: Square) -> Option { + for color in Color::iter() { + for shape in Shape::iter() { + let piece = Piece::new(*color, *shape); + if self.pieces.bitboard_for_piece(&piece).is_set(sq) { + return Some(PlacedPiece::new(piece, sq)); + } + } + } + + None + } + + #[must_use] + pub fn pieces(&self, color: Color) -> Pieces { + Pieces::new(self, color) + } + + #[must_use] + pub fn has_en_passant_square(&self) -> bool { + self.en_passant.is_some() + } + + #[must_use] + pub fn en_passant(&self) -> Option { + self.en_passant + } + + fn king_bitboard(&self, player: Color) -> &BitBoard { + self.pieces.bitboard_for_piece(&Piece::king(player)) + } + + pub(crate) fn king_square(&self, player: Color) -> Square { + self.king_bitboard(player) + .occupied_squares() + .next() + .unwrap() + } + + #[must_use] + pub fn display(&self) -> DiagramFormatter<'_> { + DiagramFormatter::new(self) + } + + #[must_use] + pub fn bitboard_for_color(&self, color: Color) -> &BitBoard { + self.pieces.bitboard_for_color(color) + } + + #[must_use] + pub fn bitboard_for_piece(&self, piece: Piece) -> &BitBoard { + self.pieces.bitboard_for_piece(&piece) + } +} + +#[cfg(test)] +impl Board { + pub(crate) fn test_set_en_passant(&mut self, en_passant: EnPassant) { + self.en_passant = Some(en_passant); + } +} + +impl Default for Board { + fn default() -> Self { + Self { + player_to_move: Color::White, + flags: Flags::default(), + pieces: PieceBitBoards::default(), + en_passant: None, + half_move_counter: 0, + full_move_number: 1, + } + } +} + +impl PartialEq for Board { + fn eq(&self, other: &Self) -> bool { + self.pieces == other.pieces + && self.player_to_move == other.player_to_move + && self.flags == other.flags + && self.en_passant == other.en_passant + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_board; + use chessfriend_core::piece; + + #[test] + fn piece_on_square() { + let pos = test_board![ + Black Bishop on F7, + ]; + + let piece = pos.piece_on_square(Square::F7); + assert_eq!(piece, Some(piece!(Black Bishop on F7))); + } + + #[test] + fn piece_in_starting_position() { + let board = test_board!(starting); + + assert_eq!( + board.piece_on_square(Square::H1), + Some(piece!(White Rook on H1)) + ); + assert_eq!( + board.piece_on_square(Square::A8), + Some(piece!(Black Rook on A8)) + ); + } + + #[test] + fn king_not_on_starting_square_cannot_castle() { + let board = test_board!(White King on E4); + assert!(!board.player_has_right_to_castle(Color::White, Castle::KingSide)); + assert!(!board.player_has_right_to_castle(Color::White, Castle::QueenSide)); + } + + #[test] + fn king_on_starting_square_can_castle() { + let board = test_board!( + White King on E1, + White Rook on A1, + White Rook on H1 + ); + + assert!(board.player_has_right_to_castle(Color::White, Castle::KingSide)); + assert!(board.player_has_right_to_castle(Color::White, Castle::QueenSide)); + } + + #[test] + fn rook_for_castle() { + let board = test_board![ + White King on E1, + White Rook on H1, + White Rook on A1, + ]; + + assert_eq!( + board.rook_for_castle(Color::White, Castle::KingSide), + Some(piece!(White Rook on H1)) + ); + assert_eq!( + board.rook_for_castle(Color::White, Castle::QueenSide), + Some(piece!(White Rook on A1)) + ); + } +} diff --git a/board/src/builder.rs b/board/src/builder.rs new file mode 100644 index 0000000..f52d18c --- /dev/null +++ b/board/src/builder.rs @@ -0,0 +1,179 @@ +// Eryn Wells + +use crate::{Board, Castle, EnPassant, Flags, PieceBitBoards}; +use chessfriend_core::{piece, Color, Piece, PlacedPiece, Rank, Shape, Square}; +use std::collections::BTreeMap; + +#[derive(Clone)] +pub struct Builder { + player_to_move: Color, + flags: Flags, + pieces: BTreeMap, + kings: [Option; 2], + en_passant: Option, + ply_counter: u16, + move_number: u16, +} + +impl Builder { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + #[must_use] + pub fn from_board(board: &Board) -> Self { + let pieces = board + .pieces(Color::White) + .chain(board.pieces(Color::Black)) + .map(|placed_piece| (placed_piece.square(), *placed_piece.piece())) + .collect::>(); + + let white_king = board.king_square(Color::White); + let black_king = board.king_square(Color::Black); + + Self { + player_to_move: board.player_to_move(), + flags: *board.flags(), + pieces, + kings: [Some(white_king), Some(black_king)], + en_passant: board.en_passant(), + ply_counter: board.ply_counter(), + move_number: board.move_number(), + } + } + + pub fn to_move(&mut self, player: Color) -> &mut Self { + self.player_to_move = player; + self + } + + pub fn ply_counter(&mut self, num: u16) -> &mut Self { + self.ply_counter = num; + self + } + + pub fn move_number(&mut self, num: u16) -> &mut Self { + self.move_number = num; + self + } + + pub fn en_passant(&mut self, en_passant: Option) -> &mut Self { + self.en_passant = en_passant; + self + } + + pub fn place_piece(&mut self, piece: PlacedPiece) -> &mut Self { + let square = piece.square(); + let shape = piece.shape(); + + if shape == Shape::King { + let color = piece.color(); + let color_index: usize = color as usize; + + if let Some(king_square) = self.kings[color_index] { + self.pieces.remove(&king_square); + } + self.kings[color_index] = Some(square); + } + + self.pieces.insert(square, *piece.piece()); + + self + } + + pub fn player_can_castle(&mut self, color: Color, castle: Castle) -> &mut Self { + self.flags + .set_player_has_right_to_castle_flag(color, castle); + self + } + + pub fn no_castling_rights(&mut self) -> &mut Self { + self.flags.clear_all_castling_rights(); + self + } + + pub fn build(&self) -> Board { + let pieces = self + .pieces + .iter() + .map(PlacedPiece::from) + .filter(Self::is_piece_placement_valid) + .collect::(); + + let mut flags = self.flags; + + for color in Color::ALL { + for castle in Castle::ALL { + let parameters = castle.parameters(color); + let has_rook_on_starting_square = self + .pieces + .get(¶meters.rook_origin_square()) + .is_some_and(|piece| piece.shape() == Shape::Rook); + let king_is_on_starting_square = + self.kings[color as usize] == Some(parameters.king_origin_square()); + + if !king_is_on_starting_square || !has_rook_on_starting_square { + flags.clear_player_has_right_to_castle_flag(color, castle); + } + } + } + + Board::new( + self.player_to_move, + flags, + pieces, + self.en_passant, + self.ply_counter, + self.move_number, + ) + } +} + +impl Builder { + fn is_piece_placement_valid(piece: &PlacedPiece) -> bool { + if piece.shape() == Shape::Pawn { + // Pawns cannot be placed on the first (back) rank of their side, + // and cannot be placed on the final rank without a promotion. + let rank = piece.square().rank(); + return rank != Rank::ONE && rank != Rank::EIGHT; + } + + true + } +} + +impl Default for Builder { + fn default() -> Self { + let white_king_square = Square::E1; + let black_king_square = Square::E8; + + let pieces = BTreeMap::from_iter([ + (white_king_square, piece!(White King)), + (black_king_square, piece!(Black King)), + ]); + + Self { + player_to_move: Color::White, + flags: Flags::default(), + pieces, + kings: [Some(white_king_square), Some(black_king_square)], + en_passant: None, + ply_counter: 0, + move_number: 1, + } + } +} + +#[cfg(test)] +mod tests { + use crate::Builder; + use chessfriend_core::piece; + + #[test] + fn place_piece() { + let piece = piece!(White Queen on E4); + let builder = Builder::new().place_piece(piece).build(); + assert_eq!(builder.piece_on_square(piece.square()), Some(piece)); + } +} diff --git a/moves/src/castle.rs b/board/src/castle.rs similarity index 62% rename from moves/src/castle.rs rename to board/src/castle.rs index 731f25f..17b2942 100644 --- a/moves/src/castle.rs +++ b/board/src/castle.rs @@ -12,41 +12,41 @@ pub enum Castle { pub struct Parameters { /// Origin squares of the king and rook. - origin_squares: Squares, + origin: Squares, /// Target or destination squares for the king and rook. - target_squares: Squares, + target: Squares, /// The set of squares that must be clear of any pieces in order to perform this castle. - clear_squares: BitBoard, + clear: BitBoard, /// The set of squares that must not be attacked in order to perform this castle. - check_squares: BitBoard, + check: BitBoard, } impl Parameters { pub fn king_origin_square(&self) -> Square { - self.origin_squares.king + self.origin.king } pub fn rook_origin_square(&self) -> Square { - self.origin_squares.rook + self.origin.rook } pub fn king_target_square(&self) -> Square { - self.target_squares.king + self.target.king } pub fn rook_target_square(&self) -> Square { - self.target_squares.rook + self.target.rook } pub fn clear_squares(&self) -> &BitBoard { - &self.clear_squares + &self.clear } pub fn check_squares(&self) -> &BitBoard { - &self.check_squares + &self.check } } @@ -63,59 +63,59 @@ impl Castle { const PARAMETERS: [[Parameters; 2]; 2] = [ [ Parameters { - origin_squares: Squares { + origin: Squares { king: Square::E1, rook: Square::H1, }, - target_squares: Squares { + target: Squares { king: Square::G1, rook: Square::F1, }, - clear_squares: BitBoard::new(0b01100000), - check_squares: BitBoard::new(0b01110000), + clear: BitBoard::new(0b0110_0000), + check: BitBoard::new(0b0111_0000), }, Parameters { - origin_squares: Squares { + origin: Squares { king: Square::E1, rook: Square::A1, }, - target_squares: Squares { + target: Squares { king: Square::C1, rook: Square::D1, }, - clear_squares: BitBoard::new(0b00001110), - check_squares: BitBoard::new(0b00011100), + clear: BitBoard::new(0b0000_1110), + check: BitBoard::new(0b0001_1100), }, ], [ Parameters { - origin_squares: Squares { + origin: Squares { king: Square::E8, rook: Square::H8, }, - target_squares: Squares { + target: Squares { king: Square::G8, rook: Square::F8, }, - clear_squares: BitBoard::new(0b01100000 << 8 * 7), - check_squares: BitBoard::new(0b01110000 << 8 * 7), + clear: BitBoard::new(0b0110_0000 << (8 * 7)), + check: BitBoard::new(0b0111_0000 << (8 * 7)), }, Parameters { - origin_squares: Squares { + origin: Squares { king: Square::E8, rook: Square::A8, }, - target_squares: Squares { + target: Squares { king: Square::C8, rook: Square::D8, }, - clear_squares: BitBoard::new(0b00001110 << 8 * 7), - check_squares: BitBoard::new(0b00011100 << 8 * 7), + clear: BitBoard::new(0b0000_1110 << (8 * 7)), + check: BitBoard::new(0b0001_1100 << (8 * 7)), }, ], ]; - pub fn parameters(&self, color: Color) -> &'static Parameters { - &Castle::PARAMETERS[color as usize][*self as usize] + pub fn parameters(self, color: Color) -> &'static Parameters { + &Castle::PARAMETERS[color as usize][self as usize] } } diff --git a/board/src/display.rs b/board/src/display.rs new file mode 100644 index 0000000..053f9a0 --- /dev/null +++ b/board/src/display.rs @@ -0,0 +1,68 @@ +// Eryn Wells + +use crate::Board; +use chessfriend_core::{File, Rank, Square}; +use std::fmt; + +pub struct DiagramFormatter<'a>(&'a Board); + +impl<'a> DiagramFormatter<'a> { + pub fn new(board: &'a Board) -> Self { + Self(board) + } +} + +impl<'a> fmt::Display for DiagramFormatter<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, " ╔═════════════════╗")?; + + for rank in Rank::ALL.into_iter().rev() { + write!(f, "{rank} ║ ")?; + + for file in File::ALL { + let square = Square::from_file_rank(file, rank); + match self.0.piece_on_square(square) { + Some(placed_piece) => write!(f, "{} ", placed_piece.piece())?, + None => write!(f, "· ")?, + } + } + + writeln!(f, "║")?; + } + + writeln!(f, " ╚═════════════════╝")?; + writeln!(f, " a b c d e f g h")?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{test_board, Board}; + + #[test] + #[ignore] + fn empty() { + let pos = test_board!(empty); + let diagram = DiagramFormatter(&pos); + println!("{diagram}"); + } + + #[test] + #[ignore] + fn one_king() { + let pos = test_board![Black King on H3]; + let diagram = DiagramFormatter(&pos); + println!("{diagram}"); + } + + #[test] + #[ignore] + fn starting() { + let pos = test_board!(starting); + let diagram = DiagramFormatter(&pos); + println!("{diagram}"); + } +} diff --git a/board/src/en_passant.rs b/board/src/en_passant.rs new file mode 100644 index 0000000..2da5abc --- /dev/null +++ b/board/src/en_passant.rs @@ -0,0 +1,48 @@ +// Eryn Wells + +use chessfriend_core::{Rank, Square}; + +/// En passant information. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EnPassant { + target: Square, + capture: Square, +} + +impl EnPassant { + fn _capture_square(target: Square) -> Option { + let (file, rank) = target.file_rank(); + match rank { + Rank::THREE => Some(Square::from_file_rank(file, Rank::FOUR)), + Rank::SIX => Some(Square::from_file_rank(file, Rank::FIVE)), + _ => None, + } + } + + /// Return en passant information for a particular target square. The target + /// square is the square a pawn capturing en passant will move to. + /// + /// Return `None` if the square is not eligible for en passant. + /// + /// ## Examples + /// + /// ``` + /// use chessfriend_board::en_passant::EnPassant; + /// use chessfriend_core::Square; + /// assert!(EnPassant::from_target_square(Square::E3).is_some()); + /// assert!(EnPassant::from_target_square(Square::B4).is_none()); + /// ``` + pub fn from_target_square(target: Square) -> Option { + Self::_capture_square(target).map(|capture| Self { target, capture }) + } + + /// The square the capturing piece will move to. + pub fn target_square(self) -> Square { + self.target + } + + /// The square on which the captured pawn sits. + pub fn capture_square(self) -> Square { + self.capture + } +} diff --git a/board/src/flags.rs b/board/src/flags.rs new file mode 100644 index 0000000..548c00f --- /dev/null +++ b/board/src/flags.rs @@ -0,0 +1,90 @@ +// Eryn Wells + +use crate::Castle; +use chessfriend_core::Color; +use std::fmt; + +#[derive(Clone, Copy, Eq, Hash, PartialEq)] +pub struct Flags(u8); + +impl Flags { + #[inline] + fn player_has_right_to_castle_flag_offset(color: Color, castle: Castle) -> usize { + ((color as usize) << 1) + castle as usize + } + + #[allow(dead_code)] + pub(super) fn player_has_right_to_castle(self, color: Color, castle: Castle) -> bool { + (self.0 & (1 << Self::player_has_right_to_castle_flag_offset(color, castle))) != 0 + } + + pub(super) fn set_player_has_right_to_castle_flag(&mut self, color: Color, castle: Castle) { + self.0 |= 1 << Self::player_has_right_to_castle_flag_offset(color, castle); + } + + pub(super) fn clear_player_has_right_to_castle_flag(&mut self, color: Color, castle: Castle) { + self.0 &= !(1 << Self::player_has_right_to_castle_flag_offset(color, castle)); + } + + pub(super) fn clear_all_castling_rights(&mut self) { + self.0 &= 0b1111_1100; + } +} + +impl fmt::Debug for Flags { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Flags({:08b})", self.0) + } +} + +impl Default for Flags { + fn default() -> Self { + Flags(0b0000_1111) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn castle_flags() { + assert_eq!( + Flags::player_has_right_to_castle_flag_offset(Color::White, Castle::KingSide), + 0 + ); + assert_eq!( + Flags::player_has_right_to_castle_flag_offset(Color::White, Castle::QueenSide), + 1 + ); + assert_eq!( + Flags::player_has_right_to_castle_flag_offset(Color::Black, Castle::KingSide), + 2 + ); + assert_eq!( + Flags::player_has_right_to_castle_flag_offset(Color::Black, Castle::QueenSide), + 3 + ); + } + + #[test] + fn defaults() { + let mut flags: Flags = Default::default(); + assert!(flags.player_has_right_to_castle(Color::White, Castle::KingSide)); + assert!(flags.player_has_right_to_castle(Color::White, Castle::QueenSide)); + assert!(flags.player_has_right_to_castle(Color::Black, Castle::KingSide)); + assert!(flags.player_has_right_to_castle(Color::Black, Castle::QueenSide)); + + flags.clear_player_has_right_to_castle_flag(Color::White, Castle::QueenSide); + assert!(flags.player_has_right_to_castle(Color::White, Castle::KingSide)); + assert!(!flags.player_has_right_to_castle(Color::White, Castle::QueenSide)); + assert!(flags.player_has_right_to_castle(Color::Black, Castle::KingSide)); + assert!(flags.player_has_right_to_castle(Color::Black, Castle::QueenSide)); + + flags.set_player_has_right_to_castle_flag(Color::White, Castle::QueenSide); + assert!(flags.player_has_right_to_castle(Color::White, Castle::KingSide)); + assert!(flags.player_has_right_to_castle(Color::White, Castle::QueenSide)); + assert!(flags.player_has_right_to_castle(Color::Black, Castle::KingSide)); + assert!(flags.player_has_right_to_castle(Color::Black, Castle::QueenSide)); + } +} diff --git a/board/src/lib.rs b/board/src/lib.rs index 7d12d9a..67f2c6f 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -1,14 +1,21 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right -} +// Eryn Wells -#[cfg(test)] -mod tests { - use super::*; +pub mod en_passant; - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +mod board; +mod builder; +mod castle; +mod display; +mod flags; +mod macros; +mod piece_sets; +mod pieces; + +pub use board::Board; +pub use builder::Builder; + +use castle::Castle; +use en_passant::EnPassant; +use flags::Flags; +use piece_sets::PieceBitBoards; +use pieces::Pieces; diff --git a/board/src/macros.rs b/board/src/macros.rs new file mode 100644 index 0000000..547071b --- /dev/null +++ b/board/src/macros.rs @@ -0,0 +1,96 @@ +// Eryn Wells + +#[macro_export] +macro_rules! board { + [$($color:ident $shape:ident on $square:ident),* $(,)?] => { + $crate::Builder::new() + $(.place_piece( + chessfriend_core::PlacedPiece::new( + chessfriend_core::Piece::new( + chessfriend_core::Color::$color, + chessfriend_core::Shape::$shape), + chessfriend_core::Square::$square + ) + ))* + .build() + }; +} + +#[cfg(test)] +#[macro_export] +macro_rules! test_board { + ($to_move:ident, [ $($color:ident $shape:ident on $square:ident),* $(,)? ], $en_passant:ident) => { + { + let board = $crate::Builder::new() + $(.place_piece( + chessfriend_core::PlacedPiece::new( + chessfriend_core::Piece::new( + chessfriend_core::Color::$color, + chessfriend_core::Shape::$shape + ), + chessfriend_core::Square::$square + )) + )* + .to_move(chessfriend_core::Color::$to_move) + .en_passant(Some(chessfriend_moves::EnPassant::from_target_square(chessfriend_core::Square::$en_passant)).unwrap()) + .build(); + + println!("{}", board.display()); + + board + } + }; + ($to_move:ident, [ $($color:ident $shape:ident on $square:ident),* $(,)? ]) => { + { + let board = $crate::Builder::new() + $(.place_piece( + chessfriend_core::PlacedPiece::new( + chessfriend_core::Piece::new( + chessfriend_core::Color::$color, + chessfriend_core::Shape::$shape + ), + chessfriend_core::Square::$square + )) + )* + .to_move(chessfriend_core::Color::$to_move) + .build(); + + println!("{}", board.display()); + + pos + } + }; + ($($color:ident $shape:ident on $square:ident),* $(,)?) => { + { + let board = $crate::Builder::new() + $(.place_piece( + chessfriend_core::PlacedPiece::new( + chessfriend_core::Piece::new( + chessfriend_core::Color::$color, + chessfriend_core::Shape::$shape + ), + chessfriend_core::Square::$square + )) + )* + .build(); + + println!("{}", board.display()); + + board + } + }; + (empty) => { + { + let board = Board::empty(); + println!("{}", board.display()); + board + } + }; + (starting) => { + { + let board = Board::starting(); + println!("{}", board.display()); + board + } + }; +} diff --git a/board/src/piece_sets.rs b/board/src/piece_sets.rs new file mode 100644 index 0000000..fee1a3f --- /dev/null +++ b/board/src/piece_sets.rs @@ -0,0 +1,174 @@ +// Eryn Wells + +use chessfriend_bitboard::BitBoard; +use chessfriend_core::{Color, Piece, PlacedPiece, Square}; + +#[derive(Debug, Eq, PartialEq)] +pub enum PlacePieceStrategy { + Replace, + PreserveExisting, +} + +#[derive(Debug, Eq, PartialEq)] +pub enum PlacePieceError { + ExisitingPiece, +} + +impl Default for PlacePieceStrategy { + fn default() -> Self { + Self::Replace + } +} + +#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] +pub(crate) struct PieceBitBoards { + by_color: ByColor, + by_color_and_shape: ByColorAndShape, +} + +#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] +struct ByColor(BitBoard, [BitBoard; 2]); + +#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] +struct ByColorAndShape([[BitBoard; 6]; 2]); + +impl PieceBitBoards { + pub(super) fn new(pieces: [[BitBoard; 6]; 2]) -> Self { + use std::ops::BitOr; + + let white_pieces = pieces[Color::White as usize] + .iter() + .fold(BitBoard::empty(), BitOr::bitor); + let black_pieces = pieces[Color::Black as usize] + .iter() + .fold(BitBoard::empty(), BitOr::bitor); + + let all_pieces = white_pieces | black_pieces; + + Self { + by_color: ByColor(all_pieces, [white_pieces, black_pieces]), + by_color_and_shape: ByColorAndShape(pieces), + } + } + + /// A BitBoard representing all the pieces currently on the board. Other + /// engines might refer to this concept as 'occupancy'. + pub(crate) fn all_pieces(&self) -> &BitBoard { + self.by_color.all() + } + + pub(crate) fn all_pieces_of_color(&self, color: Color) -> &BitBoard { + self.by_color.bitboard(color) + } + + pub(super) fn bitboard_for_color(&self, color: Color) -> &BitBoard { + self.by_color.bitboard(color) + } + + pub(super) fn bitboard_for_color_mut(&mut self, color: Color) -> &mut BitBoard { + self.by_color.bitboard_mut(color) + } + + pub(super) fn bitboard_for_piece(&self, piece: &Piece) -> &BitBoard { + self.by_color_and_shape.bitboard_for_piece(piece) + } + + pub(super) fn bitboard_for_piece_mut(&mut self, piece: &Piece) -> &mut BitBoard { + self.by_color_and_shape.bitboard_for_piece_mut(piece) + } + + pub(super) fn place_piece(&mut self, piece: &PlacedPiece) -> Result<(), PlacePieceError> { + self.place_piece_with_strategy(piece, PlacePieceStrategy::default()) + } + + pub(super) fn place_piece_with_strategy( + &mut self, + piece: &PlacedPiece, + strategy: PlacePieceStrategy, + ) -> Result<(), PlacePieceError> { + let color = piece.color(); + let square = piece.square(); + + if strategy == PlacePieceStrategy::PreserveExisting + && self.by_color.bitboard(color).is_set(piece.square()) + { + return Err(PlacePieceError::ExisitingPiece); + } + + self.by_color_and_shape.set_square(square, piece.piece()); + self.by_color.set_square(square, color); + + Ok(()) + } + + pub(super) fn remove_piece(&mut self, piece: &PlacedPiece) { + let color = piece.color(); + let square = piece.square(); + + self.by_color_and_shape.clear_square(square, piece.piece()); + self.by_color.clear_square(square, color); + } + + pub(super) fn move_piece(&mut self, piece: &Piece, from_square: Square, to_square: Square) { + let color = piece.color(); + + self.by_color_and_shape.clear_square(from_square, piece); + self.by_color.clear_square(from_square, color); + self.by_color_and_shape.set_square(to_square, piece); + self.by_color.set_square(to_square, color); + } +} + +impl FromIterator for PieceBitBoards { + fn from_iter>(iter: T) -> Self { + let mut pieces: Self = Default::default(); + + for piece in iter { + let _ = pieces.place_piece(&piece); + } + + pieces + } +} + +impl ByColor { + fn all(&self) -> &BitBoard { + &self.0 + } + + pub(super) fn bitboard(&self, color: Color) -> &BitBoard { + &self.1[color as usize] + } + + pub(super) fn bitboard_mut(&mut self, color: Color) -> &mut BitBoard { + &mut self.1[color as usize] + } + + fn set_square(&mut self, square: Square, color: Color) { + self.0.set_square(square); + self.1[color as usize].set_square(square) + } + + fn clear_square(&mut self, square: Square, color: Color) { + self.0.clear_square(square); + self.1[color as usize].clear_square(square); + } +} + +impl ByColorAndShape { + fn bitboard_for_piece(&self, piece: &Piece) -> &BitBoard { + &self.0[piece.color() as usize][piece.shape() as usize] + } + + fn bitboard_for_piece_mut(&mut self, piece: &Piece) -> &mut BitBoard { + &mut self.0[piece.color() as usize][piece.shape() as usize] + } + + fn set_square(&mut self, square: Square, piece: &Piece) { + self.bitboard_for_piece_mut(piece).set_square(square); + } + + fn clear_square(&mut self, square: Square, piece: &Piece) { + self.bitboard_for_piece_mut(piece).clear_square(square); + } +} diff --git a/board/src/pieces.rs b/board/src/pieces.rs new file mode 100644 index 0000000..537db20 --- /dev/null +++ b/board/src/pieces.rs @@ -0,0 +1,127 @@ +// Eryn Wells + +use super::Board; +use chessfriend_bitboard::BitBoard; +use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; + +pub struct Pieces<'a> { + color: Color, + board: &'a Board, + current_shape: Option, + shape_iterator: Box>, + square_iterator: Option>>, +} + +impl<'a> Pieces<'a> { + pub(crate) fn new(board: &Board, color: Color) -> Pieces { + Pieces { + color, + board, + current_shape: None, + shape_iterator: Box::new(Shape::iter()), + square_iterator: None, + } + } +} + +impl<'a> Iterator for Pieces<'a> { + type Item = PlacedPiece; + + fn next(&mut self) -> Option { + if let Some(square_iterator) = &mut self.square_iterator { + if let (Some(square), Some(shape)) = (square_iterator.next(), self.current_shape) { + return Some(PlacedPiece::new(Piece::new(self.color, shape), square)); + } + } + + let mut current_shape: Option = None; + let mut next_nonempty_bitboard: Option<&BitBoard> = None; + + for shape in self.shape_iterator.by_ref() { + let piece = Piece::new(self.color, *shape); + + let bitboard = self.board.bitboard_for_piece(piece); + if bitboard.is_empty() { + continue; + } + + next_nonempty_bitboard = Some(bitboard); + current_shape = Some(*shape); + + break; + } + + if let (Some(bitboard), Some(shape)) = (next_nonempty_bitboard, current_shape) { + let mut square_iterator = bitboard.occupied_squares(); + + let mut next_placed_piece: Option = None; + if let Some(square) = square_iterator.next() { + next_placed_piece = Some(PlacedPiece::new(Piece::new(self.color, shape), square)); + } + + self.square_iterator = Some(Box::new(square_iterator)); + self.current_shape = Some(shape); + + return next_placed_piece; + } + + None + } +} + +#[cfg(test)] +mod tests { + use crate::{test_board, Board, Builder}; + use chessfriend_core::{piece, Color}; + use std::collections::HashSet; + + #[test] + fn empty() { + let board = Board::empty(); + let mut pieces = board.pieces(Color::White); + assert_eq!(pieces.next(), None); + } + + #[test] + fn one() { + let pos = Builder::new() + .place_piece(piece!(White Queen on E4)) + .build(); + println!("{:#?}", &pos); + + let mut pieces = pos.pieces(Color::White); + assert_eq!(pieces.next(), Some(piece!(White Queen on E4))); + assert_eq!(pieces.next(), Some(piece!(White King on E1))); + assert_eq!(pieces.next(), None); + } + + #[test] + fn multiple_pieces() { + let board = test_board![ + White Queen on E4, + White King on A1, + White Pawn on B2, + White Pawn on C2, + ]; + + let expected_placed_pieces = HashSet::from([ + piece!(White Queen on E4), + piece!(White King on A1), + piece!(White Pawn on B2), + piece!(White Pawn on C2), + ]); + + let placed_pieces = HashSet::from_iter(board.pieces(Color::White)); + + assert_eq!( + placed_pieces, + expected_placed_pieces, + "{:#?}", + placed_pieces + .symmetric_difference(&expected_placed_pieces) + .into_iter() + .map(|pp| format!("{}", pp)) + .collect::>() + ); + } +} From b3c472fbce32c878b86352ec79e00cbcd001542d Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 26 Apr 2024 09:50:42 -0400 Subject: [PATCH 208/423] Fix some imports in the moves package Castle and EnPassant moved to the board package. Reference these types there. Add the board packages as a dependency to the moves package. --- Cargo.lock | 1 + board/src/lib.rs | 2 +- moves/Cargo.toml | 3 ++- moves/src/builder.rs | 3 ++- moves/src/en_passant.rs | 54 ----------------------------------------- moves/src/lib.rs | 4 --- moves/src/moves.rs | 3 ++- 7 files changed, 8 insertions(+), 62 deletions(-) delete mode 100644 moves/src/en_passant.rs diff --git a/Cargo.lock b/Cargo.lock index 50995a7..3049c76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,6 +86,7 @@ name = "chessfriend_moves" version = "0.1.0" dependencies = [ "chessfriend_bitboard", + "chessfriend_board", "chessfriend_core", ] diff --git a/board/src/lib.rs b/board/src/lib.rs index 67f2c6f..e599040 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -1,10 +1,10 @@ // Eryn Wells +pub mod castle; pub mod en_passant; mod board; mod builder; -mod castle; mod display; mod flags; mod macros; diff --git a/moves/Cargo.toml b/moves/Cargo.toml index 3c9e6cf..b042344 100644 --- a/moves/Cargo.toml +++ b/moves/Cargo.toml @@ -6,5 +6,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -chessfriend_core = { path = "../core" } chessfriend_bitboard = { path = "../bitboard" } +chessfriend_board = { path = "../board" } +chessfriend_core = { path = "../core" } diff --git a/moves/src/builder.rs b/moves/src/builder.rs index 5f02364..0ead866 100644 --- a/moves/src/builder.rs +++ b/moves/src/builder.rs @@ -1,6 +1,7 @@ // Eryn Wells -use crate::{castle, defs::Kind, EnPassant, Move, PromotionShape}; +use crate::{defs::Kind, Move, PromotionShape}; +use chessfriend_board::{castle, en_passant::EnPassant}; use chessfriend_core::{Color, File, PlacedPiece, Rank, Square}; use std::result::Result as StdResult; diff --git a/moves/src/en_passant.rs b/moves/src/en_passant.rs deleted file mode 100644 index 9a0f972..0000000 --- a/moves/src/en_passant.rs +++ /dev/null @@ -1,54 +0,0 @@ -// Eryn Wells - -use chessfriend_core::{Rank, Square}; - -/// En passant information. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct EnPassant { - target_square: Square, - capture_square: Square, -} - -impl EnPassant { - fn _capture_square(target: Square) -> Option { - let (file, rank) = target.file_rank(); - match rank { - Rank::THREE => Some(Square::from_file_rank(file, Rank::FOUR)), - Rank::SIX => Some(Square::from_file_rank(file, Rank::FIVE)), - _ => None, - } - } - - /// Return en passant information for a particular target square. The target - /// square is the square a pawn capturing en passant will move to. - /// - /// Return `None` if the square is not eligible for en passant. - /// - /// ## Examples - /// - /// ``` - /// use chessfriend_core::Square; - /// use chessfriend_moves::EnPassant; - /// assert!(EnPassant::from_target_square(Square::E3).is_some()); - /// assert!(EnPassant::from_target_square(Square::B4).is_none()); - /// ``` - pub fn from_target_square(target: Square) -> Option { - match Self::_capture_square(target) { - Some(capture) => Some(Self { - target_square: target, - capture_square: capture, - }), - None => None, - } - } - - /// The square the capturing piece will move to. - pub fn target_square(&self) -> Square { - self.target_square - } - - /// The square on which the captured pawn sits. - pub fn capture_square(&self) -> Square { - self.capture_square - } -} diff --git a/moves/src/lib.rs b/moves/src/lib.rs index 297f82e..e43a61d 100644 --- a/moves/src/lib.rs +++ b/moves/src/lib.rs @@ -3,13 +3,9 @@ pub mod testing; mod builder; -mod castle; mod defs; -mod en_passant; mod moves; pub use builder::{Builder, Error as BuildMoveError, Result as BuildMoveResult}; -pub use castle::Castle; pub use defs::PromotionShape; -pub use en_passant::EnPassant; pub use moves::Move; diff --git a/moves/src/moves.rs b/moves/src/moves.rs index 5348306..8e7c926 100644 --- a/moves/src/moves.rs +++ b/moves/src/moves.rs @@ -1,6 +1,7 @@ // Eryn Wells -use crate::{castle::Castle, defs::Kind}; +use crate::defs::Kind; +use chessfriend_board::castle::Castle; use chessfriend_core::{Rank, Shape, Square}; use std::fmt; From 19feff9591618a6bbf0252eda4ab7ca0afbbb405 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 12 Jul 2024 15:52:41 -0700 Subject: [PATCH 209/423] Solidify PieceSet and supporting types --- board/src/piece_sets.rs | 173 +++++++++++++----------------- board/src/piece_sets/bitboards.rs | 56 ++++++++++ board/src/piece_sets/mailbox.rs | 59 ++++++++++ 3 files changed, 187 insertions(+), 101 deletions(-) create mode 100644 board/src/piece_sets/bitboards.rs create mode 100644 board/src/piece_sets/mailbox.rs diff --git a/board/src/piece_sets.rs b/board/src/piece_sets.rs index fee1a3f..f7435e5 100644 --- a/board/src/piece_sets.rs +++ b/board/src/piece_sets.rs @@ -1,9 +1,14 @@ // Eryn Wells -use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece, PlacedPiece, Square}; +mod bitboards; +mod mailbox; -#[derive(Debug, Eq, PartialEq)] +use bitboards::{ByColor, ByColorAndShape}; +use chessfriend_bitboard::BitBoard; +use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; +use mailbox::Mailbox; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum PlacePieceStrategy { Replace, PreserveExisting, @@ -11,7 +16,7 @@ pub enum PlacePieceStrategy { #[derive(Debug, Eq, PartialEq)] pub enum PlacePieceError { - ExisitingPiece, + ExisitingPiece(PlacedPiece), } impl Default for PlacePieceStrategy { @@ -20,20 +25,17 @@ impl Default for PlacePieceStrategy { } } +/// The internal data structure of a [Board] that efficiently manages the +/// placement of pieces on the board. #[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] -pub(crate) struct PieceBitBoards { +pub(crate) struct PieceSet { by_color: ByColor, by_color_and_shape: ByColorAndShape, + mailbox: Mailbox, } -#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] -struct ByColor(BitBoard, [BitBoard; 2]); - -#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] -struct ByColorAndShape([[BitBoard; 6]; 2]); - -impl PieceBitBoards { - pub(super) fn new(pieces: [[BitBoard; 6]; 2]) -> Self { +impl PieceSet { + pub(crate) fn new(pieces: [[BitBoard; Shape::NUM]; Color::NUM]) -> Self { use std::ops::BitOr; let white_pieces = pieces[Color::White as usize] @@ -45,130 +47,99 @@ impl PieceBitBoards { let all_pieces = white_pieces | black_pieces; + let mut mailbox = Mailbox::default(); + for c in Color::into_iter() { + for s in Shape::into_iter() { + let bitboard = pieces[c as usize][s as usize]; + for square in bitboard.occupied_squares() { + mailbox.set(square, Piece::new(c, s)); + } + } + } + Self { - by_color: ByColor(all_pieces, [white_pieces, black_pieces]), - by_color_and_shape: ByColorAndShape(pieces), + by_color: ByColor::new(all_pieces, [white_pieces, black_pieces]), + by_color_and_shape: ByColorAndShape::new(pieces), + mailbox, } } - /// A BitBoard representing all the pieces currently on the board. Other + /// A [`BitBoard`] representing all the pieces currently on the board. Other /// engines might refer to this concept as 'occupancy'. - pub(crate) fn all_pieces(&self) -> &BitBoard { + pub(crate) fn all_pieces(&self) -> BitBoard { self.by_color.all() } - pub(crate) fn all_pieces_of_color(&self, color: Color) -> &BitBoard { + pub(crate) fn all_pieces_of_color(&self, color: Color) -> BitBoard { self.by_color.bitboard(color) } - pub(super) fn bitboard_for_color(&self, color: Color) -> &BitBoard { + pub(super) fn bitboard_for_color(&self, color: Color) -> BitBoard { self.by_color.bitboard(color) } - pub(super) fn bitboard_for_color_mut(&mut self, color: Color) -> &mut BitBoard { - self.by_color.bitboard_mut(color) - } - - pub(super) fn bitboard_for_piece(&self, piece: &Piece) -> &BitBoard { + pub(crate) fn bitboard_for_piece(&self, piece: Piece) -> BitBoard { self.by_color_and_shape.bitboard_for_piece(piece) } - pub(super) fn bitboard_for_piece_mut(&mut self, piece: &Piece) -> &mut BitBoard { - self.by_color_and_shape.bitboard_for_piece_mut(piece) + pub(crate) fn get(&self, square: Square) -> Option { + self.mailbox.get(square) } - pub(super) fn place_piece(&mut self, piece: &PlacedPiece) -> Result<(), PlacePieceError> { - self.place_piece_with_strategy(piece, PlacePieceStrategy::default()) - } - - pub(super) fn place_piece_with_strategy( + pub(crate) fn place_piece_on_square( &mut self, - piece: &PlacedPiece, - strategy: PlacePieceStrategy, - ) -> Result<(), PlacePieceError> { - let color = piece.color(); - let square = piece.square(); + piece: Piece, + square: Square, + ) -> Result { + self.place_piece_on_square_with_strategy(piece, square, PlacePieceStrategy::default()) + } - if strategy == PlacePieceStrategy::PreserveExisting - && self.by_color.bitboard(color).is_set(piece.square()) - { - return Err(PlacePieceError::ExisitingPiece); + pub(crate) fn place_piece_on_square_with_strategy( + &mut self, + piece: Piece, + square: Square, + strategy: PlacePieceStrategy, + ) -> Result { + let color = piece.color(); + + if strategy == PlacePieceStrategy::PreserveExisting { + if let Some(existing_piece) = self.mailbox.get(square) { + return Err(PlacePieceError::ExisitingPiece(PlacedPiece::new( + existing_piece, + square, + ))); + } } - self.by_color_and_shape.set_square(square, piece.piece()); + let piece: Piece = piece.into(); + self.by_color_and_shape.set_square(square, piece); self.by_color.set_square(square, color); + self.mailbox.set(square, piece); - Ok(()) + Ok(PlacedPiece::new(piece, square)) } - pub(super) fn remove_piece(&mut self, piece: &PlacedPiece) { - let color = piece.color(); - let square = piece.square(); + pub(crate) fn remove_piece_from_square(&mut self, square: Square) -> Option { + if let Some(piece) = self.mailbox.get(square) { + self.by_color_and_shape.clear_square(square, piece.into()); + self.by_color.clear_square(square, piece.color()); + self.mailbox.clear(square); - self.by_color_and_shape.clear_square(square, piece.piece()); - self.by_color.clear_square(square, color); - } - - pub(super) fn move_piece(&mut self, piece: &Piece, from_square: Square, to_square: Square) { - let color = piece.color(); - - self.by_color_and_shape.clear_square(from_square, piece); - self.by_color.clear_square(from_square, color); - self.by_color_and_shape.set_square(to_square, piece); - self.by_color.set_square(to_square, color); + Some(PlacedPiece::new(piece, square)) + } else { + None + } } } -impl FromIterator for PieceBitBoards { +impl FromIterator for PieceSet { fn from_iter>(iter: T) -> Self { - let mut pieces: Self = Default::default(); + let mut pieces: Self = Self::default(); for piece in iter { - let _ = pieces.place_piece(&piece); + let _ = pieces.place_piece_on_square(piece.piece(), piece.square()); } pieces } } - -impl ByColor { - fn all(&self) -> &BitBoard { - &self.0 - } - - pub(super) fn bitboard(&self, color: Color) -> &BitBoard { - &self.1[color as usize] - } - - pub(super) fn bitboard_mut(&mut self, color: Color) -> &mut BitBoard { - &mut self.1[color as usize] - } - - fn set_square(&mut self, square: Square, color: Color) { - self.0.set_square(square); - self.1[color as usize].set_square(square) - } - - fn clear_square(&mut self, square: Square, color: Color) { - self.0.clear_square(square); - self.1[color as usize].clear_square(square); - } -} - -impl ByColorAndShape { - fn bitboard_for_piece(&self, piece: &Piece) -> &BitBoard { - &self.0[piece.color() as usize][piece.shape() as usize] - } - - fn bitboard_for_piece_mut(&mut self, piece: &Piece) -> &mut BitBoard { - &mut self.0[piece.color() as usize][piece.shape() as usize] - } - - fn set_square(&mut self, square: Square, piece: &Piece) { - self.bitboard_for_piece_mut(piece).set_square(square); - } - - fn clear_square(&mut self, square: Square, piece: &Piece) { - self.bitboard_for_piece_mut(piece).clear_square(square); - } -} diff --git a/board/src/piece_sets/bitboards.rs b/board/src/piece_sets/bitboards.rs new file mode 100644 index 0000000..5c09432 --- /dev/null +++ b/board/src/piece_sets/bitboards.rs @@ -0,0 +1,56 @@ +use chessfriend_bitboard::BitBoard; +use chessfriend_core::{Color, Piece, Shape, Square}; + +/// A collection of bitboards that organize pieces by color. +#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] +pub(super) struct ByColor(BitBoard, [BitBoard; Color::NUM]); + +/// A collection of bitboards that organize pieces first by color and then by piece type. +#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] +pub(super) struct ByColorAndShape([[BitBoard; Shape::NUM]; Color::NUM]); + +impl ByColor { + pub(super) fn new(all_pieces: BitBoard, bitboards_by_color: [BitBoard; Color::NUM]) -> Self { + ByColor(all_pieces, bitboards_by_color) + } + + pub(crate) fn all(&self) -> BitBoard { + self.0 + } + + pub(crate) fn bitboard(&self, color: Color) -> BitBoard { + self.1[color as usize] + } + + pub(super) fn set_square(&mut self, square: Square, color: Color) { + self.0.set(square); + self.1[color as usize].set(square); + } + + pub(super) fn clear_square(&mut self, square: Square, color: Color) { + self.0.clear(square); + self.1[color as usize].clear(square); + } +} + +impl ByColorAndShape { + pub(super) fn new(bitboards: [[BitBoard; Shape::NUM]; Color::NUM]) -> Self { + Self(bitboards) + } + + pub(super) fn bitboard_for_piece(&self, piece: Piece) -> BitBoard { + self.0[piece.color() as usize][piece.shape() as usize] + } + + pub(super) fn bitboard_for_piece_mut(&mut self, piece: Piece) -> &mut BitBoard { + &mut self.0[piece.color() as usize][piece.shape() as usize] + } + + pub(super) fn set_square(&mut self, square: Square, piece: Piece) { + self.bitboard_for_piece_mut(piece).set(square); + } + + pub(super) fn clear_square(&mut self, square: Square, piece: Piece) { + self.bitboard_for_piece_mut(piece).clear(square); + } +} diff --git a/board/src/piece_sets/mailbox.rs b/board/src/piece_sets/mailbox.rs new file mode 100644 index 0000000..c551fb6 --- /dev/null +++ b/board/src/piece_sets/mailbox.rs @@ -0,0 +1,59 @@ +// Eryn Wells + +use chessfriend_core::{Piece, PlacedPiece, Square}; +use std::iter::FromIterator; + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub(super) struct Mailbox([Option; Square::NUM]); + +impl Mailbox { + pub(super) fn new() -> Self { + Self::default() + } + + pub(super) fn get(&self, square: Square) -> Option { + self.0[square as usize] + } + + pub(super) fn set(&mut self, square: Square, piece: Piece) { + self.0[square as usize] = Some(piece); + } + + pub(super) fn clear(&mut self, square: Square) { + self.0[square as usize] = None; + } +} + +impl Default for Mailbox { + fn default() -> Self { + Self([None; Square::NUM]) + } +} + +impl From<[Option; Square::NUM]> for Mailbox { + fn from(value: [Option; Square::NUM]) -> Self { + Mailbox(value) + } +} + +impl FromIterator for Mailbox { + fn from_iter>(iter: T) -> Self { + let mut mailbox = Self::new(); + for placed_piece in iter { + mailbox.set(placed_piece.square(), placed_piece.piece()); + } + + mailbox + } +} + +impl FromIterator<(Square, Piece)> for Mailbox { + fn from_iter>(iter: T) -> Self { + let mut mailbox = Self::new(); + for (square, piece) in iter { + mailbox.set(square, piece); + } + + mailbox + } +} From 2480ef25e959da8325a97899e1c52e841b9bb7ed Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 13 Jul 2024 07:04:49 -0700 Subject: [PATCH 210/423] Remove BitBoardBuilder It's unused except for the macro, and BitBoard itself can be declared mutable, and implements Copy and Clone. So, I don't think having a separate Builder type helps much. --- bitboard/src/bitboard.rs | 40 ++++++++++------------------------------ bitboard/src/lib.rs | 12 +++++++----- 2 files changed, 17 insertions(+), 35 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 9b893b6..205b13a 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -355,26 +355,6 @@ impl Not for &BitBoard { } } -pub struct BitBoardBuilder(BitBoard); - -impl BitBoardBuilder { - pub const fn empty() -> BitBoardBuilder { - BitBoardBuilder(BitBoard::empty()) - } - - pub fn new(bits: u64) -> BitBoardBuilder { - BitBoardBuilder(BitBoard::new(bits)) - } - - pub fn square(mut self, square: Square) -> BitBoardBuilder { - self.0.set_square(square); - self - } - - pub fn build(&self) -> BitBoard { - self.0 - } -} #[cfg(test)] mod tests { @@ -453,18 +433,18 @@ mod tests { #[test] fn xor() { - let a = bitboard![C5, G7]; - let b = bitboard![B5, G7, H3]; + let a = bitboard![C5 G7]; + let b = bitboard![B5 G7 H3]; - assert_eq!(a ^ b, bitboard![B5, C5, H3]); + assert_eq!(a ^ b, bitboard![B5 C5 H3]); assert_eq!(a ^ BitBoard::empty(), a); assert_eq!(BitBoard::empty() ^ BitBoard::empty(), BitBoard::empty()); } #[test] fn bitand_assign() { - let mut a = bitboard![C5, G7]; - let b = bitboard![B5, G7, H3]; + let mut a = bitboard![C5 G7]; + let b = bitboard![B5 G7 H3]; a &= b; @@ -473,12 +453,12 @@ mod tests { #[test] fn bitor_assign() { - let mut a = bitboard![C5, G7]; - let b = bitboard![B5, G7, H3]; + let mut a = bitboard![C5 G7]; + let b = bitboard![B5 G7 H3]; a |= b; - assert_eq!(a, bitboard![B5, C5, G7, H3]); + assert_eq!(a, bitboard![B5 C5 G7 H3]); } #[test] @@ -489,11 +469,11 @@ mod tests { #[test] fn first_occupied_squares() { - let bb = bitboard![A8, E1]; + let bb = bitboard![A8 E1]; assert_eq!(bb.first_occupied_square(), Some(Square::A8)); assert_eq!(bb.first_occupied_square_trailing(), Some(Square::E1)); - let bb = bitboard![D6, E7, F8]; + let bb = bitboard![D6 E7 F8]; assert_eq!(bb.first_occupied_square_trailing(), Some(Square::D6)); } } diff --git a/bitboard/src/lib.rs b/bitboard/src/lib.rs index 30a35f2..a39d3c9 100644 --- a/bitboard/src/lib.rs +++ b/bitboard/src/lib.rs @@ -5,15 +5,17 @@ mod bitboard; mod library; mod shifts; -pub use bitboard::{BitBoard, BitBoardBuilder}; +pub use bitboard::BitBoard; pub(crate) use bit_scanner::{LeadingBitScanner, TrailingBitScanner}; #[macro_export] macro_rules! bitboard { - ($($sq:ident),* $(,)?) => { - $crate::BitBoardBuilder::empty() - $(.square(chessfriend_core::Square::$sq))* - .build() + ($($sq:ident)* $(,)?) => { + { + let mut bitboard = $crate::BitBoard::empty(); + $(bitboard.set(chessfriend_core::Square::$sq);)* + bitboard + } }; } From 14ab6697632bb5ab3d3188ddb06a4eccb0db52dd Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 13 Jul 2024 07:08:18 -0700 Subject: [PATCH 211/423] Clean up BitBoard's bit ops impl macros Declare the `forward_ref` crate as a dependency (my first external dependency!) and use it to clean up the infix, assign, and unary op impls. This crate automatically implements A+&B, &A+B, and &A+&B for me. --- bitboard/Cargo.toml | 1 + bitboard/src/bitboard.rs | 42 +++++++++++++--------------------------- 2 files changed, 14 insertions(+), 29 deletions(-) diff --git a/bitboard/Cargo.toml b/bitboard/Cargo.toml index df608b5..26df504 100644 --- a/bitboard/Cargo.toml +++ b/bitboard/Cargo.toml @@ -7,3 +7,4 @@ edition = "2021" [dependencies] chessfriend_core = { path = "../core" } +forward_ref = "1.0.0" diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 205b13a..6adc05c 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -3,8 +3,9 @@ use crate::library; use crate::{LeadingBitScanner, TrailingBitScanner}; use chessfriend_core::{Color, Direction, File, Rank, Square}; +use forward_ref::{forward_ref_binop, forward_ref_op_assign, forward_ref_unop}; use std::fmt; -use std::ops::Not; +use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not}; #[derive(Clone, Copy, Eq, Hash, PartialEq)] pub struct BitBoard(pub(crate) u64); @@ -293,61 +294,43 @@ impl fmt::Debug for BitBoard { } macro_rules! infix_op { - ($trait_type:ident, $func_name:ident, $type:ty) => { - infix_op!($trait_type, $func_name, $type, $type); - infix_op!($trait_type, $func_name, $type, &$type); - infix_op!($trait_type, $func_name, &$type, $type); - infix_op!($trait_type, $func_name, &$type, &$type); - }; ($trait_type:ident, $func_name:ident, $left_type:ty, $right_type:ty) => { impl std::ops::$trait_type<$right_type> for $left_type { - type Output = BitBoard; + type Output = Self; #[inline] fn $func_name(self, rhs: $right_type) -> Self::Output { BitBoard(std::ops::$trait_type::$func_name(self.0, rhs.0)) } } + + forward_ref_binop!(impl $trait_type, $func_name for $left_type, $right_type); }; } macro_rules! assign_op { ($trait_type:ident, $func_name:ident, $type:ty) => { - impl std::ops::$trait_type for $type { + impl $trait_type for $type { #[inline] fn $func_name(&mut self, rhs: $type) { - std::ops::$trait_type::$func_name(&mut self.0, rhs.0) + $trait_type::$func_name(&mut self.0, rhs.0) } } - impl std::ops::$trait_type<&$type> for $type { - #[inline] - fn $func_name(&mut self, rhs: &$type) { - std::ops::$trait_type::$func_name(&mut self.0, rhs.0) - } - } + forward_ref_op_assign!(impl $trait_type, $func_name for $type, $type); }; } -infix_op!(BitAnd, bitand, BitBoard); -infix_op!(BitOr, bitor, BitBoard); -infix_op!(BitXor, bitxor, BitBoard); +infix_op!(BitAnd, bitand, BitBoard, BitBoard); +infix_op!(BitOr, bitor, BitBoard, BitBoard); +infix_op!(BitXor, bitxor, BitBoard, BitBoard); assign_op!(BitAndAssign, bitand_assign, BitBoard); assign_op!(BitOrAssign, bitor_assign, BitBoard); assign_op!(BitXorAssign, bitxor_assign, BitBoard); impl Not for BitBoard { - type Output = BitBoard; - - #[inline] - fn not(self) -> Self::Output { - BitBoard(!self.0) - } -} - -impl Not for &BitBoard { - type Output = BitBoard; + type Output = Self; #[inline] fn not(self) -> Self::Output { @@ -355,6 +338,7 @@ impl Not for &BitBoard { } } +forward_ref_unop!(impl Not, not for BitBoard); #[cfg(test)] mod tests { From 480a009e631844b3d489acb2fc992c1e14643815 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 13 Jul 2024 07:17:43 -0700 Subject: [PATCH 212/423] [BitBoard] Address a bunch of rust-analyzer suggestions Add #[must_use] to many methods --- bitboard/src/bitboard.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 6adc05c..215e917 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -12,6 +12,7 @@ pub struct BitBoard(pub(crate) u64); macro_rules! moves_getter { ($getter_name:ident) => { + #[must_use] pub fn $getter_name(sq: Square) -> BitBoard { library::library().$getter_name(sq) } @@ -22,32 +23,39 @@ impl BitBoard { pub const EMPTY: BitBoard = BitBoard(u64::MIN); pub const FULL: BitBoard = BitBoard(u64::MAX); + #[must_use] pub const fn empty() -> BitBoard { BitBoard(0) } + #[must_use] pub const fn new(bits: u64) -> BitBoard { BitBoard(bits) } + #[must_use] pub fn rank(rank: &u8) -> BitBoard { debug_assert!(*rank < 8); library::RANKS[*rank as usize] } + #[must_use] pub fn file(file: &u8) -> BitBoard { debug_assert!(*file < 8); library::FILES[*file as usize] } + #[must_use] pub fn ray(sq: Square, dir: Direction) -> &'static BitBoard { library::library().ray(sq, dir) } + #[must_use] pub fn pawn_attacks(sq: Square, color: Color) -> BitBoard { library::library().pawn_attacks(sq, color) } + #[must_use] pub fn pawn_pushes(sq: Square, color: Color) -> BitBoard { library::library().pawn_pushes(sq, color) } @@ -58,16 +66,19 @@ impl BitBoard { moves_getter!(queen_moves); moves_getter!(king_moves); + #[must_use] pub const fn kingside(color: Color) -> &'static BitBoard { &library::KINGSIDES[color as usize] } + #[must_use] pub const fn queenside(color: Color) -> &'static BitBoard { &library::QUEENSIDES[color as usize] } } impl BitBoard { + #[must_use] pub const fn as_bits(&self) -> &u64 { &self.0 } @@ -82,6 +93,7 @@ impl BitBoard { /// assert!(!BitBoard::FULL.is_populated()); /// assert!(!BitBoard::new(0b1000).is_populated()); /// ``` + #[must_use] pub const fn is_empty(&self) -> bool { self.0 == 0 } @@ -269,7 +281,7 @@ impl fmt::Display for BitBoard { let binary_ranks = format!("{:064b}", self.0) .chars() .rev() - .map(|c| String::from(c)) + .map(String::from) .collect::>(); let mut ranks_written = 0; @@ -279,7 +291,7 @@ impl fmt::Display for BitBoard { ranks_written += 1; if ranks_written < 8 { - write!(f, "\n")?; + writeln!(f)?; } } From daf5c8679275d1c8012c2f444ad8396b44f05f61 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 13 Jul 2024 07:19:47 -0700 Subject: [PATCH 213/423] [BitBoard] Clean up the API; implement some traits Clean up the BitBoard API by renaming methods with simpler names. run-help Redo Redo the implementation of a couple methods to be more succinct. --- bitboard/src/bitboard.rs | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 215e917..3787bb8 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -25,7 +25,7 @@ impl BitBoard { #[must_use] pub const fn empty() -> BitBoard { - BitBoard(0) + Self::EMPTY } #[must_use] @@ -79,8 +79,8 @@ impl BitBoard { impl BitBoard { #[must_use] - pub const fn as_bits(&self) -> &u64 { - &self.0 + pub const fn as_bits(&self) -> u64 { + self.0 } /// Returns `true` if the [`BitBoard`] has no bits set. @@ -113,7 +113,7 @@ impl BitBoard { } /// Returns `true` if this [`BitBoard`] has the bit corresponding to `square` set. - pub fn is_set(self, square: Square) -> bool { + pub fn contains(self, square: Square) -> bool { let square_bitboard: BitBoard = square.into(); !(self & square_bitboard).is_empty() } @@ -132,14 +132,14 @@ impl BitBoard { self.0.count_ones() } - pub fn set_square(&mut self, sq: Square) { - let sq_bb: BitBoard = sq.into(); - *self |= sq_bb + pub fn set(&mut self, square: Square) { + let square_bitboard: BitBoard = square.into(); + self.0 |= square_bitboard.0 } - pub fn clear_square(&mut self, sq: Square) { - let sq_bb: BitBoard = sq.into(); - *self &= !sq_bb + pub fn clear(&mut self, square: Square) { + let square_bitboard: BitBoard = square.into(); + self.0 &= !square_bitboard.0 } /// Returns `true` if this BitBoard represents a single square. @@ -202,6 +202,12 @@ impl Default for BitBoard { } } +impl From for u64 { + fn from(value: BitBoard) -> Self { + value.as_bits() + } +} + impl From for BitBoard { fn from(value: File) -> Self { library::FILES[*value.as_index() as usize] @@ -228,13 +234,10 @@ impl From for BitBoard { impl FromIterator for BitBoard { fn from_iter>(iter: T) -> Self { - let mut builder = BitBoardBuilder::empty(); - - for sq in iter { - builder = builder.square(sq) - } - - builder.build() + iter.into_iter().fold(BitBoard::EMPTY, |mut acc, sq| { + acc.set(sq); + acc + }) } } From 7e45e495028b3407038f69982db0a3fceea74f30 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 13 Jul 2024 07:20:18 -0700 Subject: [PATCH 214/423] [BitBoard] Build out the documentation --- bitboard/src/bitboard.rs | 125 +++++++++++++++++++++++++-------------- bitboard/src/library.rs | 2 +- 2 files changed, 81 insertions(+), 46 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 3787bb8..2b30b9a 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -7,6 +7,25 @@ use forward_ref::{forward_ref_binop, forward_ref_op_assign, forward_ref_unop}; use std::fmt; use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not}; +/// A bitfield representation of a chess board that uses the bits of a 64-bit +/// unsigned integer to represent whether a square on the board is occupied. +/// Squares are laid out as follows, starting at the bottom left, going row-wise, +/// and ending at the top right corner: +/// +/// ```text +/// +-------------------------+ +/// 8 | 56 57 58 59 60 61 62 63 | +/// 7 | 48 49 50 51 52 53 54 55 | +/// 6 | 40 41 42 43 44 45 46 47 | +/// 5 | 32 33 34 35 36 37 38 39 | +/// 4 | 24 25 26 27 28 29 30 31 | +/// 3 | 16 17 18 19 20 21 22 23 | +/// 2 | 8 9 10 11 12 13 14 15 | +/// 1 | 0 1 2 3 4 5 6 7 | +/// +-------------------------+ +/// A B C D E F G H +/// ``` +/// #[derive(Clone, Copy, Eq, Hash, PartialEq)] pub struct BitBoard(pub(crate) u64); @@ -78,27 +97,30 @@ impl BitBoard { } impl BitBoard { + /// Converts this [BitBoard] to an unsigned 64-bit integer. #[must_use] pub const fn as_bits(&self) -> u64 { self.0 } - /// Returns `true` if the [`BitBoard`] has no bits set. + /// Returns `true` if this [BitBoard] has no bits set. This is the opposite + /// of [`BitBoard::is_populated`]. /// /// ## Examples /// /// ``` /// use chessfriend_bitboard::BitBoard; - /// assert!(BitBoard::EMPTY.is_populated()); - /// assert!(!BitBoard::FULL.is_populated()); - /// assert!(!BitBoard::new(0b1000).is_populated()); + /// assert!(BitBoard::EMPTY.is_empty()); + /// assert!(!BitBoard::FULL.is_empty()); + /// assert!(!BitBoard::new(0b1000).is_empty()); /// ``` #[must_use] pub const fn is_empty(&self) -> bool { self.0 == 0 } - /// Returns `true` if the [`BitBoard`] has at least one bit set. + /// Returns `true` if the [BitBoard] has at least one bit set. This is the + /// opposite of [`BitBoard::is_empty`]. /// /// ## Examples /// @@ -112,13 +134,26 @@ impl BitBoard { self.0 != 0 } - /// Returns `true` if this [`BitBoard`] has the bit corresponding to `square` set. + /// Returns `true` if this [BitBoard] has the bit corresponding to `square` set. + /// + /// ## Examples + /// + /// ``` + /// use chessfriend_bitboard::BitBoard; + /// use chessfriend_core::Square; + /// + /// let square = Square::E4; + /// let mut bitboard = BitBoard::new(0b1001100); + /// + /// assert!(bitboard.contains(Square::C1)); + /// assert!(!bitboard.contains(Square::B1)); + /// ``` pub fn contains(self, square: Square) -> bool { let square_bitboard: BitBoard = square.into(); !(self & square_bitboard).is_empty() } - /// The number of 1 bits in the BitBoard. + /// Counts the number of set squares (1 bits) in this [BitBoard]. /// /// ## Examples /// @@ -132,11 +167,37 @@ impl BitBoard { self.0.count_ones() } + /// Set a square in this [BitBoard] by toggling the corresponding bit to 1. + /// This always succeeds, even if the bit was already set. + /// + /// ## Examples + /// + /// ``` + /// use chessfriend_bitboard::BitBoard; + /// use chessfriend_core::Square; + /// + /// let mut bitboard = BitBoard::new(0b1001100); + /// bitboard.set(Square::E4); + /// assert!(bitboard.contains(Square::E4)); + /// ``` pub fn set(&mut self, square: Square) { let square_bitboard: BitBoard = square.into(); self.0 |= square_bitboard.0 } + /// Clear a square (set it to 0) in this [BitBoard]. This always succeeds + /// even if the bit is not set. + /// + /// ## Examples + /// + /// ``` + /// use chessfriend_bitboard::BitBoard; + /// use chessfriend_core::Square; + /// + /// let mut bitboard = BitBoard::new(0b1001100); + /// bitboard.clear(Square::C1); + /// assert!(!bitboard.contains(Square::C1)); + /// ``` pub fn clear(&mut self, square: Square) { let square_bitboard: BitBoard = square.into(); self.0 &= !square_bitboard.0 @@ -156,25 +217,25 @@ impl BitBoard { pub fn is_single_square(&self) -> bool { self.0.is_power_of_two() } -} -impl BitBoard { - /// Returns an Iterator over the occupied squares. - /// - /// The Iterator yields squares starting from the leading (most-significant bit) end of the - /// board to the trailing (least-significant bit) end. + /// Return an Iterator over the occupied squares. The Iterator yields + /// squares starting from the leading (most-significant bit) end of the + /// board. #[must_use] pub fn occupied_squares(&self) -> impl Iterator { LeadingBitScanner::new(self.0).map(|idx| unsafe { Square::from_index(idx as u8) }) } - /// Return an Iterator over the occupied squares, starting from the trailing - /// (least-significant bit) end of the field. - #[must_use] + /// Return an Iterator over the occupied squares. The Iterator yields + /// squares starting from the trailing (least-significant bit) end of the + /// board. pub fn occupied_squares_trailing(&self) -> impl Iterator { TrailingBitScanner::new(self.0).map(|idx| unsafe { Square::from_index(idx as u8) }) } + /// If the board is not empty, returns the first occupied square on the + /// board, starting at the leading (most-significant) end of the board. If + /// the board is empty, returns `None`. #[must_use] pub fn first_occupied_square(&self) -> Option { let leading_zeros = self.0.leading_zeros() as u8; @@ -185,6 +246,9 @@ impl BitBoard { } } + /// If the board is not empty, returns the first occupied square on the + /// board, starting at the trailing (least-significant) end of the board. + /// If the board is empty, returns `None`. #[must_use] pub fn first_occupied_square_trailing(&self) -> Option { let trailing_zeros = self.0.trailing_zeros() as u8; @@ -380,35 +444,6 @@ mod tests { assert_eq!(BitBoard::rank(&7).0, 0xFF00000000000000, "Rank 8"); } - #[test] - fn is_empty() { - assert!(BitBoard(0).is_empty()); - assert!(!BitBoard(0xFF).is_empty()); - } - - #[test] - fn has_piece_at() { - let bb = BitBoard(0b1001100); - assert!(bb.is_set(Square::C1)); - assert!(!bb.is_set(Square::B1)); - } - - #[test] - fn set_square() { - let sq = Square::E4; - let mut bb = BitBoard(0b1001100); - bb.set_square(sq); - assert!(bb.is_set(sq)); - } - - #[test] - fn clear_square() { - let sq = Square::A3; - let mut bb = BitBoard(0b1001100); - bb.clear_square(sq); - assert!(!bb.is_set(sq)); - } - #[test] fn single_rank_occupancy() { let bb = BitBoard(0b01010100); diff --git a/bitboard/src/library.rs b/bitboard/src/library.rs index 7a45906..419f6e2 100644 --- a/bitboard/src/library.rs +++ b/bitboard/src/library.rs @@ -3,7 +3,7 @@ //! # The Bitboard Library //! //! This module implements a collection of commonly used bitboards that can be -//! looked up efficiently as needed. +//! looked up efficiently. //! //! The `library()` method returns a static instance of a `Library`, which //! provides getters for all available bitboards. From 534c022981c31f139bde33f541e0b1b1c032b774 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 13 Jul 2024 07:21:21 -0700 Subject: [PATCH 215/423] [core] Add #[must_use] to several methods in coordinates --- core/src/coordinates.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/src/coordinates.rs b/core/src/coordinates.rs index 4805a87..e650ad7 100644 --- a/core/src/coordinates.rs +++ b/core/src/coordinates.rs @@ -85,7 +85,6 @@ macro_rules! range_bound_struct { &self.0 } - #[must_use] $vis fn iter(&self) -> impl Iterator { (Self::FIRST.0..=Self::LAST.0).map(Self) } @@ -195,10 +194,12 @@ impl Rank { pub const PAWN_DOUBLE_PUSH_TARGET_RANKS: [Rank; 2] = [Rank::FOUR, Rank::FIVE]; + #[must_use] pub fn is_pawn_starting_rank(&self, color: Color) -> bool { self == &Self::PAWN_STARTING_RANKS[color as usize] } + #[must_use] pub fn is_pawn_double_push_target_rank(&self, color: Color) -> bool { self == &Self::PAWN_DOUBLE_PUSH_TARGET_RANKS[color as usize] } @@ -220,12 +221,14 @@ impl Square { /// # Safety /// /// This function does not do any bounds checking on the input. + #[must_use] pub unsafe fn from_index(x: u8) -> Square { debug_assert!((x as usize) < Self::NUM); Self::try_from(x).unwrap_unchecked() } #[inline] + #[must_use] pub fn from_file_rank(file: File, rank: Rank) -> Square { let file_int: u8 = file.into(); let rank_int: u8 = rank.into(); From 7c65232c35bed12a54e4dc79a5bc2e546ee51b9e Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 13 Jul 2024 07:34:50 -0700 Subject: [PATCH 216/423] [core] Improve API of Shape and Color Clean up type and method declarations by using better type spelling. Use more standard method spelling for iterators. Implement some useful traits. --- core/src/colors.rs | 6 +++++- core/src/pieces.rs | 45 +++++++++++++++++++++++++++++++++------------ 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/core/src/colors.rs b/core/src/colors.rs index b60531b..9d67877 100644 --- a/core/src/colors.rs +++ b/core/src/colors.rs @@ -17,10 +17,14 @@ impl Color { /// Slice of all possible colors pub const ALL: [Color; Color::NUM] = [Color::White, Color::Black]; - pub fn iter() -> impl Iterator { + pub fn iter() -> std::slice::Iter<'static, Color> { Color::ALL.iter() } + pub fn into_iter() -> std::array::IntoIter { + Color::ALL.into_iter() + } + /// The other color #[must_use] pub const fn other(&self) -> Color { diff --git a/core/src/pieces.rs b/core/src/pieces.rs index d980105..00d0536 100644 --- a/core/src/pieces.rs +++ b/core/src/pieces.rs @@ -1,7 +1,7 @@ // Eryn Wells use crate::{errors::TryFromCharError, try_from_string, Color, Square}; -use std::{fmt, slice::Iter}; +use std::{array, fmt, slice}; #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum Shape { @@ -14,7 +14,11 @@ pub enum Shape { } impl Shape { - pub const ALL: [Shape; 6] = [ + /// Number of piece shapes + pub const NUM: usize = 6; + + /// A slice of all piece shapes + pub const ALL: [Shape; Self::NUM] = [ Shape::Pawn, Shape::Knight, Shape::Bishop, @@ -23,11 +27,16 @@ impl Shape { Shape::King, ]; - pub fn iter() -> Iter<'static, Shape> { + pub fn iter() -> slice::Iter<'static, Self> { Shape::ALL.iter() } - pub fn promotable() -> Iter<'static, Shape> { + pub fn into_iter() -> array::IntoIter { + Shape::ALL.into_iter() + } + + /// An iterator over the shapes that a pawn can promote to + pub fn promotable() -> slice::Iter<'static, Shape> { const PROMOTABLE_SHAPES: [Shape; 4] = [Shape::Queen, Shape::Rook, Shape::Bishop, Shape::Knight]; @@ -64,15 +73,15 @@ impl TryFrom for Shape { try_from_string!(Shape); -impl Into for &Shape { - fn into(self) -> char { - self.to_ascii() +impl From<&Shape> for char { + fn from(shape: &Shape) -> char { + char::from(*shape) } } -impl Into for Shape { - fn into(self) -> char { - self.to_ascii() +impl From for char { + fn from(shape: Shape) -> char { + shape.to_ascii() } } @@ -174,6 +183,18 @@ impl fmt::Display for Piece { } } +impl From for Piece { + fn from(value: PlacedPiece) -> Self { + value.piece + } +} + +impl From<&PlacedPiece> for Piece { + fn from(value: &PlacedPiece) -> Self { + value.piece + } +} + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct PlacedPiece { piece: Piece, @@ -198,8 +219,8 @@ impl PlacedPiece { /// The [Piece] itself #[inline] #[must_use] - pub fn piece(&self) -> &Piece { - &self.piece + pub fn piece(&self) -> Piece { + self.piece } /// The square the piece is on From 3a2ead26687721267460c59bbb50f696f36f8f98 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 13 Jul 2024 08:08:12 -0700 Subject: [PATCH 217/423] [board] Replace Builder's BTreeMap with a Mailbox More efficient and easier to work with. :) --- board/src/builder.rs | 21 ++++++++------------- board/src/piece_sets/mailbox.rs | 24 +++++++++++++++++------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/board/src/builder.rs b/board/src/builder.rs index f52d18c..b20cd41 100644 --- a/board/src/builder.rs +++ b/board/src/builder.rs @@ -1,14 +1,13 @@ // Eryn Wells -use crate::{Board, Castle, EnPassant, Flags, PieceBitBoards}; -use chessfriend_core::{piece, Color, Piece, PlacedPiece, Rank, Shape, Square}; -use std::collections::BTreeMap; +use crate::{piece_sets::Mailbox, Board, Castle, EnPassant, Flags, PieceSet}; +use chessfriend_core::{piece, Color, PlacedPiece, Rank, Shape, Square}; #[derive(Clone)] pub struct Builder { player_to_move: Color, flags: Flags, - pieces: BTreeMap, + pieces: Mailbox, kings: [Option; 2], en_passant: Option, ply_counter: u16, @@ -23,11 +22,7 @@ impl Builder { #[must_use] pub fn from_board(board: &Board) -> Self { - let pieces = board - .pieces(Color::White) - .chain(board.pieces(Color::Black)) - .map(|placed_piece| (placed_piece.square(), *placed_piece.piece())) - .collect::>(); + let pieces = board.iter_all_pieces().collect::(); let white_king = board.king_square(Color::White); let black_king = board.king_square(Color::Black); @@ -72,12 +67,12 @@ impl Builder { let color_index: usize = color as usize; if let Some(king_square) = self.kings[color_index] { - self.pieces.remove(&king_square); + self.pieces.remove(king_square); } self.kings[color_index] = Some(square); } - self.pieces.insert(square, *piece.piece()); + self.pieces.set(piece.piece(), square); self } @@ -108,7 +103,7 @@ impl Builder { let parameters = castle.parameters(color); let has_rook_on_starting_square = self .pieces - .get(¶meters.rook_origin_square()) + .get(parameters.rook_origin_square()) .is_some_and(|piece| piece.shape() == Shape::Rook); let king_is_on_starting_square = self.kings[color as usize] == Some(parameters.king_origin_square()); @@ -148,7 +143,7 @@ impl Default for Builder { let white_king_square = Square::E1; let black_king_square = Square::E8; - let pieces = BTreeMap::from_iter([ + let pieces = Mailbox::from_iter([ (white_king_square, piece!(White King)), (black_king_square, piece!(Black King)), ]); diff --git a/board/src/piece_sets/mailbox.rs b/board/src/piece_sets/mailbox.rs index c551fb6..05cdf9b 100644 --- a/board/src/piece_sets/mailbox.rs +++ b/board/src/piece_sets/mailbox.rs @@ -4,24 +4,34 @@ use chessfriend_core::{Piece, PlacedPiece, Square}; use std::iter::FromIterator; #[derive(Clone, Debug, Eq, Hash, PartialEq)] -pub(super) struct Mailbox([Option; Square::NUM]); +pub(crate) struct Mailbox([Option; Square::NUM]); impl Mailbox { - pub(super) fn new() -> Self { + pub(crate) fn new() -> Self { Self::default() } - pub(super) fn get(&self, square: Square) -> Option { + pub(crate) fn get(&self, square: Square) -> Option { self.0[square as usize] } - pub(super) fn set(&mut self, square: Square, piece: Piece) { + pub(crate) fn set(&mut self, piece: Piece, square: Square) { self.0[square as usize] = Some(piece); } - pub(super) fn clear(&mut self, square: Square) { + pub(crate) fn remove(&mut self, square: Square) { self.0[square as usize] = None; } + + pub(crate) fn iter(&self) -> impl Iterator { + self.0 + .into_iter() + .flatten() // Remove the Nones + .zip(0u8..) // Enumerate with u8 instead of usize + .map(|(piece, index)| { + PlacedPiece::new(piece, unsafe { Square::from_index_unchecked(index) }) + }) + } } impl Default for Mailbox { @@ -40,7 +50,7 @@ impl FromIterator for Mailbox { fn from_iter>(iter: T) -> Self { let mut mailbox = Self::new(); for placed_piece in iter { - mailbox.set(placed_piece.square(), placed_piece.piece()); + mailbox.set(placed_piece.piece(), placed_piece.square()); } mailbox @@ -51,7 +61,7 @@ impl FromIterator<(Square, Piece)> for Mailbox { fn from_iter>(iter: T) -> Self { let mut mailbox = Self::new(); for (square, piece) in iter { - mailbox.set(square, piece); + mailbox.set(piece, square); } mailbox From d9c2cfb90c6bc6b30a642b4c5d7c4765360cf387 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 13 Jul 2024 08:08:26 -0700 Subject: [PATCH 218/423] [board] Copy fen.rs here from the position crate --- board/src/fen.rs | 330 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 board/src/fen.rs diff --git a/board/src/fen.rs b/board/src/fen.rs new file mode 100644 index 0000000..bd72ccd --- /dev/null +++ b/board/src/fen.rs @@ -0,0 +1,330 @@ +// Eryn Wells + +use crate::{Board, Builder, Castle, EnPassant}; +use chessfriend_core::{ + coordinates::ParseSquareError, piece, Color, File, Piece, PlacedPiece, Rank, Square, +}; +use std::fmt::Write; + +#[macro_export] +macro_rules! fen { + ($fen_string:literal) => { + Board::from_fen_str($fen_string) + }; +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ToFenStrError { + FmtError(std::fmt::Error), +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum FromFenStrError { + MissingField(Field), + MissingPlacement, + InvalidValue, + ParseIntError(std::num::ParseIntError), + ParseSquareError(ParseSquareError), +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Field { + Placements, + PlayerToMove, + CastlingRights, + EnPassantSquare, + HalfMoveClock, + FullMoveCounter, +} + +pub trait ToFenStr { + type Error; + + /// Create a FEN string from `Self`. + /// + /// # Errors + /// + /// + fn to_fen_str(&self) -> Result; +} + +pub trait FromFenStr: Sized { + type Error; + + /// Create a `Self` from a FEN string. + /// + /// # Errors + /// + /// + fn from_fen_str(string: &str) -> Result; +} + +impl ToFenStr for Board { + type Error = ToFenStrError; + + fn to_fen_str(&self) -> Result { + let mut fen_string = String::new(); + + let mut empty_squares: u8 = 0; + for rank in Rank::ALL.into_iter().rev() { + for file in File::ALL { + let square = Square::from_file_rank(file, rank); + match self.piece_on_square(square) { + Some(piece) => { + if empty_squares > 0 { + write!(fen_string, "{empty_squares}") + .map_err(ToFenStrError::FmtError)?; + empty_squares = 0; + } + write!(fen_string, "{}", piece.to_fen_str()?) + .map_err(ToFenStrError::FmtError)?; + } + None => empty_squares += 1, + } + } + + if empty_squares > 0 { + write!(fen_string, "{empty_squares}").map_err(ToFenStrError::FmtError)?; + empty_squares = 0; + } + + if rank != Rank::ONE { + write!(fen_string, "/").map_err(ToFenStrError::FmtError)?; + } + } + + write!(fen_string, " {}", self.player_to_move().to_fen_str()?) + .map_err(ToFenStrError::FmtError)?; + + let castling = [ + (Color::White, Castle::KingSide), + (Color::White, Castle::QueenSide), + (Color::Black, Castle::KingSide), + (Color::Black, Castle::QueenSide), + ] + .map(|(color, castle)| { + let can_castle = self.player_has_right_to_castle(color, castle); + if !can_castle { + return ""; + } + + match (color, castle) { + (Color::White, Castle::KingSide) => "K", + (Color::White, Castle::QueenSide) => "Q", + (Color::Black, Castle::KingSide) => "k", + (Color::Black, Castle::QueenSide) => "q", + } + }) + .concat(); + + write!( + fen_string, + " {}", + if castling.is_empty() { "-" } else { &castling } + ) + .map_err(ToFenStrError::FmtError)?; + + write!( + fen_string, + " {}", + self.en_passant() + .map_or("-".to_string(), |ep| ep.target_square().to_string()) + ) + .map_err(ToFenStrError::FmtError)?; + + write!(fen_string, " {}", self.ply_counter()).map_err(ToFenStrError::FmtError)?; + write!(fen_string, " {}", self.move_number()).map_err(ToFenStrError::FmtError)?; + + Ok(fen_string) + } +} + +impl ToFenStr for Color { + type Error = ToFenStrError; + + fn to_fen_str(&self) -> Result { + match self { + Color::White => Ok("w".to_string()), + Color::Black => Ok("b".to_string()), + } + } +} + +impl ToFenStr for Piece { + type Error = ToFenStrError; + + fn to_fen_str(&self) -> Result { + let ascii: char = self.to_ascii(); + Ok(String::from(match self.color() { + Color::White => ascii.to_ascii_uppercase(), + Color::Black => ascii.to_ascii_lowercase(), + })) + } +} + +impl ToFenStr for PlacedPiece { + type Error = ToFenStrError; + + fn to_fen_str(&self) -> Result { + self.piece().to_fen_str() + } +} + +impl FromFenStr for Board { + type Error = FromFenStrError; + + fn from_fen_str(string: &str) -> Result { + let mut builder = Builder::default(); + + let mut fields = string.split(' '); + + let placements = fields + .next() + .ok_or(FromFenStrError::MissingField(Field::Placements))?; + let ranks = placements.split('/'); + + for (rank, pieces) in Rank::ALL.iter().rev().zip(ranks) { + let mut files = File::ALL.iter(); + for ch in pieces.chars() { + if let Some(skip) = ch.to_digit(10) { + // TODO: Use advance_by() when it's available. + for _ in 0..skip { + files.next(); + } + + continue; + } + + let file = files.next().ok_or(FromFenStrError::MissingPlacement)?; + let piece = Piece::from_fen_str(&ch.to_string())?; + + builder.place_piece(PlacedPiece::new( + piece, + Square::from_file_rank(*file, *rank), + )); + } + + debug_assert_eq!(files.next(), None); + } + + let player_to_move = Color::from_fen_str( + fields + .next() + .ok_or(FromFenStrError::MissingField(Field::PlayerToMove))?, + )?; + builder.to_move(player_to_move); + + let castling_rights = fields + .next() + .ok_or(FromFenStrError::MissingField(Field::CastlingRights))?; + if castling_rights == "-" { + builder.no_castling_rights(); + } else { + for ch in castling_rights.chars() { + match ch { + 'K' => builder.player_can_castle(Color::White, Castle::KingSide), + 'Q' => builder.player_can_castle(Color::White, Castle::QueenSide), + 'k' => builder.player_can_castle(Color::Black, Castle::KingSide), + 'q' => builder.player_can_castle(Color::Black, Castle::QueenSide), + _ => return Err(FromFenStrError::InvalidValue), + }; + } + } + + let en_passant_square = fields + .next() + .ok_or(FromFenStrError::MissingField(Field::EnPassantSquare))?; + if en_passant_square != "-" { + let square = Square::from_algebraic_str(en_passant_square) + .map_err(FromFenStrError::ParseSquareError)?; + builder.en_passant(Some(EnPassant::from_target_square(square).unwrap())); + } + + let half_move_clock = fields + .next() + .ok_or(FromFenStrError::MissingField(Field::HalfMoveClock))?; + let half_move_clock: u16 = half_move_clock + .parse() + .map_err(FromFenStrError::ParseIntError)?; + builder.ply_counter(half_move_clock); + + let full_move_counter = fields + .next() + .ok_or(FromFenStrError::MissingField(Field::FullMoveCounter))?; + let full_move_counter: u16 = full_move_counter + .parse() + .map_err(FromFenStrError::ParseIntError)?; + builder.move_number(full_move_counter); + + debug_assert_eq!(fields.next(), None); + + Ok(builder.build()) + } +} + +impl FromFenStr for Color { + type Error = FromFenStrError; + + fn from_fen_str(string: &str) -> Result { + if string.len() != 1 { + return Err(FromFenStrError::InvalidValue); + } + + match string.chars().take(1).next().unwrap() { + 'w' => Ok(Color::White), + 'b' => Ok(Color::Black), + _ => Err(FromFenStrError::InvalidValue), + } + } +} + +impl FromFenStr for Piece { + type Error = FromFenStrError; + + fn from_fen_str(string: &str) -> Result { + if string.len() != 1 { + return Err(FromFenStrError::InvalidValue); + } + + match string.chars().take(1).next().unwrap() { + 'P' => Ok(piece!(White Pawn)), + 'N' => Ok(piece!(White Knight)), + 'B' => Ok(piece!(White Bishop)), + 'R' => Ok(piece!(White Rook)), + 'Q' => Ok(piece!(White Queen)), + 'K' => Ok(piece!(White King)), + 'p' => Ok(piece!(Black Pawn)), + 'n' => Ok(piece!(Black Knight)), + 'b' => Ok(piece!(Black Bishop)), + 'r' => Ok(piece!(Black Rook)), + 'q' => Ok(piece!(Black Queen)), + 'k' => Ok(piece!(Black King)), + _ => Err(FromFenStrError::InvalidValue), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_board; + + #[test] + fn starting_position() { + let pos = test_board!(starting); + + assert_eq!( + pos.to_fen_str(), + Ok(String::from( + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" + )) + ); + } + + #[test] + fn from_starting_fen() { + let board = fen!("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1").unwrap(); + let expected = Board::starting(); + assert_eq!(board, expected, "{board:#?}\n{expected:#?}"); + } +} From e8c3d2b8db5b1fcf7aacd4f1146dacb29b67ee1a Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 13 Jul 2024 08:08:48 -0700 Subject: [PATCH 219/423] [board] Add documentation to the types in castle.rs --- board/src/castle.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/board/src/castle.rs b/board/src/castle.rs index 17b2942..3534d8d 100644 --- a/board/src/castle.rs +++ b/board/src/castle.rs @@ -17,10 +17,12 @@ pub struct Parameters { /// Target or destination squares for the king and rook. target: Squares, - /// The set of squares that must be clear of any pieces in order to perform this castle. + /// The set of squares that must be clear of any pieces in order to perform + /// this castle. clear: BitBoard, - /// The set of squares that must not be attacked in order to perform this castle. + /// The set of squares that must not be attacked (i.e. visible to opposing + /// pieces) in order to perform this castle. check: BitBoard, } @@ -41,10 +43,14 @@ impl Parameters { self.target.rook } + /// A [`BitBoard`] of the squares that must be clear of any piece in order + /// to perform this castle move. pub fn clear_squares(&self) -> &BitBoard { &self.clear } + /// A [`BitBoard`] of the squares that must not be visible to opposing + /// pieces in order to perform this castle move. pub fn check_squares(&self) -> &BitBoard { &self.check } @@ -60,7 +66,7 @@ impl Castle { pub const ALL: [Castle; 2] = [Castle::KingSide, Castle::QueenSide]; /// Parameters for each castling move, organized by color and board-side. - const PARAMETERS: [[Parameters; 2]; 2] = [ + const PARAMETERS: [[Parameters; 2]; Color::NUM] = [ [ Parameters { origin: Squares { From c290f00b9ec5519e714b58f6191b6ecbb0282d71 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 13 Jul 2024 08:09:02 -0700 Subject: [PATCH 220/423] [board] fen declaration --- board/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/board/src/lib.rs b/board/src/lib.rs index e599040..d22d32b 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -2,6 +2,7 @@ pub mod castle; pub mod en_passant; +pub mod fen; mod board; mod builder; From ee51a138701b7d1f14bcd6097d72bcdd0259d649 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 13 Jul 2024 08:10:21 -0700 Subject: [PATCH 221/423] =?UTF-8?q?Rename=20Square::from=5Findex=20?= =?UTF-8?q?=E2=86=92=20from=5Findex=5Funchecked?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bitboard/src/bitboard.rs | 15 ++++++++++----- core/src/coordinates.rs | 7 ++++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 2b30b9a..3f18a78 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -223,14 +223,15 @@ impl BitBoard { /// board. #[must_use] pub fn occupied_squares(&self) -> impl Iterator { - LeadingBitScanner::new(self.0).map(|idx| unsafe { Square::from_index(idx as u8) }) + LeadingBitScanner::new(self.0).map(|idx| unsafe { Square::from_index_unchecked(idx as u8) }) } /// Return an Iterator over the occupied squares. The Iterator yields /// squares starting from the trailing (least-significant bit) end of the /// board. pub fn occupied_squares_trailing(&self) -> impl Iterator { - TrailingBitScanner::new(self.0).map(|idx| unsafe { Square::from_index(idx as u8) }) + TrailingBitScanner::new(self.0) + .map(|idx| unsafe { Square::from_index_unchecked(idx as u8) }) } /// If the board is not empty, returns the first occupied square on the @@ -240,7 +241,11 @@ impl BitBoard { pub fn first_occupied_square(&self) -> Option { let leading_zeros = self.0.leading_zeros() as u8; if leading_zeros < Square::NUM as u8 { - unsafe { Some(Square::from_index(Square::NUM as u8 - leading_zeros - 1)) } + unsafe { + Some(Square::from_index_unchecked( + Square::NUM as u8 - leading_zeros - 1, + )) + } } else { None } @@ -253,7 +258,7 @@ impl BitBoard { pub fn first_occupied_square_trailing(&self) -> Option { let trailing_zeros = self.0.trailing_zeros() as u8; if trailing_zeros < Square::NUM as u8 { - unsafe { Some(Square::from_index(trailing_zeros)) } + unsafe { Some(Square::from_index_unchecked(trailing_zeros)) } } else { None } @@ -318,7 +323,7 @@ impl TryFrom for Square { return Err(TryFromBitBoardError::NotSingleSquare); } - unsafe { Ok(Square::from_index(value.0.trailing_zeros() as u8)) } + unsafe { Ok(Square::from_index_unchecked(value.0.trailing_zeros() as u8)) } } } diff --git a/core/src/coordinates.rs b/core/src/coordinates.rs index e650ad7..e796949 100644 --- a/core/src/coordinates.rs +++ b/core/src/coordinates.rs @@ -220,9 +220,10 @@ coordinate_enum!(Square, [ impl Square { /// # Safety /// - /// This function does not do any bounds checking on the input. + /// This function does not do any bounds checking on the input. In debug + /// builds, this function will assert that the argument is in bounds. #[must_use] - pub unsafe fn from_index(x: u8) -> Square { + pub unsafe fn from_index_unchecked(x: u8) -> Square { debug_assert!((x as usize) < Self::NUM); Self::try_from(x).unwrap_unchecked() } @@ -232,7 +233,7 @@ impl Square { pub fn from_file_rank(file: File, rank: Rank) -> Square { let file_int: u8 = file.into(); let rank_int: u8 = rank.into(); - unsafe { Self::from_index(rank_int << 3 | file_int) } + unsafe { Self::from_index_unchecked(rank_int << 3 | file_int) } } pub fn from_algebraic_str(s: &str) -> Result { From 634f9e02f489232260162c02b3b84b426c9588a2 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 13 Jul 2024 08:10:59 -0700 Subject: [PATCH 222/423] [board] Mailbox-related changes --- board/src/piece_sets.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/board/src/piece_sets.rs b/board/src/piece_sets.rs index f7435e5..46f0164 100644 --- a/board/src/piece_sets.rs +++ b/board/src/piece_sets.rs @@ -3,10 +3,11 @@ mod bitboards; mod mailbox; +pub(crate) use mailbox::Mailbox; + use bitboards::{ByColor, ByColorAndShape}; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; -use mailbox::Mailbox; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum PlacePieceStrategy { @@ -52,7 +53,7 @@ impl PieceSet { for s in Shape::into_iter() { let bitboard = pieces[c as usize][s as usize]; for square in bitboard.occupied_squares() { - mailbox.set(square, Piece::new(c, s)); + mailbox.set(Piece::new(c, s), square); } } } @@ -114,7 +115,7 @@ impl PieceSet { let piece: Piece = piece.into(); self.by_color_and_shape.set_square(square, piece); self.by_color.set_square(square, color); - self.mailbox.set(square, piece); + self.mailbox.set(piece, square); Ok(PlacedPiece::new(piece, square)) } @@ -123,7 +124,7 @@ impl PieceSet { if let Some(piece) = self.mailbox.get(square) { self.by_color_and_shape.clear_square(square, piece.into()); self.by_color.clear_square(square, piece.color()); - self.mailbox.clear(square); + self.mailbox.remove(square); Some(PlacedPiece::new(piece, square)) } else { From f96fa79dc1f61ad95feb1a3ff11fe99b2802a897 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 13 Jul 2024 08:14:33 -0700 Subject: [PATCH 223/423] [position] Remove fen.rs --- position/src/fen.rs | 282 -------------------------------------------- position/src/lib.rs | 2 - 2 files changed, 284 deletions(-) delete mode 100644 position/src/fen.rs diff --git a/position/src/fen.rs b/position/src/fen.rs deleted file mode 100644 index fde920c..0000000 --- a/position/src/fen.rs +++ /dev/null @@ -1,282 +0,0 @@ -// Eryn Wells - -use crate::{Position, PositionBuilder}; -use chessfriend_core::{piece, Color, File, Piece, PlacedPiece, Rank, Square}; -use chessfriend_moves::{Castle, EnPassant}; -use std::fmt::Write; - -#[macro_export] -macro_rules! fen { - ($fen_string:literal) => { - Position::from_fen_str($fen_string) - }; -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum ToFenError { - FmtError(std::fmt::Error), -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct FromFenError; - -pub trait ToFen { - type Error; - fn to_fen(&self) -> Result; -} - -pub trait FromFen: Sized { - type Error; - fn from_fen_str(string: &str) -> Result; -} - -impl ToFen for Position { - type Error = ToFenError; - - fn to_fen(&self) -> Result { - let mut fen_string = String::new(); - - let mut empty_squares: u8 = 0; - for rank in Rank::ALL.iter().rev() { - for file in File::ALL.iter() { - let square = Square::from_file_rank(*file, *rank); - match self.piece_on_square(square) { - Some(piece) => { - if empty_squares > 0 { - write!(fen_string, "{}", empty_squares) - .map_err(|err| ToFenError::FmtError(err))?; - empty_squares = 0; - } - write!(fen_string, "{}", piece.to_fen()?) - .map_err(|err| ToFenError::FmtError(err))?; - } - None => empty_squares += 1, - } - } - - if empty_squares > 0 { - write!(fen_string, "{}", empty_squares).map_err(|err| ToFenError::FmtError(err))?; - empty_squares = 0; - } - if rank != &Rank::ONE { - write!(fen_string, "/").map_err(|err| ToFenError::FmtError(err))?; - } - } - - write!(fen_string, " {}", self.player_to_move().to_fen()?) - .map_err(|err| ToFenError::FmtError(err))?; - - let castling = [ - (Color::White, Castle::KingSide), - (Color::White, Castle::QueenSide), - (Color::Black, Castle::KingSide), - (Color::Black, Castle::QueenSide), - ] - .map(|(color, castle)| { - let can_castle = self.player_has_right_to_castle(color, castle); - if !can_castle { - "" - } else { - match (color, castle) { - (Color::White, Castle::KingSide) => "K", - (Color::White, Castle::QueenSide) => "Q", - (Color::Black, Castle::KingSide) => "k", - (Color::Black, Castle::QueenSide) => "q", - } - } - }) - .concat(); - - write!( - fen_string, - " {}", - if castling.len() > 0 { &castling } else { "-" } - ) - .map_err(|err| ToFenError::FmtError(err))?; - - write!( - fen_string, - " {}", - self.en_passant() - .map_or("-".to_string(), |ep| ep.target_square().to_string()) - ) - .map_err(|err| ToFenError::FmtError(err))?; - - write!(fen_string, " {}", self.ply_counter()).map_err(|err| ToFenError::FmtError(err))?; - write!(fen_string, " {}", self.move_number()).map_err(|err| ToFenError::FmtError(err))?; - - Ok(fen_string) - } -} - -impl ToFen for Color { - type Error = ToFenError; - - fn to_fen(&self) -> Result { - match self { - Color::White => Ok("w".to_string()), - Color::Black => Ok("b".to_string()), - } - } -} - -impl ToFen for Piece { - type Error = ToFenError; - - fn to_fen(&self) -> Result { - let ascii: char = self.to_ascii(); - Ok(String::from(match self.color() { - Color::White => ascii.to_ascii_uppercase(), - Color::Black => ascii.to_ascii_lowercase(), - })) - } -} - -impl ToFen for PlacedPiece { - type Error = ToFenError; - - fn to_fen(&self) -> Result { - Ok(self.piece().to_fen()?) - } -} - -impl FromFen for Position { - type Error = FromFenError; - - fn from_fen_str(string: &str) -> Result { - let mut builder = PositionBuilder::empty(); - - let mut fields = string.split(" "); - - let placements = fields.next().ok_or(FromFenError)?; - let ranks = placements.split("/"); - - for (rank, pieces) in Rank::ALL.iter().rev().zip(ranks) { - let mut files = File::ALL.iter(); - for ch in pieces.chars() { - if let Some(skip) = ch.to_digit(10) { - // TODO: Use advance_by() when it's available. - for _ in 0..skip { - files.next(); - } - - continue; - } - - let file = files.next().ok_or(FromFenError)?; - let piece = Piece::from_fen_str(&ch.to_string())?; - - builder.place_piece(PlacedPiece::new( - piece, - Square::from_file_rank(*file, *rank), - )); - } - - debug_assert_eq!(files.next(), None); - } - - let player_to_move = Color::from_fen_str(fields.next().ok_or(FromFenError)?)?; - builder.to_move(player_to_move); - - let castling_rights = fields.next().ok_or(FromFenError)?; - if castling_rights == "-" { - builder.no_castling_rights(); - } else { - for ch in castling_rights.chars() { - match ch { - 'K' => builder.player_can_castle(Color::White, Castle::KingSide), - 'Q' => builder.player_can_castle(Color::White, Castle::QueenSide), - 'k' => builder.player_can_castle(Color::Black, Castle::KingSide), - 'q' => builder.player_can_castle(Color::Black, Castle::QueenSide), - _ => return Err(FromFenError), - }; - } - } - - let en_passant_square = fields.next().ok_or(FromFenError)?; - if en_passant_square != "-" { - let square = Square::from_algebraic_str(en_passant_square).map_err(|_| FromFenError)?; - builder.en_passant(Some(EnPassant::from_target_square(square).unwrap())); - } - - let half_move_clock = fields.next().ok_or(FromFenError)?; - let half_move_clock: u16 = half_move_clock.parse().map_err(|_| FromFenError)?; - builder.ply_counter(half_move_clock); - - let full_move_counter = fields.next().ok_or(FromFenError)?; - let full_move_counter: u16 = full_move_counter.parse().map_err(|_| FromFenError)?; - builder.move_number(full_move_counter); - - debug_assert_eq!(fields.next(), None); - - Ok(builder.build()) - } -} - -impl FromFen for Color { - type Error = FromFenError; - - fn from_fen_str(string: &str) -> Result { - if string.len() != 1 { - return Err(FromFenError); - } - - match string.chars().take(1).next().unwrap() { - 'w' => Ok(Color::White), - 'b' => Ok(Color::Black), - _ => Err(FromFenError), - } - } -} - -impl FromFen for Piece { - type Error = FromFenError; - - fn from_fen_str(string: &str) -> Result { - if string.len() != 1 { - return Err(FromFenError); - } - - match string.chars().take(1).next().unwrap() { - 'P' => Ok(piece!(White Pawn)), - 'N' => Ok(piece!(White Knight)), - 'B' => Ok(piece!(White Bishop)), - 'R' => Ok(piece!(White Rook)), - 'Q' => Ok(piece!(White Queen)), - 'K' => Ok(piece!(White King)), - 'p' => Ok(piece!(Black Pawn)), - 'n' => Ok(piece!(Black Knight)), - 'b' => Ok(piece!(Black Bishop)), - 'r' => Ok(piece!(Black Rook)), - 'q' => Ok(piece!(Black Queen)), - 'k' => Ok(piece!(Black King)), - _ => Err(FromFenError), - } - } -} - -#[cfg(test)] -mod tests { - use crate::test_position; - - use super::*; - - #[test] - fn starting_position() { - let pos = test_position!(starting); - - assert_eq!( - pos.to_fen(), - Ok(String::from( - "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" - )) - ); - } - - #[test] - fn from_starting_fen() { - let pos = fen!("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1").unwrap(); - let expected = Position::starting(); - assert_eq!(pos, expected, "{pos:#?}\n{expected:#?}"); - } -} diff --git a/position/src/lib.rs b/position/src/lib.rs index 1995be0..c740782 100644 --- a/position/src/lib.rs +++ b/position/src/lib.rs @@ -1,7 +1,5 @@ // Eryn Wells -pub mod fen; - mod check; mod display; mod move_generator; From 51de32fa0a28c4ac959f7f9a21b3fd7bc0e3a22a Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 13 Jul 2024 08:15:14 -0700 Subject: [PATCH 224/423] =?UTF-8?q?[board]=20Update=20reference=20to=20Pie?= =?UTF-8?q?ceBitBoard=20=E2=86=92=20PieceSet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This isn't all of them... --- board/src/board.rs | 13 ++++++++----- board/src/lib.rs | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/board/src/board.rs b/board/src/board.rs index 94c2d97..f0c1192 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -1,6 +1,9 @@ // Eryn Wells -use crate::{display::DiagramFormatter, Castle, EnPassant, Flags, PieceBitBoards, Pieces}; +use crate::{ + display::DiagramFormatter, piece_sets::PlacePieceError, Castle, EnPassant, Flags, PieceSet, + Pieces, +}; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; @@ -8,7 +11,7 @@ use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; pub struct Board { player_to_move: Color, flags: Flags, - pieces: PieceBitBoards, + pieces: PieceSet, en_passant: Option, half_move_counter: u16, full_move_number: u16, @@ -44,7 +47,7 @@ impl Board { Self { player_to_move: Color::White, - pieces: PieceBitBoards::new([WHITE_PIECES, BLACK_PIECES]), + pieces: PieceSet::new([WHITE_PIECES, BLACK_PIECES]), ..Default::default() } } @@ -52,7 +55,7 @@ impl Board { pub(crate) fn new( player_to_move: Color, flags: Flags, - pieces: PieceBitBoards, + pieces: PieceSet, en_passant: Option, half_move_counter: u16, full_move_number: u16, @@ -204,7 +207,7 @@ impl Default for Board { Self { player_to_move: Color::White, flags: Flags::default(), - pieces: PieceBitBoards::default(), + pieces: PieceSet::default(), en_passant: None, half_move_counter: 0, full_move_number: 1, diff --git a/board/src/lib.rs b/board/src/lib.rs index d22d32b..6192053 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -18,5 +18,5 @@ pub use builder::Builder; use castle::Castle; use en_passant::EnPassant; use flags::Flags; -use piece_sets::PieceBitBoards; use pieces::Pieces; +use piece_sets::PieceSet; From 90e33d1202051f8fa0c421758d2db8d6531d5e2c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 13 Jul 2024 11:51:52 -0700 Subject: [PATCH 225/423] [board] Replace Pieces iterator with Mailbox::iter() Mailbox can use a standard iterator because it's just a slice. So nice! --- board/src/board.rs | 13 +++-- board/src/lib.rs | 2 - board/src/pieces.rs | 127 -------------------------------------------- 3 files changed, 10 insertions(+), 132 deletions(-) delete mode 100644 board/src/pieces.rs diff --git a/board/src/board.rs b/board/src/board.rs index f0c1192..93402dc 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -2,10 +2,10 @@ use crate::{ display::DiagramFormatter, piece_sets::PlacePieceError, Castle, EnPassant, Flags, PieceSet, - Pieces, }; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; +use std::iter::Iterator; #[derive(Clone, Debug, Eq)] pub struct Board { @@ -154,8 +154,15 @@ impl Board { } #[must_use] - pub fn pieces(&self, color: Color) -> Pieces { - Pieces::new(self, color) + pub fn iter_all_pieces(&self) -> impl Iterator + '_ { + self.pieces.iter() + } + + #[must_use] + pub fn iter_pieces_of_color(&self, color: Color) -> impl Iterator + '_ { + self.pieces + .iter() + .filter(move |piece| piece.color() == color) } #[must_use] diff --git a/board/src/lib.rs b/board/src/lib.rs index 6192053..fc37d9d 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -10,7 +10,6 @@ mod display; mod flags; mod macros; mod piece_sets; -mod pieces; pub use board::Board; pub use builder::Builder; @@ -18,5 +17,4 @@ pub use builder::Builder; use castle::Castle; use en_passant::EnPassant; use flags::Flags; -use pieces::Pieces; use piece_sets::PieceSet; diff --git a/board/src/pieces.rs b/board/src/pieces.rs deleted file mode 100644 index 537db20..0000000 --- a/board/src/pieces.rs +++ /dev/null @@ -1,127 +0,0 @@ -// Eryn Wells - -use super::Board; -use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; - -pub struct Pieces<'a> { - color: Color, - board: &'a Board, - current_shape: Option, - shape_iterator: Box>, - square_iterator: Option>>, -} - -impl<'a> Pieces<'a> { - pub(crate) fn new(board: &Board, color: Color) -> Pieces { - Pieces { - color, - board, - current_shape: None, - shape_iterator: Box::new(Shape::iter()), - square_iterator: None, - } - } -} - -impl<'a> Iterator for Pieces<'a> { - type Item = PlacedPiece; - - fn next(&mut self) -> Option { - if let Some(square_iterator) = &mut self.square_iterator { - if let (Some(square), Some(shape)) = (square_iterator.next(), self.current_shape) { - return Some(PlacedPiece::new(Piece::new(self.color, shape), square)); - } - } - - let mut current_shape: Option = None; - let mut next_nonempty_bitboard: Option<&BitBoard> = None; - - for shape in self.shape_iterator.by_ref() { - let piece = Piece::new(self.color, *shape); - - let bitboard = self.board.bitboard_for_piece(piece); - if bitboard.is_empty() { - continue; - } - - next_nonempty_bitboard = Some(bitboard); - current_shape = Some(*shape); - - break; - } - - if let (Some(bitboard), Some(shape)) = (next_nonempty_bitboard, current_shape) { - let mut square_iterator = bitboard.occupied_squares(); - - let mut next_placed_piece: Option = None; - if let Some(square) = square_iterator.next() { - next_placed_piece = Some(PlacedPiece::new(Piece::new(self.color, shape), square)); - } - - self.square_iterator = Some(Box::new(square_iterator)); - self.current_shape = Some(shape); - - return next_placed_piece; - } - - None - } -} - -#[cfg(test)] -mod tests { - use crate::{test_board, Board, Builder}; - use chessfriend_core::{piece, Color}; - use std::collections::HashSet; - - #[test] - fn empty() { - let board = Board::empty(); - let mut pieces = board.pieces(Color::White); - assert_eq!(pieces.next(), None); - } - - #[test] - fn one() { - let pos = Builder::new() - .place_piece(piece!(White Queen on E4)) - .build(); - println!("{:#?}", &pos); - - let mut pieces = pos.pieces(Color::White); - assert_eq!(pieces.next(), Some(piece!(White Queen on E4))); - assert_eq!(pieces.next(), Some(piece!(White King on E1))); - assert_eq!(pieces.next(), None); - } - - #[test] - fn multiple_pieces() { - let board = test_board![ - White Queen on E4, - White King on A1, - White Pawn on B2, - White Pawn on C2, - ]; - - let expected_placed_pieces = HashSet::from([ - piece!(White Queen on E4), - piece!(White King on A1), - piece!(White Pawn on B2), - piece!(White Pawn on C2), - ]); - - let placed_pieces = HashSet::from_iter(board.pieces(Color::White)); - - assert_eq!( - placed_pieces, - expected_placed_pieces, - "{:#?}", - placed_pieces - .symmetric_difference(&expected_placed_pieces) - .into_iter() - .map(|pp| format!("{}", pp)) - .collect::>() - ); - } -} From da4e2f1d505d93af7811b517c9b128bb13df9f7f Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 13 Jul 2024 11:52:25 -0700 Subject: [PATCH 226/423] [core] Use Color::default() instead of Color::White in Board::default() --- board/src/board.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/board/src/board.rs b/board/src/board.rs index 93402dc..308cbf7 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -212,7 +212,7 @@ impl Board { impl Default for Board { fn default() -> Self { Self { - player_to_move: Color::White, + player_to_move: Color::default(), flags: Flags::default(), pieces: PieceSet::default(), en_passant: None, From fff2e084b5ee2b4171b2d500a42becb50e47dab8 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 13 Jul 2024 11:52:50 -0700 Subject: [PATCH 227/423] [board] Remove turbofish from PieceSet pipeline in Builder::build() --- board/src/builder.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/board/src/builder.rs b/board/src/builder.rs index b20cd41..e4f8ec0 100644 --- a/board/src/builder.rs +++ b/board/src/builder.rs @@ -89,18 +89,18 @@ impl Builder { } pub fn build(&self) -> Board { - let pieces = self + let pieces: PieceSet = self .pieces .iter() - .map(PlacedPiece::from) .filter(Self::is_piece_placement_valid) - .collect::(); + .collect(); let mut flags = self.flags; for color in Color::ALL { for castle in Castle::ALL { let parameters = castle.parameters(color); + let has_rook_on_starting_square = self .pieces .get(parameters.rook_origin_square()) From a14cb30781c355992405b236bb9e8424527d6a00 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 13 Jul 2024 11:53:16 -0700 Subject: [PATCH 228/423] [board] Implement PieceSet::iter() as an iterator over its Mailbox --- board/src/piece_sets.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/board/src/piece_sets.rs b/board/src/piece_sets.rs index 46f0164..5f5b281 100644 --- a/board/src/piece_sets.rs +++ b/board/src/piece_sets.rs @@ -75,6 +75,10 @@ impl PieceSet { self.by_color.bitboard(color) } + pub(crate) fn iter(&self) -> impl Iterator { + self.mailbox.iter() + } + pub(super) fn bitboard_for_color(&self, color: Color) -> BitBoard { self.by_color.bitboard(color) } From fb6d1ad2f0f5e095d26383e6abdffa7e29f985eb Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 13 Jul 2024 11:53:43 -0700 Subject: [PATCH 229/423] [board] Make the display module public --- board/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/board/src/lib.rs b/board/src/lib.rs index fc37d9d..0dcfc08 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -1,12 +1,12 @@ // Eryn Wells pub mod castle; +pub mod display; pub mod en_passant; pub mod fen; mod board; mod builder; -mod display; mod flags; mod macros; mod piece_sets; From 7a46d52e8dad809189beb94fe9e54fae58e80432 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 13 Jul 2024 11:54:00 -0700 Subject: [PATCH 230/423] [board] Make the flags and macros modules public --- board/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/board/src/lib.rs b/board/src/lib.rs index 0dcfc08..0a5516a 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -4,11 +4,11 @@ pub mod castle; pub mod display; pub mod en_passant; pub mod fen; +pub mod flags; +pub mod macros; mod board; mod builder; -mod flags; -mod macros; mod piece_sets; pub use board::Board; From 26ae79e17d7158acd5a92c1b5b64688910db4987 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 13 Jul 2024 11:54:58 -0700 Subject: [PATCH 231/423] [board] Remove explicit #[inline] from several Board methods Trust the compiler to inline where appropriate, until I know otherwise. --- board/src/board.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/board/src/board.rs b/board/src/board.rs index 308cbf7..02ab7af 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -107,25 +107,21 @@ impl Board { } /// A [`BitBoard`] representing the set of squares containing a piece. - #[inline] #[must_use] pub fn occupied_squares(&self) -> &BitBoard { self.pieces.all_pieces() } - #[inline] #[must_use] pub fn friendly_pieces(&self) -> &BitBoard { self.pieces.all_pieces_of_color(self.player_to_move) } - #[inline] #[must_use] pub fn opposing_pieces(&self) -> &BitBoard { self.pieces.all_pieces_of_color(self.player_to_move.other()) } - #[inline] #[must_use] pub fn all_pieces(&self) -> (&BitBoard, &BitBoard) { (self.friendly_pieces(), self.opposing_pieces()) @@ -133,7 +129,6 @@ impl Board { /// A [`BitBoard`] representing the set of squares containing a piece. This set is the inverse of /// `Board::occupied_squares`. - #[inline] #[must_use] pub fn empty_squares(&self) -> BitBoard { !self.occupied_squares() From 3785502ea096ede3f9c3f21d47410d176d28d29a Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 13 Jul 2024 11:56:31 -0700 Subject: [PATCH 232/423] [board] Declare Board::flags() pub(crate) instead of pub --- board/src/board.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/board/src/board.rs b/board/src/board.rs index 02ab7af..036d4ee 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -86,7 +86,7 @@ impl Board { } #[must_use] - pub(crate) fn flags(&self) -> &Flags { + pub fn flags(&self) -> &Flags { &self.flags } From c297e4cbfaa2e08c5f6c17326061e870f0efa76f Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 13 Jul 2024 11:57:57 -0700 Subject: [PATCH 233/423] [board] Replace implementation of Board::piece_on_square with a simpler one Now that board has a Mailbox, the implementation of this routine can be greatly simplified. Instead of needing to iterate through all BitBoards to find the occupied square, just consult the mailbox. --- board/src/board.rs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/board/src/board.rs b/board/src/board.rs index 036d4ee..b602237 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -135,17 +135,10 @@ impl Board { } #[must_use] - pub fn piece_on_square(&self, sq: Square) -> Option { - for color in Color::iter() { - for shape in Shape::iter() { - let piece = Piece::new(*color, *shape); - if self.pieces.bitboard_for_piece(&piece).is_set(sq) { - return Some(PlacedPiece::new(piece, sq)); - } - } - } - - None + pub fn piece_on_square(&self, square: Square) -> Option { + self.pieces + .get(square) + .map(|piece| PlacedPiece::new(piece, square)) } #[must_use] From d221de700d4aef599d38ed7633525da2c0686ad3 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 13 Jul 2024 12:00:02 -0700 Subject: [PATCH 234/423] [board] Copy edit and add new documentation to Board --- board/src/board.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/board/src/board.rs b/board/src/board.rs index b602237..8da5857 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -90,10 +90,12 @@ impl Board { &self.flags } - /// Returns `true` if the player has the right to castle on the given side of the board. + /// Returns `true` if the player has the right to castle on the given side + /// of the board. /// - /// The right to castle on a particular side of the board is retained as long as the player has - /// not moved their king, or the rook on that side of the board. + /// A player retains the right to castle on a particular side of the board + /// as long as they have not moved their king, or the rook on that side of + /// the board. #[must_use] pub fn player_has_right_to_castle(&self, color: Color, castle: Castle) -> bool { self.flags.player_has_right_to_castle(color, castle) @@ -106,7 +108,8 @@ impl Board { self.piece_on_square(square) } - /// A [`BitBoard`] representing the set of squares containing a piece. + /// A [`BitBoard`] representing the set of squares containing a piece. This + /// set is the inverse of [`Board::empty_squares`]. #[must_use] pub fn occupied_squares(&self) -> &BitBoard { self.pieces.all_pieces() @@ -127,8 +130,8 @@ impl Board { (self.friendly_pieces(), self.opposing_pieces()) } - /// A [`BitBoard`] representing the set of squares containing a piece. This set is the inverse of - /// `Board::occupied_squares`. + /// A [BitBoard] representing the set of squares containing a piece. This + /// set is the inverse of [`Board::occupied_squares`]. #[must_use] pub fn empty_squares(&self) -> BitBoard { !self.occupied_squares() From 7ec72035ae7cbdcc754d0a2e70e26f8b7d046ee3 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 13 Jul 2024 12:01:01 -0700 Subject: [PATCH 235/423] [board] Remove en passant test helper method --- board/src/board.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/board/src/board.rs b/board/src/board.rs index 8da5857..15ae1e1 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -193,13 +193,6 @@ impl Board { } } -#[cfg(test)] -impl Board { - pub(crate) fn test_set_en_passant(&mut self, en_passant: EnPassant) { - self.en_passant = Some(en_passant); - } -} - impl Default for Board { fn default() -> Self { Self { From 30188d478e6ee08cd2635bda61f88cec3f832051 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 13 Jul 2024 12:03:19 -0700 Subject: [PATCH 236/423] [board] Convert &BitBoard to BitBoard for several Board methods I don't think passing by reference buys much for BitBoard. So simplify the logic and borrowing semantics to make these easier to work with. --- board/src/board.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/board/src/board.rs b/board/src/board.rs index 15ae1e1..b1df95c 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -111,23 +111,26 @@ impl Board { /// A [`BitBoard`] representing the set of squares containing a piece. This /// set is the inverse of [`Board::empty_squares`]. #[must_use] - pub fn occupied_squares(&self) -> &BitBoard { + pub fn occupied_squares(&self) -> BitBoard { self.pieces.all_pieces() } #[must_use] - pub fn friendly_pieces(&self) -> &BitBoard { + pub fn friendly_pieces_bitboard(&self) -> BitBoard { self.pieces.all_pieces_of_color(self.player_to_move) } #[must_use] - pub fn opposing_pieces(&self) -> &BitBoard { + pub fn opposing_pieces_bitboard(&self) -> BitBoard { self.pieces.all_pieces_of_color(self.player_to_move.other()) } #[must_use] - pub fn all_pieces(&self) -> (&BitBoard, &BitBoard) { - (self.friendly_pieces(), self.opposing_pieces()) + pub fn all_pieces(&self) -> (BitBoard, BitBoard) { + ( + self.friendly_pieces_bitboard(), + self.opposing_pieces_bitboard(), + ) } /// A [BitBoard] representing the set of squares containing a piece. This @@ -166,8 +169,8 @@ impl Board { self.en_passant } - fn king_bitboard(&self, player: Color) -> &BitBoard { - self.pieces.bitboard_for_piece(&Piece::king(player)) + fn king_bitboard(&self, player: Color) -> BitBoard { + self.pieces.bitboard_for_piece(Piece::king(player)) } pub(crate) fn king_square(&self, player: Color) -> Square { @@ -183,13 +186,13 @@ impl Board { } #[must_use] - pub fn bitboard_for_color(&self, color: Color) -> &BitBoard { + pub fn bitboard_for_color(&self, color: Color) -> BitBoard { self.pieces.bitboard_for_color(color) } #[must_use] - pub fn bitboard_for_piece(&self, piece: Piece) -> &BitBoard { - self.pieces.bitboard_for_piece(&piece) + pub fn bitboard_for_piece(&self, piece: Piece) -> BitBoard { + self.pieces.bitboard_for_piece(piece) } } From 90657e3818875c980ef44812c344a4e104342c66 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 13 Jul 2024 12:08:20 -0700 Subject: [PATCH 237/423] [position] Rewrite sight methods in terms of Board and pass BitBoard arguments by value --- position/src/sight.rs | 132 +++++++++++++++++++----------------------- 1 file changed, 60 insertions(+), 72 deletions(-) diff --git a/position/src/sight.rs b/position/src/sight.rs index 3b9170d..072806e 100644 --- a/position/src/sight.rs +++ b/position/src/sight.rs @@ -4,8 +4,8 @@ //! that a piece can see. In other words, it's the set of squares attacked or //! controled by a piece. -use crate::position::piece_sets::PieceBitBoards; use chessfriend_bitboard::BitBoard; +use chessfriend_board::Board; use chessfriend_core::{Color, Direction, PlacedPiece, Shape, Square}; macro_rules! ray_in_direction { @@ -24,10 +24,10 @@ macro_rules! ray_in_direction { /// Compute sight of a white pawn. fn _white_pawn_sight( - pawn: &BitBoard, - occupancy: &BitBoard, - blockers: &BitBoard, - en_passant_square: &BitBoard, + pawn: BitBoard, + occupancy: BitBoard, + blockers: BitBoard, + en_passant_square: BitBoard, ) -> BitBoard { let possible_squares = !occupancy | blockers | en_passant_square; let pawn = pawn.shift_north_west_one() | pawn.shift_north_east_one(); @@ -35,21 +35,21 @@ fn _white_pawn_sight( } fn _black_pawn_sight( - pawn: &BitBoard, - occupancy: &BitBoard, - blockers: &BitBoard, - en_passant_square: &BitBoard, + pawn: BitBoard, + occupancy: BitBoard, + blockers: BitBoard, + en_passant_square: BitBoard, ) -> BitBoard { let possible_squares = !occupancy | blockers | en_passant_square; let pawn = pawn.shift_south_west_one() | pawn.shift_south_east_one(); pawn & possible_squares } -fn _knight_sight(knight_square: Square, blockers: &BitBoard) -> BitBoard { +fn _knight_sight(knight_square: Square, blockers: BitBoard) -> BitBoard { BitBoard::knight_moves(knight_square) & !blockers } -fn _bishop_sight(bishop_square: Square, occupancy: &BitBoard) -> BitBoard { +fn _bishop_sight(bishop_square: Square, occupancy: BitBoard) -> BitBoard { #[rustfmt::skip] let sight = ray_in_direction!(bishop_square, occupancy, NorthEast, occupied_squares_trailing) | ray_in_direction!(bishop_square, occupancy, NorthWest, occupied_squares_trailing) @@ -59,7 +59,7 @@ fn _bishop_sight(bishop_square: Square, occupancy: &BitBoard) -> BitBoard { sight } -fn _rook_sight(rook_square: Square, occupancy: &BitBoard) -> BitBoard { +fn _rook_sight(rook_square: Square, occupancy: BitBoard) -> BitBoard { #[rustfmt::skip] let sight = ray_in_direction!(rook_square, occupancy, North, occupied_squares_trailing) | ray_in_direction!(rook_square, occupancy, East, occupied_squares_trailing) @@ -69,7 +69,7 @@ fn _rook_sight(rook_square: Square, occupancy: &BitBoard) -> BitBoard { sight } -fn _queen_sight(queen_square: Square, occupancy: &BitBoard) -> BitBoard { +fn _queen_sight(queen_square: Square, occupancy: BitBoard) -> BitBoard { #[rustfmt::skip] let sight = ray_in_direction!(queen_square, occupancy, NorthWest, occupied_squares_trailing) | ray_in_direction!(queen_square, occupancy, North, occupied_squares_trailing) @@ -83,48 +83,38 @@ fn _queen_sight(queen_square: Square, occupancy: &BitBoard) -> BitBoard { sight } -fn _king_sight(king_square: Square, blockers: &BitBoard) -> BitBoard { +fn _king_sight(king_square: Square, blockers: BitBoard) -> BitBoard { BitBoard::king_moves(king_square) & !blockers } pub(crate) trait BishopSightExt { - fn bishop_sight(&self, occupancy: &BitBoard) -> BitBoard; + fn bishop_sight(&self, occupancy: BitBoard) -> BitBoard; } pub(crate) trait KingSightExt { - fn king_sight(&self, pieces: &PieceBitBoards) -> BitBoard; + fn king_sight(&self, board: &Board) -> BitBoard; } pub(crate) trait KnightSightExt { - fn knight_sight(&self, pieces: &PieceBitBoards) -> BitBoard; + fn knight_sight(&self, board: &Board) -> BitBoard; } pub(crate) trait PawnSightExt { - fn pawn_sight(&self, pieces: &PieceBitBoards, en_passant_square: Option) -> BitBoard; - - fn white_pawn_sight( - &self, - pieces: &PieceBitBoards, - en_passant_square: Option, - ) -> BitBoard; - - fn black_pawn_sight( - &self, - pieces: &PieceBitBoards, - en_passant_square: Option, - ) -> BitBoard; + fn pawn_sight(&self, board: &Board, en_passant_square: Option) -> BitBoard; + fn white_pawn_sight(&self, board: &Board, en_passant_square: Option) -> BitBoard; + fn black_pawn_sight(&self, board: &Board, en_passant_square: Option) -> BitBoard; } pub(crate) trait QueenSightExt { - fn queen_sight(&self, occupancy: &BitBoard) -> BitBoard; + fn queen_sight(&self, occupancy: BitBoard) -> BitBoard; } pub(crate) trait RookSightExt { - fn rook_sight(&self, occupancy: &BitBoard) -> BitBoard; + fn rook_sight(&self, occupancy: BitBoard) -> BitBoard; } pub(crate) trait SliderSightExt: BishopSightExt + QueenSightExt + RookSightExt {} pub(crate) trait SightExt { - fn sight(&self, pieces: &PieceBitBoards, en_passant_square: Option) -> BitBoard; + fn sight(&self, board: &Board, en_passant_square: Option) -> BitBoard; } pub(crate) trait SliderRayToSquareExt { @@ -132,84 +122,82 @@ pub(crate) trait SliderRayToSquareExt { } impl SightExt for PlacedPiece { - fn sight(&self, pieces: &PieceBitBoards, en_passant_square: Option) -> BitBoard { + fn sight(&self, board: &Board, en_passant_square: Option) -> BitBoard { match self.shape() { Shape::Pawn => match self.color() { - Color::White => self.white_pawn_sight(pieces, en_passant_square), - Color::Black => self.black_pawn_sight(pieces, en_passant_square), + Color::White => self.white_pawn_sight(board, en_passant_square), + Color::Black => self.black_pawn_sight(board, en_passant_square), }, - Shape::Knight => self.knight_sight(pieces), - Shape::Bishop => self.bishop_sight(pieces.all_pieces()), - Shape::Rook => self.rook_sight(pieces.all_pieces()), - Shape::Queen => self.queen_sight(pieces.all_pieces()), - Shape::King => self.king_sight(pieces), + Shape::Knight => self.knight_sight(board), + Shape::Bishop => self.bishop_sight(board.all_pieces_bitboard()), + Shape::Rook => self.rook_sight(board.all_pieces_bitboard()), + Shape::Queen => self.queen_sight(board.all_pieces_bitboard()), + Shape::King => self.king_sight(board), } } } impl KingSightExt for PlacedPiece { - fn king_sight(&self, pieces: &PieceBitBoards) -> BitBoard { - _king_sight(self.square(), pieces.all_pieces_of_color(self.color())) + fn king_sight(&self, board: &Board) -> BitBoard { + _king_sight( + self.square(), + board.all_pieces_of_color_bitboard(self.color()), + ) } } impl KnightSightExt for PlacedPiece { - fn knight_sight(&self, pieces: &PieceBitBoards) -> BitBoard { - _knight_sight(self.square(), pieces.all_pieces_of_color(self.color())) + fn knight_sight(&self, board: &Board) -> BitBoard { + _knight_sight( + self.square(), + board.all_pieces_of_color_bitboard(self.color()), + ) } } impl PawnSightExt for PlacedPiece { - fn pawn_sight(&self, pieces: &PieceBitBoards, en_passant_square: Option) -> BitBoard { + fn pawn_sight(&self, board: &Board, en_passant_square: Option) -> BitBoard { match self.color() { - Color::White => self.white_pawn_sight(pieces, en_passant_square), - Color::Black => self.black_pawn_sight(pieces, en_passant_square), + Color::White => self.white_pawn_sight(board, en_passant_square), + Color::Black => self.black_pawn_sight(board, en_passant_square), } } - fn white_pawn_sight( - &self, - pieces: &PieceBitBoards, - en_passant_square: Option, - ) -> BitBoard { + fn white_pawn_sight(&self, board: &Board, en_passant_square: Option) -> BitBoard { let opponent = self.color().other(); _white_pawn_sight( - &self.square().into(), - pieces.all_pieces(), - pieces.all_pieces_of_color(opponent), - &en_passant_square.into(), + self.square().into(), + board.all_pieces_bitboard(), + board.all_pieces_of_color_bitboard(opponent), + en_passant_square.into(), ) } - fn black_pawn_sight( - &self, - pieces: &PieceBitBoards, - en_passant_square: Option, - ) -> BitBoard { + fn black_pawn_sight(&self, board: &Board, en_passant_square: Option) -> BitBoard { let opponent = self.color().other(); _black_pawn_sight( - &self.square().into(), - pieces.all_pieces(), - pieces.all_pieces_of_color(opponent), - &en_passant_square.into(), + self.square().into(), + board.all_pieces_bitboard(), + board.all_pieces_of_color_bitboard(opponent), + en_passant_square.into(), ) } } impl BishopSightExt for PlacedPiece { - fn bishop_sight(&self, occupancy: &BitBoard) -> BitBoard { + fn bishop_sight(&self, occupancy: BitBoard) -> BitBoard { _bishop_sight(self.square(), occupancy) } } impl RookSightExt for PlacedPiece { - fn rook_sight(&self, occupancy: &BitBoard) -> BitBoard { + fn rook_sight(&self, occupancy: BitBoard) -> BitBoard { _rook_sight(self.square(), occupancy) } } impl QueenSightExt for PlacedPiece { - fn queen_sight(&self, occupancy: &BitBoard) -> BitBoard { + fn queen_sight(&self, occupancy: BitBoard) -> BitBoard { _queen_sight(self.square(), occupancy) } } @@ -271,19 +259,19 @@ impl SliderRayToSquareExt for Shape { } impl BishopSightExt for Square { - fn bishop_sight(&self, occupancy: &BitBoard) -> BitBoard { + fn bishop_sight(&self, occupancy: BitBoard) -> BitBoard { _bishop_sight(*self, occupancy) } } impl QueenSightExt for Square { - fn queen_sight(&self, occupancy: &BitBoard) -> BitBoard { + fn queen_sight(&self, occupancy: BitBoard) -> BitBoard { _queen_sight(*self, occupancy) } } impl RookSightExt for Square { - fn rook_sight(&self, occupancy: &BitBoard) -> BitBoard { + fn rook_sight(&self, occupancy: BitBoard) -> BitBoard { _rook_sight(*self, occupancy) } } From 53c637f424e4961483a028593228cb145d9a96bc Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 2 May 2025 14:23:29 -0700 Subject: [PATCH 238/423] [bitboard] Make BitBoard::EMPTY and BitBoard::FULL private; export BitBoard::full() --- bitboard/src/bitboard.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 3f18a78..d025038 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -39,14 +39,19 @@ macro_rules! moves_getter { } impl BitBoard { - pub const EMPTY: BitBoard = BitBoard(u64::MIN); - pub const FULL: BitBoard = BitBoard(u64::MAX); + const EMPTY: BitBoard = BitBoard(u64::MIN); + const FULL: BitBoard = BitBoard(u64::MAX); #[must_use] pub const fn empty() -> BitBoard { Self::EMPTY } + #[must_use] + pub const fn full() -> BitBoard { + Self::FULL + } + #[must_use] pub const fn new(bits: u64) -> BitBoard { BitBoard(bits) From 9f6299617572b044dc72bcb0b8a000962d89272e Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 2 May 2025 14:23:57 -0700 Subject: [PATCH 239/423] [bitboard] Return a copy of a BitBoard from BitBoard::ray() --- bitboard/src/bitboard.rs | 2 +- bitboard/src/library.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index d025038..7dcc164 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -70,7 +70,7 @@ impl BitBoard { } #[must_use] - pub fn ray(sq: Square, dir: Direction) -> &'static BitBoard { + pub fn ray(sq: Square, dir: Direction) -> BitBoard { library::library().ray(sq, dir) } diff --git a/bitboard/src/library.rs b/bitboard/src/library.rs index 419f6e2..177b6b7 100644 --- a/bitboard/src/library.rs +++ b/bitboard/src/library.rs @@ -237,8 +237,8 @@ impl MoveLibrary { ray } - pub(super) const fn ray(&self, sq: Square, dir: Direction) -> &BitBoard { - &self.rays[sq as usize][dir as usize] + pub(super) const fn ray(&self, sq: Square, dir: Direction) -> BitBoard { + self.rays[sq as usize][dir as usize] } pub(super) const fn pawn_pushes(&self, sq: Square, color: Color) -> BitBoard { From 7b0469d68998fb6ebf1ea012a8acaa4a86291f24 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 2 May 2025 14:26:09 -0700 Subject: [PATCH 240/423] [bitboard] Replace separate methods for leading and trailing iteration Add chessfriend_bitboard::IterationDirection Make BitBoard::occupied_squares() take an IterationDirection and return an iterator corresponding to the direction. Do the same for ::first_occupied_square(). --- bitboard/src/bitboard.rs | 53 ++++++++++++++++++++++++++------------- bitboard/src/direction.rs | 6 +++++ bitboard/src/lib.rs | 4 +-- 3 files changed, 44 insertions(+), 19 deletions(-) create mode 100644 bitboard/src/direction.rs diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 7dcc164..1ef26e9 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -1,7 +1,8 @@ // Eryn Wells +use crate::bit_scanner::{LeadingBitScanner, TrailingBitScanner}; +use crate::direction::IterationDirection; use crate::library; -use crate::{LeadingBitScanner, TrailingBitScanner}; use chessfriend_core::{Color, Direction, File, Rank, Square}; use forward_ref::{forward_ref_binop, forward_ref_op_assign, forward_ref_unop}; use std::fmt; @@ -223,27 +224,39 @@ impl BitBoard { self.0.is_power_of_two() } - /// Return an Iterator over the occupied squares. The Iterator yields - /// squares starting from the leading (most-significant bit) end of the - /// board. + /// Return an Iterator over the occupied squares. #[must_use] - pub fn occupied_squares(&self) -> impl Iterator { - LeadingBitScanner::new(self.0).map(|idx| unsafe { Square::from_index_unchecked(idx as u8) }) + pub fn occupied_squares( + &self, + direction: IterationDirection, + ) -> Box> { + fn index_to_square(index: usize) -> Square { + unsafe { Square::from_index_unchecked(index as u8) } + } + + match direction { + IterationDirection::Leading => { + Box::new(LeadingBitScanner::new(self.0).map(index_to_square)) + } + IterationDirection::Trailing => { + Box::new(TrailingBitScanner::new(self.0).map(index_to_square)) + } + } } - /// Return an Iterator over the occupied squares. The Iterator yields - /// squares starting from the trailing (least-significant bit) end of the - /// board. - pub fn occupied_squares_trailing(&self) -> impl Iterator { - TrailingBitScanner::new(self.0) - .map(|idx| unsafe { Square::from_index_unchecked(idx as u8) }) + #[must_use] + pub fn first_occupied_square(&self, direction: IterationDirection) -> Option { + match direction { + IterationDirection::Leading => self.first_occupied_square_leading(), + IterationDirection::Trailing => self.first_occupied_square_trailing(), + } } /// If the board is not empty, returns the first occupied square on the /// board, starting at the leading (most-significant) end of the board. If /// the board is empty, returns `None`. #[must_use] - pub fn first_occupied_square(&self) -> Option { + fn first_occupied_square_leading(&self) -> Option { let leading_zeros = self.0.leading_zeros() as u8; if leading_zeros < Square::NUM as u8 { unsafe { @@ -260,7 +273,7 @@ impl BitBoard { /// board, starting at the trailing (least-significant) end of the board. /// If the board is empty, returns `None`. #[must_use] - pub fn first_occupied_square_trailing(&self) -> Option { + fn first_occupied_square_trailing(&self) -> Option { let trailing_zeros = self.0.trailing_zeros() as u8; if trailing_zeros < Square::NUM as u8 { unsafe { Some(Square::from_index_unchecked(trailing_zeros)) } @@ -458,7 +471,10 @@ mod tests { fn single_rank_occupancy() { let bb = BitBoard(0b01010100); let expected_squares = [Square::G1, Square::E1, Square::C1]; - for (a, b) in bb.occupied_squares().zip(expected_squares.iter().cloned()) { + for (a, b) in bb + .occupied_squares(IterationDirection::Leading) + .zip(expected_squares.iter().cloned()) + { assert_eq!(a, b); } } @@ -470,7 +486,10 @@ mod tests { let expected_squares = [Square::H8, Square::F6, Square::C5, Square::E2, Square::D1]; - for (a, b) in bb.occupied_squares().zip(expected_squares.iter().cloned()) { + for (a, b) in bb + .occupied_squares(IterationDirection::Leading) + .zip(expected_squares.iter().cloned()) + { assert_eq!(a, b); } } @@ -514,7 +533,7 @@ mod tests { #[test] fn first_occupied_squares() { let bb = bitboard![A8 E1]; - assert_eq!(bb.first_occupied_square(), Some(Square::A8)); + assert_eq!(bb.first_occupied_square_leading(), Some(Square::A8)); assert_eq!(bb.first_occupied_square_trailing(), Some(Square::E1)); let bb = bitboard![D6 E7 F8]; diff --git a/bitboard/src/direction.rs b/bitboard/src/direction.rs new file mode 100644 index 0000000..b1298fc --- /dev/null +++ b/bitboard/src/direction.rs @@ -0,0 +1,6 @@ +#[derive(Default)] +pub enum IterationDirection { + #[default] + Leading, + Trailing, +} diff --git a/bitboard/src/lib.rs b/bitboard/src/lib.rs index a39d3c9..983f65e 100644 --- a/bitboard/src/lib.rs +++ b/bitboard/src/lib.rs @@ -2,12 +2,12 @@ mod bit_scanner; mod bitboard; +mod direction; mod library; mod shifts; pub use bitboard::BitBoard; - -pub(crate) use bit_scanner::{LeadingBitScanner, TrailingBitScanner}; +pub use direction::IterationDirection; #[macro_export] macro_rules! bitboard { From c733342fca69911853f7d4681d86fcacffd24cf0 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 2 May 2025 14:42:31 -0700 Subject: [PATCH 241/423] [board] Add MoveCounter struct to track current color, half move counter, and full move counter --- board/src/lib.rs | 2 ++ board/src/move_counter.rs | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 board/src/move_counter.rs diff --git a/board/src/lib.rs b/board/src/lib.rs index 0a5516a..013128c 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -6,6 +6,7 @@ pub mod en_passant; pub mod fen; pub mod flags; pub mod macros; +pub mod move_counter; mod board; mod builder; @@ -17,4 +18,5 @@ pub use builder::Builder; use castle::Castle; use en_passant::EnPassant; use flags::Flags; +use move_counter::MoveCounter; use piece_sets::PieceSet; diff --git a/board/src/move_counter.rs b/board/src/move_counter.rs new file mode 100644 index 0000000..cca9840 --- /dev/null +++ b/board/src/move_counter.rs @@ -0,0 +1,34 @@ +use chessfriend_core::Color; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct MoveCounter { + /// The player who's turn it is to move. + pub active_color: Color, + /// The number of completed turns. A turn finishes when every player has moved. + pub fullmove_number: u16, + /// The number of moves by all players since the last pawn advance or capture. + pub halfmove_number: u16, +} + +impl MoveCounter { + pub fn advance(&mut self, should_reset_halfmove_number: bool) { + self.active_color = self.active_color.next(); + + self.fullmove_number += 1; + self.halfmove_number = if should_reset_halfmove_number { + 0 + } else { + self.halfmove_number + 1 + }; + } +} + +impl Default for MoveCounter { + fn default() -> Self { + Self { + active_color: Color::default(), + fullmove_number: 0, + halfmove_number: 0, + } + } +} From 72fd9382384d596d979d04b66203d3b9485870f1 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 2 May 2025 14:48:01 -0700 Subject: [PATCH 242/423] [board] Implement Copy for Mailbox struct --- board/src/piece_sets/mailbox.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/board/src/piece_sets/mailbox.rs b/board/src/piece_sets/mailbox.rs index 05cdf9b..ce11cf0 100644 --- a/board/src/piece_sets/mailbox.rs +++ b/board/src/piece_sets/mailbox.rs @@ -3,7 +3,7 @@ use chessfriend_core::{Piece, PlacedPiece, Square}; use std::iter::FromIterator; -#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub(crate) struct Mailbox([Option; Square::NUM]); impl Mailbox { From 46b19ff616b392af9706d332ec9a80c74a5c3c8c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 2 May 2025 14:48:37 -0700 Subject: [PATCH 243/423] [board] Implement Mailbox::from_iter as a reduce (aka fold) over the incoming iterator --- board/src/piece_sets/mailbox.rs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/board/src/piece_sets/mailbox.rs b/board/src/piece_sets/mailbox.rs index ce11cf0..4fc9ec4 100644 --- a/board/src/piece_sets/mailbox.rs +++ b/board/src/piece_sets/mailbox.rs @@ -48,22 +48,20 @@ impl From<[Option; Square::NUM]> for Mailbox { impl FromIterator for Mailbox { fn from_iter>(iter: T) -> Self { - let mut mailbox = Self::new(); - for placed_piece in iter { - mailbox.set(placed_piece.piece(), placed_piece.square()); - } - - mailbox + iter.into_iter() + .fold(Self::new(), |mut mailbox, placed_piece| { + mailbox.set(placed_piece.piece(), placed_piece.square()); + mailbox + }) } } impl FromIterator<(Square, Piece)> for Mailbox { fn from_iter>(iter: T) -> Self { - let mut mailbox = Self::new(); - for (square, piece) in iter { - mailbox.set(piece, square); - } - - mailbox + iter.into_iter() + .fold(Self::new(), |mut mailbox, (square, piece)| { + mailbox.set(piece, square); + mailbox + }) } } From 9f2bfc045754ef62d48f018485a73baa66e5c15f Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 2 May 2025 14:49:16 -0700 Subject: [PATCH 244/423] [board] Replace the length of the Builder::kings array with Color::NUM instead of constant 2 --- board/src/builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/board/src/builder.rs b/board/src/builder.rs index e4f8ec0..f7f7cdf 100644 --- a/board/src/builder.rs +++ b/board/src/builder.rs @@ -8,7 +8,7 @@ pub struct Builder { player_to_move: Color, flags: Flags, pieces: Mailbox, - kings: [Option; 2], + kings: [Option; Color::NUM], en_passant: Option, ply_counter: u16, move_number: u16, From 0b100d5f147da130c0759c15db6722b9d91d91e5 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 2 May 2025 15:03:48 -0700 Subject: [PATCH 245/423] [board] Remove Flags struct, replace it with Castle and supporting structs Encapsulate castling rights within a small module. Castle provides the API to the rest of the board package. Rights encodes the castling rights for each player. Parameters defines castling parameters for each player. --- board/src/castle.rs | 119 +++------------------------------ board/src/castle/parameters.rs | 110 ++++++++++++++++++++++++++++++ board/src/castle/rights.rs | 92 +++++++++++++++++++++++++ board/src/flags.rs | 90 ------------------------- board/src/lib.rs | 2 - 5 files changed, 210 insertions(+), 203 deletions(-) create mode 100644 board/src/castle/parameters.rs create mode 100644 board/src/castle/rights.rs delete mode 100644 board/src/flags.rs diff --git a/board/src/castle.rs b/board/src/castle.rs index 3534d8d..7f477df 100644 --- a/board/src/castle.rs +++ b/board/src/castle.rs @@ -1,7 +1,12 @@ // Eryn Wells -use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Square}; +mod parameters; +mod rights; + +pub use rights::Rights; + +use chessfriend_core::Color; +use parameters::Parameters; #[repr(u8)] #[derive(Copy, Clone, Debug, Eq, PartialEq)] @@ -10,118 +15,10 @@ pub enum Castle { QueenSide = 1, } -pub struct Parameters { - /// Origin squares of the king and rook. - origin: Squares, - - /// Target or destination squares for the king and rook. - target: Squares, - - /// The set of squares that must be clear of any pieces in order to perform - /// this castle. - clear: BitBoard, - - /// The set of squares that must not be attacked (i.e. visible to opposing - /// pieces) in order to perform this castle. - check: BitBoard, -} - -impl Parameters { - pub fn king_origin_square(&self) -> Square { - self.origin.king - } - - pub fn rook_origin_square(&self) -> Square { - self.origin.rook - } - - pub fn king_target_square(&self) -> Square { - self.target.king - } - - pub fn rook_target_square(&self) -> Square { - self.target.rook - } - - /// A [`BitBoard`] of the squares that must be clear of any piece in order - /// to perform this castle move. - pub fn clear_squares(&self) -> &BitBoard { - &self.clear - } - - /// A [`BitBoard`] of the squares that must not be visible to opposing - /// pieces in order to perform this castle move. - pub fn check_squares(&self) -> &BitBoard { - &self.check - } -} - -#[derive(Debug)] -struct Squares { - king: Square, - rook: Square, -} - impl Castle { pub const ALL: [Castle; 2] = [Castle::KingSide, Castle::QueenSide]; - /// Parameters for each castling move, organized by color and board-side. - const PARAMETERS: [[Parameters; 2]; Color::NUM] = [ - [ - Parameters { - origin: Squares { - king: Square::E1, - rook: Square::H1, - }, - target: Squares { - king: Square::G1, - rook: Square::F1, - }, - clear: BitBoard::new(0b0110_0000), - check: BitBoard::new(0b0111_0000), - }, - Parameters { - origin: Squares { - king: Square::E1, - rook: Square::A1, - }, - target: Squares { - king: Square::C1, - rook: Square::D1, - }, - clear: BitBoard::new(0b0000_1110), - check: BitBoard::new(0b0001_1100), - }, - ], - [ - Parameters { - origin: Squares { - king: Square::E8, - rook: Square::H8, - }, - target: Squares { - king: Square::G8, - rook: Square::F8, - }, - clear: BitBoard::new(0b0110_0000 << (8 * 7)), - check: BitBoard::new(0b0111_0000 << (8 * 7)), - }, - Parameters { - origin: Squares { - king: Square::E8, - rook: Square::A8, - }, - target: Squares { - king: Square::C8, - rook: Square::D8, - }, - clear: BitBoard::new(0b0000_1110 << (8 * 7)), - check: BitBoard::new(0b0001_1100 << (8 * 7)), - }, - ], - ]; - pub fn parameters(self, color: Color) -> &'static Parameters { - &Castle::PARAMETERS[color as usize][self as usize] + &Parameters::BY_COLOR[color as usize][self as usize] } } diff --git a/board/src/castle/parameters.rs b/board/src/castle/parameters.rs new file mode 100644 index 0000000..cfe5901 --- /dev/null +++ b/board/src/castle/parameters.rs @@ -0,0 +1,110 @@ +use chessfriend_bitboard::BitBoard; +use chessfriend_core::{Color, Square}; + +pub struct Parameters { + /// Origin squares of the king and rook. + origin: Squares, + + /// Target or destination squares for the king and rook. + target: Squares, + + /// The set of squares that must be clear of any pieces in order to perform + /// this castle. + clear: BitBoard, + + /// The set of squares that must not be attacked (i.e. visible to opposing + /// pieces) in order to perform this castle. + check: BitBoard, +} + +#[derive(Debug)] +pub(super) struct Squares { + pub king: Square, + pub rook: Square, +} + +impl Parameters { + /// Parameters for each castling move, organized by color and board-side. + pub(super) const BY_COLOR: [[Self; 2]; Color::NUM] = [ + [ + Parameters { + origin: Squares { + king: Square::E1, + rook: Square::H1, + }, + target: Squares { + king: Square::G1, + rook: Square::F1, + }, + clear: BitBoard::new(0b0110_0000), + check: BitBoard::new(0b0111_0000), + }, + Parameters { + origin: Squares { + king: Square::E1, + rook: Square::A1, + }, + target: Squares { + king: Square::C1, + rook: Square::D1, + }, + clear: BitBoard::new(0b0000_1110), + check: BitBoard::new(0b0001_1100), + }, + ], + [ + Parameters { + origin: Squares { + king: Square::E8, + rook: Square::H8, + }, + target: Squares { + king: Square::G8, + rook: Square::F8, + }, + clear: BitBoard::new(0b0110_0000 << (8 * 7)), + check: BitBoard::new(0b0111_0000 << (8 * 7)), + }, + Parameters { + origin: Squares { + king: Square::E8, + rook: Square::A8, + }, + target: Squares { + king: Square::C8, + rook: Square::D8, + }, + clear: BitBoard::new(0b0000_1110 << (8 * 7)), + check: BitBoard::new(0b0001_1100 << (8 * 7)), + }, + ], + ]; + + pub fn king_origin_square(&self) -> Square { + self.origin.king + } + + pub fn rook_origin_square(&self) -> Square { + self.origin.rook + } + + pub fn king_target_square(&self) -> Square { + self.target.king + } + + pub fn rook_target_square(&self) -> Square { + self.target.rook + } + + /// A [`BitBoard`] of the squares that must be clear of any piece in order + /// to perform this castle move. + pub fn clear_squares(&self) -> &BitBoard { + &self.clear + } + + /// A [`BitBoard`] of the squares that must not be visible to opposing + /// pieces in order to perform this castle move. + pub fn check_squares(&self) -> &BitBoard { + &self.check + } +} diff --git a/board/src/castle/rights.rs b/board/src/castle/rights.rs new file mode 100644 index 0000000..f6016e8 --- /dev/null +++ b/board/src/castle/rights.rs @@ -0,0 +1,92 @@ +use super::Castle; +use chessfriend_core::Color; +use std::fmt; + +#[derive(Clone, Copy, Eq, Hash, PartialEq)] +pub struct Rights(u8); + +impl Rights { + /// Returns `true` if the player has the right to castle on the given side + /// of the board. + /// + /// A player retains the right to castle on a particular side of the board + /// as long as they have not moved their king, or the rook on that side of + /// the board. + pub fn player_has_right_to_castle(self, color: Color, castle: Castle) -> bool { + (self.0 & (1 << Self::_player_has_right_to_castle_flag_offset(color, castle))) != 0 + } + + pub fn set_player_has_right_to_castle_flag(&mut self, color: Color, castle: Castle) { + self.0 |= 1 << Self::_player_has_right_to_castle_flag_offset(color, castle); + } + + pub fn clear_player_has_right_to_castle_flag(&mut self, color: Color, castle: Castle) { + self.0 &= !(1 << Self::_player_has_right_to_castle_flag_offset(color, castle)); + } + + pub fn clear_all(&mut self) { + self.0 &= 0b1111_1100; + } + + fn _player_has_right_to_castle_flag_offset(color: Color, castle: Castle) -> usize { + ((color as usize) << 1) & castle as usize + } +} + +impl fmt::Debug for Rights { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Flags({:08b})", self.0) + } +} + +impl Default for Rights { + fn default() -> Self { + Self(0b0000_1111) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn castling_rights() { + assert_eq!( + Rights::_player_has_right_to_castle_flag_offset(Color::White, Castle::KingSide), + 0 + ); + assert_eq!( + Rights::_player_has_right_to_castle_flag_offset(Color::White, Castle::QueenSide), + 1 + ); + assert_eq!( + Rights::_player_has_right_to_castle_flag_offset(Color::Black, Castle::KingSide), + 2 + ); + assert_eq!( + Rights::_player_has_right_to_castle_flag_offset(Color::Black, Castle::QueenSide), + 3 + ); + } + + #[test] + fn default_rights() { + let mut rights = Rights::default(); + assert!(rights.player_has_right_to_castle(Color::White, Castle::KingSide)); + assert!(rights.player_has_right_to_castle(Color::White, Castle::QueenSide)); + assert!(rights.player_has_right_to_castle(Color::Black, Castle::KingSide)); + assert!(rights.player_has_right_to_castle(Color::Black, Castle::QueenSide)); + + rights.clear_player_has_right_to_castle_flag(Color::White, Castle::QueenSide); + assert!(rights.player_has_right_to_castle(Color::White, Castle::KingSide)); + assert!(!rights.player_has_right_to_castle(Color::White, Castle::QueenSide)); + assert!(rights.player_has_right_to_castle(Color::Black, Castle::KingSide)); + assert!(rights.player_has_right_to_castle(Color::Black, Castle::QueenSide)); + + rights.set_player_has_right_to_castle_flag(Color::White, Castle::QueenSide); + assert!(rights.player_has_right_to_castle(Color::White, Castle::KingSide)); + assert!(rights.player_has_right_to_castle(Color::White, Castle::QueenSide)); + assert!(rights.player_has_right_to_castle(Color::Black, Castle::KingSide)); + assert!(rights.player_has_right_to_castle(Color::Black, Castle::QueenSide)); + } +} diff --git a/board/src/flags.rs b/board/src/flags.rs deleted file mode 100644 index 548c00f..0000000 --- a/board/src/flags.rs +++ /dev/null @@ -1,90 +0,0 @@ -// Eryn Wells - -use crate::Castle; -use chessfriend_core::Color; -use std::fmt; - -#[derive(Clone, Copy, Eq, Hash, PartialEq)] -pub struct Flags(u8); - -impl Flags { - #[inline] - fn player_has_right_to_castle_flag_offset(color: Color, castle: Castle) -> usize { - ((color as usize) << 1) + castle as usize - } - - #[allow(dead_code)] - pub(super) fn player_has_right_to_castle(self, color: Color, castle: Castle) -> bool { - (self.0 & (1 << Self::player_has_right_to_castle_flag_offset(color, castle))) != 0 - } - - pub(super) fn set_player_has_right_to_castle_flag(&mut self, color: Color, castle: Castle) { - self.0 |= 1 << Self::player_has_right_to_castle_flag_offset(color, castle); - } - - pub(super) fn clear_player_has_right_to_castle_flag(&mut self, color: Color, castle: Castle) { - self.0 &= !(1 << Self::player_has_right_to_castle_flag_offset(color, castle)); - } - - pub(super) fn clear_all_castling_rights(&mut self) { - self.0 &= 0b1111_1100; - } -} - -impl fmt::Debug for Flags { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Flags({:08b})", self.0) - } -} - -impl Default for Flags { - fn default() -> Self { - Flags(0b0000_1111) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn castle_flags() { - assert_eq!( - Flags::player_has_right_to_castle_flag_offset(Color::White, Castle::KingSide), - 0 - ); - assert_eq!( - Flags::player_has_right_to_castle_flag_offset(Color::White, Castle::QueenSide), - 1 - ); - assert_eq!( - Flags::player_has_right_to_castle_flag_offset(Color::Black, Castle::KingSide), - 2 - ); - assert_eq!( - Flags::player_has_right_to_castle_flag_offset(Color::Black, Castle::QueenSide), - 3 - ); - } - - #[test] - fn defaults() { - let mut flags: Flags = Default::default(); - assert!(flags.player_has_right_to_castle(Color::White, Castle::KingSide)); - assert!(flags.player_has_right_to_castle(Color::White, Castle::QueenSide)); - assert!(flags.player_has_right_to_castle(Color::Black, Castle::KingSide)); - assert!(flags.player_has_right_to_castle(Color::Black, Castle::QueenSide)); - - flags.clear_player_has_right_to_castle_flag(Color::White, Castle::QueenSide); - assert!(flags.player_has_right_to_castle(Color::White, Castle::KingSide)); - assert!(!flags.player_has_right_to_castle(Color::White, Castle::QueenSide)); - assert!(flags.player_has_right_to_castle(Color::Black, Castle::KingSide)); - assert!(flags.player_has_right_to_castle(Color::Black, Castle::QueenSide)); - - flags.set_player_has_right_to_castle_flag(Color::White, Castle::QueenSide); - assert!(flags.player_has_right_to_castle(Color::White, Castle::KingSide)); - assert!(flags.player_has_right_to_castle(Color::White, Castle::QueenSide)); - assert!(flags.player_has_right_to_castle(Color::Black, Castle::KingSide)); - assert!(flags.player_has_right_to_castle(Color::Black, Castle::QueenSide)); - } -} diff --git a/board/src/lib.rs b/board/src/lib.rs index 013128c..35c7803 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -4,7 +4,6 @@ pub mod castle; pub mod display; pub mod en_passant; pub mod fen; -pub mod flags; pub mod macros; pub mod move_counter; @@ -17,6 +16,5 @@ pub use builder::Builder; use castle::Castle; use en_passant::EnPassant; -use flags::Flags; use move_counter::MoveCounter; use piece_sets::PieceSet; From bf535f876a24d0f0115b8633225805d7d8584787 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 2 May 2025 15:05:05 -0700 Subject: [PATCH 246/423] [board] Update call to BitBoard::occupied_squares to take an IterationDirection --- board/src/piece_sets.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/board/src/piece_sets.rs b/board/src/piece_sets.rs index 5f5b281..f8760bd 100644 --- a/board/src/piece_sets.rs +++ b/board/src/piece_sets.rs @@ -6,7 +6,7 @@ mod mailbox; pub(crate) use mailbox::Mailbox; use bitboards::{ByColor, ByColorAndShape}; -use chessfriend_bitboard::BitBoard; +use chessfriend_bitboard::{BitBoard, IterationDirection}; use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -52,7 +52,7 @@ impl PieceSet { for c in Color::into_iter() { for s in Shape::into_iter() { let bitboard = pieces[c as usize][s as usize]; - for square in bitboard.occupied_squares() { + for square in bitboard.occupied_squares(IterationDirection::default()) { mailbox.set(Piece::new(c, s), square); } } From 58cbe07136400d4a2516ec3f395c13097d430099 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 2 May 2025 15:05:39 -0700 Subject: [PATCH 247/423] [board] Implement PieceSet::mailbox() method to return a reference to its Mailbox --- board/src/piece_sets.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/board/src/piece_sets.rs b/board/src/piece_sets.rs index f8760bd..9ef1647 100644 --- a/board/src/piece_sets.rs +++ b/board/src/piece_sets.rs @@ -30,9 +30,9 @@ impl Default for PlacePieceStrategy { /// placement of pieces on the board. #[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] pub(crate) struct PieceSet { + mailbox: Mailbox, by_color: ByColor, by_color_and_shape: ByColorAndShape, - mailbox: Mailbox, } impl PieceSet { @@ -65,6 +65,10 @@ impl PieceSet { } } + pub(crate) fn mailbox(&self) -> &Mailbox { + &self.mailbox + } + /// A [`BitBoard`] representing all the pieces currently on the board. Other /// engines might refer to this concept as 'occupancy'. pub(crate) fn all_pieces(&self) -> BitBoard { From cd60a453aa63d9339b8c7ee679178e65864f5893 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 2 May 2025 15:18:37 -0700 Subject: [PATCH 248/423] [board] Replace active player and move properties on Board with MoveCounter instance --- board/src/board.rs | 31 ++++++------------------------- board/src/builder.rs | 32 ++++++++++++-------------------- board/src/fen.rs | 10 ++++++---- 3 files changed, 24 insertions(+), 49 deletions(-) diff --git a/board/src/board.rs b/board/src/board.rs index b1df95c..70589a1 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -1,7 +1,8 @@ // Eryn Wells use crate::{ - display::DiagramFormatter, piece_sets::PlacePieceError, Castle, EnPassant, Flags, PieceSet, + display::DiagramFormatter, piece_sets::PlacePieceError, Castle, EnPassant, Flags, MoveCounter, + PieceSet, }; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; @@ -9,12 +10,10 @@ use std::iter::Iterator; #[derive(Clone, Debug, Eq)] pub struct Board { - player_to_move: Color, flags: Flags, pieces: PieceSet, en_passant: Option, - half_move_counter: u16, - full_move_number: u16, + pub move_counter: MoveCounter, } impl Board { @@ -46,7 +45,6 @@ impl Board { ]; Self { - player_to_move: Color::White, pieces: PieceSet::new([WHITE_PIECES, BLACK_PIECES]), ..Default::default() } @@ -72,22 +70,7 @@ impl Board { #[must_use] pub fn player_to_move(&self) -> Color { - self.player_to_move - } - - #[must_use] - pub fn move_number(&self) -> u16 { - self.full_move_number - } - - #[must_use] - pub fn ply_counter(&self) -> u16 { - self.half_move_counter - } - - #[must_use] - pub fn flags(&self) -> &Flags { - &self.flags + self.move_counter.active_color } /// Returns `true` if the player has the right to castle on the given side @@ -199,12 +182,10 @@ impl Board { impl Default for Board { fn default() -> Self { Self { - player_to_move: Color::default(), flags: Flags::default(), pieces: PieceSet::default(), en_passant: None, - half_move_counter: 0, - full_move_number: 1, + move_counter: MoveCounter::default(), } } } @@ -212,9 +193,9 @@ impl Default for Board { impl PartialEq for Board { fn eq(&self, other: &Self) -> bool { self.pieces == other.pieces - && self.player_to_move == other.player_to_move && self.flags == other.flags && self.en_passant == other.en_passant + && self.move_counter == other.move_counter } } diff --git a/board/src/builder.rs b/board/src/builder.rs index f7f7cdf..6d89799 100644 --- a/board/src/builder.rs +++ b/board/src/builder.rs @@ -1,17 +1,15 @@ // Eryn Wells -use crate::{piece_sets::Mailbox, Board, Castle, EnPassant, Flags, PieceSet}; +use crate::{piece_sets::Mailbox, Board, Castle, EnPassant, Flags, MoveCounter, PieceSet}; use chessfriend_core::{piece, Color, PlacedPiece, Rank, Shape, Square}; #[derive(Clone)] pub struct Builder { - player_to_move: Color, flags: Flags, pieces: Mailbox, kings: [Option; Color::NUM], en_passant: Option, - ply_counter: u16, - move_number: u16, + move_counter: MoveCounter, } impl Builder { @@ -28,28 +26,26 @@ impl Builder { let black_king = board.king_square(Color::Black); Self { - player_to_move: board.player_to_move(), flags: *board.flags(), pieces, kings: [Some(white_king), Some(black_king)], en_passant: board.en_passant(), - ply_counter: board.ply_counter(), - move_number: board.move_number(), + move_counter: board.move_counter, } } pub fn to_move(&mut self, player: Color) -> &mut Self { - self.player_to_move = player; + self.move_counter.active_color = player; self } - pub fn ply_counter(&mut self, num: u16) -> &mut Self { - self.ply_counter = num; + pub fn fullmove_number(&mut self, num: u16) -> &mut Self { + self.move_counter.fullmove_number = num; self } - pub fn move_number(&mut self, num: u16) -> &mut Self { - self.move_number = num; + pub fn halfmove_number(&mut self, num: u16) -> &mut Self { + self.move_counter.halfmove_number = num; self } @@ -114,14 +110,12 @@ impl Builder { } } - Board::new( - self.player_to_move, + Board { flags, pieces, self.en_passant, - self.ply_counter, - self.move_number, - ) + move_counter: self.move_counter, + } } } @@ -149,13 +143,11 @@ impl Default for Builder { ]); Self { - player_to_move: Color::White, flags: Flags::default(), pieces, kings: [Some(white_king_square), Some(black_king_square)], en_passant: None, - ply_counter: 0, - move_number: 1, + move_counter: MoveCounter::default(), } } } diff --git a/board/src/fen.rs b/board/src/fen.rs index bd72ccd..55efdd2 100644 --- a/board/src/fen.rs +++ b/board/src/fen.rs @@ -132,8 +132,10 @@ impl ToFenStr for Board { ) .map_err(ToFenStrError::FmtError)?; - write!(fen_string, " {}", self.ply_counter()).map_err(ToFenStrError::FmtError)?; - write!(fen_string, " {}", self.move_number()).map_err(ToFenStrError::FmtError)?; + write!(fen_string, " {}", self.move_counter.halfmove_number) + .map_err(ToFenStrError::FmtError)?; + write!(fen_string, " {}", self.move_counter.fullmove_number) + .map_err(ToFenStrError::FmtError)?; Ok(fen_string) } @@ -246,7 +248,7 @@ impl FromFenStr for Board { let half_move_clock: u16 = half_move_clock .parse() .map_err(FromFenStrError::ParseIntError)?; - builder.ply_counter(half_move_clock); + builder.halfmove_number(half_move_clock); let full_move_counter = fields .next() @@ -254,7 +256,7 @@ impl FromFenStr for Board { let full_move_counter: u16 = full_move_counter .parse() .map_err(FromFenStrError::ParseIntError)?; - builder.move_number(full_move_counter); + builder.fullmove_number(full_move_counter); debug_assert_eq!(fields.next(), None); From bb8d5a6aa3c22d37a55f812807ec8107c9ea9c1c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 2 May 2025 15:41:45 -0700 Subject: [PATCH 249/423] [board] Replace Flags with castle::Rights --- board/src/board.rs | 57 +++++++++++++++++++++++++++++++++++++------- board/src/builder.rs | 18 +++++++------- board/src/fen.rs | 6 +++-- 3 files changed, 62 insertions(+), 19 deletions(-) diff --git a/board/src/board.rs b/board/src/board.rs index 70589a1..71ccd60 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -1,7 +1,7 @@ // Eryn Wells use crate::{ - display::DiagramFormatter, piece_sets::PlacePieceError, Castle, EnPassant, Flags, MoveCounter, + castle, display::DiagramFormatter, piece_sets::PlacePieceError, EnPassant, MoveCounter, PieceSet, }; use chessfriend_bitboard::BitBoard; @@ -10,10 +10,10 @@ use std::iter::Iterator; #[derive(Clone, Debug, Eq)] pub struct Board { - flags: Flags, pieces: PieceSet, en_passant: Option, pub move_counter: MoveCounter, + pub castling_rights: castle::Rights, } impl Board { @@ -91,6 +91,39 @@ impl Board { self.piece_on_square(square) } + /// Returns `true` if the player is able to castle on the given side of the board. + /// + /// The following requirements must be met: + /// + /// 1. The player must still have the right to castle on that side of the + /// board. The king and rook involved in the castle must not have moved. + /// 1. The spaces between the king and rook must be clear + /// 2. The king must not be in check + /// 3. In the course of castling on that side, the king must not pass + /// through a square that an enemy piece can see + pub fn player_can_castle(&self, player: Color, castle: Castle) -> bool { + if !self + .castling_rights + .player_has_right_to_castle(player, castle.into()) + { + return false; + } + + let castling_parameters = castle.parameters(player); + + let all_pieces = self.all_pieces_bitboard(); + if !(all_pieces & castling_parameters.clear_squares()).is_empty() { + return false; + } + + let danger_squares = self.king_danger(player); + if !(danger_squares & castling_parameters.check_squares()).is_empty() { + return false; + } + + true + } + /// A [`BitBoard`] representing the set of squares containing a piece. This /// set is the inverse of [`Board::empty_squares`]. #[must_use] @@ -182,7 +215,7 @@ impl Board { impl Default for Board { fn default() -> Self { Self { - flags: Flags::default(), + castling_rights: castle::Rights::default(), pieces: PieceSet::default(), en_passant: None, move_counter: MoveCounter::default(), @@ -193,7 +226,7 @@ impl Default for Board { impl PartialEq for Board { fn eq(&self, other: &Self) -> bool { self.pieces == other.pieces - && self.flags == other.flags + && self.castling_rights == other.castling_rights && self.en_passant == other.en_passant && self.move_counter == other.move_counter } @@ -232,8 +265,12 @@ mod tests { #[test] fn king_not_on_starting_square_cannot_castle() { let board = test_board!(White King on E4); - assert!(!board.player_has_right_to_castle(Color::White, Castle::KingSide)); - assert!(!board.player_has_right_to_castle(Color::White, Castle::QueenSide)); + assert!(!board + .castling_rights + .player_has_right_to_castle(Color::White, Castle::KingSide)); + assert!(!board + .castling_rights + .player_has_right_to_castle(Color::White, Castle::QueenSide)); } #[test] @@ -244,8 +281,12 @@ mod tests { White Rook on H1 ); - assert!(board.player_has_right_to_castle(Color::White, Castle::KingSide)); - assert!(board.player_has_right_to_castle(Color::White, Castle::QueenSide)); + assert!(board + .castling_rights + .player_has_right_to_castle(Color::White, Castle::KingSide)); + assert!(board + .castling_rights + .player_has_right_to_castle(Color::White, Castle::QueenSide)); } #[test] diff --git a/board/src/builder.rs b/board/src/builder.rs index 6d89799..434e517 100644 --- a/board/src/builder.rs +++ b/board/src/builder.rs @@ -1,11 +1,11 @@ // Eryn Wells -use crate::{piece_sets::Mailbox, Board, Castle, EnPassant, Flags, MoveCounter, PieceSet}; +use crate::{castle, piece_sets::Mailbox, Board, Castle, EnPassant, MoveCounter, PieceSet}; use chessfriend_core::{piece, Color, PlacedPiece, Rank, Shape, Square}; #[derive(Clone)] pub struct Builder { - flags: Flags, + castling_rights: castle::Rights, pieces: Mailbox, kings: [Option; Color::NUM], en_passant: Option, @@ -26,8 +26,8 @@ impl Builder { let black_king = board.king_square(Color::Black); Self { - flags: *board.flags(), pieces, + castling_rights: board.castling_rights, kings: [Some(white_king), Some(black_king)], en_passant: board.en_passant(), move_counter: board.move_counter, @@ -74,13 +74,13 @@ impl Builder { } pub fn player_can_castle(&mut self, color: Color, castle: Castle) -> &mut Self { - self.flags + self.castling_rights .set_player_has_right_to_castle_flag(color, castle); self } pub fn no_castling_rights(&mut self) -> &mut Self { - self.flags.clear_all_castling_rights(); + self.castling_rights.clear_all(); self } @@ -91,7 +91,7 @@ impl Builder { .filter(Self::is_piece_placement_valid) .collect(); - let mut flags = self.flags; + let mut castling_rights = self.castling_rights; for color in Color::ALL { for castle in Castle::ALL { @@ -105,16 +105,16 @@ impl Builder { self.kings[color as usize] == Some(parameters.king_origin_square()); if !king_is_on_starting_square || !has_rook_on_starting_square { - flags.clear_player_has_right_to_castle_flag(color, castle); + castling_rights.clear_player_has_right_to_castle_flag(color, castle); } } } Board { - flags, pieces, self.en_passant, move_counter: self.move_counter, + castling_rights, } } } @@ -143,7 +143,7 @@ impl Default for Builder { ]); Self { - flags: Flags::default(), + castling_rights: castle::Rights::default(), pieces, kings: [Some(white_king_square), Some(black_king_square)], en_passant: None, diff --git a/board/src/fen.rs b/board/src/fen.rs index 55efdd2..3ee39d6 100644 --- a/board/src/fen.rs +++ b/board/src/fen.rs @@ -103,8 +103,10 @@ impl ToFenStr for Board { (Color::Black, Castle::QueenSide), ] .map(|(color, castle)| { - let can_castle = self.player_has_right_to_castle(color, castle); - if !can_castle { + if !self + .castling_rights + .player_has_right_to_castle(color, castle) + { return ""; } From b0c403992050cb04cd52565a7c57f3659e28e23e Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 2 May 2025 15:42:06 -0700 Subject: [PATCH 250/423] [board] Remove Board::new() --- board/src/board.rs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/board/src/board.rs b/board/src/board.rs index 71ccd60..e091b52 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -50,24 +50,6 @@ impl Board { } } - pub(crate) fn new( - player_to_move: Color, - flags: Flags, - pieces: PieceSet, - en_passant: Option, - half_move_counter: u16, - full_move_number: u16, - ) -> Self { - Self { - player_to_move, - flags, - pieces, - en_passant, - half_move_counter, - full_move_number, - } - } - #[must_use] pub fn player_to_move(&self) -> Color { self.move_counter.active_color From 99dd2d1be26ce02ac43e11a9e60f0036f118becd Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 3 May 2025 15:47:56 -0700 Subject: [PATCH 251/423] [board] Remove the Builder --- board/src/builder.rs | 166 ------------------------------------------- board/src/lib.rs | 2 - 2 files changed, 168 deletions(-) delete mode 100644 board/src/builder.rs diff --git a/board/src/builder.rs b/board/src/builder.rs deleted file mode 100644 index 434e517..0000000 --- a/board/src/builder.rs +++ /dev/null @@ -1,166 +0,0 @@ -// Eryn Wells - -use crate::{castle, piece_sets::Mailbox, Board, Castle, EnPassant, MoveCounter, PieceSet}; -use chessfriend_core::{piece, Color, PlacedPiece, Rank, Shape, Square}; - -#[derive(Clone)] -pub struct Builder { - castling_rights: castle::Rights, - pieces: Mailbox, - kings: [Option; Color::NUM], - en_passant: Option, - move_counter: MoveCounter, -} - -impl Builder { - #[must_use] - pub fn new() -> Self { - Self::default() - } - - #[must_use] - pub fn from_board(board: &Board) -> Self { - let pieces = board.iter_all_pieces().collect::(); - - let white_king = board.king_square(Color::White); - let black_king = board.king_square(Color::Black); - - Self { - pieces, - castling_rights: board.castling_rights, - kings: [Some(white_king), Some(black_king)], - en_passant: board.en_passant(), - move_counter: board.move_counter, - } - } - - pub fn to_move(&mut self, player: Color) -> &mut Self { - self.move_counter.active_color = player; - self - } - - pub fn fullmove_number(&mut self, num: u16) -> &mut Self { - self.move_counter.fullmove_number = num; - self - } - - pub fn halfmove_number(&mut self, num: u16) -> &mut Self { - self.move_counter.halfmove_number = num; - self - } - - pub fn en_passant(&mut self, en_passant: Option) -> &mut Self { - self.en_passant = en_passant; - self - } - - pub fn place_piece(&mut self, piece: PlacedPiece) -> &mut Self { - let square = piece.square(); - let shape = piece.shape(); - - if shape == Shape::King { - let color = piece.color(); - let color_index: usize = color as usize; - - if let Some(king_square) = self.kings[color_index] { - self.pieces.remove(king_square); - } - self.kings[color_index] = Some(square); - } - - self.pieces.set(piece.piece(), square); - - self - } - - pub fn player_can_castle(&mut self, color: Color, castle: Castle) -> &mut Self { - self.castling_rights - .set_player_has_right_to_castle_flag(color, castle); - self - } - - pub fn no_castling_rights(&mut self) -> &mut Self { - self.castling_rights.clear_all(); - self - } - - pub fn build(&self) -> Board { - let pieces: PieceSet = self - .pieces - .iter() - .filter(Self::is_piece_placement_valid) - .collect(); - - let mut castling_rights = self.castling_rights; - - for color in Color::ALL { - for castle in Castle::ALL { - let parameters = castle.parameters(color); - - let has_rook_on_starting_square = self - .pieces - .get(parameters.rook_origin_square()) - .is_some_and(|piece| piece.shape() == Shape::Rook); - let king_is_on_starting_square = - self.kings[color as usize] == Some(parameters.king_origin_square()); - - if !king_is_on_starting_square || !has_rook_on_starting_square { - castling_rights.clear_player_has_right_to_castle_flag(color, castle); - } - } - } - - Board { - pieces, - self.en_passant, - move_counter: self.move_counter, - castling_rights, - } - } -} - -impl Builder { - fn is_piece_placement_valid(piece: &PlacedPiece) -> bool { - if piece.shape() == Shape::Pawn { - // Pawns cannot be placed on the first (back) rank of their side, - // and cannot be placed on the final rank without a promotion. - let rank = piece.square().rank(); - return rank != Rank::ONE && rank != Rank::EIGHT; - } - - true - } -} - -impl Default for Builder { - fn default() -> Self { - let white_king_square = Square::E1; - let black_king_square = Square::E8; - - let pieces = Mailbox::from_iter([ - (white_king_square, piece!(White King)), - (black_king_square, piece!(Black King)), - ]); - - Self { - castling_rights: castle::Rights::default(), - pieces, - kings: [Some(white_king_square), Some(black_king_square)], - en_passant: None, - move_counter: MoveCounter::default(), - } - } -} - -#[cfg(test)] -mod tests { - use crate::Builder; - use chessfriend_core::piece; - - #[test] - fn place_piece() { - let piece = piece!(White Queen on E4); - let builder = Builder::new().place_piece(piece).build(); - assert_eq!(builder.piece_on_square(piece.square()), Some(piece)); - } -} diff --git a/board/src/lib.rs b/board/src/lib.rs index 35c7803..10916c6 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -8,11 +8,9 @@ pub mod macros; pub mod move_counter; mod board; -mod builder; mod piece_sets; pub use board::Board; -pub use builder::Builder; use castle::Castle; use en_passant::EnPassant; From 867deafd135f3e20d0633706a708c0969cab062e Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 3 May 2025 16:02:56 -0700 Subject: [PATCH 252/423] [board] A ton of API refinements --- board/src/board.rs | 133 +++++++++++-------------------------- board/src/castle/rights.rs | 69 ++++++++----------- board/src/fen.rs | 52 +++++++-------- board/src/lib.rs | 6 +- board/src/macros.rs | 68 +++++++++---------- board/src/move_counter.rs | 102 ++++++++++++++++++++++------ board/src/piece_sets.rs | 22 ++---- 7 files changed, 208 insertions(+), 244 deletions(-) diff --git a/board/src/board.rs b/board/src/board.rs index e091b52..334885e 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -1,19 +1,16 @@ // Eryn Wells -use crate::{ - castle, display::DiagramFormatter, piece_sets::PlacePieceError, EnPassant, MoveCounter, - PieceSet, -}; +use crate::{castle, display::DiagramFormatter, Castle, Clock, PieceSet}; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; use std::iter::Iterator; -#[derive(Clone, Debug, Eq)] +#[derive(Clone, Debug, Default, Eq)] pub struct Board { - pieces: PieceSet, - en_passant: Option, - pub move_counter: MoveCounter, + pub clock: Clock, + pub pieces: PieceSet, pub castling_rights: castle::Rights, + pub en_passant_target: Option, } impl Board { @@ -52,18 +49,7 @@ impl Board { #[must_use] pub fn player_to_move(&self) -> Color { - self.move_counter.active_color - } - - /// Returns `true` if the player has the right to castle on the given side - /// of the board. - /// - /// A player retains the right to castle on a particular side of the board - /// as long as they have not moved their king, or the rook on that side of - /// the board. - #[must_use] - pub fn player_has_right_to_castle(&self, color: Color, castle: Castle) -> bool { - self.flags.player_has_right_to_castle(color, castle) + self.clock.active_color() } /// The rook to use for a castling move. @@ -83,11 +69,9 @@ impl Board { /// 2. The king must not be in check /// 3. In the course of castling on that side, the king must not pass /// through a square that an enemy piece can see + #[must_use] pub fn player_can_castle(&self, player: Color, castle: Castle) -> bool { - if !self - .castling_rights - .player_has_right_to_castle(player, castle.into()) - { + if !self.castling_rights.is_set(player, castle.into()) { return false; } @@ -115,12 +99,13 @@ impl Board { #[must_use] pub fn friendly_pieces_bitboard(&self) -> BitBoard { - self.pieces.all_pieces_of_color(self.player_to_move) + self.pieces.all_pieces_of_color(self.clock.active_color()) } #[must_use] pub fn opposing_pieces_bitboard(&self) -> BitBoard { - self.pieces.all_pieces_of_color(self.player_to_move.other()) + self.pieces + .all_pieces_of_color(self.clock.active_color().other()) } #[must_use] @@ -131,7 +116,15 @@ impl Board { ) } - /// A [BitBoard] representing the set of squares containing a piece. This + pub fn all_pieces_bitboard(&self) -> BitBoard { + self.pieces.all_pieces() + } + + pub fn all_pieces_of_color_bitboard(&self, color: Color) -> BitBoard { + self.pieces.all_pieces_of_color(color) + } + + /// A [`BitBoard`] representing the set of squares containing a piece. This /// set is the inverse of [`Board::occupied_squares`]. #[must_use] pub fn empty_squares(&self) -> BitBoard { @@ -145,12 +138,10 @@ impl Board { .map(|piece| PlacedPiece::new(piece, square)) } - #[must_use] pub fn iter_all_pieces(&self) -> impl Iterator + '_ { self.pieces.iter() } - #[must_use] pub fn iter_pieces_of_color(&self, color: Color) -> impl Iterator + '_ { self.pieces .iter() @@ -158,13 +149,8 @@ impl Board { } #[must_use] - pub fn has_en_passant_square(&self) -> bool { - self.en_passant.is_some() - } - - #[must_use] - pub fn en_passant(&self) -> Option { - self.en_passant + pub fn en_passant_target(&self) -> Option { + self.en_passant_target } fn king_bitboard(&self, player: Color) -> BitBoard { @@ -172,10 +158,7 @@ impl Board { } pub(crate) fn king_square(&self, player: Color) -> Square { - self.king_bitboard(player) - .occupied_squares() - .next() - .unwrap() + self.king_bitboard(player).try_into().unwrap() } #[must_use] @@ -192,16 +175,19 @@ impl Board { pub fn bitboard_for_piece(&self, piece: Piece) -> BitBoard { self.pieces.bitboard_for_piece(piece) } -} -impl Default for Board { - fn default() -> Self { - Self { - castling_rights: castle::Rights::default(), - pieces: PieceSet::default(), - en_passant: None, - move_counter: MoveCounter::default(), - } + /// A [`BitBoard`] representing the squares where a king of the given color will + /// be in danger of being captured by the opposing player. If the king is on + /// one of these squares, it is in check. The king cannot move to these + /// squares. + pub(crate) fn king_danger(&self, color: Color) -> BitBoard { + let board_without_king = { + let mut cloned_board = self.clone(); + cloned_board.pieces.remove(self.king_square(color)); + cloned_board + }; + + BitBoard::full() } } @@ -209,8 +195,8 @@ impl PartialEq for Board { fn eq(&self, other: &Self) -> bool { self.pieces == other.pieces && self.castling_rights == other.castling_rights - && self.en_passant == other.en_passant - && self.move_counter == other.move_counter + && self.en_passant_target == other.en_passant_target + && self.clock == other.clock } } @@ -243,49 +229,4 @@ mod tests { Some(piece!(Black Rook on A8)) ); } - - #[test] - fn king_not_on_starting_square_cannot_castle() { - let board = test_board!(White King on E4); - assert!(!board - .castling_rights - .player_has_right_to_castle(Color::White, Castle::KingSide)); - assert!(!board - .castling_rights - .player_has_right_to_castle(Color::White, Castle::QueenSide)); - } - - #[test] - fn king_on_starting_square_can_castle() { - let board = test_board!( - White King on E1, - White Rook on A1, - White Rook on H1 - ); - - assert!(board - .castling_rights - .player_has_right_to_castle(Color::White, Castle::KingSide)); - assert!(board - .castling_rights - .player_has_right_to_castle(Color::White, Castle::QueenSide)); - } - - #[test] - fn rook_for_castle() { - let board = test_board![ - White King on E1, - White Rook on H1, - White Rook on A1, - ]; - - assert_eq!( - board.rook_for_castle(Color::White, Castle::KingSide), - Some(piece!(White Rook on H1)) - ); - assert_eq!( - board.rook_for_castle(Color::White, Castle::QueenSide), - Some(piece!(White Rook on A1)) - ); - } } diff --git a/board/src/castle/rights.rs b/board/src/castle/rights.rs index f6016e8..c32a46e 100644 --- a/board/src/castle/rights.rs +++ b/board/src/castle/rights.rs @@ -12,24 +12,25 @@ impl Rights { /// A player retains the right to castle on a particular side of the board /// as long as they have not moved their king, or the rook on that side of /// the board. - pub fn player_has_right_to_castle(self, color: Color, castle: Castle) -> bool { - (self.0 & (1 << Self::_player_has_right_to_castle_flag_offset(color, castle))) != 0 + #[must_use] + pub fn is_set(self, color: Color, castle: Castle) -> bool { + (self.0 & (1 << Self::flag_offset(color, castle))) != 0 } - pub fn set_player_has_right_to_castle_flag(&mut self, color: Color, castle: Castle) { - self.0 |= 1 << Self::_player_has_right_to_castle_flag_offset(color, castle); + pub fn set(&mut self, color: Color, castle: Castle) { + self.0 |= 1 << Self::flag_offset(color, castle); } - pub fn clear_player_has_right_to_castle_flag(&mut self, color: Color, castle: Castle) { - self.0 &= !(1 << Self::_player_has_right_to_castle_flag_offset(color, castle)); + pub fn clear(&mut self, color: Color, castle: Castle) { + self.0 &= !(1 << Self::flag_offset(color, castle)); } pub fn clear_all(&mut self) { - self.0 &= 0b1111_1100; + self.0 = 0; } - fn _player_has_right_to_castle_flag_offset(color: Color, castle: Castle) -> usize { - ((color as usize) << 1) & castle as usize + fn flag_offset(color: Color, castle: Castle) -> usize { + ((color as usize) << 1) + castle as usize } } @@ -50,43 +51,31 @@ mod tests { use super::*; #[test] - fn castling_rights() { - assert_eq!( - Rights::_player_has_right_to_castle_flag_offset(Color::White, Castle::KingSide), - 0 - ); - assert_eq!( - Rights::_player_has_right_to_castle_flag_offset(Color::White, Castle::QueenSide), - 1 - ); - assert_eq!( - Rights::_player_has_right_to_castle_flag_offset(Color::Black, Castle::KingSide), - 2 - ); - assert_eq!( - Rights::_player_has_right_to_castle_flag_offset(Color::Black, Castle::QueenSide), - 3 - ); + fn bitfield_offsets() { + assert_eq!(Rights::flag_offset(Color::White, Castle::KingSide), 0); + assert_eq!(Rights::flag_offset(Color::White, Castle::QueenSide), 1); + assert_eq!(Rights::flag_offset(Color::Black, Castle::KingSide), 2); + assert_eq!(Rights::flag_offset(Color::Black, Castle::QueenSide), 3); } #[test] fn default_rights() { let mut rights = Rights::default(); - assert!(rights.player_has_right_to_castle(Color::White, Castle::KingSide)); - assert!(rights.player_has_right_to_castle(Color::White, Castle::QueenSide)); - assert!(rights.player_has_right_to_castle(Color::Black, Castle::KingSide)); - assert!(rights.player_has_right_to_castle(Color::Black, Castle::QueenSide)); + assert!(rights.is_set(Color::White, Castle::KingSide)); + assert!(rights.is_set(Color::White, Castle::QueenSide)); + assert!(rights.is_set(Color::Black, Castle::KingSide)); + assert!(rights.is_set(Color::Black, Castle::QueenSide)); - rights.clear_player_has_right_to_castle_flag(Color::White, Castle::QueenSide); - assert!(rights.player_has_right_to_castle(Color::White, Castle::KingSide)); - assert!(!rights.player_has_right_to_castle(Color::White, Castle::QueenSide)); - assert!(rights.player_has_right_to_castle(Color::Black, Castle::KingSide)); - assert!(rights.player_has_right_to_castle(Color::Black, Castle::QueenSide)); + rights.clear(Color::White, Castle::QueenSide); + assert!(rights.is_set(Color::White, Castle::KingSide)); + assert!(!rights.is_set(Color::White, Castle::QueenSide)); + assert!(rights.is_set(Color::Black, Castle::KingSide)); + assert!(rights.is_set(Color::Black, Castle::QueenSide)); - rights.set_player_has_right_to_castle_flag(Color::White, Castle::QueenSide); - assert!(rights.player_has_right_to_castle(Color::White, Castle::KingSide)); - assert!(rights.player_has_right_to_castle(Color::White, Castle::QueenSide)); - assert!(rights.player_has_right_to_castle(Color::Black, Castle::KingSide)); - assert!(rights.player_has_right_to_castle(Color::Black, Castle::QueenSide)); + rights.set(Color::White, Castle::QueenSide); + assert!(rights.is_set(Color::White, Castle::KingSide)); + assert!(rights.is_set(Color::White, Castle::QueenSide)); + assert!(rights.is_set(Color::Black, Castle::KingSide)); + assert!(rights.is_set(Color::Black, Castle::QueenSide)); } } diff --git a/board/src/fen.rs b/board/src/fen.rs index 3ee39d6..a370702 100644 --- a/board/src/fen.rs +++ b/board/src/fen.rs @@ -1,6 +1,6 @@ // Eryn Wells -use crate::{Board, Builder, Castle, EnPassant}; +use crate::{piece_sets::PlacePieceStrategy, Board, Castle, EnPassant}; use chessfriend_core::{ coordinates::ParseSquareError, piece, Color, File, Piece, PlacedPiece, Rank, Square, }; @@ -103,10 +103,7 @@ impl ToFenStr for Board { (Color::Black, Castle::QueenSide), ] .map(|(color, castle)| { - if !self - .castling_rights - .player_has_right_to_castle(color, castle) - { + if !self.castling_rights.is_set(color, castle) { return ""; } @@ -129,14 +126,14 @@ impl ToFenStr for Board { write!( fen_string, " {}", - self.en_passant() - .map_or("-".to_string(), |ep| ep.target_square().to_string()) + self.en_passant_target + .map_or("-".to_string(), |square| square.to_string()) ) .map_err(ToFenStrError::FmtError)?; - write!(fen_string, " {}", self.move_counter.halfmove_number) + write!(fen_string, " {}", self.clock.half_move_number()) .map_err(ToFenStrError::FmtError)?; - write!(fen_string, " {}", self.move_counter.fullmove_number) + write!(fen_string, " {}", self.clock.full_move_number()) .map_err(ToFenStrError::FmtError)?; Ok(fen_string) @@ -178,7 +175,7 @@ impl FromFenStr for Board { type Error = FromFenStrError; fn from_fen_str(string: &str) -> Result { - let mut builder = Builder::default(); + let mut board = Board::empty(); let mut fields = string.split(' '); @@ -202,34 +199,35 @@ impl FromFenStr for Board { let file = files.next().ok_or(FromFenStrError::MissingPlacement)?; let piece = Piece::from_fen_str(&ch.to_string())?; - builder.place_piece(PlacedPiece::new( + let _ = board.pieces.place( piece, Square::from_file_rank(*file, *rank), - )); + PlacePieceStrategy::default(), + ); } debug_assert_eq!(files.next(), None); } - let player_to_move = Color::from_fen_str( + let active_color = Color::from_fen_str( fields .next() .ok_or(FromFenStrError::MissingField(Field::PlayerToMove))?, )?; - builder.to_move(player_to_move); + board.clock.active_color = active_color; let castling_rights = fields .next() .ok_or(FromFenStrError::MissingField(Field::CastlingRights))?; if castling_rights == "-" { - builder.no_castling_rights(); + board.castling_rights.clear_all(); } else { for ch in castling_rights.chars() { match ch { - 'K' => builder.player_can_castle(Color::White, Castle::KingSide), - 'Q' => builder.player_can_castle(Color::White, Castle::QueenSide), - 'k' => builder.player_can_castle(Color::Black, Castle::KingSide), - 'q' => builder.player_can_castle(Color::Black, Castle::QueenSide), + 'K' => board.castling_rights.set(Color::White, Castle::KingSide), + 'Q' => board.castling_rights.set(Color::White, Castle::QueenSide), + 'k' => board.castling_rights.set(Color::Black, Castle::KingSide), + 'q' => board.castling_rights.set(Color::Black, Castle::QueenSide), _ => return Err(FromFenStrError::InvalidValue), }; } @@ -241,7 +239,7 @@ impl FromFenStr for Board { if en_passant_square != "-" { let square = Square::from_algebraic_str(en_passant_square) .map_err(FromFenStrError::ParseSquareError)?; - builder.en_passant(Some(EnPassant::from_target_square(square).unwrap())); + board.en_passant_target = Some(square); } let half_move_clock = fields @@ -250,7 +248,7 @@ impl FromFenStr for Board { let half_move_clock: u16 = half_move_clock .parse() .map_err(FromFenStrError::ParseIntError)?; - builder.halfmove_number(half_move_clock); + board.clock.half_move_number = half_move_clock; let full_move_counter = fields .next() @@ -258,11 +256,11 @@ impl FromFenStr for Board { let full_move_counter: u16 = full_move_counter .parse() .map_err(FromFenStrError::ParseIntError)?; - builder.fullmove_number(full_move_counter); + board.clock.full_move_number = full_move_counter; debug_assert_eq!(fields.next(), None); - Ok(builder.build()) + Ok(board) } } @@ -318,16 +316,14 @@ mod tests { let pos = test_board!(starting); assert_eq!( - pos.to_fen_str(), - Ok(String::from( - "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" - )) + pos.to_fen_str().unwrap(), + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 0" ); } #[test] fn from_starting_fen() { - let board = fen!("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1").unwrap(); + let board = fen!("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 0").unwrap(); let expected = Board::starting(); assert_eq!(board, expected, "{board:#?}\n{expected:#?}"); } diff --git a/board/src/lib.rs b/board/src/lib.rs index 10916c6..6a5bc2a 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -5,14 +5,14 @@ pub mod display; pub mod en_passant; pub mod fen; pub mod macros; -pub mod move_counter; mod board; +mod move_counter; mod piece_sets; pub use board::Board; +pub use move_counter::Clock; use castle::Castle; use en_passant::EnPassant; -use move_counter::MoveCounter; -use piece_sets::PieceSet; +use piece_sets::{PieceSet, PlacePieceError, PlacePieceStrategy}; diff --git a/board/src/macros.rs b/board/src/macros.rs index 547071b..f3ca5dc 100644 --- a/board/src/macros.rs +++ b/board/src/macros.rs @@ -16,24 +16,20 @@ macro_rules! board { }; } -#[cfg(test)] #[macro_export] macro_rules! test_board { ($to_move:ident, [ $($color:ident $shape:ident on $square:ident),* $(,)? ], $en_passant:ident) => { { - let board = $crate::Builder::new() - $(.place_piece( - chessfriend_core::PlacedPiece::new( - chessfriend_core::Piece::new( - chessfriend_core::Color::$color, - chessfriend_core::Shape::$shape - ), - chessfriend_core::Square::$square - )) - )* - .to_move(chessfriend_core::Color::$to_move) - .en_passant(Some(chessfriend_moves::EnPassant::from_target_square(chessfriend_core::Square::$en_passant)).unwrap()) - .build(); + let mut board = $crate::Board::empty(); + $(let _ = board.pieces.place( + chessfriend_core::Piece::new( + chessfriend_core::Color::$color, + chessfriend_core::Shape::$shape + ), + chessfriend_core::Square::$square); + )* + board.clock.active_color = chessfriend_core::Color::$to_move; + board.en_passant_target = Some(chessfriend_core::Square::$en_passant); println!("{}", board.display()); @@ -42,37 +38,33 @@ macro_rules! test_board { }; ($to_move:ident, [ $($color:ident $shape:ident on $square:ident),* $(,)? ]) => { { - let board = $crate::Builder::new() - $(.place_piece( - chessfriend_core::PlacedPiece::new( - chessfriend_core::Piece::new( - chessfriend_core::Color::$color, - chessfriend_core::Shape::$shape - ), - chessfriend_core::Square::$square - )) - )* - .to_move(chessfriend_core::Color::$to_move) - .build(); + let mut board = $crate::Board::empty(); + $(let _ = board.pieces.place( + chessfriend_core::Piece::new( + chessfriend_core::Color::$color, + chessfriend_core::Shape::$shape + ), + chessfriend_core::Square::$square, + $crate::PlacePieceStrategy::default()); + )* + board.clock.active_color = chessfriend_core::Color::$to_move; println!("{}", board.display()); - pos + board } }; ($($color:ident $shape:ident on $square:ident),* $(,)?) => { { - let board = $crate::Builder::new() - $(.place_piece( - chessfriend_core::PlacedPiece::new( - chessfriend_core::Piece::new( - chessfriend_core::Color::$color, - chessfriend_core::Shape::$shape - ), - chessfriend_core::Square::$square - )) - )* - .build(); + let mut board = $crate::Board::empty(); + $(let _ = board.pieces.place( + chessfriend_core::Piece::new( + chessfriend_core::Color::$color, + chessfriend_core::Shape::$shape + ), + chessfriend_core::Square::$square, + $crate::PlacePieceStrategy::default()); + )* println!("{}", board.display()); diff --git a/board/src/move_counter.rs b/board/src/move_counter.rs index cca9840..cd24bdf 100644 --- a/board/src/move_counter.rs +++ b/board/src/move_counter.rs @@ -1,34 +1,92 @@ use chessfriend_core::Color; -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct MoveCounter { +#[derive(Default)] +pub enum AdvanceHalfMove { + Reset, + #[default] + Advance, +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct Clock { /// The player who's turn it is to move. - pub active_color: Color, + pub(crate) active_color: Color, + /// The number of completed turns. A turn finishes when every player has moved. - pub fullmove_number: u16, + pub(crate) full_move_number: u16, + /// The number of moves by all players since the last pawn advance or capture. - pub halfmove_number: u16, + pub(crate) half_move_number: u16, } -impl MoveCounter { - pub fn advance(&mut self, should_reset_halfmove_number: bool) { - self.active_color = self.active_color.next(); - - self.fullmove_number += 1; - self.halfmove_number = if should_reset_halfmove_number { - 0 - } else { - self.halfmove_number + 1 - }; +impl Clock { + #[must_use] + pub fn active_color(&self) -> Color { + self.active_color } -} -impl Default for MoveCounter { - fn default() -> Self { - Self { - active_color: Color::default(), - fullmove_number: 0, - halfmove_number: 0, + #[must_use] + pub fn full_move_number(&self) -> u16 { + self.full_move_number + } + + #[must_use] + pub fn half_move_number(&self) -> u16 { + self.half_move_number + } + + pub fn advance(&mut self, advance_half_move: &AdvanceHalfMove) { + let next_color = self.active_color.next(); + + match self.active_color { + Color::Black => self.full_move_number += 1, + Color::White => {} } + + self.half_move_number = match advance_half_move { + AdvanceHalfMove::Reset => 0, + AdvanceHalfMove::Advance => self.half_move_number + 1, + }; + + self.active_color = next_color; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_state() { + let clock = Clock::default(); + assert_eq!(clock.active_color, Color::White); + assert_eq!(clock.half_move_number, 0); + assert_eq!(clock.full_move_number, 0); + } + + #[test] + fn advance() { + let mut clock = Clock::default(); + + clock.advance(&AdvanceHalfMove::default()); + assert_eq!(clock.active_color, Color::Black); + assert_eq!(clock.half_move_number, 1); + assert_eq!(clock.full_move_number, 0); + + clock.advance(&AdvanceHalfMove::default()); + assert_eq!(clock.active_color, Color::White); + assert_eq!(clock.half_move_number, 2); + assert_eq!(clock.full_move_number, 1); + + clock.advance(&AdvanceHalfMove::default()); + assert_eq!(clock.active_color, Color::Black); + assert_eq!(clock.half_move_number, 3); + assert_eq!(clock.full_move_number, 1); + + // The half move clock resets after a capture or pawn push. + clock.advance(&AdvanceHalfMove::Reset); + assert_eq!(clock.active_color, Color::White); + assert_eq!(clock.half_move_number, 0); + assert_eq!(clock.full_move_number, 2); } } diff --git a/board/src/piece_sets.rs b/board/src/piece_sets.rs index 9ef1647..2467b9d 100644 --- a/board/src/piece_sets.rs +++ b/board/src/piece_sets.rs @@ -29,7 +29,7 @@ impl Default for PlacePieceStrategy { /// The internal data structure of a [Board] that efficiently manages the /// placement of pieces on the board. #[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] -pub(crate) struct PieceSet { +pub struct PieceSet { mailbox: Mailbox, by_color: ByColor, by_color_and_shape: ByColorAndShape, @@ -52,7 +52,7 @@ impl PieceSet { for c in Color::into_iter() { for s in Shape::into_iter() { let bitboard = pieces[c as usize][s as usize]; - for square in bitboard.occupied_squares(IterationDirection::default()) { + for square in bitboard.occupied_squares(&IterationDirection::default()) { mailbox.set(Piece::new(c, s), square); } } @@ -65,10 +65,6 @@ impl PieceSet { } } - pub(crate) fn mailbox(&self) -> &Mailbox { - &self.mailbox - } - /// A [`BitBoard`] representing all the pieces currently on the board. Other /// engines might refer to this concept as 'occupancy'. pub(crate) fn all_pieces(&self) -> BitBoard { @@ -95,15 +91,7 @@ impl PieceSet { self.mailbox.get(square) } - pub(crate) fn place_piece_on_square( - &mut self, - piece: Piece, - square: Square, - ) -> Result { - self.place_piece_on_square_with_strategy(piece, square, PlacePieceStrategy::default()) - } - - pub(crate) fn place_piece_on_square_with_strategy( + pub(crate) fn place( &mut self, piece: Piece, square: Square, @@ -128,7 +116,7 @@ impl PieceSet { Ok(PlacedPiece::new(piece, square)) } - pub(crate) fn remove_piece_from_square(&mut self, square: Square) -> Option { + pub(crate) fn remove(&mut self, square: Square) -> Option { if let Some(piece) = self.mailbox.get(square) { self.by_color_and_shape.clear_square(square, piece.into()); self.by_color.clear_square(square, piece.color()); @@ -146,7 +134,7 @@ impl FromIterator for PieceSet { let mut pieces: Self = Self::default(); for piece in iter { - let _ = pieces.place_piece_on_square(piece.piece(), piece.square()); + let _ = pieces.place(piece.piece(), piece.square(), PlacePieceStrategy::default()); } pieces From d5cdf273c86abfcf27e49623cf702b79284416f8 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 3 May 2025 16:03:18 -0700 Subject: [PATCH 253/423] [bitboard] Fix some random clippy issues --- bitboard/src/bitboard.rs | 88 ++++++++++++++++++++++++++-------------- 1 file changed, 57 insertions(+), 31 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 1ef26e9..ce1852d 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -8,6 +8,9 @@ use forward_ref::{forward_ref_binop, forward_ref_op_assign, forward_ref_unop}; use std::fmt; use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not}; +#[allow(clippy::cast_possible_truncation)] +const SQUARES_NUM: u8 = Square::NUM as u8; + /// A bitfield representation of a chess board that uses the bits of a 64-bit /// unsigned integer to represent whether a square on the board is occupied. /// Squares are laid out as follows, starting at the bottom left, going row-wise, @@ -103,21 +106,21 @@ impl BitBoard { } impl BitBoard { - /// Converts this [BitBoard] to an unsigned 64-bit integer. + /// Converts this [`BitBoard`] to an unsigned 64-bit integer. #[must_use] pub const fn as_bits(&self) -> u64 { self.0 } - /// Returns `true` if this [BitBoard] has no bits set. This is the opposite + /// Returns `true` if this [`BitBoard`] has no bits set. This is the opposite /// of [`BitBoard::is_populated`]. /// /// ## Examples /// /// ``` /// use chessfriend_bitboard::BitBoard; - /// assert!(BitBoard::EMPTY.is_empty()); - /// assert!(!BitBoard::FULL.is_empty()); + /// assert!(BitBoard::empty().is_empty()); + /// assert!(!BitBoard::full().is_empty()); /// assert!(!BitBoard::new(0b1000).is_empty()); /// ``` #[must_use] @@ -125,17 +128,18 @@ impl BitBoard { self.0 == 0 } - /// Returns `true` if the [BitBoard] has at least one bit set. This is the + /// Returns `true` if the [`BitBoard`] has at least one bit set. This is the /// opposite of [`BitBoard::is_empty`]. /// /// ## Examples /// /// ``` /// use chessfriend_bitboard::BitBoard; - /// assert!(!BitBoard::EMPTY.is_populated()); - /// assert!(BitBoard::FULL.is_populated()); + /// assert!(!BitBoard::empty().is_populated()); + /// assert!(BitBoard::full().is_populated()); /// assert!(BitBoard::new(0b1).is_populated()); /// ``` + #[must_use] pub const fn is_populated(&self) -> bool { self.0 != 0 } @@ -154,21 +158,23 @@ impl BitBoard { /// assert!(bitboard.contains(Square::C1)); /// assert!(!bitboard.contains(Square::B1)); /// ``` + #[must_use] pub fn contains(self, square: Square) -> bool { let square_bitboard: BitBoard = square.into(); !(self & square_bitboard).is_empty() } - /// Counts the number of set squares (1 bits) in this [BitBoard]. + /// Counts the number of set squares (1 bits) in this [`BitBoard`]. /// /// ## Examples /// /// ``` /// use chessfriend_bitboard::BitBoard; - /// assert_eq!(BitBoard::EMPTY.population_count(), 0); + /// assert_eq!(BitBoard::empty().population_count(), 0); /// assert_eq!(BitBoard::new(0b01011110010).population_count(), 6); - /// assert_eq!(BitBoard::FULL.population_count(), 64); + /// assert_eq!(BitBoard::full().population_count(), 64); /// ``` + #[must_use] pub const fn population_count(&self) -> u32 { self.0.count_ones() } @@ -188,10 +194,10 @@ impl BitBoard { /// ``` pub fn set(&mut self, square: Square) { let square_bitboard: BitBoard = square.into(); - self.0 |= square_bitboard.0 + self.0 |= square_bitboard.0; } - /// Clear a square (set it to 0) in this [BitBoard]. This always succeeds + /// Clear a square (set it to 0) in this [`BitBoard`]. This always succeeds /// even if the bit is not set. /// /// ## Examples @@ -206,20 +212,21 @@ impl BitBoard { /// ``` pub fn clear(&mut self, square: Square) { let square_bitboard: BitBoard = square.into(); - self.0 &= !square_bitboard.0 + self.0 &= !square_bitboard.0; } - /// Returns `true` if this BitBoard represents a single square. + /// Returns `true` if this [`BitBoard`] represents a single square. /// /// ## Examples /// /// ``` /// use chessfriend_bitboard::BitBoard; - /// assert!(!BitBoard::EMPTY.is_single_square(), "Empty bitboards represent no squares"); - /// assert!(!BitBoard::FULL.is_single_square(), "Full bitboards represent all the squares"); + /// assert!(!BitBoard::empty().is_single_square(), "Empty bitboards represent no squares"); + /// assert!(!BitBoard::full().is_single_square(), "Full bitboards represent all the squares"); /// assert!(!BitBoard::new(0b010011110101101100).is_single_square(), "This bitboard represents a bunch of squares"); /// assert!(BitBoard::new(0b10000000000000).is_single_square()); /// ``` + #[must_use] pub fn is_single_square(&self) -> bool { self.0.is_power_of_two() } @@ -228,8 +235,9 @@ impl BitBoard { #[must_use] pub fn occupied_squares( &self, - direction: IterationDirection, + direction: &IterationDirection, ) -> Box> { + #[allow(clippy::cast_possible_truncation)] fn index_to_square(index: usize) -> Square { unsafe { Square::from_index_unchecked(index as u8) } } @@ -245,7 +253,7 @@ impl BitBoard { } #[must_use] - pub fn first_occupied_square(&self, direction: IterationDirection) -> Option { + pub fn first_occupied_square(&self, direction: &IterationDirection) -> Option { match direction { IterationDirection::Leading => self.first_occupied_square_leading(), IterationDirection::Trailing => self.first_occupied_square_trailing(), @@ -256,12 +264,12 @@ impl BitBoard { /// board, starting at the leading (most-significant) end of the board. If /// the board is empty, returns `None`. #[must_use] - fn first_occupied_square_leading(&self) -> Option { - let leading_zeros = self.0.leading_zeros() as u8; - if leading_zeros < Square::NUM as u8 { + fn first_occupied_square_leading(self) -> Option { + let leading_zeros = self._leading_zeros(); + if leading_zeros < SQUARES_NUM { unsafe { Some(Square::from_index_unchecked( - Square::NUM as u8 - leading_zeros - 1, + SQUARES_NUM - leading_zeros - 1, )) } } else { @@ -273,9 +281,10 @@ impl BitBoard { /// board, starting at the trailing (least-significant) end of the board. /// If the board is empty, returns `None`. #[must_use] - fn first_occupied_square_trailing(&self) -> Option { - let trailing_zeros = self.0.trailing_zeros() as u8; - if trailing_zeros < Square::NUM as u8 { + fn first_occupied_square_trailing(self) -> Option { + let trailing_zeros = self._trailing_zeros(); + + if trailing_zeros < SQUARES_NUM { unsafe { Some(Square::from_index_unchecked(trailing_zeros)) } } else { None @@ -283,6 +292,20 @@ impl BitBoard { } } +impl BitBoard { + #[must_use] + #[allow(clippy::cast_possible_truncation)] + fn _leading_zeros(self) -> u8 { + self.0.leading_zeros() as u8 + } + + #[must_use] + #[allow(clippy::cast_possible_truncation)] + fn _trailing_zeros(self) -> u8 { + self.0.trailing_zeros() as u8 + } +} + impl Default for BitBoard { fn default() -> Self { BitBoard::EMPTY @@ -341,7 +364,7 @@ impl TryFrom for Square { return Err(TryFromBitBoardError::NotSingleSquare); } - unsafe { Ok(Square::from_index_unchecked(value.0.trailing_zeros() as u8)) } + unsafe { Ok(Square::from_index_unchecked(value._trailing_zeros())) } } } @@ -376,8 +399,7 @@ impl fmt::Display for BitBoard { let mut ranks_written = 0; for rank in binary_ranks.chunks(8).rev() { - let joined_rank = rank.join(" "); - write!(f, "{}", joined_rank)?; + write!(f, "{}", rank.join(" "))?; ranks_written += 1; if ranks_written < 8 { @@ -456,6 +478,7 @@ mod tests { } #[test] + #[allow(clippy::unreadable_literal)] fn rank() { assert_eq!(BitBoard::rank(&0).0, 0xFF, "Rank 1"); assert_eq!(BitBoard::rank(&1).0, 0xFF00, "Rank 2"); @@ -469,11 +492,13 @@ mod tests { #[test] fn single_rank_occupancy() { + #[allow(clippy::unreadable_literal)] let bb = BitBoard(0b01010100); + let expected_squares = [Square::G1, Square::E1, Square::C1]; for (a, b) in bb - .occupied_squares(IterationDirection::Leading) - .zip(expected_squares.iter().cloned()) + .occupied_squares(&IterationDirection::Leading) + .zip(expected_squares.iter().copied()) { assert_eq!(a, b); } @@ -481,13 +506,14 @@ mod tests { #[test] fn occupancy_spot_check() { + #[allow(clippy::unreadable_literal)] let bb = BitBoard(0b10000000_00000000_00100000_00000100_00000000_00000000_00010000_00001000); let expected_squares = [Square::H8, Square::F6, Square::C5, Square::E2, Square::D1]; for (a, b) in bb - .occupied_squares(IterationDirection::Leading) + .occupied_squares(&IterationDirection::Leading) .zip(expected_squares.iter().cloned()) { assert_eq!(a, b); From 091cc99cb35bcab26f8c3bbd08c07fb3be31e0d3 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 8 May 2025 17:37:51 -0700 Subject: [PATCH 254/423] WIP --- Cargo.lock | 8 + bitboard/src/bit_scanner.rs | 39 ++- bitboard/src/bitboard.rs | 47 ++- board/src/board.rs | 186 ++++------- board/src/castle/rights.rs | 39 +-- board/src/display.rs | 5 +- board/src/fen.rs | 36 +-- board/src/lib.rs | 8 +- board/src/macros.rs | 29 +- board/src/move_counter.rs | 22 +- board/src/piece_sets.rs | 133 ++++---- board/src/piece_sets/bitboards.rs | 4 +- board/src/piece_sets/mailbox.rs | 12 +- core/src/colors.rs | 17 +- core/src/coordinates.rs | 48 ++- core/src/pieces.rs | 74 +++-- moves/src/builder.rs | 98 +++--- moves/src/defs.rs | 21 ++ moves/src/moves.rs | 53 ++- moves/tests/flags.rs | 3 +- position/Cargo.toml | 1 + position/src/check.rs | 4 +- position/src/lib.rs | 2 +- position/src/macros.rs | 73 +---- position/src/move_generator.rs | 33 +- position/src/move_generator/bishop.rs | 22 +- position/src/move_generator/king.rs | 61 ++-- position/src/move_generator/knight.rs | 10 +- position/src/move_generator/move_set.rs | 5 +- position/src/move_generator/pawn.rs | 77 ++--- position/src/move_generator/queen.rs | 56 ++-- position/src/move_generator/rook.rs | 28 +- position/src/position/builders/mod.rs | 2 - .../src/position/builders/move_builder.rs | 121 ++++--- .../src/position/builders/position_builder.rs | 194 ----------- position/src/position/diagram_formatter.rs | 68 ---- position/src/position/flags.rs | 89 ----- position/src/position/mod.rs | 9 +- position/src/position/piece_sets.rs | 174 ---------- position/src/position/pieces.rs | 130 -------- position/src/position/position.rs | 304 +++++------------- position/src/sight.rs | 122 ++++--- 42 files changed, 805 insertions(+), 1662 deletions(-) delete mode 100644 position/src/position/builders/position_builder.rs delete mode 100644 position/src/position/diagram_formatter.rs delete mode 100644 position/src/position/flags.rs delete mode 100644 position/src/position/piece_sets.rs delete mode 100644 position/src/position/pieces.rs diff --git a/Cargo.lock b/Cargo.lock index 3049c76..ffc42b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,6 +67,7 @@ name = "chessfriend_bitboard" version = "0.1.0" dependencies = [ "chessfriend_core", + "forward_ref", ] [[package]] @@ -95,6 +96,7 @@ name = "chessfriend_position" version = "0.1.0" dependencies = [ "chessfriend_bitboard", + "chessfriend_board", "chessfriend_core", "chessfriend_moves", ] @@ -199,6 +201,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "forward_ref" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" + [[package]] name = "heck" version = "0.4.1" diff --git a/bitboard/src/bit_scanner.rs b/bitboard/src/bit_scanner.rs index c9d5190..7287d27 100644 --- a/bitboard/src/bit_scanner.rs +++ b/bitboard/src/bit_scanner.rs @@ -1,8 +1,10 @@ // Eryn Wells +use chessfriend_core::Square; + macro_rules! bit_scanner { ($name:ident) => { - pub(crate) struct $name { + pub struct $name { bits: u64, shift: usize, } @@ -18,8 +20,15 @@ macro_rules! bit_scanner { bit_scanner!(LeadingBitScanner); bit_scanner!(TrailingBitScanner); +fn _index_to_square(index: usize) -> Square { + unsafe { + #[allow(clippy::cast_possible_truncation)] + Square::from_index_unchecked(index as u8) + } +} + impl Iterator for LeadingBitScanner { - type Item = usize; + type Item = Square; fn next(&mut self) -> Option { let u64bits = u64::BITS as usize; @@ -40,12 +49,12 @@ impl Iterator for LeadingBitScanner { // Shift 1 additional place to account for the 1 that `leading_zeros` found. self.shift += leading_zeros + 1; - Some(position) + Some(_index_to_square(position)) } } impl Iterator for TrailingBitScanner { - type Item = usize; + type Item = Square; fn next(&mut self) -> Option { let u64bits = u64::BITS as usize; @@ -66,7 +75,7 @@ impl Iterator for TrailingBitScanner { // Shift 1 additional place to account for the 1 that `leading_zeros` found. self.shift += trailing_zeros + 1; - Some(position) + Some(_index_to_square(position)) } } @@ -83,17 +92,17 @@ mod tests { #[test] fn leading_one() { let mut scanner = LeadingBitScanner::new(1); - assert_eq!(scanner.next(), Some(0)); + assert_eq!(scanner.next(), Some(Square::A1)); assert_eq!(scanner.next(), None); } #[test] fn leading_complex() { let mut scanner = LeadingBitScanner::new(0b_1100_0101); - assert_eq!(scanner.next(), Some(7)); - assert_eq!(scanner.next(), Some(6)); - assert_eq!(scanner.next(), Some(2)); - assert_eq!(scanner.next(), Some(0)); + assert_eq!(scanner.next(), Some(Square::H1)); + assert_eq!(scanner.next(), Some(Square::G1)); + assert_eq!(scanner.next(), Some(Square::C1)); + assert_eq!(scanner.next(), Some(Square::A1)); assert_eq!(scanner.next(), None); } @@ -106,17 +115,17 @@ mod tests { #[test] fn trailing_one() { let mut scanner = TrailingBitScanner::new(1); - assert_eq!(scanner.next(), Some(0)); + assert_eq!(scanner.next(), Some(Square::A1)); assert_eq!(scanner.next(), None); } #[test] fn trailing_complex() { let mut scanner = TrailingBitScanner::new(0b_1100_0101); - assert_eq!(scanner.next(), Some(0)); - assert_eq!(scanner.next(), Some(2)); - assert_eq!(scanner.next(), Some(6)); - assert_eq!(scanner.next(), Some(7)); + assert_eq!(scanner.next(), Some(Square::A1)); + assert_eq!(scanner.next(), Some(Square::C1)); + assert_eq!(scanner.next(), Some(Square::G1)); + assert_eq!(scanner.next(), Some(Square::H1)); assert_eq!(scanner.next(), None); } } diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index ce1852d..ea1233c 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -144,7 +144,7 @@ impl BitBoard { self.0 != 0 } - /// Returns `true` if this [BitBoard] has the bit corresponding to `square` set. + /// Returns `true` if this [`BitBoard`] has the bit corresponding to `square` set. /// /// ## Examples /// @@ -179,7 +179,7 @@ impl BitBoard { self.0.count_ones() } - /// Set a square in this [BitBoard] by toggling the corresponding bit to 1. + /// Set a square in this [`BitBoard`] by toggling the corresponding bit to 1. /// This always succeeds, even if the bit was already set. /// /// ## Examples @@ -237,21 +237,20 @@ impl BitBoard { &self, direction: &IterationDirection, ) -> Box> { - #[allow(clippy::cast_possible_truncation)] - fn index_to_square(index: usize) -> Square { - unsafe { Square::from_index_unchecked(index as u8) } - } - match direction { - IterationDirection::Leading => { - Box::new(LeadingBitScanner::new(self.0).map(index_to_square)) - } - IterationDirection::Trailing => { - Box::new(TrailingBitScanner::new(self.0).map(index_to_square)) - } + IterationDirection::Leading => Box::new(self.occupied_squares_leading()), + IterationDirection::Trailing => Box::new(self.occupied_squares_trailing()), } } + pub fn occupied_squares_leading(&self) -> LeadingBitScanner { + LeadingBitScanner::new(self.0) + } + + pub fn occupied_squares_trailing(&self) -> TrailingBitScanner { + TrailingBitScanner::new(self.0) + } + #[must_use] pub fn first_occupied_square(&self, direction: &IterationDirection) -> Option { match direction { @@ -264,7 +263,7 @@ impl BitBoard { /// board, starting at the leading (most-significant) end of the board. If /// the board is empty, returns `None`. #[must_use] - fn first_occupied_square_leading(self) -> Option { + pub fn first_occupied_square_leading(self) -> Option { let leading_zeros = self._leading_zeros(); if leading_zeros < SQUARES_NUM { unsafe { @@ -281,7 +280,7 @@ impl BitBoard { /// board, starting at the trailing (least-significant) end of the board. /// If the board is empty, returns `None`. #[must_use] - fn first_occupied_square_trailing(self) -> Option { + pub fn first_occupied_square_trailing(self) -> Option { let trailing_zeros = self._trailing_zeros(); if trailing_zeros < SQUARES_NUM { @@ -496,12 +495,9 @@ mod tests { let bb = BitBoard(0b01010100); let expected_squares = [Square::G1, Square::E1, Square::C1]; - for (a, b) in bb - .occupied_squares(&IterationDirection::Leading) - .zip(expected_squares.iter().copied()) - { - assert_eq!(a, b); - } + bb.occupied_squares(&IterationDirection::Leading) + .zip(expected_squares) + .for_each(|(a, b)| assert_eq!(a, b)); } #[test] @@ -512,12 +508,9 @@ mod tests { let expected_squares = [Square::H8, Square::F6, Square::C5, Square::E2, Square::D1]; - for (a, b) in bb - .occupied_squares(&IterationDirection::Leading) - .zip(expected_squares.iter().cloned()) - { - assert_eq!(a, b); - } + bb.occupied_squares(&IterationDirection::Leading) + .zip(expected_squares) + .for_each(|(a, b)| assert_eq!(a, b)); } #[test] diff --git a/board/src/board.rs b/board/src/board.rs index 334885e..ed719a5 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -1,16 +1,22 @@ // Eryn Wells -use crate::{castle, display::DiagramFormatter, Castle, Clock, PieceSet}; +use crate::{ + castle, + display::DiagramFormatter, + piece_sets::{PlacePieceError, PlacePieceStrategy}, + PieceSet, +}; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; -use std::iter::Iterator; +use chessfriend_core::{Color, Piece, Shape, Square}; -#[derive(Clone, Debug, Default, Eq)] +#[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct Board { - pub clock: Clock, + pub active_color: Color, pub pieces: PieceSet, pub castling_rights: castle::Rights, pub en_passant_target: Option, + pub half_move_clock: u32, + pub full_move_number: u32, } impl Board { @@ -46,12 +52,44 @@ impl Board { ..Default::default() } } +} +impl Board { #[must_use] - pub fn player_to_move(&self) -> Color { - self.clock.active_color() + pub fn get_piece(&self, square: Square) -> Option { + self.pieces.get(square) } + /// Place a piece on the board. + /// + /// ## Errors + /// + /// When is called with [`PlacePieceStrategy::PreserveExisting`], and a piece already exists on + /// `square`, this method returns a [`PlacePieceError::ExistingPiece`] error. + /// + pub fn place_piece( + &mut self, + piece: Piece, + square: Square, + strategy: PlacePieceStrategy, + ) -> Result<(), PlacePieceError> { + self.pieces.place(piece, square, strategy) + } + + pub fn remove_piece(&mut self, square: Square) -> Option { + self.pieces.remove(square) + } +} + +impl Board { + #[must_use] + pub fn display(&self) -> DiagramFormatter<'_> { + DiagramFormatter::new(self) + } +} + +/* +impl Board { /// The rook to use for a castling move. #[must_use] pub fn rook_for_castle(&self, player: Color, castle: Castle) -> Option { @@ -71,73 +109,26 @@ impl Board { /// through a square that an enemy piece can see #[must_use] pub fn player_can_castle(&self, player: Color, castle: Castle) -> bool { - if !self.castling_rights.is_set(player, castle.into()) { + if !self.castling_rights.is_set(player, castle) { return false; } let castling_parameters = castle.parameters(player); - let all_pieces = self.all_pieces_bitboard(); + let all_pieces = self.pieces.all_pieces(); if !(all_pieces & castling_parameters.clear_squares()).is_empty() { return false; } - let danger_squares = self.king_danger(player); - if !(danger_squares & castling_parameters.check_squares()).is_empty() { - return false; - } + // TODO: Reimplement king_danger here or in Position. + // let danger_squares = self.king_danger(player); + // if !(danger_squares & castling_parameters.check_squares()).is_empty() { + // return false; + // } true } - /// A [`BitBoard`] representing the set of squares containing a piece. This - /// set is the inverse of [`Board::empty_squares`]. - #[must_use] - pub fn occupied_squares(&self) -> BitBoard { - self.pieces.all_pieces() - } - - #[must_use] - pub fn friendly_pieces_bitboard(&self) -> BitBoard { - self.pieces.all_pieces_of_color(self.clock.active_color()) - } - - #[must_use] - pub fn opposing_pieces_bitboard(&self) -> BitBoard { - self.pieces - .all_pieces_of_color(self.clock.active_color().other()) - } - - #[must_use] - pub fn all_pieces(&self) -> (BitBoard, BitBoard) { - ( - self.friendly_pieces_bitboard(), - self.opposing_pieces_bitboard(), - ) - } - - pub fn all_pieces_bitboard(&self) -> BitBoard { - self.pieces.all_pieces() - } - - pub fn all_pieces_of_color_bitboard(&self, color: Color) -> BitBoard { - self.pieces.all_pieces_of_color(color) - } - - /// A [`BitBoard`] representing the set of squares containing a piece. This - /// set is the inverse of [`Board::occupied_squares`]. - #[must_use] - pub fn empty_squares(&self) -> BitBoard { - !self.occupied_squares() - } - - #[must_use] - pub fn piece_on_square(&self, square: Square) -> Option { - self.pieces - .get(square) - .map(|piece| PlacedPiece::new(piece, square)) - } - pub fn iter_all_pieces(&self) -> impl Iterator + '_ { self.pieces.iter() } @@ -147,58 +138,8 @@ impl Board { .iter() .filter(move |piece| piece.color() == color) } - - #[must_use] - pub fn en_passant_target(&self) -> Option { - self.en_passant_target - } - - fn king_bitboard(&self, player: Color) -> BitBoard { - self.pieces.bitboard_for_piece(Piece::king(player)) - } - - pub(crate) fn king_square(&self, player: Color) -> Square { - self.king_bitboard(player).try_into().unwrap() - } - - #[must_use] - pub fn display(&self) -> DiagramFormatter<'_> { - DiagramFormatter::new(self) - } - - #[must_use] - pub fn bitboard_for_color(&self, color: Color) -> BitBoard { - self.pieces.bitboard_for_color(color) - } - - #[must_use] - pub fn bitboard_for_piece(&self, piece: Piece) -> BitBoard { - self.pieces.bitboard_for_piece(piece) - } - - /// A [`BitBoard`] representing the squares where a king of the given color will - /// be in danger of being captured by the opposing player. If the king is on - /// one of these squares, it is in check. The king cannot move to these - /// squares. - pub(crate) fn king_danger(&self, color: Color) -> BitBoard { - let board_without_king = { - let mut cloned_board = self.clone(); - cloned_board.pieces.remove(self.king_square(color)); - cloned_board - }; - - BitBoard::full() - } -} - -impl PartialEq for Board { - fn eq(&self, other: &Self) -> bool { - self.pieces == other.pieces - && self.castling_rights == other.castling_rights - && self.en_passant_target == other.en_passant_target - && self.clock == other.clock - } } +*/ #[cfg(test)] mod tests { @@ -207,26 +148,11 @@ mod tests { use chessfriend_core::piece; #[test] - fn piece_on_square() { - let pos = test_board![ + fn get_piece_on_square() { + let board = test_board![ Black Bishop on F7, ]; - let piece = pos.piece_on_square(Square::F7); - assert_eq!(piece, Some(piece!(Black Bishop on F7))); - } - - #[test] - fn piece_in_starting_position() { - let board = test_board!(starting); - - assert_eq!( - board.piece_on_square(Square::H1), - Some(piece!(White Rook on H1)) - ); - assert_eq!( - board.piece_on_square(Square::A8), - Some(piece!(Black Rook on A8)) - ); + assert_eq!(board.get_piece(Square::F7), Some(piece!(Black Bishop))); } } diff --git a/board/src/castle/rights.rs b/board/src/castle/rights.rs index c32a46e..e79aa86 100644 --- a/board/src/castle/rights.rs +++ b/board/src/castle/rights.rs @@ -13,22 +13,25 @@ impl Rights { /// as long as they have not moved their king, or the rook on that side of /// the board. #[must_use] - pub fn is_set(self, color: Color, castle: Castle) -> bool { + pub fn color_has_right(self, color: Color, castle: Castle) -> bool { (self.0 & (1 << Self::flag_offset(color, castle))) != 0 } - pub fn set(&mut self, color: Color, castle: Castle) { + pub fn grant(&mut self, color: Color, castle: Castle) { self.0 |= 1 << Self::flag_offset(color, castle); } - pub fn clear(&mut self, color: Color, castle: Castle) { + pub fn revoke(&mut self, color: Color, castle: Castle) { self.0 &= !(1 << Self::flag_offset(color, castle)); } - pub fn clear_all(&mut self) { + /// Revoke castling rights for all colors and all sides of the board. + pub fn revoke_all(&mut self) { self.0 = 0; } +} +impl Rights { fn flag_offset(color: Color, castle: Castle) -> usize { ((color as usize) << 1) + castle as usize } @@ -61,21 +64,21 @@ mod tests { #[test] fn default_rights() { let mut rights = Rights::default(); - assert!(rights.is_set(Color::White, Castle::KingSide)); - assert!(rights.is_set(Color::White, Castle::QueenSide)); - assert!(rights.is_set(Color::Black, Castle::KingSide)); - assert!(rights.is_set(Color::Black, Castle::QueenSide)); + assert!(rights.color_has_right(Color::White, Castle::KingSide)); + assert!(rights.color_has_right(Color::White, Castle::QueenSide)); + assert!(rights.color_has_right(Color::Black, Castle::KingSide)); + assert!(rights.color_has_right(Color::Black, Castle::QueenSide)); - rights.clear(Color::White, Castle::QueenSide); - assert!(rights.is_set(Color::White, Castle::KingSide)); - assert!(!rights.is_set(Color::White, Castle::QueenSide)); - assert!(rights.is_set(Color::Black, Castle::KingSide)); - assert!(rights.is_set(Color::Black, Castle::QueenSide)); + rights.revoke(Color::White, Castle::QueenSide); + assert!(rights.color_has_right(Color::White, Castle::KingSide)); + assert!(!rights.color_has_right(Color::White, Castle::QueenSide)); + assert!(rights.color_has_right(Color::Black, Castle::KingSide)); + assert!(rights.color_has_right(Color::Black, Castle::QueenSide)); - rights.set(Color::White, Castle::QueenSide); - assert!(rights.is_set(Color::White, Castle::KingSide)); - assert!(rights.is_set(Color::White, Castle::QueenSide)); - assert!(rights.is_set(Color::Black, Castle::KingSide)); - assert!(rights.is_set(Color::Black, Castle::QueenSide)); + rights.grant(Color::White, Castle::QueenSide); + assert!(rights.color_has_right(Color::White, Castle::KingSide)); + assert!(rights.color_has_right(Color::White, Castle::QueenSide)); + assert!(rights.color_has_right(Color::Black, Castle::KingSide)); + assert!(rights.color_has_right(Color::Black, Castle::QueenSide)); } } diff --git a/board/src/display.rs b/board/src/display.rs index 053f9a0..8736b84 100644 --- a/board/src/display.rs +++ b/board/src/display.rs @@ -4,6 +4,7 @@ use crate::Board; use chessfriend_core::{File, Rank, Square}; use std::fmt; +#[must_use] pub struct DiagramFormatter<'a>(&'a Board); impl<'a> DiagramFormatter<'a> { @@ -21,8 +22,8 @@ impl<'a> fmt::Display for DiagramFormatter<'a> { for file in File::ALL { let square = Square::from_file_rank(file, rank); - match self.0.piece_on_square(square) { - Some(placed_piece) => write!(f, "{} ", placed_piece.piece())?, + match self.0.get_piece(square) { + Some(piece) => write!(f, "{piece} ")?, None => write!(f, "· ")?, } } diff --git a/board/src/fen.rs b/board/src/fen.rs index a370702..6fe1c08 100644 --- a/board/src/fen.rs +++ b/board/src/fen.rs @@ -1,6 +1,6 @@ // Eryn Wells -use crate::{piece_sets::PlacePieceStrategy, Board, Castle, EnPassant}; +use crate::{piece_sets::PlacePieceStrategy, Board, Castle}; use chessfriend_core::{ coordinates::ParseSquareError, piece, Color, File, Piece, PlacedPiece, Rank, Square, }; @@ -69,7 +69,7 @@ impl ToFenStr for Board { for rank in Rank::ALL.into_iter().rev() { for file in File::ALL { let square = Square::from_file_rank(file, rank); - match self.piece_on_square(square) { + match self.get_piece(square) { Some(piece) => { if empty_squares > 0 { write!(fen_string, "{empty_squares}") @@ -93,7 +93,7 @@ impl ToFenStr for Board { } } - write!(fen_string, " {}", self.player_to_move().to_fen_str()?) + write!(fen_string, " {}", self.active_color.to_fen_str()?) .map_err(ToFenStrError::FmtError)?; let castling = [ @@ -103,7 +103,7 @@ impl ToFenStr for Board { (Color::Black, Castle::QueenSide), ] .map(|(color, castle)| { - if !self.castling_rights.is_set(color, castle) { + if !self.castling_rights.color_has_right(color, castle) { return ""; } @@ -131,10 +131,8 @@ impl ToFenStr for Board { ) .map_err(ToFenStrError::FmtError)?; - write!(fen_string, " {}", self.clock.half_move_number()) - .map_err(ToFenStrError::FmtError)?; - write!(fen_string, " {}", self.clock.full_move_number()) - .map_err(ToFenStrError::FmtError)?; + write!(fen_string, " {}", self.half_move_clock).map_err(ToFenStrError::FmtError)?; + write!(fen_string, " {}", self.full_move_number).map_err(ToFenStrError::FmtError)?; Ok(fen_string) } @@ -156,7 +154,7 @@ impl ToFenStr for Piece { fn to_fen_str(&self) -> Result { let ascii: char = self.to_ascii(); - Ok(String::from(match self.color() { + Ok(String::from(match self.color { Color::White => ascii.to_ascii_uppercase(), Color::Black => ascii.to_ascii_lowercase(), })) @@ -214,20 +212,20 @@ impl FromFenStr for Board { .next() .ok_or(FromFenStrError::MissingField(Field::PlayerToMove))?, )?; - board.clock.active_color = active_color; + board.active_color = active_color; let castling_rights = fields .next() .ok_or(FromFenStrError::MissingField(Field::CastlingRights))?; if castling_rights == "-" { - board.castling_rights.clear_all(); + board.castling_rights.revoke_all(); } else { for ch in castling_rights.chars() { match ch { - 'K' => board.castling_rights.set(Color::White, Castle::KingSide), - 'Q' => board.castling_rights.set(Color::White, Castle::QueenSide), - 'k' => board.castling_rights.set(Color::Black, Castle::KingSide), - 'q' => board.castling_rights.set(Color::Black, Castle::QueenSide), + 'K' => board.castling_rights.grant(Color::White, Castle::KingSide), + 'Q' => board.castling_rights.grant(Color::White, Castle::QueenSide), + 'k' => board.castling_rights.grant(Color::Black, Castle::KingSide), + 'q' => board.castling_rights.grant(Color::Black, Castle::QueenSide), _ => return Err(FromFenStrError::InvalidValue), }; } @@ -245,18 +243,18 @@ impl FromFenStr for Board { let half_move_clock = fields .next() .ok_or(FromFenStrError::MissingField(Field::HalfMoveClock))?; - let half_move_clock: u16 = half_move_clock + let half_move_clock: u32 = half_move_clock .parse() .map_err(FromFenStrError::ParseIntError)?; - board.clock.half_move_number = half_move_clock; + board.half_move_clock = half_move_clock; let full_move_counter = fields .next() .ok_or(FromFenStrError::MissingField(Field::FullMoveCounter))?; - let full_move_counter: u16 = full_move_counter + let full_move_counter: u32 = full_move_counter .parse() .map_err(FromFenStrError::ParseIntError)?; - board.clock.full_move_number = full_move_counter; + board.full_move_number = full_move_counter; debug_assert_eq!(fields.next(), None); diff --git a/board/src/lib.rs b/board/src/lib.rs index 6a5bc2a..f6c8adb 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -7,12 +7,14 @@ pub mod fen; pub mod macros; mod board; -mod move_counter; mod piece_sets; pub use board::Board; -pub use move_counter::Clock; use castle::Castle; use en_passant::EnPassant; -use piece_sets::{PieceSet, PlacePieceError, PlacePieceStrategy}; +use piece_sets::PieceSet; + +// Used by macros. +#[allow(unused_imports)] +use piece_sets::{PlacePieceError, PlacePieceStrategy}; diff --git a/board/src/macros.rs b/board/src/macros.rs index f3ca5dc..da1984b 100644 --- a/board/src/macros.rs +++ b/board/src/macros.rs @@ -1,34 +1,19 @@ // Eryn Wells -#[macro_export] -macro_rules! board { - [$($color:ident $shape:ident on $square:ident),* $(,)?] => { - $crate::Builder::new() - $(.place_piece( - chessfriend_core::PlacedPiece::new( - chessfriend_core::Piece::new( - chessfriend_core::Color::$color, - chessfriend_core::Shape::$shape), - chessfriend_core::Square::$square - ) - ))* - .build() - }; -} - #[macro_export] macro_rules! test_board { ($to_move:ident, [ $($color:ident $shape:ident on $square:ident),* $(,)? ], $en_passant:ident) => { { let mut board = $crate::Board::empty(); - $(let _ = board.pieces.place( + $(let _ = board.place_piece( chessfriend_core::Piece::new( chessfriend_core::Color::$color, chessfriend_core::Shape::$shape ), - chessfriend_core::Square::$square); + chessfriend_core::Square::$square, + $crate::PlacePieceStrategy::default()); )* - board.clock.active_color = chessfriend_core::Color::$to_move; + board.active_color = chessfriend_core::Color::$to_move; board.en_passant_target = Some(chessfriend_core::Square::$en_passant); println!("{}", board.display()); @@ -39,7 +24,7 @@ macro_rules! test_board { ($to_move:ident, [ $($color:ident $shape:ident on $square:ident),* $(,)? ]) => { { let mut board = $crate::Board::empty(); - $(let _ = board.pieces.place( + $(let _ = board.place_piece( chessfriend_core::Piece::new( chessfriend_core::Color::$color, chessfriend_core::Shape::$shape @@ -47,7 +32,7 @@ macro_rules! test_board { chessfriend_core::Square::$square, $crate::PlacePieceStrategy::default()); )* - board.clock.active_color = chessfriend_core::Color::$to_move; + board.active_color = chessfriend_core::Color::$to_move; println!("{}", board.display()); @@ -57,7 +42,7 @@ macro_rules! test_board { ($($color:ident $shape:ident on $square:ident),* $(,)?) => { { let mut board = $crate::Board::empty(); - $(let _ = board.pieces.place( + $(let _ = board.place_piece( chessfriend_core::Piece::new( chessfriend_core::Color::$color, chessfriend_core::Shape::$shape diff --git a/board/src/move_counter.rs b/board/src/move_counter.rs index cd24bdf..5de7760 100644 --- a/board/src/move_counter.rs +++ b/board/src/move_counter.rs @@ -9,32 +9,14 @@ pub enum AdvanceHalfMove { #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct Clock { - /// The player who's turn it is to move. - pub(crate) active_color: Color, - /// The number of completed turns. A turn finishes when every player has moved. - pub(crate) full_move_number: u16, + pub full_move_number: u16, /// The number of moves by all players since the last pawn advance or capture. - pub(crate) half_move_number: u16, + pub half_move_number: u16, } impl Clock { - #[must_use] - pub fn active_color(&self) -> Color { - self.active_color - } - - #[must_use] - pub fn full_move_number(&self) -> u16 { - self.full_move_number - } - - #[must_use] - pub fn half_move_number(&self) -> u16 { - self.half_move_number - } - pub fn advance(&mut self, advance_half_move: &AdvanceHalfMove) { let next_color = self.active_color.next(); diff --git a/board/src/piece_sets.rs b/board/src/piece_sets.rs index 2467b9d..8096100 100644 --- a/board/src/piece_sets.rs +++ b/board/src/piece_sets.rs @@ -1,16 +1,15 @@ // Eryn Wells -mod bitboards; mod mailbox; -pub(crate) use mailbox::Mailbox; - -use bitboards::{ByColor, ByColorAndShape}; +use self::mailbox::Mailbox; use chessfriend_bitboard::{BitBoard, IterationDirection}; use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; +use std::ops::BitOr; -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum PlacePieceStrategy { + #[default] Replace, PreserveExisting, } @@ -20,73 +19,54 @@ pub enum PlacePieceError { ExisitingPiece(PlacedPiece), } -impl Default for PlacePieceStrategy { - fn default() -> Self { - Self::Replace - } -} - /// The internal data structure of a [Board] that efficiently manages the /// placement of pieces on the board. #[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] pub struct PieceSet { mailbox: Mailbox, - by_color: ByColor, - by_color_and_shape: ByColorAndShape, + color_occupancy: [BitBoard; Color::NUM], + shape_occupancy: [BitBoard; Shape::NUM], } impl PieceSet { pub(crate) fn new(pieces: [[BitBoard; Shape::NUM]; Color::NUM]) -> Self { - use std::ops::BitOr; - - let white_pieces = pieces[Color::White as usize] - .iter() - .fold(BitBoard::empty(), BitOr::bitor); - let black_pieces = pieces[Color::Black as usize] - .iter() - .fold(BitBoard::empty(), BitOr::bitor); - - let all_pieces = white_pieces | black_pieces; - let mut mailbox = Mailbox::default(); - for c in Color::into_iter() { - for s in Shape::into_iter() { - let bitboard = pieces[c as usize][s as usize]; + let mut color_occupancy: [BitBoard; Color::NUM] = Default::default(); + let mut shape_occupancy: [BitBoard; Shape::NUM] = Default::default(); + + for (color_index, color) in Color::iter().enumerate() { + for (shape_index, shape) in Shape::into_iter().enumerate() { + let bitboard = pieces[color_index][shape_index]; + + color_occupancy[color_index] |= bitboard; + shape_occupancy[shape_index] |= bitboard; + for square in bitboard.occupied_squares(&IterationDirection::default()) { - mailbox.set(Piece::new(c, s), square); + let piece = Piece::new(*color, shape); + mailbox.set(piece, square); } } } Self { - by_color: ByColor::new(all_pieces, [white_pieces, black_pieces]), - by_color_and_shape: ByColorAndShape::new(pieces), mailbox, + color_occupancy, + shape_occupancy, } } /// A [`BitBoard`] representing all the pieces currently on the board. Other /// engines might refer to this concept as 'occupancy'. - pub(crate) fn all_pieces(&self) -> BitBoard { - self.by_color.all() - } - - pub(crate) fn all_pieces_of_color(&self, color: Color) -> BitBoard { - self.by_color.bitboard(color) + pub fn all_pieces(&self) -> BitBoard { + self.color_occupancy + .iter() + .fold(BitBoard::empty(), BitOr::bitor) } pub(crate) fn iter(&self) -> impl Iterator { self.mailbox.iter() } - pub(super) fn bitboard_for_color(&self, color: Color) -> BitBoard { - self.by_color.bitboard(color) - } - - pub(crate) fn bitboard_for_piece(&self, piece: Piece) -> BitBoard { - self.by_color_and_shape.bitboard_for_piece(piece) - } - pub(crate) fn get(&self, square: Square) -> Option { self.mailbox.get(square) } @@ -96,9 +76,7 @@ impl PieceSet { piece: Piece, square: Square, strategy: PlacePieceStrategy, - ) -> Result { - let color = piece.color(); - + ) -> Result<(), PlacePieceError> { if strategy == PlacePieceStrategy::PreserveExisting { if let Some(existing_piece) = self.mailbox.get(square) { return Err(PlacePieceError::ExisitingPiece(PlacedPiece::new( @@ -108,35 +86,78 @@ impl PieceSet { } } - let piece: Piece = piece.into(); - self.by_color_and_shape.set_square(square, piece); - self.by_color.set_square(square, color); + let color = piece.color; + let shape = piece.shape; + + self.color_occupancy[color as usize].set(square); + self.shape_occupancy[shape as usize].set(square); self.mailbox.set(piece, square); - Ok(PlacedPiece::new(piece, square)) + Ok(()) } - pub(crate) fn remove(&mut self, square: Square) -> Option { + pub(crate) fn remove(&mut self, square: Square) -> Option { if let Some(piece) = self.mailbox.get(square) { - self.by_color_and_shape.clear_square(square, piece.into()); - self.by_color.clear_square(square, piece.color()); + self.color_occupancy[piece.color as usize].clear(square); + self.shape_occupancy[piece.shape as usize].clear(square); self.mailbox.remove(square); - Some(PlacedPiece::new(piece, square)) + Some(piece) } else { None } } } +impl PieceSet { + pub fn color_bitboard(&self, color: Color) -> BitBoard { + self.color_occupancy[color as usize] + } + + pub fn piece_bitboard(&self, piece: Piece) -> BitBoard { + let color_occupancy = self.color_occupancy[piece.color as usize]; + let shape_occupancy = self.shape_occupancy[piece.shape as usize]; + color_occupancy & shape_occupancy + } +} + impl FromIterator for PieceSet { fn from_iter>(iter: T) -> Self { let mut pieces: Self = Self::default(); for piece in iter { - let _ = pieces.place(piece.piece(), piece.square(), PlacePieceStrategy::default()); + let _ = pieces.place(piece.piece, piece.square, PlacePieceStrategy::default()); } pieces } } + +#[cfg(test)] +mod tests { + use super::*; + use chessfriend_bitboard::bitboard; + + #[test] + fn place_piece() -> Result<(), PlacePieceError> { + let mut pieces = PieceSet::default(); + + pieces.place( + Piece::king(Color::White), + Square::F5, + PlacePieceStrategy::default(), + )?; + + assert_eq!( + pieces.mailbox.get(Square::F5), + Some(Piece::king(Color::White)) + ); + assert_eq!(pieces.color_bitboard(Color::White), bitboard![F5]); + assert_eq!( + pieces.piece_bitboard(Piece::king(Color::White)), + bitboard![F5] + ); + + Ok(()) + } +} diff --git a/board/src/piece_sets/bitboards.rs b/board/src/piece_sets/bitboards.rs index 5c09432..c8c8959 100644 --- a/board/src/piece_sets/bitboards.rs +++ b/board/src/piece_sets/bitboards.rs @@ -39,11 +39,11 @@ impl ByColorAndShape { } pub(super) fn bitboard_for_piece(&self, piece: Piece) -> BitBoard { - self.0[piece.color() as usize][piece.shape() as usize] + self.0[piece.color as usize][piece.shape as usize] } pub(super) fn bitboard_for_piece_mut(&mut self, piece: Piece) -> &mut BitBoard { - &mut self.0[piece.color() as usize][piece.shape() as usize] + &mut self.0[piece.color as usize][piece.shape as usize] } pub(super) fn set_square(&mut self, square: Square, piece: Piece) { diff --git a/board/src/piece_sets/mailbox.rs b/board/src/piece_sets/mailbox.rs index 4fc9ec4..0ab8e21 100644 --- a/board/src/piece_sets/mailbox.rs +++ b/board/src/piece_sets/mailbox.rs @@ -7,23 +7,23 @@ use std::iter::FromIterator; pub(crate) struct Mailbox([Option; Square::NUM]); impl Mailbox { - pub(crate) fn new() -> Self { + pub fn new() -> Self { Self::default() } - pub(crate) fn get(&self, square: Square) -> Option { + pub fn get(&self, square: Square) -> Option { self.0[square as usize] } - pub(crate) fn set(&mut self, piece: Piece, square: Square) { + pub fn set(&mut self, piece: Piece, square: Square) { self.0[square as usize] = Some(piece); } - pub(crate) fn remove(&mut self, square: Square) { + pub fn remove(&mut self, square: Square) { self.0[square as usize] = None; } - pub(crate) fn iter(&self) -> impl Iterator { + pub fn iter(&self) -> impl Iterator { self.0 .into_iter() .flatten() // Remove the Nones @@ -50,7 +50,7 @@ impl FromIterator for Mailbox { fn from_iter>(iter: T) -> Self { iter.into_iter() .fold(Self::new(), |mut mailbox, placed_piece| { - mailbox.set(placed_piece.piece(), placed_piece.square()); + mailbox.set(placed_piece.piece(), placed_piece.square); mailbox }) } diff --git a/core/src/colors.rs b/core/src/colors.rs index 9d67877..fee6558 100644 --- a/core/src/colors.rs +++ b/core/src/colors.rs @@ -1,6 +1,6 @@ // Eryn Wells -use crate::{errors::TryFromCharError, try_from_string}; +use crate::{errors::TryFromCharError, try_from_string, Direction}; use std::fmt; #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] @@ -21,6 +21,7 @@ impl Color { Color::ALL.iter() } + #[must_use] pub fn into_iter() -> std::array::IntoIter { Color::ALL.into_iter() } @@ -33,6 +34,20 @@ impl Color { Color::Black => Color::White, } } + + /// "Forward" direction of pawn pushes for this color. + #[must_use] + pub fn push_direction(&self) -> Direction { + match self { + Color::White => Direction::North, + Color::Black => Direction::South, + } + } + + #[must_use] + pub const fn next(&self) -> Color { + Self::ALL[((*self as usize) + 1) % Self::NUM] + } } impl fmt::Display for Color { diff --git a/core/src/coordinates.rs b/core/src/coordinates.rs index e796949..9bdfc23 100644 --- a/core/src/coordinates.rs +++ b/core/src/coordinates.rs @@ -217,6 +217,43 @@ coordinate_enum!(Square, [ A8, B8, C8, D8, E8, F8, G8, H8 ]); +/// Generate an enum that maps its values to variants of [Square]. +macro_rules! to_square_enum { + ($vis:vis $name:ident { $($variant:ident)* }) => { + #[repr(u8)] + #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] + $vis enum $name { + $($variant = Square::$variant as u8,)* + } + + impl From<$name> for Square { + fn from(value: $name) -> Self { + unsafe { Square::from_index_unchecked(value as u8) } + } + } + }; +} + +to_square_enum!( + pub EnPassantTargetSquare { + A3 B3 C3 D3 E3 F3 G3 H3 + A6 B6 C6 D6 E6 F6 G6 H6 + } +); + +// impl TryFrom for EnPassantTargetSquare { +// type Error = (); + +// fn try_from(value: Square) -> Result { +// let square = Self::ALL[value as usize]; +// if square as usize == value as usize { +// Ok(square) +// } else { +// Err(()) +// } +// } +// } + impl Square { /// # Safety /// @@ -240,20 +277,24 @@ impl Square { s.parse() } + #[must_use] #[inline] pub fn file(self) -> File { unsafe { File::new_unchecked((self as u8) & 0b000_00111) } } + #[must_use] #[inline] pub fn rank(self) -> Rank { unsafe { Rank::new_unchecked((self as u8) >> 3) } } + #[must_use] pub fn file_rank(&self) -> (File, Rank) { (self.file(), self.rank()) } + #[must_use] pub fn neighbor(self, direction: Direction) -> Option { let index: u8 = self as u8; let dir: i8 = direction.to_offset(); @@ -347,10 +388,9 @@ impl From for char { } } -impl Into for Rank { - fn into(self) -> char { - let value: u8 = self.into(); - (value + b'1') as char +impl From for char { + fn from(value: Rank) -> Self { + Self::from(value.0) } } diff --git a/core/src/pieces.rs b/core/src/pieces.rs index 00d0536..dd6de93 100644 --- a/core/src/pieces.rs +++ b/core/src/pieces.rs @@ -3,6 +3,34 @@ use crate::{errors::TryFromCharError, try_from_string, Color, Square}; use std::{array, fmt, slice}; +trait _Shape { + fn symbol(&self) -> char; + fn index(&self) -> usize; +} + +macro_rules! shape { + ($name:ident, $index:expr, $symbol:expr) => { + struct $name; + + impl _Shape for $name { + fn symbol(&self) -> char { + $symbol + } + + fn index(&self) -> usize { + $index + } + } + }; +} + +shape!(Pawn, 0, 'P'); +shape!(Knight, 1, 'K'); +shape!(Bishop, 2, 'B'); +shape!(Rook, 3, 'R'); +shape!(Queen, 4, 'Q'); +shape!(King, 5, 'K'); + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum Shape { Pawn = 0, @@ -94,8 +122,8 @@ impl fmt::Display for Shape { #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct Piece { - color: Color, - shape: Shape, + pub color: Color, + pub shape: Shape, } macro_rules! piece_constructor { @@ -132,16 +160,6 @@ impl Piece { piece_constructor!(queen, Queen); piece_constructor!(king, King); - #[must_use] - pub fn color(&self) -> Color { - self.color - } - - #[must_use] - pub fn shape(&self) -> Shape { - self.shape - } - is_shape!(is_pawn, Pawn); is_shape!(is_knight, Knight); is_shape!(is_bishop, Bishop); @@ -195,10 +213,11 @@ impl From<&PlacedPiece> for Piece { } } +#[deprecated] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct PlacedPiece { - piece: Piece, - square: Square, + pub piece: Piece, + pub square: Square, } macro_rules! is_shape { @@ -223,27 +242,6 @@ impl PlacedPiece { self.piece } - /// The square the piece is on - #[inline] - #[must_use] - pub fn square(&self) -> Square { - self.square - } - - /// The piece's [Color] - #[inline] - #[must_use] - pub fn color(&self) -> Color { - self.piece.color - } - - /// The piece's [Shape] - #[inline] - #[must_use] - pub fn shape(&self) -> Shape { - self.piece.shape - } - is_shape!(is_pawn, Pawn); is_shape!(is_knight, Knight); is_shape!(is_bishop, Bishop); @@ -254,7 +252,7 @@ impl PlacedPiece { #[must_use] pub fn is_kingside_rook(&self) -> bool { self.is_rook() - && match self.color() { + && match self.piece.color { Color::White => self.square == Square::H1, Color::Black => self.square == Square::H8, } @@ -263,7 +261,7 @@ impl PlacedPiece { #[must_use] pub fn is_queenside_rook(&self) -> bool { self.is_rook() - && match self.color() { + && match self.piece.color { Color::White => self.square == Square::A1, Color::Black => self.square == Square::A8, } @@ -294,6 +292,6 @@ mod tests { #[test] fn shape_into_char() { - assert_eq!(>::into(Shape::Pawn) as char, 'P'); + assert_eq!(>::into(Shape::Pawn), 'P'); } } diff --git a/moves/src/builder.rs b/moves/src/builder.rs index 0ead866..e75a511 100644 --- a/moves/src/builder.rs +++ b/moves/src/builder.rs @@ -16,6 +16,12 @@ pub enum Error { InvalidEnPassantSquare, } +const MASK: u16 = 0b111_111; + +fn build_move_bits(origin_square: Square, target_square: Square) -> u16 { + (origin_square as u16 & MASK) << 4 | (target_square as u16 & MASK) << 10 +} + pub trait Style { fn origin_square(&self) -> Option { None @@ -25,22 +31,18 @@ pub trait Style { None } - fn into_move_bits(&self) -> EncodedMoveResult { + fn move_bits(&self) -> EncodedMoveResult { let origin_square = self.origin_square().ok_or(Error::MissingOriginSquare)?; let target_square = self.target_square().ok_or(Error::MissingTargetSquare)?; - Ok(self._build_move_bits(origin_square, target_square)) + Ok(build_move_bits(origin_square, target_square)) } - unsafe fn into_move_bits_unchecked(&self) -> u16 { + unsafe fn move_bits_unchecked(&self) -> u16 { let origin_square = self.origin_square().unwrap(); let target_square = self.target_square().unwrap(); - self._build_move_bits(origin_square, target_square) - } - - fn _build_move_bits(&self, origin_square: Square, target_square: Square) -> u16 { - (origin_square as u16 & 0b111111) << 4 | (target_square as u16 & 0b111111) << 10 + build_move_bits(origin_square, target_square) } } @@ -88,12 +90,6 @@ pub struct Castle { castle: castle::Castle, } -impl EnPassantCapture { - fn _build_move_bits(&self, origin_square: Square, target_square: Square) -> u16 { - (origin_square as u16 & 0b111111) << 4 | (target_square as u16 & 0b111111) << 10 - } -} - impl Style for Null {} impl Style for Push { @@ -137,10 +133,12 @@ impl Style for DoublePush { Some(self.to) } - fn into_move_bits(&self) -> StdResult { - Ok(Kind::DoublePush as u16 - | (self.from as u16 & 0b111111) << 4 - | (self.to as u16 & 0b111111) << 10) + fn move_bits(&self) -> StdResult { + Ok( + Kind::DoublePush as u16 + | (self.from as u16 & MASK) << 4 + | (self.to as u16 & MASK) << 10, + ) } } @@ -153,11 +151,11 @@ impl Style for EnPassantCapture { self.push.to } - fn into_move_bits(&self) -> EncodedMoveResult { + fn move_bits(&self) -> EncodedMoveResult { let origin_square = self.origin_square().ok_or(Error::MissingOriginSquare)?; let target_square = self.target_square().ok_or(Error::MissingTargetSquare)?; - Ok(self._build_move_bits(origin_square, target_square)) + Ok(build_move_bits(origin_square, target_square)) } } @@ -182,7 +180,7 @@ impl Style for Promotion { } impl Promotion { - fn into_move_bits(&self) -> StdResult { + fn move_bits(&self) -> StdResult { let origin_square = self .style .origin_square() @@ -194,13 +192,13 @@ impl Promotion { Ok(Kind::Promotion as u16 | self.promotion as u16 - | (origin_square & 0b111111) << 4 - | (target_square & 0b111111) << 10) + | (origin_square & MASK << 4) + | (target_square & MASK << 10)) } } impl Promotion { - fn into_move_bits(&self) -> StdResult { + fn move_bits(&self) -> StdResult { let origin_square = self .style .origin_square() @@ -212,25 +210,28 @@ impl Promotion { Ok(Kind::CapturePromotion as u16 | self.promotion as u16 - | (origin_square & 0b111111) << 4 - | (target_square & 0b111111) << 10) + | (origin_square & MASK) << 4 + | (target_square & MASK) << 10) } } impl Builder { + #[must_use] pub fn new() -> Self { - Self { style: Null } + Self::default() } + #[must_use] pub fn push(piece: &PlacedPiece) -> Builder { Builder { style: Push { - from: Some(piece.square()), + from: Some(piece.square), to: None, }, } } + #[must_use] pub fn double_push(file: File, color: Color) -> Builder { let (from, to) = match color { Color::White => ( @@ -248,16 +249,19 @@ impl Builder { } } + #[must_use] pub fn castling(color: Color, castle: castle::Castle) -> Builder { Builder { style: Castle { color, castle }, } } + #[must_use] pub fn capturing_piece(piece: &PlacedPiece, capturing: &PlacedPiece) -> Builder { - Self::push(piece).capturing_piece(&capturing) + Self::push(piece).capturing_piece(capturing) } + #[must_use] pub fn from(&self, square: Square) -> Builder { Builder { style: Push { @@ -267,11 +271,18 @@ impl Builder { } } + #[must_use] pub fn build(&self) -> Move { Move(0) } } +impl Default for Builder { + fn default() -> Self { + Self { style: Null } + } +} + impl Builder { pub fn from(&mut self, square: Square) -> &mut Self { self.style.from = Some(square); @@ -283,6 +294,7 @@ impl Builder { self } + #[must_use] pub fn capturing_on(&self, square: Square) -> Builder { let mut style = self.style.clone(); style.to = Some(square); @@ -295,6 +307,7 @@ impl Builder { } } + #[must_use] pub fn capturing_en_passant_on(&self, target_square: Square) -> Builder { match EnPassant::from_target_square(target_square) { Some(en_passant) => { @@ -312,15 +325,17 @@ impl Builder { } } + #[must_use] pub fn capturing_piece(&self, piece: &PlacedPiece) -> Builder { Builder { style: Capture { push: self.style.clone(), - capture: Some(piece.square()), + capture: Some(piece.square), }, } } + #[must_use] pub fn promoting_to(&self, shape: PromotionShape) -> Builder> { Builder { style: Promotion { @@ -331,7 +346,7 @@ impl Builder { } pub fn build(&self) -> Result { - Ok(Move(Kind::Quiet as u16 | self.style.into_move_bits()?)) + Ok(Move(Kind::Quiet as u16 | self.style.move_bits()?)) } } @@ -346,11 +361,12 @@ impl Builder { } pub fn build(&self) -> Result { - Ok(Move(self.bits() | self.style.into_move_bits()?)) + Ok(Move(self.bits() | self.style.move_bits()?)) } } impl Builder { + #[must_use] pub fn promoting_to(self, shape: PromotionShape) -> Builder> { Builder { style: Promotion { @@ -361,36 +377,42 @@ impl Builder { } pub fn build(&self) -> Result { - Ok(Move(Kind::Capture as u16 | self.style.into_move_bits()?)) + Ok(Move(Kind::Capture as u16 | self.style.move_bits()?)) } } impl Builder { pub fn build(&self) -> Result { - Ok(Move(Kind::DoublePush as u16 | self.style.into_move_bits()?)) + Ok(Move(Kind::DoublePush as u16 | self.style.move_bits()?)) } } impl Builder { + /// Builds an en passant move. + /// + /// ## Safety + /// + /// This method builds without doing error checking. + #[must_use] pub unsafe fn build_unchecked(&self) -> Move { - Move(Kind::EnPassantCapture as u16 | self.style.into_move_bits_unchecked()) + Move(Kind::EnPassantCapture as u16 | self.style.move_bits_unchecked()) } pub fn build(&self) -> Result { Ok(Move( - Kind::EnPassantCapture as u16 | self.style.into_move_bits()?, + Kind::EnPassantCapture as u16 | self.style.move_bits()?, )) } } impl Builder> { pub fn build(&self) -> Result { - Ok(Move(self.style.into_move_bits()?)) + Ok(Move(self.style.move_bits()?)) } } impl Builder> { pub fn build(&self) -> Result { - Ok(Move(self.style.into_move_bits()?)) + Ok(Move(self.style.move_bits()?)) } } diff --git a/moves/src/defs.rs b/moves/src/defs.rs index 337f30e..8254cb9 100644 --- a/moves/src/defs.rs +++ b/moves/src/defs.rs @@ -2,6 +2,7 @@ use chessfriend_core::Shape; +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum Kind { Quiet = 0b0000, DoublePush = 0b0001, @@ -13,6 +14,12 @@ pub(crate) enum Kind { CapturePromotion = 0b1100, } +impl Kind { + fn is_reversible(self) -> bool { + (self as u16) & 0b1100 == 0 + } +} + #[repr(u16)] #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum PromotionShape { @@ -32,3 +39,17 @@ impl From for Shape { } } } + +impl TryFrom for PromotionShape { + type Error = (); + + fn try_from(value: Shape) -> Result { + match value { + Shape::Knight => Ok(PromotionShape::Knight), + Shape::Bishop => Ok(PromotionShape::Bishop), + Shape::Rook => Ok(PromotionShape::Rook), + Shape::Queen => Ok(PromotionShape::Queen), + _ => Err(()), + } + } +} diff --git a/moves/src/moves.rs b/moves/src/moves.rs index 8e7c926..b7d2539 100644 --- a/moves/src/moves.rs +++ b/moves/src/moves.rs @@ -5,19 +5,29 @@ use chessfriend_board::castle::Castle; use chessfriend_core::{Rank, Shape, Square}; use std::fmt; -/// A single player's move. In chess parlance, this is a "ply". +/// A single player's move. In game theory parlance, this is a "ply". +/// +/// ## TODO +/// +/// - Rename this class `Ply`. +/// #[derive(Clone, Copy, Eq, Hash, PartialEq)] pub struct Move(pub(crate) u16); impl Move { + #[must_use] + #[allow(clippy::missing_panics_doc)] pub fn origin_square(&self) -> Square { - ((self.0 >> 4) & 0b111111).try_into().unwrap() + ((self.0 >> 4) & 0b111_111).try_into().unwrap() } + #[must_use] + #[allow(clippy::missing_panics_doc)] pub fn target_square(&self) -> Square { (self.0 >> 10).try_into().unwrap() } + #[must_use] pub fn capture_square(&self) -> Option { if self.is_en_passant() { let target_square = self.target_square(); @@ -35,18 +45,22 @@ impl Move { None } + #[must_use] pub fn is_quiet(&self) -> bool { self.flags() == Kind::Quiet as u16 } + #[must_use] pub fn is_double_push(&self) -> bool { self.flags() == Kind::DoublePush as u16 } + #[must_use] pub fn is_castle(&self) -> bool { self.castle().is_some() } + #[must_use] pub fn castle(&self) -> Option { match self.flags() { 0b0010 => Some(Castle::KingSide), @@ -55,18 +69,22 @@ impl Move { } } + #[must_use] pub fn is_capture(&self) -> bool { (self.0 & 0b0100) != 0 } + #[must_use] pub fn is_en_passant(&self) -> bool { self.flags() == 0b0101 } + #[must_use] pub fn is_promotion(&self) -> bool { (self.0 & 0b1000) != 0 } + #[must_use] pub fn promotion(&self) -> Option { if !self.is_promotion() { return None; @@ -80,18 +98,22 @@ impl Move { _ => unreachable!(), }) } +} +impl Move { #[inline] - fn flags(&self) -> u16 { + fn flags(self) -> u16 { self.0 & 0b1111 } #[inline] - fn special(&self) -> u16 { + fn special(self) -> u16 { self.0 & 0b11 } +} - fn _transfer_char(&self) -> char { +impl Move { + fn _transfer_char(self) -> char { if self.is_capture() || self.is_en_passant() { 'x' } else { @@ -103,22 +125,19 @@ impl Move { impl fmt::Display for Move { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(castle) = self.castle() { - match castle { - Castle::KingSide => return write!(f, "0-0"), - Castle::QueenSide => return write!(f, "0-0-0"), - } + return match castle { + Castle::KingSide => write!(f, "0-0"), + Castle::QueenSide => write!(f, "0-0-0"), + }; } - write!( - f, - "{}{}{}", - self.origin_square(), - self._transfer_char(), - self.target_square() - )?; + let origin = self.origin_square(); + let target = self.target_square(); + let transfer_char = self._transfer_char(); + write!(f, "{origin}{transfer_char}{target}")?; if let Some(promotion) = self.promotion() { - write!(f, "={}", promotion)?; + write!(f, "={promotion}")?; } else if self.is_en_passant() { write!(f, " e.p.")?; } diff --git a/moves/tests/flags.rs b/moves/tests/flags.rs index 6d001cb..c611f31 100644 --- a/moves/tests/flags.rs +++ b/moves/tests/flags.rs @@ -1,7 +1,8 @@ // Eryn Wells +use chessfriend_board::castle::Castle; use chessfriend_core::{piece, Color, File, Shape, Square}; -use chessfriend_moves::{testing::*, Builder, Castle, PromotionShape}; +use chessfriend_moves::{testing::*, Builder, PromotionShape}; macro_rules! assert_flag { ($move:expr, $left:expr, $right:expr, $desc:expr) => { diff --git a/position/Cargo.toml b/position/Cargo.toml index 07157eb..949e00c 100644 --- a/position/Cargo.toml +++ b/position/Cargo.toml @@ -8,4 +8,5 @@ edition = "2021" [dependencies] chessfriend_core = { path = "../core" } chessfriend_bitboard = { path = "../bitboard" } +chessfriend_board = { path = "../board" } chessfriend_moves = { path = "../moves" } diff --git a/position/src/check.rs b/position/src/check.rs index 06baf74..8619e29 100644 --- a/position/src/check.rs +++ b/position/src/check.rs @@ -40,8 +40,8 @@ impl CheckingPieces { /// A BitBoard representing the set of squares to which a player can move a piece to block a /// checking piece. - pub fn push_mask(&self, king: &BitBoard) -> BitBoard { - let target = king.first_occupied_square().unwrap(); + pub fn push_mask(&self, king: BitBoard) -> BitBoard { + let target = king.first_occupied_square_leading().unwrap(); macro_rules! push_mask_for_shape { ($push_mask:expr, $shape:ident, $king:expr) => {{ diff --git a/position/src/lib.rs b/position/src/lib.rs index c740782..4584f3b 100644 --- a/position/src/lib.rs +++ b/position/src/lib.rs @@ -13,4 +13,4 @@ mod macros; #[macro_use] mod testing; -pub use position::{MakeMoveError, MoveBuilder as MakeMoveBuilder, Position, PositionBuilder}; +pub use position::{MakeMoveError, MoveBuilder as MakeMoveBuilder, Position}; diff --git a/position/src/macros.rs b/position/src/macros.rs index 260a502..3bce9c1 100644 --- a/position/src/macros.rs +++ b/position/src/macros.rs @@ -3,16 +3,7 @@ #[macro_export] macro_rules! position { [$($color:ident $shape:ident on $square:ident),* $(,)?] => { - $crate::PositionBuilder::new() - $(.place_piece( - chessfriend_core::PlacedPiece::new( - chessfriend_core::Piece::new( - chessfriend_core::Color::$color, - chessfriend_core::Shape::$shape), - chessfriend_core::Square::$square - ) - ))* - .build() + $crate::Position::new(chessfriend_board::board!($($color $shape on $square),*)) }; } @@ -21,72 +12,26 @@ macro_rules! position { macro_rules! test_position { ($to_move:ident, [ $($color:ident $shape:ident on $square:ident),* $(,)? ], $en_passant:ident) => { { - let pos = $crate::PositionBuilder::new() - $(.place_piece( - chessfriend_core::PlacedPiece::new( - chessfriend_core::Piece::new( - chessfriend_core::Color::$color, - chessfriend_core::Shape::$shape - ), - chessfriend_core::Square::$square - )) - )* - .to_move(chessfriend_core::Color::$to_move) - .en_passant(Some(chessfriend_moves::EnPassant::from_target_square(chessfriend_core::Square::$en_passant)).unwrap()) - .build(); - println!("{pos}"); - - pos + let board = chessfriend_board::test_board!($to_move, [ $($color $shape on $square),*], $en_passant); + $crate::Position::new(board) } }; ($to_move:ident, [ $($color:ident $shape:ident on $square:ident),* $(,)? ]) => { { - let pos = $crate::PositionBuilder::new() - $(.place_piece( - chessfriend_core::PlacedPiece::new( - chessfriend_core::Piece::new( - chessfriend_core::Color::$color, - chessfriend_core::Shape::$shape - ), - chessfriend_core::Square::$square - )) - )* - .to_move(chessfriend_core::Color::$to_move) - .build(); - println!("{pos}"); - - pos + let board = chessfriend_board::test_board!($to_move, [ $($color $shape on $square),* ]); + $crate::Position::new(board) } }; ($($color:ident $shape:ident on $square:ident),* $(,)?) => { { - let pos = $crate::PositionBuilder::new() - $(.place_piece( - chessfriend_core::PlacedPiece::new( - chessfriend_core::Piece::new( - chessfriend_core::Color::$color, - chessfriend_core::Shape::$shape - ), - chessfriend_core::Square::$square - )) - )* - .build(); - println!("{pos}"); - pos + let board = chessfriend_board::test_board!($($color $shape on $square),*); + $crate::Position::new(board) } }; (empty) => { - { - let pos = Position::empty(); - println!("{pos}"); - pos - } + Position::new(chessfriend_board::test_board!(empty)) }; (starting) => { - { - let pos = Position::starting(); - println!("{pos}"); - pos - } + Position::new(chessfriend_board::test_board!(starting)) }; } diff --git a/position/src/move_generator.rs b/position/src/move_generator.rs index c4271ce..2f23616 100644 --- a/position/src/move_generator.rs +++ b/position/src/move_generator.rs @@ -21,6 +21,7 @@ use self::{ }; use crate::Position; use chessfriend_bitboard::BitBoard; +use chessfriend_board::Board; use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; use chessfriend_moves::Move; use std::collections::BTreeMap; @@ -50,7 +51,7 @@ macro_rules! move_generator_declaration { ($name:ident, new) => { impl $name { pub(super) fn new( - position: &$crate::Position, + board: &chessfriend_board::Board, color: chessfriend_core::Color, capture_mask: chessfriend_bitboard::BitBoard, push_mask: chessfriend_bitboard::BitBoard, @@ -58,7 +59,7 @@ macro_rules! move_generator_declaration { let move_sets = if Self::shape() == chessfriend_core::Shape::King || !(capture_mask.is_empty() && push_mask.is_empty()) { - Self::move_sets(position, color, capture_mask, push_mask) + Self::move_sets(board, color, capture_mask, push_mask) } else { std::collections::BTreeMap::new() }; @@ -101,27 +102,26 @@ trait MoveGeneratorInternal { } fn move_sets( - position: &Position, + board: &Board, color: Color, capture_mask: BitBoard, push_mask: BitBoard, ) -> BTreeMap { let piece = Self::piece(color); BTreeMap::from_iter( - position + board .bitboard_for_piece(piece) .occupied_squares() .map(|square| { let piece = PlacedPiece::new(piece, square); - let move_set = - Self::move_set_for_piece(position, &piece, capture_mask, push_mask); + let move_set = Self::move_set_for_piece(board, &piece, capture_mask, push_mask); (square, move_set) }), ) } fn move_set_for_piece( - position: &Position, + board: &Board, piece: &PlacedPiece, capture_mask: BitBoard, push_mask: BitBoard, @@ -139,19 +139,14 @@ pub struct Moves { } impl Moves { - pub fn new( - position: &Position, - color: Color, - capture_mask: BitBoard, - push_mask: BitBoard, - ) -> Moves { + pub fn new(board: &Board, color: Color, capture_mask: BitBoard, push_mask: BitBoard) -> Moves { Moves { - pawn_moves: PawnMoveGenerator::new(position, color, capture_mask, push_mask), - knight_moves: KnightMoveGenerator::new(position, color, capture_mask, push_mask), - bishop_moves: BishopMoveGenerator::new(position, color, capture_mask, push_mask), - rook_moves: RookMoveGenerator::new(position, color, capture_mask, push_mask), - queen_moves: QueenMoveGenerator::new(position, color, capture_mask, push_mask), - king_moves: KingMoveGenerator::new(position, color, capture_mask, push_mask), + pawn_moves: PawnMoveGenerator::new(board, color, capture_mask, push_mask), + knight_moves: KnightMoveGenerator::new(board, color, capture_mask, push_mask), + bishop_moves: BishopMoveGenerator::new(board, color, capture_mask, push_mask), + rook_moves: RookMoveGenerator::new(board, color, capture_mask, push_mask), + queen_moves: QueenMoveGenerator::new(board, color, capture_mask, push_mask), + king_moves: KingMoveGenerator::new(board, color, capture_mask, push_mask), } } diff --git a/position/src/move_generator/bishop.rs b/position/src/move_generator/bishop.rs index a57b1a4..45d9086 100644 --- a/position/src/move_generator/bishop.rs +++ b/position/src/move_generator/bishop.rs @@ -1,8 +1,8 @@ // Eryn Wells use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use crate::Position; use chessfriend_bitboard::BitBoard; +use chessfriend_board::Board; use chessfriend_core::{Direction, PlacedPiece, Shape}; move_generator_declaration!(ClassicalMoveGenerator); @@ -13,16 +13,16 @@ impl MoveGeneratorInternal for ClassicalMoveGenerator { } fn move_set_for_piece( - position: &Position, + board: &Board, piece: &PlacedPiece, capture_mask: BitBoard, push_mask: BitBoard, ) -> MoveSet { let square = piece.square(); - let blockers = position.occupied_squares(); + let blockers = board.occupied_squares(); let empty_squares = !blockers; - let (friendly_pieces, opposing_pieces) = position.all_pieces(); + let (friendly_pieces, opposing_pieces) = board.all_pieces(); let mut all_moves = BitBoard::empty(); @@ -58,7 +58,7 @@ impl MoveGeneratorInternal for ClassicalMoveGenerator { #[cfg(test)] mod tests { use super::*; - use crate::{position, position::DiagramFormatter}; + use crate::position; use chessfriend_bitboard::BitBoard; use chessfriend_core::Color; @@ -69,7 +69,7 @@ mod tests { ]; let generator = - ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + ClassicalMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( generator._test_bitboard(), @@ -87,10 +87,10 @@ mod tests { White Knight on E5, ]; - println!("{}", DiagramFormatter::new(&pos)); + println!("{}", pos.display()); let generator = - ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + ClassicalMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( generator._test_bitboard(), @@ -109,7 +109,7 @@ mod tests { ]; let generator = - ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + ClassicalMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( generator._test_bitboard(), @@ -125,10 +125,10 @@ mod tests { White Bishop on E4, ]; - println!("{}", DiagramFormatter::new(&pos)); + println!("{}", pos.display()); let generator = - ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + ClassicalMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); let bitboard = generator._test_bitboard(); let expected = BitBoard::new( 0b00000001_10000010_01000100_00101000_00000000_00101000_01000100_10000010, diff --git a/position/src/move_generator/king.rs b/position/src/move_generator/king.rs index 3443d46..4daa155 100644 --- a/position/src/move_generator/king.rs +++ b/position/src/move_generator/king.rs @@ -4,10 +4,9 @@ //! generating the possible moves for the king in the given position. use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use crate::Position; use chessfriend_bitboard::BitBoard; +use chessfriend_board::{castle::Castle, Board}; use chessfriend_core::{PlacedPiece, Shape}; -use chessfriend_moves::Castle; move_generator_declaration!(KingMoveGenerator, struct); move_generator_declaration!(KingMoveGenerator, new); @@ -19,7 +18,7 @@ impl MoveGeneratorInternal for KingMoveGenerator { } fn move_set_for_piece( - position: &Position, + board: &Board, placed_piece: &PlacedPiece, _capture_mask: BitBoard, _push_mask: BitBoard, @@ -28,13 +27,13 @@ impl MoveGeneratorInternal for KingMoveGenerator { let color = piece.color(); let square = placed_piece.square(); - let safe_squares = !position.king_danger(color); + let safe_squares = BitBoard::FULL; let all_king_moves = BitBoard::king_moves(square); - let empty_squares = position.empty_squares(); + let empty_squares = board.empty_squares(); let safe_empty_squares = empty_squares & safe_squares; - let opposing_pieces = position.bitboard_for_color(color.other()); + let opposing_pieces = board.bitboard_for_color(color.other()); let opposing_pieces_on_safe_squares = opposing_pieces & safe_squares; let quiet_moves = all_king_moves & safe_empty_squares; @@ -44,10 +43,10 @@ impl MoveGeneratorInternal for KingMoveGenerator { .quiet_moves(quiet_moves) .capture_moves(capture_moves); - if position.player_can_castle(color, Castle::KingSide) { + if board.player_can_castle(color, Castle::KingSide) { move_set.kingside_castle(); } - if position.player_can_castle(color, Castle::QueenSide) { + if board.player_can_castle(color, Castle::QueenSide) { move_set.queenside_castle(); } @@ -58,21 +57,23 @@ impl MoveGeneratorInternal for KingMoveGenerator { #[cfg(test)] mod tests { use super::*; - use crate::{assert_move_list, position, test_position, testing::*, PositionBuilder}; + use crate::{assert_move_list, test_position, testing::*}; use chessfriend_bitboard::bitboard; + use chessfriend_board::castle::Castle; use chessfriend_core::{piece, Color, Square}; - use chessfriend_moves::{Builder as MoveBuilder, Castle, Move}; + use chessfriend_moves::{Builder as MoveBuilder, Move}; use std::collections::HashSet; #[test] fn one_king() -> TestResult { - let pos = position![White King on E4]; + let pos = test_position![White King on E4]; - let generator = KingMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + let generator = + KingMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( generator._test_bitboard(), - bitboard![E5, F5, F4, F3, E3, D3, D4, D5] + bitboard![E5 F5 F4 F3 E3 D3 D4 D5] ); let builder = MoveBuilder::push(&piece!(White King on E4)); @@ -96,15 +97,16 @@ mod tests { #[test] fn one_king_corner() -> TestResult { - let pos = position![White King on A1]; + let pos = test_position![White King on A1]; - let generator = KingMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + let generator = + KingMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); let generated_bitboard = generator._test_bitboard(); - let expected_bitboard = bitboard![A2, B2, B1]; + let expected_bitboard = bitboard![A2 B2 B1]; assert_eq!( generator._test_bitboard(), - bitboard![A2, B2, B1], + bitboard![A2 B2 B1], "Generated:\n{generated_bitboard}\nExpected:\n{expected_bitboard}" ); @@ -136,19 +138,19 @@ mod tests { #[test] fn black_king_in_check_by_rook() { - let pos = PositionBuilder::new() - .place_piece(piece!(White King on E1)) - .place_piece(piece!(White Rook on E4)) - .place_piece(piece!(Black King on E7)) - .to_move(Color::Black) - .build(); + let pos = test_position!(Black, [ + White King on E1, + White Rook on E4, + Black King on E7, + ]); assert!(pos.is_king_in_check()); - let generator = KingMoveGenerator::new(&pos, Color::Black, BitBoard::FULL, BitBoard::FULL); + let generator = + KingMoveGenerator::new(&pos.board, Color::Black, BitBoard::FULL, BitBoard::FULL); let generated_moves = generator._test_bitboard(); - let expected_moves = bitboard![F8, F7, F6, D6, D7, D8]; + let expected_moves = bitboard![F8 F7 F6 D6 D7 D8]; assert_eq!(generated_moves, expected_moves); } @@ -164,7 +166,8 @@ mod tests { assert!(pos.player_can_castle(Color::White, Castle::KingSide)); assert!(pos.player_can_castle(Color::White, Castle::QueenSide)); - let generator = KingMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + let generator = + KingMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); let generated_moves: HashSet = generator.iter().collect(); assert!(generated_moves @@ -187,7 +190,8 @@ mod tests { assert!(pos.player_can_castle(Color::White, Castle::KingSide)); assert!(!pos.player_can_castle(Color::White, Castle::QueenSide)); - let generator = KingMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + let generator = + KingMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); let generated_moves: HashSet = generator.iter().collect(); assert!(generated_moves @@ -210,7 +214,8 @@ mod tests { assert!(!pos.player_can_castle(Color::White, Castle::KingSide)); assert!(pos.player_can_castle(Color::White, Castle::QueenSide)); - let generator = KingMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + let generator = + KingMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); let generated_moves: HashSet = generator.iter().collect(); assert!(!generated_moves diff --git a/position/src/move_generator/knight.rs b/position/src/move_generator/knight.rs index 2b385ec..76afa44 100644 --- a/position/src/move_generator/knight.rs +++ b/position/src/move_generator/knight.rs @@ -1,8 +1,8 @@ // Eryn Wells use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use crate::Position; use chessfriend_bitboard::BitBoard; +use chessfriend_board::Board; use chessfriend_core::{PlacedPiece, Shape}; move_generator_declaration!(KnightMoveGenerator); @@ -13,13 +13,13 @@ impl MoveGeneratorInternal for KnightMoveGenerator { } fn move_set_for_piece( - position: &Position, + board: &Board, placed_piece: &PlacedPiece, capture_mask: BitBoard, push_mask: BitBoard, ) -> MoveSet { - let opposing_pieces = position.bitboard_for_color(placed_piece.piece().color().other()); - let empty_squares = position.empty_squares(); + let opposing_pieces = board.bitboard_for_color(placed_piece.piece().color().other()); + let empty_squares = board.empty_squares(); let knight_moves = BitBoard::knight_moves(placed_piece.square()); let quiet_moves = knight_moves & empty_squares & push_mask; @@ -46,7 +46,7 @@ mod tests { ]; let generator = - KnightMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + KnightMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); let generated_moves: HashSet<_> = generator.iter().collect(); let piece = piece!(White Knight on E4); diff --git a/position/src/move_generator/move_set.rs b/position/src/move_generator/move_set.rs index 61adb03..b4ae2d3 100644 --- a/position/src/move_generator/move_set.rs +++ b/position/src/move_generator/move_set.rs @@ -1,8 +1,9 @@ // Eryn Wells use chessfriend_bitboard::BitBoard; +use chessfriend_board::{castle::Castle, en_passant::EnPassant}; use chessfriend_core::{PlacedPiece, Square}; -use chessfriend_moves::{Builder as MoveBuilder, Castle, EnPassant, Move}; +use chessfriend_moves::{Builder as MoveBuilder, Move}; /// A set of bitboards defining the moves for a single piece on the board. #[derive(Clone, Debug, Default, Eq, PartialEq)] @@ -63,7 +64,7 @@ impl MoveSet { None => {} } - self.bitboard().is_set(target_square) + self.bitboard().contains(target_square) } pub(crate) fn can_castle(&self, castle: Castle) -> bool { diff --git a/position/src/move_generator/pawn.rs b/position/src/move_generator/pawn.rs index 1a07c4c..ddc1767 100644 --- a/position/src/move_generator/pawn.rs +++ b/position/src/move_generator/pawn.rs @@ -1,10 +1,10 @@ // Eryn Wells use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use crate::Position; use chessfriend_bitboard::BitBoard; +use chessfriend_board::{en_passant::EnPassant, Board}; use chessfriend_core::{Color, PlacedPiece, Rank, Shape, Square}; -use chessfriend_moves::{EnPassant, Move}; +use chessfriend_moves::Move; use std::collections::BTreeMap; #[derive(Debug)] @@ -25,21 +25,19 @@ impl MoveGeneratorInternal for PawnMoveGenerator { } fn move_set_for_piece( - position: &Position, + board: &Board, placed_piece: &PlacedPiece, capture_mask: BitBoard, push_mask: BitBoard, ) -> MoveSet { - let capture_moves = Self::attacks(position, &placed_piece) & capture_mask; - let quiet_moves = Self::pushes(position, &placed_piece) & push_mask; + let capture_moves = Self::attacks(board, &placed_piece) & capture_mask; + let quiet_moves = Self::pushes(board, &placed_piece) & push_mask; let mut move_set = MoveSet::new(*placed_piece) .quiet_moves(quiet_moves) .capture_moves(capture_moves); - if let Some(en_passant) = - Self::en_passant(position, placed_piece, &push_mask, &capture_mask) - { + if let Some(en_passant) = Self::en_passant(board, placed_piece, &push_mask, &capture_mask) { move_set.en_passant(en_passant); } @@ -49,13 +47,13 @@ impl MoveGeneratorInternal for PawnMoveGenerator { impl PawnMoveGenerator { pub(super) fn new( - position: &Position, + board: &Board, player_to_move: Color, capture_mask: BitBoard, push_mask: BitBoard, ) -> Self { let move_sets = if !(capture_mask.is_empty() && push_mask.is_empty()) { - Self::move_sets(position, player_to_move, capture_mask, push_mask) + Self::move_sets(board, player_to_move, capture_mask, push_mask) } else { std::collections::BTreeMap::new() }; @@ -68,32 +66,33 @@ impl PawnMoveGenerator { } fn move_sets( - position: &Position, + board: &Board, color: Color, capture_mask: BitBoard, push_mask: BitBoard, ) -> BTreeMap { let piece = Self::piece(color); - let moves_for_pieces = - BTreeMap::from_iter(position.bitboard_for_piece(piece).occupied_squares().map( - |square| { + let moves_for_pieces = BTreeMap::from_iter( + board + .bitboard_for_piece(piece) + .occupied_squares() + .map(|square| { let piece = PlacedPiece::new(piece, square); - let move_set = - Self::move_set_for_piece(position, &piece, capture_mask, push_mask); + let move_set = Self::move_set_for_piece(board, &piece, capture_mask, push_mask); (square, move_set) - }, - )); + }), + ); moves_for_pieces } - fn pushes(position: &Position, piece: &PlacedPiece) -> BitBoard { + fn pushes(board: &Board, piece: &PlacedPiece) -> BitBoard { let color = piece.color(); let square = piece.square(); let bitboard: BitBoard = square.into(); let starting_rank = Rank::PAWN_STARTING_RANKS[color as usize]; - let empty_squares = position.empty_squares(); + let empty_squares = board.empty_squares(); match color { Color::White => { @@ -115,21 +114,21 @@ impl PawnMoveGenerator { } } - fn attacks(position: &Position, piece: &PlacedPiece) -> BitBoard { + fn attacks(board: &Board, piece: &PlacedPiece) -> BitBoard { let color = piece.color(); - let opponent_pieces = position.bitboard_for_color(color.other()); + let opponent_pieces = board.bitboard_for_color(color.other()); BitBoard::pawn_attacks(piece.square(), color) & opponent_pieces } fn en_passant( - position: &Position, + board: &Board, piece: &PlacedPiece, push_mask: &BitBoard, capture_mask: &BitBoard, ) -> Option { - match position.en_passant() { + match board.en_passant() { Some(en_passant) => { let target_square: BitBoard = en_passant.target_square().into(); let capture_square: BitBoard = en_passant.capture_square().into(); @@ -147,7 +146,7 @@ impl PawnMoveGenerator { return None; } - match position.piece_on_square(en_passant.capture_square()) { + match board.piece_on_square(en_passant.capture_square()) { Some(_) => Some(en_passant), None => None, } @@ -173,10 +172,7 @@ impl PawnMoveGenerator { #[cfg(test)] mod tests { use super::*; - use crate::{ - assert_move_list, formatted_move_list, position::DiagramFormatter, test_position, - testing::*, - }; + use crate::{assert_move_list, formatted_move_list, test_position, testing::*}; use chessfriend_core::{piece, Color, Square}; use chessfriend_moves::{Builder as MoveBuilder, Move}; use std::collections::HashSet; @@ -185,7 +181,8 @@ mod tests { fn one_double_push() -> TestResult { let pos = test_position![White Pawn on E2]; - let generator = PawnMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + let generator = + PawnMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); let pawn = piece!(White Pawn on E2); let expected_moves = HashSet::from_iter([ @@ -204,7 +201,8 @@ mod tests { fn one_single_push() -> TestResult { let pos = test_position![White Pawn on E3]; - let generator = PawnMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + let generator = + PawnMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); let generated_moves: HashSet<_> = generator.iter().collect(); let expected_moves = HashSet::from_iter([MoveBuilder::push(&piece!(White Pawn on E3)) @@ -223,9 +221,8 @@ mod tests { White Knight on E4, ]; - println!("{}", DiagramFormatter::new(&pos)); - - let generator = PawnMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + let generator = + PawnMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); let expected_moves = HashSet::from_iter([MoveBuilder::push(&piece!(White Pawn on E2)) .to(Square::E3) @@ -245,7 +242,8 @@ mod tests { White Knight on E3, ]; - let generator = PawnMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + let generator = + PawnMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); let generated_moves: HashSet<_> = generator.iter().collect(); let expected_moves: HashSet<_> = HashSet::new(); @@ -261,7 +259,8 @@ mod tests { Black Knight on D5, ]; - let generator = PawnMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + let generator = + PawnMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); let expected_moves = HashSet::from_iter([MoveBuilder::push(&piece!(White Pawn on E4)) .capturing_on(Square::D5) @@ -283,7 +282,8 @@ mod tests { Black Queen on F5, ]; - let generator = PawnMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + let generator = + PawnMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); let builder = MoveBuilder::push(&piece!(White Pawn on E4)); let expected_moves = HashSet::from_iter([ @@ -309,7 +309,8 @@ mod tests { Black Pawn on E4, ], D3); - let generator = PawnMoveGenerator::new(&pos, Color::Black, BitBoard::FULL, BitBoard::FULL); + let generator = + PawnMoveGenerator::new(&pos.board, Color::Black, BitBoard::FULL, BitBoard::FULL); let generated_moves: HashSet = generator.iter().collect(); let builder = MoveBuilder::push(&piece!(Black Pawn on E4)); diff --git a/position/src/move_generator/queen.rs b/position/src/move_generator/queen.rs index e0817cb..b189805 100644 --- a/position/src/move_generator/queen.rs +++ b/position/src/move_generator/queen.rs @@ -1,8 +1,8 @@ // Eryn Wells use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use crate::Position; use chessfriend_bitboard::BitBoard; +use chessfriend_board::Board; use chessfriend_core::{Direction, PlacedPiece, Shape}; move_generator_declaration!(ClassicalMoveGenerator); @@ -13,7 +13,7 @@ impl MoveGeneratorInternal for ClassicalMoveGenerator { } fn move_set_for_piece( - position: &Position, + board: &Board, placed_piece: &PlacedPiece, capture_mask: BitBoard, push_mask: BitBoard, @@ -22,10 +22,10 @@ impl MoveGeneratorInternal for ClassicalMoveGenerator { let color = piece.color(); let square = placed_piece.square(); - let blockers = position.occupied_squares(); + let blockers = board.occupied_squares(); let empty_squares = !blockers; - let friendly_pieces = position.bitboard_for_color(color); - let opposing_pieces = position.bitboard_for_color(color.other()); + let friendly_pieces = board.bitboard_for_color(color); + let opposing_pieces = board.bitboard_for_color(color.other()); let mut all_moves = BitBoard::empty(); @@ -65,22 +65,22 @@ impl MoveGeneratorInternal for ClassicalMoveGenerator { #[cfg(test)] mod tests { use super::*; - use crate::{position, position::DiagramFormatter}; + use crate::test_position; use chessfriend_bitboard::{bitboard, BitBoard}; use chessfriend_core::Color; #[test] fn classical_single_queen_bitboard() { - let pos = position![White Queen on B2]; + let pos = test_position![White Queen on B2]; let generator = - ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + ClassicalMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); let bitboard = generator._test_bitboard(); let expected = bitboard![ - A2, C2, D2, E2, F2, G2, H2, // Rank - B1, B3, B4, B5, B6, B7, B8, // File - A1, C3, D4, E5, F6, G7, H8, // Diagonal - C1, A3 // Anti-diagonal + A2 C2 D2 E2 F2 G2 H2 // Rank + B1 B3 B4 B5 B6 B7 B8 // File + A1 C3 D4 E5 F6 G7 H8 // Diagonal + C1 A3 // Anti-diagonal ]; assert_eq!( @@ -93,15 +93,13 @@ mod tests { /// Test that a rook can move up to, but not onto, a friendly piece. #[test] fn classical_single_queen_with_same_color_blocker_bitboard() { - let pos = position![ + let pos = test_position![ White Queen on A1, White Knight on E1, ]; - println!("{}", DiagramFormatter::new(&pos)); - let generator = - ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + ClassicalMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); let bitboard = generator._test_bitboard(); let expected = BitBoard::new( 0b10000001_01000001_00100001_00010001_00001001_00000101_00000011_00001110, @@ -117,41 +115,39 @@ mod tests { /// Test that a rook can move up to, and then capture, an enemy piece. #[test] fn classical_single_queen_with_opposing_color_blocker_bitboard() { - let pos = position![ + let pos = test_position![ White Queen on B2, Black Knight on E5, ]; - println!("{}", DiagramFormatter::new(&pos)); let generator = - ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + ClassicalMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( generator._test_bitboard(), bitboard![ - A2, C2, D2, E2, F2, G2, H2, // Rank - B1, B3, B4, B5, B6, B7, B8, // File - A1, C3, D4, E5, // Diagonal - C1, A3 // Anti-diagonal + A2 C2 D2 E2 F2 G2 H2 // Rank + B1 B3 B4 B5 B6 B7 B8 // File + A1 C3 D4 E5 // Diagonal + C1 A3 // Anti-diagonal ] ); } #[test] fn classical_single_queen_in_center() { - let pos = position![White Queen on D3]; - println!("{}", DiagramFormatter::new(&pos)); + let pos = test_position![White Queen on D3]; let generator = - ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + ClassicalMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( generator._test_bitboard(), bitboard![ - A3, B3, C3, E3, F3, G3, H3, // Rank - D1, D2, D4, D5, D6, D7, D8, // File - B1, C2, E4, F5, G6, H7, // Diagonal - F1, E2, C4, B5, A6, // Anti-diagonal + A3 B3 C3 E3 F3 G3 H3 // Rank + D1 D2 D4 D5 D6 D7 D8 // File + B1 C2 E4 F5 G6 H7 // Diagonal + F1 E2 C4 B5 A6 // Anti-diagonal ] ); } diff --git a/position/src/move_generator/rook.rs b/position/src/move_generator/rook.rs index f692da5..e01f733 100644 --- a/position/src/move_generator/rook.rs +++ b/position/src/move_generator/rook.rs @@ -1,8 +1,8 @@ // Eryn Wells use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use crate::Position; use chessfriend_bitboard::BitBoard; +use chessfriend_board::Board; use chessfriend_core::{Direction, PlacedPiece, Shape}; move_generator_declaration!(ClassicalMoveGenerator); @@ -13,7 +13,7 @@ impl MoveGeneratorInternal for ClassicalMoveGenerator { } fn move_set_for_piece( - position: &Position, + board: &Board, placed_piece: &PlacedPiece, capture_mask: BitBoard, push_mask: BitBoard, @@ -22,10 +22,10 @@ impl MoveGeneratorInternal for ClassicalMoveGenerator { let color = piece.color(); let square = placed_piece.square(); - let blockers = position.occupied_squares(); + let blockers = board.occupied_squares(); let empty_squares = !blockers; - let friendly_pieces = position.bitboard_for_color(color); - let opposing_pieces = position.bitboard_for_color(color.other()); + let friendly_pieces = board.bitboard_for_color(color); + let opposing_pieces = board.bitboard_for_color(color.other()); let mut all_moves = BitBoard::empty(); @@ -61,7 +61,7 @@ impl MoveGeneratorInternal for ClassicalMoveGenerator { #[cfg(test)] mod tests { use super::*; - use crate::{position::DiagramFormatter, test_position}; + use crate::test_position; use chessfriend_bitboard::{bitboard, BitBoard}; use chessfriend_core::Color; @@ -70,11 +70,11 @@ mod tests { let pos = test_position![White Rook on A2]; let generator = - ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + ClassicalMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( generator._test_bitboard(), - bitboard![A1, A3, A4, A5, A6, A7, A8, B2, C2, D2, E2, F2, G2, H2] + bitboard![A1 A3 A4 A5 A6 A7 A8 B2 C2 D2 E2 F2 G2 H2] ); } @@ -86,10 +86,10 @@ mod tests { White Knight on E1, ]; - println!("{}", DiagramFormatter::new(&pos)); + println!("{}", pos.display()); let generator = - ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + ClassicalMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( generator._test_bitboard(), @@ -108,11 +108,11 @@ mod tests { ]; let generator = - ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + ClassicalMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( generator._test_bitboard(), - bitboard![A2, A3, A4, A5, A6, A7, A8, B1, C1, D1, E1] + bitboard![A2 A3 A4 A5 A6 A7 A8 B1 C1 D1 E1] ); } @@ -121,11 +121,11 @@ mod tests { let pos = test_position![White Rook on D4]; let generator = - ClassicalMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + ClassicalMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); assert_eq!( generator._test_bitboard(), - bitboard![A4, B4, C4, E4, F4, G4, H4, D1, D2, D3, D5, D6, D7, D8] + bitboard![A4 B4 C4 E4 F4 G4 H4 D1 D2 D3 D5 D6 D7 D8] ); } } diff --git a/position/src/position/builders/mod.rs b/position/src/position/builders/mod.rs index ea1f60d..67ee23e 100644 --- a/position/src/position/builders/mod.rs +++ b/position/src/position/builders/mod.rs @@ -1,7 +1,5 @@ // Eryn Wells mod move_builder; -mod position_builder; pub use move_builder::{Builder as MoveBuilder, MakeMoveError}; -pub use position_builder::Builder as PositionBuilder; diff --git a/position/src/position/builders/move_builder.rs b/position/src/position/builders/move_builder.rs index 307bae6..9cf0806 100644 --- a/position/src/position/builders/move_builder.rs +++ b/position/src/position/builders/move_builder.rs @@ -1,9 +1,10 @@ // Eryn Wells -use crate::{position::flags::Flags, Position}; +use crate::Position; use chessfriend_bitboard::BitBoard; +use chessfriend_board::{castle, castle::Castle, en_passant::EnPassant}; use chessfriend_core::{Color, Direction, Piece, PlacedPiece, Shape, Square}; -use chessfriend_moves::{Castle, EnPassant, Move}; +use chessfriend_moves::Move; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum MakeMoveError { @@ -33,15 +34,15 @@ pub enum ValidatedMove { moving_piece: PlacedPiece, captured_piece: Option, promotion: Option, - flags: Flags, + castling_rights: castle::Rights, en_passant: Option, - increment_ply: bool, + should_increment_ply: bool, }, Castle { castle: Castle, king: PlacedPiece, rook: PlacedPiece, - flags: Flags, + castling_rights: castle::Rights, }, } @@ -49,7 +50,7 @@ impl MoveToMake for NoMove {} impl MoveToMake for ValidatedMove {} impl<'p> Builder<'p, NoMove> { - pub fn new(position: &'p Position) -> Builder<'p, NoMove> { + pub fn new(position: &'p Position) -> Self { Builder { position, move_to_make: NoMove, @@ -66,6 +67,7 @@ where let piece = self .position + .board .piece_on_square(origin_square) .ok_or(MakeMoveError::NoPiece)?; @@ -101,12 +103,14 @@ where Some( self.position + .board .piece_on_square(capture_square) .ok_or(MakeMoveError::NoCapturedPiece)?, ) } else if mv.is_capture() { Some( self.position + .board .piece_on_square(target_square) .ok_or(MakeMoveError::NoCapturedPiece)?, ) @@ -117,15 +121,15 @@ where // TODO: Check whether the move is legal. let piece_is_king = piece.is_king(); - let mut flags = self.position.flags().clone(); + let mut castling_rights = self.position.board.castling_rights; if piece_is_king { - flags.clear_player_has_right_to_castle_flag(player, Castle::KingSide); - flags.clear_player_has_right_to_castle_flag(player, Castle::QueenSide); + castling_rights.clear_player_has_right_to_castle_flag(player, Castle::KingSide); + castling_rights.clear_player_has_right_to_castle_flag(player, Castle::QueenSide); } else if piece.is_kingside_rook() { - flags.clear_player_has_right_to_castle_flag(player, Castle::KingSide); + castling_rights.clear_player_has_right_to_castle_flag(player, Castle::KingSide); } else if piece.is_queenside_rook() { - flags.clear_player_has_right_to_castle_flag(player, Castle::QueenSide); + castling_rights.clear_player_has_right_to_castle_flag(player, Castle::QueenSide); } if let Some(castle) = mv.castle() { @@ -145,7 +149,7 @@ where castle, king: piece, rook, - flags, + castling_rights, }, }) } else { @@ -167,9 +171,9 @@ where moving_piece: piece, captured_piece, promotion: mv.promotion(), - flags, + castling_rights, en_passant, - increment_ply: !(mv.is_capture() || piece.is_pawn()), + should_increment_ply: !(mv.is_capture() || piece.is_pawn()), }, }) } @@ -180,8 +184,8 @@ impl<'p> Builder<'p, ValidatedMove> { pub fn build(&self) -> Position { let player = self.position.player_to_move(); - let updated_move_number = - self.position.move_number() + if player == Color::Black { 1 } else { 0 }; + let updated_move_number = self.position.board.move_counter.fullmove_number + + if player == Color::Black { 1 } else { 0 }; match self.move_to_make { ValidatedMove::RegularMove { @@ -190,69 +194,57 @@ impl<'p> Builder<'p, ValidatedMove> { moving_piece, captured_piece, promotion, - flags, + castling_rights, en_passant, - increment_ply, + should_increment_ply: increment_ply, } => { - let mut pieces = self.position.piece_bitboards().clone(); + let mut board = self.position.board.clone(); + + board.castling_rights = castling_rights; + board.en_passant = en_passant; if let Some(captured_piece) = captured_piece { - pieces.remove_piece(&captured_piece); + board.remove_piece_from_square(captured_piece.square()); } if let Some(promotion) = promotion { - pieces.remove_piece(&moving_piece); - let _ = pieces - .place_piece(&PlacedPiece::new(Piece::new(player, promotion), to_square)); + board.remove_piece_from_square(moving_piece.square()); + board.place_piece_on_square(Piece::new(player, promotion), to_square); } else { - pieces.move_piece(moving_piece.piece(), from_square, to_square); + board.remove_piece_from_square(from_square); + board.place_piece_on_square(moving_piece.piece(), to_square); } let ply = if increment_ply { - self.position.ply_counter() + 1 + self.position.board.move_counter.halfmove_number + 1 } else { 0 }; - Position::new( - self.position.player_to_move().other(), - flags, - pieces, - en_passant, - ply, - updated_move_number, - ) + Position::new(board) } ValidatedMove::Castle { castle, king, rook, - flags, + castling_rights, } => { - let mut pieces = self.position.piece_bitboards().clone(); + let mut board = self.position.board.clone(); + + board.castling_rights = castling_rights; + + let next_active_color = board.move_counter.active_color.next(); + board.move_counter.active_color = next_active_color; let parameters = castle.parameters(player); - let king_origin_square: BitBoard = king.square().into(); - let king_target_square: BitBoard = parameters.king_target_square().into(); - *pieces.bitboard_for_piece_mut(king.piece()) ^= - king_origin_square | king_target_square; + board.remove_piece_from_square(king.square()); + board.place_piece_on_square(king.piece(), parameters.king_target_square()); - let rook_from: BitBoard = rook.square().into(); - let rook_to: BitBoard = parameters.rook_target_square().into(); - *pieces.bitboard_for_piece_mut(rook.piece()) ^= rook_from | rook_to; + board.remove_piece_from_square(rook.square()); + board.place_piece_on_square(rook.piece(), parameters.rook_target_square()); - *pieces.bitboard_for_color_mut(player) &= - !(king_origin_square | rook_from) | (king_target_square | rook_to); - - Position::new( - player.other(), - flags, - pieces, - None, - self.position.ply_counter() + 1, - updated_move_number, - ) + Position::new(board) } } } @@ -291,7 +283,7 @@ mod tests { println!("{}", &new_position); assert_eq!( - new_position.piece_on_square(Square::E3), + new_position.board.piece_on_square(Square::E3), Some(piece!(White Pawn on E3)) ); @@ -308,16 +300,13 @@ mod tests { println!("{}", &new_position); assert_eq!( - new_position.piece_on_square(Square::E4), + new_position.board.piece_on_square(Square::E4), Some(piece!(White Pawn on E4)) ); let en_passant = new_position.en_passant(); assert!(en_passant.is_some()); - assert_eq!( - en_passant.as_ref().map(EnPassant::target_square), - Some(Square::E3) - ); + assert_eq!(en_passant.map(EnPassant::target_square), Some(Square::E3)); Ok(()) } @@ -339,11 +328,11 @@ mod tests { println!("{}", &new_position); assert_eq!( - new_position.piece_on_square(Square::G1), + new_position.board.piece_on_square(Square::G1), Some(piece!(White King on G1)) ); assert_eq!( - new_position.piece_on_square(Square::F1), + new_position.board.piece_on_square(Square::F1), Some(piece!(White Rook on F1)) ); @@ -366,11 +355,11 @@ mod tests { println!("{en_passant_position}"); assert_eq!( - en_passant_position.piece_on_square(Square::A5), + en_passant_position.board.piece_on_square(Square::A5), Some(piece!(Black Pawn on A5)) ); assert_eq!( - en_passant_position.piece_on_square(Square::B5), + en_passant_position.board.piece_on_square(Square::B5), Some(piece!(White Pawn on B5)) ); @@ -382,10 +371,10 @@ mod tests { .build(); println!("{en_passant_capture}"); - assert_eq!(en_passant_capture.piece_on_square(Square::A5), None); - assert_eq!(en_passant_capture.piece_on_square(Square::B5), None); + assert_eq!(en_passant_capture.board.piece_on_square(Square::A5), None); + assert_eq!(en_passant_capture.board.piece_on_square(Square::B5), None); assert_eq!( - en_passant_capture.piece_on_square(Square::A6), + en_passant_capture.board.piece_on_square(Square::A6), Some(piece!(White Pawn on A6)) ); diff --git a/position/src/position/builders/position_builder.rs b/position/src/position/builders/position_builder.rs deleted file mode 100644 index 08fc193..0000000 --- a/position/src/position/builders/position_builder.rs +++ /dev/null @@ -1,194 +0,0 @@ -// Eryn Wells - -use crate::{ - position::{flags::Flags, piece_sets::PieceBitBoards}, - Position, -}; -use chessfriend_core::{piece, Color, Piece, PlacedPiece, Rank, Shape, Square}; -use chessfriend_moves::{Castle, EnPassant}; -use std::collections::BTreeMap; - -#[derive(Clone)] -pub struct Builder { - player_to_move: Color, - flags: Flags, - pieces: BTreeMap, - kings: [Option; 2], - en_passant: Option, - ply_counter: u16, - move_number: u16, -} - -impl Builder { - pub fn new() -> Self { - Self::default() - } - - pub(crate) fn empty() -> Self { - Self { - player_to_move: Color::default(), - flags: Flags::default(), - pieces: BTreeMap::default(), - kings: [None, None], - en_passant: None, - ply_counter: 0, - move_number: 1, - } - } - - pub fn from_position(position: &Position) -> Self { - let pieces = BTreeMap::from_iter( - position - .pieces(Color::White) - .chain(position.pieces(Color::Black)) - .map(|placed_piece| (placed_piece.square(), *placed_piece.piece())), - ); - - let white_king = position.king_square(Color::White); - let black_king = position.king_square(Color::Black); - - Self { - player_to_move: position.player_to_move(), - flags: position.flags(), - pieces, - kings: [Some(white_king), Some(black_king)], - en_passant: position.en_passant(), - ply_counter: position.ply_counter(), - move_number: position.move_number(), - } - } - - pub fn to_move(&mut self, player: Color) -> &mut Self { - self.player_to_move = player; - self - } - - pub fn ply_counter(&mut self, num: u16) -> &mut Self { - self.ply_counter = num; - self - } - - pub fn move_number(&mut self, num: u16) -> &mut Self { - self.move_number = num; - self - } - - pub fn en_passant(&mut self, en_passant: Option) -> &mut Self { - self.en_passant = en_passant; - self - } - - pub fn place_piece(&mut self, piece: PlacedPiece) -> &mut Self { - let square = piece.square(); - let shape = piece.shape(); - - if shape == Shape::King { - let color = piece.color(); - let color_index: usize = color as usize; - - if let Some(king_square) = self.kings[color_index] { - self.pieces.remove(&king_square); - } - self.kings[color_index] = Some(square); - } - - self.pieces.insert(square, *piece.piece()); - - self - } - - pub fn player_can_castle(&mut self, color: Color, castle: Castle) -> &mut Self { - self.flags - .set_player_has_right_to_castle_flag(color, castle); - self - } - - pub fn no_castling_rights(&mut self) -> &mut Self { - self.flags.clear_all_castling_rights(); - self - } - - pub fn build(&self) -> Position { - let pieces = PieceBitBoards::from_iter( - self.pieces - .iter() - .map(PlacedPiece::from) - .filter(Self::is_piece_placement_valid), - ); - - let mut flags = self.flags; - - for color in Color::ALL { - for castle in Castle::ALL { - let parameters = castle.parameters(color); - let has_rook_on_starting_square = self - .pieces - .get(¶meters.rook_origin_square()) - .is_some_and(|piece| piece.shape() == Shape::Rook); - let king_is_on_starting_square = - self.kings[color as usize] == Some(parameters.king_origin_square()); - - if !king_is_on_starting_square || !has_rook_on_starting_square { - flags.clear_player_has_right_to_castle_flag(color, castle); - } - } - } - - Position::new( - self.player_to_move, - flags, - pieces, - self.en_passant, - self.ply_counter, - self.move_number, - ) - } -} - -impl Builder { - fn is_piece_placement_valid(piece: &PlacedPiece) -> bool { - if piece.shape() == Shape::Pawn { - // Pawns cannot be placed on the first (back) rank of their side, - // and cannot be placed on the final rank without a promotion. - let rank = piece.square().rank(); - return rank != Rank::ONE && rank != Rank::EIGHT; - } - - true - } -} - -impl Default for Builder { - fn default() -> Self { - let white_king_square = Square::E1; - let black_king_square = Square::E8; - - let pieces = BTreeMap::from_iter([ - (white_king_square, piece!(White King)), - (black_king_square, piece!(Black King)), - ]); - - Self { - player_to_move: Color::White, - flags: Flags::default(), - pieces: pieces, - kings: [Some(white_king_square), Some(black_king_square)], - en_passant: None, - ply_counter: 0, - move_number: 1, - } - } -} - -#[cfg(test)] -mod tests { - use crate::PositionBuilder; - use chessfriend_core::piece; - - #[test] - fn place_piece() { - let piece = piece!(White Queen on E4); - let builder = PositionBuilder::new().place_piece(piece).build(); - assert_eq!(builder.piece_on_square(piece.square()), Some(piece)); - } -} diff --git a/position/src/position/diagram_formatter.rs b/position/src/position/diagram_formatter.rs deleted file mode 100644 index 64eb507..0000000 --- a/position/src/position/diagram_formatter.rs +++ /dev/null @@ -1,68 +0,0 @@ -// Eryn Wells - -use crate::Position; -use chessfriend_core::{File, Rank, Square}; -use std::fmt; - -pub struct DiagramFormatter<'a>(&'a Position); - -impl<'a> DiagramFormatter<'a> { - pub fn new(position: &'a Position) -> DiagramFormatter { - DiagramFormatter(position) - } -} - -impl<'a> fmt::Display for DiagramFormatter<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, " ╔═════════════════╗\n")?; - - for rank in Rank::ALL.iter().rev() { - write!(f, "{rank} ║ ")?; - - for file in File::ALL.iter() { - let square = Square::from_file_rank(*file, *rank); - match self.0.piece_on_square(square) { - Some(placed_piece) => write!(f, "{} ", placed_piece.piece())?, - None => write!(f, "· ")?, - } - } - - write!(f, "║\n")?; - } - - write!(f, " ╚═════════════════╝\n")?; - write!(f, " a b c d e f g h\n")?; - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{position, Position}; - - #[test] - #[ignore] - fn empty() { - let pos = Position::empty(); - let diagram = DiagramFormatter(&pos); - println!("{}", diagram); - } - - #[test] - #[ignore] - fn one_king() { - let pos = position![Black King on H3]; - let diagram = DiagramFormatter(&pos); - println!("{}", diagram); - } - - #[test] - #[ignore] - fn starting() { - let pos = Position::starting(); - let diagram = DiagramFormatter(&pos); - println!("{}", diagram); - } -} diff --git a/position/src/position/flags.rs b/position/src/position/flags.rs deleted file mode 100644 index 19d3165..0000000 --- a/position/src/position/flags.rs +++ /dev/null @@ -1,89 +0,0 @@ -// Eryn Wells - -use chessfriend_core::Color; -use chessfriend_moves::Castle; -use std::fmt; - -#[derive(Clone, Copy, Eq, Hash, PartialEq)] -pub struct Flags(u8); - -impl Flags { - #[inline] - pub(super) fn player_has_right_to_castle_flag_offset(color: Color, castle: Castle) -> usize { - ((color as usize) << 1) + castle as usize - } - - pub(super) fn player_has_right_to_castle(&self, color: Color, castle: Castle) -> bool { - (self.0 & (1 << Self::player_has_right_to_castle_flag_offset(color, castle))) != 0 - } - - pub(super) fn set_player_has_right_to_castle_flag(&mut self, color: Color, castle: Castle) { - self.0 |= 1 << Self::player_has_right_to_castle_flag_offset(color, castle); - } - - pub(super) fn clear_player_has_right_to_castle_flag(&mut self, color: Color, castle: Castle) { - self.0 &= !(1 << Self::player_has_right_to_castle_flag_offset(color, castle)); - } - - pub(super) fn clear_all_castling_rights(&mut self) { - self.0 &= 0b11111100; - } -} - -impl fmt::Debug for Flags { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Flags({:08b})", self.0) - } -} - -impl Default for Flags { - fn default() -> Self { - Flags(0b00001111) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn castle_flags() { - assert_eq!( - Flags::player_has_right_to_castle_flag_offset(Color::White, Castle::KingSide), - 0 - ); - assert_eq!( - Flags::player_has_right_to_castle_flag_offset(Color::White, Castle::QueenSide), - 1 - ); - assert_eq!( - Flags::player_has_right_to_castle_flag_offset(Color::Black, Castle::KingSide), - 2 - ); - assert_eq!( - Flags::player_has_right_to_castle_flag_offset(Color::Black, Castle::QueenSide), - 3 - ); - } - - #[test] - fn defaults() { - let mut flags: Flags = Default::default(); - assert!(flags.player_has_right_to_castle(Color::White, Castle::KingSide)); - assert!(flags.player_has_right_to_castle(Color::White, Castle::QueenSide)); - assert!(flags.player_has_right_to_castle(Color::Black, Castle::KingSide)); - assert!(flags.player_has_right_to_castle(Color::Black, Castle::QueenSide)); - - flags.clear_player_has_right_to_castle_flag(Color::White, Castle::QueenSide); - assert!(flags.player_has_right_to_castle(Color::White, Castle::KingSide)); - assert!(!flags.player_has_right_to_castle(Color::White, Castle::QueenSide)); - assert!(flags.player_has_right_to_castle(Color::Black, Castle::KingSide)); - assert!(flags.player_has_right_to_castle(Color::Black, Castle::QueenSide)); - - flags.set_player_has_right_to_castle_flag(Color::White, Castle::QueenSide); - assert!(flags.player_has_right_to_castle(Color::White, Castle::KingSide)); - assert!(flags.player_has_right_to_castle(Color::White, Castle::QueenSide)); - assert!(flags.player_has_right_to_castle(Color::Black, Castle::KingSide)); - assert!(flags.player_has_right_to_castle(Color::Black, Castle::QueenSide)); - } -} diff --git a/position/src/position/mod.rs b/position/src/position/mod.rs index 644ccc1..0782305 100644 --- a/position/src/position/mod.rs +++ b/position/src/position/mod.rs @@ -1,16 +1,9 @@ // Eryn Wells -pub mod piece_sets; - mod builders; -mod diagram_formatter; -mod flags; -mod pieces; mod position; pub use { - builders::{MakeMoveError, MoveBuilder, PositionBuilder}, - diagram_formatter::DiagramFormatter, - pieces::Pieces, + builders::{MakeMoveError, MoveBuilder}, position::Position, }; diff --git a/position/src/position/piece_sets.rs b/position/src/position/piece_sets.rs deleted file mode 100644 index fee1a3f..0000000 --- a/position/src/position/piece_sets.rs +++ /dev/null @@ -1,174 +0,0 @@ -// Eryn Wells - -use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece, PlacedPiece, Square}; - -#[derive(Debug, Eq, PartialEq)] -pub enum PlacePieceStrategy { - Replace, - PreserveExisting, -} - -#[derive(Debug, Eq, PartialEq)] -pub enum PlacePieceError { - ExisitingPiece, -} - -impl Default for PlacePieceStrategy { - fn default() -> Self { - Self::Replace - } -} - -#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] -pub(crate) struct PieceBitBoards { - by_color: ByColor, - by_color_and_shape: ByColorAndShape, -} - -#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] -struct ByColor(BitBoard, [BitBoard; 2]); - -#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] -struct ByColorAndShape([[BitBoard; 6]; 2]); - -impl PieceBitBoards { - pub(super) fn new(pieces: [[BitBoard; 6]; 2]) -> Self { - use std::ops::BitOr; - - let white_pieces = pieces[Color::White as usize] - .iter() - .fold(BitBoard::empty(), BitOr::bitor); - let black_pieces = pieces[Color::Black as usize] - .iter() - .fold(BitBoard::empty(), BitOr::bitor); - - let all_pieces = white_pieces | black_pieces; - - Self { - by_color: ByColor(all_pieces, [white_pieces, black_pieces]), - by_color_and_shape: ByColorAndShape(pieces), - } - } - - /// A BitBoard representing all the pieces currently on the board. Other - /// engines might refer to this concept as 'occupancy'. - pub(crate) fn all_pieces(&self) -> &BitBoard { - self.by_color.all() - } - - pub(crate) fn all_pieces_of_color(&self, color: Color) -> &BitBoard { - self.by_color.bitboard(color) - } - - pub(super) fn bitboard_for_color(&self, color: Color) -> &BitBoard { - self.by_color.bitboard(color) - } - - pub(super) fn bitboard_for_color_mut(&mut self, color: Color) -> &mut BitBoard { - self.by_color.bitboard_mut(color) - } - - pub(super) fn bitboard_for_piece(&self, piece: &Piece) -> &BitBoard { - self.by_color_and_shape.bitboard_for_piece(piece) - } - - pub(super) fn bitboard_for_piece_mut(&mut self, piece: &Piece) -> &mut BitBoard { - self.by_color_and_shape.bitboard_for_piece_mut(piece) - } - - pub(super) fn place_piece(&mut self, piece: &PlacedPiece) -> Result<(), PlacePieceError> { - self.place_piece_with_strategy(piece, PlacePieceStrategy::default()) - } - - pub(super) fn place_piece_with_strategy( - &mut self, - piece: &PlacedPiece, - strategy: PlacePieceStrategy, - ) -> Result<(), PlacePieceError> { - let color = piece.color(); - let square = piece.square(); - - if strategy == PlacePieceStrategy::PreserveExisting - && self.by_color.bitboard(color).is_set(piece.square()) - { - return Err(PlacePieceError::ExisitingPiece); - } - - self.by_color_and_shape.set_square(square, piece.piece()); - self.by_color.set_square(square, color); - - Ok(()) - } - - pub(super) fn remove_piece(&mut self, piece: &PlacedPiece) { - let color = piece.color(); - let square = piece.square(); - - self.by_color_and_shape.clear_square(square, piece.piece()); - self.by_color.clear_square(square, color); - } - - pub(super) fn move_piece(&mut self, piece: &Piece, from_square: Square, to_square: Square) { - let color = piece.color(); - - self.by_color_and_shape.clear_square(from_square, piece); - self.by_color.clear_square(from_square, color); - self.by_color_and_shape.set_square(to_square, piece); - self.by_color.set_square(to_square, color); - } -} - -impl FromIterator for PieceBitBoards { - fn from_iter>(iter: T) -> Self { - let mut pieces: Self = Default::default(); - - for piece in iter { - let _ = pieces.place_piece(&piece); - } - - pieces - } -} - -impl ByColor { - fn all(&self) -> &BitBoard { - &self.0 - } - - pub(super) fn bitboard(&self, color: Color) -> &BitBoard { - &self.1[color as usize] - } - - pub(super) fn bitboard_mut(&mut self, color: Color) -> &mut BitBoard { - &mut self.1[color as usize] - } - - fn set_square(&mut self, square: Square, color: Color) { - self.0.set_square(square); - self.1[color as usize].set_square(square) - } - - fn clear_square(&mut self, square: Square, color: Color) { - self.0.clear_square(square); - self.1[color as usize].clear_square(square); - } -} - -impl ByColorAndShape { - fn bitboard_for_piece(&self, piece: &Piece) -> &BitBoard { - &self.0[piece.color() as usize][piece.shape() as usize] - } - - fn bitboard_for_piece_mut(&mut self, piece: &Piece) -> &mut BitBoard { - &mut self.0[piece.color() as usize][piece.shape() as usize] - } - - fn set_square(&mut self, square: Square, piece: &Piece) { - self.bitboard_for_piece_mut(piece).set_square(square); - } - - fn clear_square(&mut self, square: Square, piece: &Piece) { - self.bitboard_for_piece_mut(piece).clear_square(square); - } -} diff --git a/position/src/position/pieces.rs b/position/src/position/pieces.rs deleted file mode 100644 index 655aca2..0000000 --- a/position/src/position/pieces.rs +++ /dev/null @@ -1,130 +0,0 @@ -// Eryn Wells - -use super::Position; -use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; - -pub struct Pieces<'a> { - color: Color, - position: &'a Position, - - current_shape: Option, - - shape_iterator: Box>, - square_iterator: Option>>, -} - -impl<'a> Pieces<'a> { - pub(crate) fn new(position: &Position, color: Color) -> Pieces { - Pieces { - color, - position, - current_shape: None, - shape_iterator: Box::new(Shape::iter()), - square_iterator: None, - } - } -} - -impl<'a> Iterator for Pieces<'a> { - type Item = PlacedPiece; - - fn next(&mut self) -> Option { - if let Some(square_iterator) = &mut self.square_iterator { - if let (Some(square), Some(shape)) = (square_iterator.next(), self.current_shape) { - return Some(PlacedPiece::new(Piece::new(self.color, shape), square)); - } - } - - let mut current_shape: Option = None; - let mut next_nonempty_bitboard: Option<&BitBoard> = None; - - while let Some(shape) = self.shape_iterator.next() { - let piece = Piece::new(self.color, *shape); - - let bitboard = self.position.bitboard_for_piece(piece); - if bitboard.is_empty() { - continue; - } - - next_nonempty_bitboard = Some(bitboard); - current_shape = Some(*shape); - - break; - } - - if let (Some(bitboard), Some(shape)) = (next_nonempty_bitboard, current_shape) { - let mut square_iterator = bitboard.occupied_squares(); - - let mut next_placed_piece: Option = None; - if let Some(square) = square_iterator.next() { - next_placed_piece = Some(PlacedPiece::new(Piece::new(self.color, shape), square)); - } - - self.square_iterator = Some(Box::new(square_iterator)); - self.current_shape = Some(shape); - - return next_placed_piece; - } - - None - } -} - -#[cfg(test)] -mod tests { - use crate::{Position, PositionBuilder}; - use chessfriend_core::{piece, Color}; - use std::collections::HashSet; - - #[test] - fn empty() { - let pos = Position::empty(); - let mut pieces = pos.pieces(Color::White); - assert_eq!(pieces.next(), None); - } - - #[test] - fn one() { - let pos = PositionBuilder::new() - .place_piece(piece!(White Queen on E4)) - .build(); - println!("{:#?}", &pos); - - let mut pieces = pos.pieces(Color::White); - assert_eq!(pieces.next(), Some(piece!(White Queen on E4))); - assert_eq!(pieces.next(), Some(piece!(White King on E1))); - assert_eq!(pieces.next(), None); - } - - #[test] - fn multiple_pieces() { - let pos = PositionBuilder::new() - .place_piece(piece!(White Queen on E4)) - .place_piece(piece!(White King on A1)) - .place_piece(piece!(White Pawn on B2)) - .place_piece(piece!(White Pawn on C2)) - .build(); - println!("{}", crate::position::DiagramFormatter::new(&pos)); - - let expected_placed_pieces = HashSet::from([ - piece!(White Queen on E4), - piece!(White King on A1), - piece!(White Pawn on B2), - piece!(White Pawn on C2), - ]); - - let placed_pieces = HashSet::from_iter(pos.pieces(Color::White)); - - assert_eq!( - placed_pieces, - expected_placed_pieces, - "{:#?}", - placed_pieces - .symmetric_difference(&expected_placed_pieces) - .into_iter() - .map(|pp| format!("{}", pp)) - .collect::>() - ); - } -} diff --git a/position/src/position/position.rs b/position/src/position/position.rs index f7e05d7..8c34449 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -1,112 +1,45 @@ // Eryn Wells -use super::{flags::Flags, piece_sets::PieceBitBoards, Pieces}; use crate::{ check::CheckingPieces, move_generator::{MoveSet, Moves}, - position::DiagramFormatter, sight::SightExt, }; use chessfriend_bitboard::BitBoard; +use chessfriend_board::{castle::Castle, display::DiagramFormatter, en_passant::EnPassant, Board}; use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; -use chessfriend_moves::{Castle, EnPassant}; +use chessfriend_moves::Move; use std::{cell::OnceCell, fmt}; #[derive(Clone, Debug, Eq)] pub struct Position { - color_to_move: Color, - flags: Flags, - pieces: PieceBitBoards, - en_passant: Option, + pub board: Board, moves: OnceCell, - half_move_counter: u16, - full_move_number: u16, } impl Position { - pub fn empty() -> Position { + pub fn empty() -> Self { Default::default() } /// Return a starting position. pub fn starting() -> Self { - const BLACK_PIECES: [BitBoard; 6] = [ - BitBoard::new(0b0000000011111111 << 48), - BitBoard::new(0b0100001000000000 << 48), - BitBoard::new(0b0010010000000000 << 48), - BitBoard::new(0b1000000100000000 << 48), - BitBoard::new(0b0000100000000000 << 48), - BitBoard::new(0b0001000000000000 << 48), - ]; - - const WHITE_PIECES: [BitBoard; 6] = [ - BitBoard::new(0b1111111100000000), - BitBoard::new(0b0000000001000010), - BitBoard::new(0b0000000000100100), - BitBoard::new(0b0000000010000001), - BitBoard::new(0b0000000000001000), - BitBoard::new(0b0000000000010000), - ]; - Self { - color_to_move: Color::White, - pieces: PieceBitBoards::new([WHITE_PIECES, BLACK_PIECES]), + board: Board::starting(), ..Default::default() } } - pub fn player_to_move(&self) -> Color { - self.color_to_move - } - - pub fn move_number(&self) -> u16 { - self.full_move_number - } - - pub fn ply_counter(&self) -> u16 { - self.half_move_counter - } - - /// Returns true if the player has the right to castle on the given side of - /// the board. - /// - /// The right to castle on a particular side of the board is retained as - /// long as the player has not moved their king, or the rook on that side of - /// the board. - pub(crate) fn player_has_right_to_castle(&self, color: Color, castle: Castle) -> bool { - self.flags.player_has_right_to_castle(color, castle) - } - - /// Returns `true` if the player is able to castle on the given side of the board. - /// - /// The following requirements must be met: - /// - /// 1. The player must still have the right to castle on that side of the - /// board. The king and rook involved in the castle must not have moved. - /// 1. The spaces between the king and rook must be clear - /// 2. The king must not be in check - /// 3. In the course of castling on that side, the king must not pass - /// through a square that an enemy piece can see - pub(crate) fn player_can_castle(&self, player: Color, castle: Castle) -> bool { - if !self.player_has_right_to_castle(player, castle.into()) { - return false; + pub fn new(board: Board) -> Self { + Self { + board, + ..Default::default() } - - let castling_parameters = castle.parameters(player); - - let all_pieces = self.occupied_squares(); - if !(all_pieces & castling_parameters.clear_squares()).is_empty() { - return false; - } - - let danger_squares = self.king_danger(player); - if !(danger_squares & castling_parameters.check_squares()).is_empty() { - return false; - } - - true } +} +/* +impl Position { /// Return a PlacedPiece representing the rook to use for a castling move. pub(crate) fn rook_for_castle(&self, player: Color, castle: Castle) -> Option { let square = match (player, castle) { @@ -116,103 +49,68 @@ impl Position { (Color::Black, Castle::QueenSide) => Square::A8, }; - self.piece_on_square(square) + self.board.piece_on_square(square) } pub fn moves(&self) -> &Moves { self.moves.get_or_init(|| { + let player_to_move = self.player_to_move(); let checking_pieces = self.checking_pieces(); match checking_pieces.count() { // Normal, unrestricted move generation - 0 => Moves::new(self, self.color_to_move, BitBoard::FULL, BitBoard::FULL), + 0 => Moves::new( + &self.board, + player_to_move, + BitBoard::full(), + BitBoard::full(), + ), 1 => { - // Calculate push and capture masks for checking piece. Moves are restricted to those that intersect those masks. + // Calculate push and capture masks for checking piece. Moves are restricted to + // those that intersect those masks. let capture_mask = checking_pieces.capture_mask(); - let push_mask = - checking_pieces.push_mask(self.king_bitboard(self.color_to_move)); - Moves::new(self, self.color_to_move, capture_mask, push_mask) + let push_mask = checking_pieces.push_mask(self.king_bitboard(player_to_move)); + Moves::new(&self.board, player_to_move, capture_mask, push_mask) } // With more than one checking piece, the only legal moves are king moves. - _ => Moves::new(self, self.color_to_move, BitBoard::EMPTY, BitBoard::EMPTY), + _ => Moves::new( + &self.board, + player_to_move, + BitBoard::empty(), + BitBoard::empty(), + ), } }) } - /// A [BitBoard] representing the set of squares containing a piece. - #[inline] - #[must_use] - pub(crate) fn occupied_squares(&self) -> &BitBoard { - &self.pieces.all_pieces() - } - - #[inline] - #[must_use] - pub(crate) fn friendly_pieces(&self) -> &BitBoard { - self.pieces.all_pieces_of_color(self.color_to_move) - } - - #[inline] - #[must_use] - pub(crate) fn opposing_pieces(&self) -> &BitBoard { - self.pieces.all_pieces_of_color(self.color_to_move.other()) - } - - #[inline] - #[must_use] - pub(crate) fn all_pieces(&self) -> (&BitBoard, &BitBoard) { - (self.friendly_pieces(), self.opposing_pieces()) - } - - /// Return a BitBoard representing the set of squares containing a piece. - /// This set is the inverse of `occupied_squares`. - #[inline] - #[must_use] - pub(crate) fn empty_squares(&self) -> BitBoard { - !self.occupied_squares() - } - - pub fn piece_on_square(&self, sq: Square) -> Option { - for color in Color::iter() { - for shape in Shape::iter() { - let piece = Piece::new(*color, *shape); - if self.pieces.bitboard_for_piece(&piece).is_set(sq) { - return Some(PlacedPiece::new(piece, sq)); - } - } - } - - None - } - - pub fn pieces(&self, color: Color) -> Pieces { - Pieces::new(&self, color) - } - pub fn has_en_passant_square(&self) -> bool { - self.en_passant.is_some() + self.board.en_passant().is_some() } pub fn en_passant(&self) -> Option { - self.en_passant + self.board.en_passant() } - fn _sight_of_player(&self, player: Color, pieces: &PieceBitBoards) -> BitBoard { - let en_passant_target_square = self.en_passant.map(|ep| ep.target_square()); + fn _en_passant_target_square(&self) -> Option { + self.board.en_passant().map(EnPassant::target_square) + } + + fn _sight_of_player(&self, player: Color, board: &Board) -> BitBoard { + let en_passant_target_square = self._en_passant_target_square(); Shape::ALL .iter() .filter_map(|&shape| { let piece = Piece::new(player, shape); - let bitboard = pieces.bitboard_for_piece(&piece); + let bitboard = board.bitboard_for_piece(piece); if !bitboard.is_empty() { Some((piece, bitboard)) } else { None } }) - .flat_map(|(piece, &bitboard)| { + .flat_map(|(piece, bitboard)| { bitboard.occupied_squares().map(move |square| { - PlacedPiece::new(piece, square).sight(pieces, en_passant_target_square) + PlacedPiece::new(piece, square).sight(board, en_passant_target_square) }) }) .fold(BitBoard::empty(), |acc, sight| acc | sight) @@ -224,52 +122,37 @@ impl Position { #[cfg(test)] pub(crate) fn sight_of_piece(&self, piece: &PlacedPiece) -> BitBoard { - piece.sight(&self.pieces, self.en_passant.map(|ep| ep.target_square())) - } - - /// A bitboard representing the squares where a king of the given color will - /// be in danger of being captured by the opposing player. If the king is on - /// one of these squares, it is in check. The king cannot move to these - /// squares. - pub(crate) fn king_danger(&self, color: Color) -> BitBoard { - let pieces_without_king = { - let mut cloned_pieces = self.pieces.clone(); - let placed_king = PlacedPiece::new(Piece::king(color), self.king_square(color)); - cloned_pieces.remove_piece(&placed_king); - - cloned_pieces - }; - - self._sight_of_player(color.other(), &pieces_without_king) + piece.sight(&self.board, self._en_passant_target_square()) } #[cfg(test)] pub(crate) fn is_king_in_check(&self) -> bool { - let danger_squares = self.king_danger(self.color_to_move); - !(danger_squares & self.king_bitboard(self.color_to_move)).is_empty() + let danger_squares = self.king_danger(self.player_to_move()); + !(danger_squares & self.king_bitboard(self.player_to_move())).is_empty() } - fn king_bitboard(&self, player: Color) -> &BitBoard { - self.pieces.bitboard_for_piece(&Piece::king(player)) + fn king_bitboard(&self, player: Color) -> BitBoard { + self.board.pieces.bitboard_for_piece(Piece::king(player)) } pub(crate) fn king_square(&self, player: Color) -> Square { self.king_bitboard(player) - .occupied_squares() + .occupied_squares(&IterationDirection::default()) .next() .unwrap() } pub(crate) fn checking_pieces(&self) -> CheckingPieces { - let opponent = self.color_to_move.other(); - let king_square = self.king_square(self.color_to_move); + let opponent = self.player_to_move().other(); + let king_square = self.king_square(self.player_to_move()); let checking_pawns = { // The current player's pawn attack moves *from* this square are the // same as the pawn moves for the opposing player attacking this square. - let pawn_moves_to_king_square = BitBoard::pawn_attacks(king_square, self.color_to_move); + let pawn_moves_to_king_square = + BitBoard::pawn_attacks(king_square, self.player_to_move()); let opposing_pawn = Piece::pawn(opponent); - let opposing_pawns = self.pieces.bitboard_for_piece(&opposing_pawn); + let opposing_pawns = self.board.bitboard_for_piece(opposing_pawn); pawn_moves_to_king_square & opposing_pawns }; @@ -278,7 +161,7 @@ impl Position { ($moves_bb_fn:path, $piece_fn:ident) => {{ let moves_from_opposing_square = $moves_bb_fn(king_square); let piece = Piece::$piece_fn(opponent); - let opposing_pieces = self.pieces.bitboard_for_piece(&piece); + let opposing_pieces = self.board.bitboard_for_piece(piece); moves_from_opposing_square & opposing_pieces }}; @@ -298,88 +181,49 @@ impl Position { ) } } +*/ -// crate::position methods impl Position { - pub(super) fn new( - player_to_move: Color, - flags: Flags, - pieces: PieceBitBoards, - en_passant: Option, - half_move_counter: u16, - full_move_number: u16, - ) -> Self { - Self { - color_to_move: player_to_move, - flags, - en_passant, - pieces, - half_move_counter, - full_move_number, - ..Default::default() - } - } - - pub(super) fn flags(&self) -> Flags { - self.flags - } - - pub(super) fn piece_bitboards(&self) -> &PieceBitBoards { - &self.pieces + pub fn display(&self) -> DiagramFormatter { + self.board.display() } } -// crate methods impl Position { - pub(crate) fn bitboard_for_color(&self, color: Color) -> &BitBoard { - self.pieces.bitboard_for_color(color) + pub fn make_move(&mut self, ply: &Move) -> Result<(), MakeMoveError> { + Ok(()) } - pub(crate) fn bitboard_for_piece(&self, piece: Piece) -> &BitBoard { - self.pieces.bitboard_for_piece(&piece) - } -} - -#[cfg(test)] -impl Position { - pub(crate) fn test_set_en_passant(&mut self, en_passant: EnPassant) { - self.en_passant = Some(en_passant); + pub fn unmake_move(&mut self, ply: &Move) -> Result<(), UnmakeMoveError> { + Ok(()) } } impl Default for Position { fn default() -> Self { Self { - color_to_move: Color::White, - flags: Flags::default(), - pieces: PieceBitBoards::default(), - en_passant: None, + board: Board::default(), moves: OnceCell::new(), - half_move_counter: 0, - full_move_number: 1, } } } impl PartialEq for Position { fn eq(&self, other: &Self) -> bool { - self.pieces == other.pieces - && self.color_to_move == other.color_to_move - && self.flags == other.flags - && self.en_passant == other.en_passant + self.board == other.board } } impl fmt::Display for Position { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", DiagramFormatter::new(self)) + write!(f, "{}", self.board.display()) } } #[cfg(test)] mod tests { use super::*; - use crate::{assert_eq_bitboards, position, test_position, Position, PositionBuilder}; + use crate::{assert_eq_bitboards, position, test_position, Position}; use chessfriend_bitboard::bitboard; use chessfriend_core::piece; @@ -389,7 +233,7 @@ mod tests { Black Bishop on F7, ]; - let piece = pos.piece_on_square(Square::F7); + let piece = pos.board.piece_on_square(Square::F7); assert_eq!(piece, Some(piece!(Black Bishop on F7))); } @@ -398,11 +242,11 @@ mod tests { let pos = test_position!(starting); assert_eq!( - pos.piece_on_square(Square::H1), + pos.board.piece_on_square(Square::H1), Some(piece!(White Rook on H1)) ); assert_eq!( - pos.piece_on_square(Square::A8), + pos.board.piece_on_square(Square::A8), Some(piece!(Black Rook on A8)) ); } @@ -464,16 +308,14 @@ mod tests { #[test] fn danger_squares() { - let pos = PositionBuilder::new() - .place_piece(piece!(White King on E1)) - .place_piece(piece!(Black King on E7)) - .place_piece(piece!(White Rook on E4)) - .to_move(Color::Black) - .build(); + let pos = test_position!(Black, [ + White King on E1, + Black King on E7, + White Rook on E4, + ]); let danger_squares = pos.king_danger(Color::Black); - let expected = - bitboard![D1, F1, D2, E2, F2, E3, A4, B4, C4, D4, F4, G4, H4, E5, E6, E7, E8]; + let expected = bitboard![D1 F1 D2 E2 F2 E3 A4 B4 C4 D4 F4 G4 H4 E5 E6 E7 E8]; assert_eq_bitboards!(danger_squares, expected); } } diff --git a/position/src/sight.rs b/position/src/sight.rs index 072806e..bff21d6 100644 --- a/position/src/sight.rs +++ b/position/src/sight.rs @@ -9,15 +9,15 @@ use chessfriend_board::Board; use chessfriend_core::{Color, Direction, PlacedPiece, Shape, Square}; macro_rules! ray_in_direction { - ($square:expr, $blockers:expr, $direction:ident, $occupied_squares:tt) => {{ + ($square:expr, $blockers:expr, $direction:ident, $first_occupied_square:tt) => {{ let ray = BitBoard::ray($square, Direction::$direction); - if let Some(first_occupied_square) = BitBoard::$occupied_squares(&(ray & $blockers)).next() - { + let ray_blockers = ray & $blockers; + if let Some(first_occupied_square) = ray_blockers.$first_occupied_square() { let remainder = BitBoard::ray(first_occupied_square, Direction::$direction); let attack_ray = ray & !remainder; attack_ray } else { - *ray + ray } }}; } @@ -51,34 +51,34 @@ fn _knight_sight(knight_square: Square, blockers: BitBoard) -> BitBoard { fn _bishop_sight(bishop_square: Square, occupancy: BitBoard) -> BitBoard { #[rustfmt::skip] - let sight = ray_in_direction!(bishop_square, occupancy, NorthEast, occupied_squares_trailing) - | ray_in_direction!(bishop_square, occupancy, NorthWest, occupied_squares_trailing) - | ray_in_direction!(bishop_square, occupancy, SouthEast, occupied_squares) - | ray_in_direction!(bishop_square, occupancy, SouthWest, occupied_squares); + let sight = ray_in_direction!(bishop_square, occupancy, NorthEast, first_occupied_square_trailing) + | ray_in_direction!(bishop_square, occupancy, NorthWest, first_occupied_square_trailing) + | ray_in_direction!(bishop_square, occupancy, SouthEast, first_occupied_square_leading) + | ray_in_direction!(bishop_square, occupancy, SouthWest, first_occupied_square_leading); sight } fn _rook_sight(rook_square: Square, occupancy: BitBoard) -> BitBoard { #[rustfmt::skip] - let sight = ray_in_direction!(rook_square, occupancy, North, occupied_squares_trailing) - | ray_in_direction!(rook_square, occupancy, East, occupied_squares_trailing) - | ray_in_direction!(rook_square, occupancy, South, occupied_squares) - | ray_in_direction!(rook_square, occupancy, West, occupied_squares); + let sight = ray_in_direction!(rook_square, occupancy, North, first_occupied_square_trailing) + | ray_in_direction!(rook_square, occupancy, East, first_occupied_square_trailing) + | ray_in_direction!(rook_square, occupancy, South, first_occupied_square_leading) + | ray_in_direction!(rook_square, occupancy, West, first_occupied_square_leading); sight } fn _queen_sight(queen_square: Square, occupancy: BitBoard) -> BitBoard { #[rustfmt::skip] - let sight = ray_in_direction!(queen_square, occupancy, NorthWest, occupied_squares_trailing) - | ray_in_direction!(queen_square, occupancy, North, occupied_squares_trailing) - | ray_in_direction!(queen_square, occupancy, NorthEast, occupied_squares_trailing) - | ray_in_direction!(queen_square, occupancy, East, occupied_squares_trailing) - | ray_in_direction!(queen_square, occupancy, SouthEast, occupied_squares) - | ray_in_direction!(queen_square, occupancy, South, occupied_squares) - | ray_in_direction!(queen_square, occupancy, SouthWest, occupied_squares) - | ray_in_direction!(queen_square, occupancy, West, occupied_squares); + let sight = ray_in_direction!(queen_square, occupancy, NorthWest, first_occupied_square_trailing) + | ray_in_direction!(queen_square, occupancy, North, first_occupied_square_trailing) + | ray_in_direction!(queen_square, occupancy, NorthEast, first_occupied_square_trailing) + | ray_in_direction!(queen_square, occupancy, East, first_occupied_square_trailing) + | ray_in_direction!(queen_square, occupancy, SouthEast, first_occupied_square_leading) + | ray_in_direction!(queen_square, occupancy, South, first_occupied_square_leading) + | ray_in_direction!(queen_square, occupancy, SouthWest, first_occupied_square_leading) + | ray_in_direction!(queen_square, occupancy, West, first_occupied_square_leading); sight } @@ -129,9 +129,9 @@ impl SightExt for PlacedPiece { Color::Black => self.black_pawn_sight(board, en_passant_square), }, Shape::Knight => self.knight_sight(board), - Shape::Bishop => self.bishop_sight(board.all_pieces_bitboard()), - Shape::Rook => self.rook_sight(board.all_pieces_bitboard()), - Shape::Queen => self.queen_sight(board.all_pieces_bitboard()), + Shape::Bishop => self.bishop_sight(board.pieces.all_pieces()), + Shape::Rook => self.rook_sight(board.pieces.all_pieces()), + Shape::Queen => self.queen_sight(board.pieces.all_pieces()), Shape::King => self.king_sight(board), } } @@ -139,46 +139,40 @@ impl SightExt for PlacedPiece { impl KingSightExt for PlacedPiece { fn king_sight(&self, board: &Board) -> BitBoard { - _king_sight( - self.square(), - board.all_pieces_of_color_bitboard(self.color()), - ) + _king_sight(self.square, board.pieces.all_pieces_of_color(self.color)) } } impl KnightSightExt for PlacedPiece { fn knight_sight(&self, board: &Board) -> BitBoard { - _knight_sight( - self.square(), - board.all_pieces_of_color_bitboard(self.color()), - ) + _knight_sight(self.square, board.pieces.all_pieces_of_color(self.color)) } } impl PawnSightExt for PlacedPiece { fn pawn_sight(&self, board: &Board, en_passant_square: Option) -> BitBoard { - match self.color() { + match self.color { Color::White => self.white_pawn_sight(board, en_passant_square), Color::Black => self.black_pawn_sight(board, en_passant_square), } } fn white_pawn_sight(&self, board: &Board, en_passant_square: Option) -> BitBoard { - let opponent = self.color().other(); + let opponent = self.color.other(); _white_pawn_sight( - self.square().into(), - board.all_pieces_bitboard(), - board.all_pieces_of_color_bitboard(opponent), + self.square.into(), + board.pieces.all_pieces(), + board.pieces.all_pieces_of_color(opponent), en_passant_square.into(), ) } fn black_pawn_sight(&self, board: &Board, en_passant_square: Option) -> BitBoard { - let opponent = self.color().other(); + let opponent = self.piece.color.other(); _black_pawn_sight( - self.square().into(), - board.all_pieces_bitboard(), - board.all_pieces_of_color_bitboard(opponent), + self.square.into(), + board.pieces.all_pieces(), + board.pieces.all_pieces_of_color(opponent), en_passant_square.into(), ) } @@ -186,19 +180,19 @@ impl PawnSightExt for PlacedPiece { impl BishopSightExt for PlacedPiece { fn bishop_sight(&self, occupancy: BitBoard) -> BitBoard { - _bishop_sight(self.square(), occupancy) + _bishop_sight(self.square, occupancy) } } impl RookSightExt for PlacedPiece { fn rook_sight(&self, occupancy: BitBoard) -> BitBoard { - _rook_sight(self.square(), occupancy) + _rook_sight(self.square, occupancy) } } impl QueenSightExt for PlacedPiece { fn queen_sight(&self, occupancy: BitBoard) -> BitBoard { - _queen_sight(self.square(), occupancy) + _queen_sight(self.square, occupancy) } } @@ -225,7 +219,7 @@ impl SliderRayToSquareExt for Shape { ray!(origin, NorthWest), ] .into_iter() - .find(|(&ray, _)| !(target_bitboard & ray).is_empty()), + .find(|(ray, _)| !(target_bitboard & ray).is_empty()), Shape::Rook => [ ray!(origin, North), ray!(origin, East), @@ -233,7 +227,7 @@ impl SliderRayToSquareExt for Shape { ray!(origin, West), ] .into_iter() - .find(|(&ray, _)| !(target_bitboard & ray).is_empty()), + .find(|(ray, _)| !(target_bitboard & ray).is_empty()), Shape::Queen => [ ray!(origin, North), ray!(origin, NorthEast), @@ -245,7 +239,7 @@ impl SliderRayToSquareExt for Shape { ray!(origin, NorthWest), ] .into_iter() - .find(|(&ray, _)| !(target_bitboard & ray).is_empty()), + .find(|(ray, _)| !(target_bitboard & ray).is_empty()), _ => None, }; @@ -307,10 +301,9 @@ mod tests { mod pawn { use crate::test_position; use chessfriend_bitboard::{bitboard, BitBoard}; - use chessfriend_core::{piece, Square}; - use chessfriend_moves::EnPassant; + use chessfriend_core::piece; - sight_test!(e4_pawn, piece!(White Pawn on E4), bitboard!(D5, F5)); + sight_test!(e4_pawn, piece!(White Pawn on E4), bitboard![D5 F5]); sight_test!( e4_pawn_one_blocker, @@ -352,15 +345,14 @@ mod tests { #[test] fn e5_en_passant() { - let mut pos = test_position!( + let pos = test_position!(White, [ White Pawn on E5, Black Pawn on D5, - ); - pos.test_set_en_passant(EnPassant::from_target_square(Square::D6).unwrap()); + ], D6); let piece = piece!(White Pawn on E5); let sight = pos.sight_of_piece(&piece); - assert_eq!(sight, bitboard!(D6, F6)); + assert_eq!(sight, bitboard!(D6 F6)); } } @@ -372,7 +364,7 @@ mod tests { sight_test!( f6_knight, piece!(Black Knight on F6), - bitboard!(H7, G8, E8, D7, D5, E4, G4, H5) + bitboard![H7 G8 E8 D7 D5 E4 G4 H5] ); } @@ -382,13 +374,13 @@ mod tests { sight_test!( c2_bishop, piece!(Black Bishop on C2), - bitboard!(D1, B3, A4, B1, D3, E4, F5, G6, H7) + bitboard![D1 B3 A4 B1 D3 E4 F5 G6 H7] ); #[test] fn ray_to_square() { let generated_ray = Shape::Bishop.ray_to_square(Square::C5, Square::E7); - let expected_ray = bitboard![D6, E7]; + let expected_ray = bitboard![D6 E7]; assert_eq!(generated_ray, Some(expected_ray)); } } @@ -400,7 +392,7 @@ mod tests { sight_test!( g3_rook, piece!(White Rook on G3), - bitboard!(G1, G2, G4, G5, G6, G7, G8, A3, B3, C3, D3, E3, F3, H3) + bitboard![G1 G2 G4 G5 G6 G7 G8 A3 B3 C3 D3 E3 F3 H3] ); sight_test!( @@ -411,25 +403,25 @@ mod tests { Black King on E7, ], piece!(White Rook on E4), - bitboard!(A4, B4, C4, D4, F4, G4, H4, E2, E3, E5, E6, E7, E1) + bitboard![A4 B4 C4 D4 F4 G4 H4 E2 E3 E5 E6 E7 E1] ); #[test] fn ray_to_square() { let generated_ray = Shape::Rook.ray_to_square(Square::C2, Square::C6); - let expected_ray = bitboard![C3, C4, C5, C6]; + let expected_ray = bitboard![C3 C4 C5 C6]; assert_eq!(generated_ray, Some(expected_ray)); let generated_ray = Shape::Rook.ray_to_square(Square::D2, Square::H2); - let expected_ray = bitboard![E2, F2, G2, H2]; + let expected_ray = bitboard![E2 F2 G2 H2]; assert_eq!(generated_ray, Some(expected_ray)); let generated_ray = Shape::Rook.ray_to_square(Square::G6, Square::B6); - let expected_ray = bitboard![B6, C6, D6, E6, F6]; + let expected_ray = bitboard![B6 C6 D6 E6 F6]; assert_eq!(generated_ray, Some(expected_ray)); let generated_ray = Shape::Rook.ray_to_square(Square::A6, Square::A3); - let expected_ray = bitboard![A5, A4, A3]; + let expected_ray = bitboard![A5 A4 A3]; assert_eq!(generated_ray, Some(expected_ray)); } } @@ -438,10 +430,6 @@ mod tests { use chessfriend_bitboard::bitboard; use chessfriend_core::piece; - sight_test!( - e1_king, - piece!(White King on E1), - bitboard![D1, D2, E2, F2, F1] - ); + sight_test!(e1_king, piece!(White King on E1), bitboard![D1 D2 E2 F2 F1]); } } From 424d348b2d19a947eadbdd9387332e97ec1957db Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 8 May 2025 17:37:59 -0700 Subject: [PATCH 255/423] WIP --- Makefile | 4 ++++ board/src/errors.rs | 0 position/src/sight/rays.rs | 27 +++++++++++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 Makefile create mode 100644 board/src/errors.rs create mode 100644 position/src/sight/rays.rs diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..500830f --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ + + +all: + cargo build diff --git a/board/src/errors.rs b/board/src/errors.rs new file mode 100644 index 0000000..e69de29 diff --git a/position/src/sight/rays.rs b/position/src/sight/rays.rs new file mode 100644 index 0000000..df376d7 --- /dev/null +++ b/position/src/sight/rays.rs @@ -0,0 +1,27 @@ +// Eryn Wells + +use chessfriend_bitboard::BitBoard; +use chessfriend_core::{Direction, Square}; + +fn _find_ray_connecting_squares(origin_square: Square, target_square: Square) -> BitBoard { + macro_rules! ray { + ($square:expr, $direction:expr) => { + ($direction, BitBoard::ray($square, $direction)) + }; + } + + let target: BitBoard = target_square.into(); + match Direction::ALL + .iter() + .map(|direction| ray!(origin_square, *direction)) + .find(|(direction, &ray)| (ray & target).is_populated()) + { + Some((direction, ray)) => { + if let Some(first_occupied_square) = BitBoard::$occupied_squares(&(ray & $blockers)).next() + let remainder = BitBoard::ray(first_occupied_square, direction); + let attack_ray = ray & !remainder; + attack_ray * ray + } + None => BitBoard::EMPTY, + } +} From f1431ea4e92e7f2c92420874676de45c320e24a3 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 8 May 2025 17:54:49 -0700 Subject: [PATCH 256/423] [position] Move postion/mod.rs -> position.rs --- position/src/{position/mod.rs => position.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename position/src/{position/mod.rs => position.rs} (100%) diff --git a/position/src/position/mod.rs b/position/src/position.rs similarity index 100% rename from position/src/position/mod.rs rename to position/src/position.rs From 184e81a7c8c2bf42e57784fd0a5545a13a8b8c36 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 16 May 2025 07:44:05 -0700 Subject: [PATCH 257/423] [bitboard] Remove #[must_use] from method calls; add it to BitBoard type --- bitboard/src/bitboard.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index ea1233c..573dff5 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -30,12 +30,12 @@ const SQUARES_NUM: u8 = Square::NUM as u8; /// A B C D E F G H /// ``` /// +#[must_use] #[derive(Clone, Copy, Eq, Hash, PartialEq)] pub struct BitBoard(pub(crate) u64); macro_rules! moves_getter { ($getter_name:ident) => { - #[must_use] pub fn $getter_name(sq: Square) -> BitBoard { library::library().$getter_name(sq) } @@ -46,44 +46,38 @@ impl BitBoard { const EMPTY: BitBoard = BitBoard(u64::MIN); const FULL: BitBoard = BitBoard(u64::MAX); - #[must_use] pub const fn empty() -> BitBoard { Self::EMPTY } - #[must_use] pub const fn full() -> BitBoard { Self::FULL } - #[must_use] pub const fn new(bits: u64) -> BitBoard { BitBoard(bits) } - #[must_use] + // TODO: Is &u8 really necessary here? pub fn rank(rank: &u8) -> BitBoard { debug_assert!(*rank < 8); library::RANKS[*rank as usize] } - #[must_use] + // TODO: Is &u8 really necessary here? pub fn file(file: &u8) -> BitBoard { debug_assert!(*file < 8); library::FILES[*file as usize] } - #[must_use] pub fn ray(sq: Square, dir: Direction) -> BitBoard { library::library().ray(sq, dir) } - #[must_use] pub fn pawn_attacks(sq: Square, color: Color) -> BitBoard { library::library().pawn_attacks(sq, color) } - #[must_use] pub fn pawn_pushes(sq: Square, color: Color) -> BitBoard { library::library().pawn_pushes(sq, color) } @@ -94,12 +88,10 @@ impl BitBoard { moves_getter!(queen_moves); moves_getter!(king_moves); - #[must_use] pub const fn kingside(color: Color) -> &'static BitBoard { &library::KINGSIDES[color as usize] } - #[must_use] pub const fn queenside(color: Color) -> &'static BitBoard { &library::QUEENSIDES[color as usize] } @@ -243,10 +235,12 @@ impl BitBoard { } } + #[must_use] pub fn occupied_squares_leading(&self) -> LeadingBitScanner { LeadingBitScanner::new(self.0) } + #[must_use] pub fn occupied_squares_trailing(&self) -> TrailingBitScanner { TrailingBitScanner::new(self.0) } From e5a53678644c16b707a915de8836e99242f22b45 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 16 May 2025 07:44:37 -0700 Subject: [PATCH 258/423] [bitboard] Remove leading underscore from leading_zeros and trailing_zeros methods This is a clippy suggestion. --- bitboard/src/bitboard.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 573dff5..bb6738c 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -258,7 +258,7 @@ impl BitBoard { /// the board is empty, returns `None`. #[must_use] pub fn first_occupied_square_leading(self) -> Option { - let leading_zeros = self._leading_zeros(); + let leading_zeros = self.leading_zeros(); if leading_zeros < SQUARES_NUM { unsafe { Some(Square::from_index_unchecked( @@ -275,7 +275,7 @@ impl BitBoard { /// If the board is empty, returns `None`. #[must_use] pub fn first_occupied_square_trailing(self) -> Option { - let trailing_zeros = self._trailing_zeros(); + let trailing_zeros = self.trailing_zeros(); if trailing_zeros < SQUARES_NUM { unsafe { Some(Square::from_index_unchecked(trailing_zeros)) } @@ -288,13 +288,13 @@ impl BitBoard { impl BitBoard { #[must_use] #[allow(clippy::cast_possible_truncation)] - fn _leading_zeros(self) -> u8 { + fn leading_zeros(self) -> u8 { self.0.leading_zeros() as u8 } #[must_use] #[allow(clippy::cast_possible_truncation)] - fn _trailing_zeros(self) -> u8 { + fn trailing_zeros(self) -> u8 { self.0.trailing_zeros() as u8 } } @@ -357,7 +357,7 @@ impl TryFrom for Square { return Err(TryFromBitBoardError::NotSingleSquare); } - unsafe { Ok(Square::from_index_unchecked(value._trailing_zeros())) } + unsafe { Ok(Square::from_index_unchecked(value.trailing_zeros())) } } } From 9943224ee0a75d71fa004bdf5df3ebce9e90da4c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 16 May 2025 07:44:59 -0700 Subject: [PATCH 259/423] [bitboard] Add separators to the NOT_A_FILE and NOT_H_FILE constants This is a clippy suggestion. --- bitboard/src/shifts.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bitboard/src/shifts.rs b/bitboard/src/shifts.rs index 2370306..98b5adf 100644 --- a/bitboard/src/shifts.rs +++ b/bitboard/src/shifts.rs @@ -3,8 +3,8 @@ use super::BitBoard; impl BitBoard { - const NOT_A_FILE: u64 = 0xfefefefefefefefe; - const NOT_H_FILE: u64 = 0x7f7f7f7f7f7f7f7f; + const NOT_A_FILE: u64 = 0xfefe_fefe_fefe_fefe; + const NOT_H_FILE: u64 = 0x7f7f_7f7f_7f7f_7f7f; #[inline] pub fn shift_north(&self, n: u8) -> BitBoard { From 3b5b2f16a3b5b17b259acefc5ecfcc75d40660ea Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 16 May 2025 07:47:28 -0700 Subject: [PATCH 260/423] [board] Add occupancy methods that return BitBoards --- board/src/board.rs | 19 ++++++++++++++++++- board/src/piece_sets.rs | 26 +++++++++++++++++++------- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/board/src/board.rs b/board/src/board.rs index ed719a5..3922511 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -82,7 +82,24 @@ impl Board { } impl Board { - #[must_use] + pub fn occupancy(&self) -> BitBoard { + self.pieces.occpuancy() + } + + pub fn vacancy(&self) -> BitBoard { + !self.occupancy() + } + + pub fn friendly_occupancy(&self, color: Color) -> BitBoard { + self.pieces.friendly_occupancy(color) + } + + pub fn opposing_occupancy(&self, color: Color) -> BitBoard { + self.pieces.opposing_occupancy(color) + } +} + +impl Board { pub fn display(&self) -> DiagramFormatter<'_> { DiagramFormatter::new(self) } diff --git a/board/src/piece_sets.rs b/board/src/piece_sets.rs index 8096100..ff66b98 100644 --- a/board/src/piece_sets.rs +++ b/board/src/piece_sets.rs @@ -4,7 +4,7 @@ mod mailbox; use self::mailbox::Mailbox; use chessfriend_bitboard::{BitBoard, IterationDirection}; -use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; +use chessfriend_core::{Color, Piece, Shape, Square}; use std::ops::BitOr; #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] @@ -55,16 +55,28 @@ impl PieceSet { } } - /// A [`BitBoard`] representing all the pieces currently on the board. Other - /// engines might refer to this concept as 'occupancy'. - pub fn all_pieces(&self) -> BitBoard { + /// A [`BitBoard`] representing all the pieces on the board. + pub fn occpuancy(&self) -> BitBoard { self.color_occupancy .iter() - .fold(BitBoard::empty(), BitOr::bitor) + .fold(BitBoard::default(), BitOr::bitor) } - pub(crate) fn iter(&self) -> impl Iterator { - self.mailbox.iter() + pub fn friendly_occupancy(&self, color: Color) -> BitBoard { + self.color_occupancy[color as usize] + } + + pub fn opposing_occupancy(&self, color: Color) -> BitBoard { + self.color_occupancy.iter().enumerate().fold( + BitBoard::default(), + |acc, (index, occupancy)| { + if index == color as usize { + acc | occupancy + } else { + acc + } + }, + ) } pub(crate) fn get(&self, square: Square) -> Option { From 5553bab65938f97e5cfd07e59e4fe812d8d71756 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 16 May 2025 07:49:09 -0700 Subject: [PATCH 261/423] [board] Teach DiagramFormatter how to highlight and mark squares Add two BitBoard attributes to the struct that mark squares that should be marked or highlighted. Empty marked squares are shown with an asterisk (*). Marked squares with a piece don't have any change of appearance. (Something I'm still thinking about.) Highlighted squares are shown with the ANSI escape sequence for Reverse Video. --- board/src/display.rs | 63 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/board/src/display.rs b/board/src/display.rs index 8736b84..73c59a9 100644 --- a/board/src/display.rs +++ b/board/src/display.rs @@ -1,19 +1,38 @@ // Eryn Wells use crate::Board; +use chessfriend_bitboard::BitBoard; use chessfriend_core::{File, Rank, Square}; use std::fmt; #[must_use] -pub struct DiagramFormatter<'a>(&'a Board); +pub struct DiagramFormatter<'a> { + board: &'a Board, + marked_squares: BitBoard, + highlighted_squares: BitBoard, +} impl<'a> DiagramFormatter<'a> { pub fn new(board: &'a Board) -> Self { - Self(board) + Self { + board, + marked_squares: BitBoard::default(), + highlighted_squares: BitBoard::default(), + } + } + + pub fn mark(mut self, bitboard: BitBoard) -> Self { + self.marked_squares = bitboard; + self + } + + pub fn highlight(mut self, bitboard: BitBoard) -> Self { + self.highlighted_squares = bitboard; + self } } -impl<'a> fmt::Display for DiagramFormatter<'a> { +impl fmt::Display for DiagramFormatter<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, " ╔═════════════════╗")?; @@ -22,17 +41,35 @@ impl<'a> fmt::Display for DiagramFormatter<'a> { for file in File::ALL { let square = Square::from_file_rank(file, rank); - match self.0.get_piece(square) { - Some(piece) => write!(f, "{piece} ")?, - None => write!(f, "· ")?, + + let should_highlight = self.highlighted_squares.contains(square); + if should_highlight { + write!(f, "\x1b[7m")?; } + + if let Some(piece) = self.board.get_piece(square) { + write!(f, "{piece}")?; + } else { + let ch = if self.marked_squares.contains(square) { + '*' + } else { + '·' + }; + write!(f, "{ch}")?; + } + + if should_highlight { + write!(f, "\x1b[0m")?; + } + + write!(f, " ")?; } writeln!(f, "║")?; } writeln!(f, " ╚═════════════════╝")?; - writeln!(f, " a b c d e f g h")?; + write!(f, " a b c d e f g h")?; Ok(()) } @@ -46,24 +83,24 @@ mod tests { #[test] #[ignore] fn empty() { - let pos = test_board!(empty); - let diagram = DiagramFormatter(&pos); + let board = test_board!(empty); + let diagram = DiagramFormatter::new(&board); println!("{diagram}"); } #[test] #[ignore] fn one_king() { - let pos = test_board![Black King on H3]; - let diagram = DiagramFormatter(&pos); + let board = test_board![Black King on H3]; + let diagram = DiagramFormatter::new(&board); println!("{diagram}"); } #[test] #[ignore] fn starting() { - let pos = test_board!(starting); - let diagram = DiagramFormatter(&pos); + let board = test_board!(starting); + let diagram = DiagramFormatter::new(&board); println!("{diagram}"); } } From 669a7c00ecd924462479354f6c5a7eb2790bfcd8 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 18 May 2025 08:07:12 -0700 Subject: [PATCH 262/423] [position] Add thiserror dependency --- Cargo.lock | 37 ++++++++++++++++++++++++++++++++----- position/Cargo.toml | 1 + 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ffc42b0..7414d4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "anstream" @@ -76,11 +76,15 @@ version = "0.1.0" dependencies = [ "chessfriend_bitboard", "chessfriend_core", + "thiserror", ] [[package]] name = "chessfriend_core" version = "0.1.0" +dependencies = [ + "thiserror", +] [[package]] name = "chessfriend_moves" @@ -89,6 +93,7 @@ dependencies = [ "chessfriend_bitboard", "chessfriend_board", "chessfriend_core", + "thiserror", ] [[package]] @@ -99,6 +104,7 @@ dependencies = [ "chessfriend_board", "chessfriend_core", "chessfriend_moves", + "thiserror", ] [[package]] @@ -182,6 +188,7 @@ checksum = "281e452d3bad4005426416cdba5ccfd4f5c1280e10099e21db27f7c1c28347fc" name = "explorer" version = "0.1.0" dependencies = [ + "chessfriend_board", "chessfriend_core", "chessfriend_moves", "chessfriend_position", @@ -268,9 +275,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -349,15 +356,35 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "2.0.48" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "unicode-ident" version = "1.0.12" diff --git a/position/Cargo.toml b/position/Cargo.toml index 949e00c..c0c5225 100644 --- a/position/Cargo.toml +++ b/position/Cargo.toml @@ -10,3 +10,4 @@ chessfriend_core = { path = "../core" } chessfriend_bitboard = { path = "../bitboard" } chessfriend_board = { path = "../board" } chessfriend_moves = { path = "../moves" } +thiserror = "2" From a78526befac864008a857eb3b07ce31d13acecbf Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 18 May 2025 08:08:47 -0700 Subject: [PATCH 263/423] [position] Move make_move to its own module: position::make_move Rework sight.rs and add a new module, movement.rs, to calculate piece sight and movement. I discovered during this process that "sight" and "movement" are different because pawns (in particular) can move in ways that don't follow their sight lines. The routines in the movement module account for this, but also pass through to the sight routines for other pieces. --- position/src/lib.rs | 1 + position/src/movement.rs | 121 ++++++++ position/src/position.rs | 4 +- position/src/position/make_move.rs | 182 +++++++++++++ position/src/position/position.rs | 10 - position/src/sight.rs | 424 +++++++++++------------------ 6 files changed, 459 insertions(+), 283 deletions(-) create mode 100644 position/src/movement.rs create mode 100644 position/src/position/make_move.rs diff --git a/position/src/lib.rs b/position/src/lib.rs index 4584f3b..5215041 100644 --- a/position/src/lib.rs +++ b/position/src/lib.rs @@ -3,6 +3,7 @@ mod check; mod display; mod move_generator; +mod movement; mod position; mod sight; diff --git a/position/src/movement.rs b/position/src/movement.rs new file mode 100644 index 0000000..a3ee154 --- /dev/null +++ b/position/src/movement.rs @@ -0,0 +1,121 @@ +// Eryn Wells + +//! Defines routines for computing the movement of a piece. Movement is the set +//! of squares a piece can move to. For all pieces except pawns, the Movement +//! set is equal to the Sight set. + +use crate::sight::Sight; +use chessfriend_bitboard::BitBoard; +use chessfriend_board::Board; +use chessfriend_core::{Color, Piece, Rank, Shape, Square}; + +pub trait Movement { + fn movement(&self, square: Square, board: &Board) -> BitBoard; +} + +impl Movement for Piece { + fn movement(&self, square: Square, board: &Board) -> BitBoard { + match self.shape { + Shape::Pawn => { + // Pawns can only move to squares they can see to capture. + let opposing_occupancy = board.opposing_occupancy(self.color); + let sight = self.sight(square, board) & opposing_occupancy; + let pushes = pawn_pushes(square.into(), self.color, board.occupancy()); + sight | pushes + } + _ => self.sight(square, board), + } + } +} + +fn pawn_pushes(pawn: BitBoard, color: Color, occupancy: BitBoard) -> BitBoard { + let vacancy = !occupancy; + + match color { + Color::White => { + let second_rank = BitBoard::rank(&Rank::TWO.into()); + + let mut pushes = pawn.shift_north_one() & vacancy; + if !(pawn & second_rank).is_empty() { + // Double push + pushes = pushes | (pushes.shift_north_one() & vacancy); + } + + pushes + } + Color::Black => { + let seventh_rank = BitBoard::rank(&Rank::SEVEN.into()); + + let mut pushes = pawn.shift_south_one() & vacancy; + if !(pawn & seventh_rank).is_empty() { + // Double push + pushes = pushes | (pushes.shift_south_one() & vacancy); + } + + pushes + } + } +} + +#[cfg(test)] +mod tests { + use super::pawn_pushes; + use chessfriend_bitboard::{bitboard, BitBoard}; + use chessfriend_core::{Color, Square}; + + #[test] + fn white_pushes_empty_board() { + assert_eq!( + pawn_pushes(Square::E4.into(), Color::White, BitBoard::empty()), + bitboard![E5] + ); + assert_eq!( + pawn_pushes(Square::E2.into(), Color::White, BitBoard::empty()), + bitboard![E3 E4] + ); + } + + #[test] + fn black_pawn_empty_board() { + assert_eq!( + pawn_pushes(Square::A4.into(), Color::Black, BitBoard::empty()), + bitboard![A3] + ); + assert_eq!( + pawn_pushes(Square::B7.into(), Color::Black, BitBoard::empty()), + bitboard![B6 B5] + ); + } + + #[test] + fn white_pushes_blocker() { + assert_eq!( + pawn_pushes(Square::C5.into(), Color::White, bitboard![C6]), + BitBoard::empty() + ); + assert_eq!( + pawn_pushes(Square::D2.into(), Color::White, bitboard![D4]), + bitboard![D3] + ); + assert_eq!( + pawn_pushes(Square::D2.into(), Color::White, bitboard![D3]), + BitBoard::empty() + ); + } + + #[test] + fn black_pushes_blocker() { + assert_eq!( + pawn_pushes(Square::C5.into(), Color::Black, bitboard![C4]), + BitBoard::empty() + ); + assert_eq!( + pawn_pushes(Square::D7.into(), Color::Black, bitboard![D5]), + bitboard![D6] + ); + assert_eq!( + pawn_pushes(Square::D7.into(), Color::Black, bitboard![D6]), + BitBoard::empty() + ); + } +} diff --git a/position/src/position.rs b/position/src/position.rs index 0782305..6c7bfca 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -1,9 +1,9 @@ // Eryn Wells -mod builders; +mod make_move; mod position; pub use { - builders::{MakeMoveError, MoveBuilder}, + make_move::{MakeMoveError, ValidateMove}, position::Position, }; diff --git a/position/src/position/make_move.rs b/position/src/position/make_move.rs new file mode 100644 index 0000000..c79185c --- /dev/null +++ b/position/src/position/make_move.rs @@ -0,0 +1,182 @@ +// Eryn Wells + +use crate::{movement::Movement, Position}; +use chessfriend_board::{PlacePieceError, PlacePieceStrategy}; +use chessfriend_core::{Color, Piece, Square}; +use chessfriend_moves::Move; +use thiserror::Error; + +type MakeMoveResult = Result<(), MakeMoveError>; + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum ValidateMove { + #[default] + No, + Yes, +} + +#[derive(Debug, Error, Eq, PartialEq)] +pub enum MakeMoveError { + #[error("no piece on {0}")] + NoPiece(Square), + + #[error("{piece} on {square} is not of active color")] + NonActiveColor { piece: Piece, square: Square }, + + #[error("cannot capture piece on {0}")] + InvalidCapture(Square), + + #[error("no capture square")] + NoCaptureSquare, + + #[error("{piece} on {origin} cannot move to {target}")] + NoMove { + piece: Piece, + origin: Square, + target: Square, + }, + + #[error("{0}")] + PlacePieceError(#[from] PlacePieceError), +} + +pub enum UnmakeMoveError {} + +impl Position { + /// Make a move in the position. + /// + /// ## Errors + /// + /// If `validate` is [`ValidateMove::Yes`], perform validation of move correctness prior to + /// applying the move. See [`Position::validate_move`]. + pub fn make_move(&mut self, ply: Move, validate: ValidateMove) -> MakeMoveResult { + self.validate_move(ply, validate)?; + + if ply.is_quiet() { + return self.make_quiet_move(ply.origin_square(), ply.target_square()); + } + + if ply.is_capture() { + return self.make_capture_move(ply); + } + + Ok(()) + } + + pub fn unmake_move(&mut self, ply: &Move) -> Result<(), UnmakeMoveError> { + Ok(()) + } +} + +impl Position { + fn make_quiet_move(&mut self, origin: Square, target: Square) -> MakeMoveResult { + let piece = self + .board + .remove_piece(origin) + .ok_or(MakeMoveError::NoPiece(origin))?; + + self.place_active_piece(piece, target)?; + + self.advance_clocks(HalfMoveClock::Advance); + + Ok(()) + } + + fn make_capture_move(&mut self, ply: Move) -> MakeMoveResult { + let origin_square = ply.origin_square(); + let piece = self.get_piece_for_move(origin_square)?; + + let capture_square = ply.capture_square().ok_or(MakeMoveError::NoCaptureSquare)?; + let captured_piece = self.get_piece_for_move(capture_square)?; + + // Register the capture + self.captures[piece.color as usize].push(captured_piece); + + self.remove_piece(origin_square).unwrap(); + let target_square = ply.target_square(); + self.place_piece(piece, target_square, PlacePieceStrategy::Replace)?; + + self.advance_clocks(HalfMoveClock::Reset); + + Ok(()) + } +} + +impl Position { + fn get_piece_for_move(&mut self, square: Square) -> Result { + self.get_piece(square).ok_or(MakeMoveError::NoPiece(square)) + } + + fn place_active_piece(&mut self, piece: Piece, square: Square) -> MakeMoveResult { + self.board + .place_piece(piece, square, PlacePieceStrategy::PreserveExisting) + .map_err(MakeMoveError::PlacePieceError) + } +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +enum HalfMoveClock { + Reset, + #[default] + Advance, +} + +impl Position { + fn advance_clocks(&mut self, half_move_clock: HalfMoveClock) { + match half_move_clock { + HalfMoveClock::Reset => self.board.half_move_clock = 0, + HalfMoveClock::Advance => self.board.half_move_clock += 1, + } + + self.board.active_color = self.board.active_color.next(); + + if self.board.active_color == Color::White { + self.board.full_move_number += 1; + } + } +} + +impl Position { + fn validate_move(&self, ply: Move, validate: ValidateMove) -> Result<(), MakeMoveError> { + if validate == ValidateMove::No { + return Ok(()); + } + + let origin_square = ply.origin_square(); + let active_piece = self + .board + .get_piece(origin_square) + .ok_or(MakeMoveError::NoPiece(origin_square))?; + + if active_piece.color != self.board.active_color { + return Err(MakeMoveError::NonActiveColor { + piece: active_piece, + square: origin_square, + }); + } + + let target_square = ply.target_square(); + + let movement = active_piece.movement(origin_square, &self.board); + if !movement.contains(target_square) { + return Err(MakeMoveError::NoMove { + piece: active_piece, + origin: origin_square, + target: target_square, + }); + } + + if ply.is_capture() { + let target = ply.target_square(); + if let Some(captured_piece) = self.board.get_piece(target) { + if captured_piece.color == active_piece.color { + return Err(MakeMoveError::InvalidCapture(target)); + } + } else { + return Err(MakeMoveError::NoPiece(target)); + } + } + + Ok(()) + } +} diff --git a/position/src/position/position.rs b/position/src/position/position.rs index 8c34449..e5683bf 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -189,16 +189,6 @@ impl Position { } } -impl Position { - pub fn make_move(&mut self, ply: &Move) -> Result<(), MakeMoveError> { - Ok(()) - } - - pub fn unmake_move(&mut self, ply: &Move) -> Result<(), UnmakeMoveError> { - Ok(()) - } -} - impl Default for Position { fn default() -> Self { Self { diff --git a/position/src/sight.rs b/position/src/sight.rs index bff21d6..82b5c58 100644 --- a/position/src/sight.rs +++ b/position/src/sight.rs @@ -3,10 +3,58 @@ //! Defines routines for computing sight of a piece. Sight is the set of squares //! that a piece can see. In other words, it's the set of squares attacked or //! controled by a piece. +//! +//! These functions use some common terms to describe arguments. +//! +//! occupancy +//! : The set of occupied squares on the board. +//! +//! vacancy +//! : The set of empty squares on the board, `!occpuancy`. +//! +//! blockers +//! : The set of squares occupied by friendly pieces that block moves to that +//! square and beyond. use chessfriend_bitboard::BitBoard; use chessfriend_board::Board; -use chessfriend_core::{Color, Direction, PlacedPiece, Shape, Square}; +use chessfriend_core::{Color, Direction, Piece, Shape, Square}; + +pub trait Sight { + fn sight(&self, square: Square, board: &Board) -> BitBoard; +} + +impl Sight for Piece { + fn sight(&self, square: Square, board: &Board) -> BitBoard { + let occupancy = board.occupancy(); + let info = SightInfo { + square, + occupancy, + friendly_occupancy: board.friendly_occupancy(self.color), + }; + + match self.shape { + Shape::Pawn => { + let en_passant_square: BitBoard = board.en_passant_target.into(); + match self.color { + Color::White => white_pawn_sight(&info, en_passant_square), + Color::Black => black_pawn_sight(&info, en_passant_square), + } + } + Shape::Knight => knight_sight(&info), + Shape::Bishop => bishop_sight(&info), + Shape::Rook => rook_sight(&info), + Shape::Queen => queen_sight(&info), + Shape::King => king_sight(&info), + } + } +} + +struct SightInfo { + square: Square, + occupancy: BitBoard, + friendly_occupancy: BitBoard, +} macro_rules! ray_in_direction { ($square:expr, $blockers:expr, $direction:ident, $first_occupied_square:tt) => {{ @@ -23,287 +71,111 @@ macro_rules! ray_in_direction { } /// Compute sight of a white pawn. -fn _white_pawn_sight( - pawn: BitBoard, - occupancy: BitBoard, - blockers: BitBoard, - en_passant_square: BitBoard, -) -> BitBoard { - let possible_squares = !occupancy | blockers | en_passant_square; +fn white_pawn_sight(info: &SightInfo, en_passant_square: BitBoard) -> BitBoard { + let possible_squares = !info.friendly_occupancy | en_passant_square; + + let pawn: BitBoard = info.square.into(); let pawn = pawn.shift_north_west_one() | pawn.shift_north_east_one(); + pawn & possible_squares } -fn _black_pawn_sight( - pawn: BitBoard, - occupancy: BitBoard, - blockers: BitBoard, - en_passant_square: BitBoard, -) -> BitBoard { - let possible_squares = !occupancy | blockers | en_passant_square; +fn black_pawn_sight(info: &SightInfo, en_passant_square: BitBoard) -> BitBoard { + let possible_squares = !info.friendly_occupancy | en_passant_square; + + let pawn: BitBoard = info.square.into(); let pawn = pawn.shift_south_west_one() | pawn.shift_south_east_one(); + pawn & possible_squares } -fn _knight_sight(knight_square: Square, blockers: BitBoard) -> BitBoard { - BitBoard::knight_moves(knight_square) & !blockers +fn knight_sight(info: &SightInfo) -> BitBoard { + BitBoard::knight_moves(info.square) } -fn _bishop_sight(bishop_square: Square, occupancy: BitBoard) -> BitBoard { +fn bishop_sight(info: &SightInfo) -> BitBoard { + let bishop = info.square; + let occupancy = info.occupancy; + #[rustfmt::skip] - let sight = ray_in_direction!(bishop_square, occupancy, NorthEast, first_occupied_square_trailing) - | ray_in_direction!(bishop_square, occupancy, NorthWest, first_occupied_square_trailing) - | ray_in_direction!(bishop_square, occupancy, SouthEast, first_occupied_square_leading) - | ray_in_direction!(bishop_square, occupancy, SouthWest, first_occupied_square_leading); + let sight = ray_in_direction!(bishop, occupancy, NorthEast, first_occupied_square_trailing) + | ray_in_direction!(bishop, occupancy, SouthEast, first_occupied_square_leading) + | ray_in_direction!(bishop, occupancy, SouthWest, first_occupied_square_leading) + | ray_in_direction!(bishop, occupancy, NorthWest, first_occupied_square_trailing); sight } -fn _rook_sight(rook_square: Square, occupancy: BitBoard) -> BitBoard { +fn rook_sight(info: &SightInfo) -> BitBoard { + let rook = info.square; + let occupancy = info.occupancy; + #[rustfmt::skip] - let sight = ray_in_direction!(rook_square, occupancy, North, first_occupied_square_trailing) - | ray_in_direction!(rook_square, occupancy, East, first_occupied_square_trailing) - | ray_in_direction!(rook_square, occupancy, South, first_occupied_square_leading) - | ray_in_direction!(rook_square, occupancy, West, first_occupied_square_leading); + let sight = ray_in_direction!(rook, occupancy, North, first_occupied_square_trailing) + | ray_in_direction!(rook, occupancy, East, first_occupied_square_trailing) + | ray_in_direction!(rook, occupancy, South, first_occupied_square_leading) + | ray_in_direction!(rook, occupancy, West, first_occupied_square_leading); sight } -fn _queen_sight(queen_square: Square, occupancy: BitBoard) -> BitBoard { +fn queen_sight(info: &SightInfo) -> BitBoard { + let queen = info.square; + let occupancy = info.occupancy; + #[rustfmt::skip] - let sight = ray_in_direction!(queen_square, occupancy, NorthWest, first_occupied_square_trailing) - | ray_in_direction!(queen_square, occupancy, North, first_occupied_square_trailing) - | ray_in_direction!(queen_square, occupancy, NorthEast, first_occupied_square_trailing) - | ray_in_direction!(queen_square, occupancy, East, first_occupied_square_trailing) - | ray_in_direction!(queen_square, occupancy, SouthEast, first_occupied_square_leading) - | ray_in_direction!(queen_square, occupancy, South, first_occupied_square_leading) - | ray_in_direction!(queen_square, occupancy, SouthWest, first_occupied_square_leading) - | ray_in_direction!(queen_square, occupancy, West, first_occupied_square_leading); + let sight = ray_in_direction!(queen, occupancy, NorthWest, first_occupied_square_trailing) + | ray_in_direction!(queen, occupancy, North, first_occupied_square_trailing) + | ray_in_direction!(queen, occupancy, NorthEast, first_occupied_square_trailing) + | ray_in_direction!(queen, occupancy, East, first_occupied_square_trailing) + | ray_in_direction!(queen, occupancy, SouthEast, first_occupied_square_leading) + | ray_in_direction!(queen, occupancy, South, first_occupied_square_leading) + | ray_in_direction!(queen, occupancy, SouthWest, first_occupied_square_leading) + | ray_in_direction!(queen, occupancy, West, first_occupied_square_leading); sight } -fn _king_sight(king_square: Square, blockers: BitBoard) -> BitBoard { - BitBoard::king_moves(king_square) & !blockers -} -pub(crate) trait BishopSightExt { - fn bishop_sight(&self, occupancy: BitBoard) -> BitBoard; -} - -pub(crate) trait KingSightExt { - fn king_sight(&self, board: &Board) -> BitBoard; -} - -pub(crate) trait KnightSightExt { - fn knight_sight(&self, board: &Board) -> BitBoard; -} - -pub(crate) trait PawnSightExt { - fn pawn_sight(&self, board: &Board, en_passant_square: Option) -> BitBoard; - fn white_pawn_sight(&self, board: &Board, en_passant_square: Option) -> BitBoard; - fn black_pawn_sight(&self, board: &Board, en_passant_square: Option) -> BitBoard; -} -pub(crate) trait QueenSightExt { - fn queen_sight(&self, occupancy: BitBoard) -> BitBoard; -} - -pub(crate) trait RookSightExt { - fn rook_sight(&self, occupancy: BitBoard) -> BitBoard; -} - -pub(crate) trait SliderSightExt: BishopSightExt + QueenSightExt + RookSightExt {} - -pub(crate) trait SightExt { - fn sight(&self, board: &Board, en_passant_square: Option) -> BitBoard; -} - -pub(crate) trait SliderRayToSquareExt { - fn ray_to_square(&self, origin: Square, target: Square) -> Option; -} - -impl SightExt for PlacedPiece { - fn sight(&self, board: &Board, en_passant_square: Option) -> BitBoard { - match self.shape() { - Shape::Pawn => match self.color() { - Color::White => self.white_pawn_sight(board, en_passant_square), - Color::Black => self.black_pawn_sight(board, en_passant_square), - }, - Shape::Knight => self.knight_sight(board), - Shape::Bishop => self.bishop_sight(board.pieces.all_pieces()), - Shape::Rook => self.rook_sight(board.pieces.all_pieces()), - Shape::Queen => self.queen_sight(board.pieces.all_pieces()), - Shape::King => self.king_sight(board), - } - } -} - -impl KingSightExt for PlacedPiece { - fn king_sight(&self, board: &Board) -> BitBoard { - _king_sight(self.square, board.pieces.all_pieces_of_color(self.color)) - } -} - -impl KnightSightExt for PlacedPiece { - fn knight_sight(&self, board: &Board) -> BitBoard { - _knight_sight(self.square, board.pieces.all_pieces_of_color(self.color)) - } -} - -impl PawnSightExt for PlacedPiece { - fn pawn_sight(&self, board: &Board, en_passant_square: Option) -> BitBoard { - match self.color { - Color::White => self.white_pawn_sight(board, en_passant_square), - Color::Black => self.black_pawn_sight(board, en_passant_square), - } - } - - fn white_pawn_sight(&self, board: &Board, en_passant_square: Option) -> BitBoard { - let opponent = self.color.other(); - _white_pawn_sight( - self.square.into(), - board.pieces.all_pieces(), - board.pieces.all_pieces_of_color(opponent), - en_passant_square.into(), - ) - } - - fn black_pawn_sight(&self, board: &Board, en_passant_square: Option) -> BitBoard { - let opponent = self.piece.color.other(); - _black_pawn_sight( - self.square.into(), - board.pieces.all_pieces(), - board.pieces.all_pieces_of_color(opponent), - en_passant_square.into(), - ) - } -} - -impl BishopSightExt for PlacedPiece { - fn bishop_sight(&self, occupancy: BitBoard) -> BitBoard { - _bishop_sight(self.square, occupancy) - } -} - -impl RookSightExt for PlacedPiece { - fn rook_sight(&self, occupancy: BitBoard) -> BitBoard { - _rook_sight(self.square, occupancy) - } -} - -impl QueenSightExt for PlacedPiece { - fn queen_sight(&self, occupancy: BitBoard) -> BitBoard { - _queen_sight(self.square, occupancy) - } -} - -impl SliderSightExt for PlacedPiece {} - -impl SliderRayToSquareExt for Shape { - fn ray_to_square(&self, origin: Square, target: Square) -> Option { - macro_rules! ray { - ($square:expr, $direction:ident) => { - ( - BitBoard::ray($square, Direction::$direction), - Direction::$direction, - ) - }; - } - - let target_bitboard: BitBoard = target.into(); - - let ray_and_direction = match self { - Shape::Bishop => [ - ray!(origin, NorthEast), - ray!(origin, SouthEast), - ray!(origin, SouthWest), - ray!(origin, NorthWest), - ] - .into_iter() - .find(|(ray, _)| !(target_bitboard & ray).is_empty()), - Shape::Rook => [ - ray!(origin, North), - ray!(origin, East), - ray!(origin, South), - ray!(origin, West), - ] - .into_iter() - .find(|(ray, _)| !(target_bitboard & ray).is_empty()), - Shape::Queen => [ - ray!(origin, North), - ray!(origin, NorthEast), - ray!(origin, East), - ray!(origin, SouthEast), - ray!(origin, South), - ray!(origin, SouthWest), - ray!(origin, West), - ray!(origin, NorthWest), - ] - .into_iter() - .find(|(ray, _)| !(target_bitboard & ray).is_empty()), - _ => None, - }; - - if let Some((ray, direction)) = ray_and_direction { - let remainder = BitBoard::ray(target, direction); - return Some(ray & !remainder); - } - - None - } -} - -impl BishopSightExt for Square { - fn bishop_sight(&self, occupancy: BitBoard) -> BitBoard { - _bishop_sight(*self, occupancy) - } -} - -impl QueenSightExt for Square { - fn queen_sight(&self, occupancy: BitBoard) -> BitBoard { - _queen_sight(*self, occupancy) - } -} - -impl RookSightExt for Square { - fn rook_sight(&self, occupancy: BitBoard) -> BitBoard { - _rook_sight(*self, occupancy) - } +fn king_sight(info: &SightInfo) -> BitBoard { + BitBoard::king_moves(info.square) } #[cfg(test)] mod tests { - use super::*; use chessfriend_bitboard::bitboard; - use chessfriend_core::{piece, Square}; + use chessfriend_core::piece; macro_rules! sight_test { - ($test_name:ident, $position:expr, $piece:expr, $bitboard:expr) => { + ($test_name:ident, $position:expr, $piece:expr, $square:expr, $bitboard:expr) => { #[test] fn $test_name() { + use chessfriend_core::Square; + use $crate::sight::Sight; + let pos = $position; let piece = $piece; - let sight = pos.sight_of_piece(&piece); + let sight = piece.sight($square, &pos.board); assert_eq!(sight, $bitboard); } }; - ($test_name:ident, $piece:expr, $bitboard:expr) => { - sight_test! {$test_name, $crate::Position::empty(), $piece, $bitboard} + ($test_name:ident, $piece:expr, $square:expr, $bitboard:expr) => { + sight_test! {$test_name, $crate::Position::empty(), $piece, $square, $bitboard} }; } - #[test] - fn pawns_and_knights_cannot_make_rays() { - assert_eq!(Shape::Pawn.ray_to_square(Square::F7, Square::E8), None); - assert_eq!(Shape::Knight.ray_to_square(Square::F6, Square::E8), None); - } + // #[test] + // fn pawns_and_knights_cannot_make_rays() { + // assert_eq!(Shape::Pawn.ray_to_square(Square::F7, Square::E8), None); + // assert_eq!(Shape::Knight.ray_to_square(Square::F6, Square::E8), None); + // } mod pawn { - use crate::test_position; + use crate::{sight::Sight, test_position}; use chessfriend_bitboard::{bitboard, BitBoard}; - use chessfriend_core::piece; + use chessfriend_core::{piece, Square}; - sight_test!(e4_pawn, piece!(White Pawn on E4), bitboard![D5 F5]); + sight_test!(e4_pawn, piece!(White Pawn), Square::E4, bitboard![D5 F5]); sight_test!( e4_pawn_one_blocker, @@ -311,7 +183,8 @@ mod tests { White Bishop on D5, White Pawn on E4, ], - piece!(White Pawn on E4), + piece!(White Pawn), + Square::E4, bitboard!(F5) ); @@ -323,8 +196,8 @@ mod tests { White Pawn on E4, ); - let piece = piece!(White Pawn on E4); - let sight = pos.sight_of_piece(&piece); + let piece = piece!(White Pawn); + let sight = piece.sight(Square::E4, &pos.board); assert_eq!(sight, BitBoard::empty()); } @@ -337,10 +210,10 @@ mod tests { White Pawn on E4, ); - let piece = piece!(White Pawn on E4); - let sight = pos.sight_of_piece(&piece); + let piece = piece!(White Pawn); + let sight = piece.sight(Square::E4, &pos.board); - assert_eq!(sight, bitboard!(D5)); + assert_eq!(sight, bitboard![D5]); } #[test] @@ -349,8 +222,8 @@ mod tests { White Pawn on E5, Black Pawn on D5, ], D6); - let piece = piece!(White Pawn on E5); - let sight = pos.sight_of_piece(&piece); + let piece = piece!(White Pawn); + let sight = piece.sight(Square::E5, &pos.board); assert_eq!(sight, bitboard!(D6 F6)); } @@ -363,7 +236,8 @@ mod tests { sight_test!( f6_knight, - piece!(Black Knight on F6), + piece!(Black Knight), + Square::F6, bitboard![H7 G8 E8 D7 D5 E4 G4 H5] ); } @@ -373,16 +247,17 @@ mod tests { sight_test!( c2_bishop, - piece!(Black Bishop on C2), + piece!(Black Bishop), + Square::C2, bitboard![D1 B3 A4 B1 D3 E4 F5 G6 H7] ); - #[test] - fn ray_to_square() { - let generated_ray = Shape::Bishop.ray_to_square(Square::C5, Square::E7); - let expected_ray = bitboard![D6 E7]; - assert_eq!(generated_ray, Some(expected_ray)); - } + // #[test] + // fn ray_to_square() { + // let generated_ray = Shape::Bishop.ray_to_square(Square::C5, Square::E7); + // let expected_ray = bitboard![D6 E7]; + // assert_eq!(generated_ray, Some(expected_ray)); + // } } mod rook { @@ -391,7 +266,8 @@ mod tests { sight_test!( g3_rook, - piece!(White Rook on G3), + piece!(White Rook), + Square::G3, bitboard![G1 G2 G4 G5 G6 G7 G8 A3 B3 C3 D3 E3 F3 H3] ); @@ -399,37 +275,43 @@ mod tests { e4_rook_with_e1_white_king_e7_black_king, test_position![ White Rook on E4, - White King on E1, + White King on E2, Black King on E7, ], - piece!(White Rook on E4), - bitboard![A4 B4 C4 D4 F4 G4 H4 E2 E3 E5 E6 E7 E1] + piece!(White Rook), + Square::E4, + bitboard![A4 B4 C4 D4 F4 G4 H4 E2 E3 E5 E6 E7] ); - #[test] - fn ray_to_square() { - let generated_ray = Shape::Rook.ray_to_square(Square::C2, Square::C6); - let expected_ray = bitboard![C3 C4 C5 C6]; - assert_eq!(generated_ray, Some(expected_ray)); + // #[test] + // fn ray_to_square() { + // let generated_ray = Shape::Rook.ray_to_square(Square::C2, Square::C6); + // let expected_ray = bitboard![C3 C4 C5 C6]; + // assert_eq!(generated_ray, Some(expected_ray)); - let generated_ray = Shape::Rook.ray_to_square(Square::D2, Square::H2); - let expected_ray = bitboard![E2 F2 G2 H2]; - assert_eq!(generated_ray, Some(expected_ray)); + // let generated_ray = Shape::Rook.ray_to_square(Square::D2, Square::H2); + // let expected_ray = bitboard![E2 F2 G2 H2]; + // assert_eq!(generated_ray, Some(expected_ray)); - let generated_ray = Shape::Rook.ray_to_square(Square::G6, Square::B6); - let expected_ray = bitboard![B6 C6 D6 E6 F6]; - assert_eq!(generated_ray, Some(expected_ray)); + // let generated_ray = Shape::Rook.ray_to_square(Square::G6, Square::B6); + // let expected_ray = bitboard![B6 C6 D6 E6 F6]; + // assert_eq!(generated_ray, Some(expected_ray)); - let generated_ray = Shape::Rook.ray_to_square(Square::A6, Square::A3); - let expected_ray = bitboard![A5 A4 A3]; - assert_eq!(generated_ray, Some(expected_ray)); - } + // let generated_ray = Shape::Rook.ray_to_square(Square::A6, Square::A3); + // let expected_ray = bitboard![A5 A4 A3]; + // assert_eq!(generated_ray, Some(expected_ray)); + // } } mod king { use chessfriend_bitboard::bitboard; use chessfriend_core::piece; - sight_test!(e1_king, piece!(White King on E1), bitboard![D1 D2 E2 F2 F1]); + sight_test!( + e1_king, + piece!(White King), + Square::E1, + bitboard![D1 D2 E2 F2 F1] + ); } } From 539b1fca6e4a7f676af2956e0bab90fa440fdb6b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 19 May 2025 08:28:23 -0700 Subject: [PATCH 264/423] [expolorer] Add two new commands for showing available moves and sight of a piece on a square --- explorer/src/main.rs | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/explorer/src/main.rs b/explorer/src/main.rs index 5b8a17d..954007e 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -54,6 +54,16 @@ fn command_line() -> Command { .arg(Arg::new("square").required(true)) .about("Place a piece on the board"), ) + .subcommand( + Command::new("sight") + .arg(Arg::new("square").required(true)) + .about("Show sight of a piece on a square"), + ) + .subcommand( + Command::new("moves") + .arg(Arg::new("square").required(true)) + .about("Show moves of a piece on a square"), + ) .subcommand(Command::new("print").about("Print the board")) .subcommand(Command::new("quit").alias("exit").about("Quit the program")) .subcommand(Command::new("starting").about("Reset the board to the starting position")) @@ -134,6 +144,34 @@ fn respond(line: &str, state: &mut State) -> Result { state.builder.place_piece(piece); state.position = state.builder.build(); } + Some(("sight", matches)) => { + let square = matches + .get_one::("square") + .ok_or("Missing square")?; + let square = Square::from_algebraic_str(square) + .map_err(|_| "Error: invalid square specifier")?; + + let sight = state.position.sight(square); + + let display = state.position.display().highlight(sight); + println!("\n{display}"); + + result.should_print_position = false; + } + Some(("moves", matches)) => { + let square = matches + .get_one::("square") + .ok_or("Missing square")?; + let square = Square::from_algebraic_str(square) + .map_err(|_| "Error: invalid square specifier")?; + + let movement = state.position.movement(square); + + let display = state.position.display().highlight(movement); + println!("\n{display}"); + + result.should_print_position = false; + } Some(("starting", _matches)) => { let starting_position = Position::starting(); state.builder = PositionBuilder::from_position(&starting_position); From b229049e2789191baeef942a467df9fe480c56b1 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 19 May 2025 08:34:32 -0700 Subject: [PATCH 265/423] [board, core] Update error types to use thiserror::Error --- board/Cargo.toml | 1 + board/src/fen.rs | 30 +++++++++++++++++---- board/src/lib.rs | 1 + board/src/piece_sets.rs | 6 +++-- core/Cargo.toml | 1 + core/src/coordinates.rs | 59 +++++++++++++++++++++++++++++++++++------ 6 files changed, 83 insertions(+), 15 deletions(-) diff --git a/board/Cargo.toml b/board/Cargo.toml index 54a0dd4..098d764 100644 --- a/board/Cargo.toml +++ b/board/Cargo.toml @@ -8,3 +8,4 @@ edition = "2021" [dependencies] chessfriend_bitboard = { path = "../bitboard" } chessfriend_core = { path = "../core" } +thiserror = "2" diff --git a/board/src/fen.rs b/board/src/fen.rs index 6fe1c08..4221f1f 100644 --- a/board/src/fen.rs +++ b/board/src/fen.rs @@ -5,6 +5,7 @@ use chessfriend_core::{ coordinates::ParseSquareError, piece, Color, File, Piece, PlacedPiece, Rank, Square, }; use std::fmt::Write; +use thiserror::Error; #[macro_export] macro_rules! fen { @@ -13,18 +14,24 @@ macro_rules! fen { }; } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Error, Eq, PartialEq)] pub enum ToFenStrError { - FmtError(std::fmt::Error), + #[error("{0}")] + FmtError(#[from] std::fmt::Error), } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Error, Eq, PartialEq)] pub enum FromFenStrError { + #[error("missing {0} field")] MissingField(Field), + #[error("missing piece placement")] MissingPlacement, + #[error("invalid value")] InvalidValue, - ParseIntError(std::num::ParseIntError), - ParseSquareError(ParseSquareError), + #[error("{0}")] + ParseIntError(#[from] std::num::ParseIntError), + #[error("{0}")] + ParseSquareError(#[from] ParseSquareError), } #[derive(Clone, Debug, Eq, PartialEq)] @@ -37,6 +44,19 @@ pub enum Field { FullMoveCounter, } +impl std::fmt::Display for Field { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Field::Placements => write!(f, "Placements"), + Field::PlayerToMove => write!(f, "Player To Move"), + Field::CastlingRights => write!(f, "Castling Rights"), + Field::EnPassantSquare => write!(f, "En Passant Square"), + Field::HalfMoveClock => write!(f, "Half Move Clock"), + Field::FullMoveCounter => write!(f, "Full move Counter"), + } + } +} + pub trait ToFenStr { type Error; diff --git a/board/src/lib.rs b/board/src/lib.rs index f6c8adb..da05df2 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -10,6 +10,7 @@ mod board; mod piece_sets; pub use board::Board; +pub use piece_sets::{PlacePieceError, PlacePieceStrategy}; use castle::Castle; use en_passant::EnPassant; diff --git a/board/src/piece_sets.rs b/board/src/piece_sets.rs index ff66b98..02a7152 100644 --- a/board/src/piece_sets.rs +++ b/board/src/piece_sets.rs @@ -6,6 +6,7 @@ use self::mailbox::Mailbox; use chessfriend_bitboard::{BitBoard, IterationDirection}; use chessfriend_core::{Color, Piece, Shape, Square}; use std::ops::BitOr; +use thiserror::Error; #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum PlacePieceStrategy { @@ -14,9 +15,10 @@ pub enum PlacePieceStrategy { PreserveExisting, } -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Error, Eq, PartialEq)] pub enum PlacePieceError { - ExisitingPiece(PlacedPiece), + #[error("cannot place piece on {square} with existing {piece}")] + ExisitingPiece { piece: Piece, square: Square }, } /// The internal data structure of a [Board] that efficiently manages the diff --git a/core/Cargo.toml b/core/Cargo.toml index 9b33128..0905978 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -6,3 +6,4 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +thiserror = "2" diff --git a/core/src/coordinates.rs b/core/src/coordinates.rs index 9bdfc23..89dfde9 100644 --- a/core/src/coordinates.rs +++ b/core/src/coordinates.rs @@ -2,6 +2,7 @@ use crate::Color; use std::{fmt, str::FromStr}; +use thiserror::Error; macro_rules! try_from_integer { ($type:ident, $int_type:ident) => { @@ -334,8 +335,14 @@ impl Square { } } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct ParseSquareError; +#[derive(Clone, Debug, Error, Eq, PartialEq)] +pub enum ParseSquareError { + #[error("{0}")] + RankError(#[from] ParseRankError), + + #[error("{0}")] + FileError(#[from] ParseFileError), +} impl FromStr for Square { type Err = ParseSquareError; @@ -346,21 +353,57 @@ impl FromStr for Square { let file: File = chars .next() .and_then(|c| c.try_into().ok()) - .ok_or(ParseSquareError)?; + .ok_or(ParseSquareError::FileError(ParseFileError))?; let rank: Rank = chars .next() .and_then(|c| c.try_into().ok()) - .ok_or(ParseSquareError)?; - - if chars.next().is_some() { - return Err(ParseSquareError); - } + .ok_or(ParseSquareError::RankError(ParseRankError))?; Ok(Square::from_file_rank(file, rank)) } } +#[derive(Clone, Debug, Error, Eq, PartialEq)] +#[error("invalid rank")] +pub struct ParseRankError; + +impl std::str::FromStr for Rank { + type Err = ParseRankError; + + fn from_str(s: &str) -> Result { + let ch = s + .chars() + .nth(0) + .ok_or(ParseRankError) + .map(|ch| ch.to_ascii_lowercase())?; + let offset = 'a' as usize - (ch as usize); + let rank = Rank::ALL[offset]; + + Ok(rank) + } +} + +#[derive(Clone, Debug, Error, Eq, PartialEq)] +#[error("invalid file")] +pub struct ParseFileError; + +impl std::str::FromStr for File { + type Err = ParseFileError; + + fn from_str(s: &str) -> Result { + let ch = s + .chars() + .nth(0) + .ok_or(ParseFileError) + .map(|ch| ch.to_ascii_lowercase())?; + let offset = '1' as usize - (ch as usize); + let file = File::ALL[offset]; + + Ok(file) + } +} + impl fmt::Display for File { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", Into::::into(*self)) From cd3efa61c942ee37906c126f449fd2502e5c0c1b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 19 May 2025 08:35:35 -0700 Subject: [PATCH 266/423] [core] Fix the coordinate tests Use symbols instead of magic numbers. --- core/src/coordinates.rs | 46 +++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/core/src/coordinates.rs b/core/src/coordinates.rs index 89dfde9..0a01ba4 100644 --- a/core/src/coordinates.rs +++ b/core/src/coordinates.rs @@ -433,7 +433,7 @@ impl From for char { impl From for char { fn from(value: Rank) -> Self { - Self::from(value.0) + Self::from(value.0 + b'1') } } @@ -473,41 +473,39 @@ mod tests { #[test] fn good_algebraic_input() -> Result<(), ParseSquareError> { let sq = Square::from_algebraic_str("a4")?; - assert_eq!(sq.file(), File(0)); - assert_eq!(sq.rank(), Rank(3)); + assert_eq!(sq.file(), File::A); + assert_eq!(sq.rank(), Rank::FOUR); let sq = Square::from_algebraic_str("B8")?; - assert_eq!(sq.file(), File(1)); - assert_eq!(sq.rank(), Rank(7)); + assert_eq!(sq.file(), File::B); + assert_eq!(sq.rank(), Rank::EIGHT); let sq = Square::from_algebraic_str("e4")?; - assert_eq!(sq.file(), File(4)); - assert_eq!(sq.rank(), Rank(3)); + assert_eq!(sq.file(), File::E); + assert_eq!(sq.rank(), Rank::FOUR); Ok(()) } #[test] - fn bad_algebraic_input() -> Result<(), ParseSquareError> { - Square::from_algebraic_str("a0")?; - Square::from_algebraic_str("j3")?; - Square::from_algebraic_str("a11")?; - Square::from_algebraic_str("b-1")?; - Square::from_algebraic_str("a 1")?; - Square::from_algebraic_str("")?; - - Ok(()) + fn bad_algebraic_input() { + assert!(Square::from_algebraic_str("a0").is_err()); + assert!(Square::from_algebraic_str("j3").is_err()); + assert!(Square::from_algebraic_str("a11").is_err()); + assert!(Square::from_algebraic_str("b-1").is_err()); + assert!(Square::from_algebraic_str("a 1").is_err()); + assert!(Square::from_algebraic_str("").is_err()); } #[test] fn from_index() -> Result<(), ()> { let sq = Square::try_from(4u32)?; - assert_eq!(sq.file(), File(4)); - assert_eq!(sq.rank(), Rank(0)); + assert_eq!(sq.file(), File::E); + assert_eq!(sq.rank(), Rank::ONE); let sq = Square::try_from(28u32)?; - assert_eq!(sq.file(), File(4)); - assert_eq!(sq.rank(), Rank(3)); + assert_eq!(sq.file(), File::E); + assert_eq!(sq.rank(), Rank::FOUR); Ok(()) } @@ -560,5 +558,13 @@ mod tests { assert_eq!(format!("{}", Square::C5), "c5"); assert_eq!(format!("{}", Square::A1), "a1"); assert_eq!(format!("{}", Square::H8), "h8"); + assert_eq!(format!("{}", Rank::FIVE), "5"); + assert_eq!(format!("{}", File::H), "h"); + } + + #[test] + fn into_char() { + assert_eq!(Into::::into(Rank::ONE), '1'); + assert_eq!(Into::::into(File::B), 'b'); } } From c7be0e3e2b47922a3a9dde3c6e4db63fe1b6d43e Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 19 May 2025 08:35:56 -0700 Subject: [PATCH 267/423] [board] Remove the PlacedPiece import from fen.rs --- board/src/fen.rs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/board/src/fen.rs b/board/src/fen.rs index 4221f1f..00f543a 100644 --- a/board/src/fen.rs +++ b/board/src/fen.rs @@ -1,9 +1,7 @@ // Eryn Wells use crate::{piece_sets::PlacePieceStrategy, Board, Castle}; -use chessfriend_core::{ - coordinates::ParseSquareError, piece, Color, File, Piece, PlacedPiece, Rank, Square, -}; +use chessfriend_core::{coordinates::ParseSquareError, piece, Color, File, Piece, Rank, Square}; use std::fmt::Write; use thiserror::Error; @@ -181,14 +179,6 @@ impl ToFenStr for Piece { } } -impl ToFenStr for PlacedPiece { - type Error = ToFenStrError; - - fn to_fen_str(&self) -> Result { - self.piece().to_fen_str() - } -} - impl FromFenStr for Board { type Error = FromFenStrError; From 67448b44d7d552acfc99673a62caf4927f475769 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 19 May 2025 08:36:44 -0700 Subject: [PATCH 268/423] [board] Clean up variables names in piece_sets.rs so creating an error is a little more succinct --- board/src/piece_sets.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/board/src/piece_sets.rs b/board/src/piece_sets.rs index 02a7152..2371d45 100644 --- a/board/src/piece_sets.rs +++ b/board/src/piece_sets.rs @@ -92,11 +92,8 @@ impl PieceSet { strategy: PlacePieceStrategy, ) -> Result<(), PlacePieceError> { if strategy == PlacePieceStrategy::PreserveExisting { - if let Some(existing_piece) = self.mailbox.get(square) { - return Err(PlacePieceError::ExisitingPiece(PlacedPiece::new( - existing_piece, - square, - ))); + if let Some(piece) = self.mailbox.get(square) { + return Err(PlacePieceError::ExisitingPiece { piece, square }); } } From 6e0e33b5f9dc4cb34b25d8ac5ce1a5a2fcf54980 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 19 May 2025 08:37:04 -0700 Subject: [PATCH 269/423] [board] Remove the FromIterator impl for PieceSet --- board/src/piece_sets.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/board/src/piece_sets.rs b/board/src/piece_sets.rs index 2371d45..e974064 100644 --- a/board/src/piece_sets.rs +++ b/board/src/piece_sets.rs @@ -132,18 +132,6 @@ impl PieceSet { } } -impl FromIterator for PieceSet { - fn from_iter>(iter: T) -> Self { - let mut pieces: Self = Self::default(); - - for piece in iter { - let _ = pieces.place(piece.piece, piece.square, PlacePieceStrategy::default()); - } - - pieces - } -} - #[cfg(test)] mod tests { use super::*; From 00c4aa38f0f43490a2fe62df273b0116375b1e2c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 19 May 2025 08:38:52 -0700 Subject: [PATCH 270/423] [explorer] make command no longer requires specifying a piece --- explorer/src/main.rs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/explorer/src/main.rs b/explorer/src/main.rs index 954007e..9be160a 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -42,7 +42,6 @@ fn command_line() -> Command { .subcommand(Command::new("fen").about("Print the current position as a FEN string")) .subcommand( Command::new("make") - .arg(Arg::new("piece").required(true)) .arg(Arg::new("from").required(true)) .arg(Arg::new("to").required(true)) .about("Make a move"), @@ -95,11 +94,6 @@ fn respond(line: &str, state: &mut State) -> Result { result.should_print_position = false; } Some(("make", matches)) => { - let shape = matches - .get_one::("piece") - .ok_or("Missing piece descriptor")?; - let shape = Shape::try_from(shape).map_err(|_| "Invalid piece descriptor")?; - let from_square = Square::from_algebraic_str( matches.get_one::("from").ok_or("Missing square")?, ) @@ -110,17 +104,16 @@ fn respond(line: &str, state: &mut State) -> Result { ) .map_err(|_| "Error: invalid square specifier")?; - let mv = MoveBuilder::new() + let ply = MoveBuilder::new() .from(from_square) .to(to_square) .build() - .map_err(|err| format!("Error: cannot build move: {:?}", err))?; + .map_err(|err| format!("Error: {err:?}"))?; - state.position = MakeMoveBuilder::new(&state.position) - .make(&mv) - .map_err(|err| format!("Error: cannot make move: {:?}", err))? - .build(); - state.builder = PositionBuilder::from_position(&state.position); + state + .position + .make_move(ply, ValidateMove::Yes) + .map_err(|err| format!("Error: {err}"))?; } Some(("place", matches)) => { let color = matches From d67c2cfb99e4b840f0d80d836a99486c9c734f4a Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 19 May 2025 08:41:48 -0700 Subject: [PATCH 271/423] [explorer] A bunch of random changes to this binary Too many changes mixed up together to tease apart. --- explorer/src/main.rs | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/explorer/src/main.rs b/explorer/src/main.rs index 9be160a..c37e6a4 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -1,8 +1,9 @@ // Eryn Wells -use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; +use chessfriend_board::{fen::FromFenStr, Board}; +use chessfriend_core::{Color, Piece, Shape, Square}; use chessfriend_moves::Builder as MoveBuilder; -use chessfriend_position::{fen::ToFen, MakeMoveBuilder, Position, PositionBuilder}; +use chessfriend_position::{fen::ToFenStr, PlacePieceStrategy, Position, ValidateMove}; use clap::{Arg, Command}; use rustyline::error::ReadlineError; use rustyline::DefaultEditor; @@ -22,7 +23,6 @@ impl Default for CommandResult { } struct State { - builder: PositionBuilder, position: Position, } @@ -87,7 +87,7 @@ fn respond(line: &str, state: &mut State) -> Result { "{}", state .position - .to_fen() + .to_fen_str() .map_err(|_| "error: Unable to generate FEN for current position")? ); @@ -132,10 +132,12 @@ fn respond(line: &str, state: &mut State) -> Result { let square = Square::from_algebraic_str(square) .map_err(|_| "Error: invalid square specifier")?; - let piece = PlacedPiece::new(Piece::new(color, shape), square); + let piece = Piece::new(color, shape); - state.builder.place_piece(piece); - state.position = state.builder.build(); + state + .position + .place_piece(piece, square, PlacePieceStrategy::default()) + .map_err(|err| format!("Error: could not place piece: {err:?}"))?; } Some(("sight", matches)) => { let square = matches @@ -167,7 +169,6 @@ fn respond(line: &str, state: &mut State) -> Result { } Some(("starting", _matches)) => { let starting_position = Position::starting(); - state.builder = PositionBuilder::from_position(&starting_position); state.position = starting_position; } Some((name, _matches)) => unimplemented!("{name}"), @@ -178,12 +179,10 @@ fn respond(line: &str, state: &mut State) -> Result { } fn main() -> Result<(), String> { - let mut editor = DefaultEditor::new().map_err(|err| format!("Error: {}", err.to_string()))?; + let mut editor = DefaultEditor::new().map_err(|err| format!("Error: {err}"))?; let starting_position = Position::starting(); - let builder = PositionBuilder::from_position(&starting_position); let mut state = State { - builder, position: starting_position, }; @@ -192,7 +191,7 @@ fn main() -> Result<(), String> { loop { if should_print_position { println!("{}", &state.position); - println!("{} to move.", state.position.player_to_move()); + println!("{} to move.", state.position.board.active_color); } let readline = editor.readline("\n? "); @@ -210,7 +209,7 @@ fn main() -> Result<(), String> { break; } } - Err(message) => println!("{}", message), + Err(message) => println!("{message}"), } } Err(ReadlineError::Interrupted) => { @@ -222,7 +221,7 @@ fn main() -> Result<(), String> { break; } Err(err) => { - println!("Error: {:?}", err); + println!("Error: {err}"); break; } } From 39ca74459d762a0f56b72cf462a108ba7fa2fb3f Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 19 May 2025 08:42:34 -0700 Subject: [PATCH 272/423] [explorer] Implement a `reset` command Resets the board to an empty or starting state, or to a position specified by a FEN string. --- explorer/src/main.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/explorer/src/main.rs b/explorer/src/main.rs index c37e6a4..bf688d1 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -63,6 +63,17 @@ fn command_line() -> Command { .arg(Arg::new("square").required(true)) .about("Show moves of a piece on a square"), ) + .subcommand( + Command::new("reset") + .subcommand(Command::new("clear").about("Reset to a cleared board")) + .subcommand(Command::new("starting").about("Reset to the starting position")) + .subcommand( + Command::new("fen") + .arg(Arg::new("fen").required(true)) + .about("Reset to a position described by a FEN string"), + ) + .about("Reset the board"), + ) .subcommand(Command::new("print").about("Print the board")) .subcommand(Command::new("quit").alias("exit").about("Quit the program")) .subcommand(Command::new("starting").about("Reset the board to the starting position")) @@ -171,6 +182,7 @@ fn respond(line: &str, state: &mut State) -> Result { let starting_position = Position::starting(); state.position = starting_position; } + Some(("reset", matches)) => do_reset_command(state, matches)?, Some((name, _matches)) => unimplemented!("{name}"), None => unreachable!("Subcommand required"), } @@ -178,6 +190,21 @@ fn respond(line: &str, state: &mut State) -> Result { Ok(result) } +fn do_reset_command(state: &mut State, matches: &clap::ArgMatches) -> Result<(), String> { + match matches.subcommand() { + None | Some(("clear", _)) => state.position = Position::empty(), + Some(("starting", _)) => state.position = Position::starting(), + Some(("fen", matches)) => { + let fen = matches.get_one::("fen").ok_or("Missing FEN")?; + let board = Board::from_fen_str(fen).map_err(|err| format!("{err}"))?; + state.position = Position::new(board); + } + Some((name, _matches)) => unimplemented!("{name}"), + } + + Ok(()) +} + fn main() -> Result<(), String> { let mut editor = DefaultEditor::new().map_err(|err| format!("Error: {err}"))?; From 72eeba84ba4be882b71d37ef9ae7c9b54f946064 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 19 May 2025 08:42:53 -0700 Subject: [PATCH 273/423] [explorer] Specify the chessfriend_board dependency --- explorer/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/explorer/Cargo.toml b/explorer/Cargo.toml index 9689332..a81f966 100644 --- a/explorer/Cargo.toml +++ b/explorer/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] chessfriend_core = { path = "../core" } +chessfriend_board = { path = "../board" } chessfriend_moves = { path = "../moves" } chessfriend_position = { path = "../position" } clap = { version = "4.4.12", features = ["derive"] } From 9010f1e9c24599eb1e5a372bc2f4f6f1cc1eb5ca Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 19 May 2025 14:18:31 -0700 Subject: [PATCH 274/423] [explorer, moves, core] Improve error handling in explorer Implement thiserror::Error for a bunch of error types, and remove string errors from the implementation of the command handler in explorer. Clean up parsing of basic types all over the place. Update Cargo files to include thiserror and anyhow. --- Cargo.lock | 8 +++ core/src/colors.rs | 52 +++++++++++--- core/src/coordinates.rs | 60 +++++++++++----- core/src/errors.rs | 7 -- core/src/lib.rs | 5 +- core/src/macros.rs | 21 ------ core/src/pieces.rs | 137 ++++--------------------------------- core/src/pieces/display.rs | 28 ++++++++ core/src/shapes.rs | 137 +++++++++++++++++++++++++++++++++++++ explorer/Cargo.toml | 2 + explorer/src/main.rs | 93 ++++++++++++------------- moves/src/builder.rs | 7 +- 12 files changed, 331 insertions(+), 226 deletions(-) delete mode 100644 core/src/errors.rs create mode 100644 core/src/pieces/display.rs create mode 100644 core/src/shapes.rs diff --git a/Cargo.lock b/Cargo.lock index 7414d4e..238efc3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,6 +50,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + [[package]] name = "bitflags" version = "2.4.2" @@ -188,6 +194,7 @@ checksum = "281e452d3bad4005426416cdba5ccfd4f5c1280e10099e21db27f7c1c28347fc" name = "explorer" version = "0.1.0" dependencies = [ + "anyhow", "chessfriend_board", "chessfriend_core", "chessfriend_moves", @@ -195,6 +202,7 @@ dependencies = [ "clap", "rustyline", "shlex", + "thiserror", ] [[package]] diff --git a/core/src/colors.rs b/core/src/colors.rs index fee6558..5cf633b 100644 --- a/core/src/colors.rs +++ b/core/src/colors.rs @@ -1,7 +1,7 @@ // Eryn Wells -use crate::{errors::TryFromCharError, try_from_string, Direction}; -use std::fmt; +use crate::Direction; +use thiserror::Error; #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] pub enum Color { @@ -48,31 +48,65 @@ impl Color { pub const fn next(&self) -> Color { Self::ALL[((*self as usize) + 1) % Self::NUM] } + + #[must_use] + pub const fn name(self) -> &'static str { + match self { + Color::White => "white", + Color::Black => "black", + } + } } -impl fmt::Display for Color { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl std::fmt::Display for Color { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{}", match self { - Color::White => "White", - Color::Black => "Black", + Color::White => "white", + Color::Black => "black", }, ) } } +#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)] +#[error("no matching color for character '{0}'")] +pub struct ColorFromCharError(char); + impl TryFrom for Color { - type Error = TryFromCharError; + type Error = ColorFromCharError; fn try_from(value: char) -> Result { match value { 'w' | 'W' => Ok(Color::White), 'b' | 'B' => Ok(Color::Black), - _ => Err(TryFromCharError), + _ => Err(ColorFromCharError(value)), } } } -try_from_string!(Color); +#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)] +#[error("no matching color for string")] +pub struct ColorFromStrError; + +impl TryFrom<&str> for Color { + type Error = ColorFromStrError; + + fn try_from(value: &str) -> Result { + match value { + "w" | "white" => Ok(Color::White), + "b" | "black" => Ok(Color::Black), + _ => Err(ColorFromStrError), + } + } +} + +impl std::str::FromStr for Color { + type Err = ColorFromStrError; + + fn from_str(s: &str) -> Result { + Self::try_from(s.to_lowercase().as_str()) + } +} diff --git a/core/src/coordinates.rs b/core/src/coordinates.rs index 0a01ba4..1a13ced 100644 --- a/core/src/coordinates.rs +++ b/core/src/coordinates.rs @@ -1,7 +1,7 @@ // Eryn Wells use crate::Color; -use std::{fmt, str::FromStr}; +use std::fmt; use thiserror::Error; macro_rules! try_from_integer { @@ -10,6 +10,7 @@ macro_rules! try_from_integer { type Error = (); fn try_from(value: $int_type) -> Result { + #[allow(clippy::cast_possible_truncation)] Self::try_from(value as u8) } } @@ -55,6 +56,7 @@ macro_rules! range_bound_struct { #[allow(dead_code)] impl $type { + $vis const NUM: usize = $max; $vis const FIRST: $type = $type(0); $vis const LAST: $type = $type($max - 1); } @@ -148,7 +150,7 @@ impl File { pub const G: File = File(6); pub const H: File = File(7); - pub const ALL: [File; 8] = [ + pub const ALL: [File; File::NUM] = [ File::A, File::B, File::C, @@ -173,7 +175,7 @@ impl Rank { pub const SEVEN: Rank = Rank(6); pub const EIGHT: Rank = Rank(7); - pub const ALL: [Rank; 8] = [ + pub const ALL: [Rank; Self::NUM] = [ Rank::ONE, Rank::TWO, Rank::THREE, @@ -344,7 +346,27 @@ pub enum ParseSquareError { FileError(#[from] ParseFileError), } -impl FromStr for Square { +impl TryFrom<&str> for Square { + type Error = ParseSquareError; + + fn try_from(value: &str) -> Result { + let mut chars = value.chars(); + + let file: File = chars + .next() + .and_then(|c| c.try_into().ok()) + .ok_or(ParseSquareError::FileError(ParseFileError))?; + + let rank: Rank = chars + .next() + .and_then(|c| c.try_into().ok()) + .ok_or(ParseSquareError::RankError(ParseRankError))?; + + Ok(Square::from_file_rank(file, rank)) + } +} + +impl std::str::FromStr for Square { type Err = ParseSquareError; fn from_str(s: &str) -> Result { @@ -377,10 +399,13 @@ impl std::str::FromStr for Rank { .nth(0) .ok_or(ParseRankError) .map(|ch| ch.to_ascii_lowercase())?; - let offset = 'a' as usize - (ch as usize); - let rank = Rank::ALL[offset]; - Ok(rank) + let offset = 'a' as usize - (ch as usize); + if offset >= Rank::ALL.len() { + return Err(ParseRankError); + } + + Ok(Rank::ALL[offset]) } } @@ -397,10 +422,13 @@ impl std::str::FromStr for File { .nth(0) .ok_or(ParseFileError) .map(|ch| ch.to_ascii_lowercase())?; - let offset = '1' as usize - (ch as usize); - let file = File::ALL[offset]; - Ok(file) + let offset = '1' as usize - (ch as usize); + if offset >= File::ALL.len() { + return Err(ParseFileError); + } + + Ok(File::ALL[offset]) } } @@ -489,12 +517,12 @@ mod tests { #[test] fn bad_algebraic_input() { - assert!(Square::from_algebraic_str("a0").is_err()); - assert!(Square::from_algebraic_str("j3").is_err()); - assert!(Square::from_algebraic_str("a11").is_err()); - assert!(Square::from_algebraic_str("b-1").is_err()); - assert!(Square::from_algebraic_str("a 1").is_err()); - assert!(Square::from_algebraic_str("").is_err()); + assert!("a0".parse::().is_err()); + assert!("j3".parse::().is_err()); + assert!("a11".parse::().is_err()); + assert!("b-1".parse::().is_err()); + assert!("a 1".parse::().is_err()); + assert!("".parse::().is_err()); } #[test] diff --git a/core/src/errors.rs b/core/src/errors.rs deleted file mode 100644 index c897997..0000000 --- a/core/src/errors.rs +++ /dev/null @@ -1,7 +0,0 @@ -// Eryn Wells - -#[derive(Debug, Eq, PartialEq)] -pub struct TryFromCharError; - -#[derive(Debug, Eq, PartialEq)] -pub struct TryFromStrError; diff --git a/core/src/lib.rs b/core/src/lib.rs index fc3dfbc..4d54037 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -2,11 +2,12 @@ pub mod colors; pub mod coordinates; -pub mod errors; pub mod pieces; +pub mod shapes; mod macros; pub use colors::Color; pub use coordinates::{Direction, File, Rank, Square}; -pub use pieces::{Piece, PlacedPiece, Shape}; +pub use pieces::{Piece, PlacedPiece}; +pub use shapes::Shape; diff --git a/core/src/macros.rs b/core/src/macros.rs index 9cce129..dc99d9a 100644 --- a/core/src/macros.rs +++ b/core/src/macros.rs @@ -1,26 +1,5 @@ // Eryn Wells -#[macro_export] -macro_rules! try_from_string { - ($type:ty) => { - try_from_string!($type, &str); - try_from_string!($type, &String); - }; - ($type:ty, $from_type:ty) => { - impl TryFrom<$from_type> for $type { - type Error = $crate::errors::TryFromStrError; - - fn try_from(value: $from_type) -> Result { - let first_char = value - .chars() - .nth(0) - .ok_or($crate::errors::TryFromStrError)?; - Self::try_from(first_char).map_err(|_| $crate::errors::TryFromStrError) - } - } - }; -} - #[macro_export] macro_rules! piece { ($color:ident $shape:ident) => { diff --git a/core/src/pieces.rs b/core/src/pieces.rs index dd6de93..a580967 100644 --- a/core/src/pieces.rs +++ b/core/src/pieces.rs @@ -1,124 +1,10 @@ // Eryn Wells -use crate::{errors::TryFromCharError, try_from_string, Color, Square}; -use std::{array, fmt, slice}; +mod display; -trait _Shape { - fn symbol(&self) -> char; - fn index(&self) -> usize; -} +pub use self::display::{PieceDisplay, PieceDisplayStyle}; -macro_rules! shape { - ($name:ident, $index:expr, $symbol:expr) => { - struct $name; - - impl _Shape for $name { - fn symbol(&self) -> char { - $symbol - } - - fn index(&self) -> usize { - $index - } - } - }; -} - -shape!(Pawn, 0, 'P'); -shape!(Knight, 1, 'K'); -shape!(Bishop, 2, 'B'); -shape!(Rook, 3, 'R'); -shape!(Queen, 4, 'Q'); -shape!(King, 5, 'K'); - -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum Shape { - Pawn = 0, - Knight = 1, - Bishop = 2, - Rook = 3, - Queen = 4, - King = 5, -} - -impl Shape { - /// Number of piece shapes - pub const NUM: usize = 6; - - /// A slice of all piece shapes - pub const ALL: [Shape; Self::NUM] = [ - Shape::Pawn, - Shape::Knight, - Shape::Bishop, - Shape::Rook, - Shape::Queen, - Shape::King, - ]; - - pub fn iter() -> slice::Iter<'static, Self> { - Shape::ALL.iter() - } - - pub fn into_iter() -> array::IntoIter { - Shape::ALL.into_iter() - } - - /// An iterator over the shapes that a pawn can promote to - pub fn promotable() -> slice::Iter<'static, Shape> { - const PROMOTABLE_SHAPES: [Shape; 4] = - [Shape::Queen, Shape::Rook, Shape::Bishop, Shape::Knight]; - - PROMOTABLE_SHAPES.iter() - } - - const fn to_ascii(self) -> char { - match self { - Shape::Pawn => 'P', - Shape::Knight => 'N', - Shape::Bishop => 'B', - Shape::Rook => 'R', - Shape::Queen => 'Q', - Shape::King => 'K', - } - } -} - -impl TryFrom for Shape { - type Error = TryFromCharError; - - fn try_from(value: char) -> Result { - match value { - 'P' | 'p' => Ok(Shape::Pawn), - 'N' | 'n' => Ok(Shape::Knight), - 'B' | 'b' => Ok(Shape::Bishop), - 'R' | 'r' => Ok(Shape::Rook), - 'Q' | 'q' => Ok(Shape::Queen), - 'K' | 'k' => Ok(Shape::King), - _ => Err(TryFromCharError), - } - } -} - -try_from_string!(Shape); - -impl From<&Shape> for char { - fn from(shape: &Shape) -> char { - char::from(*shape) - } -} - -impl From for char { - fn from(shape: Shape) -> char { - shape.to_ascii() - } -} - -impl fmt::Display for Shape { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let self_char: char = self.into(); - write!(f, "{self_char}") - } -} +use crate::{Color, Shape, Square}; #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct Piece { @@ -195,9 +81,16 @@ impl Piece { } } -impl fmt::Display for Piece { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.to_unicode()) +impl std::fmt::Display for Piece { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + PieceDisplay { + piece: *self, + style: PieceDisplayStyle::default() + } + ) } } @@ -268,8 +161,8 @@ impl PlacedPiece { } } -impl fmt::Display for PlacedPiece { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl std::fmt::Display for PlacedPiece { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}{}", self.piece, self.square) } } diff --git a/core/src/pieces/display.rs b/core/src/pieces/display.rs new file mode 100644 index 0000000..77fadb6 --- /dev/null +++ b/core/src/pieces/display.rs @@ -0,0 +1,28 @@ +// Eryn Wells + +use super::Piece; + +#[derive(Default)] +pub enum PieceDisplayStyle { + #[default] + Unicode, + ASCII, + LongForm, +} + +pub struct PieceDisplay { + pub(super) piece: Piece, + pub(super) style: PieceDisplayStyle, +} + +impl std::fmt::Display for PieceDisplay { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.style { + PieceDisplayStyle::Unicode => write!(f, "{}", self.piece.to_unicode()), + PieceDisplayStyle::ASCII => write!(f, "{}", self.piece.to_ascii()), + PieceDisplayStyle::LongForm => { + write!(f, "{} {}", self.piece.color.name(), self.piece.shape.name()) + } + } + } +} diff --git a/core/src/shapes.rs b/core/src/shapes.rs new file mode 100644 index 0000000..0bdfea8 --- /dev/null +++ b/core/src/shapes.rs @@ -0,0 +1,137 @@ +// Eryn Wells + +use std::{array, slice}; +use thiserror::Error; + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum Shape { + Pawn = 0, + Knight = 1, + Bishop = 2, + Rook = 3, + Queen = 4, + King = 5, +} + +impl Shape { + /// Number of piece shapes + pub const NUM: usize = 6; + + /// A slice of all piece shapes + pub const ALL: [Shape; Self::NUM] = [ + Shape::Pawn, + Shape::Knight, + Shape::Bishop, + Shape::Rook, + Shape::Queen, + Shape::King, + ]; + + pub fn iter() -> slice::Iter<'static, Self> { + Shape::ALL.iter() + } + + #[must_use] + pub fn into_iter() -> array::IntoIter { + Shape::ALL.into_iter() + } + + /// An iterator over the shapes that a pawn can promote to + pub fn promotable() -> slice::Iter<'static, Shape> { + const PROMOTABLE_SHAPES: [Shape; 4] = + [Shape::Queen, Shape::Rook, Shape::Bishop, Shape::Knight]; + + PROMOTABLE_SHAPES.iter() + } + + #[must_use] + pub const fn to_ascii(self) -> char { + match self { + Shape::Pawn => 'P', + Shape::Knight => 'N', + Shape::Bishop => 'B', + Shape::Rook => 'R', + Shape::Queen => 'Q', + Shape::King => 'K', + } + } + + #[must_use] + pub const fn name(self) -> &'static str { + match self { + Shape::Pawn => "pawn", + Shape::Knight => "knight", + Shape::Bishop => "bishop", + Shape::Rook => "rook", + Shape::Queen => "queen", + Shape::King => "king", + } + } +} + +#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)] +#[error("no matching piece shape for character '{0:?}'")] +pub struct ShapeFromCharError(char); + +impl TryFrom for Shape { + type Error = ShapeFromCharError; + + fn try_from(value: char) -> Result { + match value { + 'P' | 'p' => Ok(Shape::Pawn), + 'N' | 'n' => Ok(Shape::Knight), + 'B' | 'b' => Ok(Shape::Bishop), + 'R' | 'r' => Ok(Shape::Rook), + 'Q' | 'q' => Ok(Shape::Queen), + 'K' | 'k' => Ok(Shape::King), + _ => Err(ShapeFromCharError(value)), + } + } +} + +#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)] +#[error("no matching piece shape for string")] +pub struct ShapeFromStrError; + +impl TryFrom<&str> for Shape { + type Error = ShapeFromStrError; + + fn try_from(value: &str) -> Result { + match value.to_lowercase().as_str() { + "p" | "pawn" => Ok(Shape::Pawn), + "n" | "knight" => Ok(Shape::Knight), + "b" | "bishop" => Ok(Shape::Bishop), + "r" | "rook" => Ok(Shape::Rook), + "q" | "queen" => Ok(Shape::Queen), + "k" | "king" => Ok(Shape::King), + _ => Err(ShapeFromStrError), + } + } +} + +impl std::str::FromStr for Shape { + type Err = ShapeFromStrError; + + fn from_str(s: &str) -> Result { + Self::try_from(s) + } +} + +impl From<&Shape> for char { + fn from(shape: &Shape) -> char { + char::from(*shape) + } +} + +impl From for char { + fn from(shape: Shape) -> char { + shape.to_ascii() + } +} + +impl std::fmt::Display for Shape { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let self_char: char = self.into(); + write!(f, "{self_char}") + } +} diff --git a/explorer/Cargo.toml b/explorer/Cargo.toml index a81f966..00fa045 100644 --- a/explorer/Cargo.toml +++ b/explorer/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow = "1.0.98" chessfriend_core = { path = "../core" } chessfriend_board = { path = "../board" } chessfriend_moves = { path = "../moves" } @@ -13,3 +14,4 @@ chessfriend_position = { path = "../position" } clap = { version = "4.4.12", features = ["derive"] } rustyline = "13.0.0" shlex = "1.2.0" +thiserror = "2" diff --git a/explorer/src/main.rs b/explorer/src/main.rs index bf688d1..8ba0a6b 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -4,9 +4,11 @@ use chessfriend_board::{fen::FromFenStr, Board}; use chessfriend_core::{Color, Piece, Shape, Square}; use chessfriend_moves::Builder as MoveBuilder; use chessfriend_position::{fen::ToFenStr, PlacePieceStrategy, Position, ValidateMove}; + use clap::{Arg, Command}; use rustyline::error::ReadlineError; use rustyline::DefaultEditor; +use thiserror::Error; struct CommandResult { should_continue: bool, @@ -79,11 +81,17 @@ fn command_line() -> Command { .subcommand(Command::new("starting").about("Reset the board to the starting position")) } -fn respond(line: &str, state: &mut State) -> Result { - let args = shlex::split(line).ok_or("error: Invalid quoting")?; - let matches = command_line() - .try_get_matches_from(args) - .map_err(|e| e.to_string())?; +#[derive(Clone, Debug, Error, Eq, PartialEq)] +enum CommandHandlingError<'a> { + #[error("lexer error")] + LexerError, + #[error("missing {0} argument")] + MissingArgument(&'a str), +} + +fn respond(line: &str, state: &mut State) -> anyhow::Result { + let args = shlex::split(line).ok_or(CommandHandlingError::LexerError)?; + let matches = command_line().try_get_matches_from(args)?; let mut result = CommandResult::default(); @@ -94,68 +102,53 @@ fn respond(line: &str, state: &mut State) -> Result { result.should_print_position = false; } Some(("fen", _matches)) => { - println!( - "{}", - state - .position - .to_fen_str() - .map_err(|_| "error: Unable to generate FEN for current position")? - ); - + println!("{}", state.position.to_fen_str()?); result.should_print_position = false; } Some(("make", matches)) => { let from_square = Square::from_algebraic_str( - matches.get_one::("from").ok_or("Missing square")?, - ) - .map_err(|_| "Error: invalid square specifier")?; + matches + .get_one::("from") + .ok_or(CommandHandlingError::MissingArgument("from"))?, + )?; let to_square = Square::from_algebraic_str( - matches.get_one::("to").ok_or("Missing square")?, - ) - .map_err(|_| "Error: invalid square specifier")?; + matches + .get_one::("to") + .ok_or(CommandHandlingError::MissingArgument("to"))?, + )?; - let ply = MoveBuilder::new() - .from(from_square) - .to(to_square) - .build() - .map_err(|err| format!("Error: {err:?}"))?; + let ply = MoveBuilder::new().from(from_square).to(to_square).build()?; - state - .position - .make_move(ply, ValidateMove::Yes) - .map_err(|err| format!("Error: {err}"))?; + state.position.make_move(ply, ValidateMove::Yes)?; } Some(("place", matches)) => { let color = matches .get_one::("color") - .ok_or("Missing color descriptor")?; - let color = Color::try_from(color).map_err(|_| "Invalid color descriptor")?; + .ok_or(CommandHandlingError::MissingArgument("color"))?; + let color = color.parse::()?; let shape = matches .get_one::("piece") - .ok_or("Missing piece descriptor")?; - let shape = Shape::try_from(shape).map_err(|_| "Invalid piece descriptor")?; + .ok_or(CommandHandlingError::MissingArgument("piece"))?; + let shape = shape.parse::()?; let square = matches .get_one::("square") - .ok_or("Missing square")?; - let square = Square::from_algebraic_str(square) - .map_err(|_| "Error: invalid square specifier")?; + .ok_or(CommandHandlingError::MissingArgument("square"))?; + let square = Square::from_algebraic_str(square)?; let piece = Piece::new(color, shape); state .position - .place_piece(piece, square, PlacePieceStrategy::default()) - .map_err(|err| format!("Error: could not place piece: {err:?}"))?; + .place_piece(piece, square, PlacePieceStrategy::default())?; } Some(("sight", matches)) => { let square = matches .get_one::("square") - .ok_or("Missing square")?; - let square = Square::from_algebraic_str(square) - .map_err(|_| "Error: invalid square specifier")?; + .ok_or(CommandHandlingError::MissingArgument("square"))?; + let square = square.parse::()?; let sight = state.position.sight(square); @@ -167,9 +160,8 @@ fn respond(line: &str, state: &mut State) -> Result { Some(("moves", matches)) => { let square = matches .get_one::("square") - .ok_or("Missing square")?; - let square = Square::from_algebraic_str(square) - .map_err(|_| "Error: invalid square specifier")?; + .ok_or(CommandHandlingError::MissingArgument("square"))?; + let square = square.parse::()?; let movement = state.position.movement(square); @@ -182,7 +174,7 @@ fn respond(line: &str, state: &mut State) -> Result { let starting_position = Position::starting(); state.position = starting_position; } - Some(("reset", matches)) => do_reset_command(state, matches)?, + Some(("reset", matches)) => result = do_reset_command(state, matches)?, Some((name, _matches)) => unimplemented!("{name}"), None => unreachable!("Subcommand required"), } @@ -190,19 +182,24 @@ fn respond(line: &str, state: &mut State) -> Result { Ok(result) } -fn do_reset_command(state: &mut State, matches: &clap::ArgMatches) -> Result<(), String> { +fn do_reset_command( + state: &mut State, + matches: &clap::ArgMatches, +) -> anyhow::Result { match matches.subcommand() { None | Some(("clear", _)) => state.position = Position::empty(), Some(("starting", _)) => state.position = Position::starting(), Some(("fen", matches)) => { - let fen = matches.get_one::("fen").ok_or("Missing FEN")?; - let board = Board::from_fen_str(fen).map_err(|err| format!("{err}"))?; + let fen = matches + .get_one::("fen") + .ok_or(CommandHandlingError::MissingArgument("fen"))?; + let board = Board::from_fen_str(fen)?; state.position = Position::new(board); } Some((name, _matches)) => unimplemented!("{name}"), } - Ok(()) + Ok(CommandResult::default()) } fn main() -> Result<(), String> { diff --git a/moves/src/builder.rs b/moves/src/builder.rs index e75a511..b6a0794 100644 --- a/moves/src/builder.rs +++ b/moves/src/builder.rs @@ -4,15 +4,20 @@ use crate::{defs::Kind, Move, PromotionShape}; use chessfriend_board::{castle, en_passant::EnPassant}; use chessfriend_core::{Color, File, PlacedPiece, Rank, Square}; use std::result::Result as StdResult; +use thiserror::Error; pub type Result = std::result::Result; type EncodedMoveResult = std::result::Result; -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)] pub enum Error { + #[error("no origin square")] MissingOriginSquare, + #[error("no target square")] MissingTargetSquare, + #[error("no capture square")] MissingCaptureSquare, + #[error("invalid en passant square")] InvalidEnPassantSquare, } From 6816e350eb481eb68ea6b122a6ad77087e203d5e Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 19 May 2025 14:19:05 -0700 Subject: [PATCH 275/423] [moves] Clean up implementation of Move and export Kind enum --- moves/Cargo.toml | 1 + moves/src/defs.rs | 9 ++------- moves/src/lib.rs | 2 +- moves/src/moves.rs | 20 ++++++++++++-------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/moves/Cargo.toml b/moves/Cargo.toml index b042344..797f7c8 100644 --- a/moves/Cargo.toml +++ b/moves/Cargo.toml @@ -9,3 +9,4 @@ edition = "2021" chessfriend_bitboard = { path = "../bitboard" } chessfriend_board = { path = "../board" } chessfriend_core = { path = "../core" } +thiserror = "2" diff --git a/moves/src/defs.rs b/moves/src/defs.rs index 8254cb9..caea420 100644 --- a/moves/src/defs.rs +++ b/moves/src/defs.rs @@ -2,8 +2,9 @@ use chessfriend_core::Shape; +#[repr(u16)] #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) enum Kind { +pub enum Kind { Quiet = 0b0000, DoublePush = 0b0001, KingSideCastle = 0b0010, @@ -14,12 +15,6 @@ pub(crate) enum Kind { CapturePromotion = 0b1100, } -impl Kind { - fn is_reversible(self) -> bool { - (self as u16) & 0b1100 == 0 - } -} - #[repr(u16)] #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum PromotionShape { diff --git a/moves/src/lib.rs b/moves/src/lib.rs index e43a61d..09ccb0d 100644 --- a/moves/src/lib.rs +++ b/moves/src/lib.rs @@ -7,5 +7,5 @@ mod defs; mod moves; pub use builder::{Builder, Error as BuildMoveError, Result as BuildMoveResult}; -pub use defs::PromotionShape; +pub use defs::{Kind, PromotionShape}; pub use moves::Move; diff --git a/moves/src/moves.rs b/moves/src/moves.rs index b7d2539..bc66d77 100644 --- a/moves/src/moves.rs +++ b/moves/src/moves.rs @@ -1,5 +1,6 @@ // Eryn Wells +use crate::builder::Builder; use crate::defs::Kind; use chessfriend_board::castle::Castle; use chessfriend_core::{Rank, Shape, Square}; @@ -57,7 +58,7 @@ impl Move { #[must_use] pub fn is_castle(&self) -> bool { - self.castle().is_some() + (self.0 & 0b0010) != 0 } #[must_use] @@ -71,17 +72,17 @@ impl Move { #[must_use] pub fn is_capture(&self) -> bool { - (self.0 & 0b0100) != 0 + (self.0 & Kind::Capture as u16) != 0 } #[must_use] pub fn is_en_passant(&self) -> bool { - self.flags() == 0b0101 + self.0 == Kind::EnPassantCapture as u16 } #[must_use] pub fn is_promotion(&self) -> bool { - (self.0 & 0b1000) != 0 + (self.0 & Kind::Promotion as u16) != 0 } #[must_use] @@ -113,7 +114,7 @@ impl Move { } impl Move { - fn _transfer_char(self) -> char { + fn transfer_char(self) -> char { if self.is_capture() || self.is_en_passant() { 'x' } else { @@ -122,18 +123,21 @@ impl Move { } } +const KINGSIDE_CASTLE_STR: &str = "0-0"; +const QUEENSIDE_CASTLE_STR: &str = "0-0-0"; + impl fmt::Display for Move { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(castle) = self.castle() { return match castle { - Castle::KingSide => write!(f, "0-0"), - Castle::QueenSide => write!(f, "0-0-0"), + Castle::KingSide => write!(f, "{KINGSIDE_CASTLE_STR}"), + Castle::QueenSide => write!(f, "{QUEENSIDE_CASTLE_STR}"), }; } let origin = self.origin_square(); let target = self.target_square(); - let transfer_char = self._transfer_char(); + let transfer_char = self.transfer_char(); write!(f, "{origin}{transfer_char}{target}")?; if let Some(promotion) = self.promotion() { From 0c1863acb93e6d9176f31b5aadc72c1f208d0afd Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 19 May 2025 16:50:30 -0700 Subject: [PATCH 276/423] [board, core, moves, position] Implement castling Implement a new method on Position that evaluates whether the active color can castle on a given wing of the board. Then, implement making a castling move in the position. Make a new Wing enum in the core crate to specify kingside or queenside. Replace the Castle enum from the board crate with this one. This caused a lot of churn... Along the way fix a bunch of tests. Note: there's still no way to actually make a castling move in explorer. --- board/src/board.rs | 11 +- board/src/castle.rs | 19 +- board/src/castle/parameters.rs | 44 +--- board/src/castle/rights.rs | 55 +++-- board/src/fen.rs | 30 +-- board/src/lib.rs | 7 +- board/src/piece_sets/bitboards.rs | 56 ----- core/src/coordinates.rs | 6 +- core/src/coordinates/wings.rs | 22 ++ core/src/lib.rs | 2 +- moves/src/builder.rs | 29 ++- moves/src/moves.rs | 15 +- moves/tests/flags.rs | 36 ++- position/src/lib.rs | 3 +- position/src/position.rs | 2 +- position/src/position/make_move.rs | 71 ++++-- position/src/position/position.rs | 347 +++++++++++++++++++++++++---- position/src/testing.rs | 2 +- 18 files changed, 499 insertions(+), 258 deletions(-) delete mode 100644 board/src/piece_sets/bitboards.rs create mode 100644 core/src/coordinates/wings.rs diff --git a/board/src/board.rs b/board/src/board.rs index 3922511..d5b89c9 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -7,7 +7,7 @@ use crate::{ PieceSet, }; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece, Shape, Square}; +use chessfriend_core::{Color, Piece, Shape, Square, Wing}; #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct Board { @@ -82,10 +82,12 @@ impl Board { } impl Board { + /// A [`BitBoard`] of squares occupied by pieces of all colors. pub fn occupancy(&self) -> BitBoard { self.pieces.occpuancy() } + /// A [`BitBoard`] of squares that are vacant. pub fn vacancy(&self) -> BitBoard { !self.occupancy() } @@ -99,6 +101,13 @@ impl Board { } } +impl Board { + #[must_use] + pub fn castling_parameters(&self, wing: Wing) -> &'static castle::Parameters { + &castle::Parameters::BY_COLOR[self.active_color as usize][wing as usize] + } +} + impl Board { pub fn display(&self) -> DiagramFormatter<'_> { DiagramFormatter::new(self) diff --git a/board/src/castle.rs b/board/src/castle.rs index 7f477df..c29859e 100644 --- a/board/src/castle.rs +++ b/board/src/castle.rs @@ -3,22 +3,5 @@ mod parameters; mod rights; +pub use parameters::Parameters; pub use rights::Rights; - -use chessfriend_core::Color; -use parameters::Parameters; - -#[repr(u8)] -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum Castle { - KingSide = 0, - QueenSide = 1, -} - -impl Castle { - pub const ALL: [Castle; 2] = [Castle::KingSide, Castle::QueenSide]; - - pub fn parameters(self, color: Color) -> &'static Parameters { - &Parameters::BY_COLOR[color as usize][self as usize] - } -} diff --git a/board/src/castle/parameters.rs b/board/src/castle/parameters.rs index cfe5901..bc4e238 100644 --- a/board/src/castle/parameters.rs +++ b/board/src/castle/parameters.rs @@ -1,31 +1,32 @@ use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Square}; +use chessfriend_core::{Color, Square, Wing}; +#[derive(Debug)] pub struct Parameters { /// Origin squares of the king and rook. - origin: Squares, + pub origin: Squares, /// Target or destination squares for the king and rook. - target: Squares, + pub target: Squares, /// The set of squares that must be clear of any pieces in order to perform /// this castle. - clear: BitBoard, + pub clear: BitBoard, /// The set of squares that must not be attacked (i.e. visible to opposing /// pieces) in order to perform this castle. - check: BitBoard, + pub check: BitBoard, } #[derive(Debug)] -pub(super) struct Squares { +pub struct Squares { pub king: Square, pub rook: Square, } impl Parameters { /// Parameters for each castling move, organized by color and board-side. - pub(super) const BY_COLOR: [[Self; 2]; Color::NUM] = [ + pub(crate) const BY_COLOR: [[Self; Wing::NUM]; Color::NUM] = [ [ Parameters { origin: Squares { @@ -80,31 +81,8 @@ impl Parameters { ], ]; - pub fn king_origin_square(&self) -> Square { - self.origin.king - } - - pub fn rook_origin_square(&self) -> Square { - self.origin.rook - } - - pub fn king_target_square(&self) -> Square { - self.target.king - } - - pub fn rook_target_square(&self) -> Square { - self.target.rook - } - - /// A [`BitBoard`] of the squares that must be clear of any piece in order - /// to perform this castle move. - pub fn clear_squares(&self) -> &BitBoard { - &self.clear - } - - /// A [`BitBoard`] of the squares that must not be visible to opposing - /// pieces in order to perform this castle move. - pub fn check_squares(&self) -> &BitBoard { - &self.check + #[must_use] + pub fn get(color: Color, wing: Wing) -> &'static Parameters { + &Self::BY_COLOR[color as usize][wing as usize] } } diff --git a/board/src/castle/rights.rs b/board/src/castle/rights.rs index e79aa86..ccd56c6 100644 --- a/board/src/castle/rights.rs +++ b/board/src/castle/rights.rs @@ -1,5 +1,4 @@ -use super::Castle; -use chessfriend_core::Color; +use chessfriend_core::{Color, Wing}; use std::fmt; #[derive(Clone, Copy, Eq, Hash, PartialEq)] @@ -13,16 +12,16 @@ impl Rights { /// as long as they have not moved their king, or the rook on that side of /// the board. #[must_use] - pub fn color_has_right(self, color: Color, castle: Castle) -> bool { - (self.0 & (1 << Self::flag_offset(color, castle))) != 0 + pub fn color_has_right(self, color: Color, wing: Wing) -> bool { + (self.0 & (1 << Self::flag_offset(color, wing))) != 0 } - pub fn grant(&mut self, color: Color, castle: Castle) { - self.0 |= 1 << Self::flag_offset(color, castle); + pub fn grant(&mut self, color: Color, wing: Wing) { + self.0 |= 1 << Self::flag_offset(color, wing); } - pub fn revoke(&mut self, color: Color, castle: Castle) { - self.0 &= !(1 << Self::flag_offset(color, castle)); + pub fn revoke(&mut self, color: Color, wing: Wing) { + self.0 &= !(1 << Self::flag_offset(color, wing)); } /// Revoke castling rights for all colors and all sides of the board. @@ -32,8 +31,8 @@ impl Rights { } impl Rights { - fn flag_offset(color: Color, castle: Castle) -> usize { - ((color as usize) << 1) + castle as usize + fn flag_offset(color: Color, wing: Wing) -> usize { + ((color as usize) << 1) + wing as usize } } @@ -55,30 +54,30 @@ mod tests { #[test] fn bitfield_offsets() { - assert_eq!(Rights::flag_offset(Color::White, Castle::KingSide), 0); - assert_eq!(Rights::flag_offset(Color::White, Castle::QueenSide), 1); - assert_eq!(Rights::flag_offset(Color::Black, Castle::KingSide), 2); - assert_eq!(Rights::flag_offset(Color::Black, Castle::QueenSide), 3); + assert_eq!(Rights::flag_offset(Color::White, Wing::KingSide), 0); + assert_eq!(Rights::flag_offset(Color::White, Wing::QueenSide), 1); + assert_eq!(Rights::flag_offset(Color::Black, Wing::KingSide), 2); + assert_eq!(Rights::flag_offset(Color::Black, Wing::QueenSide), 3); } #[test] fn default_rights() { let mut rights = Rights::default(); - assert!(rights.color_has_right(Color::White, Castle::KingSide)); - assert!(rights.color_has_right(Color::White, Castle::QueenSide)); - assert!(rights.color_has_right(Color::Black, Castle::KingSide)); - assert!(rights.color_has_right(Color::Black, Castle::QueenSide)); + assert!(rights.color_has_right(Color::White, Wing::KingSide)); + assert!(rights.color_has_right(Color::White, Wing::QueenSide)); + assert!(rights.color_has_right(Color::Black, Wing::KingSide)); + assert!(rights.color_has_right(Color::Black, Wing::QueenSide)); - rights.revoke(Color::White, Castle::QueenSide); - assert!(rights.color_has_right(Color::White, Castle::KingSide)); - assert!(!rights.color_has_right(Color::White, Castle::QueenSide)); - assert!(rights.color_has_right(Color::Black, Castle::KingSide)); - assert!(rights.color_has_right(Color::Black, Castle::QueenSide)); + rights.revoke(Color::White, Wing::QueenSide); + assert!(rights.color_has_right(Color::White, Wing::KingSide)); + assert!(!rights.color_has_right(Color::White, Wing::QueenSide)); + assert!(rights.color_has_right(Color::Black, Wing::KingSide)); + assert!(rights.color_has_right(Color::Black, Wing::QueenSide)); - rights.grant(Color::White, Castle::QueenSide); - assert!(rights.color_has_right(Color::White, Castle::KingSide)); - assert!(rights.color_has_right(Color::White, Castle::QueenSide)); - assert!(rights.color_has_right(Color::Black, Castle::KingSide)); - assert!(rights.color_has_right(Color::Black, Castle::QueenSide)); + rights.grant(Color::White, Wing::QueenSide); + assert!(rights.color_has_right(Color::White, Wing::KingSide)); + assert!(rights.color_has_right(Color::White, Wing::QueenSide)); + assert!(rights.color_has_right(Color::Black, Wing::KingSide)); + assert!(rights.color_has_right(Color::Black, Wing::QueenSide)); } } diff --git a/board/src/fen.rs b/board/src/fen.rs index 00f543a..657f87b 100644 --- a/board/src/fen.rs +++ b/board/src/fen.rs @@ -1,7 +1,9 @@ // Eryn Wells -use crate::{piece_sets::PlacePieceStrategy, Board, Castle}; -use chessfriend_core::{coordinates::ParseSquareError, piece, Color, File, Piece, Rank, Square}; +use crate::{piece_sets::PlacePieceStrategy, Board}; +use chessfriend_core::{ + coordinates::ParseSquareError, piece, Color, File, Piece, Rank, Square, Wing, +}; use std::fmt::Write; use thiserror::Error; @@ -115,10 +117,10 @@ impl ToFenStr for Board { .map_err(ToFenStrError::FmtError)?; let castling = [ - (Color::White, Castle::KingSide), - (Color::White, Castle::QueenSide), - (Color::Black, Castle::KingSide), - (Color::Black, Castle::QueenSide), + (Color::White, Wing::KingSide), + (Color::White, Wing::QueenSide), + (Color::Black, Wing::KingSide), + (Color::Black, Wing::QueenSide), ] .map(|(color, castle)| { if !self.castling_rights.color_has_right(color, castle) { @@ -126,10 +128,10 @@ impl ToFenStr for Board { } match (color, castle) { - (Color::White, Castle::KingSide) => "K", - (Color::White, Castle::QueenSide) => "Q", - (Color::Black, Castle::KingSide) => "k", - (Color::Black, Castle::QueenSide) => "q", + (Color::White, Wing::KingSide) => "K", + (Color::White, Wing::QueenSide) => "Q", + (Color::Black, Wing::KingSide) => "k", + (Color::Black, Wing::QueenSide) => "q", } }) .concat(); @@ -232,10 +234,10 @@ impl FromFenStr for Board { } else { for ch in castling_rights.chars() { match ch { - 'K' => board.castling_rights.grant(Color::White, Castle::KingSide), - 'Q' => board.castling_rights.grant(Color::White, Castle::QueenSide), - 'k' => board.castling_rights.grant(Color::Black, Castle::KingSide), - 'q' => board.castling_rights.grant(Color::Black, Castle::QueenSide), + 'K' => board.castling_rights.grant(Color::White, Wing::KingSide), + 'Q' => board.castling_rights.grant(Color::White, Wing::QueenSide), + 'k' => board.castling_rights.grant(Color::Black, Wing::KingSide), + 'q' => board.castling_rights.grant(Color::Black, Wing::QueenSide), _ => return Err(FromFenStrError::InvalidValue), }; } diff --git a/board/src/lib.rs b/board/src/lib.rs index da05df2..86ba8ac 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -10,12 +10,7 @@ mod board; mod piece_sets; pub use board::Board; +pub use castle::Parameters as CastleParameters; pub use piece_sets::{PlacePieceError, PlacePieceStrategy}; -use castle::Castle; -use en_passant::EnPassant; use piece_sets::PieceSet; - -// Used by macros. -#[allow(unused_imports)] -use piece_sets::{PlacePieceError, PlacePieceStrategy}; diff --git a/board/src/piece_sets/bitboards.rs b/board/src/piece_sets/bitboards.rs deleted file mode 100644 index c8c8959..0000000 --- a/board/src/piece_sets/bitboards.rs +++ /dev/null @@ -1,56 +0,0 @@ -use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece, Shape, Square}; - -/// A collection of bitboards that organize pieces by color. -#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] -pub(super) struct ByColor(BitBoard, [BitBoard; Color::NUM]); - -/// A collection of bitboards that organize pieces first by color and then by piece type. -#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] -pub(super) struct ByColorAndShape([[BitBoard; Shape::NUM]; Color::NUM]); - -impl ByColor { - pub(super) fn new(all_pieces: BitBoard, bitboards_by_color: [BitBoard; Color::NUM]) -> Self { - ByColor(all_pieces, bitboards_by_color) - } - - pub(crate) fn all(&self) -> BitBoard { - self.0 - } - - pub(crate) fn bitboard(&self, color: Color) -> BitBoard { - self.1[color as usize] - } - - pub(super) fn set_square(&mut self, square: Square, color: Color) { - self.0.set(square); - self.1[color as usize].set(square); - } - - pub(super) fn clear_square(&mut self, square: Square, color: Color) { - self.0.clear(square); - self.1[color as usize].clear(square); - } -} - -impl ByColorAndShape { - pub(super) fn new(bitboards: [[BitBoard; Shape::NUM]; Color::NUM]) -> Self { - Self(bitboards) - } - - pub(super) fn bitboard_for_piece(&self, piece: Piece) -> BitBoard { - self.0[piece.color as usize][piece.shape as usize] - } - - pub(super) fn bitboard_for_piece_mut(&mut self, piece: Piece) -> &mut BitBoard { - &mut self.0[piece.color as usize][piece.shape as usize] - } - - pub(super) fn set_square(&mut self, square: Square, piece: Piece) { - self.bitboard_for_piece_mut(piece).set(square); - } - - pub(super) fn clear_square(&mut self, square: Square, piece: Piece) { - self.bitboard_for_piece_mut(piece).clear(square); - } -} diff --git a/core/src/coordinates.rs b/core/src/coordinates.rs index 1a13ced..a81daad 100644 --- a/core/src/coordinates.rs +++ b/core/src/coordinates.rs @@ -1,5 +1,9 @@ // Eryn Wells +mod wings; + +pub use wings::Wing; + use crate::Color; use std::fmt; use thiserror::Error; @@ -519,7 +523,7 @@ mod tests { fn bad_algebraic_input() { assert!("a0".parse::().is_err()); assert!("j3".parse::().is_err()); - assert!("a11".parse::().is_err()); + assert!("a9".parse::().is_err()); assert!("b-1".parse::().is_err()); assert!("a 1".parse::().is_err()); assert!("".parse::().is_err()); diff --git a/core/src/coordinates/wings.rs b/core/src/coordinates/wings.rs new file mode 100644 index 0000000..ed0d0b8 --- /dev/null +++ b/core/src/coordinates/wings.rs @@ -0,0 +1,22 @@ +// Eryn Wells + +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Wing { + KingSide = 0, + QueenSide = 1, +} + +impl Wing { + pub const NUM: usize = 2; + pub const ALL: [Wing; Self::NUM] = [Self::KingSide, Self::QueenSide]; +} + +impl std::fmt::Display for Wing { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Wing::KingSide => write!(f, "kingside"), + Wing::QueenSide => write!(f, "queenside"), + } + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs index 4d54037..64458b4 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -8,6 +8,6 @@ pub mod shapes; mod macros; pub use colors::Color; -pub use coordinates::{Direction, File, Rank, Square}; +pub use coordinates::{Direction, File, Rank, Square, Wing}; pub use pieces::{Piece, PlacedPiece}; pub use shapes::Shape; diff --git a/moves/src/builder.rs b/moves/src/builder.rs index b6a0794..53bc894 100644 --- a/moves/src/builder.rs +++ b/moves/src/builder.rs @@ -1,8 +1,8 @@ // Eryn Wells use crate::{defs::Kind, Move, PromotionShape}; -use chessfriend_board::{castle, en_passant::EnPassant}; -use chessfriend_core::{Color, File, PlacedPiece, Rank, Square}; +use chessfriend_board::{en_passant::EnPassant, CastleParameters}; +use chessfriend_core::{Color, File, PlacedPiece, Rank, Square, Wing}; use std::result::Result as StdResult; use thiserror::Error; @@ -92,7 +92,7 @@ pub struct Promotion { #[derive(Clone, Debug, Eq, PartialEq)] pub struct Castle { color: Color, - castle: castle::Castle, + wing: Wing, } impl Style for Null {} @@ -119,13 +119,13 @@ impl Style for Capture { impl Style for Castle { fn origin_square(&self) -> Option { - let parameters = self.castle.parameters(self.color); - Some(parameters.king_origin_square()) + let parameters = CastleParameters::get(self.color, self.wing); + Some(parameters.origin.king) } fn target_square(&self) -> Option { - let parameters = self.castle.parameters(self.color); - Some(parameters.king_target_square()) + let parameters = CastleParameters::get(self.color, self.wing); + Some(parameters.target.king) } } @@ -255,9 +255,9 @@ impl Builder { } #[must_use] - pub fn castling(color: Color, castle: castle::Castle) -> Builder { + pub fn castling(color: Color, wing: Wing) -> Builder { Builder { - style: Castle { color, castle }, + style: Castle { color, wing }, } } @@ -357,9 +357,9 @@ impl Builder { impl Builder { fn bits(&self) -> u16 { - let bits = match self.style.castle { - castle::Castle::KingSide => Kind::KingSideCastle, - castle::Castle::QueenSide => Kind::QueenSideCastle, + let bits = match self.style.wing { + Wing::KingSide => Kind::KingSideCastle, + Wing::QueenSide => Kind::QueenSideCastle, }; bits as u16 @@ -403,6 +403,11 @@ impl Builder { Move(Kind::EnPassantCapture as u16 | self.style.move_bits_unchecked()) } + /// Build an en passant move. + /// + /// ## Errors + /// + /// Returns an error if the target or origin squares are invalid. pub fn build(&self) -> Result { Ok(Move( Kind::EnPassantCapture as u16 | self.style.move_bits()?, diff --git a/moves/src/moves.rs b/moves/src/moves.rs index bc66d77..94f79a3 100644 --- a/moves/src/moves.rs +++ b/moves/src/moves.rs @@ -2,8 +2,7 @@ use crate::builder::Builder; use crate::defs::Kind; -use chessfriend_board::castle::Castle; -use chessfriend_core::{Rank, Shape, Square}; +use chessfriend_core::{Rank, Shape, Square, Wing}; use std::fmt; /// A single player's move. In game theory parlance, this is a "ply". @@ -62,10 +61,10 @@ impl Move { } #[must_use] - pub fn castle(&self) -> Option { + pub fn castle(&self) -> Option { match self.flags() { - 0b0010 => Some(Castle::KingSide), - 0b0011 => Some(Castle::QueenSide), + 0b0010 => Some(Wing::KingSide), + 0b0011 => Some(Wing::QueenSide), _ => None, } } @@ -77,7 +76,7 @@ impl Move { #[must_use] pub fn is_en_passant(&self) -> bool { - self.0 == Kind::EnPassantCapture as u16 + self.flags() == Kind::EnPassantCapture as u16 } #[must_use] @@ -130,8 +129,8 @@ impl fmt::Display for Move { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(castle) = self.castle() { return match castle { - Castle::KingSide => write!(f, "{KINGSIDE_CASTLE_STR}"), - Castle::QueenSide => write!(f, "{QUEENSIDE_CASTLE_STR}"), + Wing::KingSide => write!(f, "{KINGSIDE_CASTLE_STR}"), + Wing::QueenSide => write!(f, "{QUEENSIDE_CASTLE_STR}"), }; } diff --git a/moves/tests/flags.rs b/moves/tests/flags.rs index c611f31..ecf194a 100644 --- a/moves/tests/flags.rs +++ b/moves/tests/flags.rs @@ -1,7 +1,6 @@ // Eryn Wells -use chessfriend_board::castle::Castle; -use chessfriend_core::{piece, Color, File, Shape, Square}; +use chessfriend_core::{piece, Color, File, Shape, Square, Wing}; use chessfriend_moves::{testing::*, Builder, PromotionShape}; macro_rules! assert_flag { @@ -58,51 +57,50 @@ fn move_flags_capture() -> TestResult { #[test] fn move_flags_en_passant_capture() -> TestResult { - let mv = unsafe { - Builder::new() - .from(Square::A4) - .capturing_en_passant_on(Square::B3) - .build_unchecked() - }; + let ply = Builder::new() + .from(Square::A4) + .capturing_en_passant_on(Square::B3) + .build()?; - assert_flags!(mv, false, false, true, true, false, false); - assert_eq!(mv.origin_square(), Square::A4); - assert_eq!(mv.target_square(), Square::B3); - assert_eq!(mv.capture_square(), Some(Square::B4)); + assert!(ply.is_en_passant()); + assert_eq!(ply.origin_square(), Square::A4); + assert_eq!(ply.target_square(), Square::B3); + assert_eq!(ply.capture_square(), Some(Square::B4)); Ok(()) } #[test] fn move_flags_promotion() -> TestResult { - let mv = Builder::push(&piece!(White Pawn on H7)) + let ply = Builder::push(&piece!(White Pawn on H7)) .to(Square::H8) .promoting_to(PromotionShape::Queen) .build()?; - assert_flags!(mv, false, false, false, false, false, true); - assert_eq!(mv.promotion(), Some(Shape::Queen)); + assert!(ply.is_promotion()); + assert_eq!(ply.promotion(), Some(Shape::Queen)); Ok(()) } #[test] fn move_flags_capture_promotion() -> TestResult { - let mv = Builder::push(&piece!(White Pawn on H7)) + let ply = Builder::push(&piece!(White Pawn on H7)) .to(Square::H8) .capturing_piece(&piece!(Black Knight on G8)) .promoting_to(PromotionShape::Queen) .build()?; - assert_flags!(mv, false, false, false, true, false, true); - assert_eq!(mv.promotion(), Some(Shape::Queen)); + assert!(ply.is_capture()); + assert!(ply.is_promotion()); + assert_eq!(ply.promotion(), Some(Shape::Queen)); Ok(()) } #[test] fn move_flags_castle() -> TestResult { - let mv = Builder::castling(Color::White, Castle::KingSide).build()?; + let mv = Builder::castling(Color::White, Wing::KingSide).build()?; assert_flags!(mv, false, false, false, false, true, false); diff --git a/position/src/lib.rs b/position/src/lib.rs index 5215041..a17c5f6 100644 --- a/position/src/lib.rs +++ b/position/src/lib.rs @@ -14,4 +14,5 @@ mod macros; #[macro_use] mod testing; -pub use position::{MakeMoveError, MoveBuilder as MakeMoveBuilder, Position}; +pub use chessfriend_board::{fen, PlacePieceError, PlacePieceStrategy}; +pub use position::{CastleEvaluationError, Position, ValidateMove}; diff --git a/position/src/position.rs b/position/src/position.rs index 6c7bfca..f5f4213 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -5,5 +5,5 @@ mod position; pub use { make_move::{MakeMoveError, ValidateMove}, - position::Position, + position::{CastleEvaluationError, Position}, }; diff --git a/position/src/position/make_move.rs b/position/src/position/make_move.rs index c79185c..abc5515 100644 --- a/position/src/position/make_move.rs +++ b/position/src/position/make_move.rs @@ -1,11 +1,13 @@ // Eryn Wells use crate::{movement::Movement, Position}; -use chessfriend_board::{PlacePieceError, PlacePieceStrategy}; -use chessfriend_core::{Color, Piece, Square}; +use chessfriend_board::{CastleParameters, PlacePieceError, PlacePieceStrategy}; +use chessfriend_core::{Color, Piece, Square, Wing}; use chessfriend_moves::Move; use thiserror::Error; +use super::CastleEvaluationError; + type MakeMoveResult = Result<(), MakeMoveError>; #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] @@ -38,6 +40,9 @@ pub enum MakeMoveError { #[error("{0}")] PlacePieceError(#[from] PlacePieceError), + + #[error("{0}")] + CastleError(#[from] CastleEvaluationError), } pub enum UnmakeMoveError {} @@ -60,6 +65,10 @@ impl Position { return self.make_capture_move(ply); } + if let Some(wing) = ply.castle() { + return self.make_castle_move(wing); + } + Ok(()) } @@ -100,6 +109,27 @@ impl Position { Ok(()) } + + fn make_castle_move(&mut self, wing: Wing) -> MakeMoveResult { + self.active_color_can_castle(wing)?; + + let active_color = self.board.active_color; + let parameters = self.board.castling_parameters(wing); + + let king = self.board.remove_piece(parameters.origin.king).unwrap(); + self.board + .place_piece(king, parameters.target.king, PlacePieceStrategy::default())?; + + let rook = self.board.remove_piece(parameters.origin.rook).unwrap(); + self.board + .place_piece(rook, parameters.target.rook, PlacePieceStrategy::default())?; + + self.board.castling_rights.revoke(active_color, wing); + + self.advance_clocks(HalfMoveClock::Advance); + + Ok(()) + } } impl Position { @@ -142,21 +172,14 @@ impl Position { return Ok(()); } + let active_piece = self.validate_active_piece(ply)?; + let origin_square = ply.origin_square(); - let active_piece = self - .board - .get_piece(origin_square) - .ok_or(MakeMoveError::NoPiece(origin_square))?; - - if active_piece.color != self.board.active_color { - return Err(MakeMoveError::NonActiveColor { - piece: active_piece, - square: origin_square, - }); - } - let target_square = ply.target_square(); + // Pawns can see squares they can't move to. So, calculating valid + // squares requires a concept that includes Sight, but adds pawn pushes. + // In ChessFriend, that concept is Movement. let movement = active_piece.movement(origin_square, &self.board); if !movement.contains(target_square) { return Err(MakeMoveError::NoMove { @@ -166,6 +189,8 @@ impl Position { }); } + // TODO: En Passant capture. + if ply.is_capture() { let target = ply.target_square(); if let Some(captured_piece) = self.board.get_piece(target) { @@ -179,4 +204,22 @@ impl Position { Ok(()) } + + fn validate_active_piece(&self, ply: Move) -> Result { + let origin_square = ply.origin_square(); + + let active_piece = self + .board + .get_piece(origin_square) + .ok_or(MakeMoveError::NoPiece(origin_square))?; + + if active_piece.color != self.board.active_color { + return Err(MakeMoveError::NonActiveColor { + piece: active_piece, + square: origin_square, + }); + } + + Ok(active_piece) + } } diff --git a/position/src/position/position.rs b/position/src/position/position.rs index e5683bf..7ab96dd 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -38,20 +38,168 @@ impl Position { } } -/* impl Position { - /// Return a PlacedPiece representing the rook to use for a castling move. - pub(crate) fn rook_for_castle(&self, player: Color, castle: Castle) -> Option { - let square = match (player, castle) { - (Color::White, Castle::KingSide) => Square::H1, - (Color::White, Castle::QueenSide) => Square::A1, - (Color::Black, Castle::KingSide) => Square::H8, - (Color::Black, Castle::QueenSide) => Square::A8, - }; - - self.board.piece_on_square(square) + /// Place a piece on the board. + /// + /// ## Errors + /// + /// See [`chessfriend_board::Board::place_piece`]. + pub fn place_piece( + &mut self, + piece: Piece, + square: Square, + strategy: PlacePieceStrategy, + ) -> Result<(), PlacePieceError> { + self.board.place_piece(piece, square, strategy) } + #[must_use] + pub fn get_piece(&self, square: Square) -> Option { + self.board.get_piece(square) + } + + pub fn remove_piece(&mut self, square: Square) -> Option { + self.board.remove_piece(square) + } +} + +impl Position { + pub fn sight(&self, square: Square) -> BitBoard { + if let Some(piece) = self.get_piece(square) { + piece.sight(square, &self.board) + } else { + BitBoard::empty() + } + } + + pub fn movement(&self, square: Square) -> BitBoard { + if let Some(piece) = self.get_piece(square) { + piece.movement(square, &self.board) + } else { + BitBoard::empty() + } + } +} + +impl Position { + pub fn active_sight(&self) -> BitBoard { + self.friendly_sight(self.board.active_color) + } + + /// A [`BitBoard`] of all squares the given color can see. + pub fn friendly_sight(&self, color: Color) -> BitBoard { + // TODO: Probably want to implement a caching layer here. + self.board + .friendly_occupancy(color) + .occupied_squares(&IterationDirection::default()) + .map(|square| self.sight(square)) + .fold(BitBoard::empty(), BitOr::bitor) + } + + /// A [`BitBoard`] of all squares visible by colors that oppose the given color. + pub fn opposing_sight(&self) -> BitBoard { + // TODO: Probably want to implement a caching layer here. + let active_color = self.board.active_color; + Color::ALL + .into_iter() + .filter_map(|c| { + if c == active_color { + None + } else { + Some(self.friendly_sight(c)) + } + }) + .fold(BitBoard::empty(), BitOr::bitor) + } +} + +#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)] +pub enum CastleEvaluationError { + #[error("{color} does not have the right to castle {wing}")] + NoRights { color: Color, wing: Wing }, + #[error("no king")] + NoKing, + #[error("no rook")] + NoRook, + #[error("castling path is not clear")] + ObstructingPieces, + #[error("opposing pieces check castling path")] + CheckingPieces, +} + +impl Position { + /// Evaluates whether the active color can castle toward the given wing of the board in the + /// current position. + /// + /// ## Errors + /// + /// Returns an error indicating why the active color cannot castle. + pub fn active_color_can_castle(&self, wing: Wing) -> Result<(), CastleEvaluationError> { + // TODO: Cache this result. It's expensive! + + let active_color = self.board.active_color; + + if !self + .board + .castling_rights + .color_has_right(active_color, wing) + { + return Err(CastleEvaluationError::NoRights { + color: active_color, + wing, + }); + } + + let parameters = self.board.castling_parameters(wing); + + if self.castling_king(parameters.origin.king).is_none() { + return Err(CastleEvaluationError::NoKing); + } + + if self.castling_rook(parameters.origin.rook).is_none() { + return Err(CastleEvaluationError::NoRook); + } + + // All squares must be clear. + let has_obstructing_pieces = (self.board.occupancy() & parameters.clear).is_populated(); + if has_obstructing_pieces { + return Err(CastleEvaluationError::ObstructingPieces); + } + + // King cannot pass through check. + let opposing_sight = self.opposing_sight(); + let opposing_pieces_can_see_castling_path = + (parameters.check & opposing_sight).is_populated(); + if opposing_pieces_can_see_castling_path { + return Err(CastleEvaluationError::CheckingPieces); + } + + Ok(()) + } + + fn castling_king(&self, square: Square) -> Option { + self.get_piece(square).and_then(|piece| { + if piece.color == self.board.active_color && piece.is_king() { + Some(piece) + } else { + None + } + }) + } + + fn castling_rook(&self, square: Square) -> Option { + self.get_piece(square).and_then(|piece| { + if piece.color == self.board.active_color && piece.is_rook() { + Some(piece) + } else { + None + } + }) + } +} + +/* +impl Position { pub fn moves(&self) -> &Moves { self.moves.get_or_init(|| { let player_to_move = self.player_to_move(); @@ -120,11 +268,6 @@ impl Position { self.moves().moves_for_piece(piece) } - #[cfg(test)] - pub(crate) fn sight_of_piece(&self, piece: &PlacedPiece) -> BitBoard { - piece.sight(&self.board, self._en_passant_target_square()) - } - #[cfg(test)] pub(crate) fn is_king_in_check(&self) -> bool { let danger_squares = self.king_danger(self.player_to_move()); @@ -213,9 +356,9 @@ impl fmt::Display for Position { #[cfg(test)] mod tests { use super::*; - use crate::{assert_eq_bitboards, position, test_position, Position}; + use crate::{test_position, Position}; use chessfriend_bitboard::bitboard; - use chessfriend_core::piece; + use chessfriend_core::{piece, Wing}; #[test] fn piece_on_square() { @@ -223,22 +366,16 @@ mod tests { Black Bishop on F7, ]; - let piece = pos.board.piece_on_square(Square::F7); - assert_eq!(piece, Some(piece!(Black Bishop on F7))); + let piece = pos.board.get_piece(Square::F7); + assert_eq!(piece, Some(piece!(Black Bishop))); } #[test] fn piece_in_starting_position() { let pos = test_position!(starting); - assert_eq!( - pos.board.piece_on_square(Square::H1), - Some(piece!(White Rook on H1)) - ); - assert_eq!( - pos.board.piece_on_square(Square::A8), - Some(piece!(Black Rook on A8)) - ); + assert_eq!(pos.board.get_piece(Square::H1), Some(piece!(White Rook))); + assert_eq!(pos.board.get_piece(Square::A8), Some(piece!(Black Rook))); } #[test] @@ -274,38 +411,160 @@ mod tests { White Rook on H1 ); - assert!(pos.player_can_castle(Color::White, Castle::KingSide)); - assert!(pos.player_can_castle(Color::White, Castle::QueenSide)); + let rights = pos.board.castling_rights; + assert!(rights.color_has_right(Color::White, Wing::KingSide)); + assert!(rights.color_has_right(Color::White, Wing::QueenSide)); } #[test] - fn rook_for_castle() { - let pos = position![ + fn friendly_sight() { + let pos = test_position!( + White King on E4, + ); + + let sight = pos.active_sight(); + assert_eq!(sight, bitboard![E5 F5 F4 F3 E3 D3 D4 D5]); + } + + #[test] + fn opposing_sight() { + let pos = test_position!( + White King on E4, + Black Rook on E7, + ); + + let sight = pos.opposing_sight(); + assert_eq!(sight, bitboard![A7 B7 C7 D7 F7 G7 H7 E8 E6 E5 E4]); + } + + #[test] + fn king_for_castle() { + let pos = test_position![ White King on E1, White Rook on H1, White Rook on A1, ]; + let kingside_parameters = pos.board.castling_parameters(Wing::KingSide); assert_eq!( - pos.rook_for_castle(Color::White, Castle::KingSide), - Some(piece!(White Rook on H1)) + pos.castling_king(kingside_parameters.origin.king), + Some(piece!(White King)) ); + + let queenside_parameters = pos.board.castling_parameters(Wing::QueenSide); assert_eq!( - pos.rook_for_castle(Color::White, Castle::QueenSide), - Some(piece!(White Rook on A1)) + pos.castling_king(queenside_parameters.origin.king), + Some(piece!(White King)) ); } #[test] - fn danger_squares() { - let pos = test_position!(Black, [ + fn rook_for_castle() { + let pos = test_position![ White King on E1, - Black King on E7, - White Rook on E4, - ]); + White Rook on H1, + ]; - let danger_squares = pos.king_danger(Color::Black); - let expected = bitboard![D1 F1 D2 E2 F2 E3 A4 B4 C4 D4 F4 G4 H4 E5 E6 E7 E8]; - assert_eq_bitboards!(danger_squares, expected); + let kingside_parameters = pos.board.castling_parameters(Wing::KingSide); + assert_eq!( + pos.castling_rook(kingside_parameters.origin.rook), + Some(piece!(White Rook)) + ); + + let pos = test_position![ + White King on E1, + White Rook on A1, + ]; + + let queenside_parameters = pos.board.castling_parameters(Wing::QueenSide); + assert_eq!( + pos.castling_rook(queenside_parameters.origin.rook), + Some(piece!(White Rook)) + ); + } + + #[test] + fn white_can_castle() { + let pos = test_position![ + White King on E1, + White Rook on H1, + White Rook on A1, + ]; + + assert_eq!(pos.active_color_can_castle(Wing::KingSide), Ok(())); + assert_eq!(pos.active_color_can_castle(Wing::QueenSide), Ok(())); + } + + #[test] + fn white_cannot_castle_missing_king() { + let pos = test_position![ + White King on E2, + White Rook on H1, + White Rook on A1, + ]; + + assert_eq!( + pos.active_color_can_castle(Wing::KingSide), + Err(CastleEvaluationError::NoKing) + ); + assert_eq!( + pos.active_color_can_castle(Wing::QueenSide), + Err(CastleEvaluationError::NoKing) + ); + } + + #[test] + fn white_cannot_castle_missing_rook() { + let pos = test_position![ + White King on E1, + White Rook on A1, + ]; + + assert_eq!( + pos.active_color_can_castle(Wing::KingSide), + Err(CastleEvaluationError::NoRook) + ); + + let pos = test_position![ + White King on E1, + White Rook on H1, + ]; + + assert_eq!( + pos.active_color_can_castle(Wing::QueenSide), + Err(CastleEvaluationError::NoRook) + ); + } + + #[test] + fn white_cannot_castle_obstructing_piece() { + let pos = test_position![ + White King on E1, + White Bishop on F1, + White Rook on H1, + White Rook on A1, + ]; + + assert_eq!( + pos.active_color_can_castle(Wing::KingSide), + Err(CastleEvaluationError::ObstructingPieces) + ); + assert_eq!(pos.active_color_can_castle(Wing::QueenSide), Ok(())); + } + + #[test] + fn white_cannot_castle_checking_pieces() { + let pos = test_position![ + White King on E1, + White Rook on H1, + White Rook on A1, + Black Queen on C6, + ]; + + assert_eq!(pos.active_color_can_castle(Wing::KingSide), Ok(())); + assert_eq!( + pos.active_color_can_castle(Wing::QueenSide), + Err(CastleEvaluationError::CheckingPieces) + ); } } diff --git a/position/src/testing.rs b/position/src/testing.rs index 12400bc..1439dee 100644 --- a/position/src/testing.rs +++ b/position/src/testing.rs @@ -1,6 +1,6 @@ // Eryn Wells -use crate::MakeMoveError; +use crate::position::MakeMoveError; use chessfriend_moves::BuildMoveError; #[macro_export] From 54ac88aaf7fd0d8ccc7177244b319e024c43d76d Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 19 May 2025 16:50:44 -0700 Subject: [PATCH 277/423] [board] Remove some old PlacedPiece code --- board/src/piece_sets/mailbox.rs | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/board/src/piece_sets/mailbox.rs b/board/src/piece_sets/mailbox.rs index 0ab8e21..9e562dd 100644 --- a/board/src/piece_sets/mailbox.rs +++ b/board/src/piece_sets/mailbox.rs @@ -1,6 +1,6 @@ // Eryn Wells -use chessfriend_core::{Piece, PlacedPiece, Square}; +use chessfriend_core::{Piece, Square}; use std::iter::FromIterator; #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] @@ -22,16 +22,6 @@ impl Mailbox { pub fn remove(&mut self, square: Square) { self.0[square as usize] = None; } - - pub fn iter(&self) -> impl Iterator { - self.0 - .into_iter() - .flatten() // Remove the Nones - .zip(0u8..) // Enumerate with u8 instead of usize - .map(|(piece, index)| { - PlacedPiece::new(piece, unsafe { Square::from_index_unchecked(index) }) - }) - } } impl Default for Mailbox { @@ -46,16 +36,6 @@ impl From<[Option; Square::NUM]> for Mailbox { } } -impl FromIterator for Mailbox { - fn from_iter>(iter: T) -> Self { - iter.into_iter() - .fold(Self::new(), |mut mailbox, placed_piece| { - mailbox.set(placed_piece.piece(), placed_piece.square); - mailbox - }) - } -} - impl FromIterator<(Square, Piece)> for Mailbox { fn from_iter>(iter: T) -> Self { iter.into_iter() From 97552302cb95ff44393838d5ed69912ed45e7101 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 19 May 2025 16:59:13 -0700 Subject: [PATCH 278/423] [position] Move castle evaluation code to its own submodule of position Move the tests too. --- position/src/position.rs | 4 +- position/src/position/castle.rs | 241 +++++++++++++++++++++++ position/src/position/position.rs | 305 ++++-------------------------- 3 files changed, 278 insertions(+), 272 deletions(-) create mode 100644 position/src/position/castle.rs diff --git a/position/src/position.rs b/position/src/position.rs index f5f4213..86b2297 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -1,9 +1,11 @@ // Eryn Wells +mod castle; mod make_move; mod position; pub use { + castle::CastleEvaluationError, make_move::{MakeMoveError, ValidateMove}, - position::{CastleEvaluationError, Position}, + position::Position, }; diff --git a/position/src/position/castle.rs b/position/src/position/castle.rs new file mode 100644 index 0000000..717cc14 --- /dev/null +++ b/position/src/position/castle.rs @@ -0,0 +1,241 @@ +// Eryn Wells + +use crate::Position; +use chessfriend_core::{Color, Piece, Square, Wing}; +use thiserror::Error; + +#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)] +pub enum CastleEvaluationError { + #[error("{color} does not have the right to castle {wing}")] + NoRights { color: Color, wing: Wing }, + #[error("no king")] + NoKing, + #[error("no rook")] + NoRook, + #[error("castling path is not clear")] + ObstructingPieces, + #[error("opposing pieces check castling path")] + CheckingPieces, +} + +impl Position { + /// Evaluates whether the active color can castle toward the given wing of the board in the + /// current position. + /// + /// ## Errors + /// + /// Returns an error indicating why the active color cannot castle. + pub fn active_color_can_castle(&self, wing: Wing) -> Result<(), CastleEvaluationError> { + // TODO: Cache this result. It's expensive! + + let active_color = self.board.active_color; + + if !self + .board + .castling_rights + .color_has_right(active_color, wing) + { + return Err(CastleEvaluationError::NoRights { + color: active_color, + wing, + }); + } + + let parameters = self.board.castling_parameters(wing); + + if self.castling_king(parameters.origin.king).is_none() { + return Err(CastleEvaluationError::NoKing); + } + + if self.castling_rook(parameters.origin.rook).is_none() { + return Err(CastleEvaluationError::NoRook); + } + + // All squares must be clear. + let has_obstructing_pieces = (self.board.occupancy() & parameters.clear).is_populated(); + if has_obstructing_pieces { + return Err(CastleEvaluationError::ObstructingPieces); + } + + // King cannot pass through check. + let opposing_sight = self.opposing_sight(); + let opposing_pieces_can_see_castling_path = + (parameters.check & opposing_sight).is_populated(); + if opposing_pieces_can_see_castling_path { + return Err(CastleEvaluationError::CheckingPieces); + } + + Ok(()) + } + + pub(crate) fn castling_king(&self, square: Square) -> Option { + self.get_piece(square).and_then(|piece| { + if piece.color == self.board.active_color && piece.is_king() { + Some(piece) + } else { + None + } + }) + } + + pub(crate) fn castling_rook(&self, square: Square) -> Option { + self.get_piece(square).and_then(|piece| { + if piece.color == self.board.active_color && piece.is_rook() { + Some(piece) + } else { + None + } + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_position; + use chessfriend_core::{piece, Color, Wing}; + + #[test] + fn king_on_starting_square_can_castle() { + let pos = test_position!( + White King on E1, + White Rook on A1, + White Rook on H1 + ); + + let rights = pos.board.castling_rights; + assert!(rights.color_has_right(Color::White, Wing::KingSide)); + assert!(rights.color_has_right(Color::White, Wing::QueenSide)); + } + + #[test] + fn king_for_castle() { + let pos = test_position![ + White King on E1, + White Rook on H1, + White Rook on A1, + ]; + + let kingside_parameters = pos.board.castling_parameters(Wing::KingSide); + assert_eq!( + pos.castling_king(kingside_parameters.origin.king), + Some(piece!(White King)) + ); + + let queenside_parameters = pos.board.castling_parameters(Wing::QueenSide); + assert_eq!( + pos.castling_king(queenside_parameters.origin.king), + Some(piece!(White King)) + ); + } + + #[test] + fn rook_for_castle() { + let pos = test_position![ + White King on E1, + White Rook on H1, + ]; + + let kingside_parameters = pos.board.castling_parameters(Wing::KingSide); + assert_eq!( + pos.castling_rook(kingside_parameters.origin.rook), + Some(piece!(White Rook)) + ); + + let pos = test_position![ + White King on E1, + White Rook on A1, + ]; + + let queenside_parameters = pos.board.castling_parameters(Wing::QueenSide); + assert_eq!( + pos.castling_rook(queenside_parameters.origin.rook), + Some(piece!(White Rook)) + ); + } + + #[test] + fn white_can_castle() { + let pos = test_position![ + White King on E1, + White Rook on H1, + White Rook on A1, + ]; + + assert_eq!(pos.active_color_can_castle(Wing::KingSide), Ok(())); + assert_eq!(pos.active_color_can_castle(Wing::QueenSide), Ok(())); + } + + #[test] + fn white_cannot_castle_missing_king() { + let pos = test_position![ + White King on E2, + White Rook on H1, + White Rook on A1, + ]; + + assert_eq!( + pos.active_color_can_castle(Wing::KingSide), + Err(CastleEvaluationError::NoKing) + ); + assert_eq!( + pos.active_color_can_castle(Wing::QueenSide), + Err(CastleEvaluationError::NoKing) + ); + } + + #[test] + fn white_cannot_castle_missing_rook() { + let pos = test_position![ + White King on E1, + White Rook on A1, + ]; + + assert_eq!( + pos.active_color_can_castle(Wing::KingSide), + Err(CastleEvaluationError::NoRook) + ); + + let pos = test_position![ + White King on E1, + White Rook on H1, + ]; + + assert_eq!( + pos.active_color_can_castle(Wing::QueenSide), + Err(CastleEvaluationError::NoRook) + ); + } + + #[test] + fn white_cannot_castle_obstructing_piece() { + let pos = test_position![ + White King on E1, + White Bishop on F1, + White Rook on H1, + White Rook on A1, + ]; + + assert_eq!( + pos.active_color_can_castle(Wing::KingSide), + Err(CastleEvaluationError::ObstructingPieces) + ); + assert_eq!(pos.active_color_can_castle(Wing::QueenSide), Ok(())); + } + + #[test] + fn white_cannot_castle_checking_pieces() { + let pos = test_position![ + White King on E1, + White Rook on H1, + White Rook on A1, + Black Queen on C6, + ]; + + assert_eq!(pos.active_color_can_castle(Wing::KingSide), Ok(())); + assert_eq!( + pos.active_color_can_castle(Wing::QueenSide), + Err(CastleEvaluationError::CheckingPieces) + ); + } +} diff --git a/position/src/position/position.rs b/position/src/position/position.rs index 7ab96dd..9095aa7 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -113,91 +113,6 @@ impl Position { } } -#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)] -pub enum CastleEvaluationError { - #[error("{color} does not have the right to castle {wing}")] - NoRights { color: Color, wing: Wing }, - #[error("no king")] - NoKing, - #[error("no rook")] - NoRook, - #[error("castling path is not clear")] - ObstructingPieces, - #[error("opposing pieces check castling path")] - CheckingPieces, -} - -impl Position { - /// Evaluates whether the active color can castle toward the given wing of the board in the - /// current position. - /// - /// ## Errors - /// - /// Returns an error indicating why the active color cannot castle. - pub fn active_color_can_castle(&self, wing: Wing) -> Result<(), CastleEvaluationError> { - // TODO: Cache this result. It's expensive! - - let active_color = self.board.active_color; - - if !self - .board - .castling_rights - .color_has_right(active_color, wing) - { - return Err(CastleEvaluationError::NoRights { - color: active_color, - wing, - }); - } - - let parameters = self.board.castling_parameters(wing); - - if self.castling_king(parameters.origin.king).is_none() { - return Err(CastleEvaluationError::NoKing); - } - - if self.castling_rook(parameters.origin.rook).is_none() { - return Err(CastleEvaluationError::NoRook); - } - - // All squares must be clear. - let has_obstructing_pieces = (self.board.occupancy() & parameters.clear).is_populated(); - if has_obstructing_pieces { - return Err(CastleEvaluationError::ObstructingPieces); - } - - // King cannot pass through check. - let opposing_sight = self.opposing_sight(); - let opposing_pieces_can_see_castling_path = - (parameters.check & opposing_sight).is_populated(); - if opposing_pieces_can_see_castling_path { - return Err(CastleEvaluationError::CheckingPieces); - } - - Ok(()) - } - - fn castling_king(&self, square: Square) -> Option { - self.get_piece(square).and_then(|piece| { - if piece.color == self.board.active_color && piece.is_king() { - Some(piece) - } else { - None - } - }) - } - - fn castling_rook(&self, square: Square) -> Option { - self.get_piece(square).and_then(|piece| { - if piece.color == self.board.active_color && piece.is_rook() { - Some(piece) - } else { - None - } - }) - } -} - /* impl Position { pub fn moves(&self) -> &Moves { @@ -242,28 +157,6 @@ impl Position { self.board.en_passant().map(EnPassant::target_square) } - fn _sight_of_player(&self, player: Color, board: &Board) -> BitBoard { - let en_passant_target_square = self._en_passant_target_square(); - - Shape::ALL - .iter() - .filter_map(|&shape| { - let piece = Piece::new(player, shape); - let bitboard = board.bitboard_for_piece(piece); - if !bitboard.is_empty() { - Some((piece, bitboard)) - } else { - None - } - }) - .flat_map(|(piece, bitboard)| { - bitboard.occupied_squares().map(move |square| { - PlacedPiece::new(piece, square).sight(board, en_passant_target_square) - }) - }) - .fold(BitBoard::empty(), |acc, sight| acc | sight) - } - pub(crate) fn moves_for_piece(&self, piece: &PlacedPiece) -> Option<&MoveSet> { self.moves().moves_for_piece(piece) } @@ -378,43 +271,31 @@ mod tests { assert_eq!(pos.board.get_piece(Square::A8), Some(piece!(Black Rook))); } - #[test] - fn king_is_in_check() { - let pos = position![ - White King on E1, - Black Rook on E8, - ]; - assert!(pos.is_king_in_check()); - } + // #[test] + // fn king_is_in_check() { + // let pos = position![ + // White King on E1, + // Black Rook on E8, + // ]; + // assert!(pos.is_king_in_check()); + // } - #[test] - fn king_is_not_in_check() { - let pos = position![ - White King on F1, - Black Rook on E8, - ]; - assert!(!pos.is_king_in_check()); - } + // #[test] + // fn king_is_not_in_check() { + // let pos = position![ + // White King on F1, + // Black Rook on E8, + // ]; + // assert!(!pos.is_king_in_check()); + // } - #[test] - fn king_not_on_starting_square_cannot_castle() { - let pos = test_position!(White King on E4); - assert!(!pos.player_can_castle(Color::White, Castle::KingSide)); - assert!(!pos.player_can_castle(Color::White, Castle::QueenSide)); - } - - #[test] - fn king_on_starting_square_can_castle() { - let pos = test_position!( - White King on E1, - White Rook on A1, - White Rook on H1 - ); - - let rights = pos.board.castling_rights; - assert!(rights.color_has_right(Color::White, Wing::KingSide)); - assert!(rights.color_has_right(Color::White, Wing::QueenSide)); - } + // #[test] + // fn king_not_on_starting_square_cannot_castle() { + // let pos = test_position!(White King on E4); + // let rights = pos.board.castling_rights; + // assert!(!rights.color_has_right(Color::White, Castle::KingSide)); + // assert!(!rights.color_has_right(Color::White, Castle::QueenSide)); + // } #[test] fn friendly_sight() { @@ -437,134 +318,16 @@ mod tests { assert_eq!(sight, bitboard![A7 B7 C7 D7 F7 G7 H7 E8 E6 E5 E4]); } - #[test] - fn king_for_castle() { - let pos = test_position![ - White King on E1, - White Rook on H1, - White Rook on A1, - ]; + // #[test] + // fn danger_squares() { + // let pos = test_position!(Black, [ + // White King on E1, + // Black King on E7, + // White Rook on E4, + // ]); - let kingside_parameters = pos.board.castling_parameters(Wing::KingSide); - assert_eq!( - pos.castling_king(kingside_parameters.origin.king), - Some(piece!(White King)) - ); - - let queenside_parameters = pos.board.castling_parameters(Wing::QueenSide); - assert_eq!( - pos.castling_king(queenside_parameters.origin.king), - Some(piece!(White King)) - ); - } - - #[test] - fn rook_for_castle() { - let pos = test_position![ - White King on E1, - White Rook on H1, - ]; - - let kingside_parameters = pos.board.castling_parameters(Wing::KingSide); - assert_eq!( - pos.castling_rook(kingside_parameters.origin.rook), - Some(piece!(White Rook)) - ); - - let pos = test_position![ - White King on E1, - White Rook on A1, - ]; - - let queenside_parameters = pos.board.castling_parameters(Wing::QueenSide); - assert_eq!( - pos.castling_rook(queenside_parameters.origin.rook), - Some(piece!(White Rook)) - ); - } - - #[test] - fn white_can_castle() { - let pos = test_position![ - White King on E1, - White Rook on H1, - White Rook on A1, - ]; - - assert_eq!(pos.active_color_can_castle(Wing::KingSide), Ok(())); - assert_eq!(pos.active_color_can_castle(Wing::QueenSide), Ok(())); - } - - #[test] - fn white_cannot_castle_missing_king() { - let pos = test_position![ - White King on E2, - White Rook on H1, - White Rook on A1, - ]; - - assert_eq!( - pos.active_color_can_castle(Wing::KingSide), - Err(CastleEvaluationError::NoKing) - ); - assert_eq!( - pos.active_color_can_castle(Wing::QueenSide), - Err(CastleEvaluationError::NoKing) - ); - } - - #[test] - fn white_cannot_castle_missing_rook() { - let pos = test_position![ - White King on E1, - White Rook on A1, - ]; - - assert_eq!( - pos.active_color_can_castle(Wing::KingSide), - Err(CastleEvaluationError::NoRook) - ); - - let pos = test_position![ - White King on E1, - White Rook on H1, - ]; - - assert_eq!( - pos.active_color_can_castle(Wing::QueenSide), - Err(CastleEvaluationError::NoRook) - ); - } - - #[test] - fn white_cannot_castle_obstructing_piece() { - let pos = test_position![ - White King on E1, - White Bishop on F1, - White Rook on H1, - White Rook on A1, - ]; - - assert_eq!( - pos.active_color_can_castle(Wing::KingSide), - Err(CastleEvaluationError::ObstructingPieces) - ); - assert_eq!(pos.active_color_can_castle(Wing::QueenSide), Ok(())); - } - - #[test] - fn white_cannot_castle_checking_pieces() { - let pos = test_position![ - White King on E1, - White Rook on H1, - White Rook on A1, - Black Queen on C6, - ]; - - assert_eq!(pos.active_color_can_castle(Wing::KingSide), Ok(())); - assert_eq!( - pos.active_color_can_castle(Wing::QueenSide), - Err(CastleEvaluationError::CheckingPieces) - ); - } + // let danger_squares = pos.king_danger(Color::Black); + // let expected = bitboard![D1 F1 D2 E2 F2 E3 A4 B4 C4 D4 F4 G4 H4 E5 E6 E7 E8]; + // assert_eq_bitboards!(danger_squares, expected); + // } } From 6591619e32301a9fe7c209dc3b1cd34e61c541d7 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 19 May 2025 17:00:48 -0700 Subject: [PATCH 279/423] [position] Misc changes - Add a captures list to the Position struct - Implement ToFenStr - Update the imports list, which has been sorely out-of-date in the tree for many commits now. --- position/src/position/position.rs | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/position/src/position/position.rs b/position/src/position/position.rs index 9095aa7..b0e4c8a 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -1,25 +1,25 @@ // Eryn Wells -use crate::{ - check::CheckingPieces, - move_generator::{MoveSet, Moves}, - sight::SightExt, +use crate::{movement::Movement, sight::Sight}; +use chessfriend_bitboard::{BitBoard, IterationDirection}; +use chessfriend_board::{ + display::DiagramFormatter, en_passant::EnPassant, fen::ToFenStr, Board, PlacePieceError, + PlacePieceStrategy, }; -use chessfriend_bitboard::BitBoard; -use chessfriend_board::{castle::Castle, display::DiagramFormatter, en_passant::EnPassant, Board}; -use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; -use chessfriend_moves::Move; -use std::{cell::OnceCell, fmt}; +use chessfriend_core::{Color, Piece, Square}; +use std::{cell::OnceCell, fmt, ops::BitOr}; +#[must_use] #[derive(Clone, Debug, Eq)] pub struct Position { pub board: Board, moves: OnceCell, + pub(super) captures: [Vec; Color::NUM], } impl Position { pub fn empty() -> Self { - Default::default() + Position::default() } /// Return a starting position. @@ -225,11 +225,20 @@ impl Position { } } +impl ToFenStr for Position { + type Error = ::Error; + + fn to_fen_str(&self) -> Result { + self.board.to_fen_str() + } +} + impl Default for Position { fn default() -> Self { Self { board: Board::default(), moves: OnceCell::new(), + captures: Default::default(), } } } From 039fd2b0804215aa96689989290844a21a52c6f7 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 20 May 2025 19:29:02 -0700 Subject: [PATCH 280/423] [position, board, core, moves] Implement a bunch of make_move code Implement making double push and promotion moves. Then write several tests to exercise these. Add convenient static functions to the Move struct to build moves quickly, without using the Builder. Add a is_promotable_rank() method to Rank to check that a rank can be used for promotion moves. The tests found and fixed a bug in pawn movement where the en passant square was being discarded when deciding whether an e.p. move can be made. --- core/src/coordinates.rs | 6 + moves/src/moves.rs | 49 ++++++- position/src/movement.rs | 6 +- position/src/position/make_move.rs | 218 ++++++++++++++++++++++++++--- 4 files changed, 257 insertions(+), 22 deletions(-) diff --git a/core/src/coordinates.rs b/core/src/coordinates.rs index a81daad..919e262 100644 --- a/core/src/coordinates.rs +++ b/core/src/coordinates.rs @@ -210,6 +210,12 @@ impl Rank { pub fn is_pawn_double_push_target_rank(&self, color: Color) -> bool { self == &Self::PAWN_DOUBLE_PUSH_TARGET_RANKS[color as usize] } + + /// Ranks where promotions happen. + #[must_use] + pub fn is_promotable_rank(&self) -> bool { + matches!(*self, Rank::ONE | Rank::EIGHT) + } } #[rustfmt::skip] diff --git a/moves/src/moves.rs b/moves/src/moves.rs index 94f79a3..30293ec 100644 --- a/moves/src/moves.rs +++ b/moves/src/moves.rs @@ -1,7 +1,6 @@ // Eryn Wells -use crate::builder::Builder; -use crate::defs::Kind; +use crate::defs::{Kind, PromotionShape}; use chessfriend_core::{Rank, Shape, Square, Wing}; use std::fmt; @@ -14,6 +13,48 @@ use std::fmt; #[derive(Clone, Copy, Eq, Hash, PartialEq)] pub struct Move(pub(crate) u16); +impl Move { + #[must_use] + pub fn quiet(origin: Square, target: Square) -> Self { + let origin_bits = (origin as u16) << 4; + let target_bits = (target as u16) << 10; + Move(origin_bits | target_bits) + } + + #[must_use] + pub fn double_push(origin: Square, target: Square) -> Self { + let origin_bits = (origin as u16) << 4; + let target_bits = (target as u16) << 10; + let flag_bits = Kind::DoublePush as u16; + Move(origin_bits | target_bits | flag_bits) + } + + #[must_use] + pub fn capture(origin: Square, target: Square) -> Self { + let origin_bits = (origin as u16) << 4; + let target_bits = (target as u16) << 10; + let flag_bits = Kind::Capture as u16; + Move(origin_bits | target_bits | flag_bits) + } + + #[must_use] + pub fn en_passant_capture(origin: Square, target: Square) -> Self { + let origin_bits = (origin as u16) << 4; + let target_bits = (target as u16) << 10; + let flag_bits = Kind::EnPassantCapture as u16; + Move(origin_bits | target_bits | flag_bits) + } + + #[must_use] + pub fn promotion(origin: Square, target: Square, shape: PromotionShape) -> Self { + let origin_bits = (origin as u16) << 4; + let target_bits = (target as u16) << 10; + let flag_bits = Kind::Promotion as u16; + let shape_bits = shape as u16; + Move(origin_bits | target_bits | flag_bits | shape_bits) + } +} + impl Move { #[must_use] #[allow(clippy::missing_panics_doc)] @@ -85,7 +126,7 @@ impl Move { } #[must_use] - pub fn promotion(&self) -> Option { + pub fn promotion_shape(&self) -> Option { if !self.is_promotion() { return None; } @@ -139,7 +180,7 @@ impl fmt::Display for Move { let transfer_char = self.transfer_char(); write!(f, "{origin}{transfer_char}{target}")?; - if let Some(promotion) = self.promotion() { + if let Some(promotion) = self.promotion_shape() { write!(f, "={promotion}")?; } else if self.is_en_passant() { write!(f, " e.p.")?; diff --git a/position/src/movement.rs b/position/src/movement.rs index a3ee154..1d26e4b 100644 --- a/position/src/movement.rs +++ b/position/src/movement.rs @@ -15,11 +15,13 @@ pub trait Movement { impl Movement for Piece { fn movement(&self, square: Square, board: &Board) -> BitBoard { + let opposing_occupancy = board.opposing_occupancy(self.color); + match self.shape { Shape::Pawn => { + let en_passant_square: BitBoard = board.en_passant_target.into(); // Pawns can only move to squares they can see to capture. - let opposing_occupancy = board.opposing_occupancy(self.color); - let sight = self.sight(square, board) & opposing_occupancy; + let sight = self.sight(square, board) & (opposing_occupancy | en_passant_square); let pushes = pawn_pushes(square.into(), self.color, board.occupancy()); sight | pushes } diff --git a/position/src/position/make_move.rs b/position/src/position/make_move.rs index abc5515..8c0597c 100644 --- a/position/src/position/make_move.rs +++ b/position/src/position/make_move.rs @@ -1,8 +1,8 @@ // Eryn Wells use crate::{movement::Movement, Position}; -use chessfriend_board::{CastleParameters, PlacePieceError, PlacePieceStrategy}; -use chessfriend_core::{Color, Piece, Square, Wing}; +use chessfriend_board::{en_passant, CastleParameters, PlacePieceError, PlacePieceStrategy}; +use chessfriend_core::{Color, Piece, Rank, Shape, Square, Wing}; use chessfriend_moves::Move; use thiserror::Error; @@ -25,12 +25,21 @@ pub enum MakeMoveError { #[error("{piece} on {square} is not of active color")] NonActiveColor { piece: Piece, square: Square }, + #[error("{0} cannot make move")] + InvalidPiece(Piece), + #[error("cannot capture piece on {0}")] InvalidCapture(Square), + #[error("cannot capture en passant on {0}")] + InvalidEnPassantCapture(Square), + #[error("no capture square")] NoCaptureSquare, + #[error("no piece to capture on {0}")] + NoCapturePiece(Square), + #[error("{piece} on {origin} cannot move to {target}")] NoMove { piece: Piece, @@ -43,6 +52,9 @@ pub enum MakeMoveError { #[error("{0}")] CastleError(#[from] CastleEvaluationError), + + #[error("cannot promote on {0}")] + InvalidPromotion(Square), } pub enum UnmakeMoveError {} @@ -61,6 +73,10 @@ impl Position { return self.make_quiet_move(ply.origin_square(), ply.target_square()); } + if ply.is_double_push() { + return self.make_double_push_move(ply); + } + if ply.is_capture() { return self.make_capture_move(ply); } @@ -69,6 +85,10 @@ impl Position { return self.make_castle_move(wing); } + if ply.is_promotion() { + return self.make_promotion_move(ply); + } + Ok(()) } @@ -91,19 +111,59 @@ impl Position { Ok(()) } + fn make_double_push_move(&mut self, ply: Move) -> MakeMoveResult { + let origin = ply.origin_square(); + let piece = self + .board + .remove_piece(origin) + .ok_or(MakeMoveError::NoPiece(origin))?; + + let target = ply.target_square(); + self.place_active_piece(piece, target)?; + + self.board.en_passant_target = match target.rank() { + Rank::FOUR => Some(Square::from_file_rank(target.file(), Rank::THREE)), + Rank::FIVE => Some(Square::from_file_rank(target.file(), Rank::SIX)), + _ => unreachable!(), + }; + + self.advance_clocks(HalfMoveClock::Advance); + + Ok(()) + } + fn make_capture_move(&mut self, ply: Move) -> MakeMoveResult { let origin_square = ply.origin_square(); + let target_square = ply.target_square(); + let piece = self.get_piece_for_move(origin_square)?; + if ply.is_en_passant() { + let en_passant_square = self + .board + .en_passant_target + .ok_or(MakeMoveError::NoCaptureSquare)?; + if target_square != en_passant_square { + return Err(MakeMoveError::InvalidEnPassantCapture(target_square)); + } + } + let capture_square = ply.capture_square().ok_or(MakeMoveError::NoCaptureSquare)?; - let captured_piece = self.get_piece_for_move(capture_square)?; + let captured_piece = self + .remove_piece(capture_square) + .ok_or(MakeMoveError::NoCapturePiece(capture_square))?; // Register the capture self.captures[piece.color as usize].push(captured_piece); self.remove_piece(origin_square).unwrap(); - let target_square = ply.target_square(); - self.place_piece(piece, target_square, PlacePieceStrategy::Replace)?; + + if let Some(promotion_shape) = ply.promotion_shape() { + let promoted_piece = Piece::new(piece.color, promotion_shape); + self.place_piece(promoted_piece, target_square, PlacePieceStrategy::Replace)?; + } else { + self.place_piece(piece, target_square, PlacePieceStrategy::Replace)?; + } self.advance_clocks(HalfMoveClock::Reset); @@ -117,12 +177,10 @@ impl Position { let parameters = self.board.castling_parameters(wing); let king = self.board.remove_piece(parameters.origin.king).unwrap(); - self.board - .place_piece(king, parameters.target.king, PlacePieceStrategy::default())?; + self.place_piece(king, parameters.target.king, PlacePieceStrategy::default())?; let rook = self.board.remove_piece(parameters.origin.rook).unwrap(); - self.board - .place_piece(rook, parameters.target.rook, PlacePieceStrategy::default())?; + self.place_piece(rook, parameters.target.rook, PlacePieceStrategy::default())?; self.board.castling_rights.revoke(active_color, wing); @@ -130,6 +188,34 @@ impl Position { Ok(()) } + + fn make_promotion_move(&mut self, ply: Move) -> MakeMoveResult { + let origin = ply.origin_square(); + + let piece = self.get_piece_for_move(origin)?; + if !piece.is_pawn() { + return Err(MakeMoveError::InvalidPiece(piece)); + } + + let target = ply.target_square(); + if !target.rank().is_promotable_rank() { + return Err(MakeMoveError::InvalidPromotion(target)); + } + + if let Some(promotion_shape) = ply.promotion_shape() { + self.remove_piece(origin); + let promoted_piece = Piece::new(piece.color, promotion_shape); + self.place_piece(promoted_piece, target, PlacePieceStrategy::PreserveExisting)?; + } else { + unreachable!( + "Cannot make a promotion move with a ply that has no promotion shape: {ply:?}", + ); + } + + self.advance_clocks(HalfMoveClock::Reset); + + Ok(()) + } } impl Position { @@ -138,8 +224,7 @@ impl Position { } fn place_active_piece(&mut self, piece: Piece, square: Square) -> MakeMoveResult { - self.board - .place_piece(piece, square, PlacePieceStrategy::PreserveExisting) + self.place_piece(piece, square, PlacePieceStrategy::PreserveExisting) .map_err(MakeMoveError::PlacePieceError) } } @@ -191,14 +276,13 @@ impl Position { // TODO: En Passant capture. - if ply.is_capture() { - let target = ply.target_square(); - if let Some(captured_piece) = self.board.get_piece(target) { + if let Some(capture_square) = ply.capture_square() { + if let Some(captured_piece) = self.board.get_piece(capture_square) { if captured_piece.color == active_piece.color { - return Err(MakeMoveError::InvalidCapture(target)); + return Err(MakeMoveError::InvalidCapture(capture_square)); } } else { - return Err(MakeMoveError::NoPiece(target)); + return Err(MakeMoveError::NoPiece(capture_square)); } } @@ -223,3 +307,105 @@ impl Position { Ok(active_piece) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{test_position, ValidateMove}; + use chessfriend_core::{piece, Color, Square}; + use chessfriend_moves::{Move, PromotionShape}; + + #[test] + fn make_quiet_move() -> MakeMoveResult { + let mut pos = test_position!(White Pawn on C2); + + let ply = Move::quiet(Square::C2, Square::C3); + pos.make_move(ply, ValidateMove::Yes)?; + + assert_eq!(pos.get_piece(Square::C2), None); + assert_eq!(pos.get_piece(Square::C3), Some(piece!(White Pawn))); + assert_eq!(pos.board.active_color, Color::Black); + assert_eq!(pos.board.half_move_clock, 1); + + pos.board.active_color = Color::White; + + let ply = Move::quiet(Square::C3, Square::C4); + pos.make_move(ply, ValidateMove::Yes)?; + + assert_eq!(pos.get_piece(Square::C3), None); + assert_eq!(pos.get_piece(Square::C4), Some(piece!(White Pawn))); + assert_eq!(pos.board.active_color, Color::Black); + assert_eq!(pos.board.half_move_clock, 2); + + Ok(()) + } + + #[test] + fn make_capture_move() { + let mut pos = test_position![ + White Bishop on C2, + Black Rook on F5, + ]; + + let ply = Move::capture(Square::C2, Square::F5); + assert_eq!(pos.make_move(ply, ValidateMove::Yes), Ok(())); + assert_eq!(pos.get_piece(Square::C2), None); + assert_eq!(pos.get_piece(Square::F5), Some(piece!(White Bishop))); + assert_eq!(pos.captures[Color::White as usize][0], piece!(Black Rook)); + assert_eq!(pos.board.active_color, Color::Black); + assert_eq!(pos.board.half_move_clock, 0); + } + + #[test] + fn make_en_passant_capture_move() -> MakeMoveResult { + let mut pos = test_position![ + Black Pawn on F4, + White Pawn on E2 + ]; + + let ply = Move::double_push(Square::E2, Square::E4); + pos.make_move(ply, ValidateMove::Yes)?; + + assert_eq!(pos.get_piece(Square::E2), None); + assert_eq!(pos.get_piece(Square::E4), Some(piece!(White Pawn))); + assert_eq!( + pos.board.en_passant_target, + Some(Square::E3), + "en passant square not set" + ); + assert_eq!(pos.board.active_color, Color::Black); + assert_eq!(pos.board.half_move_clock, 1); + + let ply = Move::en_passant_capture(Square::F4, Square::E3); + pos.make_move(ply, ValidateMove::Yes)?; + + assert_eq!(pos.get_piece(Square::F4), None); + assert_eq!(pos.get_piece(Square::E3), Some(piece!(Black Pawn))); + assert_eq!( + pos.get_piece(Square::E4), + None, + "capture target pawn not removed" + ); + assert_eq!(pos.captures[Color::Black as usize][0], piece!(White Pawn)); + + Ok(()) + } + + #[test] + fn make_promotion_move() -> MakeMoveResult { + let mut pos = test_position![ + Black Pawn on E7, + White Pawn on F7, + ]; + + let ply = Move::promotion(Square::F7, Square::F8, PromotionShape::Queen); + pos.make_move(ply, ValidateMove::Yes)?; + + assert_eq!(pos.get_piece(Square::F7), None); + assert_eq!(pos.get_piece(Square::F8), Some(piece!(White Queen))); + assert_eq!(pos.board.active_color, Color::Black); + assert_eq!(pos.board.half_move_clock, 0); + + Ok(()) + } +} From 7c9c5484ba812e66da55b7112e1952252c9ecd1d Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 20 May 2025 19:29:39 -0700 Subject: [PATCH 281/423] [position, board] Remove a bunch of dead code --- board/src/move_counter.rs | 74 ------------------------------- position/src/lib.rs | 3 -- position/src/position/position.rs | 14 ------ position/src/sight/rays.rs | 27 ----------- 4 files changed, 118 deletions(-) delete mode 100644 board/src/move_counter.rs delete mode 100644 position/src/sight/rays.rs diff --git a/board/src/move_counter.rs b/board/src/move_counter.rs deleted file mode 100644 index 5de7760..0000000 --- a/board/src/move_counter.rs +++ /dev/null @@ -1,74 +0,0 @@ -use chessfriend_core::Color; - -#[derive(Default)] -pub enum AdvanceHalfMove { - Reset, - #[default] - Advance, -} - -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -pub struct Clock { - /// The number of completed turns. A turn finishes when every player has moved. - pub full_move_number: u16, - - /// The number of moves by all players since the last pawn advance or capture. - pub half_move_number: u16, -} - -impl Clock { - pub fn advance(&mut self, advance_half_move: &AdvanceHalfMove) { - let next_color = self.active_color.next(); - - match self.active_color { - Color::Black => self.full_move_number += 1, - Color::White => {} - } - - self.half_move_number = match advance_half_move { - AdvanceHalfMove::Reset => 0, - AdvanceHalfMove::Advance => self.half_move_number + 1, - }; - - self.active_color = next_color; - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn default_state() { - let clock = Clock::default(); - assert_eq!(clock.active_color, Color::White); - assert_eq!(clock.half_move_number, 0); - assert_eq!(clock.full_move_number, 0); - } - - #[test] - fn advance() { - let mut clock = Clock::default(); - - clock.advance(&AdvanceHalfMove::default()); - assert_eq!(clock.active_color, Color::Black); - assert_eq!(clock.half_move_number, 1); - assert_eq!(clock.full_move_number, 0); - - clock.advance(&AdvanceHalfMove::default()); - assert_eq!(clock.active_color, Color::White); - assert_eq!(clock.half_move_number, 2); - assert_eq!(clock.full_move_number, 1); - - clock.advance(&AdvanceHalfMove::default()); - assert_eq!(clock.active_color, Color::Black); - assert_eq!(clock.half_move_number, 3); - assert_eq!(clock.full_move_number, 1); - - // The half move clock resets after a capture or pawn push. - clock.advance(&AdvanceHalfMove::Reset); - assert_eq!(clock.active_color, Color::White); - assert_eq!(clock.half_move_number, 0); - assert_eq!(clock.full_move_number, 2); - } -} diff --git a/position/src/lib.rs b/position/src/lib.rs index a17c5f6..b43d063 100644 --- a/position/src/lib.rs +++ b/position/src/lib.rs @@ -1,8 +1,5 @@ // Eryn Wells -mod check; -mod display; -mod move_generator; mod movement; mod position; mod sight; diff --git a/position/src/position/position.rs b/position/src/position/position.rs index b0e4c8a..7ea543b 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -13,7 +13,6 @@ use std::{cell::OnceCell, fmt, ops::BitOr}; #[derive(Clone, Debug, Eq)] pub struct Position { pub board: Board, - moves: OnceCell, pub(super) captures: [Vec; Color::NUM], } @@ -145,18 +144,6 @@ impl Position { }) } - pub fn has_en_passant_square(&self) -> bool { - self.board.en_passant().is_some() - } - - pub fn en_passant(&self) -> Option { - self.board.en_passant() - } - - fn _en_passant_target_square(&self) -> Option { - self.board.en_passant().map(EnPassant::target_square) - } - pub(crate) fn moves_for_piece(&self, piece: &PlacedPiece) -> Option<&MoveSet> { self.moves().moves_for_piece(piece) } @@ -237,7 +224,6 @@ impl Default for Position { fn default() -> Self { Self { board: Board::default(), - moves: OnceCell::new(), captures: Default::default(), } } diff --git a/position/src/sight/rays.rs b/position/src/sight/rays.rs deleted file mode 100644 index df376d7..0000000 --- a/position/src/sight/rays.rs +++ /dev/null @@ -1,27 +0,0 @@ -// Eryn Wells - -use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Direction, Square}; - -fn _find_ray_connecting_squares(origin_square: Square, target_square: Square) -> BitBoard { - macro_rules! ray { - ($square:expr, $direction:expr) => { - ($direction, BitBoard::ray($square, $direction)) - }; - } - - let target: BitBoard = target_square.into(); - match Direction::ALL - .iter() - .map(|direction| ray!(origin_square, *direction)) - .find(|(direction, &ray)| (ray & target).is_populated()) - { - Some((direction, ray)) => { - if let Some(first_occupied_square) = BitBoard::$occupied_squares(&(ray & $blockers)).next() - let remainder = BitBoard::ray(first_occupied_square, direction); - let attack_ray = ray & !remainder; - attack_ray * ray - } - None => BitBoard::EMPTY, - } -} From feaa81bbd8ef7aaa0f0900d4f950a7fb1572b141 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 21 May 2025 08:25:49 -0700 Subject: [PATCH 282/423] [position, moves] Implement some castling tests Add white castling for both wings. Found some bugs in how king sight is computed while writing these. In order for the king to perform the castle, the Movement bitboard needs to return the two squares the king can castle to. That means anytime movement is calculated for the king, the (relatively expensive) castling evaluation needs to happen. Write two new helper static functions to create a Move for castling and promotion moves. Since structs cannot have any functions with the same name, the two methods that return properties related to those moves (Move::castle and Move::promotion) need to be renamed. They're now called Move::castle_wing and Move::promotion_shape. --- moves/src/moves.rs | 41 ++++++++++++++---------- moves/tests/flags.rs | 4 +-- position/src/movement.rs | 29 ++++++++++++++--- position/src/position/make_move.rs | 50 ++++++++++++++++++++++++++++-- position/src/position/position.rs | 2 +- 5 files changed, 99 insertions(+), 27 deletions(-) diff --git a/moves/src/moves.rs b/moves/src/moves.rs index 30293ec..f489ad6 100644 --- a/moves/src/moves.rs +++ b/moves/src/moves.rs @@ -13,45 +13,52 @@ use std::fmt; #[derive(Clone, Copy, Eq, Hash, PartialEq)] pub struct Move(pub(crate) u16); +fn origin_bits(square: Square) -> u16 { + (square as u16) << 4 +} + +fn target_bits(square: Square) -> u16 { + (square as u16) << 10 +} + impl Move { #[must_use] pub fn quiet(origin: Square, target: Square) -> Self { - let origin_bits = (origin as u16) << 4; - let target_bits = (target as u16) << 10; - Move(origin_bits | target_bits) + Move(origin_bits(origin) | target_bits(target)) } #[must_use] pub fn double_push(origin: Square, target: Square) -> Self { - let origin_bits = (origin as u16) << 4; - let target_bits = (target as u16) << 10; let flag_bits = Kind::DoublePush as u16; - Move(origin_bits | target_bits | flag_bits) + Move(origin_bits(origin) | target_bits(target) | flag_bits) } #[must_use] pub fn capture(origin: Square, target: Square) -> Self { - let origin_bits = (origin as u16) << 4; - let target_bits = (target as u16) << 10; let flag_bits = Kind::Capture as u16; - Move(origin_bits | target_bits | flag_bits) + Move(origin_bits(origin) | target_bits(target) | flag_bits) } #[must_use] pub fn en_passant_capture(origin: Square, target: Square) -> Self { - let origin_bits = (origin as u16) << 4; - let target_bits = (target as u16) << 10; let flag_bits = Kind::EnPassantCapture as u16; - Move(origin_bits | target_bits | flag_bits) + Move(origin_bits(origin) | target_bits(target) | flag_bits) } #[must_use] pub fn promotion(origin: Square, target: Square, shape: PromotionShape) -> Self { - let origin_bits = (origin as u16) << 4; - let target_bits = (target as u16) << 10; let flag_bits = Kind::Promotion as u16; let shape_bits = shape as u16; - Move(origin_bits | target_bits | flag_bits | shape_bits) + Move(origin_bits(origin) | target_bits(target) | flag_bits | shape_bits) + } + + #[must_use] + pub fn castle(origin: Square, target: Square, wing: Wing) -> Self { + let flag_bits = match wing { + Wing::KingSide => Kind::KingSideCastle, + Wing::QueenSide => Kind::QueenSideCastle, + } as u16; + Move(origin_bits(origin) | target_bits(target) | flag_bits) } } @@ -102,7 +109,7 @@ impl Move { } #[must_use] - pub fn castle(&self) -> Option { + pub fn castle_wing(&self) -> Option { match self.flags() { 0b0010 => Some(Wing::KingSide), 0b0011 => Some(Wing::QueenSide), @@ -168,7 +175,7 @@ const QUEENSIDE_CASTLE_STR: &str = "0-0-0"; impl fmt::Display for Move { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(castle) = self.castle() { + if let Some(castle) = self.castle_wing() { return match castle { Wing::KingSide => write!(f, "{KINGSIDE_CASTLE_STR}"), Wing::QueenSide => write!(f, "{QUEENSIDE_CASTLE_STR}"), diff --git a/moves/tests/flags.rs b/moves/tests/flags.rs index ecf194a..f4d7530 100644 --- a/moves/tests/flags.rs +++ b/moves/tests/flags.rs @@ -78,7 +78,7 @@ fn move_flags_promotion() -> TestResult { .build()?; assert!(ply.is_promotion()); - assert_eq!(ply.promotion(), Some(Shape::Queen)); + assert_eq!(ply.promotion_shape(), Some(Shape::Queen)); Ok(()) } @@ -93,7 +93,7 @@ fn move_flags_capture_promotion() -> TestResult { assert!(ply.is_capture()); assert!(ply.is_promotion()); - assert_eq!(ply.promotion(), Some(Shape::Queen)); + assert_eq!(ply.promotion_shape(), Some(Shape::Queen)); Ok(()) } diff --git a/position/src/movement.rs b/position/src/movement.rs index 1d26e4b..712efd1 100644 --- a/position/src/movement.rs +++ b/position/src/movement.rs @@ -4,17 +4,17 @@ //! of squares a piece can move to. For all pieces except pawns, the Movement //! set is equal to the Sight set. -use crate::sight::Sight; +use crate::{sight::Sight, Position}; use chessfriend_bitboard::BitBoard; -use chessfriend_board::Board; -use chessfriend_core::{Color, Piece, Rank, Shape, Square}; +use chessfriend_core::{Color, Piece, Rank, Shape, Square, Wing}; pub trait Movement { - fn movement(&self, square: Square, board: &Board) -> BitBoard; + fn movement(&self, square: Square, position: &Position) -> BitBoard; } impl Movement for Piece { - fn movement(&self, square: Square, board: &Board) -> BitBoard { + fn movement(&self, square: Square, position: &Position) -> BitBoard { + let board = &position.board; let opposing_occupancy = board.opposing_occupancy(self.color); match self.shape { @@ -25,6 +25,25 @@ impl Movement for Piece { let pushes = pawn_pushes(square.into(), self.color, board.occupancy()); sight | pushes } + Shape::King => { + let kingside_target_square = + if position.active_color_can_castle(Wing::KingSide).is_ok() { + let parameters = board.castling_parameters(Wing::KingSide); + parameters.target.king.into() + } else { + BitBoard::empty() + }; + + let queenside_target_square = + if position.active_color_can_castle(Wing::QueenSide).is_ok() { + let parameters = board.castling_parameters(Wing::QueenSide); + parameters.target.king.into() + } else { + BitBoard::empty() + }; + + self.sight(square, board) | kingside_target_square | queenside_target_square + } _ => self.sight(square, board), } } diff --git a/position/src/position/make_move.rs b/position/src/position/make_move.rs index 8c0597c..fcefc24 100644 --- a/position/src/position/make_move.rs +++ b/position/src/position/make_move.rs @@ -81,7 +81,7 @@ impl Position { return self.make_capture_move(ply); } - if let Some(wing) = ply.castle() { + if let Some(wing) = ply.castle_wing() { return self.make_castle_move(wing); } @@ -265,7 +265,7 @@ impl Position { // Pawns can see squares they can't move to. So, calculating valid // squares requires a concept that includes Sight, but adds pawn pushes. // In ChessFriend, that concept is Movement. - let movement = active_piece.movement(origin_square, &self.board); + let movement = active_piece.movement(origin_square, self); if !movement.contains(target_square) { return Err(MakeMoveError::NoMove { piece: active_piece, @@ -408,4 +408,50 @@ mod tests { Ok(()) } + + #[test] + fn make_white_kingside_castle() -> MakeMoveResult { + let mut pos = test_position![ + White Rook on H1, + White King on E1, + ]; + + let ply = Move::castle(Square::E1, Square::G1, Wing::KingSide); + pos.make_move(ply, ValidateMove::Yes)?; + + assert_eq!(pos.board.active_color, Color::Black); + assert_eq!(pos.get_piece(Square::E1), None); + assert_eq!(pos.get_piece(Square::H1), None); + assert_eq!(pos.get_piece(Square::G1), Some(piece!(White King))); + assert_eq!(pos.get_piece(Square::F1), Some(piece!(White Rook))); + assert!(!pos + .board + .castling_rights + .color_has_right(Color::White, Wing::KingSide)); + + Ok(()) + } + + #[test] + fn make_white_queenside_castle() -> MakeMoveResult { + let mut pos = test_position![ + White King on E1, + White Rook on A1, + ]; + + let ply = Move::castle(Square::E1, Square::C1, Wing::QueenSide); + pos.make_move(ply, ValidateMove::Yes)?; + + assert_eq!(pos.board.active_color, Color::Black); + assert_eq!(pos.get_piece(Square::E1), None); + assert_eq!(pos.get_piece(Square::A1), None); + assert_eq!(pos.get_piece(Square::C1), Some(piece!(White King))); + assert_eq!(pos.get_piece(Square::D1), Some(piece!(White Rook))); + assert!(!pos + .board + .castling_rights + .color_has_right(Color::White, Wing::QueenSide)); + + Ok(()) + } } diff --git a/position/src/position/position.rs b/position/src/position/position.rs index 7ea543b..7b8c352 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -73,7 +73,7 @@ impl Position { pub fn movement(&self, square: Square) -> BitBoard { if let Some(piece) = self.get_piece(square) { - piece.movement(square, &self.board) + piece.movement(square, self) } else { BitBoard::empty() } From 85c1a395c4547ae9fcdb4454085e2ec8c2480350 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 21 May 2025 08:26:34 -0700 Subject: [PATCH 283/423] [position] Streamline the implementation of castling_{king,rook} Instead of using .and_then with an if/else, use .filter and pass the predicate. Saves a few lines. --- position/src/position/castle.rs | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/position/src/position/castle.rs b/position/src/position/castle.rs index 717cc14..cdf4900 100644 --- a/position/src/position/castle.rs +++ b/position/src/position/castle.rs @@ -69,23 +69,15 @@ impl Position { } pub(crate) fn castling_king(&self, square: Square) -> Option { - self.get_piece(square).and_then(|piece| { - if piece.color == self.board.active_color && piece.is_king() { - Some(piece) - } else { - None - } - }) + let active_color = self.board.active_color; + self.get_piece(square) + .filter(|piece| piece.color == active_color && piece.is_king()) } pub(crate) fn castling_rook(&self, square: Square) -> Option { - self.get_piece(square).and_then(|piece| { - if piece.color == self.board.active_color && piece.is_rook() { - Some(piece) - } else { - None - } - }) + let active_color = self.board.active_color; + self.get_piece(square) + .filter(|piece| piece.color == active_color && piece.is_rook()) } } From 10ba21f7e3e03866dc1f958971735e6692954642 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 21 May 2025 08:27:15 -0700 Subject: [PATCH 284/423] [explorer] Remove the unused starting command; add aliases to make (m) and place (p) --- explorer/src/main.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/explorer/src/main.rs b/explorer/src/main.rs index 8ba0a6b..9f63e83 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -46,6 +46,7 @@ fn command_line() -> Command { Command::new("make") .arg(Arg::new("from").required(true)) .arg(Arg::new("to").required(true)) + .alias("m") .about("Make a move"), ) .subcommand( @@ -53,6 +54,7 @@ fn command_line() -> Command { .arg(Arg::new("color").required(true)) .arg(Arg::new("piece").required(true)) .arg(Arg::new("square").required(true)) + .alias("p") .about("Place a piece on the board"), ) .subcommand( @@ -78,7 +80,6 @@ fn command_line() -> Command { ) .subcommand(Command::new("print").about("Print the board")) .subcommand(Command::new("quit").alias("exit").about("Quit the program")) - .subcommand(Command::new("starting").about("Reset the board to the starting position")) } #[derive(Clone, Debug, Error, Eq, PartialEq)] From 9a4fa827f987ea56041fc1afaa3172a4e89fcc52 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 21 May 2025 09:51:16 -0700 Subject: [PATCH 285/423] [position] Add two new negative tests for making pawn moves - Ensure you cannot move a pawn to the last rank without a promotion move. - Ensure a pawn cannot make an illegal move, and that the board state remains as it was before the move was attempted. --- position/src/position/make_move.rs | 59 +++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/position/src/position/make_move.rs b/position/src/position/make_move.rs index fcefc24..e8502e9 100644 --- a/position/src/position/make_move.rs +++ b/position/src/position/make_move.rs @@ -1,8 +1,8 @@ // Eryn Wells use crate::{movement::Movement, Position}; -use chessfriend_board::{en_passant, CastleParameters, PlacePieceError, PlacePieceStrategy}; -use chessfriend_core::{Color, Piece, Rank, Shape, Square, Wing}; +use chessfriend_board::{PlacePieceError, PlacePieceStrategy}; +use chessfriend_core::{Color, Piece, Rank, Square, Wing}; use chessfriend_moves::Move; use thiserror::Error; @@ -55,6 +55,9 @@ pub enum MakeMoveError { #[error("cannot promote on {0}")] InvalidPromotion(Square), + + #[error("move to {0} requires promotion")] + PromotionRequired(Square), } pub enum UnmakeMoveError {} @@ -70,7 +73,7 @@ impl Position { self.validate_move(ply, validate)?; if ply.is_quiet() { - return self.make_quiet_move(ply.origin_square(), ply.target_square()); + return self.make_quiet_move(ply); } if ply.is_double_push() { @@ -98,13 +101,17 @@ impl Position { } impl Position { - fn make_quiet_move(&mut self, origin: Square, target: Square) -> MakeMoveResult { + fn make_quiet_move(&mut self, ply: Move) -> MakeMoveResult { + let origin = ply.origin_square(); + let piece = self - .board - .remove_piece(origin) + .get_piece(origin) .ok_or(MakeMoveError::NoPiece(origin))?; - self.place_active_piece(piece, target)?; + let target = ply.target_square(); + self.place_piece_for_move(piece, target)?; + + self.remove_piece(origin); self.advance_clocks(HalfMoveClock::Advance); @@ -119,7 +126,7 @@ impl Position { .ok_or(MakeMoveError::NoPiece(origin))?; let target = ply.target_square(); - self.place_active_piece(piece, target)?; + self.place_piece_for_move(piece, target)?; self.board.en_passant_target = match target.rank() { Rank::FOUR => Some(Square::from_file_rank(target.file(), Rank::THREE)), @@ -223,7 +230,11 @@ impl Position { self.get_piece(square).ok_or(MakeMoveError::NoPiece(square)) } - fn place_active_piece(&mut self, piece: Piece, square: Square) -> MakeMoveResult { + fn place_piece_for_move(&mut self, piece: Piece, square: Square) -> MakeMoveResult { + if piece.is_pawn() && square.rank().is_promotable_rank() { + return Err(MakeMoveError::PromotionRequired(square)); + } + self.place_piece(piece, square, PlacePieceStrategy::PreserveExisting) .map_err(MakeMoveError::PlacePieceError) } @@ -340,6 +351,20 @@ mod tests { Ok(()) } + #[test] + fn make_invalid_quiet_pawn_move() { + let mut pos = test_position!(White Pawn on C2); + + let ply = Move::quiet(Square::C2, Square::D2); + let result = pos.make_move(ply, ValidateMove::Yes); + + assert!(result.is_err()); + assert_eq!(pos.get_piece(Square::C2), Some(piece!(White Pawn))); + assert_eq!(pos.get_piece(Square::D2), None); + assert_eq!(pos.board.active_color, Color::White); + assert_eq!(pos.board.half_move_clock, 0); + } + #[test] fn make_capture_move() { let mut pos = test_position![ @@ -391,6 +416,22 @@ mod tests { Ok(()) } + #[test] + fn make_last_rank_quiet_move_without_promotion() { + let mut pos = test_position!( + White Pawn on A7 + ); + + let ply = Move::quiet(Square::A7, Square::A8); + let result = pos.make_move(ply, ValidateMove::Yes); + + assert!(result.is_err()); + assert_eq!(pos.board.active_color, Color::White); + assert_eq!(pos.get_piece(Square::A7), Some(piece!(White Pawn))); + assert_eq!(pos.get_piece(Square::A8), None); + assert_eq!(pos.board.half_move_clock, 0); + } + #[test] fn make_promotion_move() -> MakeMoveResult { let mut pos = test_position![ From dbca7b4f883b56c12ecf0f4b11f720c6bb0d5287 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 21 May 2025 10:08:59 -0700 Subject: [PATCH 286/423] [position, board] Move castle, movement, and sight modules to the board crate Nothing about this code depends on Position, so push it down to a lower layer. --- board/src/castle.rs | 229 ++++++++++++++++++++++++++++ board/src/lib.rs | 2 + {position => board}/src/movement.rs | 21 ++- {position => board}/src/sight.rs | 91 +++++++++-- position/src/lib.rs | 2 - position/src/position/make_move.rs | 6 +- position/src/position/position.rs | 63 +------- 7 files changed, 334 insertions(+), 80 deletions(-) rename {position => board}/src/movement.rs (88%) rename {position => board}/src/sight.rs (79%) diff --git a/board/src/castle.rs b/board/src/castle.rs index c29859e..40322cb 100644 --- a/board/src/castle.rs +++ b/board/src/castle.rs @@ -5,3 +5,232 @@ mod rights; pub use parameters::Parameters; pub use rights::Rights; + +use crate::Board; +use chessfriend_core::{Color, Piece, Square, Wing}; +use thiserror::Error; + +#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)] +pub enum CastleEvaluationError { + #[error("{color} does not have the right to castle {wing}")] + NoRights { color: Color, wing: Wing }, + #[error("no king")] + NoKing, + #[error("no rook")] + NoRook, + #[error("castling path is not clear")] + ObstructingPieces, + #[error("opposing pieces check castling path")] + CheckingPieces, +} + +impl Board { + /// Evaluates whether the active color can castle toward the given wing of the board in the + /// current position. + /// + /// ## Errors + /// + /// Returns an error indicating why the active color cannot castle. + pub fn active_color_can_castle(&self, wing: Wing) -> Result<(), CastleEvaluationError> { + // TODO: Cache this result. It's expensive! + // TODO: Does this actually need to rely on internal state, i.e. active_color? + + let active_color = self.active_color; + + if !self.castling_rights.color_has_right(active_color, wing) { + return Err(CastleEvaluationError::NoRights { + color: active_color, + wing, + }); + } + + let parameters = self.castling_parameters(wing); + + if self.castling_king(parameters.origin.king).is_none() { + return Err(CastleEvaluationError::NoKing); + } + + if self.castling_rook(parameters.origin.rook).is_none() { + return Err(CastleEvaluationError::NoRook); + } + + // All squares must be clear. + let has_obstructing_pieces = (self.occupancy() & parameters.clear).is_populated(); + if has_obstructing_pieces { + return Err(CastleEvaluationError::ObstructingPieces); + } + + // King cannot pass through check. + let opposing_sight = self.opposing_sight(); + let opposing_pieces_can_see_castling_path = + (parameters.check & opposing_sight).is_populated(); + if opposing_pieces_can_see_castling_path { + return Err(CastleEvaluationError::CheckingPieces); + } + + Ok(()) + } + + pub(crate) fn castling_king(&self, square: Square) -> Option { + let active_color = self.active_color; + self.get_piece(square) + .filter(|piece| piece.color == active_color && piece.is_king()) + } + + pub(crate) fn castling_rook(&self, square: Square) -> Option { + let active_color = self.active_color; + self.get_piece(square) + .filter(|piece| piece.color == active_color && piece.is_rook()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_board; + use chessfriend_core::{piece, Color, Wing}; + + #[test] + fn king_on_starting_square_can_castle() { + let pos = test_board!( + White King on E1, + White Rook on A1, + White Rook on H1 + ); + + let rights = pos.castling_rights; + assert!(rights.color_has_right(Color::White, Wing::KingSide)); + assert!(rights.color_has_right(Color::White, Wing::QueenSide)); + } + + #[test] + fn king_for_castle() { + let pos = test_board![ + White King on E1, + White Rook on H1, + White Rook on A1, + ]; + + let kingside_parameters = pos.castling_parameters(Wing::KingSide); + assert_eq!( + pos.castling_king(kingside_parameters.origin.king), + Some(piece!(White King)) + ); + + let queenside_parameters = pos.castling_parameters(Wing::QueenSide); + assert_eq!( + pos.castling_king(queenside_parameters.origin.king), + Some(piece!(White King)) + ); + } + + #[test] + fn rook_for_castle() { + let pos = test_board![ + White King on E1, + White Rook on H1, + ]; + + let kingside_parameters = pos.castling_parameters(Wing::KingSide); + assert_eq!( + pos.castling_rook(kingside_parameters.origin.rook), + Some(piece!(White Rook)) + ); + + let pos = test_board![ + White King on E1, + White Rook on A1, + ]; + + let queenside_parameters = pos.castling_parameters(Wing::QueenSide); + assert_eq!( + pos.castling_rook(queenside_parameters.origin.rook), + Some(piece!(White Rook)) + ); + } + + #[test] + fn white_can_castle() { + let pos = test_board![ + White King on E1, + White Rook on H1, + White Rook on A1, + ]; + + assert_eq!(pos.active_color_can_castle(Wing::KingSide), Ok(())); + assert_eq!(pos.active_color_can_castle(Wing::QueenSide), Ok(())); + } + + #[test] + fn white_cannot_castle_missing_king() { + let pos = test_board![ + White King on E2, + White Rook on H1, + White Rook on A1, + ]; + + assert_eq!( + pos.active_color_can_castle(Wing::KingSide), + Err(CastleEvaluationError::NoKing) + ); + assert_eq!( + pos.active_color_can_castle(Wing::QueenSide), + Err(CastleEvaluationError::NoKing) + ); + } + + #[test] + fn white_cannot_castle_missing_rook() { + let pos = test_board![ + White King on E1, + White Rook on A1, + ]; + + assert_eq!( + pos.active_color_can_castle(Wing::KingSide), + Err(CastleEvaluationError::NoRook) + ); + + let pos = test_board![ + White King on E1, + White Rook on H1, + ]; + + assert_eq!( + pos.active_color_can_castle(Wing::QueenSide), + Err(CastleEvaluationError::NoRook) + ); + } + + #[test] + fn white_cannot_castle_obstructing_piece() { + let pos = test_board![ + White King on E1, + White Bishop on F1, + White Rook on H1, + White Rook on A1, + ]; + + assert_eq!( + pos.active_color_can_castle(Wing::KingSide), + Err(CastleEvaluationError::ObstructingPieces) + ); + assert_eq!(pos.active_color_can_castle(Wing::QueenSide), Ok(())); + } + + #[test] + fn white_cannot_castle_checking_pieces() { + let pos = test_board![ + White King on E1, + White Rook on H1, + White Rook on A1, + Black Queen on C6, + ]; + + assert_eq!(pos.active_color_can_castle(Wing::KingSide), Ok(())); + assert_eq!( + pos.active_color_can_castle(Wing::QueenSide), + Err(CastleEvaluationError::CheckingPieces) + ); + } +} diff --git a/board/src/lib.rs b/board/src/lib.rs index 86ba8ac..71eac01 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -5,6 +5,8 @@ pub mod display; pub mod en_passant; pub mod fen; pub mod macros; +pub mod movement; +pub mod sight; mod board; mod piece_sets; diff --git a/position/src/movement.rs b/board/src/movement.rs similarity index 88% rename from position/src/movement.rs rename to board/src/movement.rs index 712efd1..33a3046 100644 --- a/position/src/movement.rs +++ b/board/src/movement.rs @@ -4,17 +4,26 @@ //! of squares a piece can move to. For all pieces except pawns, the Movement //! set is equal to the Sight set. -use crate::{sight::Sight, Position}; +use crate::{sight::Sight, Board}; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece, Rank, Shape, Square, Wing}; +impl Board { + pub fn movement(&self, square: Square) -> BitBoard { + if let Some(piece) = self.get_piece(square) { + piece.movement(square, self) + } else { + BitBoard::empty() + } + } +} + pub trait Movement { - fn movement(&self, square: Square, position: &Position) -> BitBoard; + fn movement(&self, square: Square, board: &Board) -> BitBoard; } impl Movement for Piece { - fn movement(&self, square: Square, position: &Position) -> BitBoard { - let board = &position.board; + fn movement(&self, square: Square, board: &Board) -> BitBoard { let opposing_occupancy = board.opposing_occupancy(self.color); match self.shape { @@ -27,7 +36,7 @@ impl Movement for Piece { } Shape::King => { let kingside_target_square = - if position.active_color_can_castle(Wing::KingSide).is_ok() { + if board.active_color_can_castle(Wing::KingSide).is_ok() { let parameters = board.castling_parameters(Wing::KingSide); parameters.target.king.into() } else { @@ -35,7 +44,7 @@ impl Movement for Piece { }; let queenside_target_square = - if position.active_color_can_castle(Wing::QueenSide).is_ok() { + if board.active_color_can_castle(Wing::QueenSide).is_ok() { let parameters = board.castling_parameters(Wing::QueenSide); parameters.target.king.into() } else { diff --git a/position/src/sight.rs b/board/src/sight.rs similarity index 79% rename from position/src/sight.rs rename to board/src/sight.rs index 82b5c58..c168343 100644 --- a/position/src/sight.rs +++ b/board/src/sight.rs @@ -16,9 +16,50 @@ //! : The set of squares occupied by friendly pieces that block moves to that //! square and beyond. -use chessfriend_bitboard::BitBoard; -use chessfriend_board::Board; +use crate::Board; +use chessfriend_bitboard::{BitBoard, IterationDirection}; use chessfriend_core::{Color, Direction, Piece, Shape, Square}; +use std::ops::BitOr; + +impl Board { + /// Compute sight of the piece on the given square. + pub fn sight(&self, square: Square) -> BitBoard { + if let Some(piece) = self.get_piece(square) { + piece.sight(square, self) + } else { + BitBoard::empty() + } + } + + pub fn active_sight(&self) -> BitBoard { + self.friendly_sight(self.active_color) + } + + /// A [`BitBoard`] of all squares the given color can see. + pub fn friendly_sight(&self, color: Color) -> BitBoard { + // TODO: Probably want to implement a caching layer here. + self.friendly_occupancy(color) + .occupied_squares(&IterationDirection::default()) + .map(|square| self.sight(square)) + .fold(BitBoard::empty(), BitOr::bitor) + } + + /// A [`BitBoard`] of all squares visible by colors that oppose the given color. + pub fn opposing_sight(&self) -> BitBoard { + // TODO: Probably want to implement a caching layer here. + let active_color = self.active_color; + Color::ALL + .into_iter() + .filter_map(|c| { + if c == active_color { + None + } else { + Some(self.friendly_sight(c)) + } + }) + .fold(BitBoard::empty(), BitOr::bitor) + } +} pub trait Sight { fn sight(&self, square: Square, board: &Board) -> BitBoard; @@ -142,6 +183,7 @@ fn king_sight(info: &SightInfo) -> BitBoard { #[cfg(test)] mod tests { + use crate::test_board; use chessfriend_bitboard::bitboard; use chessfriend_core::piece; @@ -154,16 +196,37 @@ mod tests { let pos = $position; let piece = $piece; - let sight = piece.sight($square, &pos.board); + let sight = piece.sight($square, &pos); assert_eq!(sight, $bitboard); } }; ($test_name:ident, $piece:expr, $square:expr, $bitboard:expr) => { - sight_test! {$test_name, $crate::Position::empty(), $piece, $square, $bitboard} + sight_test! {$test_name, $crate::Board::empty(), $piece, $square, $bitboard} }; } + #[test] + fn friendly_sight() { + let pos = test_board!( + White King on E4, + ); + + let sight = pos.active_sight(); + assert_eq!(sight, bitboard![E5 F5 F4 F3 E3 D3 D4 D5]); + } + + #[test] + fn opposing_sight() { + let pos = test_board!( + White King on E4, + Black Rook on E7, + ); + + let sight = pos.opposing_sight(); + assert_eq!(sight, bitboard![A7 B7 C7 D7 F7 G7 H7 E8 E6 E5 E4]); + } + // #[test] // fn pawns_and_knights_cannot_make_rays() { // assert_eq!(Shape::Pawn.ray_to_square(Square::F7, Square::E8), None); @@ -171,7 +234,7 @@ mod tests { // } mod pawn { - use crate::{sight::Sight, test_position}; + use crate::{sight::Sight, test_board}; use chessfriend_bitboard::{bitboard, BitBoard}; use chessfriend_core::{piece, Square}; @@ -179,7 +242,7 @@ mod tests { sight_test!( e4_pawn_one_blocker, - test_position![ + test_board![ White Bishop on D5, White Pawn on E4, ], @@ -190,40 +253,40 @@ mod tests { #[test] fn e4_pawn_two_blocker() { - let pos = test_position!( + let pos = test_board!( White Bishop on D5, White Queen on F5, White Pawn on E4, ); let piece = piece!(White Pawn); - let sight = piece.sight(Square::E4, &pos.board); + let sight = piece.sight(Square::E4, &pos); assert_eq!(sight, BitBoard::empty()); } #[test] fn e4_pawn_capturable() { - let pos = test_position!( + let pos = test_board!( Black Bishop on D5, White Queen on F5, White Pawn on E4, ); let piece = piece!(White Pawn); - let sight = piece.sight(Square::E4, &pos.board); + let sight = piece.sight(Square::E4, &pos); assert_eq!(sight, bitboard![D5]); } #[test] fn e5_en_passant() { - let pos = test_position!(White, [ + let pos = test_board!(White, [ White Pawn on E5, Black Pawn on D5, ], D6); let piece = piece!(White Pawn); - let sight = piece.sight(Square::E5, &pos.board); + let sight = piece.sight(Square::E5, &pos); assert_eq!(sight, bitboard!(D6 F6)); } @@ -262,7 +325,7 @@ mod tests { mod rook { use super::*; - use crate::test_position; + use crate::test_board; sight_test!( g3_rook, @@ -273,7 +336,7 @@ mod tests { sight_test!( e4_rook_with_e1_white_king_e7_black_king, - test_position![ + test_board![ White Rook on E4, White King on E2, Black King on E7, diff --git a/position/src/lib.rs b/position/src/lib.rs index b43d063..dd85dc7 100644 --- a/position/src/lib.rs +++ b/position/src/lib.rs @@ -1,8 +1,6 @@ // Eryn Wells -mod movement; mod position; -mod sight; #[macro_use] mod macros; diff --git a/position/src/position/make_move.rs b/position/src/position/make_move.rs index e8502e9..12c45d9 100644 --- a/position/src/position/make_move.rs +++ b/position/src/position/make_move.rs @@ -1,7 +1,7 @@ // Eryn Wells -use crate::{movement::Movement, Position}; -use chessfriend_board::{PlacePieceError, PlacePieceStrategy}; +use crate::Position; +use chessfriend_board::{movement::Movement, PlacePieceError, PlacePieceStrategy}; use chessfriend_core::{Color, Piece, Rank, Square, Wing}; use chessfriend_moves::Move; use thiserror::Error; @@ -276,7 +276,7 @@ impl Position { // Pawns can see squares they can't move to. So, calculating valid // squares requires a concept that includes Sight, but adds pawn pushes. // In ChessFriend, that concept is Movement. - let movement = active_piece.movement(origin_square, self); + let movement = active_piece.movement(origin_square, &self.board); if !movement.contains(target_square) { return Err(MakeMoveError::NoMove { piece: active_piece, diff --git a/position/src/position/position.rs b/position/src/position/position.rs index 7b8c352..457744d 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -1,13 +1,11 @@ // Eryn Wells -use crate::{movement::Movement, sight::Sight}; -use chessfriend_bitboard::{BitBoard, IterationDirection}; +use chessfriend_bitboard::BitBoard; use chessfriend_board::{ - display::DiagramFormatter, en_passant::EnPassant, fen::ToFenStr, Board, PlacePieceError, - PlacePieceStrategy, + display::DiagramFormatter, fen::ToFenStr, Board, PlacePieceError, PlacePieceStrategy, }; use chessfriend_core::{Color, Piece, Square}; -use std::{cell::OnceCell, fmt, ops::BitOr}; +use std::{cell::OnceCell, fmt}; #[must_use] #[derive(Clone, Debug, Eq)] @@ -64,51 +62,27 @@ impl Position { impl Position { pub fn sight(&self, square: Square) -> BitBoard { - if let Some(piece) = self.get_piece(square) { - piece.sight(square, &self.board) - } else { - BitBoard::empty() - } + self.board.sight(square) } pub fn movement(&self, square: Square) -> BitBoard { - if let Some(piece) = self.get_piece(square) { - piece.movement(square, self) - } else { - BitBoard::empty() - } + self.board.movement(square) } } impl Position { pub fn active_sight(&self) -> BitBoard { - self.friendly_sight(self.board.active_color) + self.board.active_sight() } /// A [`BitBoard`] of all squares the given color can see. pub fn friendly_sight(&self, color: Color) -> BitBoard { - // TODO: Probably want to implement a caching layer here. - self.board - .friendly_occupancy(color) - .occupied_squares(&IterationDirection::default()) - .map(|square| self.sight(square)) - .fold(BitBoard::empty(), BitOr::bitor) + self.board.friendly_sight(color) } /// A [`BitBoard`] of all squares visible by colors that oppose the given color. pub fn opposing_sight(&self) -> BitBoard { - // TODO: Probably want to implement a caching layer here. - let active_color = self.board.active_color; - Color::ALL - .into_iter() - .filter_map(|c| { - if c == active_color { - None - } else { - Some(self.friendly_sight(c)) - } - }) - .fold(BitBoard::empty(), BitOr::bitor) + self.board.opposing_sight() } } @@ -292,27 +266,6 @@ mod tests { // assert!(!rights.color_has_right(Color::White, Castle::QueenSide)); // } - #[test] - fn friendly_sight() { - let pos = test_position!( - White King on E4, - ); - - let sight = pos.active_sight(); - assert_eq!(sight, bitboard![E5 F5 F4 F3 E3 D3 D4 D5]); - } - - #[test] - fn opposing_sight() { - let pos = test_position!( - White King on E4, - Black Rook on E7, - ); - - let sight = pos.opposing_sight(); - assert_eq!(sight, bitboard![A7 B7 C7 D7 F7 G7 H7 E8 E6 E5 E4]); - } - // #[test] // fn danger_squares() { // let pos = test_position!(Black, [ From e89bca9877494edfcbe91b86e7722203e82fc85d Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 21 May 2025 10:09:55 -0700 Subject: [PATCH 287/423] [board] Remove empty errors module --- board/src/errors.rs | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 board/src/errors.rs diff --git a/board/src/errors.rs b/board/src/errors.rs deleted file mode 100644 index e69de29..0000000 From 609cda0fe5844cffe35d0cab1d3c5fedda6d714f Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 23 May 2025 09:50:57 -0700 Subject: [PATCH 288/423] [position] Remove castle module (it went to board) --- position/src/position.rs | 2 - position/src/position/castle.rs | 233 -------------------------------- 2 files changed, 235 deletions(-) delete mode 100644 position/src/position/castle.rs diff --git a/position/src/position.rs b/position/src/position.rs index 86b2297..6c7bfca 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -1,11 +1,9 @@ // Eryn Wells -mod castle; mod make_move; mod position; pub use { - castle::CastleEvaluationError, make_move::{MakeMoveError, ValidateMove}, position::Position, }; diff --git a/position/src/position/castle.rs b/position/src/position/castle.rs deleted file mode 100644 index cdf4900..0000000 --- a/position/src/position/castle.rs +++ /dev/null @@ -1,233 +0,0 @@ -// Eryn Wells - -use crate::Position; -use chessfriend_core::{Color, Piece, Square, Wing}; -use thiserror::Error; - -#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)] -pub enum CastleEvaluationError { - #[error("{color} does not have the right to castle {wing}")] - NoRights { color: Color, wing: Wing }, - #[error("no king")] - NoKing, - #[error("no rook")] - NoRook, - #[error("castling path is not clear")] - ObstructingPieces, - #[error("opposing pieces check castling path")] - CheckingPieces, -} - -impl Position { - /// Evaluates whether the active color can castle toward the given wing of the board in the - /// current position. - /// - /// ## Errors - /// - /// Returns an error indicating why the active color cannot castle. - pub fn active_color_can_castle(&self, wing: Wing) -> Result<(), CastleEvaluationError> { - // TODO: Cache this result. It's expensive! - - let active_color = self.board.active_color; - - if !self - .board - .castling_rights - .color_has_right(active_color, wing) - { - return Err(CastleEvaluationError::NoRights { - color: active_color, - wing, - }); - } - - let parameters = self.board.castling_parameters(wing); - - if self.castling_king(parameters.origin.king).is_none() { - return Err(CastleEvaluationError::NoKing); - } - - if self.castling_rook(parameters.origin.rook).is_none() { - return Err(CastleEvaluationError::NoRook); - } - - // All squares must be clear. - let has_obstructing_pieces = (self.board.occupancy() & parameters.clear).is_populated(); - if has_obstructing_pieces { - return Err(CastleEvaluationError::ObstructingPieces); - } - - // King cannot pass through check. - let opposing_sight = self.opposing_sight(); - let opposing_pieces_can_see_castling_path = - (parameters.check & opposing_sight).is_populated(); - if opposing_pieces_can_see_castling_path { - return Err(CastleEvaluationError::CheckingPieces); - } - - Ok(()) - } - - pub(crate) fn castling_king(&self, square: Square) -> Option { - let active_color = self.board.active_color; - self.get_piece(square) - .filter(|piece| piece.color == active_color && piece.is_king()) - } - - pub(crate) fn castling_rook(&self, square: Square) -> Option { - let active_color = self.board.active_color; - self.get_piece(square) - .filter(|piece| piece.color == active_color && piece.is_rook()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::test_position; - use chessfriend_core::{piece, Color, Wing}; - - #[test] - fn king_on_starting_square_can_castle() { - let pos = test_position!( - White King on E1, - White Rook on A1, - White Rook on H1 - ); - - let rights = pos.board.castling_rights; - assert!(rights.color_has_right(Color::White, Wing::KingSide)); - assert!(rights.color_has_right(Color::White, Wing::QueenSide)); - } - - #[test] - fn king_for_castle() { - let pos = test_position![ - White King on E1, - White Rook on H1, - White Rook on A1, - ]; - - let kingside_parameters = pos.board.castling_parameters(Wing::KingSide); - assert_eq!( - pos.castling_king(kingside_parameters.origin.king), - Some(piece!(White King)) - ); - - let queenside_parameters = pos.board.castling_parameters(Wing::QueenSide); - assert_eq!( - pos.castling_king(queenside_parameters.origin.king), - Some(piece!(White King)) - ); - } - - #[test] - fn rook_for_castle() { - let pos = test_position![ - White King on E1, - White Rook on H1, - ]; - - let kingside_parameters = pos.board.castling_parameters(Wing::KingSide); - assert_eq!( - pos.castling_rook(kingside_parameters.origin.rook), - Some(piece!(White Rook)) - ); - - let pos = test_position![ - White King on E1, - White Rook on A1, - ]; - - let queenside_parameters = pos.board.castling_parameters(Wing::QueenSide); - assert_eq!( - pos.castling_rook(queenside_parameters.origin.rook), - Some(piece!(White Rook)) - ); - } - - #[test] - fn white_can_castle() { - let pos = test_position![ - White King on E1, - White Rook on H1, - White Rook on A1, - ]; - - assert_eq!(pos.active_color_can_castle(Wing::KingSide), Ok(())); - assert_eq!(pos.active_color_can_castle(Wing::QueenSide), Ok(())); - } - - #[test] - fn white_cannot_castle_missing_king() { - let pos = test_position![ - White King on E2, - White Rook on H1, - White Rook on A1, - ]; - - assert_eq!( - pos.active_color_can_castle(Wing::KingSide), - Err(CastleEvaluationError::NoKing) - ); - assert_eq!( - pos.active_color_can_castle(Wing::QueenSide), - Err(CastleEvaluationError::NoKing) - ); - } - - #[test] - fn white_cannot_castle_missing_rook() { - let pos = test_position![ - White King on E1, - White Rook on A1, - ]; - - assert_eq!( - pos.active_color_can_castle(Wing::KingSide), - Err(CastleEvaluationError::NoRook) - ); - - let pos = test_position![ - White King on E1, - White Rook on H1, - ]; - - assert_eq!( - pos.active_color_can_castle(Wing::QueenSide), - Err(CastleEvaluationError::NoRook) - ); - } - - #[test] - fn white_cannot_castle_obstructing_piece() { - let pos = test_position![ - White King on E1, - White Bishop on F1, - White Rook on H1, - White Rook on A1, - ]; - - assert_eq!( - pos.active_color_can_castle(Wing::KingSide), - Err(CastleEvaluationError::ObstructingPieces) - ); - assert_eq!(pos.active_color_can_castle(Wing::QueenSide), Ok(())); - } - - #[test] - fn white_cannot_castle_checking_pieces() { - let pos = test_position![ - White King on E1, - White Rook on H1, - White Rook on A1, - Black Queen on C6, - ]; - - assert_eq!(pos.active_color_can_castle(Wing::KingSide), Ok(())); - assert_eq!( - pos.active_color_can_castle(Wing::QueenSide), - Err(CastleEvaluationError::CheckingPieces) - ); - } -} From d5c0330fbe7bff2461ad8a05c959b889653740f5 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 23 May 2025 09:52:22 -0700 Subject: [PATCH 289/423] [board] Add PieceSet::find_pieces Takes a Piece and returns a BitBoard representing where pieces of that shape and color are on the board. --- board/src/piece_sets.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/board/src/piece_sets.rs b/board/src/piece_sets.rs index e974064..d3d7fde 100644 --- a/board/src/piece_sets.rs +++ b/board/src/piece_sets.rs @@ -85,6 +85,12 @@ impl PieceSet { self.mailbox.get(square) } + pub(crate) fn find_pieces(&self, piece: Piece) -> BitBoard { + let color_occupancy = self.color_occupancy[piece.color as usize]; + let shape_occupancy = self.shape_occupancy[piece.shape as usize]; + color_occupancy & shape_occupancy + } + pub(crate) fn place( &mut self, piece: Piece, From ddd14e899915283bed43ae94dd4d73bf369f400c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 23 May 2025 09:53:29 -0700 Subject: [PATCH 290/423] [board] Define two new types for the Clock properties of Board Helpful for other parts of the code. --- board/src/board.rs | 7 +++++-- board/src/lib.rs | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/board/src/board.rs b/board/src/board.rs index d5b89c9..7b77766 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -9,14 +9,17 @@ use crate::{ use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece, Shape, Square, Wing}; +pub type HalfMoveClock = u32; +pub type FullMoveClock = u32; + #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct Board { pub active_color: Color, pub pieces: PieceSet, pub castling_rights: castle::Rights, pub en_passant_target: Option, - pub half_move_clock: u32, - pub full_move_number: u32, + pub half_move_clock: HalfMoveClock, + pub full_move_number: FullMoveClock, } impl Board { diff --git a/board/src/lib.rs b/board/src/lib.rs index 71eac01..3a347d4 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -1,5 +1,6 @@ // Eryn Wells +pub mod board; pub mod castle; pub mod display; pub mod en_passant; @@ -8,11 +9,11 @@ pub mod macros; pub mod movement; pub mod sight; -mod board; mod piece_sets; pub use board::Board; pub use castle::Parameters as CastleParameters; +pub use castle::Rights as CastleRights; pub use piece_sets::{PlacePieceError, PlacePieceStrategy}; use piece_sets::PieceSet; From a92ec9aba3d49f2b7643e766eff1b9bf29c11664 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 23 May 2025 09:53:52 -0700 Subject: [PATCH 291/423] [board] Add an option to display a board with ASCII characters --- board/src/display.rs | 15 ++++++++++++++- core/src/pieces.rs | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/board/src/display.rs b/board/src/display.rs index 73c59a9..b7eb86f 100644 --- a/board/src/display.rs +++ b/board/src/display.rs @@ -5,9 +5,17 @@ use chessfriend_bitboard::BitBoard; use chessfriend_core::{File, Rank, Square}; use std::fmt; +#[derive(Default)] +pub enum PieceDisplayStyle { + #[default] + Unicode, + ASCII, +} + #[must_use] pub struct DiagramFormatter<'a> { board: &'a Board, + piece_style: PieceDisplayStyle, marked_squares: BitBoard, highlighted_squares: BitBoard, } @@ -16,6 +24,7 @@ impl<'a> DiagramFormatter<'a> { pub fn new(board: &'a Board) -> Self { Self { board, + piece_style: PieceDisplayStyle::default(), marked_squares: BitBoard::default(), highlighted_squares: BitBoard::default(), } @@ -48,7 +57,11 @@ impl fmt::Display for DiagramFormatter<'_> { } if let Some(piece) = self.board.get_piece(square) { - write!(f, "{piece}")?; + let ch = match self.piece_style { + PieceDisplayStyle::Unicode => piece.to_unicode(), + PieceDisplayStyle::ASCII => piece.to_ascii(), + }; + write!(f, "{ch}")?; } else { let ch = if self.marked_squares.contains(square) { '*' diff --git a/core/src/pieces.rs b/core/src/pieces.rs index a580967..a335aa9 100644 --- a/core/src/pieces.rs +++ b/core/src/pieces.rs @@ -63,7 +63,7 @@ impl Piece { } #[must_use] - fn to_unicode(self) -> char { + pub fn to_unicode(self) -> char { match (self.color, self.shape) { (Color::Black, Shape::Pawn) => '♟', (Color::Black, Shape::Knight) => '♞', From 0abe9b6c19d3d5498af3d4bdbc733ddb1e2491b0 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 23 May 2025 09:56:47 -0700 Subject: [PATCH 292/423] [board, position] Add a color argument to opposing_sight New convention: active_color_ methods operate on the active color of the Board. Methods without that prefix take a color parameter and operate on that. Refactor opposing_sight to do this. --- board/src/castle.rs | 2 +- board/src/sight.rs | 11 +++++++---- position/src/position/position.rs | 4 ++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/board/src/castle.rs b/board/src/castle.rs index 40322cb..6581676 100644 --- a/board/src/castle.rs +++ b/board/src/castle.rs @@ -61,7 +61,7 @@ impl Board { } // King cannot pass through check. - let opposing_sight = self.opposing_sight(); + let opposing_sight = self.opposing_sight(active_color); let opposing_pieces_can_see_castling_path = (parameters.check & opposing_sight).is_populated(); if opposing_pieces_can_see_castling_path { diff --git a/board/src/sight.rs b/board/src/sight.rs index c168343..ea34b7d 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -44,14 +44,17 @@ impl Board { .fold(BitBoard::empty(), BitOr::bitor) } + pub fn active_color_opposing_sight(&self) -> BitBoard { + self.opposing_sight(self.active_color) + } + /// A [`BitBoard`] of all squares visible by colors that oppose the given color. - pub fn opposing_sight(&self) -> BitBoard { + pub fn opposing_sight(&self, color: Color) -> BitBoard { // TODO: Probably want to implement a caching layer here. - let active_color = self.active_color; Color::ALL .into_iter() .filter_map(|c| { - if c == active_color { + if c == color { None } else { Some(self.friendly_sight(c)) @@ -223,7 +226,7 @@ mod tests { Black Rook on E7, ); - let sight = pos.opposing_sight(); + let sight = pos.active_color_opposing_sight(); assert_eq!(sight, bitboard![A7 B7 C7 D7 F7 G7 H7 E8 E6 E5 E4]); } diff --git a/position/src/position/position.rs b/position/src/position/position.rs index 457744d..0eedb0d 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -81,8 +81,8 @@ impl Position { } /// A [`BitBoard`] of all squares visible by colors that oppose the given color. - pub fn opposing_sight(&self) -> BitBoard { - self.board.opposing_sight() + pub fn active_color_opposing_sight(&self) -> BitBoard { + self.board.active_color_opposing_sight() } } From b8a51990a35847ca44c929b9ed9259e33993224b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 23 May 2025 09:57:48 -0700 Subject: [PATCH 293/423] [board, position] Implement some methods to check for whether a king is in check Remove some dead code from Position. --- board/src/check.rs | 49 +++++++++++++++++++++++++++++++ board/src/lib.rs | 1 + position/src/position/position.rs | 17 ----------- 3 files changed, 50 insertions(+), 17 deletions(-) create mode 100644 board/src/check.rs diff --git a/board/src/check.rs b/board/src/check.rs new file mode 100644 index 0000000..92fa168 --- /dev/null +++ b/board/src/check.rs @@ -0,0 +1,49 @@ +// Eryn Wells + +use crate::Board; +use chessfriend_bitboard::BitBoard; +use chessfriend_core::{Color, Piece}; + +impl Board { + #[must_use] + pub fn active_color_is_in_check(&self) -> bool { + self.color_is_in_check(self.active_color) + } + + #[must_use] + pub fn color_is_in_check(&self, color: Color) -> bool { + let king = self.king_bitboard(color); + let opposing_sight = self.opposing_sight(color); + (king & opposing_sight).is_populated() + } + + fn king_bitboard(&self, color: Color) -> BitBoard { + self.pieces.find_pieces(Piece::king(color)) + } +} + +#[cfg(test)] +mod tests { + use crate::test_board; + use chessfriend_core::Color; + + #[test] + fn active_color_is_in_check() { + let board = test_board!( + White King on A3, + Black Rook on F3, + ); + + assert!(board.color_is_in_check(Color::White)); + } + + #[test] + fn active_color_is_not_in_check() { + let board = test_board!( + White King on A3, + Black Rook on B4, + ); + + assert!(!board.color_is_in_check(Color::White)); + } +} diff --git a/board/src/lib.rs b/board/src/lib.rs index 3a347d4..1df6832 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -9,6 +9,7 @@ pub mod macros; pub mod movement; pub mod sight; +mod check; mod piece_sets; pub use board::Board; diff --git a/position/src/position/position.rs b/position/src/position/position.rs index 0eedb0d..c3d5793 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -122,23 +122,6 @@ impl Position { self.moves().moves_for_piece(piece) } - #[cfg(test)] - pub(crate) fn is_king_in_check(&self) -> bool { - let danger_squares = self.king_danger(self.player_to_move()); - !(danger_squares & self.king_bitboard(self.player_to_move())).is_empty() - } - - fn king_bitboard(&self, player: Color) -> BitBoard { - self.board.pieces.bitboard_for_piece(Piece::king(player)) - } - - pub(crate) fn king_square(&self, player: Color) -> Square { - self.king_bitboard(player) - .occupied_squares(&IterationDirection::default()) - .next() - .unwrap() - } - pub(crate) fn checking_pieces(&self) -> CheckingPieces { let opponent = self.player_to_move().other(); let king_square = self.king_square(self.player_to_move()); From 05c62dcd9910dd95dc5762ff18141a43a468795b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 23 May 2025 09:58:29 -0700 Subject: [PATCH 294/423] [position] Remove CastleEvaluationError from this crate It moved to board. --- position/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/position/src/lib.rs b/position/src/lib.rs index dd85dc7..9bce691 100644 --- a/position/src/lib.rs +++ b/position/src/lib.rs @@ -10,4 +10,4 @@ mod macros; mod testing; pub use chessfriend_board::{fen, PlacePieceError, PlacePieceStrategy}; -pub use position::{CastleEvaluationError, Position, ValidateMove}; +pub use position::{Position, ValidateMove}; From a9268ad194a87a562fe1d5497234a6e2370eb7dd Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 23 May 2025 10:00:20 -0700 Subject: [PATCH 295/423] [position] Add move tracking to Position Create a new MoveRecord struct that tracks the move (aka ply) and irreversible board properties. This should make it easier to unmake moves in the future. --- position/src/lib.rs | 1 + position/src/move_record.rs | 15 ++++++ position/src/position/make_move.rs | 83 +++++++++++++++++++++--------- position/src/position/position.rs | 8 +-- 4 files changed, 81 insertions(+), 26 deletions(-) create mode 100644 position/src/move_record.rs diff --git a/position/src/lib.rs b/position/src/lib.rs index 9bce691..a62da34 100644 --- a/position/src/lib.rs +++ b/position/src/lib.rs @@ -1,5 +1,6 @@ // Eryn Wells +mod move_record; mod position; #[macro_use] diff --git a/position/src/move_record.rs b/position/src/move_record.rs new file mode 100644 index 0000000..72af258 --- /dev/null +++ b/position/src/move_record.rs @@ -0,0 +1,15 @@ +// Eryn Wells + +use chessfriend_board::{board::HalfMoveClock, CastleRights}; +use chessfriend_core::Square; +use chessfriend_moves::Move; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct MoveRecord { + pub ply: Move, + pub en_passant_target: Option, + pub castling_rights: CastleRights, + pub half_move_clock: HalfMoveClock, +} + +impl MoveRecord {} diff --git a/position/src/position/make_move.rs b/position/src/position/make_move.rs index 12c45d9..a65faf2 100644 --- a/position/src/position/make_move.rs +++ b/position/src/position/make_move.rs @@ -1,14 +1,14 @@ // Eryn Wells -use crate::Position; -use chessfriend_board::{movement::Movement, PlacePieceError, PlacePieceStrategy}; +use crate::{move_record::MoveRecord, Position}; +use chessfriend_board::{ + castle::CastleEvaluationError, movement::Movement, PlacePieceError, PlacePieceStrategy, +}; use chessfriend_core::{Color, Piece, Rank, Square, Wing}; use chessfriend_moves::Move; use thiserror::Error; -use super::CastleEvaluationError; - -type MakeMoveResult = Result<(), MakeMoveError>; +type MakeMoveResult = Result; #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum ValidateMove { @@ -69,7 +69,16 @@ impl Position { /// /// If `validate` is [`ValidateMove::Yes`], perform validation of move correctness prior to /// applying the move. See [`Position::validate_move`]. - pub fn make_move(&mut self, ply: Move, validate: ValidateMove) -> MakeMoveResult { + pub fn make_move(&mut self, ply: Move, validate: ValidateMove) -> Result<(), MakeMoveError> { + self.make_move_internal(ply, validate)?; + Ok(()) + } + + pub(crate) fn make_move_internal( + &mut self, + ply: Move, + validate: ValidateMove, + ) -> MakeMoveResult { self.validate_move(ply, validate)?; if ply.is_quiet() { @@ -85,14 +94,14 @@ impl Position { } if let Some(wing) = ply.castle_wing() { - return self.make_castle_move(wing); + return self.make_castle_move(ply, wing); } if ply.is_promotion() { return self.make_promotion_move(ply); } - Ok(()) + unreachable!(); } pub fn unmake_move(&mut self, ply: &Move) -> Result<(), UnmakeMoveError> { @@ -113,9 +122,11 @@ impl Position { self.remove_piece(origin); + let record = self.register_move_record(ply); + self.advance_clocks(HalfMoveClock::Advance); - Ok(()) + Ok(record) } fn make_double_push_move(&mut self, ply: Move) -> MakeMoveResult { @@ -134,9 +145,11 @@ impl Position { _ => unreachable!(), }; + let record = self.register_move_record(ply); + self.advance_clocks(HalfMoveClock::Advance); - Ok(()) + Ok(record) } fn make_capture_move(&mut self, ply: Move) -> MakeMoveResult { @@ -172,13 +185,15 @@ impl Position { self.place_piece(piece, target_square, PlacePieceStrategy::Replace)?; } + let record = self.register_move_record(ply); + self.advance_clocks(HalfMoveClock::Reset); - Ok(()) + Ok(record) } - fn make_castle_move(&mut self, wing: Wing) -> MakeMoveResult { - self.active_color_can_castle(wing)?; + fn make_castle_move(&mut self, ply: Move, wing: Wing) -> MakeMoveResult { + self.board.active_color_can_castle(wing)?; let active_color = self.board.active_color; let parameters = self.board.castling_parameters(wing); @@ -191,9 +206,11 @@ impl Position { self.board.castling_rights.revoke(active_color, wing); + let record = self.register_move_record(ply); + self.advance_clocks(HalfMoveClock::Advance); - Ok(()) + Ok(record) } fn make_promotion_move(&mut self, ply: Move) -> MakeMoveResult { @@ -219,9 +236,24 @@ impl Position { ); } + let record = self.register_move_record(ply); + self.advance_clocks(HalfMoveClock::Reset); - Ok(()) + Ok(record) + } + + fn register_move_record(&mut self, ply: Move) -> MoveRecord { + let record = MoveRecord { + ply, + en_passant_target: self.board.en_passant_target, + castling_rights: self.board.castling_rights, + half_move_clock: self.board.half_move_clock, + }; + + self.moves.push(record.clone()); + + record } } @@ -230,7 +262,7 @@ impl Position { self.get_piece(square).ok_or(MakeMoveError::NoPiece(square)) } - fn place_piece_for_move(&mut self, piece: Piece, square: Square) -> MakeMoveResult { + fn place_piece_for_move(&mut self, piece: Piece, square: Square) -> Result<(), MakeMoveError> { if piece.is_pawn() && square.rank().is_promotable_rank() { return Err(MakeMoveError::PromotionRequired(square)); } @@ -326,8 +358,10 @@ mod tests { use chessfriend_core::{piece, Color, Square}; use chessfriend_moves::{Move, PromotionShape}; + type TestResult = Result<(), MakeMoveError>; + #[test] - fn make_quiet_move() -> MakeMoveResult { + fn make_quiet_move() -> TestResult { let mut pos = test_position!(White Pawn on C2); let ply = Move::quiet(Square::C2, Square::C3); @@ -366,23 +400,26 @@ mod tests { } #[test] - fn make_capture_move() { + fn make_capture_move() -> TestResult { let mut pos = test_position![ White Bishop on C2, Black Rook on F5, ]; let ply = Move::capture(Square::C2, Square::F5); - assert_eq!(pos.make_move(ply, ValidateMove::Yes), Ok(())); + pos.make_move(ply, ValidateMove::Yes)?; + assert_eq!(pos.get_piece(Square::C2), None); assert_eq!(pos.get_piece(Square::F5), Some(piece!(White Bishop))); assert_eq!(pos.captures[Color::White as usize][0], piece!(Black Rook)); assert_eq!(pos.board.active_color, Color::Black); assert_eq!(pos.board.half_move_clock, 0); + + Ok(()) } #[test] - fn make_en_passant_capture_move() -> MakeMoveResult { + fn make_en_passant_capture_move() -> TestResult { let mut pos = test_position![ Black Pawn on F4, White Pawn on E2 @@ -433,7 +470,7 @@ mod tests { } #[test] - fn make_promotion_move() -> MakeMoveResult { + fn make_promotion_move() -> TestResult { let mut pos = test_position![ Black Pawn on E7, White Pawn on F7, @@ -451,7 +488,7 @@ mod tests { } #[test] - fn make_white_kingside_castle() -> MakeMoveResult { + fn make_white_kingside_castle() -> TestResult { let mut pos = test_position![ White Rook on H1, White King on E1, @@ -474,7 +511,7 @@ mod tests { } #[test] - fn make_white_queenside_castle() -> MakeMoveResult { + fn make_white_queenside_castle() -> TestResult { let mut pos = test_position![ White King on E1, White Rook on A1, diff --git a/position/src/position/position.rs b/position/src/position/position.rs index c3d5793..9fe5e26 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -1,5 +1,6 @@ // Eryn Wells +use crate::move_record::MoveRecord; use chessfriend_bitboard::BitBoard; use chessfriend_board::{ display::DiagramFormatter, fen::ToFenStr, Board, PlacePieceError, PlacePieceStrategy, @@ -11,7 +12,8 @@ use std::{cell::OnceCell, fmt}; #[derive(Clone, Debug, Eq)] pub struct Position { pub board: Board, - pub(super) captures: [Vec; Color::NUM], + pub(crate) moves: Vec, + pub(crate) captures: [Vec; Color::NUM], } impl Position { @@ -181,6 +183,7 @@ impl Default for Position { fn default() -> Self { Self { board: Board::default(), + moves: Vec::default(), captures: Default::default(), } } @@ -202,8 +205,7 @@ impl fmt::Display for Position { mod tests { use super::*; use crate::{test_position, Position}; - use chessfriend_bitboard::bitboard; - use chessfriend_core::{piece, Wing}; + use chessfriend_core::piece; #[test] fn piece_on_square() { From 5c5d9d50181d87266c5ad4d563671f3d874f1d5a Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 23 May 2025 14:14:49 -0700 Subject: [PATCH 296/423] [board] Remove some dead code from Board --- board/src/board.rs | 53 ---------------------------------------------- 1 file changed, 53 deletions(-) diff --git a/board/src/board.rs b/board/src/board.rs index 7b77766..ac3e54d 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -117,59 +117,6 @@ impl Board { } } -/* -impl Board { - /// The rook to use for a castling move. - #[must_use] - pub fn rook_for_castle(&self, player: Color, castle: Castle) -> Option { - let square = castle.parameters(player).rook_origin_square(); - self.piece_on_square(square) - } - - /// Returns `true` if the player is able to castle on the given side of the board. - /// - /// The following requirements must be met: - /// - /// 1. The player must still have the right to castle on that side of the - /// board. The king and rook involved in the castle must not have moved. - /// 1. The spaces between the king and rook must be clear - /// 2. The king must not be in check - /// 3. In the course of castling on that side, the king must not pass - /// through a square that an enemy piece can see - #[must_use] - pub fn player_can_castle(&self, player: Color, castle: Castle) -> bool { - if !self.castling_rights.is_set(player, castle) { - return false; - } - - let castling_parameters = castle.parameters(player); - - let all_pieces = self.pieces.all_pieces(); - if !(all_pieces & castling_parameters.clear_squares()).is_empty() { - return false; - } - - // TODO: Reimplement king_danger here or in Position. - // let danger_squares = self.king_danger(player); - // if !(danger_squares & castling_parameters.check_squares()).is_empty() { - // return false; - // } - - true - } - - pub fn iter_all_pieces(&self) -> impl Iterator + '_ { - self.pieces.iter() - } - - pub fn iter_pieces_of_color(&self, color: Color) -> impl Iterator + '_ { - self.pieces - .iter() - .filter(move |piece| piece.color() == color) - } -} -*/ - #[cfg(test)] mod tests { use super::*; From 588f049290d3c9a23baeb2777ce3fa744a798b65 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 23 May 2025 14:15:38 -0700 Subject: [PATCH 297/423] [position] Remove display module --- position/src/display.rs | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 position/src/display.rs diff --git a/position/src/display.rs b/position/src/display.rs deleted file mode 100644 index 37621a8..0000000 --- a/position/src/display.rs +++ /dev/null @@ -1,15 +0,0 @@ -// Eryn Wells - -use std::fmt; - -pub(crate) trait ASCIIDisplay { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result; -} - -pub(crate) trait UnicodeDisplay { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result; -} - -pub(crate) trait FENDisplay { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result; -} From 3684e9b425607423471991e18a58cdadcc825cd4 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 23 May 2025 18:32:18 -0700 Subject: [PATCH 298/423] [board, core, bitboard] Clean up casts between Rank, File and BitBoard Let BitBoard::rank and BitBoard::file take a Rank and File directly, instead of a u8 by reference. And then make the Rank/File::as_index const and return a value rather than a reference. All this allows you to convert between Rank, File, and BitBoard at compile tile (i.e. as a const method) rather than needing to do runtime stuff. --- bitboard/src/bitboard.rs | 37 ++++++++++++++++++------------------- board/src/movement.rs | 8 ++++---- core/src/coordinates.rs | 4 ++-- 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index bb6738c..b9e4c1c 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -58,16 +58,12 @@ impl BitBoard { BitBoard(bits) } - // TODO: Is &u8 really necessary here? - pub fn rank(rank: &u8) -> BitBoard { - debug_assert!(*rank < 8); - library::RANKS[*rank as usize] + pub const fn rank(rank: Rank) -> BitBoard { + library::RANKS[rank.as_index()] } - // TODO: Is &u8 really necessary here? - pub fn file(file: &u8) -> BitBoard { - debug_assert!(*file < 8); - library::FILES[*file as usize] + pub const fn file(file: File) -> BitBoard { + library::FILES[file.as_index()] } pub fn ray(sq: Square, dir: Direction) -> BitBoard { @@ -313,7 +309,7 @@ impl From for u64 { impl From for BitBoard { fn from(value: File) -> Self { - library::FILES[*value.as_index() as usize] + library::FILES[value.as_index()] } } @@ -325,7 +321,7 @@ impl From> for BitBoard { impl From for BitBoard { fn from(value: Rank) -> Self { - library::FILES[*value.as_index() as usize] + library::FILES[value.as_index()] } } @@ -466,21 +462,24 @@ mod tests { #[test] #[ignore] fn display_and_debug() { - let bb = BitBoard::file(&0) | BitBoard::file(&3) | BitBoard::rank(&7) | BitBoard::rank(&4); + let bb = BitBoard::file(File::A) + | BitBoard::file(File::D) + | BitBoard::rank(Rank::FIVE) + | BitBoard::rank(Rank::EIGHT); println!("{}", &bb); } #[test] #[allow(clippy::unreadable_literal)] fn rank() { - assert_eq!(BitBoard::rank(&0).0, 0xFF, "Rank 1"); - assert_eq!(BitBoard::rank(&1).0, 0xFF00, "Rank 2"); - assert_eq!(BitBoard::rank(&2).0, 0xFF0000, "Rank 3"); - assert_eq!(BitBoard::rank(&3).0, 0xFF000000, "Rank 4"); - assert_eq!(BitBoard::rank(&4).0, 0xFF00000000, "Rank 5"); - assert_eq!(BitBoard::rank(&5).0, 0xFF0000000000, "Rank 6"); - assert_eq!(BitBoard::rank(&6).0, 0xFF000000000000, "Rank 7"); - assert_eq!(BitBoard::rank(&7).0, 0xFF00000000000000, "Rank 8"); + assert_eq!(BitBoard::rank(Rank::ONE).0, 0xFF, "Rank 1"); + assert_eq!(BitBoard::rank(Rank::TWO).0, 0xFF00, "Rank 2"); + assert_eq!(BitBoard::rank(Rank::THREE).0, 0xFF0000, "Rank 3"); + assert_eq!(BitBoard::rank(Rank::FOUR).0, 0xFF000000, "Rank 4"); + assert_eq!(BitBoard::rank(Rank::FIVE).0, 0xFF00000000, "Rank 5"); + assert_eq!(BitBoard::rank(Rank::SIX).0, 0xFF0000000000, "Rank 6"); + assert_eq!(BitBoard::rank(Rank::SEVEN).0, 0xFF000000000000, "Rank 7"); + assert_eq!(BitBoard::rank(Rank::EIGHT).0, 0xFF00000000000000, "Rank 8"); } #[test] diff --git a/board/src/movement.rs b/board/src/movement.rs index 33a3046..a1387db 100644 --- a/board/src/movement.rs +++ b/board/src/movement.rs @@ -63,10 +63,10 @@ fn pawn_pushes(pawn: BitBoard, color: Color, occupancy: BitBoard) -> BitBoard { match color { Color::White => { - let second_rank = BitBoard::rank(&Rank::TWO.into()); + const SECOND_RANK: BitBoard = BitBoard::rank(Rank::TWO); let mut pushes = pawn.shift_north_one() & vacancy; - if !(pawn & second_rank).is_empty() { + if !(pawn & SECOND_RANK).is_empty() { // Double push pushes = pushes | (pushes.shift_north_one() & vacancy); } @@ -74,10 +74,10 @@ fn pawn_pushes(pawn: BitBoard, color: Color, occupancy: BitBoard) -> BitBoard { pushes } Color::Black => { - let seventh_rank = BitBoard::rank(&Rank::SEVEN.into()); + const SEVENTH_RANK: BitBoard = BitBoard::rank(Rank::SEVEN); let mut pushes = pawn.shift_south_one() & vacancy; - if !(pawn & seventh_rank).is_empty() { + if !(pawn & SEVENTH_RANK).is_empty() { // Double push pushes = pushes | (pushes.shift_south_one() & vacancy); } diff --git a/core/src/coordinates.rs b/core/src/coordinates.rs index 919e262..b5862fb 100644 --- a/core/src/coordinates.rs +++ b/core/src/coordinates.rs @@ -88,8 +88,8 @@ macro_rules! range_bound_struct { } #[must_use] - $vis fn as_index(&self) -> &$repr { - &self.0 + $vis const fn as_index(&self) -> usize { + self.0 as usize } $vis fn iter(&self) -> impl Iterator { From af2bff348fc151898d67d4d44546f3af61a2468b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 23 May 2025 18:34:19 -0700 Subject: [PATCH 299/423] [core] Add an Option argument to Square::neighbor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This argument allows computing the neighbor n squares away along the direction. Technically, squares n>1 are not neighbors… but this is a convenient calculation. --- core/src/coordinates.rs | 47 ++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/core/src/coordinates.rs b/core/src/coordinates.rs index b5862fb..d8cb202 100644 --- a/core/src/coordinates.rs +++ b/core/src/coordinates.rs @@ -308,9 +308,12 @@ impl Square { } #[must_use] - pub fn neighbor(self, direction: Direction) -> Option { + pub fn neighbor(self, direction: Direction, n: Option) -> Option { let index: u8 = self as u8; - let dir: i8 = direction.to_offset(); + + let n = n.unwrap_or(1); + let dir: i8 = direction.to_offset() * n; + match direction { Direction::North | Direction::NorthEast => { Square::try_from(index.wrapping_add_signed(dir)).ok() @@ -558,37 +561,37 @@ mod tests { fn valid_neighbors() { let sq = Square::E4; - assert_eq!(sq.neighbor(Direction::North), Some(Square::E5)); - assert_eq!(sq.neighbor(Direction::NorthEast), Some(Square::F5)); - assert_eq!(sq.neighbor(Direction::East), Some(Square::F4)); - assert_eq!(sq.neighbor(Direction::SouthEast), Some(Square::F3)); - assert_eq!(sq.neighbor(Direction::South), Some(Square::E3)); - assert_eq!(sq.neighbor(Direction::SouthWest), Some(Square::D3)); - assert_eq!(sq.neighbor(Direction::West), Some(Square::D4)); - assert_eq!(sq.neighbor(Direction::NorthWest), Some(Square::D5)); + assert_eq!(sq.neighbor(Direction::North, None), Some(Square::E5)); + assert_eq!(sq.neighbor(Direction::NorthEast, None), Some(Square::F5)); + assert_eq!(sq.neighbor(Direction::East, None), Some(Square::F4)); + assert_eq!(sq.neighbor(Direction::SouthEast, None), Some(Square::F3)); + assert_eq!(sq.neighbor(Direction::South, None), Some(Square::E3)); + assert_eq!(sq.neighbor(Direction::SouthWest, None), Some(Square::D3)); + assert_eq!(sq.neighbor(Direction::West, None), Some(Square::D4)); + assert_eq!(sq.neighbor(Direction::NorthWest, None), Some(Square::D5)); } #[test] fn invalid_neighbors() { let sq = Square::A1; - assert!(sq.neighbor(Direction::West).is_none()); - assert!(sq.neighbor(Direction::SouthWest).is_none()); - assert!(sq.neighbor(Direction::South).is_none()); + assert!(sq.neighbor(Direction::West, None).is_none()); + assert!(sq.neighbor(Direction::SouthWest, None).is_none()); + assert!(sq.neighbor(Direction::South, None).is_none()); let sq = Square::H1; - assert!(sq.neighbor(Direction::East).is_none()); - assert!(sq.neighbor(Direction::SouthEast).is_none()); - assert!(sq.neighbor(Direction::South).is_none()); + assert!(sq.neighbor(Direction::East, None).is_none()); + assert!(sq.neighbor(Direction::SouthEast, None).is_none()); + assert!(sq.neighbor(Direction::South, None).is_none()); let sq = Square::A8; - assert!(sq.neighbor(Direction::North).is_none()); - assert!(sq.neighbor(Direction::NorthWest).is_none()); - assert!(sq.neighbor(Direction::West).is_none()); + assert!(sq.neighbor(Direction::North, None).is_none()); + assert!(sq.neighbor(Direction::NorthWest, None).is_none()); + assert!(sq.neighbor(Direction::West, None).is_none()); let sq = Square::H8; - assert!(sq.neighbor(Direction::North).is_none()); - assert!(sq.neighbor(Direction::NorthEast).is_none()); - assert!(sq.neighbor(Direction::East).is_none()); + assert!(sq.neighbor(Direction::North, None).is_none()); + assert!(sq.neighbor(Direction::NorthEast, None).is_none()); + assert!(sq.neighbor(Direction::East, None).is_none()); } #[test] From 574ab803dd915810ef95dacab86b1a3a7dae8226 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 23 May 2025 18:36:22 -0700 Subject: [PATCH 300/423] [moves] Implement a move generator for pawns I used Claude to help me figure this out. First time using AI for coding. It was actually rather helpful! Calculate BitBoards representing the various kinds of pawn moves when the move generator is created, and then iterate through them. En passant still isn't implemented here. This code has not been tested yet either. --- moves/src/generators.rs | 12 +++ moves/src/generators/pawn.rs | 180 +++++++++++++++++++++++++++++++++++ moves/src/lib.rs | 1 + 3 files changed, 193 insertions(+) create mode 100644 moves/src/generators.rs create mode 100644 moves/src/generators/pawn.rs diff --git a/moves/src/generators.rs b/moves/src/generators.rs new file mode 100644 index 0000000..dda0a5a --- /dev/null +++ b/moves/src/generators.rs @@ -0,0 +1,12 @@ +// Eryn Wells + +mod pawn; + +pub use pawn::PawnMoveGenerator; + +use crate::Move; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct GeneratedMove { + pub(crate) ply: Move, +} diff --git a/moves/src/generators/pawn.rs b/moves/src/generators/pawn.rs new file mode 100644 index 0000000..d0a39af --- /dev/null +++ b/moves/src/generators/pawn.rs @@ -0,0 +1,180 @@ +// Eryn Wells + +//! Implements a move generator for pawns. + +use crate::Move; + +use super::GeneratedMove; +use chessfriend_bitboard::{bit_scanner::TrailingBitScanner, BitBoard}; +use chessfriend_board::Board; +use chessfriend_core::{Color, Direction, Rank, Square}; + +pub struct PawnMoveGenerator { + color: Color, + single_pushes: BitBoard, + double_pushes: BitBoard, + left_captures: BitBoard, + right_captures: BitBoard, + en_passant: BitBoard, + iterator: TrailingBitScanner, + move_type: MoveType, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum MoveType { + SinglePushes, + DoublePushes, + LeftCaptures, + RightCaptures, + EnPassant, +} + +impl PawnMoveGenerator { + #[must_use] + pub fn new(board: &Board, color: Option) -> Self { + let color = board.unwrap_color(color); + + let pawns = board.pawns(color); + let occupied = board.occupancy(); + let empty = !occupied; + let enemies = board.enemies(color); + + let (single_pushes, double_pushes) = Self::pushes(pawns, color, empty); + let (left_captures, right_captures) = Self::captures(pawns, color, enemies); + let en_passant: BitBoard = board.en_passant_target.into(); + + Self { + color, + single_pushes, + double_pushes, + left_captures, + right_captures, + en_passant, + iterator: single_pushes.occupied_squares_trailing(), + move_type: MoveType::SinglePushes, + } + } + + fn pushes(pawns: BitBoard, color: Color, empty: BitBoard) -> (BitBoard, BitBoard) { + match color { + Color::White => { + const THIRD_RANK: BitBoard = BitBoard::rank(Rank::THREE); + + let single_pushes = pawns.shift_north_one() & empty; + let double_pushes = (single_pushes & THIRD_RANK).shift_north_one() & empty; + + (single_pushes, double_pushes) + } + Color::Black => { + const SIXTH_RANK: BitBoard = BitBoard::rank(Rank::SIX); + + let single_pushes = pawns.shift_south_one() & empty; + let double_pushes = (single_pushes & SIXTH_RANK).shift_south_one() & empty; + + (single_pushes, double_pushes) + } + } + } + + fn captures(pawns: BitBoard, color: Color, enemies: BitBoard) -> (BitBoard, BitBoard) { + match color { + Color::White => { + let left_captures = pawns.shift_north_west_one() & enemies; + let right_captures = pawns.shift_north_east_one() & enemies; + (left_captures, right_captures) + } + Color::Black => { + let left_captures = pawns.shift_south_east_one() & enemies; + let right_captures = pawns.shift_south_west_one() & enemies; + (left_captures, right_captures) + } + } + } + + fn calculate_origin_square(&self, target: Square) -> Option { + match self.move_type { + MoveType::SinglePushes => match self.color { + Color::White => target.neighbor(Direction::South, None), + Color::Black => target.neighbor(Direction::North, None), + }, + MoveType::DoublePushes => match self.color { + Color::White => target.neighbor(Direction::South, Some(2)), + Color::Black => target.neighbor(Direction::North, Some(2)), + }, + MoveType::LeftCaptures => match self.color { + Color::White => target.neighbor(Direction::NorthWest, None), + Color::Black => target.neighbor(Direction::SouthEast, None), + }, + MoveType::RightCaptures => match self.color { + Color::White => target.neighbor(Direction::NorthEast, None), + Color::Black => target.neighbor(Direction::SouthWest, None), + }, + MoveType::EnPassant => match self.color { + Color::White => unimplemented!(), + Color::Black => unimplemented!(), + }, + } + } + + fn next_move_type(&mut self) -> Option { + let next_move_type = self.move_type.next(); + + if let Some(next_move_type) = next_move_type { + let next_bitboard = match next_move_type { + MoveType::SinglePushes => self.single_pushes, + MoveType::DoublePushes => self.double_pushes, + MoveType::LeftCaptures => self.left_captures, + MoveType::RightCaptures => self.right_captures, + MoveType::EnPassant => self.en_passant, + }; + + self.iterator = next_bitboard.occupied_squares_trailing(); + self.move_type = next_move_type; + } + + next_move_type + } +} + +impl std::iter::Iterator for PawnMoveGenerator { + type Item = GeneratedMove; + + fn next(&mut self) -> Option { + if let Some(target) = self.iterator.next() { + let origin = self + .calculate_origin_square(target) + .expect("Bogus origin square"); + + match self.move_type { + MoveType::SinglePushes => Some(GeneratedMove { + ply: Move::quiet(origin, target), + }), + MoveType::DoublePushes => Some(GeneratedMove { + ply: Move::double_push(origin, target), + }), + MoveType::LeftCaptures | MoveType::RightCaptures => Some(GeneratedMove { + ply: Move::capture(origin, target), + }), + MoveType::EnPassant => Some(GeneratedMove { + ply: Move::en_passant_capture(origin, target), + }), + } + } else if self.next_move_type().is_some() { + self.next() + } else { + None + } + } +} + +impl MoveType { + fn next(self) -> Option { + match self { + MoveType::SinglePushes => Some(MoveType::DoublePushes), + MoveType::DoublePushes => Some(MoveType::LeftCaptures), + MoveType::LeftCaptures => Some(MoveType::RightCaptures), + MoveType::RightCaptures => Some(MoveType::EnPassant), + MoveType::EnPassant => None, + } + } +} diff --git a/moves/src/lib.rs b/moves/src/lib.rs index 09ccb0d..dd44db5 100644 --- a/moves/src/lib.rs +++ b/moves/src/lib.rs @@ -1,5 +1,6 @@ // Eryn Wells +pub mod generators; pub mod testing; mod builder; From 994f17091b48457c5f6ae08c61b6a21eaeb6f2d6 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 23 May 2025 18:37:13 -0700 Subject: [PATCH 301/423] [board] Implement Board::unwrap_color Unwrap an Option using the board's active color in the default case. --- board/src/board.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/board/src/board.rs b/board/src/board.rs index ac3e54d..84b2700 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -117,6 +117,13 @@ impl Board { } } +impl Board { + #[must_use] + pub fn unwrap_color(&self, color: Option) -> Color { + color.unwrap_or(self.active_color) + } +} + #[cfg(test)] mod tests { use super::*; From c02f0170b90c9238c01fa21a08b7493863acc41a Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 23 May 2025 18:38:15 -0700 Subject: [PATCH 302/423] [bitboard] Export the bit_scanner module Clients can access TrailingBitScanner and LeadingBitScanner directly now. --- bitboard/src/bit_scanner.rs | 6 +++--- bitboard/src/lib.rs | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/bitboard/src/bit_scanner.rs b/bitboard/src/bit_scanner.rs index 7287d27..b708fdd 100644 --- a/bitboard/src/bit_scanner.rs +++ b/bitboard/src/bit_scanner.rs @@ -20,7 +20,7 @@ macro_rules! bit_scanner { bit_scanner!(LeadingBitScanner); bit_scanner!(TrailingBitScanner); -fn _index_to_square(index: usize) -> Square { +fn index_to_square(index: usize) -> Square { unsafe { #[allow(clippy::cast_possible_truncation)] Square::from_index_unchecked(index as u8) @@ -49,7 +49,7 @@ impl Iterator for LeadingBitScanner { // Shift 1 additional place to account for the 1 that `leading_zeros` found. self.shift += leading_zeros + 1; - Some(_index_to_square(position)) + Some(index_to_square(position)) } } @@ -75,7 +75,7 @@ impl Iterator for TrailingBitScanner { // Shift 1 additional place to account for the 1 that `leading_zeros` found. self.shift += trailing_zeros + 1; - Some(_index_to_square(position)) + Some(index_to_square(position)) } } diff --git a/bitboard/src/lib.rs b/bitboard/src/lib.rs index 983f65e..798e51b 100644 --- a/bitboard/src/lib.rs +++ b/bitboard/src/lib.rs @@ -1,6 +1,7 @@ // Eryn Wells -mod bit_scanner; +pub mod bit_scanner; + mod bitboard; mod direction; mod library; From 1da08df430d535c56c805204c2705eb33ebf2b6f Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 23 May 2025 18:39:18 -0700 Subject: [PATCH 303/423] [board] Implement a couple handy piece getters Board::find_pieces returns a BitBoard for all the pieces matching the given Piece. Board::enemies returns a BitBoard of all the enemy pieces of a given Color. Board::pawns returns a BitBoard of all the pawns of a given Color. --- board/src/board.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/board/src/board.rs b/board/src/board.rs index 84b2700..966d611 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -63,6 +63,10 @@ impl Board { self.pieces.get(square) } + pub fn find_pieces(&self, piece: Piece) -> BitBoard { + self.pieces.find_pieces(piece) + } + /// Place a piece on the board. /// /// ## Errors @@ -102,6 +106,15 @@ impl Board { pub fn opposing_occupancy(&self, color: Color) -> BitBoard { self.pieces.opposing_occupancy(color) } + + pub fn enemies(&self, color: Color) -> BitBoard { + self.pieces.opposing_occupancy(color) + } + + /// Return a [`BitBoard`] of all pawns of a given color. + pub fn pawns(&self, color: Color) -> BitBoard { + self.pieces.find_pieces(Piece::pawn(color)) + } } impl Board { From 5466693c1b0cafe54b60843f8c61b3d4cc9f2fca Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 23 May 2025 18:39:38 -0700 Subject: [PATCH 304/423] [position] Remove empty implementation of Position::unmake_move --- position/src/position/make_move.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/position/src/position/make_move.rs b/position/src/position/make_move.rs index a65faf2..3c3ad3b 100644 --- a/position/src/position/make_move.rs +++ b/position/src/position/make_move.rs @@ -103,10 +103,6 @@ impl Position { unreachable!(); } - - pub fn unmake_move(&mut self, ply: &Move) -> Result<(), UnmakeMoveError> { - Ok(()) - } } impl Position { From ab587f379fa7967bd71b2b7e22fd44879e08e894 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 24 May 2025 17:54:46 -0700 Subject: [PATCH 305/423] [board] Fix a bug in PieceSet::opposing_occupancy This method should compute occupancy of everything except the provided color. Instead it was computing the inverse. --- board/src/piece_sets.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/board/src/piece_sets.rs b/board/src/piece_sets.rs index d3d7fde..97ada5c 100644 --- a/board/src/piece_sets.rs +++ b/board/src/piece_sets.rs @@ -69,13 +69,15 @@ impl PieceSet { } pub fn opposing_occupancy(&self, color: Color) -> BitBoard { + let color_index = color as usize; + self.color_occupancy.iter().enumerate().fold( BitBoard::default(), |acc, (index, occupancy)| { - if index == color as usize { - acc | occupancy - } else { + if index == color_index { acc + } else { + acc | occupancy } }, ) From 09bf17d66b87b07966e467c89a6983734135cc42 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 24 May 2025 18:01:14 -0700 Subject: [PATCH 306/423] [moves] Implement promotions and en passant in the PawnMoveGenerator Add another sub-iterator to the PawnMoveGenerator that produces promotion pushes or capture moves (depending on move_type) when a pawn moves to the back rank. Also implement en passant moves. Fix a bug in the Left and Right captures branches where the wrong neighbors used to calculate origin squares. Add a whole bunch of tests. Still missing several cases though. --- moves/src/defs.rs | 11 ++ moves/src/generators.rs | 8 +- moves/src/generators/pawn.rs | 294 +++++++++++++++++++++++++++++++++-- moves/src/moves.rs | 6 + 4 files changed, 305 insertions(+), 14 deletions(-) diff --git a/moves/src/defs.rs b/moves/src/defs.rs index caea420..03f2a19 100644 --- a/moves/src/defs.rs +++ b/moves/src/defs.rs @@ -24,6 +24,17 @@ pub enum PromotionShape { Queen = 0b11, } +impl PromotionShape { + pub const NUM: usize = 4; + + pub const ALL: [PromotionShape; PromotionShape::NUM] = [ + PromotionShape::Knight, + PromotionShape::Bishop, + PromotionShape::Rook, + PromotionShape::Queen, + ]; +} + impl From for Shape { fn from(value: PromotionShape) -> Self { match value { diff --git a/moves/src/generators.rs b/moves/src/generators.rs index dda0a5a..fec84a2 100644 --- a/moves/src/generators.rs +++ b/moves/src/generators.rs @@ -6,7 +6,13 @@ pub use pawn::PawnMoveGenerator; use crate::Move; -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct GeneratedMove { pub(crate) ply: Move, } + +impl From for GeneratedMove { + fn from(value: Move) -> Self { + GeneratedMove { ply: value } + } +} diff --git a/moves/src/generators/pawn.rs b/moves/src/generators/pawn.rs index d0a39af..29cca6a 100644 --- a/moves/src/generators/pawn.rs +++ b/moves/src/generators/pawn.rs @@ -2,12 +2,12 @@ //! Implements a move generator for pawns. -use crate::Move; - use super::GeneratedMove; +use crate::{Move, PromotionShape}; use chessfriend_bitboard::{bit_scanner::TrailingBitScanner, BitBoard}; use chessfriend_board::Board; -use chessfriend_core::{Color, Direction, Rank, Square}; +use chessfriend_core::{Color, Direction, Rank, Shape, Square}; +use std::slice; pub struct PawnMoveGenerator { color: Color, @@ -16,7 +16,8 @@ pub struct PawnMoveGenerator { left_captures: BitBoard, right_captures: BitBoard, en_passant: BitBoard, - iterator: TrailingBitScanner, + target_iterator: TrailingBitScanner, + promotion_iterator: Option, move_type: MoveType, } @@ -29,6 +30,12 @@ enum MoveType { EnPassant, } +struct PromotionIterator { + origin: Square, + target: Square, + iterator: slice::Iter<'static, PromotionShape>, +} + impl PawnMoveGenerator { #[must_use] pub fn new(board: &Board, color: Option) -> Self { @@ -50,7 +57,8 @@ impl PawnMoveGenerator { left_captures, right_captures, en_passant, - iterator: single_pushes.occupied_squares_trailing(), + target_iterator: single_pushes.occupied_squares_trailing(), + promotion_iterator: None, move_type: MoveType::SinglePushes, } } @@ -102,16 +110,28 @@ impl PawnMoveGenerator { Color::Black => target.neighbor(Direction::North, Some(2)), }, MoveType::LeftCaptures => match self.color { - Color::White => target.neighbor(Direction::NorthWest, None), - Color::Black => target.neighbor(Direction::SouthEast, None), + Color::White => target.neighbor(Direction::SouthEast, None), + Color::Black => target.neighbor(Direction::NorthWest, None), }, MoveType::RightCaptures => match self.color { - Color::White => target.neighbor(Direction::NorthEast, None), - Color::Black => target.neighbor(Direction::SouthWest, None), + Color::White => target.neighbor(Direction::SouthWest, None), + Color::Black => target.neighbor(Direction::NorthEast, None), }, MoveType::EnPassant => match self.color { - Color::White => unimplemented!(), - Color::Black => unimplemented!(), + Color::White => { + if (self.en_passant & self.left_captures).is_populated() { + target.neighbor(Direction::NorthWest, None) + } else { + target.neighbor(Direction::NorthEast, None) + } + } + Color::Black => { + if (self.en_passant & self.left_captures).is_populated() { + target.neighbor(Direction::SouthEast, None) + } else { + target.neighbor(Direction::SouthWest, None) + } + } }, } } @@ -128,23 +148,60 @@ impl PawnMoveGenerator { MoveType::EnPassant => self.en_passant, }; - self.iterator = next_bitboard.occupied_squares_trailing(); + self.target_iterator = next_bitboard.occupied_squares_trailing(); self.move_type = next_move_type; } next_move_type } + + fn next_promotion_move(&mut self) -> Option { + if let Some(promotion_iterator) = self.promotion_iterator.as_mut() { + if let Some(shape) = promotion_iterator.iterator.next() { + let origin = promotion_iterator.origin; + let target = promotion_iterator.target; + + return if matches!( + self.move_type, + MoveType::LeftCaptures | MoveType::RightCaptures + ) { + Some(Move::capture_promotion(origin, target, *shape).into()) + } else { + Some(Move::promotion(origin, target, *shape).into()) + }; + } + } + + None + } } impl std::iter::Iterator for PawnMoveGenerator { type Item = GeneratedMove; fn next(&mut self) -> Option { - if let Some(target) = self.iterator.next() { + let next_promotion_move = self.next_promotion_move(); + if next_promotion_move.is_some() { + return next_promotion_move; + } + + self.promotion_iterator = None; + + if let Some(target) = self.target_iterator.next() { let origin = self .calculate_origin_square(target) .expect("Bogus origin square"); + if target.rank().is_promotable_rank() { + self.promotion_iterator = Some(PromotionIterator { + origin, + target, + iterator: PromotionShape::ALL.iter(), + }); + + return self.next(); + } + match self.move_type { MoveType::SinglePushes => Some(GeneratedMove { ply: Move::quiet(origin, target), @@ -178,3 +235,214 @@ impl MoveType { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::Move; + use chessfriend_board::test_board; + use chessfriend_core::{Color, Square}; + use std::collections::HashSet; + + #[test] + fn black_b7_pushes_and_double_pushes() { + let board = test_board!(Black Pawn on B7); + let generated_moves: HashSet = + PawnMoveGenerator::new(&board, Some(Color::Black)).collect(); + assert_eq!( + generated_moves, + [ + GeneratedMove { + ply: Move::quiet(Square::B7, Square::B6) + }, + GeneratedMove { + ply: Move::double_push(Square::B7, Square::B5) + } + ] + .into() + ); + } + + #[test] + fn black_e2_pushes_and_double_pushes() { + let board = test_board!(White Pawn on E2); + let generated_moves: HashSet = + PawnMoveGenerator::new(&board, Some(Color::White)).collect(); + assert_eq!( + generated_moves, + [ + GeneratedMove { + ply: Move::quiet(Square::E2, Square::E3) + }, + GeneratedMove { + ply: Move::double_push(Square::E2, Square::E4) + } + ] + .into() + ); + } + + #[test] + fn black_b7_pushes_with_b5_blocker() { + let board = test_board!(Black Pawn on B7, White Bishop on B5); + let generated_moves: HashSet = + PawnMoveGenerator::new(&board, Some(Color::Black)).collect(); + assert_eq!( + generated_moves, + [GeneratedMove { + ply: Move::quiet(Square::B7, Square::B6) + }] + .into() + ); + } + + #[test] + fn black_b7_pushes_with_b6_blocker() { + let board = test_board!(Black Pawn on B7, Black Bishop on B6); + let generated_moves: HashSet = + PawnMoveGenerator::new(&board, Some(Color::Black)).collect(); + assert!(generated_moves.is_empty()); + } + + #[test] + fn white_e2_moves_with_e4_blocker() { + let board = test_board!(White Pawn on E2, White Bishop on E4); + let generated_moves: HashSet = + PawnMoveGenerator::new(&board, Some(Color::White)).collect(); + assert_eq!( + generated_moves, + [GeneratedMove { + ply: Move::quiet(Square::E2, Square::E3) + }] + .into() + ); + } + + #[test] + fn white_e2_moves_with_e3_blocker() { + let board = test_board!(White Pawn on E2, White Bishop on E3); + let generated_moves: HashSet = + PawnMoveGenerator::new(&board, Some(Color::White)).collect(); + assert!(generated_moves.is_empty()); + } + + #[test] + fn white_f6_left_captures() { + let white_captures_board = test_board!(White Pawn on F6, Black Queen on E7); + let generated_moves: HashSet = + PawnMoveGenerator::new(&white_captures_board, Some(Color::White)).collect(); + assert_eq!( + generated_moves, + [ + GeneratedMove { + ply: Move::capture(Square::F6, Square::E7) + }, + GeneratedMove { + ply: Move::quiet(Square::F6, Square::F7) + } + ] + .into() + ); + } + + #[test] + fn black_d5_left_captures() { + let black_captures_board = test_board!(Black Pawn on D5, White Queen on E4); + let generated_moves: HashSet = + PawnMoveGenerator::new(&black_captures_board, Some(Color::Black)).collect(); + assert_eq!( + generated_moves, + [ + GeneratedMove { + ply: Move::capture(Square::D5, Square::E4) + }, + GeneratedMove { + ply: Move::quiet(Square::D5, Square::D4) + } + ] + .into() + ); + } + + #[test] + fn white_g7_push_promotions() { + let board = test_board!(White Pawn on G7); + let generated_moves: HashSet = + PawnMoveGenerator::new(&board, Some(Color::White)).collect(); + assert_eq!( + generated_moves, + [ + Move::promotion(Square::G7, Square::G8, PromotionShape::Knight).into(), + Move::promotion(Square::G7, Square::G8, PromotionShape::Bishop).into(), + Move::promotion(Square::G7, Square::G8, PromotionShape::Rook).into(), + Move::promotion(Square::G7, Square::G8, PromotionShape::Queen).into(), + ] + .into() + ); + } + + #[test] + fn white_d7_push_and_capture_promotions() { + let board = test_board!( + White Pawn on D7, + Black Queen on E8 + ); + let generated_moves: HashSet = + PawnMoveGenerator::new(&board, Some(Color::White)).collect(); + assert_eq!( + generated_moves, + [ + Move::promotion(Square::D7, Square::D8, PromotionShape::Knight).into(), + Move::promotion(Square::D7, Square::D8, PromotionShape::Bishop).into(), + Move::promotion(Square::D7, Square::D8, PromotionShape::Rook).into(), + Move::promotion(Square::D7, Square::D8, PromotionShape::Queen).into(), + Move::capture_promotion(Square::D7, Square::E8, PromotionShape::Knight).into(), + Move::capture_promotion(Square::D7, Square::E8, PromotionShape::Bishop).into(), + Move::capture_promotion(Square::D7, Square::E8, PromotionShape::Rook).into(), + Move::capture_promotion(Square::D7, Square::E8, PromotionShape::Queen).into(), + ] + .into() + ); + } + + #[test] + fn black_a2_push_promotions() { + let board = test_board!(Black Pawn on A2); + let generated_moves: HashSet = + PawnMoveGenerator::new(&board, Some(Color::Black)).collect(); + assert_eq!( + generated_moves, + [ + Move::promotion(Square::A2, Square::A1, PromotionShape::Knight).into(), + Move::promotion(Square::A2, Square::A1, PromotionShape::Bishop).into(), + Move::promotion(Square::A2, Square::A1, PromotionShape::Rook).into(), + Move::promotion(Square::A2, Square::A1, PromotionShape::Queen).into(), + ] + .into() + ); + } + + #[test] + fn black_h2_push_and_capture_promotions() { + let board = test_board!( + Black Pawn on H2, + White Queen on G1, + ); + let generated_moves: HashSet = + PawnMoveGenerator::new(&board, Some(Color::Black)).collect(); + assert_eq!( + generated_moves, + [ + Move::promotion(Square::H2, Square::H1, PromotionShape::Knight).into(), + Move::promotion(Square::H2, Square::H1, PromotionShape::Bishop).into(), + Move::promotion(Square::H2, Square::H1, PromotionShape::Rook).into(), + Move::promotion(Square::H2, Square::H1, PromotionShape::Queen).into(), + Move::capture_promotion(Square::H2, Square::G1, PromotionShape::Knight).into(), + Move::capture_promotion(Square::H2, Square::G1, PromotionShape::Bishop).into(), + Move::capture_promotion(Square::H2, Square::G1, PromotionShape::Rook).into(), + Move::capture_promotion(Square::H2, Square::G1, PromotionShape::Queen).into(), + ] + .into() + ); + } +} diff --git a/moves/src/moves.rs b/moves/src/moves.rs index f489ad6..4c0ac74 100644 --- a/moves/src/moves.rs +++ b/moves/src/moves.rs @@ -39,6 +39,12 @@ impl Move { Move(origin_bits(origin) | target_bits(target) | flag_bits) } + pub fn capture_promotion(origin: Square, target: Square, shape: PromotionShape) -> Self { + let flag_bits = Kind::CapturePromotion as u16; + let shape_bits = shape as u16; + Move(origin_bits(origin) | target_bits(target) | flag_bits | shape_bits) + } + #[must_use] pub fn en_passant_capture(origin: Square, target: Square) -> Self { let flag_bits = Kind::EnPassantCapture as u16; From 3f3842c7c84600f909fb862ee000baf63e3293a4 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 25 May 2025 11:04:49 -0700 Subject: [PATCH 307/423] [moves] Add several macros to help with testing: ply! and assert_move_list! ply! implements a small DSL for writing moves in code using a natural-ish algebraic notation. assert_move_list! takes a generator and an expected list of moves and asserts that they're equal. This macro is mostly a copy from one I wrote earlier in the position crate. --- moves/src/generators.rs | 9 ++++++ moves/src/generators/testing.rs | 31 +++++++++++++++++++++ moves/src/lib.rs | 1 + moves/src/moves.rs | 49 +++++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+) create mode 100644 moves/src/generators/testing.rs diff --git a/moves/src/generators.rs b/moves/src/generators.rs index fec84a2..ee0e6bd 100644 --- a/moves/src/generators.rs +++ b/moves/src/generators.rs @@ -2,6 +2,9 @@ mod pawn; +#[cfg(test)] +mod testing; + pub use pawn::PawnMoveGenerator; use crate::Move; @@ -11,6 +14,12 @@ pub struct GeneratedMove { pub(crate) ply: Move, } +impl std::fmt::Display for GeneratedMove { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.ply.fmt(f) + } +} + impl From for GeneratedMove { fn from(value: Move) -> Self { GeneratedMove { ply: value } diff --git a/moves/src/generators/testing.rs b/moves/src/generators/testing.rs new file mode 100644 index 0000000..5a8a782 --- /dev/null +++ b/moves/src/generators/testing.rs @@ -0,0 +1,31 @@ +// Eryn Wells + +#[macro_export] +macro_rules! assert_move_list { + ($generator:expr, [ $($expected:expr),* $(,)? ]) => { + { + let generated_moves: std::collections::HashSet<$crate::GeneratedMove> = $generator.collect(); + let expected_moves: std::collections::HashSet<$crate::GeneratedMove> = [ + $($expected.into(),)* + ].into(); + + assert_eq!( + generated_moves, + expected_moves, + "\n\tMatching: {:?}\n\tGenerated, not expected: {:?}\n\tExpected, not generated: {:?}", + generated_moves + .intersection(&expected_moves) + .map(|mv| format!("{}", mv)) + .collect::>(), + generated_moves + .difference(&expected_moves) + .map(|mv| format!("{}", mv)) + .collect::>(), + expected_moves + .difference(&generated_moves) + .map(|mv| format!("{}", mv)) + .collect::>(), + ); + } + }; +} diff --git a/moves/src/lib.rs b/moves/src/lib.rs index dd44db5..11f8422 100644 --- a/moves/src/lib.rs +++ b/moves/src/lib.rs @@ -9,4 +9,5 @@ mod moves; pub use builder::{Builder, Error as BuildMoveError, Result as BuildMoveResult}; pub use defs::{Kind, PromotionShape}; +pub use generators::GeneratedMove; pub use moves::Move; diff --git a/moves/src/moves.rs b/moves/src/moves.rs index 4c0ac74..d3fd940 100644 --- a/moves/src/moves.rs +++ b/moves/src/moves.rs @@ -4,6 +4,54 @@ use crate::defs::{Kind, PromotionShape}; use chessfriend_core::{Rank, Shape, Square, Wing}; use std::fmt; +#[macro_export] +macro_rules! ply { + ($origin:ident - $target:ident) => { + $crate::Move::quiet( + chessfriend_core::Square::$origin, + chessfriend_core::Square::$target, + ) + }; + ($origin:ident -- $target:ident) => { + $crate::Move::double_push( + chessfriend_core::Square::$origin, + chessfriend_core::Square::$target, + ) + }; + ($origin:ident x $target:ident) => { + $crate::Move::capture( + chessfriend_core::Square::$origin, + chessfriend_core::Square::$target, + ) + }; + ($origin:ident x $target:ident e$(.)?p$(.)?) => { + $crate::Move::en_passant_capture( + chessfriend_core::Square::$origin, + chessfriend_core::Square::$target, + ) + }; + ($origin:ident x $target:ident = $promotion:ident) => { + $crate::Move::capture_promotion( + chessfriend_core::Square::$origin, + chessfriend_core::Square::$target, + $crate::PromotionShape::$promotion, + ) + }; + ($origin:ident - $target:ident = $promotion:ident) => { + $crate::Move::promotion( + chessfriend_core::Square::$origin, + chessfriend_core::Square::$target, + $crate::PromotionShape::$promotion, + ) + }; + (0-0) => { + $crate::Move::castle(chessfriend_core::Wing::KingSide) + }; + (0-0-0) => { + $crate::Move::castle(chessfriend_core::Wing::QueenSide) + }; +} + /// A single player's move. In game theory parlance, this is a "ply". /// /// ## TODO @@ -39,6 +87,7 @@ impl Move { Move(origin_bits(origin) | target_bits(target) | flag_bits) } + #[must_use] pub fn capture_promotion(origin: Square, target: Square, shape: PromotionShape) -> Self { let flag_bits = Kind::CapturePromotion as u16; let shape_bits = shape as u16; From faca84473351558e98461571fce36b2ad8328a1d Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 25 May 2025 11:05:10 -0700 Subject: [PATCH 308/423] [moves] Knight move generator and tests --- board/src/board.rs | 4 + moves/src/generators.rs | 2 + moves/src/generators/knight.rs | 164 +++++++++++++++++++++++++++++++++ 3 files changed, 170 insertions(+) create mode 100644 moves/src/generators/knight.rs diff --git a/board/src/board.rs b/board/src/board.rs index 966d611..e7e748a 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -115,6 +115,10 @@ impl Board { pub fn pawns(&self, color: Color) -> BitBoard { self.pieces.find_pieces(Piece::pawn(color)) } + + pub fn knights(&self, color: Color) -> BitBoard { + self.find_pieces(Piece::knight(color)) + } } impl Board { diff --git a/moves/src/generators.rs b/moves/src/generators.rs index ee0e6bd..9c84bab 100644 --- a/moves/src/generators.rs +++ b/moves/src/generators.rs @@ -1,10 +1,12 @@ // Eryn Wells +mod knight; mod pawn; #[cfg(test)] mod testing; +pub use knight::KnightMoveGenerator; pub use pawn::PawnMoveGenerator; use crate::Move; diff --git a/moves/src/generators/knight.rs b/moves/src/generators/knight.rs new file mode 100644 index 0000000..4b5a696 --- /dev/null +++ b/moves/src/generators/knight.rs @@ -0,0 +1,164 @@ +// Eryn Wells + +use crate::Move; + +use super::GeneratedMove; +use chessfriend_bitboard::{bit_scanner::TrailingBitScanner, BitBoard}; +use chessfriend_board::Board; +use chessfriend_core::{Color, Square}; + +#[must_use] +pub struct KnightMoveGenerator { + origin_iterator: TrailingBitScanner, + target_iterator: Option, + current_origin: Option, + friends: BitBoard, + enemies: BitBoard, +} + +impl KnightMoveGenerator { + pub fn new(board: &Board, color: Option) -> Self { + let color = board.unwrap_color(color); + + let knights = board.knights(color); + let friends = board.friendly_occupancy(color); + let enemies = board.enemies(color); + + Self { + origin_iterator: knights.occupied_squares_trailing(), + target_iterator: None, + current_origin: None, + friends, + enemies, + } + } +} + +impl Iterator for KnightMoveGenerator { + type Item = GeneratedMove; + + fn next(&mut self) -> Option { + if self.current_origin.is_none() { + self.current_origin = self.origin_iterator.next(); + } + + let origin = self.current_origin?; + + let target_iterator = self.target_iterator.get_or_insert_with(|| { + let knight_moves = BitBoard::knight_moves(origin); + knight_moves.occupied_squares_trailing() + }); + + if let Some(target) = target_iterator.next() { + let target_bitboard: BitBoard = target.into(); + if (target_bitboard & self.friends).is_populated() { + self.next() + } else if (target_bitboard & self.enemies).is_populated() { + Some(Move::capture(origin, target).into()) + } else { + Some(Move::quiet(origin, target).into()) + } + } else { + self.current_origin = None; + self.target_iterator = None; + self.next() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{assert_move_list, ply}; + use chessfriend_board::test_board; + use chessfriend_core::Color; + + #[test] + fn white_f5_moves() { + let board = test_board!(White Knight on F5); + assert_move_list!( + KnightMoveGenerator::new(&board, Some(Color::White)), + [ + ply!(F5 - G7), + ply!(F5 - H6), + ply!(F5 - H4), + ply!(F5 - G3), + ply!(F5 - E3), + ply!(F5 - D4), + ply!(F5 - D6), + ply!(F5 - E7), + ] + ); + } + + #[test] + fn white_f5_and_c6_moves() { + let board = test_board!( + White Knight on C6, + White Knight on F5, + ); + assert_move_list!( + KnightMoveGenerator::new(&board, Some(Color::White)), + [ + ply!(C6 - D8), + ply!(C6 - D8), + ply!(C6 - E7), + ply!(C6 - E5), + ply!(C6 - D4), + ply!(C6 - B4), + ply!(C6 - A5), + ply!(C6 - A7), + ply!(C6 - B8), + ply!(F5 - G7), + ply!(F5 - H6), + ply!(F5 - H4), + ply!(F5 - G3), + ply!(F5 - E3), + ply!(F5 - D4), + ply!(F5 - D6), + ply!(F5 - E7), + ] + ); + } + + #[test] + fn white_f5_moves_with_blockers() { + let board = test_board!( + White Knight on F5, + White Queen on G3, + White Bishop on D6, + ); + assert_move_list!( + KnightMoveGenerator::new(&board, Some(Color::White)), + [ + ply!(F5 - G7), + ply!(F5 - H6), + ply!(F5 - H4), + ply!(F5 - E3), + ply!(F5 - D4), + ply!(F5 - E7), + ] + ); + } + + #[test] + fn white_f5_moves_with_captures() { + let board = test_board!( + White Knight on F5, + White Queen on G3, + White Bishop on D6, + Black Queen on D4, + ); + assert_move_list!( + KnightMoveGenerator::new(&board, Some(Color::White)), + [ + ply!(F5 - G7), + ply!(F5 - H6), + ply!(F5 - H4), + ply!(F5 - E3), + ply!(F5 - E7), + ply!(F5 x D4), + ] + ); + } +} From 2c6a7828bc717a397913d0edebd2dcfa5d178eb5 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 25 May 2025 11:05:21 -0700 Subject: [PATCH 309/423] [moves] Two new pawn move tests --- moves/src/generators/pawn.rs | 40 +++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/moves/src/generators/pawn.rs b/moves/src/generators/pawn.rs index 29cca6a..b7fd5ae 100644 --- a/moves/src/generators/pawn.rs +++ b/moves/src/generators/pawn.rs @@ -6,7 +6,7 @@ use super::GeneratedMove; use crate::{Move, PromotionShape}; use chessfriend_bitboard::{bit_scanner::TrailingBitScanner, BitBoard}; use chessfriend_board::Board; -use chessfriend_core::{Color, Direction, Rank, Shape, Square}; +use chessfriend_core::{Color, Direction, Rank, Square}; use std::slice; pub struct PawnMoveGenerator { @@ -364,6 +364,44 @@ mod tests { ); } + #[test] + fn white_f6_right_captures() { + let white_captures_board = test_board!(White Pawn on F6, Black Queen on G7); + let generated_moves: HashSet = + PawnMoveGenerator::new(&white_captures_board, Some(Color::White)).collect(); + assert_eq!( + generated_moves, + [ + GeneratedMove { + ply: Move::capture(Square::F6, Square::G7) + }, + GeneratedMove { + ply: Move::quiet(Square::F6, Square::F7) + } + ] + .into() + ); + } + + #[test] + fn black_d5_right_captures() { + let black_captures_board = test_board!(Black Pawn on D5, White Queen on C4); + let generated_moves: HashSet = + PawnMoveGenerator::new(&black_captures_board, Some(Color::Black)).collect(); + assert_eq!( + generated_moves, + [ + GeneratedMove { + ply: Move::capture(Square::D5, Square::C4) + }, + GeneratedMove { + ply: Move::quiet(Square::D5, Square::D4) + } + ] + .into() + ); + } + #[test] fn white_g7_push_promotions() { let board = test_board!(White Pawn on G7); From f005d94fc2c9ce1ef7e392800dc570b9f66d4b90 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 26 May 2025 17:41:43 -0700 Subject: [PATCH 310/423] [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. --- bitboard/src/bit_scanner.rs | 2 + board/src/board.rs | 12 + board/src/sight.rs | 22 ++ core/src/lib.rs | 2 +- core/src/shapes.rs | 30 ++ moves/src/generators.rs | 2 + moves/src/generators/slider.rs | 607 +++++++++++++++++++++++++++++++++ 7 files changed, 676 insertions(+), 1 deletion(-) create mode 100644 moves/src/generators/slider.rs 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 + ] + ); + } +} From eb6f2000a97617d129c781f56432c7061e300d6c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 26 May 2025 23:37:33 -0700 Subject: [PATCH 311/423] [board, moves, position] Implement KingMoveGenerator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement a move generator that emits moves for the king(s) of a particular color. There will, of course, only ever be one king per side in any valid board, but this iterator can (in theory) handle multiple kings on the board. This iterator is almost entirely copypasta of the SliderMoveGenerator. The major difference is castling. Castle moves are emitted by a helper CastleIterator type. This struct collects information about whether the given color can castle on each side of the board and then emits moves for each side, if indicated. Do some light refactoring of the castle-related methods on Board to accommodate this move generator. Remove the dependency on internal state and rename the "can_castle" method to color_can_castle. In order to facilitate creating castling moves without relying on Board, remove the origin and target squares from the encoded castling move. Code that makes a castling move already looks up castling parameters to move the king and rook to the right squares, so encoding those squares was redundant. This change necessitated some updates to position. Lastly, bring in a handful of unit tests courtesy of Claude. Apparently, it's my new best coding friend. 🙃 --- board/src/board.rs | 9 +- board/src/castle.rs | 60 ++--- board/src/castle/parameters.rs | 4 +- board/src/movement.rs | 23 +- board/src/sight.rs | 1 + moves/src/generators.rs | 2 + moves/src/generators/king.rs | 361 +++++++++++++++++++++++++++++ moves/src/moves.rs | 4 +- position/src/position/make_move.rs | 16 +- 9 files changed, 427 insertions(+), 53 deletions(-) create mode 100644 moves/src/generators/king.rs diff --git a/board/src/board.rs b/board/src/board.rs index e5e0cbc..4746e95 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -7,7 +7,7 @@ use crate::{ PieceSet, }; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece, Shape, Square, Wing}; +use chessfriend_core::{Color, Piece, Shape, Square}; pub type HalfMoveClock = u32; pub type FullMoveClock = u32; @@ -131,12 +131,9 @@ impl Board { pub fn queens(&self, color: Color) -> BitBoard { self.find_pieces(Piece::queen(color)) } -} -impl Board { - #[must_use] - pub fn castling_parameters(&self, wing: Wing) -> &'static castle::Parameters { - &castle::Parameters::BY_COLOR[self.active_color as usize][wing as usize] + pub fn kings(&self, color: Color) -> BitBoard { + self.find_pieces(Piece::king(color)) } } diff --git a/board/src/castle.rs b/board/src/castle.rs index 6581676..ef3a901 100644 --- a/board/src/castle.rs +++ b/board/src/castle.rs @@ -6,7 +6,7 @@ mod rights; pub use parameters::Parameters; pub use rights::Rights; -use crate::Board; +use crate::{Board, CastleParameters}; use chessfriend_core::{Color, Piece, Square, Wing}; use thiserror::Error; @@ -25,26 +25,32 @@ pub enum CastleEvaluationError { } impl Board { + #[must_use] + pub fn castling_parameters(wing: Wing, color: Color) -> &'static CastleParameters { + &CastleParameters::BY_COLOR[color as usize][wing as usize] + } + /// Evaluates whether the active color can castle toward the given wing of the board in the /// current position. /// /// ## Errors /// /// Returns an error indicating why the active color cannot castle. - pub fn active_color_can_castle(&self, wing: Wing) -> Result<(), CastleEvaluationError> { + pub fn color_can_castle( + &self, + wing: Wing, + color: Option, + ) -> Result<&'static CastleParameters, CastleEvaluationError> { // TODO: Cache this result. It's expensive! // TODO: Does this actually need to rely on internal state, i.e. active_color? - let active_color = self.active_color; + let color = self.unwrap_color(color); - if !self.castling_rights.color_has_right(active_color, wing) { - return Err(CastleEvaluationError::NoRights { - color: active_color, - wing, - }); + if !self.castling_rights.color_has_right(color, wing) { + return Err(CastleEvaluationError::NoRights { color, wing }); } - let parameters = self.castling_parameters(wing); + let parameters = Self::castling_parameters(wing, color); if self.castling_king(parameters.origin.king).is_none() { return Err(CastleEvaluationError::NoKing); @@ -61,14 +67,14 @@ impl Board { } // King cannot pass through check. - let opposing_sight = self.opposing_sight(active_color); + let opposing_sight = self.opposing_sight(color); let opposing_pieces_can_see_castling_path = (parameters.check & opposing_sight).is_populated(); if opposing_pieces_can_see_castling_path { return Err(CastleEvaluationError::CheckingPieces); } - Ok(()) + Ok(parameters) } pub(crate) fn castling_king(&self, square: Square) -> Option { @@ -111,13 +117,13 @@ mod tests { White Rook on A1, ]; - let kingside_parameters = pos.castling_parameters(Wing::KingSide); + let kingside_parameters = Board::castling_parameters(Wing::KingSide, Color::White); assert_eq!( pos.castling_king(kingside_parameters.origin.king), Some(piece!(White King)) ); - let queenside_parameters = pos.castling_parameters(Wing::QueenSide); + let queenside_parameters = Board::castling_parameters(Wing::QueenSide, Color::White); assert_eq!( pos.castling_king(queenside_parameters.origin.king), Some(piece!(White King)) @@ -131,7 +137,7 @@ mod tests { White Rook on H1, ]; - let kingside_parameters = pos.castling_parameters(Wing::KingSide); + let kingside_parameters = Board::castling_parameters(Wing::KingSide, Color::White); assert_eq!( pos.castling_rook(kingside_parameters.origin.rook), Some(piece!(White Rook)) @@ -142,7 +148,7 @@ mod tests { White Rook on A1, ]; - let queenside_parameters = pos.castling_parameters(Wing::QueenSide); + let queenside_parameters = Board::castling_parameters(Wing::QueenSide, Color::White); assert_eq!( pos.castling_rook(queenside_parameters.origin.rook), Some(piece!(White Rook)) @@ -150,15 +156,17 @@ mod tests { } #[test] - fn white_can_castle() { + fn white_can_castle() -> Result<(), CastleEvaluationError> { let pos = test_board![ White King on E1, White Rook on H1, White Rook on A1, ]; - assert_eq!(pos.active_color_can_castle(Wing::KingSide), Ok(())); - assert_eq!(pos.active_color_can_castle(Wing::QueenSide), Ok(())); + pos.color_can_castle(Wing::KingSide, None)?; + pos.color_can_castle(Wing::QueenSide, None)?; + + Ok(()) } #[test] @@ -170,11 +178,11 @@ mod tests { ]; assert_eq!( - pos.active_color_can_castle(Wing::KingSide), + pos.color_can_castle(Wing::KingSide, None), Err(CastleEvaluationError::NoKing) ); assert_eq!( - pos.active_color_can_castle(Wing::QueenSide), + pos.color_can_castle(Wing::QueenSide, None), Err(CastleEvaluationError::NoKing) ); } @@ -187,7 +195,7 @@ mod tests { ]; assert_eq!( - pos.active_color_can_castle(Wing::KingSide), + pos.color_can_castle(Wing::KingSide, None), Err(CastleEvaluationError::NoRook) ); @@ -197,7 +205,7 @@ mod tests { ]; assert_eq!( - pos.active_color_can_castle(Wing::QueenSide), + pos.color_can_castle(Wing::QueenSide, None), Err(CastleEvaluationError::NoRook) ); } @@ -212,10 +220,10 @@ mod tests { ]; assert_eq!( - pos.active_color_can_castle(Wing::KingSide), + pos.color_can_castle(Wing::KingSide, None), Err(CastleEvaluationError::ObstructingPieces) ); - assert_eq!(pos.active_color_can_castle(Wing::QueenSide), Ok(())); + assert!(pos.color_can_castle(Wing::QueenSide, None).is_ok()); } #[test] @@ -227,9 +235,9 @@ mod tests { Black Queen on C6, ]; - assert_eq!(pos.active_color_can_castle(Wing::KingSide), Ok(())); + assert!(pos.color_can_castle(Wing::KingSide, None).is_ok()); assert_eq!( - pos.active_color_can_castle(Wing::QueenSide), + pos.color_can_castle(Wing::QueenSide, None), Err(CastleEvaluationError::CheckingPieces) ); } diff --git a/board/src/castle/parameters.rs b/board/src/castle/parameters.rs index bc4e238..54c3512 100644 --- a/board/src/castle/parameters.rs +++ b/board/src/castle/parameters.rs @@ -1,7 +1,7 @@ use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Square, Wing}; -#[derive(Debug)] +#[derive(Debug, Eq, PartialEq)] pub struct Parameters { /// Origin squares of the king and rook. pub origin: Squares, @@ -18,7 +18,7 @@ pub struct Parameters { pub check: BitBoard, } -#[derive(Debug)] +#[derive(Debug, Eq, PartialEq)] pub struct Squares { pub king: Square, pub rook: Square, diff --git a/board/src/movement.rs b/board/src/movement.rs index a1387db..f386579 100644 --- a/board/src/movement.rs +++ b/board/src/movement.rs @@ -24,7 +24,8 @@ pub trait Movement { impl Movement for Piece { fn movement(&self, square: Square, board: &Board) -> BitBoard { - let opposing_occupancy = board.opposing_occupancy(self.color); + let color = self.color; + let opposing_occupancy = board.opposing_occupancy(color); match self.shape { Shape::Pawn => { @@ -36,20 +37,22 @@ impl Movement for Piece { } Shape::King => { let kingside_target_square = - if board.active_color_can_castle(Wing::KingSide).is_ok() { - let parameters = board.castling_parameters(Wing::KingSide); + if board.color_can_castle(Wing::KingSide, Some(color)).is_ok() { + let parameters = Board::castling_parameters(Wing::KingSide, color); parameters.target.king.into() } else { BitBoard::empty() }; - let queenside_target_square = - if board.active_color_can_castle(Wing::QueenSide).is_ok() { - let parameters = board.castling_parameters(Wing::QueenSide); - parameters.target.king.into() - } else { - BitBoard::empty() - }; + let queenside_target_square = if board + .color_can_castle(Wing::QueenSide, Some(self.color)) + .is_ok() + { + let parameters = Board::castling_parameters(Wing::QueenSide, color); + parameters.target.king.into() + } else { + BitBoard::empty() + }; self.sight(square, board) | kingside_target_square | queenside_target_square } diff --git a/board/src/sight.rs b/board/src/sight.rs index 8865097..8518300 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -114,6 +114,7 @@ impl Board { sight_method!(bishop_sight); sight_method!(rook_sight); sight_method!(queen_sight); + sight_method!(king_sight); } struct SightInfo { diff --git a/moves/src/generators.rs b/moves/src/generators.rs index d5c9e37..a3b6fcf 100644 --- a/moves/src/generators.rs +++ b/moves/src/generators.rs @@ -1,5 +1,6 @@ // Eryn Wells +mod king; mod knight; mod pawn; mod slider; @@ -7,6 +8,7 @@ mod slider; #[cfg(test)] mod testing; +pub use king::KingMoveGenerator; pub use knight::KnightMoveGenerator; pub use pawn::PawnMoveGenerator; pub use slider::{BishopMoveGenerator, QueenMoveGenerator, RookMoveGenerator}; diff --git a/moves/src/generators/king.rs b/moves/src/generators/king.rs new file mode 100644 index 0000000..b091b3d --- /dev/null +++ b/moves/src/generators/king.rs @@ -0,0 +1,361 @@ +// Eryn Wells + +use crate::{GeneratedMove, Move}; +use chessfriend_bitboard::{bit_scanner::TrailingBitScanner, BitBoard}; +use chessfriend_board::{Board, CastleParameters}; +use chessfriend_core::{Color, Square, Wing}; + +#[must_use] +pub struct KingMoveGenerator { + kings: Vec, + next_kings_index: usize, + current_king: Option, + castle_iterator: CastleIterator, + friends: BitBoard, + enemies: BitBoard, +} + +impl KingMoveGenerator { + pub fn new(board: &Board, color: Option) -> Self { + let color = board.unwrap_color(color); + + let friends = board.friendly_occupancy(color); + let enemies = board.enemies(color); + + let kings: Vec = board + .kings(color) + .occupied_squares_trailing() + .map(|king| KingIterator { + origin: king, + moves: board + .king_sight(king, Some(color)) + .occupied_squares_trailing(), + }) + .collect(); + + Self { + kings, + next_kings_index: 0, + current_king: None, + castle_iterator: CastleIterator::new(board, color), + friends, + enemies, + } + } +} + +impl Iterator for KingMoveGenerator { + type Item = GeneratedMove; + + fn next(&mut self) -> Option { + loop { + if self.current_king.is_none() { + if self.next_kings_index < self.kings.len() { + self.current_king = Some(self.kings[self.next_kings_index].clone()); + self.next_kings_index += 1; + } else { + break; + } + } + + if let Some(current_king) = self.current_king.as_mut() { + if let Some(target) = current_king.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(); + if is_targeting_enemy_piece { + return Some(Move::capture(current_king.origin, target).into()); + } + + return Some(Move::quiet(current_king.origin, target).into()); + } + + self.current_king = None; + } + } + + self.castle_iterator.next() + } +} + +#[derive(Clone, Debug)] +struct KingIterator { + origin: Square, + moves: TrailingBitScanner, +} + +impl Iterator for KingIterator { + type Item = Square; + + fn next(&mut self) -> Option { + self.moves.next() + } +} + +#[derive(Clone, Debug)] +struct CastleIterator { + kingside: Option<&'static CastleParameters>, + queenside: Option<&'static CastleParameters>, +} + +impl CastleIterator { + fn new(board: &Board, color: Color) -> Self { + let kingside = board.color_can_castle(Wing::KingSide, Some(color)).ok(); + let queenside = board.color_can_castle(Wing::QueenSide, Some(color)).ok(); + + Self { + kingside, + queenside, + } + } +} + +impl Iterator for CastleIterator { + type Item = GeneratedMove; + + fn next(&mut self) -> Option { + if let Some(_parameters) = self.kingside.take() { + return Some(Move::castle(Wing::KingSide).into()); + } + + if let Some(_parameters) = self.queenside.take() { + return Some(Move::castle(Wing::QueenSide).into()); + } + + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{assert_move_list, ply}; + use chessfriend_board::test_board; + + #[test] + fn white_king_center_square_ai_claude() { + let board = test_board!(White King on E4); + assert_move_list!( + KingMoveGenerator::new(&board, None), + [ + // All 8 adjacent squares + ply!(E4 - D3), // Southwest + ply!(E4 - D4), // West + ply!(E4 - D5), // Northwest + ply!(E4 - E3), // South + ply!(E4 - E5), // North + ply!(E4 - F3), // Southeast + ply!(E4 - F4), // East + ply!(E4 - F5), // Northeast + ] + ); + } + + #[test] + fn white_king_corner_square_ai_claude() { + let board = test_board!(White King on A1); + assert_move_list!( + KingMoveGenerator::new(&board, None), + [ + // Only 3 possible moves from corner + ply!(A1 - A2), // North + ply!(A1 - B1), // East + ply!(A1 - B2), // Northeast + ] + ); + } + + #[test] + fn white_king_edge_square_ai_claude() { + let board = test_board!(White King on E1); + assert_move_list!( + KingMoveGenerator::new(&board, None), + [ + // 5 possible moves from edge + ply!(E1 - D1), // West + ply!(E1 - D2), // Northwest + ply!(E1 - E2), // North + ply!(E1 - F1), // East + ply!(E1 - F2), // Northeast + ] + ); + } + + #[test] + fn white_king_with_captures_ai_claude() { + let board = test_board!( + White King on D4, + Black Pawn on C5, // Can capture + Black Knight on E5, // Can capture + Black Bishop on D3 // Can capture + ); + assert_move_list!( + KingMoveGenerator::new(&board, None), + [ + // Regular moves + ply!(D4 - C3), + ply!(D4 - C4), + ply!(D4 - D5), + ply!(D4 - E3), + ply!(D4 - E4), + // Captures + ply!(D4 x C5), // Capture pawn + ply!(D4 x E5), // Capture knight + ply!(D4 x D3), // Capture bishop + ] + ); + } + + #[test] + fn white_king_blocked_by_friendly_pieces_ai_claude() { + let board = test_board!( + White King on D4, + White Pawn on C4, // Blocks west + White Knight on D5, // Blocks north + White Bishop on E3 // Blocks southeast + ); + assert_move_list!( + KingMoveGenerator::new(&board, None), + [ + ply!(D4 - C3), + ply!(D4 - C5), + ply!(D4 - D3), + ply!(D4 - E4), + ply!(D4 - E5), + // Cannot move to C4, D5, E3 (friendly pieces) + ] + ); + } + + #[test] + fn white_king_castling_kingside_ai_claude() { + let board = test_board!( + White King on E1, + White Rook on H1 + // Assuming squares F1, G1 are empty and king/rook haven't moved + ); + assert_move_list!( + KingMoveGenerator::new(&board, None), + [ + // Regular king moves + ply!(E1 - D1), + ply!(E1 - D2), + ply!(E1 - E2), + ply!(E1 - F1), + ply!(E1 - F2), + ply!(0 - 0), + ] + ); + } + + #[test] + fn white_king_castling_queenside_ai_claude() { + let board = test_board!( + White King on E1, + White Rook on A1 + // Assuming squares B1, C1, D1 are empty and king/rook haven't moved + ); + assert_move_list!( + KingMoveGenerator::new(&board, None), + [ + // Regular king moves + ply!(E1 - D1), + ply!(E1 - D2), + ply!(E1 - E2), + ply!(E1 - F1), + ply!(E1 - F2), + ply!(0 - 0 - 0), + ] + ); + } + + #[test] + fn white_king_no_castling_through_check_ai_claude() { + let board = test_board!( + White King on E1, + White Rook on H1, + Black Rook on F8 // Attacks F1, preventing kingside castling + ); + assert_move_list!( + KingMoveGenerator::new(&board, None), + [ + ply!(E1 - D1), + ply!(E1 - D2), + ply!(E1 - E2), + ply!(E1 - F1), + ply!(E1 - F2), + // No castling moves - cannot castle through check + ] + ); + } + + #[test] + fn white_king_castling_blocked_by_pieces_ai_claude() { + let board = test_board!( + White King on E1, + White Rook on H1, + White Knight on G1, // Blocks kingside castling + ); + assert_move_list!( + KingMoveGenerator::new(&board, None), + [ + ply!(E1 - D1), + ply!(E1 - D2), + ply!(E1 - E2), + ply!(E1 - F1), + ply!(E1 - F2), + // No castling - path blocked by knight + ] + ); + } + + #[test] + fn black_king_movement_ai_claude() { + let board = test_board!(Black King on E8); + assert_move_list!( + KingMoveGenerator::new(&board, Some(Color::Black)), + [ + ply!(E8 - D7), + ply!(E8 - D8), + ply!(E8 - E7), + ply!(E8 - F7), + ply!(E8 - F8), + ] + ); + } + + #[test] + fn white_king_surrounded_by_enemies_ai_claude() { + let board = test_board!( + White King on E4, + Black Pawn on D3, + Black Pawn on D4, + Black Pawn on D5, + Black Pawn on E3, + Black Pawn on E5, + Black Pawn on F3, + Black Pawn on F4, + Black Pawn on F5 + ); + assert_move_list!( + KingMoveGenerator::new(&board, None), + [ + // Can capture all surrounding enemy pieces + ply!(E4 x D3), + ply!(E4 x D4), + ply!(E4 x D5), + ply!(E4 x E3), + ply!(E4 x E5), + ply!(E4 x F3), + ply!(E4 x F4), + ply!(E4 x F5), + ] + ); + } +} diff --git a/moves/src/moves.rs b/moves/src/moves.rs index d3fd940..59ac237 100644 --- a/moves/src/moves.rs +++ b/moves/src/moves.rs @@ -108,12 +108,12 @@ impl Move { } #[must_use] - pub fn castle(origin: Square, target: Square, wing: Wing) -> Self { + pub fn castle(wing: Wing) -> Self { let flag_bits = match wing { Wing::KingSide => Kind::KingSideCastle, Wing::QueenSide => Kind::QueenSideCastle, } as u16; - Move(origin_bits(origin) | target_bits(target) | flag_bits) + Move(flag_bits) } } diff --git a/position/src/position/make_move.rs b/position/src/position/make_move.rs index 3c3ad3b..e944e3f 100644 --- a/position/src/position/make_move.rs +++ b/position/src/position/make_move.rs @@ -2,7 +2,7 @@ use crate::{move_record::MoveRecord, Position}; use chessfriend_board::{ - castle::CastleEvaluationError, movement::Movement, PlacePieceError, PlacePieceStrategy, + castle::CastleEvaluationError, movement::Movement, Board, PlacePieceError, PlacePieceStrategy, }; use chessfriend_core::{Color, Piece, Rank, Square, Wing}; use chessfriend_moves::Move; @@ -79,17 +79,18 @@ impl Position { ply: Move, validate: ValidateMove, ) -> MakeMoveResult { - self.validate_move(ply, validate)?; - if ply.is_quiet() { + self.validate_move(ply, validate)?; return self.make_quiet_move(ply); } if ply.is_double_push() { + self.validate_move(ply, validate)?; return self.make_double_push_move(ply); } if ply.is_capture() { + self.validate_move(ply, validate)?; return self.make_capture_move(ply); } @@ -98,6 +99,7 @@ impl Position { } if ply.is_promotion() { + self.validate_move(ply, validate)?; return self.make_promotion_move(ply); } @@ -189,10 +191,10 @@ impl Position { } fn make_castle_move(&mut self, ply: Move, wing: Wing) -> MakeMoveResult { - self.board.active_color_can_castle(wing)?; + self.board.color_can_castle(wing, None)?; let active_color = self.board.active_color; - let parameters = self.board.castling_parameters(wing); + let parameters = Board::castling_parameters(wing, active_color); let king = self.board.remove_piece(parameters.origin.king).unwrap(); self.place_piece(king, parameters.target.king, PlacePieceStrategy::default())?; @@ -490,7 +492,7 @@ mod tests { White King on E1, ]; - let ply = Move::castle(Square::E1, Square::G1, Wing::KingSide); + let ply = Move::castle(Wing::KingSide); pos.make_move(ply, ValidateMove::Yes)?; assert_eq!(pos.board.active_color, Color::Black); @@ -513,7 +515,7 @@ mod tests { White Rook on A1, ]; - let ply = Move::castle(Square::E1, Square::C1, Wing::QueenSide); + let ply = Move::castle(Wing::QueenSide); pos.make_move(ply, ValidateMove::Yes)?; assert_eq!(pos.board.active_color, Color::Black); From e3ca4667375223bff102c7e5033cc9e3e002e2fe Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 27 May 2025 11:49:33 -0700 Subject: [PATCH 312/423] [position] Move capture list to a CapturesList struct This one has a custom Display implementation for easier integration with Positon's. --- position/src/position.rs | 1 + position/src/position/captures.rs | 42 ++++++++++++++++++++++++++++++ position/src/position/make_move.rs | 2 +- position/src/position/position.rs | 11 ++++++-- 4 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 position/src/position/captures.rs diff --git a/position/src/position.rs b/position/src/position.rs index 6c7bfca..f72a27d 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -1,5 +1,6 @@ // Eryn Wells +mod captures; mod make_move; mod position; diff --git a/position/src/position/captures.rs b/position/src/position/captures.rs new file mode 100644 index 0000000..d6ce3e2 --- /dev/null +++ b/position/src/position/captures.rs @@ -0,0 +1,42 @@ +// Eryn Wells + +use std::fmt::Display; + +use chessfriend_core::{Color, Piece}; + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub(crate) struct CapturesList([Vec; Color::NUM]); + +impl CapturesList { + pub fn push(&mut self, color: Color, piece: Piece) { + self.0[color as usize].push(piece); + } + + pub fn is_empty(&self) -> bool { + self.0.iter().all(Vec::is_empty) + } +} + +impl Display for CapturesList { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut is_first = true; + for color in Color::into_iter() { + if is_first { + is_first = false; + } else { + writeln!(f)?; + } + + write!(f, "{color}: ")?; + + let captures = &self.0[color as usize]; + for piece in captures { + write!(f, "{piece}")?; + } + + write!(f, "")?; + } + + Ok(()) + } +} diff --git a/position/src/position/make_move.rs b/position/src/position/make_move.rs index e944e3f..67502ea 100644 --- a/position/src/position/make_move.rs +++ b/position/src/position/make_move.rs @@ -172,7 +172,7 @@ impl Position { .ok_or(MakeMoveError::NoCapturePiece(capture_square))?; // Register the capture - self.captures[piece.color as usize].push(captured_piece); + self.captures.push(piece.color, captured_piece); self.remove_piece(origin_square).unwrap(); diff --git a/position/src/position/position.rs b/position/src/position/position.rs index 9fe5e26..8abfa4a 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -1,5 +1,6 @@ // Eryn Wells +use super::captures::CapturesList; use crate::move_record::MoveRecord; use chessfriend_bitboard::BitBoard; use chessfriend_board::{ @@ -13,7 +14,7 @@ use std::{cell::OnceCell, fmt}; pub struct Position { pub board: Board, pub(crate) moves: Vec, - pub(crate) captures: [Vec; Color::NUM], + pub(crate) captures: CapturesList, } impl Position { @@ -197,7 +198,13 @@ impl PartialEq for Position { impl fmt::Display for Position { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.board.display()) + write!(f, "{}", self.board.display())?; + + if !self.captures.is_empty() { + write!(f, "\n\n{}", self.captures)?; + } + + Ok(()) } } From a8ea248972491b4f138610dc98035e2400ac61f0 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 27 May 2025 11:49:46 -0700 Subject: [PATCH 313/423] [position] Derive Default implementation for Position --- position/src/position/position.rs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/position/src/position/position.rs b/position/src/position/position.rs index 8abfa4a..89596be 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -10,7 +10,7 @@ use chessfriend_core::{Color, Piece, Square}; use std::{cell::OnceCell, fmt}; #[must_use] -#[derive(Clone, Debug, Eq)] +#[derive(Clone, Debug, Default, Eq)] pub struct Position { pub board: Board, pub(crate) moves: Vec, @@ -180,16 +180,6 @@ impl ToFenStr for Position { } } -impl Default for Position { - fn default() -> Self { - Self { - board: Board::default(), - moves: Vec::default(), - captures: Default::default(), - } - } -} - impl PartialEq for Position { fn eq(&self, other: &Self) -> bool { self.board == other.board From db489af50b179589d42e5f172b0084cf9ba8dea2 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 27 May 2025 11:52:17 -0700 Subject: [PATCH 314/423] [position] Move unmake move stuff to an unmake_move module --- position/src/position.rs | 1 + position/src/position/make_move.rs | 2 -- position/src/position/unmake_move.rs | 3 +++ 3 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 position/src/position/unmake_move.rs diff --git a/position/src/position.rs b/position/src/position.rs index f72a27d..a5f7fb2 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -3,6 +3,7 @@ mod captures; mod make_move; mod position; +mod unmake_move; pub use { make_move::{MakeMoveError, ValidateMove}, diff --git a/position/src/position/make_move.rs b/position/src/position/make_move.rs index 67502ea..6be4814 100644 --- a/position/src/position/make_move.rs +++ b/position/src/position/make_move.rs @@ -60,8 +60,6 @@ pub enum MakeMoveError { PromotionRequired(Square), } -pub enum UnmakeMoveError {} - impl Position { /// Make a move in the position. /// diff --git a/position/src/position/unmake_move.rs b/position/src/position/unmake_move.rs new file mode 100644 index 0000000..f52f9fe --- /dev/null +++ b/position/src/position/unmake_move.rs @@ -0,0 +1,3 @@ +// Eryn Wells + +pub enum UnmakeMoveError {} From 085697d38f558b86c9e8843d3fa59e39e33c7f71 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 27 May 2025 11:59:42 -0700 Subject: [PATCH 315/423] [position] Remove position::position module Move the implementation of Position to the top-level position.rs file. --- position/src/position.rs | 150 +++++++++++++++++++++++++++++- position/src/position/position.rs | 146 +---------------------------- 2 files changed, 150 insertions(+), 146 deletions(-) diff --git a/position/src/position.rs b/position/src/position.rs index a5f7fb2..ea94971 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -2,10 +2,152 @@ mod captures; mod make_move; -mod position; mod unmake_move; -pub use { - make_move::{MakeMoveError, ValidateMove}, - position::Position, +pub use make_move::{MakeMoveError, ValidateMove}; + +use crate::move_record::MoveRecord; +use captures::CapturesList; +use chessfriend_bitboard::BitBoard; +use chessfriend_board::{ + display::DiagramFormatter, fen::ToFenStr, Board, PlacePieceError, PlacePieceStrategy, }; +use chessfriend_core::{Color, Piece, Square}; +use std::{cell::OnceCell, fmt}; + +#[must_use] +#[derive(Clone, Debug, Default, Eq)] +pub struct Position { + pub board: Board, + pub(crate) moves: Vec, + pub(crate) captures: CapturesList, +} + +impl Position { + pub fn empty() -> Self { + Position::default() + } + + /// Return a starting position. + pub fn starting() -> Self { + Self { + board: Board::starting(), + ..Default::default() + } + } + + pub fn new(board: Board) -> Self { + Self { + board, + ..Default::default() + } + } +} + +impl Position { + /// Place a piece on the board. + /// + /// ## Errors + /// + /// See [`chessfriend_board::Board::place_piece`]. + pub fn place_piece( + &mut self, + piece: Piece, + square: Square, + strategy: PlacePieceStrategy, + ) -> Result<(), PlacePieceError> { + self.board.place_piece(piece, square, strategy) + } + + #[must_use] + pub fn get_piece(&self, square: Square) -> Option { + self.board.get_piece(square) + } + + pub fn remove_piece(&mut self, square: Square) -> Option { + self.board.remove_piece(square) + } +} + +impl Position { + pub fn sight(&self, square: Square) -> BitBoard { + self.board.sight(square) + } + + pub fn movement(&self, square: Square) -> BitBoard { + self.board.movement(square) + } +} + +impl Position { + pub fn active_sight(&self) -> BitBoard { + self.board.active_sight() + } + + /// A [`BitBoard`] of all squares the given color can see. + pub fn friendly_sight(&self, color: Color) -> BitBoard { + self.board.friendly_sight(color) + } + + /// A [`BitBoard`] of all squares visible by colors that oppose the given color. + pub fn active_color_opposing_sight(&self) -> BitBoard { + self.board.active_color_opposing_sight() + } +} + +impl Position { + pub fn display(&self) -> DiagramFormatter { + self.board.display() + } +} + +impl ToFenStr for Position { + type Error = ::Error; + + fn to_fen_str(&self) -> Result { + self.board.to_fen_str() + } +} + +impl PartialEq for Position { + fn eq(&self, other: &Self) -> bool { + self.board == other.board + } +} + +impl fmt::Display for Position { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.board.display())?; + + if !self.captures.is_empty() { + write!(f, "\n\n{}", self.captures)?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{test_position, Position}; + use chessfriend_core::piece; + + #[test] + fn piece_on_square() { + let pos = test_position![ + Black Bishop on F7, + ]; + + let piece = pos.board.get_piece(Square::F7); + assert_eq!(piece, Some(piece!(Black Bishop))); + } + + #[test] + fn piece_in_starting_position() { + let pos = test_position!(starting); + + assert_eq!(pos.board.get_piece(Square::H1), Some(piece!(White Rook))); + assert_eq!(pos.board.get_piece(Square::A8), Some(piece!(Black Rook))); + } +} diff --git a/position/src/position/position.rs b/position/src/position/position.rs index 89596be..80e9f65 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -1,93 +1,9 @@ // Eryn Wells -use super::captures::CapturesList; -use crate::move_record::MoveRecord; -use chessfriend_bitboard::BitBoard; -use chessfriend_board::{ - display::DiagramFormatter, fen::ToFenStr, Board, PlacePieceError, PlacePieceStrategy, -}; -use chessfriend_core::{Color, Piece, Square}; -use std::{cell::OnceCell, fmt}; - -#[must_use] -#[derive(Clone, Debug, Default, Eq)] -pub struct Position { - pub board: Board, - pub(crate) moves: Vec, - pub(crate) captures: CapturesList, -} - -impl Position { - pub fn empty() -> Self { - Position::default() - } - - /// Return a starting position. - pub fn starting() -> Self { - Self { - board: Board::starting(), - ..Default::default() - } - } - - pub fn new(board: Board) -> Self { - Self { - board, - ..Default::default() - } - } -} - -impl Position { - /// Place a piece on the board. - /// - /// ## Errors - /// - /// See [`chessfriend_board::Board::place_piece`]. - pub fn place_piece( - &mut self, - piece: Piece, - square: Square, - strategy: PlacePieceStrategy, - ) -> Result<(), PlacePieceError> { - self.board.place_piece(piece, square, strategy) - } - - #[must_use] - pub fn get_piece(&self, square: Square) -> Option { - self.board.get_piece(square) - } - - pub fn remove_piece(&mut self, square: Square) -> Option { - self.board.remove_piece(square) - } -} - -impl Position { - pub fn sight(&self, square: Square) -> BitBoard { - self.board.sight(square) - } - - pub fn movement(&self, square: Square) -> BitBoard { - self.board.movement(square) - } -} - -impl Position { - pub fn active_sight(&self) -> BitBoard { - self.board.active_sight() - } - - /// A [`BitBoard`] of all squares the given color can see. - pub fn friendly_sight(&self, color: Color) -> BitBoard { - self.board.friendly_sight(color) - } - - /// A [`BitBoard`] of all squares visible by colors that oppose the given color. - pub fn active_color_opposing_sight(&self) -> BitBoard { - self.board.active_color_opposing_sight() - } -} +/*** + * Keeping this code around for a little while because it might still come in + * handy, but it should be considered dead. + ***/ /* impl Position { @@ -166,62 +82,8 @@ impl Position { } */ -impl Position { - pub fn display(&self) -> DiagramFormatter { - self.board.display() - } -} - -impl ToFenStr for Position { - type Error = ::Error; - - fn to_fen_str(&self) -> Result { - self.board.to_fen_str() - } -} - -impl PartialEq for Position { - fn eq(&self, other: &Self) -> bool { - self.board == other.board - } -} - -impl fmt::Display for Position { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.board.display())?; - - if !self.captures.is_empty() { - write!(f, "\n\n{}", self.captures)?; - } - - Ok(()) - } -} - #[cfg(test)] mod tests { - use super::*; - use crate::{test_position, Position}; - use chessfriend_core::piece; - - #[test] - fn piece_on_square() { - let pos = test_position![ - Black Bishop on F7, - ]; - - let piece = pos.board.get_piece(Square::F7); - assert_eq!(piece, Some(piece!(Black Bishop))); - } - - #[test] - fn piece_in_starting_position() { - let pos = test_position!(starting); - - assert_eq!(pos.board.get_piece(Square::H1), Some(piece!(White Rook))); - assert_eq!(pos.board.get_piece(Square::A8), Some(piece!(Black Rook))); - } - // #[test] // fn king_is_in_check() { // let pos = position![ From 19c6c6701a6556845b8016e7bcff166a76d971b3 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 27 May 2025 12:04:24 -0700 Subject: [PATCH 316/423] [position] Fix broken tests build The make_move tests were trying to access the last capture piece directly from a slice of captures pieces (implementation prior to CapturesList). Implement CapturesList::last() to return an Option<&Piece> and use it in the tests. --- position/src/position/captures.rs | 5 +++++ position/src/position/make_move.rs | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/position/src/position/captures.rs b/position/src/position/captures.rs index d6ce3e2..ddd9dac 100644 --- a/position/src/position/captures.rs +++ b/position/src/position/captures.rs @@ -8,6 +8,11 @@ use chessfriend_core::{Color, Piece}; pub(crate) struct CapturesList([Vec; Color::NUM]); impl CapturesList { + #[cfg(test)] + pub fn last(&self, color: Color) -> Option<&Piece> { + self.0[color as usize].last() + } + pub fn push(&mut self, color: Color, piece: Piece) { self.0[color as usize].push(piece); } diff --git a/position/src/position/make_move.rs b/position/src/position/make_move.rs index 6be4814..13c0d6a 100644 --- a/position/src/position/make_move.rs +++ b/position/src/position/make_move.rs @@ -407,7 +407,7 @@ mod tests { assert_eq!(pos.get_piece(Square::C2), None); assert_eq!(pos.get_piece(Square::F5), Some(piece!(White Bishop))); - assert_eq!(pos.captures[Color::White as usize][0], piece!(Black Rook)); + assert_eq!(pos.captures.last(Color::White), Some(&piece!(Black Rook))); assert_eq!(pos.board.active_color, Color::Black); assert_eq!(pos.board.half_move_clock, 0); @@ -444,7 +444,7 @@ mod tests { None, "capture target pawn not removed" ); - assert_eq!(pos.captures[Color::Black as usize][0], piece!(White Pawn)); + assert_eq!(pos.captures.last(Color::Black), Some(&piece!(White Pawn))); Ok(()) } From 2106e05d5739cdbb817cc27e6d60cdcffa9baad9 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 28 May 2025 16:22:16 -0700 Subject: [PATCH 317/423] [moves] Implement AllPiecesMoveGenerator A generator that yields moves for all pieces on the board. This generator combines the shape-specific move generators developed in prior commits into a single iterator that yields all moves. Implement FusedIterator for all generators so AllPiecesMoveGenerator can avoid doing a bunch of extra work once each iterator has yielded None. --- moves/src/generators.rs | 2 ++ moves/src/generators/all.rs | 64 ++++++++++++++++++++++++++++++++++ moves/src/generators/king.rs | 7 ++++ moves/src/generators/knight.rs | 6 ++-- moves/src/generators/pawn.rs | 4 ++- moves/src/generators/slider.rs | 5 +++ 6 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 moves/src/generators/all.rs diff --git a/moves/src/generators.rs b/moves/src/generators.rs index a3b6fcf..e2c0b4d 100644 --- a/moves/src/generators.rs +++ b/moves/src/generators.rs @@ -1,5 +1,6 @@ // Eryn Wells +mod all; mod king; mod knight; mod pawn; @@ -8,6 +9,7 @@ mod slider; #[cfg(test)] mod testing; +pub use all::AllPiecesMoveGenerator; pub use king::KingMoveGenerator; pub use knight::KnightMoveGenerator; pub use pawn::PawnMoveGenerator; diff --git a/moves/src/generators/all.rs b/moves/src/generators/all.rs new file mode 100644 index 0000000..d11ffd0 --- /dev/null +++ b/moves/src/generators/all.rs @@ -0,0 +1,64 @@ +// Eryn Wells + +use super::{ + BishopMoveGenerator, GeneratedMove, KingMoveGenerator, KnightMoveGenerator, PawnMoveGenerator, + QueenMoveGenerator, RookMoveGenerator, +}; +use chessfriend_board::Board; +use chessfriend_core::Color; +use std::iter::{Fuse, FusedIterator}; + +#[must_use] +pub struct AllPiecesMoveGenerator { + pawn_move_generator: Fuse, + knight_move_generator: Fuse, + bishop_move_generator: Fuse, + rook_move_generator: Fuse, + queen_move_generator: Fuse, + king_move_generator: Fuse, +} + +impl AllPiecesMoveGenerator { + pub fn new(board: &Board, color: Option) -> Self { + Self { + pawn_move_generator: PawnMoveGenerator::new(board, color).fuse(), + knight_move_generator: KnightMoveGenerator::new(board, color).fuse(), + bishop_move_generator: BishopMoveGenerator::new(board, color).fuse(), + rook_move_generator: RookMoveGenerator::new(board, color).fuse(), + queen_move_generator: QueenMoveGenerator::new(board, color).fuse(), + king_move_generator: KingMoveGenerator::new(board, color).fuse(), + } + } +} + +impl Iterator for AllPiecesMoveGenerator { + type Item = GeneratedMove; + + fn next(&mut self) -> Option { + macro_rules! return_next_move { + ($generator:expr) => {{ + let next_move = $generator.next(); + if next_move.is_some() { + return next_move; + } + }}; + } + + // All of these iterators are fused, meaning they are guaranteed to + // always return None once they've returned None the first time. Fuse + // can optimize this so that the work done in each of the move + // generators' next() methods doesn't have to be repeated every time + // next() is called. + + return_next_move!(self.pawn_move_generator); + return_next_move!(self.knight_move_generator); + return_next_move!(self.bishop_move_generator); + return_next_move!(self.rook_move_generator); + return_next_move!(self.queen_move_generator); + return_next_move!(self.king_move_generator); + + None + } +} + +impl FusedIterator for AllPiecesMoveGenerator {} diff --git a/moves/src/generators/king.rs b/moves/src/generators/king.rs index b091b3d..aa7eea0 100644 --- a/moves/src/generators/king.rs +++ b/moves/src/generators/king.rs @@ -4,6 +4,7 @@ use crate::{GeneratedMove, Move}; use chessfriend_bitboard::{bit_scanner::TrailingBitScanner, BitBoard}; use chessfriend_board::{Board, CastleParameters}; use chessfriend_core::{Color, Square, Wing}; +use std::iter::FusedIterator; #[must_use] pub struct KingMoveGenerator { @@ -84,6 +85,8 @@ impl Iterator for KingMoveGenerator { } } +impl FusedIterator for KingMoveGenerator {} + #[derive(Clone, Debug)] struct KingIterator { origin: Square, @@ -98,6 +101,8 @@ impl Iterator for KingIterator { } } +impl FusedIterator for KingIterator {} + #[derive(Clone, Debug)] struct CastleIterator { kingside: Option<&'static CastleParameters>, @@ -132,6 +137,8 @@ impl Iterator for CastleIterator { } } +impl FusedIterator for CastleIterator {} + #[cfg(test)] mod tests { use super::*; diff --git a/moves/src/generators/knight.rs b/moves/src/generators/knight.rs index 4b5a696..ab8d235 100644 --- a/moves/src/generators/knight.rs +++ b/moves/src/generators/knight.rs @@ -1,11 +1,11 @@ // Eryn Wells -use crate::Move; - use super::GeneratedMove; +use crate::Move; use chessfriend_bitboard::{bit_scanner::TrailingBitScanner, BitBoard}; use chessfriend_board::Board; use chessfriend_core::{Color, Square}; +use std::iter::FusedIterator; #[must_use] pub struct KnightMoveGenerator { @@ -66,6 +66,8 @@ impl Iterator for KnightMoveGenerator { } } +impl FusedIterator for KnightMoveGenerator {} + #[cfg(test)] mod tests { use super::*; diff --git a/moves/src/generators/pawn.rs b/moves/src/generators/pawn.rs index b7fd5ae..f4974de 100644 --- a/moves/src/generators/pawn.rs +++ b/moves/src/generators/pawn.rs @@ -7,7 +7,7 @@ use crate::{Move, PromotionShape}; use chessfriend_bitboard::{bit_scanner::TrailingBitScanner, BitBoard}; use chessfriend_board::Board; use chessfriend_core::{Color, Direction, Rank, Square}; -use std::slice; +use std::{iter::FusedIterator, slice}; pub struct PawnMoveGenerator { color: Color, @@ -224,6 +224,8 @@ impl std::iter::Iterator for PawnMoveGenerator { } } +impl FusedIterator for PawnMoveGenerator {} + impl MoveType { fn next(self) -> Option { match self { diff --git a/moves/src/generators/slider.rs b/moves/src/generators/slider.rs index b40890e..cd704d7 100644 --- a/moves/src/generators/slider.rs +++ b/moves/src/generators/slider.rs @@ -16,6 +16,7 @@ use crate::Move; use chessfriend_bitboard::{bit_scanner::TrailingBitScanner, BitBoard}; use chessfriend_board::Board; use chessfriend_core::{Color, Slider, Square}; +use std::iter::FusedIterator; macro_rules! slider_move_generator { ($vis:vis $name:ident, $slider:ident) => { @@ -35,6 +36,8 @@ macro_rules! slider_move_generator { self.0.next() } } + + impl FusedIterator for $name {} }; } @@ -117,6 +120,8 @@ impl Iterator for SliderMoveGenerator { } } +impl FusedIterator for SliderMoveGenerator {} + #[derive(Clone, Debug)] struct SliderInfo { origin: Square, From 43abbe3fe22712ca59609a76c73c6e567cb75a1b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 28 May 2025 16:23:46 -0700 Subject: [PATCH 318/423] [position] Register a captured piece in the MoveRecord For unmake_move, so the captured piece can be restored. --- position/src/move_record.rs | 19 ++++++++++++++++--- position/src/position/make_move.rs | 20 +++++++------------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/position/src/move_record.rs b/position/src/move_record.rs index 72af258..5fbfe27 100644 --- a/position/src/move_record.rs +++ b/position/src/move_record.rs @@ -1,15 +1,28 @@ // Eryn Wells -use chessfriend_board::{board::HalfMoveClock, CastleRights}; -use chessfriend_core::Square; +use chessfriend_board::{board::HalfMoveClock, Board, CastleRights}; +use chessfriend_core::{Color, Piece, Square}; use chessfriend_moves::Move; #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct MoveRecord { + pub color: Color, pub ply: Move, pub en_passant_target: Option, pub castling_rights: CastleRights, pub half_move_clock: HalfMoveClock, + pub captured_piece: Option, } -impl MoveRecord {} +impl MoveRecord { + pub fn new(board: &Board, ply: Move, capture: Option) -> Self { + Self { + color: board.active_color, + ply, + en_passant_target: board.en_passant_target, + castling_rights: board.castling_rights, + half_move_clock: board.half_move_clock, + captured_piece: capture, + } + } +} diff --git a/position/src/position/make_move.rs b/position/src/position/make_move.rs index 13c0d6a..52bff90 100644 --- a/position/src/position/make_move.rs +++ b/position/src/position/make_move.rs @@ -118,7 +118,7 @@ impl Position { self.remove_piece(origin); - let record = self.register_move_record(ply); + let record = self.register_move_record(ply, None); self.advance_clocks(HalfMoveClock::Advance); @@ -141,7 +141,7 @@ impl Position { _ => unreachable!(), }; - let record = self.register_move_record(ply); + let record = self.register_move_record(ply, None); self.advance_clocks(HalfMoveClock::Advance); @@ -181,7 +181,7 @@ impl Position { self.place_piece(piece, target_square, PlacePieceStrategy::Replace)?; } - let record = self.register_move_record(ply); + let record = self.register_move_record(ply, Some(captured_piece)); self.advance_clocks(HalfMoveClock::Reset); @@ -202,7 +202,7 @@ impl Position { self.board.castling_rights.revoke(active_color, wing); - let record = self.register_move_record(ply); + let record = self.register_move_record(ply, None); self.advance_clocks(HalfMoveClock::Advance); @@ -232,21 +232,15 @@ impl Position { ); } - let record = self.register_move_record(ply); + let record = self.register_move_record(ply, None); self.advance_clocks(HalfMoveClock::Reset); Ok(record) } - fn register_move_record(&mut self, ply: Move) -> MoveRecord { - let record = MoveRecord { - ply, - en_passant_target: self.board.en_passant_target, - castling_rights: self.board.castling_rights, - half_move_clock: self.board.half_move_clock, - }; - + fn register_move_record(&mut self, ply: Move, capture: Option) -> MoveRecord { + let record = MoveRecord::new(&self.board, ply, capture); self.moves.push(record.clone()); record From 942d9fe47bc10df60dc82184727b1c7b4b656008 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 28 May 2025 16:25:55 -0700 Subject: [PATCH 319/423] [explorer, moves, position] Implement a moves command in explorer The moves command writes all possible moves to the terminal. Move the previous implementation of the moves command, which marked squares that a piece could move to, to a 'movement' command. --- explorer/src/main.rs | 95 +++++++++++++++++++++++++++++++++------- moves/src/generators.rs | 8 ++++ position/src/lib.rs | 1 + position/src/position.rs | 36 ++++++++++++++- 4 files changed, 124 insertions(+), 16 deletions(-) diff --git a/explorer/src/main.rs b/explorer/src/main.rs index 9f63e83..7fe6fcb 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -2,7 +2,7 @@ use chessfriend_board::{fen::FromFenStr, Board}; use chessfriend_core::{Color, Piece, Shape, Square}; -use chessfriend_moves::Builder as MoveBuilder; +use chessfriend_moves::{Builder as MoveBuilder, GeneratedMove}; use chessfriend_position::{fen::ToFenStr, PlacePieceStrategy, Position, ValidateMove}; use clap::{Arg, Command}; @@ -64,8 +64,13 @@ fn command_line() -> Command { ) .subcommand( Command::new("moves") + .arg(Arg::new("square").required(false)) + .about("Show moves of a piece on a square. With no argument, show all moves for the active color."), + ) + .subcommand( + Command::new("movement") .arg(Arg::new("square").required(true)) - .about("Show moves of a piece on a square"), + .about("Show moves of a piece on a square."), ) .subcommand( Command::new("reset") @@ -86,8 +91,12 @@ fn command_line() -> Command { enum CommandHandlingError<'a> { #[error("lexer error")] LexerError, + #[error("missing {0} argument")] MissingArgument(&'a str), + + #[error("no piece on {0}")] + NoPiece(Square), } fn respond(line: &str, state: &mut State) -> anyhow::Result { @@ -158,19 +167,8 @@ fn respond(line: &str, state: &mut State) -> anyhow::Result { result.should_print_position = false; } - Some(("moves", matches)) => { - let square = matches - .get_one::("square") - .ok_or(CommandHandlingError::MissingArgument("square"))?; - let square = square.parse::()?; - - let movement = state.position.movement(square); - - let display = state.position.display().highlight(movement); - println!("\n{display}"); - - result.should_print_position = false; - } + Some(("moves", matches)) => result = do_moves_command(state, matches)?, + Some(("movement", matches)) => result = do_movement_command(state, matches)?, Some(("starting", _matches)) => { let starting_position = Position::starting(); state.position = starting_position; @@ -203,6 +201,73 @@ fn do_reset_command( Ok(CommandResult::default()) } +fn do_moves_command( + state: &mut State, + matches: &clap::ArgMatches, +) -> anyhow::Result { + let moves: Vec = if let Some(square) = matches + .get_one::("square") + .and_then(|square| square.parse::().ok()) + { + state + .position + .moves_for_piece(square) + .map(|it| it.filter(|ply| ply.origin() == square)) + .map(Iterator::collect) + .ok_or(CommandHandlingError::NoPiece(square))? + } else { + state.position.all_moves(None).collect() + }; + + let formatted_moves: Vec = moves + .iter() + .map(|ply| { + let piece = state.position.get_piece(ply.origin()).unwrap(); + format!("{piece}{ply}") + }) + .collect(); + + if !formatted_moves.is_empty() { + let max_length = formatted_moves + .iter() + .map(|s| s.chars().count()) + .max() + .unwrap_or(8) + + 2; + + let columns_count = 80 / max_length; + for row in formatted_moves.chunks(columns_count) { + for ply in row { + print!("{ply: anyhow::Result { + let square = *matches + .get_one::("square") + .ok_or(CommandHandlingError::MissingArgument("square"))?; + + let movement = state.position.movement(square); + let display = state.position.display().highlight(movement); + println!("\n{display}"); + + Ok(CommandResult { + should_continue: true, + should_print_position: false, + }) +} + fn main() -> Result<(), String> { let mut editor = DefaultEditor::new().map_err(|err| format!("Error: {err}"))?; diff --git a/moves/src/generators.rs b/moves/src/generators.rs index e2c0b4d..9c8f391 100644 --- a/moves/src/generators.rs +++ b/moves/src/generators.rs @@ -16,12 +16,20 @@ pub use pawn::PawnMoveGenerator; pub use slider::{BishopMoveGenerator, QueenMoveGenerator, RookMoveGenerator}; use crate::Move; +use chessfriend_core::Square; #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct GeneratedMove { pub(crate) ply: Move, } +impl GeneratedMove { + #[must_use] + pub fn origin(&self) -> Square { + self.ply.origin_square() + } +} + impl std::fmt::Display for GeneratedMove { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.ply.fmt(f) diff --git a/position/src/lib.rs b/position/src/lib.rs index a62da34..af97f5d 100644 --- a/position/src/lib.rs +++ b/position/src/lib.rs @@ -11,4 +11,5 @@ mod macros; mod testing; pub use chessfriend_board::{fen, PlacePieceError, PlacePieceStrategy}; +pub use chessfriend_moves::GeneratedMove; pub use position::{Position, ValidateMove}; diff --git a/position/src/position.rs b/position/src/position.rs index ea94971..700190c 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -4,6 +4,13 @@ mod captures; mod make_move; mod unmake_move; +use chessfriend_moves::{ + generators::{ + AllPiecesMoveGenerator, BishopMoveGenerator, KingMoveGenerator, KnightMoveGenerator, + PawnMoveGenerator, QueenMoveGenerator, RookMoveGenerator, + }, + GeneratedMove, +}; pub use make_move::{MakeMoveError, ValidateMove}; use crate::move_record::MoveRecord; @@ -12,7 +19,7 @@ use chessfriend_bitboard::BitBoard; use chessfriend_board::{ display::DiagramFormatter, fen::ToFenStr, Board, PlacePieceError, PlacePieceStrategy, }; -use chessfriend_core::{Color, Piece, Square}; +use chessfriend_core::{Color, Piece, Shape, Square}; use std::{cell::OnceCell, fmt}; #[must_use] @@ -79,6 +86,33 @@ impl Position { } } +impl Position { + pub fn all_moves(&self, color: Option) -> AllPiecesMoveGenerator { + AllPiecesMoveGenerator::new(&self.board, color) + } + + #[must_use] + pub fn moves_for_piece( + &self, + square: Square, + ) -> Option>> { + self.get_piece(square) + .map(|piece| Self::generator(&self.board, piece)) + } + + #[must_use] + fn generator(board: &Board, piece: Piece) -> Box> { + match piece.shape { + Shape::Pawn => Box::new(PawnMoveGenerator::new(board, Some(piece.color))), + Shape::Knight => Box::new(KnightMoveGenerator::new(board, Some(piece.color))), + Shape::Bishop => Box::new(BishopMoveGenerator::new(board, Some(piece.color))), + Shape::Rook => Box::new(RookMoveGenerator::new(board, Some(piece.color))), + Shape::Queen => Box::new(QueenMoveGenerator::new(board, Some(piece.color))), + Shape::King => Box::new(KingMoveGenerator::new(board, Some(piece.color))), + } + } +} + impl Position { pub fn active_sight(&self) -> BitBoard { self.board.active_sight() From a4b713a558f61502bc00c76fa28fc4c1b7f9aa67 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 29 May 2025 09:21:25 -0700 Subject: [PATCH 320/423] [position] Remove dead move_generator code --- position/src/move_generator.rs | 173 ------------ position/src/move_generator/bishop.rs | 143 ---------- position/src/move_generator/king.rs | 228 --------------- position/src/move_generator/knight.rs | 68 ----- position/src/move_generator/move_set.rs | 191 ------------- position/src/move_generator/pawn.rs | 353 ------------------------ position/src/move_generator/queen.rs | 154 ----------- position/src/move_generator/rook.rs | 131 --------- 8 files changed, 1441 deletions(-) delete mode 100644 position/src/move_generator.rs delete mode 100644 position/src/move_generator/bishop.rs delete mode 100644 position/src/move_generator/king.rs delete mode 100644 position/src/move_generator/knight.rs delete mode 100644 position/src/move_generator/move_set.rs delete mode 100644 position/src/move_generator/pawn.rs delete mode 100644 position/src/move_generator/queen.rs delete mode 100644 position/src/move_generator/rook.rs diff --git a/position/src/move_generator.rs b/position/src/move_generator.rs deleted file mode 100644 index 2f23616..0000000 --- a/position/src/move_generator.rs +++ /dev/null @@ -1,173 +0,0 @@ -// Eryn Wells - -mod bishop; -mod king; -mod knight; -mod move_set; -mod pawn; -mod queen; -mod rook; - -#[cfg(test)] -mod tests; - -pub(crate) use move_set::MoveSet; - -use self::{ - bishop::ClassicalMoveGenerator as BishopMoveGenerator, king::KingMoveGenerator, - knight::KnightMoveGenerator, pawn::PawnMoveGenerator, - queen::ClassicalMoveGenerator as QueenMoveGenerator, - rook::ClassicalMoveGenerator as RookMoveGenerator, -}; -use crate::Position; -use chessfriend_bitboard::BitBoard; -use chessfriend_board::Board; -use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; -use chessfriend_moves::Move; -use std::collections::BTreeMap; - -trait MoveGenerator { - fn iter(&self) -> dyn Iterator; - fn moves(&self, color: Color) -> dyn Iterator; - fn attacks(&self, color: Color) -> dyn Iterator; -} - -macro_rules! move_generator_declaration { - ($name:ident) => { - move_generator_declaration!($name, struct); - move_generator_declaration!($name, new); - move_generator_declaration!($name, getters); - }; - ($name:ident, struct) => { - #[derive(Clone, Debug, Eq, PartialEq)] - pub(super) struct $name { - color: chessfriend_core::Color, - move_sets: std::collections::BTreeMap< - chessfriend_core::Square, - $crate::move_generator::MoveSet, - >, - } - }; - ($name:ident, new) => { - impl $name { - pub(super) fn new( - board: &chessfriend_board::Board, - color: chessfriend_core::Color, - capture_mask: chessfriend_bitboard::BitBoard, - push_mask: chessfriend_bitboard::BitBoard, - ) -> $name { - let move_sets = if Self::shape() == chessfriend_core::Shape::King - || !(capture_mask.is_empty() && push_mask.is_empty()) - { - Self::move_sets(board, color, capture_mask, push_mask) - } else { - std::collections::BTreeMap::new() - }; - - $name { color, move_sets } - } - } - }; - ($name:ident, getters) => { - impl $name { - pub(super) fn iter(&self) -> impl Iterator + '_ { - self.move_sets.values().flat_map(|set| set.moves()) - } - - pub(crate) fn moves_for_piece( - &self, - piece: &chessfriend_core::PlacedPiece, - ) -> Option<&$crate::move_generator::move_set::MoveSet> { - self.move_sets.get(&piece.square()) - } - - #[cfg(test)] - fn _test_bitboard(&self) -> chessfriend_bitboard::BitBoard { - self.move_sets.values().fold( - chessfriend_bitboard::BitBoard::empty(), - |partial, mv_set| partial | mv_set.bitboard(), - ) - } - } - }; -} - -pub(self) use move_generator_declaration; - -trait MoveGeneratorInternal { - fn shape() -> Shape; - - fn piece(color: Color) -> Piece { - Piece::new(color, Self::shape()) - } - - fn move_sets( - board: &Board, - color: Color, - capture_mask: BitBoard, - push_mask: BitBoard, - ) -> BTreeMap { - let piece = Self::piece(color); - BTreeMap::from_iter( - board - .bitboard_for_piece(piece) - .occupied_squares() - .map(|square| { - let piece = PlacedPiece::new(piece, square); - let move_set = Self::move_set_for_piece(board, &piece, capture_mask, push_mask); - (square, move_set) - }), - ) - } - - fn move_set_for_piece( - board: &Board, - piece: &PlacedPiece, - capture_mask: BitBoard, - push_mask: BitBoard, - ) -> MoveSet; -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct Moves { - pawn_moves: PawnMoveGenerator, - knight_moves: KnightMoveGenerator, - bishop_moves: BishopMoveGenerator, - rook_moves: RookMoveGenerator, - queen_moves: QueenMoveGenerator, - king_moves: KingMoveGenerator, -} - -impl Moves { - pub fn new(board: &Board, color: Color, capture_mask: BitBoard, push_mask: BitBoard) -> Moves { - Moves { - pawn_moves: PawnMoveGenerator::new(board, color, capture_mask, push_mask), - knight_moves: KnightMoveGenerator::new(board, color, capture_mask, push_mask), - bishop_moves: BishopMoveGenerator::new(board, color, capture_mask, push_mask), - rook_moves: RookMoveGenerator::new(board, color, capture_mask, push_mask), - queen_moves: QueenMoveGenerator::new(board, color, capture_mask, push_mask), - king_moves: KingMoveGenerator::new(board, color, capture_mask, push_mask), - } - } - - pub(crate) fn moves_for_piece(&self, piece: &PlacedPiece) -> Option<&MoveSet> { - match piece.shape() { - Shape::Pawn => self.pawn_moves.moves_for_piece(piece), - Shape::Knight => self.knight_moves.moves_for_piece(piece), - Shape::Bishop => self.bishop_moves.moves_for_piece(piece), - Shape::Rook => self.rook_moves.moves_for_piece(piece), - Shape::Queen => self.queen_moves.moves_for_piece(piece), - Shape::King => self.king_moves.moves_for_piece(piece), - } - } - - pub fn iter(&self) -> impl Iterator + '_ { - self.pawn_moves - .iter() - .chain(self.knight_moves.iter()) - .chain(self.bishop_moves.iter()) - .chain(self.rook_moves.iter()) - .chain(self.queen_moves.iter()) - .chain(self.king_moves.iter()) - } -} diff --git a/position/src/move_generator/bishop.rs b/position/src/move_generator/bishop.rs deleted file mode 100644 index 45d9086..0000000 --- a/position/src/move_generator/bishop.rs +++ /dev/null @@ -1,143 +0,0 @@ -// Eryn Wells - -use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use chessfriend_bitboard::BitBoard; -use chessfriend_board::Board; -use chessfriend_core::{Direction, PlacedPiece, Shape}; - -move_generator_declaration!(ClassicalMoveGenerator); - -impl MoveGeneratorInternal for ClassicalMoveGenerator { - fn shape() -> Shape { - Shape::Bishop - } - - fn move_set_for_piece( - board: &Board, - piece: &PlacedPiece, - capture_mask: BitBoard, - push_mask: BitBoard, - ) -> MoveSet { - let square = piece.square(); - - let blockers = board.occupied_squares(); - let empty_squares = !blockers; - let (friendly_pieces, opposing_pieces) = board.all_pieces(); - - let mut all_moves = BitBoard::empty(); - - macro_rules! update_moves_with_ray { - ($direction:ident, $occupied_squares:tt) => { - let ray = BitBoard::ray(square, Direction::$direction); - if let Some(first_occupied_square) = - BitBoard::$occupied_squares(&(ray & blockers)).next() - { - let remainder = BitBoard::ray(first_occupied_square, Direction::$direction); - let attack_ray = ray & !remainder; - all_moves |= attack_ray; - } else { - all_moves |= ray; - } - }; - } - - update_moves_with_ray!(NorthEast, occupied_squares_trailing); - update_moves_with_ray!(NorthWest, occupied_squares_trailing); - update_moves_with_ray!(SouthEast, occupied_squares); - update_moves_with_ray!(SouthWest, occupied_squares); - - let quiet_moves = all_moves & (empty_squares | !friendly_pieces) & push_mask; - let capture_moves = all_moves & opposing_pieces & capture_mask; - - MoveSet::new(*piece) - .quiet_moves(quiet_moves) - .capture_moves(capture_moves) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::position; - use chessfriend_bitboard::BitBoard; - use chessfriend_core::Color; - - #[test] - fn classical_single_bishop_bitboard() { - let pos = position![ - White Bishop on A1, - ]; - - let generator = - ClassicalMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); - - assert_eq!( - generator._test_bitboard(), - BitBoard::new( - 0b10000000_01000000_00100000_00010000_00001000_00000100_00000010_00000000 - ) - ); - } - - /// Test that a bishop can move up to, but not onto, a friendly piece. - #[test] - fn classical_single_bishop_with_same_color_blocker_bitboard() { - let pos = position![ - White Bishop on A1, - White Knight on E5, - ]; - - println!("{}", pos.display()); - - let generator = - ClassicalMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); - - assert_eq!( - generator._test_bitboard(), - BitBoard::new( - 0b00000000_00000000_00000000_00000000_00001000_00000100_00000010_00000000 - ) - ); - } - - /// Test that a rook can move up to, and then capture, an enemy piece. - #[test] - fn classical_single_bishop_with_opposing_color_blocker_bitboard() { - let pos = position![ - White Bishop on A1, - Black Knight on C3, - ]; - - let generator = - ClassicalMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); - - assert_eq!( - generator._test_bitboard(), - BitBoard::new( - 0b00000000_00000000_00000000_00000000_00000000_00000100_00000010_00000000 - ) - ); - } - - #[test] - fn classical_single_bishop_in_center() { - let pos = position![ - White Bishop on E4, - ]; - - println!("{}", pos.display()); - - let generator = - ClassicalMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); - let bitboard = generator._test_bitboard(); - let expected = BitBoard::new( - 0b00000001_10000010_01000100_00101000_00000000_00101000_01000100_10000010, - ); - - assert_eq!( - bitboard, expected, - "actual:\n{}\nexpected:\n{}", - bitboard, expected - ); - } -} diff --git a/position/src/move_generator/king.rs b/position/src/move_generator/king.rs deleted file mode 100644 index 4daa155..0000000 --- a/position/src/move_generator/king.rs +++ /dev/null @@ -1,228 +0,0 @@ -// Eryn Wells - -//! Declares the KingMoveGenerator type. This struct is responsible for -//! generating the possible moves for the king in the given position. - -use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use chessfriend_bitboard::BitBoard; -use chessfriend_board::{castle::Castle, Board}; -use chessfriend_core::{PlacedPiece, Shape}; - -move_generator_declaration!(KingMoveGenerator, struct); -move_generator_declaration!(KingMoveGenerator, new); -move_generator_declaration!(KingMoveGenerator, getters); - -impl MoveGeneratorInternal for KingMoveGenerator { - fn shape() -> Shape { - Shape::King - } - - fn move_set_for_piece( - board: &Board, - placed_piece: &PlacedPiece, - _capture_mask: BitBoard, - _push_mask: BitBoard, - ) -> MoveSet { - let piece = placed_piece.piece(); - let color = piece.color(); - let square = placed_piece.square(); - - let safe_squares = BitBoard::FULL; - let all_king_moves = BitBoard::king_moves(square); - - let empty_squares = board.empty_squares(); - let safe_empty_squares = empty_squares & safe_squares; - - let opposing_pieces = board.bitboard_for_color(color.other()); - let opposing_pieces_on_safe_squares = opposing_pieces & safe_squares; - - let quiet_moves = all_king_moves & safe_empty_squares; - let capture_moves = all_king_moves & opposing_pieces_on_safe_squares; - - let mut move_set = MoveSet::new(*placed_piece) - .quiet_moves(quiet_moves) - .capture_moves(capture_moves); - - if board.player_can_castle(color, Castle::KingSide) { - move_set.kingside_castle(); - } - if board.player_can_castle(color, Castle::QueenSide) { - move_set.queenside_castle(); - } - - move_set - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{assert_move_list, test_position, testing::*}; - use chessfriend_bitboard::bitboard; - use chessfriend_board::castle::Castle; - use chessfriend_core::{piece, Color, Square}; - use chessfriend_moves::{Builder as MoveBuilder, Move}; - use std::collections::HashSet; - - #[test] - fn one_king() -> TestResult { - let pos = test_position![White King on E4]; - - let generator = - KingMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); - - assert_eq!( - generator._test_bitboard(), - bitboard![E5 F5 F4 F3 E3 D3 D4 D5] - ); - - let builder = MoveBuilder::push(&piece!(White King on E4)); - let expected_moves: HashSet = HashSet::from_iter([ - builder.clone().to(Square::D5).build()?, - builder.clone().to(Square::E5).build()?, - builder.clone().to(Square::F5).build()?, - builder.clone().to(Square::F4).build()?, - builder.clone().to(Square::F3).build()?, - builder.clone().to(Square::E3).build()?, - builder.clone().to(Square::D3).build()?, - builder.clone().to(Square::D4).build()?, - ]); - - let generated_moves: HashSet = generator.iter().collect(); - - assert_move_list!(generated_moves, expected_moves, pos); - - Ok(()) - } - - #[test] - fn one_king_corner() -> TestResult { - let pos = test_position![White King on A1]; - - let generator = - KingMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); - - let generated_bitboard = generator._test_bitboard(); - let expected_bitboard = bitboard![A2 B2 B1]; - assert_eq!( - generator._test_bitboard(), - bitboard![A2 B2 B1], - "Generated:\n{generated_bitboard}\nExpected:\n{expected_bitboard}" - ); - - let builder = MoveBuilder::push(&piece!(White King on A1)); - let expected_moves = [ - builder.clone().to(Square::A2).build()?, - builder.clone().to(Square::B1).build()?, - builder.clone().to(Square::B2).build()?, - ]; - - let mut generated_moves: HashSet<_> = generator.iter().collect(); - - for ex_move in expected_moves { - assert!( - generated_moves.remove(&ex_move), - "{:#?} was not generated", - &ex_move - ); - } - - assert!( - generated_moves.is_empty(), - "Moves unexpectedly present: {:#?}", - generated_moves - ); - - Ok(()) - } - - #[test] - fn black_king_in_check_by_rook() { - let pos = test_position!(Black, [ - White King on E1, - White Rook on E4, - Black King on E7, - ]); - - assert!(pos.is_king_in_check()); - - let generator = - KingMoveGenerator::new(&pos.board, Color::Black, BitBoard::FULL, BitBoard::FULL); - let generated_moves = generator._test_bitboard(); - - let expected_moves = bitboard![F8 F7 F6 D6 D7 D8]; - - assert_eq!(generated_moves, expected_moves); - } - - #[test] - fn white_king_unobstructed_castles() -> TestResult { - let pos = test_position!( - White King on E1, - White Rook on A1, - White Rook on H1, - ); - - assert!(pos.player_can_castle(Color::White, Castle::KingSide)); - assert!(pos.player_can_castle(Color::White, Castle::QueenSide)); - - let generator = - KingMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); - let generated_moves: HashSet = generator.iter().collect(); - - assert!(generated_moves - .contains(&MoveBuilder::castling(Color::White, Castle::KingSide).build()?)); - assert!(generated_moves - .contains(&MoveBuilder::castling(Color::White, Castle::QueenSide).build()?)); - - Ok(()) - } - - #[test] - fn white_king_obstructed_queenside_castle() -> TestResult { - let pos = test_position!( - White King on E1, - White Knight on B1, - White Rook on A1, - White Rook on H1, - ); - - assert!(pos.player_can_castle(Color::White, Castle::KingSide)); - assert!(!pos.player_can_castle(Color::White, Castle::QueenSide)); - - let generator = - KingMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); - let generated_moves: HashSet = generator.iter().collect(); - - assert!(generated_moves - .contains(&MoveBuilder::castling(Color::White, Castle::KingSide).build()?)); - assert!(!generated_moves - .contains(&MoveBuilder::castling(Color::White, Castle::QueenSide).build()?)); - - Ok(()) - } - - #[test] - fn white_king_obstructed_kingside_castle() -> TestResult { - let pos = test_position!( - White King on E1, - White Rook on A1, - White Knight on G1, - White Rook on H1, - ); - - assert!(!pos.player_can_castle(Color::White, Castle::KingSide)); - assert!(pos.player_can_castle(Color::White, Castle::QueenSide)); - - let generator = - KingMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); - let generated_moves: HashSet = generator.iter().collect(); - - assert!(!generated_moves - .contains(&MoveBuilder::castling(Color::White, Castle::KingSide).build()?)); - assert!(generated_moves - .contains(&MoveBuilder::castling(Color::White, Castle::QueenSide).build()?)); - - Ok(()) - } -} diff --git a/position/src/move_generator/knight.rs b/position/src/move_generator/knight.rs deleted file mode 100644 index 76afa44..0000000 --- a/position/src/move_generator/knight.rs +++ /dev/null @@ -1,68 +0,0 @@ -// Eryn Wells - -use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use chessfriend_bitboard::BitBoard; -use chessfriend_board::Board; -use chessfriend_core::{PlacedPiece, Shape}; - -move_generator_declaration!(KnightMoveGenerator); - -impl MoveGeneratorInternal for KnightMoveGenerator { - fn shape() -> Shape { - Shape::Knight - } - - fn move_set_for_piece( - board: &Board, - placed_piece: &PlacedPiece, - capture_mask: BitBoard, - push_mask: BitBoard, - ) -> MoveSet { - let opposing_pieces = board.bitboard_for_color(placed_piece.piece().color().other()); - let empty_squares = board.empty_squares(); - let knight_moves = BitBoard::knight_moves(placed_piece.square()); - - let quiet_moves = knight_moves & empty_squares & push_mask; - let capture_moves = knight_moves & opposing_pieces & capture_mask; - - MoveSet::new(*placed_piece) - .quiet_moves(quiet_moves) - .capture_moves(capture_moves) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{assert_move_list, position, testing::*}; - use chessfriend_core::{piece, Color, Square}; - use chessfriend_moves::Builder as MoveBuilder; - use std::collections::HashSet; - - #[test] - fn one_knight() -> TestResult { - let pos = position![ - White Knight on E4, - ]; - - let generator = - KnightMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); - let generated_moves: HashSet<_> = generator.iter().collect(); - - let piece = piece!(White Knight on E4); - let expected_moves = HashSet::from_iter([ - MoveBuilder::push(&piece).to(Square::C3).build()?, - MoveBuilder::push(&piece).to(Square::D2).build()?, - MoveBuilder::push(&piece).to(Square::F2).build()?, - MoveBuilder::push(&piece).to(Square::G3).build()?, - MoveBuilder::push(&piece).to(Square::C5).build()?, - MoveBuilder::push(&piece).to(Square::D6).build()?, - MoveBuilder::push(&piece).to(Square::G5).build()?, - MoveBuilder::push(&piece).to(Square::F6).build()?, - ]); - - assert_move_list!(generated_moves, expected_moves, pos); - - Ok(()) - } -} diff --git a/position/src/move_generator/move_set.rs b/position/src/move_generator/move_set.rs deleted file mode 100644 index b4ae2d3..0000000 --- a/position/src/move_generator/move_set.rs +++ /dev/null @@ -1,191 +0,0 @@ -// Eryn Wells - -use chessfriend_bitboard::BitBoard; -use chessfriend_board::{castle::Castle, en_passant::EnPassant}; -use chessfriend_core::{PlacedPiece, Square}; -use chessfriend_moves::{Builder as MoveBuilder, Move}; - -/// A set of bitboards defining the moves for a single piece on the board. -#[derive(Clone, Debug, Default, Eq, PartialEq)] -struct BitBoardSet { - quiet: BitBoard, - captures: BitBoard, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) enum Special { - Pawn { en_passant: EnPassant }, - King { castles: u8 }, -} - -/// A set of moves for a single piece on the board. -#[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) struct MoveSet { - piece: PlacedPiece, - bitboards: BitBoardSet, - special: Option, -} - -impl MoveSet { - pub(super) fn new(piece: PlacedPiece) -> MoveSet { - MoveSet { - piece, - bitboards: BitBoardSet::default(), - special: None, - } - } - - pub(crate) fn can_move_to_square(&self, target_square: Square) -> bool { - match self.special { - Some(Special::King { castles }) => { - if self.check_castle_field(castles, Castle::KingSide) - && target_square - == Castle::KingSide - .parameters(self.piece.color()) - .king_target_square() - { - return true; - } - - if self.check_castle_field(castles, Castle::KingSide) - && target_square - == Castle::QueenSide - .parameters(self.piece.color()) - .king_target_square() - { - return true; - } - } - Some(Special::Pawn { en_passant }) => { - if target_square == en_passant.target_square() { - return true; - } - } - None => {} - } - - self.bitboard().contains(target_square) - } - - pub(crate) fn can_castle(&self, castle: Castle) -> bool { - match self.special { - Some(Special::King { castles }) => self.check_castle_field(castles, castle), - _ => false, - } - } - - fn check_castle_field(&self, castle_field: u8, castle: Castle) -> bool { - (castle_field & 1 << castle as u8) != 0 - } - - pub(super) fn quiet_moves(mut self, bitboard: BitBoard) -> MoveSet { - self.bitboards.quiet = bitboard; - self - } - - pub(super) fn capture_moves(mut self, bitboard: BitBoard) -> MoveSet { - self.bitboards.captures = bitboard; - self - } - - pub(super) fn kingside_castle(&mut self) -> &mut MoveSet { - match self.special { - Some(Special::King { ref mut castles }) => *castles |= 1 << Castle::KingSide as u8, - _ => { - self.special = Some(Special::King { - castles: 1 << Castle::KingSide as u8, - }) - } - } - - self - } - - pub(super) fn queenside_castle(&mut self) -> &mut MoveSet { - match self.special { - Some(Special::King { ref mut castles }) => *castles |= 1 << Castle::QueenSide as u8, - _ => { - self.special = Some(Special::King { - castles: 1 << Castle::QueenSide as u8, - }) - } - } - - self - } - - pub(super) fn en_passant(&mut self, en_passant: EnPassant) -> &mut MoveSet { - self.special = Some(Special::Pawn { en_passant }); - self - } - - /// A `BitBoard` representing all possible moves. - pub(super) fn bitboard(&self) -> BitBoard { - self.bitboards.captures | self.bitboards.quiet - } - - pub(crate) fn moves(&self) -> impl Iterator + '_ { - let piece = &self.piece; - let color = piece.color(); - - let is_pawn_on_starting_rank = - piece.is_pawn() && piece.square().rank().is_pawn_starting_rank(color); - - self.bitboards - .quiet - .occupied_squares() - .filter_map(move |to_square| { - if is_pawn_on_starting_rank - && to_square.rank().is_pawn_double_push_target_rank(color) - { - MoveBuilder::double_push(piece.square().file(), color) - .build() - .ok() - } else { - MoveBuilder::push(piece).to(to_square).build().ok() - } - }) - .chain( - self.bitboards - .captures - .occupied_squares() - .filter_map(|to_square| { - MoveBuilder::push(piece) - .capturing_on(to_square) - .build() - .ok() - }), - ) - .chain(self.castle_move(Castle::KingSide)) - .chain(self.castle_move(Castle::QueenSide)) - .chain(self.en_passant_move()) - } - - fn castle_move(&self, castle: Castle) -> Option { - match self.special { - Some(Special::King { castles }) => { - if (castles & 1 << castle as u8) != 0 { - Some( - MoveBuilder::castling(self.piece.color(), castle) - .build() - .ok()?, - ) - } else { - None - } - } - _ => None, - } - } - - fn en_passant_move(&self) -> Option { - match self.special { - Some(Special::Pawn { en_passant }) => Some(unsafe { - MoveBuilder::push(&self.piece) - .capturing_en_passant_on(en_passant.target_square()) - .build_unchecked() - }), - _ => None, - } - } -} diff --git a/position/src/move_generator/pawn.rs b/position/src/move_generator/pawn.rs deleted file mode 100644 index ddc1767..0000000 --- a/position/src/move_generator/pawn.rs +++ /dev/null @@ -1,353 +0,0 @@ -// Eryn Wells - -use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use chessfriend_bitboard::BitBoard; -use chessfriend_board::{en_passant::EnPassant, Board}; -use chessfriend_core::{Color, PlacedPiece, Rank, Shape, Square}; -use chessfriend_moves::Move; -use std::collections::BTreeMap; - -#[derive(Debug)] -struct MoveIterator(usize, usize); - -#[derive(Clone, Debug, Eq, PartialEq)] -pub(super) struct PawnMoveGenerator { - color: chessfriend_core::Color, - move_sets: BTreeMap, - en_passant_captures: Vec, -} - -move_generator_declaration!(PawnMoveGenerator, getters); - -impl MoveGeneratorInternal for PawnMoveGenerator { - fn shape() -> Shape { - Shape::Pawn - } - - fn move_set_for_piece( - board: &Board, - placed_piece: &PlacedPiece, - capture_mask: BitBoard, - push_mask: BitBoard, - ) -> MoveSet { - let capture_moves = Self::attacks(board, &placed_piece) & capture_mask; - let quiet_moves = Self::pushes(board, &placed_piece) & push_mask; - - let mut move_set = MoveSet::new(*placed_piece) - .quiet_moves(quiet_moves) - .capture_moves(capture_moves); - - if let Some(en_passant) = Self::en_passant(board, placed_piece, &push_mask, &capture_mask) { - move_set.en_passant(en_passant); - } - - move_set - } -} - -impl PawnMoveGenerator { - pub(super) fn new( - board: &Board, - player_to_move: Color, - capture_mask: BitBoard, - push_mask: BitBoard, - ) -> Self { - let move_sets = if !(capture_mask.is_empty() && push_mask.is_empty()) { - Self::move_sets(board, player_to_move, capture_mask, push_mask) - } else { - std::collections::BTreeMap::new() - }; - - Self { - color: player_to_move, - move_sets, - en_passant_captures: Vec::new(), - } - } - - fn move_sets( - board: &Board, - color: Color, - capture_mask: BitBoard, - push_mask: BitBoard, - ) -> BTreeMap { - let piece = Self::piece(color); - let moves_for_pieces = BTreeMap::from_iter( - board - .bitboard_for_piece(piece) - .occupied_squares() - .map(|square| { - let piece = PlacedPiece::new(piece, square); - let move_set = Self::move_set_for_piece(board, &piece, capture_mask, push_mask); - (square, move_set) - }), - ); - - moves_for_pieces - } - - fn pushes(board: &Board, piece: &PlacedPiece) -> BitBoard { - let color = piece.color(); - let square = piece.square(); - let bitboard: BitBoard = square.into(); - - let starting_rank = Rank::PAWN_STARTING_RANKS[color as usize]; - let empty_squares = board.empty_squares(); - - match color { - Color::White => { - let mut moves = bitboard.shift_north_one() & empty_squares; - if !(bitboard & BitBoard::rank(starting_rank.as_index())).is_empty() { - moves |= moves.shift_north_one() & empty_squares; - } - - moves - } - Color::Black => { - let mut moves = bitboard.shift_south_one() & empty_squares; - if !(bitboard & BitBoard::rank(starting_rank.as_index())).is_empty() { - moves |= moves.shift_south_one() & empty_squares; - } - - moves - } - } - } - - fn attacks(board: &Board, piece: &PlacedPiece) -> BitBoard { - let color = piece.color(); - - let opponent_pieces = board.bitboard_for_color(color.other()); - - BitBoard::pawn_attacks(piece.square(), color) & opponent_pieces - } - - fn en_passant( - board: &Board, - piece: &PlacedPiece, - push_mask: &BitBoard, - capture_mask: &BitBoard, - ) -> Option { - match board.en_passant() { - Some(en_passant) => { - let target_square: BitBoard = en_passant.target_square().into(); - let capture_square: BitBoard = en_passant.capture_square().into(); - - if (target_square & push_mask).is_empty() - && (capture_square & capture_mask).is_empty() - { - // Do not allow en passant if capturing would not either - // block an active check, or capture a checking pawn. - return None; - } - - let capture = BitBoard::pawn_attacks(piece.square(), piece.color()) & target_square; - if capture.is_empty() { - return None; - } - - match board.piece_on_square(en_passant.capture_square()) { - Some(_) => Some(en_passant), - None => None, - } - } - None => None, - } - } - - #[cfg(none)] - fn does_en_passant_reveal_check(&self, position: &Position) -> bool { - let player_to_move = position.player_to_move(); - let opposing_player = player_to_move.other(); - - if position.king_square(opposing_player).rank() - != Rank::PAWN_DOUBLE_PUSH_TARGET_RANKS[player_to_move as usize] - { - return false; - } - false - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{assert_move_list, formatted_move_list, test_position, testing::*}; - use chessfriend_core::{piece, Color, Square}; - use chessfriend_moves::{Builder as MoveBuilder, Move}; - use std::collections::HashSet; - - #[test] - fn one_double_push() -> TestResult { - let pos = test_position![White Pawn on E2]; - - let generator = - PawnMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); - - let pawn = piece!(White Pawn on E2); - let expected_moves = HashSet::from_iter([ - MoveBuilder::push(&pawn).to(Square::E3).build()?, - MoveBuilder::double_push(pawn.square().file(), pawn.color()).build()?, - ]); - - let generated_moves: HashSet<_> = generator.iter().collect(); - - assert_move_list!(generated_moves, expected_moves, pos); - - Ok(()) - } - - #[test] - fn one_single_push() -> TestResult { - let pos = test_position![White Pawn on E3]; - - let generator = - PawnMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); - let generated_moves: HashSet<_> = generator.iter().collect(); - - let expected_moves = HashSet::from_iter([MoveBuilder::push(&piece!(White Pawn on E3)) - .to(Square::E4) - .build()?]); - - assert_move_list!(generated_moves, expected_moves, pos); - - Ok(()) - } - - #[test] - fn one_obstructed_2square_push() -> TestResult { - let pos = test_position![ - White Pawn on E2, - White Knight on E4, - ]; - - let generator = - PawnMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); - - let expected_moves = HashSet::from_iter([MoveBuilder::push(&piece!(White Pawn on E2)) - .to(Square::E3) - .build()?]); - - let generated_moves: HashSet<_> = generator.iter().collect(); - - assert_move_list!(generated_moves, expected_moves, pos); - - Ok(()) - } - - #[test] - fn one_obstructed_1square_push() { - let pos = test_position![ - White Pawn on E2, - White Knight on E3, - ]; - - let generator = - PawnMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); - - let generated_moves: HashSet<_> = generator.iter().collect(); - let expected_moves: HashSet<_> = HashSet::new(); - - assert_move_list!(generated_moves, expected_moves, pos); - } - - #[test] - fn one_attack() -> TestResult { - let pos = test_position![ - White Pawn on E4, - White Bishop on E5, - Black Knight on D5, - ]; - - let generator = - PawnMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); - - let expected_moves = HashSet::from_iter([MoveBuilder::push(&piece!(White Pawn on E4)) - .capturing_on(Square::D5) - .build()?]); - - let generated_moves: HashSet<_> = generator.iter().collect(); - - assert_move_list!(generated_moves, expected_moves, pos); - - Ok(()) - } - - #[test] - fn one_double_attack() -> TestResult { - let pos = test_position![ - White Pawn on E4, - White Bishop on E5, - Black Knight on D5, - Black Queen on F5, - ]; - - let generator = - PawnMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); - - let builder = MoveBuilder::push(&piece!(White Pawn on E4)); - let expected_moves = HashSet::from_iter([ - builder.clone().capturing_on(Square::D5).build()?, - builder.clone().capturing_on(Square::F5).build()?, - ]); - - let generated_moves: HashSet<_> = generator.iter().collect(); - - assert_eq!( - generated_moves, expected_moves, - "generated: {:#?}\nexpected: {:#?}", - generated_moves, expected_moves - ); - - Ok(()) - } - - #[test] - fn one_en_passant_attack() -> TestResult { - let pos = test_position!(Black, [ - White Pawn on D4, - Black Pawn on E4, - ], D3); - - let generator = - PawnMoveGenerator::new(&pos.board, Color::Black, BitBoard::FULL, BitBoard::FULL); - let generated_moves: HashSet = generator.iter().collect(); - - let builder = MoveBuilder::push(&piece!(Black Pawn on E4)); - let expected_moves = HashSet::from_iter([ - builder.capturing_en_passant_on(Square::D3).build()?, - builder.clone().to(Square::E3).build()?, - ]); - - assert_move_list!(generated_moves, expected_moves, pos); - - Ok(()) - } - - /// Make sure the player cannot capture en passant if doing so would not resolve the check. - #[test] - fn cannot_capture_en_passant_while_in_check() -> TestResult { - let pos = test_position!(Black, [ - Black King on B5, - Black Pawn on E4, - White Pawn on D4, - White Rook on B1, - ], D3); - - assert!(pos.is_king_in_check()); - - let generated_moves: HashSet<_> = pos.moves().iter().collect(); - - assert!( - !generated_moves.contains( - &MoveBuilder::push(&piece!(Black Pawn on E4)) - .capturing_en_passant_on(Square::D3) - .build()? - ), - "Valid moves: {:?}", - formatted_move_list!(generated_moves, pos) - ); - - Ok(()) - } -} diff --git a/position/src/move_generator/queen.rs b/position/src/move_generator/queen.rs deleted file mode 100644 index b189805..0000000 --- a/position/src/move_generator/queen.rs +++ /dev/null @@ -1,154 +0,0 @@ -// Eryn Wells - -use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use chessfriend_bitboard::BitBoard; -use chessfriend_board::Board; -use chessfriend_core::{Direction, PlacedPiece, Shape}; - -move_generator_declaration!(ClassicalMoveGenerator); - -impl MoveGeneratorInternal for ClassicalMoveGenerator { - fn shape() -> Shape { - Shape::Queen - } - - fn move_set_for_piece( - board: &Board, - placed_piece: &PlacedPiece, - capture_mask: BitBoard, - push_mask: BitBoard, - ) -> MoveSet { - let piece = placed_piece.piece(); - let color = piece.color(); - let square = placed_piece.square(); - - let blockers = board.occupied_squares(); - let empty_squares = !blockers; - let friendly_pieces = board.bitboard_for_color(color); - let opposing_pieces = board.bitboard_for_color(color.other()); - - let mut all_moves = BitBoard::empty(); - - macro_rules! update_moves_with_ray { - ($direction:ident, $occupied_squares:tt) => { - let ray = BitBoard::ray(square, Direction::$direction); - if let Some(first_occupied_square) = - BitBoard::$occupied_squares(&(ray & blockers)).next() - { - let remainder = BitBoard::ray(first_occupied_square, Direction::$direction); - let attack_ray = ray & !remainder; - all_moves |= attack_ray; - } else { - all_moves |= ray; - } - }; - } - - update_moves_with_ray!(NorthWest, occupied_squares_trailing); - update_moves_with_ray!(North, occupied_squares_trailing); - update_moves_with_ray!(NorthEast, occupied_squares_trailing); - update_moves_with_ray!(East, occupied_squares_trailing); - update_moves_with_ray!(SouthEast, occupied_squares); - update_moves_with_ray!(South, occupied_squares); - update_moves_with_ray!(SouthWest, occupied_squares); - update_moves_with_ray!(West, occupied_squares); - - let quiet_moves = all_moves & (empty_squares | !friendly_pieces) & push_mask; - let capture_moves = all_moves & opposing_pieces & capture_mask; - - MoveSet::new(*placed_piece) - .quiet_moves(quiet_moves) - .capture_moves(capture_moves) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::test_position; - use chessfriend_bitboard::{bitboard, BitBoard}; - use chessfriend_core::Color; - - #[test] - fn classical_single_queen_bitboard() { - let pos = test_position![White Queen on B2]; - - let generator = - ClassicalMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); - let bitboard = generator._test_bitboard(); - let expected = bitboard![ - A2 C2 D2 E2 F2 G2 H2 // Rank - B1 B3 B4 B5 B6 B7 B8 // File - A1 C3 D4 E5 F6 G7 H8 // Diagonal - C1 A3 // Anti-diagonal - ]; - - assert_eq!( - bitboard, expected, - "actual:\n{}\nexpected:\n{}", - bitboard, expected - ); - } - - /// Test that a rook can move up to, but not onto, a friendly piece. - #[test] - fn classical_single_queen_with_same_color_blocker_bitboard() { - let pos = test_position![ - White Queen on A1, - White Knight on E1, - ]; - - let generator = - ClassicalMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); - let bitboard = generator._test_bitboard(); - let expected = BitBoard::new( - 0b10000001_01000001_00100001_00010001_00001001_00000101_00000011_00001110, - ); - - assert_eq!( - bitboard, expected, - "actual:\n{}\nexpected:\n{}", - bitboard, expected - ); - } - - /// Test that a rook can move up to, and then capture, an enemy piece. - #[test] - fn classical_single_queen_with_opposing_color_blocker_bitboard() { - let pos = test_position![ - White Queen on B2, - Black Knight on E5, - ]; - - let generator = - ClassicalMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); - - assert_eq!( - generator._test_bitboard(), - bitboard![ - A2 C2 D2 E2 F2 G2 H2 // Rank - B1 B3 B4 B5 B6 B7 B8 // File - A1 C3 D4 E5 // Diagonal - C1 A3 // Anti-diagonal - ] - ); - } - - #[test] - fn classical_single_queen_in_center() { - let pos = test_position![White Queen on D3]; - - let generator = - ClassicalMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); - - assert_eq!( - generator._test_bitboard(), - bitboard![ - A3 B3 C3 E3 F3 G3 H3 // Rank - D1 D2 D4 D5 D6 D7 D8 // File - B1 C2 E4 F5 G6 H7 // Diagonal - F1 E2 C4 B5 A6 // Anti-diagonal - ] - ); - } -} diff --git a/position/src/move_generator/rook.rs b/position/src/move_generator/rook.rs deleted file mode 100644 index e01f733..0000000 --- a/position/src/move_generator/rook.rs +++ /dev/null @@ -1,131 +0,0 @@ -// Eryn Wells - -use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use chessfriend_bitboard::BitBoard; -use chessfriend_board::Board; -use chessfriend_core::{Direction, PlacedPiece, Shape}; - -move_generator_declaration!(ClassicalMoveGenerator); - -impl MoveGeneratorInternal for ClassicalMoveGenerator { - fn shape() -> Shape { - Shape::Rook - } - - fn move_set_for_piece( - board: &Board, - placed_piece: &PlacedPiece, - capture_mask: BitBoard, - push_mask: BitBoard, - ) -> MoveSet { - let piece = placed_piece.piece(); - let color = piece.color(); - let square = placed_piece.square(); - - let blockers = board.occupied_squares(); - let empty_squares = !blockers; - let friendly_pieces = board.bitboard_for_color(color); - let opposing_pieces = board.bitboard_for_color(color.other()); - - let mut all_moves = BitBoard::empty(); - - macro_rules! update_moves_with_ray { - ($direction:ident, $occupied_squares:tt) => { - let ray = BitBoard::ray(square, Direction::$direction); - if let Some(first_occupied_square) = - BitBoard::$occupied_squares(&(ray & blockers)).next() - { - let remainder = BitBoard::ray(first_occupied_square, Direction::$direction); - let attack_ray = ray & !remainder; - all_moves |= attack_ray; - } else { - all_moves |= ray; - } - }; - } - - update_moves_with_ray!(North, occupied_squares_trailing); - update_moves_with_ray!(East, occupied_squares_trailing); - update_moves_with_ray!(South, occupied_squares); - update_moves_with_ray!(West, occupied_squares); - - let quiet_moves = all_moves & (empty_squares | !friendly_pieces) & capture_mask; - let capture_moves = all_moves & opposing_pieces & push_mask; - - MoveSet::new(*placed_piece) - .quiet_moves(quiet_moves) - .capture_moves(capture_moves) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::test_position; - use chessfriend_bitboard::{bitboard, BitBoard}; - use chessfriend_core::Color; - - #[test] - fn classical_single_rook_bitboard() { - let pos = test_position![White Rook on A2]; - - let generator = - ClassicalMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); - - assert_eq!( - generator._test_bitboard(), - bitboard![A1 A3 A4 A5 A6 A7 A8 B2 C2 D2 E2 F2 G2 H2] - ); - } - - /// Test that a rook can move up to, but not onto, a friendly piece. - #[test] - fn classical_single_rook_with_same_color_blocker_bitboard() { - let pos = test_position![ - White Rook on A1, - White Knight on E1, - ]; - - println!("{}", pos.display()); - - let generator = - ClassicalMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); - - assert_eq!( - generator._test_bitboard(), - BitBoard::new( - 0b00000001_00000001_00000001_00000001_00000001_00000001_00000001_00001110 - ) - ); - } - - /// Test that a rook can move up to, and then capture, an enemy piece. - #[test] - fn classical_single_rook_with_opposing_color_blocker_bitboard() { - let pos = test_position![ - White Rook on A1, - Black Knight on E1, - ]; - - let generator = - ClassicalMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); - - assert_eq!( - generator._test_bitboard(), - bitboard![A2 A3 A4 A5 A6 A7 A8 B1 C1 D1 E1] - ); - } - - #[test] - fn classical_single_rook_in_center() { - let pos = test_position![White Rook on D4]; - - let generator = - ClassicalMoveGenerator::new(&pos.board, Color::White, BitBoard::FULL, BitBoard::FULL); - - assert_eq!( - generator._test_bitboard(), - bitboard![A4 B4 C4 E4 F4 G4 H4 D1 D2 D3 D5 D6 D7 D8] - ); - } -} From b8f45aaecec61ac6c2a0167db394bf4d3bfa67c8 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 31 May 2025 14:28:27 -0700 Subject: [PATCH 321/423] [position] Add some documentation to MoveRecord --- position/src/move_record.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/position/src/move_record.rs b/position/src/move_record.rs index 5fbfe27..fa80aa3 100644 --- a/position/src/move_record.rs +++ b/position/src/move_record.rs @@ -4,13 +4,27 @@ use chessfriend_board::{board::HalfMoveClock, Board, CastleRights}; use chessfriend_core::{Color, Piece, Square}; use chessfriend_moves::Move; +/// A record of a move made on a board. This struct contains all the information +/// necessary to restore the board position to its state immediately before the +/// move was made. #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct MoveRecord { + /// The color of the player who made the move pub color: Color, + + /// The move played pub ply: Move, + + /// The en passant square when the move was made pub en_passant_target: Option, + + /// Castling rights for all players when the move was made pub castling_rights: CastleRights, + + /// The half move clock when the move was made pub half_move_clock: HalfMoveClock, + + /// The piece captured by the move, if any pub captured_piece: Option, } From 34e8c08c36f8f6f388134522e70733b3324de54b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 31 May 2025 14:32:39 -0700 Subject: [PATCH 322/423] [moves, position] Move MoveRecord to the moves crate --- moves/src/lib.rs | 2 ++ position/src/move_record.rs => moves/src/record.rs | 5 +++-- position/src/lib.rs | 1 - position/src/position.rs | 3 +-- position/src/position/make_move.rs | 4 ++-- 5 files changed, 8 insertions(+), 7 deletions(-) rename position/src/move_record.rs => moves/src/record.rs (95%) diff --git a/moves/src/lib.rs b/moves/src/lib.rs index 11f8422..fd74a25 100644 --- a/moves/src/lib.rs +++ b/moves/src/lib.rs @@ -6,8 +6,10 @@ pub mod testing; mod builder; mod defs; mod moves; +mod record; pub use builder::{Builder, Error as BuildMoveError, Result as BuildMoveResult}; pub use defs::{Kind, PromotionShape}; pub use generators::GeneratedMove; pub use moves::Move; +pub use record::MoveRecord; diff --git a/position/src/move_record.rs b/moves/src/record.rs similarity index 95% rename from position/src/move_record.rs rename to moves/src/record.rs index fa80aa3..3c91867 100644 --- a/position/src/move_record.rs +++ b/moves/src/record.rs @@ -1,14 +1,14 @@ // Eryn Wells +use crate::Move; use chessfriend_board::{board::HalfMoveClock, Board, CastleRights}; use chessfriend_core::{Color, Piece, Square}; -use chessfriend_moves::Move; /// A record of a move made on a board. This struct contains all the information /// necessary to restore the board position to its state immediately before the /// move was made. #[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) struct MoveRecord { +pub struct MoveRecord { /// The color of the player who made the move pub color: Color, @@ -29,6 +29,7 @@ pub(crate) struct MoveRecord { } impl MoveRecord { + #[must_use] pub fn new(board: &Board, ply: Move, capture: Option) -> Self { Self { color: board.active_color, diff --git a/position/src/lib.rs b/position/src/lib.rs index af97f5d..0307e41 100644 --- a/position/src/lib.rs +++ b/position/src/lib.rs @@ -1,6 +1,5 @@ // Eryn Wells -mod move_record; mod position; #[macro_use] diff --git a/position/src/position.rs b/position/src/position.rs index 700190c..c4326a5 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -9,11 +9,10 @@ use chessfriend_moves::{ AllPiecesMoveGenerator, BishopMoveGenerator, KingMoveGenerator, KnightMoveGenerator, PawnMoveGenerator, QueenMoveGenerator, RookMoveGenerator, }, - GeneratedMove, + GeneratedMove, Move, MoveRecord, }; pub use make_move::{MakeMoveError, ValidateMove}; -use crate::move_record::MoveRecord; use captures::CapturesList; use chessfriend_bitboard::BitBoard; use chessfriend_board::{ diff --git a/position/src/position/make_move.rs b/position/src/position/make_move.rs index 52bff90..cfb380a 100644 --- a/position/src/position/make_move.rs +++ b/position/src/position/make_move.rs @@ -1,11 +1,11 @@ // Eryn Wells -use crate::{move_record::MoveRecord, Position}; +use crate::Position; use chessfriend_board::{ castle::CastleEvaluationError, movement::Movement, Board, PlacePieceError, PlacePieceStrategy, }; use chessfriend_core::{Color, Piece, Rank, Square, Wing}; -use chessfriend_moves::Move; +use chessfriend_moves::{Move, MoveRecord}; use thiserror::Error; type MakeMoveResult = Result; From 086f9c56668caf670af6d734b781fdcd07830046 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 31 May 2025 15:07:20 -0700 Subject: [PATCH 323/423] [board] Replace PieceSet's derived Hash and PartialEq with bespoke implementation The piece set duplicates some data to make lookups more efficient. Only use the necessary, unique data for these functions. --- board/src/piece_sets.rs | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/board/src/piece_sets.rs b/board/src/piece_sets.rs index 97ada5c..98a9d40 100644 --- a/board/src/piece_sets.rs +++ b/board/src/piece_sets.rs @@ -5,7 +5,10 @@ mod mailbox; use self::mailbox::Mailbox; use chessfriend_bitboard::{BitBoard, IterationDirection}; use chessfriend_core::{Color, Piece, Shape, Square}; -use std::ops::BitOr; +use std::{ + hash::{Hash, Hasher}, + ops::BitOr, +}; use thiserror::Error; #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] @@ -21,9 +24,9 @@ pub enum PlacePieceError { ExisitingPiece { piece: Piece, square: Square }, } -/// The internal data structure of a [Board] that efficiently manages the +/// The internal data structure of a [`Board`] that efficiently manages the /// placement of pieces on the board. -#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] +#[derive(Clone, Debug, Default, Eq)] pub struct PieceSet { mailbox: Mailbox, color_occupancy: [BitBoard; Color::NUM], @@ -140,6 +143,20 @@ impl PieceSet { } } +impl Hash for PieceSet { + fn hash(&self, state: &mut H) { + self.color_occupancy.hash(state); + self.shape_occupancy.hash(state); + } +} + +impl PartialEq for PieceSet { + fn eq(&self, other: &Self) -> bool { + self.color_occupancy == other.color_occupancy + && self.shape_occupancy == other.shape_occupancy + } +} + #[cfg(test)] mod tests { use super::*; From c3a43fd2ed544e9741043cd11f5a3d3224a62fc1 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 31 May 2025 15:14:24 -0700 Subject: [PATCH 324/423] [board, moves] Update some comments and docs --- board/src/board.rs | 5 +++-- board/src/piece_sets.rs | 1 + moves/src/moves.rs | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/board/src/board.rs b/board/src/board.rs index 4746e95..9b02af6 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -71,8 +71,9 @@ impl Board { /// /// ## Errors /// - /// When is called with [`PlacePieceStrategy::PreserveExisting`], and a piece already exists on - /// `square`, this method returns a [`PlacePieceError::ExistingPiece`] error. + /// When is called with [`PlacePieceStrategy::PreserveExisting`], and a + /// piece already exists on `square`, this method returns a + /// [`PlacePieceError::ExistingPiece`] error. /// pub fn place_piece( &mut self, diff --git a/board/src/piece_sets.rs b/board/src/piece_sets.rs index 98a9d40..8db0c93 100644 --- a/board/src/piece_sets.rs +++ b/board/src/piece_sets.rs @@ -90,6 +90,7 @@ impl PieceSet { self.mailbox.get(square) } + // TODO: Rename this. Maybe get_all() is better? pub(crate) fn find_pieces(&self, piece: Piece) -> BitBoard { let color_occupancy = self.color_occupancy[piece.color as usize]; let shape_occupancy = self.shape_occupancy[piece.shape as usize]; diff --git a/moves/src/moves.rs b/moves/src/moves.rs index 59ac237..94ef0ad 100644 --- a/moves/src/moves.rs +++ b/moves/src/moves.rs @@ -52,7 +52,7 @@ macro_rules! ply { }; } -/// A single player's move. In game theory parlance, this is a "ply". +/// A single player's move. In game theory parlance, this is a "ply." /// /// ## TODO /// From ecde3386022c5a93b704695720e053b2c57b1000 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 31 May 2025 15:14:42 -0700 Subject: [PATCH 325/423] [position] Remove some unused imports --- position/src/position.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/position/src/position.rs b/position/src/position.rs index c4326a5..1e0a829 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -11,7 +11,7 @@ use chessfriend_moves::{ }, GeneratedMove, Move, MoveRecord, }; -pub use make_move::{MakeMoveError, ValidateMove}; +pub use make_move::ValidateMove; use captures::CapturesList; use chessfriend_bitboard::BitBoard; @@ -19,7 +19,7 @@ use chessfriend_board::{ display::DiagramFormatter, fen::ToFenStr, Board, PlacePieceError, PlacePieceStrategy, }; use chessfriend_core::{Color, Piece, Shape, Square}; -use std::{cell::OnceCell, fmt}; +use std::fmt; #[must_use] #[derive(Clone, Debug, Default, Eq)] From 40e8e055f9211ef9e8e97d2007ef0fa26d26e762 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 31 May 2025 19:01:20 -0700 Subject: [PATCH 326/423] [board, moves, position] Move make_move routines to moves crate Declare a MakeMove trait and export it from chessfriend_moves. Declare a BoardProvider trait that both Board and Position implement. Implement the MakeMove trait for all types that implement BoardProvider, and move all the move making code to the moves crate. This change makes it possible to make moves directly on a Board, rather than requiring a Position. The indirection of declaring and implementing the trait in the moves crate is required because chessfriend_board is a dependency of chessfriend_moves. So, it would be a layering violation for Board to implement make_move() directly. The board crate cannot link the moves crate because that would introduce a circular dependency. --- board/src/board_provider.rs | 18 + board/src/lib.rs | 2 + moves/src/lib.rs | 2 + .../src/position => moves/src}/make_move.rs | 321 +++++++++--------- position/src/lib.rs | 2 +- position/src/position.rs | 17 +- position/src/testing.rs | 3 +- 7 files changed, 202 insertions(+), 163 deletions(-) create mode 100644 board/src/board_provider.rs rename {position/src/position => moves/src}/make_move.rs (55%) diff --git a/board/src/board_provider.rs b/board/src/board_provider.rs new file mode 100644 index 0000000..a653bfc --- /dev/null +++ b/board/src/board_provider.rs @@ -0,0 +1,18 @@ +// Eryn Wells + +use crate::Board; + +pub trait BoardProvider { + fn board(&self) -> &Board; + fn board_mut(&mut self) -> &mut Board; +} + +impl BoardProvider for Board { + fn board(&self) -> &Board { + self + } + + fn board_mut(&mut self) -> &mut Board { + self + } +} diff --git a/board/src/lib.rs b/board/src/lib.rs index 1df6832..451bc23 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -9,10 +9,12 @@ pub mod macros; pub mod movement; pub mod sight; +mod board_provider; mod check; mod piece_sets; pub use board::Board; +pub use board_provider::BoardProvider; pub use castle::Parameters as CastleParameters; pub use castle::Rights as CastleRights; pub use piece_sets::{PlacePieceError, PlacePieceStrategy}; diff --git a/moves/src/lib.rs b/moves/src/lib.rs index fd74a25..89cd4b6 100644 --- a/moves/src/lib.rs +++ b/moves/src/lib.rs @@ -5,11 +5,13 @@ pub mod testing; mod builder; mod defs; +mod make_move; mod moves; mod record; pub use builder::{Builder, Error as BuildMoveError, Result as BuildMoveResult}; pub use defs::{Kind, PromotionShape}; pub use generators::GeneratedMove; +pub use make_move::{MakeMove, MakeMoveError, ValidateMove}; pub use moves::Move; pub use record::MoveRecord; diff --git a/position/src/position/make_move.rs b/moves/src/make_move.rs similarity index 55% rename from position/src/position/make_move.rs rename to moves/src/make_move.rs index cfb380a..06278ac 100644 --- a/position/src/position/make_move.rs +++ b/moves/src/make_move.rs @@ -1,11 +1,11 @@ // Eryn Wells -use crate::Position; +use crate::{Move, MoveRecord}; use chessfriend_board::{ - castle::CastleEvaluationError, movement::Movement, Board, PlacePieceError, PlacePieceStrategy, + castle::CastleEvaluationError, movement::Movement, Board, BoardProvider, PlacePieceError, + PlacePieceStrategy, }; use chessfriend_core::{Color, Piece, Rank, Square, Wing}; -use chessfriend_moves::{Move, MoveRecord}; use thiserror::Error; type MakeMoveResult = Result; @@ -60,23 +60,35 @@ pub enum MakeMoveError { PromotionRequired(Square), } -impl Position { +pub trait MakeMove { + fn make_move(&mut self, ply: Move, validate: ValidateMove) -> MakeMoveResult; +} + +trait MakeMoveInternal { + fn make_quiet_move(&mut self, ply: Move) -> MakeMoveResult; + fn make_double_push_move(&mut self, ply: Move) -> MakeMoveResult; + fn make_capture_move(&mut self, ply: Move) -> MakeMoveResult; + fn make_castle_move(&mut self, ply: Move, wing: Wing) -> MakeMoveResult; + fn make_promotion_move(&mut self, ply: Move) -> MakeMoveResult; + + fn validate_move(&self, ply: Move, validate: ValidateMove) -> Result<(), MakeMoveError>; + fn validate_active_piece(&self, ply: Move) -> Result; + + fn advance_clocks(&mut self, half_move_clock: HalfMoveClock); +} + +impl MakeMove for T { /// Make a move in the position. /// /// ## Errors /// /// If `validate` is [`ValidateMove::Yes`], perform validation of move correctness prior to /// applying the move. See [`Position::validate_move`]. - pub fn make_move(&mut self, ply: Move, validate: ValidateMove) -> Result<(), MakeMoveError> { - self.make_move_internal(ply, validate)?; - Ok(()) - } - - pub(crate) fn make_move_internal( + fn make_move( &mut self, ply: Move, validate: ValidateMove, - ) -> MakeMoveResult { + ) -> Result { if ply.is_quiet() { self.validate_move(ply, validate)?; return self.make_quiet_move(ply); @@ -105,20 +117,28 @@ impl Position { } } -impl Position { +impl MakeMoveInternal for T { fn make_quiet_move(&mut self, ply: Move) -> MakeMoveResult { + let board = self.board_mut(); + let origin = ply.origin_square(); - let piece = self + let piece = board .get_piece(origin) .ok_or(MakeMoveError::NoPiece(origin))?; let target = ply.target_square(); - self.place_piece_for_move(piece, target)?; + if piece.is_pawn() && target.rank().is_promotable_rank() { + return Err(MakeMoveError::PromotionRequired(target)); + } - self.remove_piece(origin); + board + .place_piece(piece, target, PlacePieceStrategy::PreserveExisting) + .map_err(MakeMoveError::PlacePieceError)?; - let record = self.register_move_record(ply, None); + board.remove_piece(origin); + + let record = MoveRecord::new(board, ply, None); self.advance_clocks(HalfMoveClock::Advance); @@ -126,22 +146,25 @@ impl Position { } fn make_double_push_move(&mut self, ply: Move) -> MakeMoveResult { + let board = self.board_mut(); + let origin = ply.origin_square(); - let piece = self - .board + let piece = board .remove_piece(origin) .ok_or(MakeMoveError::NoPiece(origin))?; let target = ply.target_square(); - self.place_piece_for_move(piece, target)?; + board + .place_piece(piece, target, PlacePieceStrategy::PreserveExisting) + .map_err(MakeMoveError::PlacePieceError)?; - self.board.en_passant_target = match target.rank() { + board.en_passant_target = match target.rank() { Rank::FOUR => Some(Square::from_file_rank(target.file(), Rank::THREE)), Rank::FIVE => Some(Square::from_file_rank(target.file(), Rank::SIX)), _ => unreachable!(), }; - let record = self.register_move_record(ply, None); + let record = MoveRecord::new(board, ply, None); self.advance_clocks(HalfMoveClock::Advance); @@ -152,11 +175,14 @@ impl Position { let origin_square = ply.origin_square(); let target_square = ply.target_square(); - let piece = self.get_piece_for_move(origin_square)?; + let board = self.board_mut(); + + let piece = board + .get_piece(origin_square) + .ok_or(MakeMoveError::NoPiece(origin_square))?; if ply.is_en_passant() { - let en_passant_square = self - .board + let en_passant_square = board .en_passant_target .ok_or(MakeMoveError::NoCaptureSquare)?; if target_square != en_passant_square { @@ -164,24 +190,23 @@ impl Position { } } + let board = self.board_mut(); + let capture_square = ply.capture_square().ok_or(MakeMoveError::NoCaptureSquare)?; - let captured_piece = self + let captured_piece = board .remove_piece(capture_square) .ok_or(MakeMoveError::NoCapturePiece(capture_square))?; - // Register the capture - self.captures.push(piece.color, captured_piece); - - self.remove_piece(origin_square).unwrap(); + board.remove_piece(origin_square).unwrap(); if let Some(promotion_shape) = ply.promotion_shape() { let promoted_piece = Piece::new(piece.color, promotion_shape); - self.place_piece(promoted_piece, target_square, PlacePieceStrategy::Replace)?; + board.place_piece(promoted_piece, target_square, PlacePieceStrategy::Replace)?; } else { - self.place_piece(piece, target_square, PlacePieceStrategy::Replace)?; + board.place_piece(piece, target_square, PlacePieceStrategy::Replace)?; } - let record = self.register_move_record(ply, Some(captured_piece)); + let record = MoveRecord::new(board, ply, Some(captured_piece)); self.advance_clocks(HalfMoveClock::Reset); @@ -189,20 +214,22 @@ impl Position { } fn make_castle_move(&mut self, ply: Move, wing: Wing) -> MakeMoveResult { - self.board.color_can_castle(wing, None)?; + let board = self.board_mut(); - let active_color = self.board.active_color; + board.color_can_castle(wing, None)?; + + let active_color = board.active_color; let parameters = Board::castling_parameters(wing, active_color); - let king = self.board.remove_piece(parameters.origin.king).unwrap(); - self.place_piece(king, parameters.target.king, PlacePieceStrategy::default())?; + let king = board.remove_piece(parameters.origin.king).unwrap(); + board.place_piece(king, parameters.target.king, PlacePieceStrategy::default())?; - let rook = self.board.remove_piece(parameters.origin.rook).unwrap(); - self.place_piece(rook, parameters.target.rook, PlacePieceStrategy::default())?; + let rook = board.remove_piece(parameters.origin.rook).unwrap(); + board.place_piece(rook, parameters.target.rook, PlacePieceStrategy::default())?; - self.board.castling_rights.revoke(active_color, wing); + board.castling_rights.revoke(active_color, wing); - let record = self.register_move_record(ply, None); + let record = MoveRecord::new(board, ply, None); self.advance_clocks(HalfMoveClock::Advance); @@ -210,9 +237,13 @@ impl Position { } fn make_promotion_move(&mut self, ply: Move) -> MakeMoveResult { + let board = self.board_mut(); + let origin = ply.origin_square(); - let piece = self.get_piece_for_move(origin)?; + let piece = board + .get_piece(origin) + .ok_or(MakeMoveError::NoPiece(origin))?; if !piece.is_pawn() { return Err(MakeMoveError::InvalidPiece(piece)); } @@ -223,68 +254,37 @@ impl Position { } if let Some(promotion_shape) = ply.promotion_shape() { - self.remove_piece(origin); + board.remove_piece(origin); let promoted_piece = Piece::new(piece.color, promotion_shape); - self.place_piece(promoted_piece, target, PlacePieceStrategy::PreserveExisting)?; + board.place_piece(promoted_piece, target, PlacePieceStrategy::PreserveExisting)?; } else { unreachable!( "Cannot make a promotion move with a ply that has no promotion shape: {ply:?}", ); } - let record = self.register_move_record(ply, None); + let record = MoveRecord::new(board, ply, None); self.advance_clocks(HalfMoveClock::Reset); Ok(record) } - fn register_move_record(&mut self, ply: Move, capture: Option) -> MoveRecord { - let record = MoveRecord::new(&self.board, ply, capture); - self.moves.push(record.clone()); - - record - } -} - -impl Position { - fn get_piece_for_move(&mut self, square: Square) -> Result { - self.get_piece(square).ok_or(MakeMoveError::NoPiece(square)) - } - - fn place_piece_for_move(&mut self, piece: Piece, square: Square) -> Result<(), MakeMoveError> { - if piece.is_pawn() && square.rank().is_promotable_rank() { - return Err(MakeMoveError::PromotionRequired(square)); - } - - self.place_piece(piece, square, PlacePieceStrategy::PreserveExisting) - .map_err(MakeMoveError::PlacePieceError) - } -} - -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -enum HalfMoveClock { - Reset, - #[default] - Advance, -} - -impl Position { fn advance_clocks(&mut self, half_move_clock: HalfMoveClock) { + let board = self.board_mut(); + match half_move_clock { - HalfMoveClock::Reset => self.board.half_move_clock = 0, - HalfMoveClock::Advance => self.board.half_move_clock += 1, + HalfMoveClock::Reset => board.half_move_clock = 0, + HalfMoveClock::Advance => board.half_move_clock += 1, } - self.board.active_color = self.board.active_color.next(); + board.active_color = board.active_color.next(); - if self.board.active_color == Color::White { - self.board.full_move_number += 1; + if board.active_color == Color::White { + board.full_move_number += 1; } } -} -impl Position { fn validate_move(&self, ply: Move, validate: ValidateMove) -> Result<(), MakeMoveError> { if validate == ValidateMove::No { return Ok(()); @@ -292,13 +292,14 @@ impl Position { let active_piece = self.validate_active_piece(ply)?; + let board = self.board(); let origin_square = ply.origin_square(); let target_square = ply.target_square(); // Pawns can see squares they can't move to. So, calculating valid // squares requires a concept that includes Sight, but adds pawn pushes. // In ChessFriend, that concept is Movement. - let movement = active_piece.movement(origin_square, &self.board); + let movement = active_piece.movement(origin_square, board); if !movement.contains(target_square) { return Err(MakeMoveError::NoMove { piece: active_piece, @@ -310,7 +311,7 @@ impl Position { // TODO: En Passant capture. if let Some(capture_square) = ply.capture_square() { - if let Some(captured_piece) = self.board.get_piece(capture_square) { + if let Some(captured_piece) = board.get_piece(capture_square) { if captured_piece.color == active_piece.color { return Err(MakeMoveError::InvalidCapture(capture_square)); } @@ -325,12 +326,13 @@ impl Position { fn validate_active_piece(&self, ply: Move) -> Result { let origin_square = ply.origin_square(); - let active_piece = self - .board + let board = self.board(); + + let active_piece = board .get_piece(origin_square) .ok_or(MakeMoveError::NoPiece(origin_square))?; - if active_piece.color != self.board.active_color { + if active_piece.color != board.active_color { return Err(MakeMoveError::NonActiveColor { piece: active_piece, square: origin_square, @@ -341,159 +343,167 @@ impl Position { } } +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +enum HalfMoveClock { + Reset, + #[default] + Advance, +} + #[cfg(test)] mod tests { use super::*; - use crate::{test_position, ValidateMove}; + use crate::{Move, PromotionShape}; + use chessfriend_board::test_board; use chessfriend_core::{piece, Color, Square}; - use chessfriend_moves::{Move, PromotionShape}; type TestResult = Result<(), MakeMoveError>; #[test] fn make_quiet_move() -> TestResult { - let mut pos = test_position!(White Pawn on C2); + let mut board = test_board!(White Pawn on C2); let ply = Move::quiet(Square::C2, Square::C3); - pos.make_move(ply, ValidateMove::Yes)?; + board.make_move(ply, ValidateMove::Yes)?; - assert_eq!(pos.get_piece(Square::C2), None); - assert_eq!(pos.get_piece(Square::C3), Some(piece!(White Pawn))); - assert_eq!(pos.board.active_color, Color::Black); - assert_eq!(pos.board.half_move_clock, 1); + assert_eq!(board.get_piece(Square::C2), None); + assert_eq!(board.get_piece(Square::C3), Some(piece!(White Pawn))); + assert_eq!(board.active_color, Color::Black); + assert_eq!(board.half_move_clock, 1); - pos.board.active_color = Color::White; + board.active_color = Color::White; let ply = Move::quiet(Square::C3, Square::C4); - pos.make_move(ply, ValidateMove::Yes)?; + board.make_move(ply, ValidateMove::Yes)?; - assert_eq!(pos.get_piece(Square::C3), None); - assert_eq!(pos.get_piece(Square::C4), Some(piece!(White Pawn))); - assert_eq!(pos.board.active_color, Color::Black); - assert_eq!(pos.board.half_move_clock, 2); + assert_eq!(board.get_piece(Square::C3), None); + assert_eq!(board.get_piece(Square::C4), Some(piece!(White Pawn))); + assert_eq!(board.active_color, Color::Black); + assert_eq!(board.half_move_clock, 2); Ok(()) } #[test] fn make_invalid_quiet_pawn_move() { - let mut pos = test_position!(White Pawn on C2); + let mut board = test_board!(White Pawn on C2); let ply = Move::quiet(Square::C2, Square::D2); - let result = pos.make_move(ply, ValidateMove::Yes); + let result = board.make_move(ply, ValidateMove::Yes); assert!(result.is_err()); - assert_eq!(pos.get_piece(Square::C2), Some(piece!(White Pawn))); - assert_eq!(pos.get_piece(Square::D2), None); - assert_eq!(pos.board.active_color, Color::White); - assert_eq!(pos.board.half_move_clock, 0); + assert_eq!(board.get_piece(Square::C2), Some(piece!(White Pawn))); + assert_eq!(board.get_piece(Square::D2), None); + assert_eq!(board.active_color, Color::White); + assert_eq!(board.half_move_clock, 0); } #[test] fn make_capture_move() -> TestResult { - let mut pos = test_position![ + let mut board = test_board![ White Bishop on C2, Black Rook on F5, ]; let ply = Move::capture(Square::C2, Square::F5); - pos.make_move(ply, ValidateMove::Yes)?; + let result = board.make_move(ply, ValidateMove::Yes)?; - assert_eq!(pos.get_piece(Square::C2), None); - assert_eq!(pos.get_piece(Square::F5), Some(piece!(White Bishop))); - assert_eq!(pos.captures.last(Color::White), Some(&piece!(Black Rook))); - assert_eq!(pos.board.active_color, Color::Black); - assert_eq!(pos.board.half_move_clock, 0); + assert_eq!(result.captured_piece, Some(piece!(Black Rook))); + + assert_eq!(board.get_piece(Square::C2), None); + assert_eq!(board.get_piece(Square::F5), Some(piece!(White Bishop))); + assert_eq!(board.active_color, Color::Black); + assert_eq!(board.half_move_clock, 0); Ok(()) } #[test] fn make_en_passant_capture_move() -> TestResult { - let mut pos = test_position![ + let mut board = test_board![ Black Pawn on F4, White Pawn on E2 ]; let ply = Move::double_push(Square::E2, Square::E4); - pos.make_move(ply, ValidateMove::Yes)?; + board.make_move(ply, ValidateMove::Yes)?; - assert_eq!(pos.get_piece(Square::E2), None); - assert_eq!(pos.get_piece(Square::E4), Some(piece!(White Pawn))); + assert_eq!(board.get_piece(Square::E2), None); + assert_eq!(board.get_piece(Square::E4), Some(piece!(White Pawn))); assert_eq!( - pos.board.en_passant_target, + board.en_passant_target, Some(Square::E3), "en passant square not set" ); - assert_eq!(pos.board.active_color, Color::Black); - assert_eq!(pos.board.half_move_clock, 1); + assert_eq!(board.active_color, Color::Black); + assert_eq!(board.half_move_clock, 1); let ply = Move::en_passant_capture(Square::F4, Square::E3); - pos.make_move(ply, ValidateMove::Yes)?; + let result = board.make_move(ply, ValidateMove::Yes)?; - assert_eq!(pos.get_piece(Square::F4), None); - assert_eq!(pos.get_piece(Square::E3), Some(piece!(Black Pawn))); + assert_eq!(result.captured_piece, Some(piece!(White Pawn))); + + assert_eq!(board.get_piece(Square::F4), None); + assert_eq!(board.get_piece(Square::E3), Some(piece!(Black Pawn))); assert_eq!( - pos.get_piece(Square::E4), + board.get_piece(Square::E4), None, "capture target pawn not removed" ); - assert_eq!(pos.captures.last(Color::Black), Some(&piece!(White Pawn))); Ok(()) } #[test] fn make_last_rank_quiet_move_without_promotion() { - let mut pos = test_position!( + let mut board = test_board!( White Pawn on A7 ); let ply = Move::quiet(Square::A7, Square::A8); - let result = pos.make_move(ply, ValidateMove::Yes); + let result = board.make_move(ply, ValidateMove::Yes); assert!(result.is_err()); - assert_eq!(pos.board.active_color, Color::White); - assert_eq!(pos.get_piece(Square::A7), Some(piece!(White Pawn))); - assert_eq!(pos.get_piece(Square::A8), None); - assert_eq!(pos.board.half_move_clock, 0); + assert_eq!(board.active_color, Color::White); + assert_eq!(board.get_piece(Square::A7), Some(piece!(White Pawn))); + assert_eq!(board.get_piece(Square::A8), None); + assert_eq!(board.half_move_clock, 0); } #[test] fn make_promotion_move() -> TestResult { - let mut pos = test_position![ + let mut board = test_board![ Black Pawn on E7, White Pawn on F7, ]; let ply = Move::promotion(Square::F7, Square::F8, PromotionShape::Queen); - pos.make_move(ply, ValidateMove::Yes)?; + board.make_move(ply, ValidateMove::Yes)?; - assert_eq!(pos.get_piece(Square::F7), None); - assert_eq!(pos.get_piece(Square::F8), Some(piece!(White Queen))); - assert_eq!(pos.board.active_color, Color::Black); - assert_eq!(pos.board.half_move_clock, 0); + assert_eq!(board.get_piece(Square::F7), None); + assert_eq!(board.get_piece(Square::F8), Some(piece!(White Queen))); + assert_eq!(board.active_color, Color::Black); + assert_eq!(board.half_move_clock, 0); Ok(()) } #[test] fn make_white_kingside_castle() -> TestResult { - let mut pos = test_position![ + let mut board = test_board![ White Rook on H1, White King on E1, ]; let ply = Move::castle(Wing::KingSide); - pos.make_move(ply, ValidateMove::Yes)?; + board.make_move(ply, ValidateMove::Yes)?; - assert_eq!(pos.board.active_color, Color::Black); - assert_eq!(pos.get_piece(Square::E1), None); - assert_eq!(pos.get_piece(Square::H1), None); - assert_eq!(pos.get_piece(Square::G1), Some(piece!(White King))); - assert_eq!(pos.get_piece(Square::F1), Some(piece!(White Rook))); - assert!(!pos - .board + assert_eq!(board.active_color, Color::Black); + assert_eq!(board.get_piece(Square::E1), None); + assert_eq!(board.get_piece(Square::H1), None); + assert_eq!(board.get_piece(Square::G1), Some(piece!(White King))); + assert_eq!(board.get_piece(Square::F1), Some(piece!(White Rook))); + assert!(!board .castling_rights .color_has_right(Color::White, Wing::KingSide)); @@ -502,21 +512,20 @@ mod tests { #[test] fn make_white_queenside_castle() -> TestResult { - let mut pos = test_position![ + let mut board = test_board![ White King on E1, White Rook on A1, ]; let ply = Move::castle(Wing::QueenSide); - pos.make_move(ply, ValidateMove::Yes)?; + board.make_move(ply, ValidateMove::Yes)?; - assert_eq!(pos.board.active_color, Color::Black); - assert_eq!(pos.get_piece(Square::E1), None); - assert_eq!(pos.get_piece(Square::A1), None); - assert_eq!(pos.get_piece(Square::C1), Some(piece!(White King))); - assert_eq!(pos.get_piece(Square::D1), Some(piece!(White Rook))); - assert!(!pos - .board + assert_eq!(board.active_color, Color::Black); + assert_eq!(board.get_piece(Square::E1), None); + assert_eq!(board.get_piece(Square::A1), None); + assert_eq!(board.get_piece(Square::C1), Some(piece!(White King))); + assert_eq!(board.get_piece(Square::D1), Some(piece!(White Rook))); + assert!(!board .castling_rights .color_has_right(Color::White, Wing::QueenSide)); diff --git a/position/src/lib.rs b/position/src/lib.rs index 0307e41..931c507 100644 --- a/position/src/lib.rs +++ b/position/src/lib.rs @@ -11,4 +11,4 @@ mod testing; pub use chessfriend_board::{fen, PlacePieceError, PlacePieceStrategy}; pub use chessfriend_moves::GeneratedMove; -pub use position::{Position, ValidateMove}; +pub use position::Position; diff --git a/position/src/position.rs b/position/src/position.rs index 1e0a829..8e32030 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -1,7 +1,6 @@ // Eryn Wells mod captures; -mod make_move; mod unmake_move; use chessfriend_moves::{ @@ -9,14 +8,14 @@ use chessfriend_moves::{ AllPiecesMoveGenerator, BishopMoveGenerator, KingMoveGenerator, KnightMoveGenerator, PawnMoveGenerator, QueenMoveGenerator, RookMoveGenerator, }, - GeneratedMove, Move, MoveRecord, + GeneratedMove, MakeMove, Move, MoveRecord, ValidateMove, }; -pub use make_move::ValidateMove; use captures::CapturesList; use chessfriend_bitboard::BitBoard; use chessfriend_board::{ - display::DiagramFormatter, fen::ToFenStr, Board, PlacePieceError, PlacePieceStrategy, + display::DiagramFormatter, fen::ToFenStr, Board, BoardProvider, PlacePieceError, + PlacePieceStrategy, }; use chessfriend_core::{Color, Piece, Shape, Square}; use std::fmt; @@ -142,6 +141,16 @@ impl ToFenStr for Position { } } +impl BoardProvider for Position { + fn board(&self) -> &Board { + &self.board + } + + fn board_mut(&mut self) -> &mut Board { + &mut self.board + } +} + impl PartialEq for Position { fn eq(&self, other: &Self) -> bool { self.board == other.board diff --git a/position/src/testing.rs b/position/src/testing.rs index 1439dee..2b1be8e 100644 --- a/position/src/testing.rs +++ b/position/src/testing.rs @@ -1,7 +1,6 @@ // Eryn Wells -use crate::position::MakeMoveError; -use chessfriend_moves::BuildMoveError; +use chessfriend_moves::{BuildMoveError, MakeMoveError}; #[macro_export] macro_rules! assert_move_list { From f60cb8cf694210507eb0ec21bd8c157609a60818 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 31 May 2025 20:17:18 -0700 Subject: [PATCH 327/423] [moves, position] Implement unmaking moves on a board MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Declare an UnmakeMove trait in the moves crate, just like the MakeMove trait from an earlier commit. Implement this trait for all types that also implement BoardProvider. Bring in a whole pile of unit tests from Claude. (Holy shit, using Claude really saves time on these tests…) Several of these tests failed, and all of those failures revealed bugs in either MakeMove or UnmakeMove. Huzzah! Include fixes for those bugs here. --- moves/src/lib.rs | 2 + moves/src/make_move.rs | 27 +- moves/src/unmake_move.rs | 500 +++++++++++++++++++++++++++ position/src/position/unmake_move.rs | 3 - 4 files changed, 522 insertions(+), 10 deletions(-) create mode 100644 moves/src/unmake_move.rs delete mode 100644 position/src/position/unmake_move.rs diff --git a/moves/src/lib.rs b/moves/src/lib.rs index 89cd4b6..048d4a8 100644 --- a/moves/src/lib.rs +++ b/moves/src/lib.rs @@ -8,6 +8,7 @@ mod defs; mod make_move; mod moves; mod record; +mod unmake_move; pub use builder::{Builder, Error as BuildMoveError, Result as BuildMoveResult}; pub use defs::{Kind, PromotionShape}; @@ -15,3 +16,4 @@ pub use generators::GeneratedMove; pub use make_move::{MakeMove, MakeMoveError, ValidateMove}; pub use moves::Move; pub use record::MoveRecord; +pub use unmake_move::{UnmakeMove, UnmakeMoveError}; diff --git a/moves/src/make_move.rs b/moves/src/make_move.rs index 06278ac..77c7fc5 100644 --- a/moves/src/make_move.rs +++ b/moves/src/make_move.rs @@ -61,6 +61,13 @@ pub enum MakeMoveError { } pub trait MakeMove { + /// Make a move. + /// + /// ## Errors + /// + /// Returns one of [`MakeMoveError`] indicating why the move could not be + /// made. + /// fn make_move(&mut self, ply: Move, validate: ValidateMove) -> MakeMoveResult; } @@ -82,8 +89,8 @@ impl MakeMove for T { /// /// ## Errors /// - /// If `validate` is [`ValidateMove::Yes`], perform validation of move correctness prior to - /// applying the move. See [`Position::validate_move`]. + /// If `validate` is [`ValidateMove::Yes`], perform validation of move + /// correctness prior to applying the move. See [`Position::validate_move`]. fn make_move( &mut self, ply: Move, @@ -158,14 +165,16 @@ impl MakeMoveInternal for T { .place_piece(piece, target, PlacePieceStrategy::PreserveExisting) .map_err(MakeMoveError::PlacePieceError)?; + // Capture move record before setting the en passant square, to ensure + // board state before the change is preserved. + let record = MoveRecord::new(board, ply, None); + board.en_passant_target = match target.rank() { Rank::FOUR => Some(Square::from_file_rank(target.file(), Rank::THREE)), Rank::FIVE => Some(Square::from_file_rank(target.file(), Rank::SIX)), _ => unreachable!(), }; - let record = MoveRecord::new(board, ply, None); - self.advance_clocks(HalfMoveClock::Advance); Ok(record) @@ -227,10 +236,12 @@ impl MakeMoveInternal for T { let rook = board.remove_piece(parameters.origin.rook).unwrap(); board.place_piece(rook, parameters.target.rook, PlacePieceStrategy::default())?; - board.castling_rights.revoke(active_color, wing); - + // Capture move record before revoking castling rights to ensure + // original board state is preserved. let record = MoveRecord::new(board, ply, None); + board.castling_rights.revoke(active_color, wing); + self.advance_clocks(HalfMoveClock::Advance); Ok(record) @@ -426,7 +437,9 @@ mod tests { ]; let ply = Move::double_push(Square::E2, Square::E4); - board.make_move(ply, ValidateMove::Yes)?; + let record = board.make_move(ply, ValidateMove::Yes)?; + + assert_eq!(record.en_passant_target, None); assert_eq!(board.get_piece(Square::E2), None); assert_eq!(board.get_piece(Square::E4), Some(piece!(White Pawn))); diff --git a/moves/src/unmake_move.rs b/moves/src/unmake_move.rs new file mode 100644 index 0000000..11c95aa --- /dev/null +++ b/moves/src/unmake_move.rs @@ -0,0 +1,500 @@ +// Eryn Wells + +use crate::MoveRecord; +use chessfriend_board::{Board, BoardProvider, PlacePieceError, PlacePieceStrategy}; +use chessfriend_core::{Piece, Square}; +use thiserror::Error; + +type UnmakeMoveResult = Result<(), UnmakeMoveError>; + +#[derive(Debug, Error, Eq, PartialEq)] +pub enum UnmakeMoveError { + #[error("no move to unmake")] + NoMove, + + #[error("no piece on {0}")] + NoPiece(Square), + + #[error("no capture square")] + NoCaptureSquare, + + #[error("no captured piece to unmake capture move")] + NoCapturedPiece, + + #[error("{0}")] + PlacePieceError(#[from] PlacePieceError), +} + +pub trait UnmakeMove { + /// Unmake the given move. Unmaking a move that wasn't the most recent one + /// made will likely cause strange results. + /// + /// ## To-Do + /// + /// Some implementations I've seen in other engines take a move to unmake. I + /// don't understand why they do this because I don't think it makes sense + /// to unmake any move other than the last move made. I need to do some + /// research on this to understand if/when passing a move might be useful or + /// necessary. + /// + /// ## Errors + /// + /// Returns one of [`UnmakeMoveError`] indicating why the move cannot be + /// unmade. + /// + fn unmake_move(&mut self, record: MoveRecord) -> UnmakeMoveResult; +} + +trait UnmakeMoveInternal { + fn unmake_quiet_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult; + fn unmake_capture_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult; + fn unmake_promotion_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult; + fn unmake_castle_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult; +} + +impl UnmakeMove for T { + fn unmake_move(&mut self, record: MoveRecord) -> UnmakeMoveResult { + let ply = record.ply; + + if ply.is_quiet() || ply.is_double_push() { + self.unmake_quiet_move(&record)?; + } else if ply.is_capture() || ply.is_en_passant() { + self.unmake_capture_move(&record)?; + } else if ply.is_promotion() { + self.unmake_promotion_move(&record)?; + } else if ply.is_castle() { + self.unmake_castle_move(&record)?; + } else { + unreachable!(); + } + + let board = self.board_mut(); + board.active_color = record.color; + board.en_passant_target = record.en_passant_target; + board.castling_rights = record.castling_rights; + board.half_move_clock = record.half_move_clock; + + Ok(()) + } +} + +impl UnmakeMoveInternal for T { + fn unmake_quiet_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult { + let board = self.board_mut(); + + let ply = record.ply; + + let target = ply.target_square(); + let piece = board + .get_piece(target) + .ok_or(UnmakeMoveError::NoPiece(target))?; + + let origin = ply.origin_square(); + board.place_piece(piece, origin, PlacePieceStrategy::PreserveExisting)?; + + board.remove_piece(target); + + Ok(()) + } + + fn unmake_capture_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult { + let board = self.board_mut(); + + let ply = record.ply; + + let target = ply.target_square(); + + let mut piece = board + .get_piece(target) + .ok_or(UnmakeMoveError::NoPiece(target))?; + if ply.is_promotion() { + piece = Piece::pawn(piece.color); + } + + let origin = ply.origin_square(); + board.place_piece(piece, origin, PlacePieceStrategy::PreserveExisting)?; + + let capture_square = ply + .capture_square() + .ok_or(UnmakeMoveError::NoCaptureSquare)?; + let captured_piece = record + .captured_piece + .ok_or(UnmakeMoveError::NoCapturedPiece)?; + + board.remove_piece(target); + + board.place_piece( + captured_piece, + capture_square, + PlacePieceStrategy::PreserveExisting, + )?; + + Ok(()) + } + + fn unmake_promotion_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult { + let board = self.board_mut(); + + let ply = record.ply; + + let target = ply.target_square(); + + let piece = Piece::pawn( + board + .get_piece(target) + .ok_or(UnmakeMoveError::NoPiece(target))? + .color, + ); + + let origin = ply.origin_square(); + board.place_piece(piece, origin, PlacePieceStrategy::PreserveExisting)?; + + board.remove_piece(target); + + Ok(()) + } + + fn unmake_castle_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult { + let ply = record.ply; + + let wing = ply.castle_wing().expect("no wing for unmaking castle move"); + let color = record.color; + let parameters = Board::castling_parameters(wing, color); + + let board = self.board_mut(); + + let king = board + .get_piece(parameters.target.king) + .ok_or(UnmakeMoveError::NoPiece(parameters.target.king))?; + let rook = board + .get_piece(parameters.target.rook) + .ok_or(UnmakeMoveError::NoPiece(parameters.target.rook))?; + + board.place_piece( + king, + parameters.origin.king, + PlacePieceStrategy::PreserveExisting, + )?; + board.place_piece( + rook, + parameters.origin.rook, + PlacePieceStrategy::PreserveExisting, + )?; + + board.remove_piece(parameters.target.king); + board.remove_piece(parameters.target.rook); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{MakeMove, Move, PromotionShape, ValidateMove}; + use chessfriend_board::test_board; + use chessfriend_core::{piece, Color, Square, Wing}; + + type TestResult = Result<(), Box>; + + /// Helper function to test make/unmake idempotency + fn test_make_unmake_idempotent( + initial_board: &mut impl BoardProvider, + ply: Move, + ) -> TestResult { + // Capture initial state + let initial_state = initial_board.board().clone(); + + // Make the move + let record = initial_board.make_move(ply, ValidateMove::Yes)?; + + // Verify the move changed the board + assert_ne!( + *initial_board.board(), + initial_state, + "Move should change board state" + ); + + // Unmake the move + initial_board.unmake_move(record)?; + + // Verify we're back to the initial state + assert_eq!( + *initial_board.board(), + initial_state, + "Board should return to initial state after unmake" + ); + + Ok(()) + } + + #[test] + fn unmake_quiet_move_ai_claude() -> TestResult { + let mut board = test_board!(White Pawn on C2); + + let ply = Move::quiet(Square::C2, Square::C3); + let record = board.make_move(ply, ValidateMove::Yes)?; + + // Verify move was made + assert_eq!(board.get_piece(Square::C2), None); + assert_eq!(board.get_piece(Square::C3), Some(piece!(White Pawn))); + assert_eq!(board.active_color, Color::Black); + + // Unmake the move + board.unmake_move(record)?; + + // Verify original state restored + assert_eq!(board.get_piece(Square::C2), Some(piece!(White Pawn))); + assert_eq!(board.get_piece(Square::C3), None); + assert_eq!(board.active_color, Color::White); + + Ok(()) + } + + #[test] + fn unmake_double_push_move_ai_claude() -> TestResult { + let mut board = test_board!(White Pawn on E2); + + let ply = Move::double_push(Square::E2, Square::E4); + let record = board.make_move(ply, ValidateMove::Yes)?; + + // Verify move was made + assert_eq!(board.get_piece(Square::E2), None); + assert_eq!(board.get_piece(Square::E4), Some(piece!(White Pawn))); + assert_eq!(board.en_passant_target, Some(Square::E3)); + assert_eq!(board.active_color, Color::Black); + + // Unmake the move + board.unmake_move(record)?; + + // Verify original state restored + assert_eq!(board.get_piece(Square::E2), Some(piece!(White Pawn))); + assert_eq!(board.get_piece(Square::E4), None); + assert_eq!(board.en_passant_target, None); + assert_eq!(board.active_color, Color::White); + + Ok(()) + } + + #[test] + fn unmake_capture_move_ai_claude() -> TestResult { + let mut board = test_board![ + White Bishop on C2, + Black Rook on F5, + ]; + + let ply = Move::capture(Square::C2, Square::F5); + let record = board.make_move(ply, ValidateMove::Yes)?; + + // Verify move was made + assert_eq!(board.get_piece(Square::C2), None); + assert_eq!(board.get_piece(Square::F5), Some(piece!(White Bishop))); + assert_eq!(record.captured_piece, Some(piece!(Black Rook))); + assert_eq!(board.active_color, Color::Black); + + // Unmake the move + board.unmake_move(record)?; + + // Verify original state restored + assert_eq!(board.get_piece(Square::C2), Some(piece!(White Bishop))); + assert_eq!(board.get_piece(Square::F5), Some(piece!(Black Rook))); + assert_eq!(board.active_color, Color::White); + + Ok(()) + } + + #[test] + fn unmake_en_passant_capture_ai_claude() -> TestResult { + let mut board = test_board![ + Black Pawn on F4, + White Pawn on E2 + ]; + + // Set up en passant situation + let double_push = Move::double_push(Square::E2, Square::E4); + board.make_move(double_push, ValidateMove::Yes)?; + + // Make en passant capture + let en_passant = Move::en_passant_capture(Square::F4, Square::E3); + let record = board.make_move(en_passant, ValidateMove::Yes)?; + + // Verify en passant was made + assert_eq!(board.get_piece(Square::F4), None); + assert_eq!(board.get_piece(Square::E3), Some(piece!(Black Pawn))); + assert_eq!( + board.get_piece(Square::E4), + None, + "captured pawn was not removed" + ); + assert_eq!(record.captured_piece, Some(piece!(White Pawn))); + + // Unmake the en passant capture + board.unmake_move(record)?; + + // Verify state before en passant is restored + assert_eq!(board.get_piece(Square::F4), Some(piece!(Black Pawn))); + assert_eq!(board.get_piece(Square::E3), None); + assert_eq!( + board.get_piece(Square::E4), + Some(piece!(White Pawn)), + "captured pawn was not restored" + ); + assert_eq!(board.active_color, Color::Black); + + Ok(()) + } + + #[test] + fn unmake_promotion_move_ai_claude() -> TestResult { + let mut board = test_board![ + White Pawn on F7, + ]; + + let ply = Move::promotion(Square::F7, Square::F8, PromotionShape::Queen); + let record = board.make_move(ply, ValidateMove::Yes)?; + + // Verify promotion was made + assert_eq!(board.get_piece(Square::F7), None); + assert_eq!(board.get_piece(Square::F8), Some(piece!(White Queen))); + assert_eq!(board.active_color, Color::Black); + + // Unmake the promotion + board.unmake_move(record)?; + + // Verify original pawn is restored + assert_eq!(board.get_piece(Square::F7), Some(piece!(White Pawn))); + assert_eq!(board.get_piece(Square::F8), None); + assert_eq!(board.active_color, Color::White); + + Ok(()) + } + + #[test] + fn unmake_capture_promotion_ai_claude() -> TestResult { + let mut board = test_board![ + White Pawn on F7, + Black Rook on G8, + ]; + + let ply = Move::capture_promotion(Square::F7, Square::G8, PromotionShape::Queen); + let record = board.make_move(ply, ValidateMove::Yes)?; + + // Verify promotion capture was made + assert_eq!(board.get_piece(Square::F7), None); + assert_eq!(board.get_piece(Square::G8), Some(piece!(White Queen))); + assert_eq!(record.captured_piece, Some(piece!(Black Rook))); + + // Unmake the promotion capture + board.unmake_move(record)?; + + // Verify original state restored + assert_eq!(board.get_piece(Square::F7), Some(piece!(White Pawn))); + assert_eq!(board.get_piece(Square::G8), Some(piece!(Black Rook))); + assert_eq!(board.active_color, Color::White); + + Ok(()) + } + + #[test] + fn unmake_white_kingside_castle_ai_claude() -> TestResult { + let mut board = test_board![ + White King on E1, + White Rook on H1, + ]; + + let original_castling_rights = board.castling_rights; + + let ply = Move::castle(Wing::KingSide); + let record = board.make_move(ply, ValidateMove::Yes)?; + + // Verify castle was made + assert_eq!(board.get_piece(Square::E1), None); + assert_eq!(board.get_piece(Square::H1), None); + assert_eq!(board.get_piece(Square::G1), Some(piece!(White King))); + assert_eq!(board.get_piece(Square::F1), Some(piece!(White Rook))); + assert!(!board + .castling_rights + .color_has_right(Color::White, Wing::KingSide)); + + // Unmake the castle + board.unmake_move(record)?; + + // Verify original state restored + assert_eq!(board.get_piece(Square::E1), Some(piece!(White King))); + assert_eq!(board.get_piece(Square::H1), Some(piece!(White Rook))); + assert_eq!(board.get_piece(Square::G1), None); + assert_eq!(board.get_piece(Square::F1), None); + assert_eq!(board.castling_rights, original_castling_rights); + assert_eq!(board.active_color, Color::White); + + Ok(()) + } + + #[test] + fn unmake_white_queenside_castle_ai_claude() -> TestResult { + let mut board = test_board![ + White King on E1, + White Rook on A1, + ]; + + let original_castling_rights = board.castling_rights; + + let ply = Move::castle(Wing::QueenSide); + let record = board.make_move(ply, ValidateMove::Yes)?; + + // Verify castle was made + assert_eq!(board.get_piece(Square::E1), None); + assert_eq!(board.get_piece(Square::A1), None); + assert_eq!(board.get_piece(Square::C1), Some(piece!(White King))); + assert_eq!(board.get_piece(Square::D1), Some(piece!(White Rook))); + assert!(!board + .castling_rights + .color_has_right(Color::White, Wing::QueenSide)); + + // Unmake the castle + board.unmake_move(record)?; + + // Verify original state restored + assert_eq!(board.get_piece(Square::E1), Some(piece!(White King))); + assert_eq!(board.get_piece(Square::A1), Some(piece!(White Rook))); + assert_eq!(board.get_piece(Square::C1), None); + assert_eq!(board.get_piece(Square::D1), None); + assert_eq!(board.castling_rights, original_castling_rights); + assert_eq!(board.active_color, Color::White); + + Ok(()) + } + + #[test] + fn unmake_black_kingside_castle() -> TestResult { + let mut board = test_board![ + Black King on E8, + Black Rook on H8, + ]; + board.active_color = Color::Black; + + let original_castling_rights = board.castling_rights; + + let ply = Move::castle(Wing::KingSide); + let record = board.make_move(ply, ValidateMove::Yes)?; + + // Verify castle was made + assert_eq!(board.get_piece(Square::E8), None); + assert_eq!(board.get_piece(Square::H8), None); + assert_eq!(board.get_piece(Square::G8), Some(piece!(Black King))); + assert_eq!(board.get_piece(Square::F8), Some(piece!(Black Rook))); + + // Unmake the castle + board.unmake_move(record)?; + + // Verify original state restored + assert_eq!(board.get_piece(Square::E8), Some(piece!(Black King))); + assert_eq!(board.get_piece(Square::H8), Some(piece!(Black Rook))); + assert_eq!(board.get_piece(Square::G8), None); + assert_eq!(board.get_piece(Square::F8), None); + assert_eq!(board.castling_rights, original_castling_rights); + assert_eq!(board.active_color, Color::Black); + + Ok(()) + } +} diff --git a/position/src/position/unmake_move.rs b/position/src/position/unmake_move.rs deleted file mode 100644 index f52f9fe..0000000 --- a/position/src/position/unmake_move.rs +++ /dev/null @@ -1,3 +0,0 @@ -// Eryn Wells - -pub enum UnmakeMoveError {} From 724a98c2e2887aad1f7261eb72ffa8992f086c75 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 1 Jun 2025 17:28:47 -0700 Subject: [PATCH 328/423] [board] Implement iter() on board This iterator yields (Square, Piece) tuples for all pieces on the board. --- board/src/board.rs | 4 ++++ board/src/piece_sets.rs | 4 ++++ board/src/piece_sets/mailbox.rs | 34 +++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/board/src/board.rs b/board/src/board.rs index 9b02af6..4e3a0f6 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -90,6 +90,10 @@ impl Board { } impl Board { + pub fn iter(&self) -> impl Iterator { + self.pieces.iter() + } + /// A [`BitBoard`] of squares occupied by pieces of all colors. pub fn occupancy(&self) -> BitBoard { self.pieces.occpuancy() diff --git a/board/src/piece_sets.rs b/board/src/piece_sets.rs index 8db0c93..b6de5c8 100644 --- a/board/src/piece_sets.rs +++ b/board/src/piece_sets.rs @@ -60,6 +60,10 @@ impl PieceSet { } } + pub fn iter(&self) -> impl Iterator { + self.mailbox.iter() + } + /// A [`BitBoard`] representing all the pieces on the board. pub fn occpuancy(&self) -> BitBoard { self.color_occupancy diff --git a/board/src/piece_sets/mailbox.rs b/board/src/piece_sets/mailbox.rs index 9e562dd..5d73561 100644 --- a/board/src/piece_sets/mailbox.rs +++ b/board/src/piece_sets/mailbox.rs @@ -22,6 +22,13 @@ impl Mailbox { pub fn remove(&mut self, square: Square) { self.0[square as usize] = None; } + + pub fn iter(&self) -> impl Iterator { + Square::ALL + .into_iter() + .zip(self.0) + .filter_map(|(square, piece)| piece.map(|piece| (square, piece))) + } } impl Default for Mailbox { @@ -45,3 +52,30 @@ impl FromIterator<(Square, Piece)> for Mailbox { }) } } + +#[cfg(test)] +mod tests { + use super::*; + use chessfriend_core::piece; + use std::collections::HashSet; + + #[test] + fn iter_returns_all_pieces() { + let mut mailbox = Mailbox::new(); + mailbox.set(piece!(White Queen), Square::C3); + mailbox.set(piece!(White Rook), Square::H8); + mailbox.set(piece!(Black Bishop), Square::E4); + mailbox.set(piece!(Black Pawn), Square::F6); + + let pieces = mailbox.iter().collect::>(); + assert_eq!( + pieces, + HashSet::from([ + (Square::C3, piece!(White Queen)), + (Square::H8, piece!(White Rook)), + (Square::E4, piece!(Black Bishop)), + (Square::F6, piece!(Black Pawn)), + ]) + ); + } +} From 8f42a4c94e643343648ca4ba6d2340e75481ec58 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 1 Jun 2025 19:02:53 -0700 Subject: [PATCH 329/423] [explorer, moves, position] Implement bespoke make_move and unmake_move methods on Position Instead of inheriting the MakeMove and UnmakeMove traits by being a BoardProvider, implement bespoke versions of these two methods. This gives Position a chance to do some of its own work (tracking captures, move records, etc) when making a move. Pass the move record by reference to the unmake_move() method. Saves a copy. Export the Result types for MakeMove and UnmakeMove. --- explorer/src/main.rs | 6 +++-- moves/src/lib.rs | 4 +-- moves/src/make_move.rs | 2 +- moves/src/unmake_move.rs | 43 ++++++++++++------------------ position/src/position.rs | 56 ++++++++++++++++++++++++++++++---------- 5 files changed, 66 insertions(+), 45 deletions(-) diff --git a/explorer/src/main.rs b/explorer/src/main.rs index 7fe6fcb..4a86a00 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -1,9 +1,11 @@ // Eryn Wells +mod make_command; + use chessfriend_board::{fen::FromFenStr, Board}; use chessfriend_core::{Color, Piece, Shape, Square}; -use chessfriend_moves::{Builder as MoveBuilder, GeneratedMove}; -use chessfriend_position::{fen::ToFenStr, PlacePieceStrategy, Position, ValidateMove}; +use chessfriend_moves::{Builder as MoveBuilder, GeneratedMove, MakeMove, ValidateMove}; +use chessfriend_position::{fen::ToFenStr, PlacePieceStrategy, Position}; use clap::{Arg, Command}; use rustyline::error::ReadlineError; diff --git a/moves/src/lib.rs b/moves/src/lib.rs index 048d4a8..5488215 100644 --- a/moves/src/lib.rs +++ b/moves/src/lib.rs @@ -13,7 +13,7 @@ mod unmake_move; pub use builder::{Builder, Error as BuildMoveError, Result as BuildMoveResult}; pub use defs::{Kind, PromotionShape}; pub use generators::GeneratedMove; -pub use make_move::{MakeMove, MakeMoveError, ValidateMove}; +pub use make_move::{MakeMove, MakeMoveError, MakeMoveResult, ValidateMove}; pub use moves::Move; pub use record::MoveRecord; -pub use unmake_move::{UnmakeMove, UnmakeMoveError}; +pub use unmake_move::{UnmakeMove, UnmakeMoveError, UnmakeMoveResult}; diff --git a/moves/src/make_move.rs b/moves/src/make_move.rs index 77c7fc5..ed79fa4 100644 --- a/moves/src/make_move.rs +++ b/moves/src/make_move.rs @@ -8,7 +8,7 @@ use chessfriend_board::{ use chessfriend_core::{Color, Piece, Rank, Square, Wing}; use thiserror::Error; -type MakeMoveResult = Result; +pub type MakeMoveResult = Result; #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum ValidateMove { diff --git a/moves/src/unmake_move.rs b/moves/src/unmake_move.rs index 11c95aa..368ba6d 100644 --- a/moves/src/unmake_move.rs +++ b/moves/src/unmake_move.rs @@ -5,7 +5,7 @@ use chessfriend_board::{Board, BoardProvider, PlacePieceError, PlacePieceStrateg use chessfriend_core::{Piece, Square}; use thiserror::Error; -type UnmakeMoveResult = Result<(), UnmakeMoveError>; +pub type UnmakeMoveResult = Result<(), UnmakeMoveError>; #[derive(Debug, Error, Eq, PartialEq)] pub enum UnmakeMoveError { @@ -42,7 +42,7 @@ pub trait UnmakeMove { /// Returns one of [`UnmakeMoveError`] indicating why the move cannot be /// unmade. /// - fn unmake_move(&mut self, record: MoveRecord) -> UnmakeMoveResult; + fn unmake_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult; } trait UnmakeMoveInternal { @@ -53,17 +53,17 @@ trait UnmakeMoveInternal { } impl UnmakeMove for T { - fn unmake_move(&mut self, record: MoveRecord) -> UnmakeMoveResult { + fn unmake_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult { let ply = record.ply; if ply.is_quiet() || ply.is_double_push() { - self.unmake_quiet_move(&record)?; + self.unmake_quiet_move(record)?; } else if ply.is_capture() || ply.is_en_passant() { - self.unmake_capture_move(&record)?; + self.unmake_capture_move(record)?; } else if ply.is_promotion() { - self.unmake_promotion_move(&record)?; + self.unmake_promotion_move(record)?; } else if ply.is_castle() { - self.unmake_castle_move(&record)?; + self.unmake_castle_move(record)?; } else { unreachable!(); } @@ -216,7 +216,7 @@ mod tests { ); // Unmake the move - initial_board.unmake_move(record)?; + initial_board.unmake_move(&record)?; // Verify we're back to the initial state assert_eq!( @@ -240,8 +240,7 @@ mod tests { assert_eq!(board.get_piece(Square::C3), Some(piece!(White Pawn))); assert_eq!(board.active_color, Color::Black); - // Unmake the move - board.unmake_move(record)?; + board.unmake_move(&record)?; // Verify original state restored assert_eq!(board.get_piece(Square::C2), Some(piece!(White Pawn))); @@ -264,8 +263,7 @@ mod tests { assert_eq!(board.en_passant_target, Some(Square::E3)); assert_eq!(board.active_color, Color::Black); - // Unmake the move - board.unmake_move(record)?; + board.unmake_move(&record)?; // Verify original state restored assert_eq!(board.get_piece(Square::E2), Some(piece!(White Pawn))); @@ -292,8 +290,7 @@ mod tests { assert_eq!(record.captured_piece, Some(piece!(Black Rook))); assert_eq!(board.active_color, Color::Black); - // Unmake the move - board.unmake_move(record)?; + board.unmake_move(&record)?; // Verify original state restored assert_eq!(board.get_piece(Square::C2), Some(piece!(White Bishop))); @@ -328,8 +325,7 @@ mod tests { ); assert_eq!(record.captured_piece, Some(piece!(White Pawn))); - // Unmake the en passant capture - board.unmake_move(record)?; + board.unmake_move(&record)?; // Verify state before en passant is restored assert_eq!(board.get_piece(Square::F4), Some(piece!(Black Pawn))); @@ -358,8 +354,7 @@ mod tests { assert_eq!(board.get_piece(Square::F8), Some(piece!(White Queen))); assert_eq!(board.active_color, Color::Black); - // Unmake the promotion - board.unmake_move(record)?; + board.unmake_move(&record)?; // Verify original pawn is restored assert_eq!(board.get_piece(Square::F7), Some(piece!(White Pawn))); @@ -384,8 +379,7 @@ mod tests { assert_eq!(board.get_piece(Square::G8), Some(piece!(White Queen))); assert_eq!(record.captured_piece, Some(piece!(Black Rook))); - // Unmake the promotion capture - board.unmake_move(record)?; + board.unmake_move(&record)?; // Verify original state restored assert_eq!(board.get_piece(Square::F7), Some(piece!(White Pawn))); @@ -416,8 +410,7 @@ mod tests { .castling_rights .color_has_right(Color::White, Wing::KingSide)); - // Unmake the castle - board.unmake_move(record)?; + board.unmake_move(&record)?; // Verify original state restored assert_eq!(board.get_piece(Square::E1), Some(piece!(White King))); @@ -451,8 +444,7 @@ mod tests { .castling_rights .color_has_right(Color::White, Wing::QueenSide)); - // Unmake the castle - board.unmake_move(record)?; + board.unmake_move(&record)?; // Verify original state restored assert_eq!(board.get_piece(Square::E1), Some(piece!(White King))); @@ -484,8 +476,7 @@ mod tests { assert_eq!(board.get_piece(Square::G8), Some(piece!(Black King))); assert_eq!(board.get_piece(Square::F8), Some(piece!(Black Rook))); - // Unmake the castle - board.unmake_move(record)?; + board.unmake_move(&record)?; // Verify original state restored assert_eq!(board.get_piece(Square::E8), Some(piece!(Black King))); diff --git a/position/src/position.rs b/position/src/position.rs index 8e32030..4839095 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -1,21 +1,20 @@ // Eryn Wells mod captures; -mod unmake_move; use chessfriend_moves::{ generators::{ AllPiecesMoveGenerator, BishopMoveGenerator, KingMoveGenerator, KnightMoveGenerator, PawnMoveGenerator, QueenMoveGenerator, RookMoveGenerator, }, - GeneratedMove, MakeMove, Move, MoveRecord, ValidateMove, + GeneratedMove, MakeMove, MakeMoveError, Move, MoveRecord, UnmakeMove, UnmakeMoveError, + UnmakeMoveResult, ValidateMove, }; use captures::CapturesList; use chessfriend_bitboard::BitBoard; use chessfriend_board::{ - display::DiagramFormatter, fen::ToFenStr, Board, BoardProvider, PlacePieceError, - PlacePieceStrategy, + display::DiagramFormatter, fen::ToFenStr, Board, PlacePieceError, PlacePieceStrategy, }; use chessfriend_core::{Color, Piece, Shape, Square}; use std::fmt; @@ -127,6 +126,45 @@ impl Position { } } +impl Position { + /// Make a move on the board and record it in the move list. + /// + /// ## Errors + /// + /// Returns one of [`MakeMoveError`] if the move cannot be made. + /// + pub fn make_move(&mut self, ply: Move, validate: ValidateMove) -> Result<(), MakeMoveError> { + let record = self.board.make_move(ply, validate)?; + + if let Some(captured_piece) = record.captured_piece { + self.captures.push(record.color, captured_piece); + } + + self.moves.push(record.clone()); + + Ok(()) + } + + /// Unmake the last move made on the board and remove its record from the + /// move list. + /// + /// ## Errors + /// + /// Returns one of [`UnmakeMoveError`] if the move cannot be made. + /// + pub fn unmake_last_move(&mut self) -> UnmakeMoveResult { + let last_move_record = self.moves.pop().ok_or(UnmakeMoveError::NoMove)?; + + let unmake_result = self.board.unmake_move(&last_move_record); + + if unmake_result.is_err() { + self.moves.push(last_move_record); + } + + unmake_result + } +} + impl Position { pub fn display(&self) -> DiagramFormatter { self.board.display() @@ -141,16 +179,6 @@ impl ToFenStr for Position { } } -impl BoardProvider for Position { - fn board(&self) -> &Board { - &self.board - } - - fn board_mut(&mut self) -> &mut Board { - &mut self.board - } -} - impl PartialEq for Position { fn eq(&self, other: &Self) -> bool { self.board == other.board From f8a3d5831e8edc22e3f41297cc84446cccb08d67 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 1 Jun 2025 19:03:28 -0700 Subject: [PATCH 330/423] [moves] Implement From for Move For easy conversion of a GeneratedMove to a Move. --- moves/src/generators.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/moves/src/generators.rs b/moves/src/generators.rs index 9c8f391..de7f4fd 100644 --- a/moves/src/generators.rs +++ b/moves/src/generators.rs @@ -36,6 +36,12 @@ impl std::fmt::Display for GeneratedMove { } } +impl From for Move { + fn from(value: GeneratedMove) -> Self { + value.ply + } +} + impl From for GeneratedMove { fn from(value: Move) -> Self { GeneratedMove { ply: value } From f47654cc98af10222f9e04b93a27c478e92a2447 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 1 Jun 2025 19:07:58 -0700 Subject: [PATCH 331/423] [board] Trying out a new naming convention in the check methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I've been bothered by certain instances where the color has already been unwrapped from an Option but a subsequent call takes an Option, so you have to rewrap it to call the method. I might be overthinking this… For the Board::*_color_is_in_check set of methods, try out this naming convention: active_color_is_in_check() : Operates directly on the Board's active color, no unwrapping required color_is_in_check() : Takes an Option and unwraps it with the active color if None is given unwrapped_color_is_in_check() : Takes a Color and operates on it. This method is called by the two above, but is also public. --- board/src/check.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/board/src/check.rs b/board/src/check.rs index 92fa168..8e6246d 100644 --- a/board/src/check.rs +++ b/board/src/check.rs @@ -7,11 +7,16 @@ use chessfriend_core::{Color, Piece}; impl Board { #[must_use] pub fn active_color_is_in_check(&self) -> bool { - self.color_is_in_check(self.active_color) + self.unwrapped_color_is_in_check(self.active_color) } #[must_use] - pub fn color_is_in_check(&self, color: Color) -> bool { + pub fn color_is_in_check(&self, color: Option) -> bool { + self.unwrapped_color_is_in_check(self.unwrap_color(color)) + } + + #[must_use] + pub fn unwrapped_color_is_in_check(&self, color: Color) -> bool { let king = self.king_bitboard(color); let opposing_sight = self.opposing_sight(color); (king & opposing_sight).is_populated() @@ -34,7 +39,7 @@ mod tests { Black Rook on F3, ); - assert!(board.color_is_in_check(Color::White)); + assert!(board.unwrapped_color_is_in_check(Color::White)); } #[test] @@ -44,6 +49,6 @@ mod tests { Black Rook on B4, ); - assert!(!board.color_is_in_check(Color::White)); + assert!(!board.unwrapped_color_is_in_check(Color::White)); } } From 6d0df32f749059e4e9f88028225831f5e0ea606e Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 2 Jun 2025 15:44:38 -0700 Subject: [PATCH 332/423] [core] Random Number Generator Implement a random number generator as a thin wrapper around rand::StdRng. Provide some default seed data to get consistent random numbers. --- Cargo.lock | 99 +++++++++++++++++++++++++++++++++++++++++++++- core/Cargo.toml | 1 + core/src/lib.rs | 1 + core/src/random.rs | 44 +++++++++++++++++++++ 4 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 core/src/random.rs diff --git a/Cargo.lock b/Cargo.lock index 238efc3..bed7c0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,6 +89,7 @@ dependencies = [ name = "chessfriend_core" version = "0.1.0" dependencies = [ + "rand", "thiserror", ] @@ -222,6 +223,18 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi", +] + [[package]] name = "heck" version = "0.4.1" @@ -239,9 +252,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.152" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "linux-raw-sys" @@ -281,6 +294,15 @@ dependencies = [ "libc", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -299,6 +321,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "radix_trie" version = "0.2.1" @@ -309,6 +337,35 @@ dependencies = [ "nibble_vec", ] +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + [[package]] name = "rustix" version = "0.38.30" @@ -417,6 +474,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "winapi" version = "0.3.9" @@ -504,3 +570,32 @@ name = "windows_x86_64_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/core/Cargo.toml b/core/Cargo.toml index 0905978..26d0835 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -6,4 +6,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +rand = "0.9.1" thiserror = "2" diff --git a/core/src/lib.rs b/core/src/lib.rs index 6edd924..dcb42a2 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -3,6 +3,7 @@ pub mod colors; pub mod coordinates; pub mod pieces; +pub mod random; pub mod shapes; mod macros; diff --git a/core/src/random.rs b/core/src/random.rs new file mode 100644 index 0000000..29b357c --- /dev/null +++ b/core/src/random.rs @@ -0,0 +1,44 @@ +// Eryn Wells + +use rand::{rngs::StdRng, RngCore, SeedableRng}; + +pub struct RandomNumberGenerator { + rng: StdRng, +} + +impl RandomNumberGenerator { + /// A default value for hashing chess games. + /// + /// This value is the SHA-256 hash of a PGN I downloaded from the internet + /// of the six games Kasparov played against Deep Blue in 1996. + #[rustfmt::skip] + const DEFAULT_SEED_VALUE: [u8; 32] = [ + 0x22, 0x0b, 0xae, 0xd5, 0xc5, 0xb8, 0x20, 0xb1, + 0xee, 0x05, 0x4b, 0x72, 0x73, 0x4c, 0x1a, 0xf9, + 0xc2, 0xb7, 0x5d, 0x87, 0xc8, 0xa9, 0x44, 0x87, + 0x01, 0xda, 0xdb, 0xe0, 0x8e, 0xf8, 0xe8, 0x77, + ]; + + #[must_use] + pub fn new(seed: ::Seed) -> Self { + let rng = StdRng::from_seed(seed); + + Self { rng } + } + + #[must_use] + pub fn rand(&mut self) -> &mut StdRng { + &mut self.rng + } + + pub fn next_u64(&mut self) -> u64 { + self.rng.next_u64() + } +} + +impl Default for RandomNumberGenerator { + fn default() -> Self { + let rng = StdRng::from_seed(Self::DEFAULT_SEED_VALUE); + Self { rng } + } +} From 09fbe1be22dbab282323632bf46873b9c37b55d5 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 2 Jun 2025 15:46:10 -0700 Subject: [PATCH 333/423] [board] Make Board::pieces private Do not allow direct access to the internal piece set. Update call sites to use Board API instead. --- board/src/board.rs | 2 +- board/src/check.rs | 2 +- board/src/fen.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/board/src/board.rs b/board/src/board.rs index 4e3a0f6..3e866e0 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -15,7 +15,7 @@ pub type FullMoveClock = u32; #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct Board { pub active_color: Color, - pub pieces: PieceSet, + pieces: PieceSet, pub castling_rights: castle::Rights, pub en_passant_target: Option, pub half_move_clock: HalfMoveClock, diff --git a/board/src/check.rs b/board/src/check.rs index 8e6246d..6873cf4 100644 --- a/board/src/check.rs +++ b/board/src/check.rs @@ -23,7 +23,7 @@ impl Board { } fn king_bitboard(&self, color: Color) -> BitBoard { - self.pieces.find_pieces(Piece::king(color)) + self.find_pieces(Piece::king(color)) } } diff --git a/board/src/fen.rs b/board/src/fen.rs index 657f87b..42d5656 100644 --- a/board/src/fen.rs +++ b/board/src/fen.rs @@ -209,7 +209,7 @@ impl FromFenStr for Board { let file = files.next().ok_or(FromFenStrError::MissingPlacement)?; let piece = Piece::from_fen_str(&ch.to_string())?; - let _ = board.pieces.place( + let _ = board.place_piece( piece, Square::from_file_rank(*file, *rank), PlacePieceStrategy::default(), From cae93cb090349f7c6232ce96d04c78d3c68cbe2a Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 2 Jun 2025 15:54:00 -0700 Subject: [PATCH 334/423] [board, position] Return replaced piece when placing a piece with PlacePieceStrategy::Replace When place_piece() is called with PlacePieceStrategy::Replace, return the replaced piece in the Ok() variant of the Result as an Option. Plumb this new return type through Board and Position. --- board/src/board.rs | 2 +- board/src/piece_sets.rs | 8 +++++--- position/src/position.rs | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/board/src/board.rs b/board/src/board.rs index 3e866e0..63c0ad5 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -80,7 +80,7 @@ impl Board { piece: Piece, square: Square, strategy: PlacePieceStrategy, - ) -> Result<(), PlacePieceError> { + ) -> Result, PlacePieceError> { self.pieces.place(piece, square, strategy) } diff --git a/board/src/piece_sets.rs b/board/src/piece_sets.rs index b6de5c8..bb6d712 100644 --- a/board/src/piece_sets.rs +++ b/board/src/piece_sets.rs @@ -106,9 +106,11 @@ impl PieceSet { piece: Piece, square: Square, strategy: PlacePieceStrategy, - ) -> Result<(), PlacePieceError> { + ) -> Result, PlacePieceError> { + let existing_piece = self.mailbox.get(square); + if strategy == PlacePieceStrategy::PreserveExisting { - if let Some(piece) = self.mailbox.get(square) { + if let Some(piece) = existing_piece { return Err(PlacePieceError::ExisitingPiece { piece, square }); } } @@ -120,7 +122,7 @@ impl PieceSet { self.shape_occupancy[shape as usize].set(square); self.mailbox.set(piece, square); - Ok(()) + Ok(existing_piece) } pub(crate) fn remove(&mut self, square: Square) -> Option { diff --git a/position/src/position.rs b/position/src/position.rs index 4839095..8b08bce 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -59,7 +59,7 @@ impl Position { piece: Piece, square: Square, strategy: PlacePieceStrategy, - ) -> Result<(), PlacePieceError> { + ) -> Result, PlacePieceError> { self.board.place_piece(piece, square, strategy) } From eaab34587c7c6f5295b39e1e58c1af67ddf34dbc Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 2 Jun 2025 17:29:52 -0700 Subject: [PATCH 335/423] [board, explorer, moves] Make Board::active_color private Make the struct attribute private, and export two new methods. A getter, active_color(), and a setter, set_active_color(). Update all references to the attribute to use the methods. --- board/src/board.rs | 15 ++++++++++++++- board/src/castle.rs | 4 ++-- board/src/check.rs | 2 +- board/src/fen.rs | 4 ++-- board/src/macros.rs | 2 +- board/src/sight.rs | 4 ++-- explorer/src/main.rs | 2 +- moves/src/make_move.rs | 30 ++++++++++++++++-------------- moves/src/record.rs | 2 +- moves/src/unmake_move.rs | 30 +++++++++++++++--------------- 10 files changed, 55 insertions(+), 40 deletions(-) diff --git a/board/src/board.rs b/board/src/board.rs index 63c0ad5..7433f08 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -14,7 +14,7 @@ pub type FullMoveClock = u32; #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct Board { - pub active_color: Color, + active_color: Color, pieces: PieceSet, pub castling_rights: castle::Rights, pub en_passant_target: Option, @@ -58,6 +58,19 @@ impl Board { } impl Board { + #[must_use] + pub fn active_color(&self) -> Color { + self.active_color + } + + pub fn set_active_color(&mut self, color: Color) { + if color == self.active_color { + return; + } + + self.active_color = color; + } + #[must_use] pub fn get_piece(&self, square: Square) -> Option { self.pieces.get(square) diff --git a/board/src/castle.rs b/board/src/castle.rs index ef3a901..02228d1 100644 --- a/board/src/castle.rs +++ b/board/src/castle.rs @@ -78,13 +78,13 @@ impl Board { } pub(crate) fn castling_king(&self, square: Square) -> Option { - let active_color = self.active_color; + let active_color = self.active_color(); self.get_piece(square) .filter(|piece| piece.color == active_color && piece.is_king()) } pub(crate) fn castling_rook(&self, square: Square) -> Option { - let active_color = self.active_color; + let active_color = self.active_color(); self.get_piece(square) .filter(|piece| piece.color == active_color && piece.is_rook()) } diff --git a/board/src/check.rs b/board/src/check.rs index 6873cf4..ba9f06d 100644 --- a/board/src/check.rs +++ b/board/src/check.rs @@ -7,7 +7,7 @@ use chessfriend_core::{Color, Piece}; impl Board { #[must_use] pub fn active_color_is_in_check(&self) -> bool { - self.unwrapped_color_is_in_check(self.active_color) + self.unwrapped_color_is_in_check(self.active_color()) } #[must_use] diff --git a/board/src/fen.rs b/board/src/fen.rs index 42d5656..9a70391 100644 --- a/board/src/fen.rs +++ b/board/src/fen.rs @@ -113,7 +113,7 @@ impl ToFenStr for Board { } } - write!(fen_string, " {}", self.active_color.to_fen_str()?) + write!(fen_string, " {}", self.active_color().to_fen_str()?) .map_err(ToFenStrError::FmtError)?; let castling = [ @@ -224,7 +224,7 @@ impl FromFenStr for Board { .next() .ok_or(FromFenStrError::MissingField(Field::PlayerToMove))?, )?; - board.active_color = active_color; + board.set_active_color(active_color); let castling_rights = fields .next() diff --git a/board/src/macros.rs b/board/src/macros.rs index da1984b..e6a5c3a 100644 --- a/board/src/macros.rs +++ b/board/src/macros.rs @@ -13,7 +13,7 @@ macro_rules! test_board { chessfriend_core::Square::$square, $crate::PlacePieceStrategy::default()); )* - board.active_color = chessfriend_core::Color::$to_move; + board.set_active_color(chessfriend_core::Color::$to_move); board.en_passant_target = Some(chessfriend_core::Square::$en_passant); println!("{}", board.display()); diff --git a/board/src/sight.rs b/board/src/sight.rs index 8518300..bb6f8e1 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -32,7 +32,7 @@ impl Board { } pub fn active_sight(&self) -> BitBoard { - self.friendly_sight(self.active_color) + self.friendly_sight(self.active_color()) } /// A [`BitBoard`] of all squares the given color can see. @@ -45,7 +45,7 @@ impl Board { } pub fn active_color_opposing_sight(&self) -> BitBoard { - self.opposing_sight(self.active_color) + self.opposing_sight(self.active_color()) } /// A [`BitBoard`] of all squares visible by colors that oppose the given color. diff --git a/explorer/src/main.rs b/explorer/src/main.rs index 4a86a00..6fc13cf 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -283,7 +283,7 @@ fn main() -> Result<(), String> { loop { if should_print_position { println!("{}", &state.position); - println!("{} to move.", state.position.board.active_color); + println!("{} to move.", state.position.board.active_color()); } let readline = editor.readline("\n? "); diff --git a/moves/src/make_move.rs b/moves/src/make_move.rs index ed79fa4..d92cb43 100644 --- a/moves/src/make_move.rs +++ b/moves/src/make_move.rs @@ -227,7 +227,7 @@ impl MakeMoveInternal for T { board.color_can_castle(wing, None)?; - let active_color = board.active_color; + let active_color = board.active_color(); let parameters = Board::castling_parameters(wing, active_color); let king = board.remove_piece(parameters.origin.king).unwrap(); @@ -289,9 +289,11 @@ impl MakeMoveInternal for T { HalfMoveClock::Advance => board.half_move_clock += 1, } - board.active_color = board.active_color.next(); + let previous_active_color = board.active_color(); + let active_color = previous_active_color.next(); + board.set_active_color(active_color); - if board.active_color == Color::White { + if active_color == Color::White { board.full_move_number += 1; } } @@ -343,7 +345,7 @@ impl MakeMoveInternal for T { .get_piece(origin_square) .ok_or(MakeMoveError::NoPiece(origin_square))?; - if active_piece.color != board.active_color { + if active_piece.color != board.active_color() { return Err(MakeMoveError::NonActiveColor { piece: active_piece, square: origin_square, @@ -379,17 +381,17 @@ mod tests { assert_eq!(board.get_piece(Square::C2), None); assert_eq!(board.get_piece(Square::C3), Some(piece!(White Pawn))); - assert_eq!(board.active_color, Color::Black); + assert_eq!(board.active_color(), Color::Black); assert_eq!(board.half_move_clock, 1); - board.active_color = Color::White; + board.set_active_color(Color::White); let ply = Move::quiet(Square::C3, Square::C4); board.make_move(ply, ValidateMove::Yes)?; assert_eq!(board.get_piece(Square::C3), None); assert_eq!(board.get_piece(Square::C4), Some(piece!(White Pawn))); - assert_eq!(board.active_color, Color::Black); + assert_eq!(board.active_color(), Color::Black); assert_eq!(board.half_move_clock, 2); Ok(()) @@ -405,7 +407,7 @@ mod tests { assert!(result.is_err()); assert_eq!(board.get_piece(Square::C2), Some(piece!(White Pawn))); assert_eq!(board.get_piece(Square::D2), None); - assert_eq!(board.active_color, Color::White); + assert_eq!(board.active_color(), Color::White); assert_eq!(board.half_move_clock, 0); } @@ -423,7 +425,7 @@ mod tests { assert_eq!(board.get_piece(Square::C2), None); assert_eq!(board.get_piece(Square::F5), Some(piece!(White Bishop))); - assert_eq!(board.active_color, Color::Black); + assert_eq!(board.active_color(), Color::Black); assert_eq!(board.half_move_clock, 0); Ok(()) @@ -448,7 +450,7 @@ mod tests { Some(Square::E3), "en passant square not set" ); - assert_eq!(board.active_color, Color::Black); + assert_eq!(board.active_color(), Color::Black); assert_eq!(board.half_move_clock, 1); let ply = Move::en_passant_capture(Square::F4, Square::E3); @@ -477,7 +479,7 @@ mod tests { let result = board.make_move(ply, ValidateMove::Yes); assert!(result.is_err()); - assert_eq!(board.active_color, Color::White); + assert_eq!(board.active_color(), Color::White); assert_eq!(board.get_piece(Square::A7), Some(piece!(White Pawn))); assert_eq!(board.get_piece(Square::A8), None); assert_eq!(board.half_move_clock, 0); @@ -495,7 +497,7 @@ mod tests { assert_eq!(board.get_piece(Square::F7), None); assert_eq!(board.get_piece(Square::F8), Some(piece!(White Queen))); - assert_eq!(board.active_color, Color::Black); + assert_eq!(board.active_color(), Color::Black); assert_eq!(board.half_move_clock, 0); Ok(()) @@ -511,7 +513,7 @@ mod tests { let ply = Move::castle(Wing::KingSide); board.make_move(ply, ValidateMove::Yes)?; - assert_eq!(board.active_color, Color::Black); + assert_eq!(board.active_color(), Color::Black); assert_eq!(board.get_piece(Square::E1), None); assert_eq!(board.get_piece(Square::H1), None); assert_eq!(board.get_piece(Square::G1), Some(piece!(White King))); @@ -533,7 +535,7 @@ mod tests { let ply = Move::castle(Wing::QueenSide); board.make_move(ply, ValidateMove::Yes)?; - assert_eq!(board.active_color, Color::Black); + assert_eq!(board.active_color(), Color::Black); assert_eq!(board.get_piece(Square::E1), None); assert_eq!(board.get_piece(Square::A1), None); assert_eq!(board.get_piece(Square::C1), Some(piece!(White King))); diff --git a/moves/src/record.rs b/moves/src/record.rs index 3c91867..139f2d4 100644 --- a/moves/src/record.rs +++ b/moves/src/record.rs @@ -32,7 +32,7 @@ impl MoveRecord { #[must_use] pub fn new(board: &Board, ply: Move, capture: Option) -> Self { Self { - color: board.active_color, + color: board.active_color(), ply, en_passant_target: board.en_passant_target, castling_rights: board.castling_rights, diff --git a/moves/src/unmake_move.rs b/moves/src/unmake_move.rs index 368ba6d..b3df2ca 100644 --- a/moves/src/unmake_move.rs +++ b/moves/src/unmake_move.rs @@ -69,7 +69,7 @@ impl UnmakeMove for T { } let board = self.board_mut(); - board.active_color = record.color; + board.set_active_color(record.color); board.en_passant_target = record.en_passant_target; board.castling_rights = record.castling_rights; board.half_move_clock = record.half_move_clock; @@ -238,14 +238,14 @@ mod tests { // Verify move was made assert_eq!(board.get_piece(Square::C2), None); assert_eq!(board.get_piece(Square::C3), Some(piece!(White Pawn))); - assert_eq!(board.active_color, Color::Black); + assert_eq!(board.active_color(), Color::Black); board.unmake_move(&record)?; // Verify original state restored assert_eq!(board.get_piece(Square::C2), Some(piece!(White Pawn))); assert_eq!(board.get_piece(Square::C3), None); - assert_eq!(board.active_color, Color::White); + assert_eq!(board.active_color(), Color::White); Ok(()) } @@ -261,7 +261,7 @@ mod tests { assert_eq!(board.get_piece(Square::E2), None); assert_eq!(board.get_piece(Square::E4), Some(piece!(White Pawn))); assert_eq!(board.en_passant_target, Some(Square::E3)); - assert_eq!(board.active_color, Color::Black); + assert_eq!(board.active_color(), Color::Black); board.unmake_move(&record)?; @@ -269,7 +269,7 @@ mod tests { assert_eq!(board.get_piece(Square::E2), Some(piece!(White Pawn))); assert_eq!(board.get_piece(Square::E4), None); assert_eq!(board.en_passant_target, None); - assert_eq!(board.active_color, Color::White); + assert_eq!(board.active_color(), Color::White); Ok(()) } @@ -288,14 +288,14 @@ mod tests { assert_eq!(board.get_piece(Square::C2), None); assert_eq!(board.get_piece(Square::F5), Some(piece!(White Bishop))); assert_eq!(record.captured_piece, Some(piece!(Black Rook))); - assert_eq!(board.active_color, Color::Black); + assert_eq!(board.active_color(), Color::Black); board.unmake_move(&record)?; // Verify original state restored assert_eq!(board.get_piece(Square::C2), Some(piece!(White Bishop))); assert_eq!(board.get_piece(Square::F5), Some(piece!(Black Rook))); - assert_eq!(board.active_color, Color::White); + assert_eq!(board.active_color(), Color::White); Ok(()) } @@ -335,7 +335,7 @@ mod tests { Some(piece!(White Pawn)), "captured pawn was not restored" ); - assert_eq!(board.active_color, Color::Black); + assert_eq!(board.active_color(), Color::Black); Ok(()) } @@ -352,14 +352,14 @@ mod tests { // Verify promotion was made assert_eq!(board.get_piece(Square::F7), None); assert_eq!(board.get_piece(Square::F8), Some(piece!(White Queen))); - assert_eq!(board.active_color, Color::Black); + assert_eq!(board.active_color(), Color::Black); board.unmake_move(&record)?; // Verify original pawn is restored assert_eq!(board.get_piece(Square::F7), Some(piece!(White Pawn))); assert_eq!(board.get_piece(Square::F8), None); - assert_eq!(board.active_color, Color::White); + assert_eq!(board.active_color(), Color::White); Ok(()) } @@ -384,7 +384,7 @@ mod tests { // Verify original state restored assert_eq!(board.get_piece(Square::F7), Some(piece!(White Pawn))); assert_eq!(board.get_piece(Square::G8), Some(piece!(Black Rook))); - assert_eq!(board.active_color, Color::White); + assert_eq!(board.active_color(), Color::White); Ok(()) } @@ -418,7 +418,7 @@ mod tests { assert_eq!(board.get_piece(Square::G1), None); assert_eq!(board.get_piece(Square::F1), None); assert_eq!(board.castling_rights, original_castling_rights); - assert_eq!(board.active_color, Color::White); + assert_eq!(board.active_color(), Color::White); Ok(()) } @@ -452,7 +452,7 @@ mod tests { assert_eq!(board.get_piece(Square::C1), None); assert_eq!(board.get_piece(Square::D1), None); assert_eq!(board.castling_rights, original_castling_rights); - assert_eq!(board.active_color, Color::White); + assert_eq!(board.active_color(), Color::White); Ok(()) } @@ -463,7 +463,7 @@ mod tests { Black King on E8, Black Rook on H8, ]; - board.active_color = Color::Black; + board.set_active_color(Color::Black); let original_castling_rights = board.castling_rights; @@ -484,7 +484,7 @@ mod tests { assert_eq!(board.get_piece(Square::G8), None); assert_eq!(board.get_piece(Square::F8), None); assert_eq!(board.castling_rights, original_castling_rights); - assert_eq!(board.active_color, Color::Black); + assert_eq!(board.active_color(), Color::Black); Ok(()) } From 404212363e4a5c1ffbc813ab85059423953bf943 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 3 Jun 2025 20:25:53 -0700 Subject: [PATCH 336/423] [board, moves] Make Board::castling_rights and Board::en_passant_target private Make the struct fields private and export getters and various setters for manipulating the data. Update all the references to these fields to use the getters and setters instead. --- board/src/board.rs | 60 ++++++++++++++++++++++++++++++++++-- board/src/castle.rs | 9 +++--- board/src/fen.rs | 16 +++++----- board/src/macros.rs | 2 +- board/src/movement.rs | 2 +- board/src/sight.rs | 2 +- moves/src/generators/pawn.rs | 2 +- moves/src/make_move.rs | 22 ++++++------- moves/src/record.rs | 4 +-- moves/src/unmake_move.rs | 28 ++++++++--------- 10 files changed, 96 insertions(+), 51 deletions(-) diff --git a/board/src/board.rs b/board/src/board.rs index 7433f08..9c465e0 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -7,7 +7,7 @@ use crate::{ PieceSet, }; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece, Shape, Square}; +use chessfriend_core::{Color, Piece, Shape, Square, Wing}; pub type HalfMoveClock = u32; pub type FullMoveClock = u32; @@ -16,8 +16,8 @@ pub type FullMoveClock = u32; pub struct Board { active_color: Color, pieces: PieceSet, - pub castling_rights: castle::Rights, - pub en_passant_target: Option, + castling_rights: castle::Rights, + en_passant_target: Option, pub half_move_clock: HalfMoveClock, pub full_move_number: FullMoveClock, } @@ -71,6 +71,60 @@ impl Board { self.active_color = color; } +impl Board { + #[must_use] + pub fn castling_rights(&self) -> castle::Rights { + self.castling_rights + } + + pub fn set_castling_rights(&mut self, rights: castle::Rights) { + self.castling_rights = rights; + } + + #[must_use] + pub fn active_color_has_castling_right(&self, wing: Wing) -> bool { + self.color_has_castling_right(self.active_color, wing) + } + + #[must_use] + pub fn color_has_castling_right(&self, color: Color, wing: Wing) -> bool { + self.castling_rights.color_has_right(color, wing) + } + + pub fn grant_castling_right(&mut self, color: Color, wing: Wing) { + self.castling_rights.grant(color, wing); + } + + pub fn revoke_all_castling_rights(&mut self) { + self.castling_rights.revoke_all(); + } + + pub fn revoke_castling_right(&mut self, color: Color, wing: Wing) { + self.castling_rights.revoke(color, wing); + } +} + +impl Board { + /// Returns a copy of the current en passant square, if one exists. + #[must_use] + pub fn en_passant_target(&self) -> Option { + self.en_passant_target + } + + pub fn set_en_passant_target(&mut self, square: Square) { + self.set_en_passant_target_option(Some(square)); + } + + pub fn set_en_passant_target_option(&mut self, square: Option) { + self.en_passant_target = square; + } + + pub fn clear_en_passant_target(&mut self) { + self.en_passant_target = None; + } +} + +impl Board { #[must_use] pub fn get_piece(&self, square: Square) -> Option { self.pieces.get(square) diff --git a/board/src/castle.rs b/board/src/castle.rs index 02228d1..c5c3423 100644 --- a/board/src/castle.rs +++ b/board/src/castle.rs @@ -46,7 +46,7 @@ impl Board { let color = self.unwrap_color(color); - if !self.castling_rights.color_has_right(color, wing) { + if !self.color_has_castling_right(color, wing) { return Err(CastleEvaluationError::NoRights { color, wing }); } @@ -98,15 +98,14 @@ mod tests { #[test] fn king_on_starting_square_can_castle() { - let pos = test_board!( + let board = test_board!( White King on E1, White Rook on A1, White Rook on H1 ); - let rights = pos.castling_rights; - assert!(rights.color_has_right(Color::White, Wing::KingSide)); - assert!(rights.color_has_right(Color::White, Wing::QueenSide)); + assert!(board.color_has_castling_right(Color::White, Wing::KingSide)); + assert!(board.color_has_castling_right(Color::White, Wing::QueenSide)); } #[test] diff --git a/board/src/fen.rs b/board/src/fen.rs index 9a70391..470675b 100644 --- a/board/src/fen.rs +++ b/board/src/fen.rs @@ -123,7 +123,7 @@ impl ToFenStr for Board { (Color::Black, Wing::QueenSide), ] .map(|(color, castle)| { - if !self.castling_rights.color_has_right(color, castle) { + if !self.color_has_castling_right(color, castle) { return ""; } @@ -146,7 +146,7 @@ impl ToFenStr for Board { write!( fen_string, " {}", - self.en_passant_target + self.en_passant_target() .map_or("-".to_string(), |square| square.to_string()) ) .map_err(ToFenStrError::FmtError)?; @@ -230,14 +230,14 @@ impl FromFenStr for Board { .next() .ok_or(FromFenStrError::MissingField(Field::CastlingRights))?; if castling_rights == "-" { - board.castling_rights.revoke_all(); + board.revoke_all_castling_rights(); } else { for ch in castling_rights.chars() { match ch { - 'K' => board.castling_rights.grant(Color::White, Wing::KingSide), - 'Q' => board.castling_rights.grant(Color::White, Wing::QueenSide), - 'k' => board.castling_rights.grant(Color::Black, Wing::KingSide), - 'q' => board.castling_rights.grant(Color::Black, Wing::QueenSide), + 'K' => board.grant_castling_right(Color::White, Wing::KingSide), + 'Q' => board.grant_castling_right(Color::White, Wing::QueenSide), + 'k' => board.grant_castling_right(Color::Black, Wing::KingSide), + 'q' => board.grant_castling_right(Color::Black, Wing::QueenSide), _ => return Err(FromFenStrError::InvalidValue), }; } @@ -249,7 +249,7 @@ impl FromFenStr for Board { if en_passant_square != "-" { let square = Square::from_algebraic_str(en_passant_square) .map_err(FromFenStrError::ParseSquareError)?; - board.en_passant_target = Some(square); + board.set_en_passant_target(square); } let half_move_clock = fields diff --git a/board/src/macros.rs b/board/src/macros.rs index e6a5c3a..b3baf85 100644 --- a/board/src/macros.rs +++ b/board/src/macros.rs @@ -14,7 +14,7 @@ macro_rules! test_board { $crate::PlacePieceStrategy::default()); )* board.set_active_color(chessfriend_core::Color::$to_move); - board.en_passant_target = Some(chessfriend_core::Square::$en_passant); + board.set_en_passant_target(chessfriend_core::Square::$en_passant); println!("{}", board.display()); diff --git a/board/src/movement.rs b/board/src/movement.rs index f386579..4fa4381 100644 --- a/board/src/movement.rs +++ b/board/src/movement.rs @@ -29,7 +29,7 @@ impl Movement for Piece { match self.shape { Shape::Pawn => { - let en_passant_square: BitBoard = board.en_passant_target.into(); + let en_passant_square: BitBoard = board.en_passant_target().into(); // Pawns can only move to squares they can see to capture. let sight = self.sight(square, board) & (opposing_occupancy | en_passant_square); let pushes = pawn_pushes(square.into(), self.color, board.occupancy()); diff --git a/board/src/sight.rs b/board/src/sight.rs index bb6f8e1..9a84f7e 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -79,7 +79,7 @@ impl Sight for Piece { match self.shape { Shape::Pawn => { - let en_passant_square: BitBoard = board.en_passant_target.into(); + let en_passant_square: BitBoard = board.en_passant_target().into(); match self.color { Color::White => white_pawn_sight(&info, en_passant_square), Color::Black => black_pawn_sight(&info, en_passant_square), diff --git a/moves/src/generators/pawn.rs b/moves/src/generators/pawn.rs index f4974de..5e8b76b 100644 --- a/moves/src/generators/pawn.rs +++ b/moves/src/generators/pawn.rs @@ -48,7 +48,7 @@ impl PawnMoveGenerator { let (single_pushes, double_pushes) = Self::pushes(pawns, color, empty); let (left_captures, right_captures) = Self::captures(pawns, color, enemies); - let en_passant: BitBoard = board.en_passant_target.into(); + let en_passant: BitBoard = board.en_passant_target().into(); Self { color, diff --git a/moves/src/make_move.rs b/moves/src/make_move.rs index d92cb43..dfd9b41 100644 --- a/moves/src/make_move.rs +++ b/moves/src/make_move.rs @@ -169,11 +169,11 @@ impl MakeMoveInternal for T { // board state before the change is preserved. let record = MoveRecord::new(board, ply, None); - board.en_passant_target = match target.rank() { - Rank::FOUR => Some(Square::from_file_rank(target.file(), Rank::THREE)), - Rank::FIVE => Some(Square::from_file_rank(target.file(), Rank::SIX)), + board.set_en_passant_target(match target.rank() { + Rank::FOUR => Square::from_file_rank(target.file(), Rank::THREE), + Rank::FIVE => Square::from_file_rank(target.file(), Rank::SIX), _ => unreachable!(), - }; + }); self.advance_clocks(HalfMoveClock::Advance); @@ -192,7 +192,7 @@ impl MakeMoveInternal for T { if ply.is_en_passant() { let en_passant_square = board - .en_passant_target + .en_passant_target() .ok_or(MakeMoveError::NoCaptureSquare)?; if target_square != en_passant_square { return Err(MakeMoveError::InvalidEnPassantCapture(target_square)); @@ -240,7 +240,7 @@ impl MakeMoveInternal for T { // original board state is preserved. let record = MoveRecord::new(board, ply, None); - board.castling_rights.revoke(active_color, wing); + board.revoke_castling_right(active_color, wing); self.advance_clocks(HalfMoveClock::Advance); @@ -446,7 +446,7 @@ mod tests { assert_eq!(board.get_piece(Square::E2), None); assert_eq!(board.get_piece(Square::E4), Some(piece!(White Pawn))); assert_eq!( - board.en_passant_target, + board.en_passant_target(), Some(Square::E3), "en passant square not set" ); @@ -518,9 +518,7 @@ mod tests { assert_eq!(board.get_piece(Square::H1), None); assert_eq!(board.get_piece(Square::G1), Some(piece!(White King))); assert_eq!(board.get_piece(Square::F1), Some(piece!(White Rook))); - assert!(!board - .castling_rights - .color_has_right(Color::White, Wing::KingSide)); + assert!(!board.color_has_castling_right(Color::White, Wing::KingSide)); Ok(()) } @@ -540,9 +538,7 @@ mod tests { assert_eq!(board.get_piece(Square::A1), None); assert_eq!(board.get_piece(Square::C1), Some(piece!(White King))); assert_eq!(board.get_piece(Square::D1), Some(piece!(White Rook))); - assert!(!board - .castling_rights - .color_has_right(Color::White, Wing::QueenSide)); + assert!(!board.color_has_castling_right(Color::White, Wing::QueenSide)); Ok(()) } diff --git a/moves/src/record.rs b/moves/src/record.rs index 139f2d4..47a2c3f 100644 --- a/moves/src/record.rs +++ b/moves/src/record.rs @@ -34,8 +34,8 @@ impl MoveRecord { Self { color: board.active_color(), ply, - en_passant_target: board.en_passant_target, - castling_rights: board.castling_rights, + en_passant_target: board.en_passant_target(), + castling_rights: board.castling_rights(), half_move_clock: board.half_move_clock, captured_piece: capture, } diff --git a/moves/src/unmake_move.rs b/moves/src/unmake_move.rs index b3df2ca..7fd3fec 100644 --- a/moves/src/unmake_move.rs +++ b/moves/src/unmake_move.rs @@ -70,8 +70,8 @@ impl UnmakeMove for T { let board = self.board_mut(); board.set_active_color(record.color); - board.en_passant_target = record.en_passant_target; - board.castling_rights = record.castling_rights; + board.set_en_passant_target_option(record.en_passant_target); + board.set_castling_rights(record.castling_rights); board.half_move_clock = record.half_move_clock; Ok(()) @@ -260,7 +260,7 @@ mod tests { // Verify move was made assert_eq!(board.get_piece(Square::E2), None); assert_eq!(board.get_piece(Square::E4), Some(piece!(White Pawn))); - assert_eq!(board.en_passant_target, Some(Square::E3)); + assert_eq!(board.en_passant_target(), Some(Square::E3)); assert_eq!(board.active_color(), Color::Black); board.unmake_move(&record)?; @@ -268,7 +268,7 @@ mod tests { // Verify original state restored assert_eq!(board.get_piece(Square::E2), Some(piece!(White Pawn))); assert_eq!(board.get_piece(Square::E4), None); - assert_eq!(board.en_passant_target, None); + assert_eq!(board.en_passant_target(), None); assert_eq!(board.active_color(), Color::White); Ok(()) @@ -396,7 +396,7 @@ mod tests { White Rook on H1, ]; - let original_castling_rights = board.castling_rights; + let original_castling_rights = board.castling_rights(); let ply = Move::castle(Wing::KingSide); let record = board.make_move(ply, ValidateMove::Yes)?; @@ -406,9 +406,7 @@ mod tests { assert_eq!(board.get_piece(Square::H1), None); assert_eq!(board.get_piece(Square::G1), Some(piece!(White King))); assert_eq!(board.get_piece(Square::F1), Some(piece!(White Rook))); - assert!(!board - .castling_rights - .color_has_right(Color::White, Wing::KingSide)); + assert!(!board.color_has_castling_right(Color::White, Wing::KingSide)); board.unmake_move(&record)?; @@ -417,7 +415,7 @@ mod tests { assert_eq!(board.get_piece(Square::H1), Some(piece!(White Rook))); assert_eq!(board.get_piece(Square::G1), None); assert_eq!(board.get_piece(Square::F1), None); - assert_eq!(board.castling_rights, original_castling_rights); + assert_eq!(board.castling_rights(), original_castling_rights); assert_eq!(board.active_color(), Color::White); Ok(()) @@ -430,7 +428,7 @@ mod tests { White Rook on A1, ]; - let original_castling_rights = board.castling_rights; + let original_castling_rights = board.castling_rights(); let ply = Move::castle(Wing::QueenSide); let record = board.make_move(ply, ValidateMove::Yes)?; @@ -440,9 +438,7 @@ mod tests { assert_eq!(board.get_piece(Square::A1), None); assert_eq!(board.get_piece(Square::C1), Some(piece!(White King))); assert_eq!(board.get_piece(Square::D1), Some(piece!(White Rook))); - assert!(!board - .castling_rights - .color_has_right(Color::White, Wing::QueenSide)); + assert!(!board.color_has_castling_right(Color::White, Wing::QueenSide)); board.unmake_move(&record)?; @@ -451,7 +447,7 @@ mod tests { assert_eq!(board.get_piece(Square::A1), Some(piece!(White Rook))); assert_eq!(board.get_piece(Square::C1), None); assert_eq!(board.get_piece(Square::D1), None); - assert_eq!(board.castling_rights, original_castling_rights); + assert_eq!(board.castling_rights(), original_castling_rights); assert_eq!(board.active_color(), Color::White); Ok(()) @@ -465,7 +461,7 @@ mod tests { ]; board.set_active_color(Color::Black); - let original_castling_rights = board.castling_rights; + let original_castling_rights = board.castling_rights(); let ply = Move::castle(Wing::KingSide); let record = board.make_move(ply, ValidateMove::Yes)?; @@ -483,7 +479,7 @@ mod tests { assert_eq!(board.get_piece(Square::H8), Some(piece!(Black Rook))); assert_eq!(board.get_piece(Square::G8), None); assert_eq!(board.get_piece(Square::F8), None); - assert_eq!(board.castling_rights, original_castling_rights); + assert_eq!(board.castling_rights(), original_castling_rights); assert_eq!(board.active_color(), Color::Black); Ok(()) From d7f426697d56744eddf2bc9946e1f1a64c666c0f Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 5 Jun 2025 08:21:32 -0700 Subject: [PATCH 337/423] [board, position] Implement Zobrist hashing This change builds on several previous changes to implement Zobrist hashing of the board. This hash can be updated incrementally as changes are made to the board. In order to do that, various properties of the Board struct had to made internal. In the setters and various mutating members of Board, the hash is updated as state changes. The entire hashing mechanism is optional. If no ZobristState is provided when the Board is created, the hash is never computed. Plumb the Zobrist state through Position as well so that clients of Position (the ultimate interface for interacting with the chess engine) can provide global state to the whole engine. The explorer crate gives an example of how this works. Some global state is computed during initialization and then passed to the Position when it's created. --- Cargo.lock | 1 + board/Cargo.toml | 1 + board/src/board.rs | 113 ++++++++++++++++-- board/src/castle/rights.rs | 6 + board/src/fen.rs | 4 +- board/src/lib.rs | 2 + board/src/macros.rs | 19 ++- board/src/sight.rs | 8 +- board/src/zobrist.rs | 239 +++++++++++++++++++++++++++++++++++++ explorer/src/main.rs | 19 +-- position/src/position.rs | 14 +-- 11 files changed, 395 insertions(+), 31 deletions(-) create mode 100644 board/src/zobrist.rs diff --git a/Cargo.lock b/Cargo.lock index bed7c0d..e58f1e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,6 +82,7 @@ version = "0.1.0" dependencies = [ "chessfriend_bitboard", "chessfriend_core", + "rand", "thiserror", ] diff --git a/board/Cargo.toml b/board/Cargo.toml index 098d764..90de6eb 100644 --- a/board/Cargo.toml +++ b/board/Cargo.toml @@ -8,4 +8,5 @@ edition = "2021" [dependencies] chessfriend_bitboard = { path = "../bitboard" } chessfriend_core = { path = "../core" } +rand = "0.9.1" thiserror = "2" diff --git a/board/src/board.rs b/board/src/board.rs index 9c465e0..67f7f81 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -4,10 +4,12 @@ use crate::{ castle, display::DiagramFormatter, piece_sets::{PlacePieceError, PlacePieceStrategy}, + zobrist::{ZobristHash, ZobristState}, PieceSet, }; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece, Shape, Square, Wing}; +use std::sync::Arc; pub type HalfMoveClock = u32; pub type FullMoveClock = u32; @@ -20,18 +22,26 @@ pub struct Board { en_passant_target: Option, pub half_move_clock: HalfMoveClock, pub full_move_number: FullMoveClock, + zobrist_hash: Option, } impl Board { /// An empty board #[must_use] - pub fn empty() -> Self { - Board::default() + pub fn empty(zobrist: Option>) -> Self { + let mut board = Self { + zobrist_hash: zobrist.map(ZobristHash::new), + ..Default::default() + }; + + board.recompute_zobrist_hash(); + + board } /// The starting position #[must_use] - pub fn starting() -> Self { + pub fn starting(zobrist: Option>) -> Self { const BLACK_PIECES: [BitBoard; Shape::NUM] = [ BitBoard::new(0b0000_0000_1111_1111 << 48), BitBoard::new(0b0100_0010_0000_0000 << 48), @@ -50,10 +60,15 @@ impl Board { BitBoard::new(0b0000_0000_0001_0000), ]; - Self { + let mut board = Self { pieces: PieceSet::new([WHITE_PIECES, BLACK_PIECES]), + zobrist_hash: zobrist.map(ZobristHash::new), ..Default::default() - } + }; + + board.recompute_zobrist_hash(); + + board } } @@ -69,7 +84,12 @@ impl Board { } self.active_color = color; + + if let Some(zobrist) = self.zobrist_hash.as_mut() { + zobrist.update_setting_active_color(color); + } } +} impl Board { #[must_use] @@ -78,7 +98,13 @@ impl Board { } pub fn set_castling_rights(&mut self, rights: castle::Rights) { + if rights == self.castling_rights { + return; + } + + let old_rights = self.castling_rights; self.castling_rights = rights; + self.update_zobrist_hash_castling_rights(old_rights); } #[must_use] @@ -92,15 +118,32 @@ impl Board { } pub fn grant_castling_right(&mut self, color: Color, wing: Wing) { + let old_rights = self.castling_rights; self.castling_rights.grant(color, wing); + self.update_zobrist_hash_castling_rights(old_rights); } pub fn revoke_all_castling_rights(&mut self) { + let old_rights = self.castling_rights; self.castling_rights.revoke_all(); + self.update_zobrist_hash_castling_rights(old_rights); } pub fn revoke_castling_right(&mut self, color: Color, wing: Wing) { + let old_rights = self.castling_rights; self.castling_rights.revoke(color, wing); + self.update_zobrist_hash_castling_rights(old_rights); + } + + fn update_zobrist_hash_castling_rights(&mut self, old_rights: castle::Rights) { + let new_rights = self.castling_rights; + if old_rights == new_rights { + return; + } + + if let Some(zobrist) = self.zobrist_hash.as_mut() { + zobrist.update_modifying_castling_rights(new_rights, old_rights); + } } } @@ -116,11 +159,27 @@ impl Board { } pub fn set_en_passant_target_option(&mut self, square: Option) { + let old_target = self.en_passant_target; self.en_passant_target = square; + self.update_zobrist_hash_en_passant_target(old_target); } pub fn clear_en_passant_target(&mut self) { + let old_target = self.en_passant_target; self.en_passant_target = None; + self.update_zobrist_hash_en_passant_target(old_target); + } + + fn update_zobrist_hash_en_passant_target(&mut self, old_target: Option) { + let new_target = self.en_passant_target; + + if old_target == new_target { + return; + } + + if let Some(zobrist) = self.zobrist_hash.as_mut() { + zobrist.update_setting_en_passant_target(old_target, new_target); + } } } @@ -148,11 +207,28 @@ impl Board { square: Square, strategy: PlacePieceStrategy, ) -> Result, PlacePieceError> { - self.pieces.place(piece, square, strategy) + let place_result = self.pieces.place(piece, square, strategy); + + if let Ok(Some(existing_piece)) = place_result.as_ref() { + if let Some(zobrist) = self.zobrist_hash.as_mut() { + zobrist.update_removing_piece(square, *existing_piece); + zobrist.update_adding_piece(square, piece); + } + } + + place_result } pub fn remove_piece(&mut self, square: Square) -> Option { - self.pieces.remove(square) + let removed_piece = self.pieces.remove(square); + + if let Some(piece) = removed_piece { + if let Some(zobrist) = self.zobrist_hash.as_mut() { + zobrist.update_removing_piece(square, piece); + } + } + + removed_piece } } @@ -209,6 +285,29 @@ impl Board { } } +impl Board { + pub fn zobrist_hash(&self) -> Option { + self.zobrist_hash.as_ref().map(ZobristHash::hash_value) + } + + pub fn recompute_zobrist_hash(&mut self) { + // Avoid overlapping borrows when borrowing zobrist_hash.as_mut() and + // then also borrowing self to update the board hash by computing the + // hash with the static function first, and then setting the hash value + // on the zobrist instance. Unfortuantely this requires unwrapping + // self.zobrist_hash twice. C'est la vie. + + let new_hash = self.zobrist_hash.as_ref().map(|zobrist| { + let state = zobrist.state(); + ZobristHash::compute_board_hash(self, state.as_ref()) + }); + + if let (Some(new_hash), Some(zobrist)) = (new_hash, self.zobrist_hash.as_mut()) { + zobrist.set_hash_value(new_hash); + } + } +} + impl Board { pub fn display(&self) -> DiagramFormatter<'_> { DiagramFormatter::new(self) diff --git a/board/src/castle/rights.rs b/board/src/castle/rights.rs index ccd56c6..b65461b 100644 --- a/board/src/castle/rights.rs +++ b/board/src/castle/rights.rs @@ -30,6 +30,12 @@ impl Rights { } } +impl Rights { + pub(crate) fn as_index(&self) -> usize { + self.0 as usize + } +} + impl Rights { fn flag_offset(color: Color, wing: Wing) -> usize { ((color as usize) << 1) + wing as usize diff --git a/board/src/fen.rs b/board/src/fen.rs index 470675b..12f59ed 100644 --- a/board/src/fen.rs +++ b/board/src/fen.rs @@ -185,7 +185,7 @@ impl FromFenStr for Board { type Error = FromFenStrError; fn from_fen_str(string: &str) -> Result { - let mut board = Board::empty(); + let mut board = Board::empty(None); let mut fields = string.split(' '); @@ -334,7 +334,7 @@ mod tests { #[test] fn from_starting_fen() { let board = fen!("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 0").unwrap(); - let expected = Board::starting(); + let expected = Board::starting(None); assert_eq!(board, expected, "{board:#?}\n{expected:#?}"); } } diff --git a/board/src/lib.rs b/board/src/lib.rs index 451bc23..ae80da8 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -8,6 +8,7 @@ pub mod fen; pub mod macros; pub mod movement; pub mod sight; +pub mod zobrist; mod board_provider; mod check; @@ -18,5 +19,6 @@ pub use board_provider::BoardProvider; pub use castle::Parameters as CastleParameters; pub use castle::Rights as CastleRights; pub use piece_sets::{PlacePieceError, PlacePieceStrategy}; +pub use zobrist::ZobristState; use piece_sets::PieceSet; diff --git a/board/src/macros.rs b/board/src/macros.rs index b3baf85..e2786e2 100644 --- a/board/src/macros.rs +++ b/board/src/macros.rs @@ -4,7 +4,7 @@ macro_rules! test_board { ($to_move:ident, [ $($color:ident $shape:ident on $square:ident),* $(,)? ], $en_passant:ident) => { { - let mut board = $crate::Board::empty(); + let mut board = $crate::Board::empty(Some($crate::test_zobrist!())); $(let _ = board.place_piece( chessfriend_core::Piece::new( chessfriend_core::Color::$color, @@ -23,7 +23,7 @@ macro_rules! test_board { }; ($to_move:ident, [ $($color:ident $shape:ident on $square:ident),* $(,)? ]) => { { - let mut board = $crate::Board::empty(); + let mut board = $crate::Board::empty(Some($crate::test_zobrist!())); $(let _ = board.place_piece( chessfriend_core::Piece::new( chessfriend_core::Color::$color, @@ -41,7 +41,7 @@ macro_rules! test_board { }; ($($color:ident $shape:ident on $square:ident),* $(,)?) => { { - let mut board = $crate::Board::empty(); + let mut board = $crate::Board::empty(Some($crate::test_zobrist!())); $(let _ = board.place_piece( chessfriend_core::Piece::new( chessfriend_core::Color::$color, @@ -58,16 +58,25 @@ macro_rules! test_board { }; (empty) => { { - let board = Board::empty(); + let board = Board::empty(Some($crate::test_zobrist!())); println!("{}", board.display()); board } }; (starting) => { { - let board = Board::starting(); + let board = Board::starting(Some($crate::test_zobrist!())); println!("{}", board.display()); board } }; } + +#[macro_export] +macro_rules! test_zobrist { + () => {{ + let mut rng = chessfriend_core::random::RandomNumberGenerator::default(); + let state = $crate::zobrist::ZobristState::new(&mut rng); + std::sync::Arc::new(state) + }}; +} diff --git a/board/src/sight.rs b/board/src/sight.rs index 9a84f7e..81fb120 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -228,7 +228,13 @@ mod tests { } }; ($test_name:ident, $piece:expr, $square:expr, $bitboard:expr) => { - sight_test! {$test_name, $crate::Board::empty(), $piece, $square, $bitboard} + sight_test! { + $test_name, + $crate::Board::empty(Some($crate::test_zobrist!())), + $piece, + $square, + $bitboard + } }; } diff --git a/board/src/zobrist.rs b/board/src/zobrist.rs new file mode 100644 index 0000000..39eee98 --- /dev/null +++ b/board/src/zobrist.rs @@ -0,0 +1,239 @@ +// Eryn Wells + +//! This module implements facilities for computing hash values of board +//! positions via the Zobrist hashing algorithm. +//! +//! ## See Also +//! +//! * The Chess Programming Wiki page on [Zobrist Hashing][1] +//! +//! [1]: https://www.chessprogramming.org/Zobrist_Hashing + +use crate::{castle, Board}; +use chessfriend_core::{random::RandomNumberGenerator, Color, File, Piece, Shape, Square, Wing}; +use rand::Fill; +use std::sync::Arc; + +const NUM_SQUARE_PIECE_VALUES: usize = Shape::NUM * Color::NUM * Square::NUM; +const NUM_CASTLING_RIGHTS_VALUES: usize = 16; + +type HashValue = u64; + +type SquarePieceValues = [[[HashValue; Color::NUM]; Shape::NUM]; Square::NUM]; +type CastlingRightsValues = [HashValue; NUM_CASTLING_RIGHTS_VALUES]; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ZobristHash { + // TODO: Keep this field in mind if ChessFriend grows threads. It may also + // need a Mutex<>. + state: Arc, + + /// The current hash value. + hash_value: HashValue, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ZobristState { + square_piece_values: SquarePieceValues, + black_to_move_value: HashValue, + castling_rights_values: CastlingRightsValues, + en_passant_file_values: [HashValue; File::NUM], +} + +impl ZobristState { + #[must_use] + pub fn new(rng: &mut RandomNumberGenerator) -> Self { + let square_piece_values = { + let mut values = [[[0; Color::NUM]; Shape::NUM]; Square::NUM]; + for square in Square::ALL { + for shape in Shape::ALL { + for color in Color::ALL { + values[square as usize][shape as usize][color as usize] = rng.next_u64(); + } + } + } + + values + }; + + let mut castling_rights_values: CastlingRightsValues = [0; NUM_CASTLING_RIGHTS_VALUES]; + castling_rights_values.fill(rng.rand()); + + let mut en_passant_file_values = [0; File::NUM]; + en_passant_file_values.fill(rng.rand()); + + Self { + square_piece_values, + black_to_move_value: rng.next_u64(), + castling_rights_values, + en_passant_file_values, + } + } +} + +impl ZobristHash { + #[must_use] + pub fn new(state: Arc) -> Self { + Self { + state, + hash_value: 0, + } + } + + #[must_use] + pub fn hash_value(&self) -> HashValue { + self.hash_value + } + + pub(crate) fn set_hash_value(&mut self, value: HashValue) { + self.hash_value = value; + } + + #[must_use] + pub fn state(&self) -> Arc { + self.state.clone() + } + + #[must_use] + pub fn compute_board_hash(board: &Board, state: &ZobristState) -> HashValue { + let mut hash_value: HashValue = 0; + + for (square, piece) in board.iter() { + hash_value ^= state.square_piece_values[square as usize][piece.shape as usize] + [piece.color as usize]; + } + + if board.active_color() == Color::Black { + hash_value ^= state.black_to_move_value; + } + + if let Some(square) = board.en_passant_target() { + hash_value ^= state.en_passant_file_values[square.file().as_index()]; + } + + hash_value + } + + pub fn recompute_hash(&mut self, board: &Board) -> HashValue { + self.hash_value = Self::compute_board_hash(board, self.state.as_ref()); + self.hash_value + } + + pub fn update_adding_piece(&mut self, square: Square, piece: Piece) -> HashValue { + self.hash_value ^= self.xor_piece_square_operand(square, piece.shape, piece.color); + self.hash_value + } + + pub fn update_removing_piece(&mut self, square: Square, piece: Piece) -> HashValue { + self.hash_value ^= self.xor_piece_square_operand(square, piece.shape, piece.color); + self.hash_value + } + + pub fn update_setting_active_color(&mut self, _color: Color) -> HashValue { + self.hash_value ^= self.state.black_to_move_value; + self.hash_value + } + + pub fn update_modifying_castling_rights( + &mut self, + new_rights: castle::Rights, + old_rights: castle::Rights, + ) -> HashValue { + let state = self.state.as_ref(); + self.hash_value ^= state.castling_rights_values[new_rights.as_index()]; + self.hash_value ^= state.castling_rights_values[old_rights.as_index()]; + self.hash_value + } + + pub fn update_setting_en_passant_target( + &mut self, + old_target: Option, + new_target: Option, + ) -> HashValue { + let state = self.state.as_ref(); + + if let Some(old_target) = old_target { + self.hash_value ^= state.en_passant_file_values[old_target.file().as_index()]; + } + if let Some(new_target) = new_target { + self.hash_value ^= state.en_passant_file_values[new_target.file().as_index()]; + } + + self.hash_value + } + + fn xor_piece_square_operand(&self, square: Square, shape: Shape, color: Color) -> HashValue { + let square = square as usize; + let shape = shape as usize; + let color = color as usize; + self.state.square_piece_values[square][shape][color] + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_state() -> ZobristState { + let mut rng = RandomNumberGenerator::default(); + ZobristState::new(&mut rng) + } + + fn test_hash() -> ZobristHash { + ZobristHash::new(Arc::new(test_state())) + } + + #[test] + fn hash_empty_board_ai_claude() { + let state = test_state(); + let board = Board::empty(None); + let hash = ZobristHash::compute_board_hash(&board, &state); + + // Empty board with white to move should only contribute active color + assert_eq!(hash, 0); + } + + #[test] + fn hash_board_with_black_to_move_ai_claude() { + let state = test_state(); + + let mut board = Board::empty(None); + board.set_active_color(Color::Black); + + let hash = ZobristHash::compute_board_hash(&board, &state); + assert_eq!(hash, state.black_to_move_value); + } + + #[test] + fn hash_different_en_passant_files_ai_claude() { + let mut board1 = Board::empty(None); + let mut board2 = Board::empty(None); + + // Different file should produce different hash values, even if ranks + // are the same. + board1.set_en_passant_target(Square::A3); + board2.set_en_passant_target(Square::H3); + + let state = test_state(); + let hash1 = ZobristHash::compute_board_hash(&board1, &state); + let hash2 = ZobristHash::compute_board_hash(&board2, &state); + + assert_ne!(hash1, hash2); + } + + #[test] + fn hash_en_passant_same_file_different_rank_ai_claude() { + let mut board1 = Board::empty(None); + let mut board2 = Board::empty(None); + + // Same file, different ranks should produce same hash (only file matters) + board1.set_en_passant_target(Square::E3); + board2.set_en_passant_target(Square::E6); + + let state = test_state(); + let hash1 = ZobristHash::compute_board_hash(&board1, &state); + let hash2 = ZobristHash::compute_board_hash(&board2, &state); + + assert_eq!(hash1, hash2); + } +} diff --git a/explorer/src/main.rs b/explorer/src/main.rs index 6fc13cf..59b790d 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -2,14 +2,16 @@ mod make_command; +use chessfriend_board::ZobristState; use chessfriend_board::{fen::FromFenStr, Board}; +use chessfriend_core::random::RandomNumberGenerator; use chessfriend_core::{Color, Piece, Shape, Square}; use chessfriend_moves::{Builder as MoveBuilder, GeneratedMove, MakeMove, ValidateMove}; use chessfriend_position::{fen::ToFenStr, PlacePieceStrategy, Position}; - use clap::{Arg, Command}; use rustyline::error::ReadlineError; use rustyline::DefaultEditor; +use std::sync::Arc; use thiserror::Error; struct CommandResult { @@ -28,6 +30,7 @@ impl Default for CommandResult { struct State { position: Position, + zobrist: Arc, } fn command_line() -> Command { @@ -171,10 +174,6 @@ fn respond(line: &str, state: &mut State) -> anyhow::Result { } Some(("moves", matches)) => result = do_moves_command(state, matches)?, Some(("movement", matches)) => result = do_movement_command(state, matches)?, - Some(("starting", _matches)) => { - let starting_position = Position::starting(); - state.position = starting_position; - } Some(("reset", matches)) => result = do_reset_command(state, matches)?, Some((name, _matches)) => unimplemented!("{name}"), None => unreachable!("Subcommand required"), @@ -188,8 +187,8 @@ fn do_reset_command( matches: &clap::ArgMatches, ) -> anyhow::Result { match matches.subcommand() { - None | Some(("clear", _)) => state.position = Position::empty(), - Some(("starting", _)) => state.position = Position::starting(), + None | Some(("clear", _)) => state.position = Position::empty(Some(state.zobrist.clone())), + Some(("starting", _)) => state.position = Position::starting(Some(state.zobrist.clone())), Some(("fen", matches)) => { let fen = matches .get_one::("fen") @@ -273,9 +272,13 @@ fn do_movement_command( fn main() -> Result<(), String> { let mut editor = DefaultEditor::new().map_err(|err| format!("Error: {err}"))?; - let starting_position = Position::starting(); + let mut rng = RandomNumberGenerator::default(); + let zobrist_state = Arc::new(ZobristState::new(&mut rng)); + + let starting_position = Position::starting(Some(zobrist_state.clone())); let mut state = State { position: starting_position, + zobrist: zobrist_state, }; let mut should_print_position = true; diff --git a/position/src/position.rs b/position/src/position.rs index 8b08bce..30afe0d 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -15,9 +15,10 @@ use captures::CapturesList; use chessfriend_bitboard::BitBoard; use chessfriend_board::{ display::DiagramFormatter, fen::ToFenStr, Board, PlacePieceError, PlacePieceStrategy, + ZobristState, }; use chessfriend_core::{Color, Piece, Shape, Square}; -use std::fmt; +use std::{fmt, sync::Arc}; #[must_use] #[derive(Clone, Debug, Default, Eq)] @@ -28,16 +29,13 @@ pub struct Position { } impl Position { - pub fn empty() -> Self { - Position::default() + pub fn empty(zobrist: Option>) -> Self { + Self::new(Board::empty(zobrist)) } /// Return a starting position. - pub fn starting() -> Self { - Self { - board: Board::starting(), - ..Default::default() - } + pub fn starting(zobrist: Option>) -> Self { + Self::new(Board::starting(zobrist)) } pub fn new(board: Board) -> Self { From 651c982eadf7a047a894d7bd8216c3c2152dcf36 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 6 Jun 2025 21:45:07 -0700 Subject: [PATCH 338/423] [board, moves, position] Make the Peter Ellis Jones gotcha unit tests work Move this file over to position/tests. That makes it an integration test, technically. Update it to comply with the current API conventions. This is a pretty radical change from when I first wrote these! In the process of running these tests, I found a bug in my PawnMoveGenerator where it was generating the origin squares for en passant captures incorrectly. Fix those bugs and write two new tests to exercise those code paths. One of the test_board! variants was setting .active_color instead of using the setter. This is a build failure. Fix it. Move the assert_move_list! macro to the moves crate. This is a more natural home for it. Additionally, add assert_move_list_contains! and assert_move_list_does_not_contain! to generate assertions that a collection of moves (anything that implements .contains()) has or doesn't have a set of moves. Also remove formatted_move_list!, which is no longer used. All that done, make the tests pass! --- board/src/macros.rs | 2 +- moves/src/generators.rs | 8 +- moves/src/generators/pawn.rs | 34 ++- moves/src/generators/testing.rs | 31 --- moves/src/lib.rs | 1 + moves/src/macros.rs | 77 ++++++ position/src/lib.rs | 7 +- .../move_generator/tests/peterellisjones.rs | 238 ------------------ position/src/position.rs | 34 +++ position/src/testing.rs | 33 --- position/tests/peterellisjones.rs | 166 ++++++++++++ 11 files changed, 316 insertions(+), 315 deletions(-) delete mode 100644 moves/src/generators/testing.rs create mode 100644 moves/src/macros.rs delete mode 100644 position/src/move_generator/tests/peterellisjones.rs create mode 100644 position/tests/peterellisjones.rs diff --git a/board/src/macros.rs b/board/src/macros.rs index e2786e2..29df6fc 100644 --- a/board/src/macros.rs +++ b/board/src/macros.rs @@ -32,7 +32,7 @@ macro_rules! test_board { chessfriend_core::Square::$square, $crate::PlacePieceStrategy::default()); )* - board.active_color = chessfriend_core::Color::$to_move; + board.set_active_color(chessfriend_core::Color::$to_move); println!("{}", board.display()); diff --git a/moves/src/generators.rs b/moves/src/generators.rs index de7f4fd..8c25078 100644 --- a/moves/src/generators.rs +++ b/moves/src/generators.rs @@ -6,9 +6,6 @@ mod knight; mod pawn; mod slider; -#[cfg(test)] -mod testing; - pub use all::AllPiecesMoveGenerator; pub use king::KingMoveGenerator; pub use knight::KnightMoveGenerator; @@ -28,6 +25,11 @@ impl GeneratedMove { pub fn origin(&self) -> Square { self.ply.origin_square() } + + #[must_use] + pub fn target(&self) -> Square { + self.ply.target_square() + } } impl std::fmt::Display for GeneratedMove { diff --git a/moves/src/generators/pawn.rs b/moves/src/generators/pawn.rs index 5e8b76b..f68bab6 100644 --- a/moves/src/generators/pawn.rs +++ b/moves/src/generators/pawn.rs @@ -120,16 +120,16 @@ impl PawnMoveGenerator { MoveType::EnPassant => match self.color { Color::White => { if (self.en_passant & self.left_captures).is_populated() { - target.neighbor(Direction::NorthWest, None) + target.neighbor(Direction::SouthEast, None) } else { - target.neighbor(Direction::NorthEast, None) + target.neighbor(Direction::SouthWest, None) } } Color::Black => { if (self.en_passant & self.left_captures).is_populated() { - target.neighbor(Direction::SouthEast, None) + target.neighbor(Direction::NorthWest, None) } else { - target.neighbor(Direction::SouthWest, None) + target.neighbor(Direction::NorthEast, None) } } }, @@ -241,7 +241,7 @@ impl MoveType { #[cfg(test)] mod tests { use super::*; - use crate::Move; + use crate::{assert_move_list, ply, Move}; use chessfriend_board::test_board; use chessfriend_core::{Color, Square}; use std::collections::HashSet; @@ -485,4 +485,28 @@ mod tests { .into() ); } + + #[test] + fn black_e4_captures_d4_en_passant() { + let board = test_board!(Black, [ + White Pawn on D4, + Black Pawn on E4 + ], D3); + + let generated_moves = PawnMoveGenerator::new(&board, None); + + assert_move_list!(generated_moves, [ply!(E4 - E3), ply!(E4 x D3 e.p.),]); + } + + #[test] + fn white_e5_captures_f5_en_passant() { + let board = test_board!(White, [ + White Pawn on E5, + Black Pawn on F5 + ], F6); + + let generated_moves = PawnMoveGenerator::new(&board, None); + + assert_move_list!(generated_moves, [ply!(E5 - E6), ply!(E5 x F6 e.p.),]); + } } diff --git a/moves/src/generators/testing.rs b/moves/src/generators/testing.rs deleted file mode 100644 index 5a8a782..0000000 --- a/moves/src/generators/testing.rs +++ /dev/null @@ -1,31 +0,0 @@ -// Eryn Wells - -#[macro_export] -macro_rules! assert_move_list { - ($generator:expr, [ $($expected:expr),* $(,)? ]) => { - { - let generated_moves: std::collections::HashSet<$crate::GeneratedMove> = $generator.collect(); - let expected_moves: std::collections::HashSet<$crate::GeneratedMove> = [ - $($expected.into(),)* - ].into(); - - assert_eq!( - generated_moves, - expected_moves, - "\n\tMatching: {:?}\n\tGenerated, not expected: {:?}\n\tExpected, not generated: {:?}", - generated_moves - .intersection(&expected_moves) - .map(|mv| format!("{}", mv)) - .collect::>(), - generated_moves - .difference(&expected_moves) - .map(|mv| format!("{}", mv)) - .collect::>(), - expected_moves - .difference(&generated_moves) - .map(|mv| format!("{}", mv)) - .collect::>(), - ); - } - }; -} diff --git a/moves/src/lib.rs b/moves/src/lib.rs index 5488215..4ed188c 100644 --- a/moves/src/lib.rs +++ b/moves/src/lib.rs @@ -5,6 +5,7 @@ pub mod testing; mod builder; mod defs; +mod macros; mod make_move; mod moves; mod record; diff --git a/moves/src/macros.rs b/moves/src/macros.rs new file mode 100644 index 0000000..4c9c66b --- /dev/null +++ b/moves/src/macros.rs @@ -0,0 +1,77 @@ +// Eryn Wells + +#[macro_export] +macro_rules! assert_move_list { + ($generator:expr, [ $($expected:expr),* $(,)? ]) => { + { + let generated_moves: std::collections::HashSet<$crate::GeneratedMove> = $generator.collect(); + let expected_moves: std::collections::HashSet<$crate::GeneratedMove> = [ + $($expected.into(),)* + ].into(); + + assert_eq!( + generated_moves, + expected_moves, + "\n\tMatching: {:?}\n\tGenerated, not expected: {:?}\n\tExpected, not generated: {:?}", + generated_moves + .intersection(&expected_moves) + .map(|mv| format!("{}", mv)) + .collect::>(), + generated_moves + .difference(&expected_moves) + .map(|mv| format!("{}", mv)) + .collect::>(), + expected_moves + .difference(&generated_moves) + .map(|mv| format!("{}", mv)) + .collect::>(), + ); + } + }; + ($generator:expr, $expected:expr) => { + { + use std::collections::HashSet; + + let generated_moves: HashSet<$crate::GeneratedMove> = $generator.collect(); + let expected_moves: HashSet<$crate::GeneratedMove> = $expected.collect(); + + assert_eq!( + generated_moves, + expected_moves, + "\n\tMatching: {:?}\n\tGenerated, not expected: {:?}\n\tExpected, not generated: {:?}", + generated_moves + .intersection(&expected_moves) + .map(|mv| format!("{}", mv)) + .collect::>(), + generated_moves + .difference(&expected_moves) + .map(|mv| format!("{}", mv)) + .collect::>(), + expected_moves + .difference(&generated_moves) + .map(|mv| format!("{}", mv)) + .collect::>(), + ); + } + }; +} + +#[macro_export] +macro_rules! assert_move_list_contains { + ($generated_moves:expr, [ $($expected:expr),* $(,)? ]) => { + $( + assert!($generated_moves.contains(&$expected.into())); + )* + }; +} + +#[macro_export] +macro_rules! assert_move_list_does_not_contain { + ($generated_moves:expr, [ $($expected:expr),* $(,)? ]) => { + { + $( + assert!(!$generated_moves.contains(&$expected.into())); + )* + } + }; +} diff --git a/position/src/lib.rs b/position/src/lib.rs index 931c507..fe66fc9 100644 --- a/position/src/lib.rs +++ b/position/src/lib.rs @@ -5,10 +5,9 @@ mod position; #[macro_use] mod macros; -#[cfg(test)] -#[macro_use] -mod testing; - pub use chessfriend_board::{fen, PlacePieceError, PlacePieceStrategy}; pub use chessfriend_moves::GeneratedMove; pub use position::Position; + +#[macro_use] +pub mod testing; diff --git a/position/src/move_generator/tests/peterellisjones.rs b/position/src/move_generator/tests/peterellisjones.rs deleted file mode 100644 index 4a12b92..0000000 --- a/position/src/move_generator/tests/peterellisjones.rs +++ /dev/null @@ -1,238 +0,0 @@ -// Eryn Wells - -//! Move generator tests based on board positions described in [Peter Ellis -//! Jones][1]' excellent [blog post][2] on generated legal chess moves. -//! -//! [1]: https://peterellisjones.com -//! [2]: https://peterellisjones.com/posts/generating-legal-chess-moves-efficiently/ - -use crate::{assert_move_list, formatted_move_list, test_position, testing::*}; -use chessfriend_core::{piece, Square}; -use chessfriend_moves::Builder as MoveBuilder; -use std::collections::HashSet; - -#[test] -fn pseudo_legal_move_generation() -> TestResult { - let pos = test_position!(Black, [ - Black King on E8, - White King on E1, - White Rook on F5, - ]); - - let generated_moves = pos.moves(); - let king_moves = generated_moves - .moves_for_piece(&piece!(Black King on E8)) - .ok_or(TestError::NoLegalMoves)?; - - assert!(!king_moves.can_move_to_square(Square::F8)); - assert!(!king_moves.can_move_to_square(Square::F7)); - - Ok(()) -} - -#[test] -fn gotcha_king_moves_away_from_a_checking_slider() -> TestResult { - let pos = test_position!(Black, [ - Black King on E7, - White King on E1, - White Rook on E4, - ]); - - let generated_moves = pos.moves(); - let king_moves = generated_moves - .moves_for_piece(&piece!(Black King on E7)) - .ok_or(TestError::NoLegalMoves)?; - - assert!(!king_moves.can_move_to_square(Square::E8)); - - Ok(()) -} - -#[test] -fn check_evasions_1() -> TestResult { - let pos = test_position!(Black, [ - Black King on E8, - White King on E1, - White Knight on F6, - ]); - - let generated_moves = pos.moves(); - - let builder = MoveBuilder::push(&piece!(Black King on E8)); - let expected_moves = HashSet::from_iter([ - builder.clone().to(Square::D8).build()?, - builder.clone().to(Square::E7).build()?, - builder.clone().to(Square::F7).build()?, - builder.clone().to(Square::F8).build()?, - ]); - - assert_move_list!( - generated_moves.iter().collect::>(), - expected_moves, - pos - ); - - Ok(()) -} - -#[test] -fn check_evasions_double_check() -> TestResult { - let pos = test_position!(Black, [ - Black King on E8, - Black Bishop on F6, - White King on E1, - White Knight on G7, - White Rook on E5, - ]); - - let generated_moves = pos.moves(); - - let builder = MoveBuilder::push(&piece!(Black King on E8)); - let expected_moves = HashSet::from_iter([ - builder.clone().to(Square::D8).build()?, - builder.clone().to(Square::D7).build()?, - builder.clone().to(Square::F7).build()?, - builder.clone().to(Square::F8).build()?, - ]); - - assert_move_list!( - generated_moves.iter().collect::>(), - expected_moves, - pos - ); - - Ok(()) -} - -#[test] -fn single_check_with_blocker() -> TestResult { - let pos = test_position!(Black, [ - Black King on E8, - Black Knight on G6, - White King on E1, - White Rook on E5, - ]); - - let generated_moves = pos.moves(); - - let king_builder = MoveBuilder::push(&piece!(Black King on E8)); - let knight_builder = MoveBuilder::push(&piece!(Black Knight on G6)); - let expected_moves = HashSet::from_iter([ - king_builder.clone().to(Square::D8).build()?, - king_builder.clone().to(Square::D7).build()?, - king_builder.clone().to(Square::F7).build()?, - king_builder.clone().to(Square::F8).build()?, - knight_builder.clone().to(Square::E7).build()?, - knight_builder.clone().capturing_on(Square::E5).build()?, - ]); - - assert_move_list!( - generated_moves.iter().collect::>(), - expected_moves, - pos - ); - - Ok(()) -} - -#[test] -fn en_passant_check_capture() -> TestResult { - let pos = test_position!(Black, [ - Black King on C5, - Black Pawn on E4, - White Pawn on D4, - ], D3); - - assert!(pos.is_king_in_check()); - - let generated_moves: HashSet<_> = pos.moves().iter().collect(); - - assert!( - generated_moves.contains( - &MoveBuilder::push(&piece!(Black Pawn on E4)) - .capturing_en_passant_on(Square::D3) - .build()? - ), - "Valid moves: {:?}", - formatted_move_list!(generated_moves, pos) - ); - - Ok(()) -} - -#[test] -fn en_passant_check_block() -> TestResult { - let pos = test_position!(Black, [ - Black King on B5, - Black Pawn on E4, - White Pawn on D4, - White Queen on F1, - ], D3); - - assert!(pos.is_king_in_check()); - - let generated_moves: HashSet<_> = pos.moves().iter().collect(); - - assert!( - generated_moves.contains( - &MoveBuilder::push(&piece!(Black Pawn on E4)) - .capturing_en_passant_on(Square::D3) - .build()? - ), - "Valid moves: {:?}", - formatted_move_list!(generated_moves, pos) - ); - - Ok(()) -} - -#[test] -fn pinned_pieces_rook_cannot_move_out_of_pin() -> TestResult { - let pos = test_position!(Black, [ - Black King on E8, - Black Rook on E6, - White Queen on E3, - White King on C1, - ]); - - assert!(!pos.is_king_in_check()); - - let generated_moves = pos.moves(); - let rook_moves = generated_moves - .moves_for_piece(&piece!(Black Rook on E6)) - .ok_or(TestError::NoLegalMoves)?; - - assert!(!rook_moves.can_move_to_square(Square::D6)); - assert!(!rook_moves.can_move_to_square(Square::F6)); - - assert!(rook_moves.can_move_to_square(Square::E3)); - assert!(rook_moves.can_move_to_square(Square::E4)); - assert!(rook_moves.can_move_to_square(Square::E5)); - assert!(rook_moves.can_move_to_square(Square::E7)); - - Ok(()) -} - -#[test] -fn en_passant_discovered_check() -> TestResult { - let pos = test_position!(Black, [ - Black King on A4, - Black Pawn on E4, - White Pawn on D4, - White Queen on H4, - ], D3); - - let generated_moves: HashSet<_> = pos.moves().iter().collect(); - - let unexpected_move = MoveBuilder::push(&piece!(Black Pawn on E4)) - .capturing_en_passant_on(Square::D3) - .build()?; - - assert!( - !generated_moves.contains(&unexpected_move), - "Valid moves: {:?}", - formatted_move_list!(generated_moves, pos) - ); - - Ok(()) -} diff --git a/position/src/position.rs b/position/src/position.rs index 30afe0d..dc589cd 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -86,6 +86,40 @@ impl Position { AllPiecesMoveGenerator::new(&self.board, color) } + /// Generate legal moves. + /// + /// ## Panics + /// + /// If the position failed to make a move generated by the internal move + /// generator, this method will panic. + #[must_use] + pub fn all_legal_moves( + &self, + color: Option, + ) -> Box + '_> { + let generator = self.all_moves(color); + + let mut test_board = self.board.clone(); + Box::new(generator.filter(move |ply| { + let active_color_before_move = test_board.active_color(); + + println!("{:?} from:{} to:{}", ply, ply.origin(), ply.target()); + + let ply: Move = ply.clone().into(); + let record = test_board + .make_move(ply, ValidateMove::No) + .expect("unable to make generated move"); + + let move_is_legal = !test_board.color_is_in_check(Some(active_color_before_move)); + + test_board + .unmake_move(&record) + .expect("unable to unmake generated move"); + + move_is_legal + })) + } + #[must_use] pub fn moves_for_piece( &self, diff --git a/position/src/testing.rs b/position/src/testing.rs index 2b1be8e..c546a79 100644 --- a/position/src/testing.rs +++ b/position/src/testing.rs @@ -2,39 +2,6 @@ use chessfriend_moves::{BuildMoveError, MakeMoveError}; -#[macro_export] -macro_rules! assert_move_list { - ($generated:expr, $expected:expr, $position:expr) => { - assert_eq!( - $generated, - $expected, - "\n\tMatching: {:?}\n\tGenerated, not expected: {:?}\n\tExpected, not generated: {:?}", - $generated - .intersection(&$expected) - .map(|mv| format!("{}", mv)) - .collect::>(), - $generated - .difference(&$expected) - .map(|mv| format!("{}", mv)) - .collect::>(), - $expected - .difference(&$generated) - .map(|mv| format!("{}", mv)) - .collect::>(), - ) - }; -} - -#[macro_export] -macro_rules! formatted_move_list { - ($move_list:expr, $position:expr) => { - $move_list - .iter() - .map(|mv| format!("{}", mv)) - .collect::>() - }; -} - #[macro_export] macro_rules! assert_eq_bitboards { ($result:expr, $expected:expr) => {{ diff --git a/position/tests/peterellisjones.rs b/position/tests/peterellisjones.rs new file mode 100644 index 0000000..bfd960c --- /dev/null +++ b/position/tests/peterellisjones.rs @@ -0,0 +1,166 @@ +// Eryn Wells + +//! Move generator tests based on board positions described in [Peter Ellis +//! Jones][1]' excellent [blog post][2] on generated legal chess moves. +//! +//! [1]: https://peterellisjones.com +//! [2]: https://peterellisjones.com/posts/generating-legal-chess-moves-efficiently/ + +use chessfriend_core::{Color, Square}; +use chessfriend_moves::{ + assert_move_list, assert_move_list_contains, assert_move_list_does_not_contain, ply, Move, +}; +use chessfriend_position::{test_position, testing::*}; +use std::collections::HashSet; + +#[test] +fn pseudo_legal_move_generation() { + let pos = test_position!(Black, [ + Black King on E8, + White King on E1, + White Rook on F5, + ]); + + let king_moves: HashSet<_> = pos.all_legal_moves(Some(Color::Black)).collect(); + + assert_move_list_does_not_contain!(king_moves, [ply!(E8 - F8), ply!(E8 - F7)]); +} + +#[test] +fn gotcha_king_moves_away_from_checking_slider() { + let position = test_position!(Black, [ + Black King on E7, + White King on E1, + White Rook on E4, + ]); + + let king_moves: HashSet = position.all_legal_moves(None).map(Move::from).collect(); + + assert_move_list_does_not_contain!(king_moves, [ply!(E7 - E8)]); +} + +#[test] +fn check_evasions_1() { + let pos = test_position!(Black, [ + Black King on E8, + White King on E1, + White Knight on F6, + ]); + + let generated_moves = pos.all_legal_moves(Some(Color::Black)); + + assert_move_list!( + generated_moves, + [ply!(E8 - D8), ply!(E8 - E7), ply!(E8 - F7), ply!(E8 - F8),] + ); +} + +#[test] +fn check_evasions_double_check() { + let pos = test_position!(Black, [ + Black King on E8, + Black Bishop on F6, + White King on E1, + White Knight on G7, + White Rook on E5, + ]); + + let generated_moves = pos.all_legal_moves(Some(Color::Black)); + + assert_move_list!( + generated_moves, + [ply!(E8 - D8), ply!(E8 - D7), ply!(E8 - F7), ply!(E8 - F8),] + ); +} + +#[test] +fn single_check_with_blocker() { + let pos = test_position!(Black, [ + Black King on E8, + Black Knight on G6, + White King on E1, + White Rook on E5, + ]); + + let generated_moves = pos.all_legal_moves(Some(Color::Black)); + + assert_move_list!( + generated_moves, + [ + // King moves + ply!(E8 - D8), + ply!(E8 - D7), + ply!(E8 - F7), + ply!(E8 - F8), + // Knight moves + ply!(G6 - E7), + ply!(G6 x E5), + ] + ); +} + +#[test] +fn en_passant_check_capture() { + let pos = test_position!(Black, [ + Black King on C5, + Black Pawn on E4, + White Pawn on D4, + ], D3); + + assert!(pos.board.active_color_is_in_check()); + + let generated_moves: HashSet<_> = pos.all_legal_moves(Some(Color::Black)).collect(); + + assert_move_list_contains!(generated_moves, [ply!(E4 x D3 e.p.)]); +} + +#[test] +fn en_passant_check_block() { + let pos = test_position!(Black, [ + Black King on B5, + Black Pawn on E4, + White Pawn on D4, + White Queen on F1, + ], D3); + + assert!(pos.board.active_color_is_in_check()); + + let generated_moves: HashSet<_> = pos.all_legal_moves(Some(Color::Black)).collect(); + + assert_move_list_contains!(generated_moves, [ply!(E4 x D3 e.p.)]); +} + +#[test] +fn pinned_pieces_rook_cannot_move_out_of_pin() { + let pos = test_position!(Black, [ + Black King on E8, + Black Rook on E6, + White Queen on E3, + White King on C1, + ]); + + assert!(!pos.board.active_color_is_in_check()); + + let rook_moves: HashSet<_> = pos.all_legal_moves(None).collect(); + + assert_move_list_does_not_contain!(rook_moves, [ply!(E6 - D6), ply!(E6 - F6)]); + + assert_move_list_contains!( + rook_moves, + [ply!(E6 x E3), ply!(E6 - E4), ply!(E6 - E5), ply!(E6 - E7)] + ); +} + +#[test] +fn en_passant_discovered_check() { + let pos = test_position!(Black, [ + Black King on A4, + Black Pawn on E4, + White Pawn on D4, + White Queen on H4, + ], D3); + + let generated_moves: HashSet<_> = pos.all_legal_moves(Some(Color::Black)).collect(); + + assert_move_list_does_not_contain!(generated_moves, [ply!(E4 x D3 e.p.)]); +} From 1686eeb35a643e86896e5b04f8a73ee5a1a5b4aa Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 6 Jun 2025 21:45:48 -0700 Subject: [PATCH 339/423] [board] A small set of unit tests for Zobrist hashes on Board A few tests to ensure the zobrist hash changes when changes are made to the Board. This set isn't exhaustive. --- board/src/board.rs | 53 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/board/src/board.rs b/board/src/board.rs index 67f7f81..a3afd64 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -325,7 +325,7 @@ impl Board { mod tests { use super::*; use crate::test_board; - use chessfriend_core::piece; + use chessfriend_core::{piece, random::RandomNumberGenerator}; #[test] fn get_piece_on_square() { @@ -335,4 +335,55 @@ mod tests { assert_eq!(board.get_piece(Square::F7), Some(piece!(Black Bishop))); } + + // MARK: - Zobrist Hashing + + fn test_state() -> ZobristState { + let mut rng = RandomNumberGenerator::default(); + ZobristState::new(&mut rng) + } + + #[test] + fn zobrist_hash_set_for_empty_board() { + let state = Arc::new(test_state()); + let board = Board::empty(Some(state.clone())); + let hash = board.zobrist_hash(); + assert_eq!(hash, Some(0)); + } + + #[test] + fn zobrist_hash_set_for_starting_position_board() { + let state = Arc::new(test_state()); + let board = Board::starting(Some(state.clone())); + let hash = board.zobrist_hash(); + assert!(hash.is_some()); + } + + #[test] + fn zobrist_hash_updated_when_changing_active_color() { + let state = Arc::new(test_state()); + + let mut board = Board::empty(Some(state.clone())); + board.set_active_color(Color::Black); + + // Just verify that the value is real and has changed. The actual value + // computation is covered by the tests in zobrist.rs. + let hash = board.zobrist_hash(); + assert!(hash.is_some()); + assert_ne!(hash, Some(0)); + } + + #[test] + fn zobrist_hash_updated_when_changing_en_passant_target() { + let state = Arc::new(test_state()); + + let mut board = Board::empty(Some(state.clone())); + board.set_en_passant_target(Square::C3); + + // Just verify that the value is real and has changed. The actual value + // computation is covered by the tests in zobrist.rs. + let hash = board.zobrist_hash(); + assert!(hash.is_some()); + assert_ne!(hash, Some(0)); + } } From d44c5d6a11779e48743b9f031e5301884bd12a85 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 6 Jun 2025 21:46:13 -0700 Subject: [PATCH 340/423] [board] Remove an unused test helper function test_hash() was never used. Remove it. --- board/src/zobrist.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/board/src/zobrist.rs b/board/src/zobrist.rs index 39eee98..911bd26 100644 --- a/board/src/zobrist.rs +++ b/board/src/zobrist.rs @@ -179,10 +179,6 @@ mod tests { ZobristState::new(&mut rng) } - fn test_hash() -> ZobristHash { - ZobristHash::new(Arc::new(test_state())) - } - #[test] fn hash_empty_board_ai_claude() { let state = test_state(); From fb8a8d6110bf38cd1f28e69a882362310c2747a0 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 7 Jun 2025 08:09:36 -0700 Subject: [PATCH 341/423] [explorer] Add `zobrist` command It prints the hash of the board! --- explorer/src/main.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/explorer/src/main.rs b/explorer/src/main.rs index 59b790d..9da0ae6 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -90,6 +90,7 @@ fn command_line() -> Command { ) .subcommand(Command::new("print").about("Print the board")) .subcommand(Command::new("quit").alias("exit").about("Quit the program")) + .subcommand(Command::new("zobrist").about("Print the Zobrist hash of the current board")) } #[derive(Clone, Debug, Error, Eq, PartialEq)] @@ -175,6 +176,7 @@ fn respond(line: &str, state: &mut State) -> anyhow::Result { Some(("moves", matches)) => result = do_moves_command(state, matches)?, Some(("movement", matches)) => result = do_movement_command(state, matches)?, Some(("reset", matches)) => result = do_reset_command(state, matches)?, + Some(("zobrist", matches)) => result = do_zobrist_command(state, matches), Some((name, _matches)) => unimplemented!("{name}"), None => unreachable!("Subcommand required"), } @@ -269,6 +271,19 @@ fn do_movement_command( }) } +fn do_zobrist_command(state: &mut State, _matches: &clap::ArgMatches) -> CommandResult { + if let Some(hash) = state.position.zobrist_hash() { + println!("hash:{hash}"); + } else { + println!("No Zobrist hash available"); + } + + CommandResult { + should_continue: true, + should_print_position: false, + } +} + fn main() -> Result<(), String> { let mut editor = DefaultEditor::new().map_err(|err| format!("Error: {err}"))?; From 046aae72c8b6deb3ec3adaa4acecc815779a0604 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 7 Jun 2025 08:48:45 -0700 Subject: [PATCH 342/423] Add a rustfmt.toml file Reorganize and reformat imports, and make sure comments are wrapped to 80. --- rustfmt.toml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 rustfmt.toml diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..be6f4bf --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,5 @@ +imports_layout = "HorizontalVertical" +group_imports = "StdExternalCrate" + +wrap_comments = true + From bba88bd0e6dd3f16ab51297718839ebfe345e732 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 7 Jun 2025 08:50:20 -0700 Subject: [PATCH 343/423] [position] Export Position::zobrist_hash() Pass-through call to self.board.zobrist_hash() to return the hash value of the current board position. --- position/src/position.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/position/src/position.rs b/position/src/position.rs index dc589cd..bac5b01 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -197,6 +197,13 @@ impl Position { } } +impl Position { + #[must_use] + pub fn zobrist_hash(&self) -> Option { + self.board.zobrist_hash() + } +} + impl Position { pub fn display(&self) -> DiagramFormatter { self.board.display() From c476e93f3372f7c8df41310fa6e7e80577da5694 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 7 Jun 2025 08:51:46 -0700 Subject: [PATCH 344/423] [position] Remove cfg(test) from test_position! macro --- position/src/macros.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/position/src/macros.rs b/position/src/macros.rs index 3bce9c1..1d66d1b 100644 --- a/position/src/macros.rs +++ b/position/src/macros.rs @@ -7,7 +7,6 @@ macro_rules! position { }; } -#[cfg(test)] #[macro_export] macro_rules! test_position { ($to_move:ident, [ $($color:ident $shape:ident on $square:ident),* $(,)? ], $en_passant:ident) => { From c70cefc848dede2c1954d7d823f373434fc06843 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 7 Jun 2025 08:53:42 -0700 Subject: [PATCH 345/423] [position] Pop captured piece from CapturesList on unmake --- position/src/position.rs | 7 ++++++- position/src/position/captures.rs | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/position/src/position.rs b/position/src/position.rs index bac5b01..e674b81 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -189,7 +189,12 @@ impl Position { let unmake_result = self.board.unmake_move(&last_move_record); - if unmake_result.is_err() { + if unmake_result.is_ok() { + if let Some(capture) = last_move_record.captured_piece { + let popped_piece = self.captures.pop(last_move_record.color); + debug_assert_eq!(Some(capture), popped_piece); + } + } else { self.moves.push(last_move_record); } diff --git a/position/src/position/captures.rs b/position/src/position/captures.rs index ddd9dac..f879c5b 100644 --- a/position/src/position/captures.rs +++ b/position/src/position/captures.rs @@ -17,6 +17,10 @@ impl CapturesList { self.0[color as usize].push(piece); } + pub fn pop(&mut self, color: Color) -> Option { + self.0[color as usize].pop() + } + pub fn is_empty(&self) -> bool { self.0.iter().all(Vec::is_empty) } From d2fc285af58c40b0a5cdd2fa02175d838f63b37a Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 7 Jun 2025 08:55:34 -0700 Subject: [PATCH 346/423] [position] Track seen board positions in a HashSet As moves are made, keep track of the hashes of those board position. When moves are unmade, pop seen hashes from the HashSet. Reformat the imports in position.rs. --- position/src/position.rs | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/position/src/position.rs b/position/src/position.rs index e674b81..17f36eb 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -2,15 +2,6 @@ mod captures; -use chessfriend_moves::{ - generators::{ - AllPiecesMoveGenerator, BishopMoveGenerator, KingMoveGenerator, KnightMoveGenerator, - PawnMoveGenerator, QueenMoveGenerator, RookMoveGenerator, - }, - GeneratedMove, MakeMove, MakeMoveError, Move, MoveRecord, UnmakeMove, UnmakeMoveError, - UnmakeMoveResult, ValidateMove, -}; - use captures::CapturesList; use chessfriend_bitboard::BitBoard; use chessfriend_board::{ @@ -18,7 +9,15 @@ use chessfriend_board::{ ZobristState, }; use chessfriend_core::{Color, Piece, Shape, Square}; -use std::{fmt, sync::Arc}; +use chessfriend_moves::{ + generators::{ + AllPiecesMoveGenerator, BishopMoveGenerator, KingMoveGenerator, KnightMoveGenerator, + PawnMoveGenerator, QueenMoveGenerator, RookMoveGenerator, + }, + GeneratedMove, MakeMove, MakeMoveError, Move, MoveRecord, UnmakeMove, UnmakeMoveError, + UnmakeMoveResult, ValidateMove, +}; +use std::{collections::HashSet, fmt, sync::Arc}; #[must_use] #[derive(Clone, Debug, Default, Eq)] @@ -26,6 +25,9 @@ pub struct Position { pub board: Board, pub(crate) moves: Vec, pub(crate) captures: CapturesList, + + /// A set of hashes of board positions seen throughout the move record. + boards_seen: HashSet, } impl Position { @@ -172,6 +174,13 @@ impl Position { self.captures.push(record.color, captured_piece); } + if let Some(hash) = self.board.zobrist_hash() { + // TODO: If the hash already exists here, it's a duplicate position + // and this move results in a draw. Find a way to indicate that, + // in either Board or Position. + self.boards_seen.insert(hash); + } + self.moves.push(record.clone()); Ok(()) @@ -187,6 +196,8 @@ impl Position { pub fn unmake_last_move(&mut self) -> UnmakeMoveResult { let last_move_record = self.moves.pop().ok_or(UnmakeMoveError::NoMove)?; + let hash_before_unmake = self.board.zobrist_hash(); + let unmake_result = self.board.unmake_move(&last_move_record); if unmake_result.is_ok() { @@ -194,6 +205,10 @@ impl Position { let popped_piece = self.captures.pop(last_move_record.color); debug_assert_eq!(Some(capture), popped_piece); } + + if let Some(hash_before_unmake) = hash_before_unmake { + self.boards_seen.remove(&hash_before_unmake); + } } else { self.moves.push(last_move_record); } From 6b5a54f6b4cad84bf1b53b648361937c208a27e4 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 7 Jun 2025 08:55:54 -0700 Subject: [PATCH 347/423] [explorer] Remove unused MakeMove import --- explorer/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/explorer/src/main.rs b/explorer/src/main.rs index 9da0ae6..9be313a 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -6,7 +6,7 @@ use chessfriend_board::ZobristState; use chessfriend_board::{fen::FromFenStr, Board}; use chessfriend_core::random::RandomNumberGenerator; use chessfriend_core::{Color, Piece, Shape, Square}; -use chessfriend_moves::{Builder as MoveBuilder, GeneratedMove, MakeMove, ValidateMove}; +use chessfriend_moves::{Builder as MoveBuilder, GeneratedMove, ValidateMove}; use chessfriend_position::{fen::ToFenStr, PlacePieceStrategy, Position}; use clap::{Arg, Command}; use rustyline::error::ReadlineError; From 6cca3a0f5287c726742e9c3c1b5401071fa6489d Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 7 Jun 2025 10:09:33 -0700 Subject: [PATCH 348/423] [moves] Bug: En passant moves were generated when no pawn was available to capture Found another bug in the pawn move generator related to en passant moves. The generator was emitting e.p. captures even when no pawn was available to capture on that target square. The solution was to include the e.p. square in the enemies list, effectively treating the e.p. target as if it were occupied by an enemy piece, and then remove it from the capture bitboards before move generation. Include a couple tests to exercise this functionality. --- moves/src/generators/pawn.rs | 49 +++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/moves/src/generators/pawn.rs b/moves/src/generators/pawn.rs index f68bab6..613b6e8 100644 --- a/moves/src/generators/pawn.rs +++ b/moves/src/generators/pawn.rs @@ -46,10 +46,26 @@ impl PawnMoveGenerator { let empty = !occupied; let enemies = board.enemies(color); - let (single_pushes, double_pushes) = Self::pushes(pawns, color, empty); - let (left_captures, right_captures) = Self::captures(pawns, color, enemies); + // En passant captures present a particular challenge. Include the + // target e.p. square when computing captures (i.e. treat it like an + // enemy piece is on that square) but do not include it when generating + // capture moves. If it is included, a regular capture move will be + // generated where a special e.p. move should be created instead. + // + // So, include it in the enemies set when computing captures, then + // remove it from the left and right captures bitboards before passing + // them into the move generator. Additionally, include the target e.p. + // square in the e.p. bitboard iff the capture bitboards include it. + let en_passant: BitBoard = board.en_passant_target().into(); + let (single_pushes, double_pushes) = Self::pushes(pawns, color, empty); + let (left_captures, right_captures) = Self::captures(pawns, color, enemies | en_passant); + + let en_passant = en_passant & (left_captures | right_captures); + let left_captures = left_captures & !en_passant; + let right_captures = right_captures & !en_passant; + Self { color, single_pushes, @@ -241,7 +257,7 @@ impl MoveType { #[cfg(test)] mod tests { use super::*; - use crate::{assert_move_list, ply, Move}; + use crate::{assert_move_list, assert_move_list_does_not_contain, ply, Move}; use chessfriend_board::test_board; use chessfriend_core::{Color, Square}; use std::collections::HashSet; @@ -509,4 +525,31 @@ mod tests { assert_move_list!(generated_moves, [ply!(E5 - E6), ply!(E5 x F6 e.p.),]); } + + #[test] + fn white_no_en_passant_if_no_pawn() { + let board = test_board!(White, [ + White Pawn on A3, + Black Pawn on F5, + ], F6); + + let generated_moves: HashSet<_> = PawnMoveGenerator::new(&board, None).collect(); + + assert_move_list_does_not_contain!( + generated_moves, + [ply!(E5 x F6 e.p.), ply!(G5 x F6 e.p.)] + ); + } + + #[test] + fn black_no_en_passant_if_no_pawn() { + let board = test_board!(Black, [ + White Pawn on A4, + Black Pawn on D4, + ], A3); + + let generated_moves: HashSet<_> = PawnMoveGenerator::new(&board, None).collect(); + + assert_move_list_does_not_contain!(generated_moves, [ply!(B4 x A3 e.p.)]); + } } From a9674e321544421818c6886d4c0932d27e84d0d8 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 7 Jun 2025 19:33:05 -0700 Subject: [PATCH 349/423] [moves] Mark generated moves with square brackets Update the implementation of Display for GeneratedMove to wrap the ply in [ ]. --- moves/src/generators.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moves/src/generators.rs b/moves/src/generators.rs index 8c25078..bf8121d 100644 --- a/moves/src/generators.rs +++ b/moves/src/generators.rs @@ -34,7 +34,7 @@ impl GeneratedMove { impl std::fmt::Display for GeneratedMove { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.ply.fmt(f) + write!(f, "[{}]", self.ply) } } From 8fd7ffa58639a9180eb1ff3b34fff401cdc2d069 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 7 Jun 2025 19:35:32 -0700 Subject: [PATCH 350/423] [moves, position] Improve the error messages when asserting during legal move generation The move generators should only generate moves that can be made, so calling make_move() and unmake_move() should never give an error during legal move generation. Both of these calls assert that the result is not an Err(). Improve the error messaging so that they log the move, the current board position, and the error message. Highlight the squares relevant to the move (origin, target, and capture) when printing the board. --- moves/src/moves.rs | 14 ++++++++++++++ position/src/position.rs | 16 ++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/moves/src/moves.rs b/moves/src/moves.rs index 94ef0ad..c1fda0c 100644 --- a/moves/src/moves.rs +++ b/moves/src/moves.rs @@ -1,6 +1,7 @@ // Eryn Wells use crate::defs::{Kind, PromotionShape}; +use chessfriend_bitboard::BitBoard; use chessfriend_core::{Rank, Shape, Square, Wing}; use std::fmt; @@ -215,6 +216,19 @@ impl Move { } } +impl Move { + pub fn relevant_squares(&self) -> BitBoard { + [ + Some(self.origin_square()), + Some(self.target_square()), + self.capture_square(), + ] + .into_iter() + .flatten() + .collect() + } +} + impl Move { fn transfer_char(self) -> char { if self.is_capture() || self.is_en_passant() { diff --git a/position/src/position.rs b/position/src/position.rs index 17f36eb..332bf65 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -110,13 +110,21 @@ impl Position { let ply: Move = ply.clone().into(); let record = test_board .make_move(ply, ValidateMove::No) - .expect("unable to make generated move"); + .unwrap_or_else(|err| { + panic!( + "unable to make generated move [{ply}]: {err}\n\n{}", + test_board.display().highlight(ply.relevant_squares()) + ); + }); let move_is_legal = !test_board.color_is_in_check(Some(active_color_before_move)); - test_board - .unmake_move(&record) - .expect("unable to unmake generated move"); + test_board.unmake_move(&record).unwrap_or_else(|err| { + panic!( + "unable to unmake generated move [{ply}]: {err}\n\n{}", + test_board.display().highlight(ply.relevant_squares()) + ); + }); move_is_legal })) From a8d83ad81dd1402f5ed81acf9dbe77150d691f3a Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 7 Jun 2025 19:36:51 -0700 Subject: [PATCH 351/423] [moves] Remove common suffix of attributes of AllPiecesMoveGenerator Clippy complained that all the properties of the AllPiecesMoveGenerator struct had the same suffix. It suggested I remove the _move_generator suffix. So I did. --- moves/src/generators/all.rs | 40 ++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/moves/src/generators/all.rs b/moves/src/generators/all.rs index d11ffd0..581930f 100644 --- a/moves/src/generators/all.rs +++ b/moves/src/generators/all.rs @@ -10,23 +10,23 @@ use std::iter::{Fuse, FusedIterator}; #[must_use] pub struct AllPiecesMoveGenerator { - pawn_move_generator: Fuse, - knight_move_generator: Fuse, - bishop_move_generator: Fuse, - rook_move_generator: Fuse, - queen_move_generator: Fuse, - king_move_generator: Fuse, + pawn: Fuse, + knight: Fuse, + bishop: Fuse, + rook: Fuse, + queen: Fuse, + king: Fuse, } impl AllPiecesMoveGenerator { pub fn new(board: &Board, color: Option) -> Self { Self { - pawn_move_generator: PawnMoveGenerator::new(board, color).fuse(), - knight_move_generator: KnightMoveGenerator::new(board, color).fuse(), - bishop_move_generator: BishopMoveGenerator::new(board, color).fuse(), - rook_move_generator: RookMoveGenerator::new(board, color).fuse(), - queen_move_generator: QueenMoveGenerator::new(board, color).fuse(), - king_move_generator: KingMoveGenerator::new(board, color).fuse(), + pawn: PawnMoveGenerator::new(board, color).fuse(), + knight: KnightMoveGenerator::new(board, color).fuse(), + bishop: BishopMoveGenerator::new(board, color).fuse(), + rook: RookMoveGenerator::new(board, color).fuse(), + queen: QueenMoveGenerator::new(board, color).fuse(), + king: KingMoveGenerator::new(board, color).fuse(), } } } @@ -36,12 +36,12 @@ impl Iterator for AllPiecesMoveGenerator { fn next(&mut self) -> Option { macro_rules! return_next_move { - ($generator:expr) => {{ + ($generator:expr) => { let next_move = $generator.next(); if next_move.is_some() { return next_move; } - }}; + }; } // All of these iterators are fused, meaning they are guaranteed to @@ -50,12 +50,12 @@ impl Iterator for AllPiecesMoveGenerator { // generators' next() methods doesn't have to be repeated every time // next() is called. - return_next_move!(self.pawn_move_generator); - return_next_move!(self.knight_move_generator); - return_next_move!(self.bishop_move_generator); - return_next_move!(self.rook_move_generator); - return_next_move!(self.queen_move_generator); - return_next_move!(self.king_move_generator); + return_next_move!(self.pawn); + return_next_move!(self.knight); + return_next_move!(self.bishop); + return_next_move!(self.rook); + return_next_move!(self.queen); + return_next_move!(self.king); None } From e2ce7782477a9c1649accd46c46cf5375fb96f51 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 7 Jun 2025 20:06:14 -0700 Subject: [PATCH 352/423] [core, moves] Improve bounds checking of Square::neighbor Remove the argument from this method. I think it was a bad idea to begin with but at the time I was looking for an expedient solution for getting neighbor squares 2 squares away. Overhaul bounds checking in this method so horizontal (west and east) bounds are checked in addition to vertical (north and south) bounds. For the diagonal directions in particular, it was easy to generate some bogus neighbor squares because this method didn't check for wrapping when calculating horizontal neighbors. --- core/src/coordinates.rs | 108 ++++++++++++++++++--------------- moves/src/generators/knight.rs | 6 +- moves/src/generators/pawn.rs | 28 +++++---- 3 files changed, 80 insertions(+), 62 deletions(-) diff --git a/core/src/coordinates.rs b/core/src/coordinates.rs index d8cb202..aceec8c 100644 --- a/core/src/coordinates.rs +++ b/core/src/coordinates.rs @@ -308,45 +308,57 @@ impl Square { } #[must_use] - pub fn neighbor(self, direction: Direction, n: Option) -> Option { - let index: u8 = self as u8; - - let n = n.unwrap_or(1); - let dir: i8 = direction.to_offset() * n; - + pub fn neighbor(self, direction: Direction) -> Option { match direction { - Direction::North | Direction::NorthEast => { - Square::try_from(index.wrapping_add_signed(dir)).ok() - } - Direction::NorthWest => { + Direction::North => { if self.rank() == Rank::EIGHT { - None - } else { - Square::try_from(index.wrapping_add_signed(dir)).ok() + return None; } } - Direction::West => { - if self.file() == File::A { - None - } else { - Square::try_from(index.wrapping_add_signed(dir)).ok() - } - } - Direction::SouthEast | Direction::South | Direction::SouthWest => { - if self.rank() == Rank::ONE { - None - } else { - Square::try_from(index.wrapping_add_signed(dir)).ok() + Direction::NorthEast => { + let (file, rank) = self.file_rank(); + if rank == Rank::EIGHT || file == File::H { + return None; } } Direction::East => { if self.file() == File::H { - None - } else { - Square::try_from(index.wrapping_add_signed(dir)).ok() + return None; + } + } + Direction::SouthEast => { + let (file, rank) = self.file_rank(); + if rank == Rank::ONE || file == File::H { + return None; + } + } + Direction::South => { + if self.rank() == Rank::ONE { + return None; + } + } + Direction::SouthWest => { + let (file, rank) = self.file_rank(); + if rank == Rank::ONE || file == File::A { + return None; + } + } + Direction::West => { + if self.file() == File::A { + return None; + } + } + Direction::NorthWest => { + let (file, rank) = self.file_rank(); + if rank == Rank::EIGHT || file == File::A { + return None; } } } + + let index: u8 = self as u8; + let direction = direction.to_offset(); + Square::try_from(index.wrapping_add_signed(direction)).ok() } } @@ -561,37 +573,37 @@ mod tests { fn valid_neighbors() { let sq = Square::E4; - assert_eq!(sq.neighbor(Direction::North, None), Some(Square::E5)); - assert_eq!(sq.neighbor(Direction::NorthEast, None), Some(Square::F5)); - assert_eq!(sq.neighbor(Direction::East, None), Some(Square::F4)); - assert_eq!(sq.neighbor(Direction::SouthEast, None), Some(Square::F3)); - assert_eq!(sq.neighbor(Direction::South, None), Some(Square::E3)); - assert_eq!(sq.neighbor(Direction::SouthWest, None), Some(Square::D3)); - assert_eq!(sq.neighbor(Direction::West, None), Some(Square::D4)); - assert_eq!(sq.neighbor(Direction::NorthWest, None), Some(Square::D5)); + assert_eq!(sq.neighbor(Direction::North), Some(Square::E5)); + assert_eq!(sq.neighbor(Direction::NorthEast), Some(Square::F5)); + assert_eq!(sq.neighbor(Direction::East), Some(Square::F4)); + assert_eq!(sq.neighbor(Direction::SouthEast), Some(Square::F3)); + assert_eq!(sq.neighbor(Direction::South), Some(Square::E3)); + assert_eq!(sq.neighbor(Direction::SouthWest), Some(Square::D3)); + assert_eq!(sq.neighbor(Direction::West), Some(Square::D4)); + assert_eq!(sq.neighbor(Direction::NorthWest), Some(Square::D5)); } #[test] fn invalid_neighbors() { let sq = Square::A1; - assert!(sq.neighbor(Direction::West, None).is_none()); - assert!(sq.neighbor(Direction::SouthWest, None).is_none()); - assert!(sq.neighbor(Direction::South, None).is_none()); + assert!(sq.neighbor(Direction::West).is_none()); + assert!(sq.neighbor(Direction::SouthWest).is_none()); + assert!(sq.neighbor(Direction::South).is_none()); let sq = Square::H1; - assert!(sq.neighbor(Direction::East, None).is_none()); - assert!(sq.neighbor(Direction::SouthEast, None).is_none()); - assert!(sq.neighbor(Direction::South, None).is_none()); + assert!(sq.neighbor(Direction::East).is_none()); + assert!(sq.neighbor(Direction::SouthEast).is_none()); + assert!(sq.neighbor(Direction::South).is_none()); let sq = Square::A8; - assert!(sq.neighbor(Direction::North, None).is_none()); - assert!(sq.neighbor(Direction::NorthWest, None).is_none()); - assert!(sq.neighbor(Direction::West, None).is_none()); + assert!(sq.neighbor(Direction::North).is_none()); + assert!(sq.neighbor(Direction::NorthWest).is_none()); + assert!(sq.neighbor(Direction::West).is_none()); let sq = Square::H8; - assert!(sq.neighbor(Direction::North, None).is_none()); - assert!(sq.neighbor(Direction::NorthEast, None).is_none()); - assert!(sq.neighbor(Direction::East, None).is_none()); + assert!(sq.neighbor(Direction::North).is_none()); + assert!(sq.neighbor(Direction::NorthEast).is_none()); + assert!(sq.neighbor(Direction::East).is_none()); } #[test] diff --git a/moves/src/generators/knight.rs b/moves/src/generators/knight.rs index ab8d235..96bd3bd 100644 --- a/moves/src/generators/knight.rs +++ b/moves/src/generators/knight.rs @@ -54,9 +54,11 @@ impl Iterator for KnightMoveGenerator { if (target_bitboard & self.friends).is_populated() { self.next() } else if (target_bitboard & self.enemies).is_populated() { - Some(Move::capture(origin, target).into()) + let ply = Move::capture(origin, target); + Some(ply.into()) } else { - Some(Move::quiet(origin, target).into()) + let ply = Move::quiet(origin, target); + Some(ply.into()) } } else { self.current_origin = None; diff --git a/moves/src/generators/pawn.rs b/moves/src/generators/pawn.rs index 613b6e8..99f5c31 100644 --- a/moves/src/generators/pawn.rs +++ b/moves/src/generators/pawn.rs @@ -118,34 +118,38 @@ impl PawnMoveGenerator { fn calculate_origin_square(&self, target: Square) -> Option { match self.move_type { MoveType::SinglePushes => match self.color { - Color::White => target.neighbor(Direction::South, None), - Color::Black => target.neighbor(Direction::North, None), + Color::White => target.neighbor(Direction::South), + Color::Black => target.neighbor(Direction::North), }, MoveType::DoublePushes => match self.color { - Color::White => target.neighbor(Direction::South, Some(2)), - Color::Black => target.neighbor(Direction::North, Some(2)), + Color::White => target + .neighbor(Direction::South)? + .neighbor(Direction::South), + Color::Black => target + .neighbor(Direction::North)? + .neighbor(Direction::North), }, MoveType::LeftCaptures => match self.color { - Color::White => target.neighbor(Direction::SouthEast, None), - Color::Black => target.neighbor(Direction::NorthWest, None), + Color::White => target.neighbor(Direction::SouthEast), + Color::Black => target.neighbor(Direction::NorthWest), }, MoveType::RightCaptures => match self.color { - Color::White => target.neighbor(Direction::SouthWest, None), - Color::Black => target.neighbor(Direction::NorthEast, None), + Color::White => target.neighbor(Direction::SouthWest), + Color::Black => target.neighbor(Direction::NorthEast), }, MoveType::EnPassant => match self.color { Color::White => { if (self.en_passant & self.left_captures).is_populated() { - target.neighbor(Direction::SouthEast, None) + target.neighbor(Direction::SouthEast) } else { - target.neighbor(Direction::SouthWest, None) + target.neighbor(Direction::SouthWest) } } Color::Black => { if (self.en_passant & self.left_captures).is_populated() { - target.neighbor(Direction::NorthWest, None) + target.neighbor(Direction::NorthWest) } else { - target.neighbor(Direction::NorthEast, None) + target.neighbor(Direction::NorthEast) } } }, From 638ef5e66ac66c11a09547de515e10207274405a Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 7 Jun 2025 20:08:25 -0700 Subject: [PATCH 353/423] [moves] Make sure en passant state is cleared after making moves En passant state should be cleared after any move that isn't a double push. To support this fix, rename advance_clocks to advance_board_state and let it take an Option. Every supporting make_move method calls this method to update board state when the make is done. So this is the ideal place to also update e.p. state. --- moves/src/make_move.rs | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/moves/src/make_move.rs b/moves/src/make_move.rs index dfd9b41..59a881e 100644 --- a/moves/src/make_move.rs +++ b/moves/src/make_move.rs @@ -81,7 +81,11 @@ trait MakeMoveInternal { fn validate_move(&self, ply: Move, validate: ValidateMove) -> Result<(), MakeMoveError>; fn validate_active_piece(&self, ply: Move) -> Result; - fn advance_clocks(&mut self, half_move_clock: HalfMoveClock); + fn advance_board_state( + &mut self, + en_passant_target: Option, + half_move_clock: HalfMoveClock, + ); } impl MakeMove for T { @@ -147,7 +151,7 @@ impl MakeMoveInternal for T { let record = MoveRecord::new(board, ply, None); - self.advance_clocks(HalfMoveClock::Advance); + self.advance_board_state(None, HalfMoveClock::Advance); Ok(record) } @@ -169,13 +173,13 @@ impl MakeMoveInternal for T { // board state before the change is preserved. let record = MoveRecord::new(board, ply, None); - board.set_en_passant_target(match target.rank() { + let en_passant_target = match target.rank() { Rank::FOUR => Square::from_file_rank(target.file(), Rank::THREE), Rank::FIVE => Square::from_file_rank(target.file(), Rank::SIX), _ => unreachable!(), - }); + }; - self.advance_clocks(HalfMoveClock::Advance); + self.advance_board_state(Some(en_passant_target), HalfMoveClock::Advance); Ok(record) } @@ -217,7 +221,7 @@ impl MakeMoveInternal for T { let record = MoveRecord::new(board, ply, Some(captured_piece)); - self.advance_clocks(HalfMoveClock::Reset); + self.advance_board_state(None, HalfMoveClock::Reset); Ok(record) } @@ -242,7 +246,7 @@ impl MakeMoveInternal for T { board.revoke_castling_right(active_color, wing); - self.advance_clocks(HalfMoveClock::Advance); + self.advance_board_state(None, HalfMoveClock::Advance); Ok(record) } @@ -276,12 +280,16 @@ impl MakeMoveInternal for T { let record = MoveRecord::new(board, ply, None); - self.advance_clocks(HalfMoveClock::Reset); + self.advance_board_state(None, HalfMoveClock::Reset); Ok(record) } - fn advance_clocks(&mut self, half_move_clock: HalfMoveClock) { + fn advance_board_state( + &mut self, + en_passant_target: Option, + half_move_clock: HalfMoveClock, + ) { let board = self.board_mut(); match half_move_clock { @@ -293,6 +301,8 @@ impl MakeMoveInternal for T { let active_color = previous_active_color.next(); board.set_active_color(active_color); + board.set_en_passant_target_option(en_passant_target); + if active_color == Color::White { board.full_move_number += 1; } From 5085cef197e477448776883047b4b2e23c12dac1 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 7 Jun 2025 20:09:15 -0700 Subject: [PATCH 354/423] [position] Remove some unused imports from peterellisjones tests --- position/tests/peterellisjones.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/position/tests/peterellisjones.rs b/position/tests/peterellisjones.rs index bfd960c..eadcf4d 100644 --- a/position/tests/peterellisjones.rs +++ b/position/tests/peterellisjones.rs @@ -6,11 +6,11 @@ //! [1]: https://peterellisjones.com //! [2]: https://peterellisjones.com/posts/generating-legal-chess-moves-efficiently/ -use chessfriend_core::{Color, Square}; +use chessfriend_core::Color; use chessfriend_moves::{ assert_move_list, assert_move_list_contains, assert_move_list_does_not_contain, ply, Move, }; -use chessfriend_position::{test_position, testing::*}; +use chessfriend_position::test_position; use std::collections::HashSet; #[test] From 3bdeea00e5941855fe245be38bd500a69e589e90 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 7 Jun 2025 20:09:51 -0700 Subject: [PATCH 355/423] [position] Re-export moves::ValidateMove from the position crate --- position/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/position/src/lib.rs b/position/src/lib.rs index fe66fc9..c575b44 100644 --- a/position/src/lib.rs +++ b/position/src/lib.rs @@ -6,7 +6,7 @@ mod position; mod macros; pub use chessfriend_board::{fen, PlacePieceError, PlacePieceStrategy}; -pub use chessfriend_moves::GeneratedMove; +pub use chessfriend_moves::{GeneratedMove, ValidateMove}; pub use position::Position; #[macro_use] From 93c17a27401177104b5418d8273c9193d12ac650 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 7 Jun 2025 20:52:27 -0700 Subject: [PATCH 356/423] [moves] Lowercase expect() error message Rust convention is short lowercase strings for these kinds of messages. --- moves/src/generators/pawn.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moves/src/generators/pawn.rs b/moves/src/generators/pawn.rs index 99f5c31..515ab16 100644 --- a/moves/src/generators/pawn.rs +++ b/moves/src/generators/pawn.rs @@ -210,7 +210,7 @@ impl std::iter::Iterator for PawnMoveGenerator { if let Some(target) = self.target_iterator.next() { let origin = self .calculate_origin_square(target) - .expect("Bogus origin square"); + .expect("bogus origin square"); if target.rank().is_promotable_rank() { self.promotion_iterator = Some(PromotionIterator { From bba910090c8ecf33be448bcc472a0924a38faaae Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 7 Jun 2025 20:53:41 -0700 Subject: [PATCH 357/423] [bitboard] Specify array lengths with ::NUM values from core types These were static bare integer values. Use ::NUM properties of various core types instead. --- bitboard/src/library.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bitboard/src/library.rs b/bitboard/src/library.rs index 177b6b7..c878f8f 100644 --- a/bitboard/src/library.rs +++ b/bitboard/src/library.rs @@ -9,12 +9,12 @@ //! provides getters for all available bitboards. use crate::BitBoard; -use chessfriend_core::{Color, Direction, Square}; +use chessfriend_core::{Color, Direction, File, Rank, Square}; use std::sync::OnceLock; #[allow(clippy::identity_op)] #[allow(clippy::erasing_op)] -pub(crate) const RANKS: [BitBoard; 8] = [ +pub(crate) const RANKS: [BitBoard; Rank::NUM] = [ BitBoard(0xFF << (0 * 8)), BitBoard(0xFF << (1 * 8)), BitBoard(0xFF << (2 * 8)), @@ -27,7 +27,7 @@ pub(crate) const RANKS: [BitBoard; 8] = [ #[allow(clippy::identity_op)] #[allow(clippy::erasing_op)] -pub(crate) const FILES: [BitBoard; 8] = [ +pub(crate) const FILES: [BitBoard; File::NUM] = [ BitBoard(0x0101_0101_0101_0101 << 0), BitBoard(0x0101_0101_0101_0101 << 1), BitBoard(0x0101_0101_0101_0101 << 2), @@ -39,13 +39,13 @@ pub(crate) const FILES: [BitBoard; 8] = [ ]; /// Bitboards representing the kingside of the board, per color. -pub(crate) const KINGSIDES: [BitBoard; 2] = [ +pub(crate) const KINGSIDES: [BitBoard; Color::NUM] = [ BitBoard(0xF0F0_F0F0_F0F0_F0F0), BitBoard(0x0F0F_0F0F_0F0F_0F0F), ]; /// Bitboards representing the queenside of the board, per color. -pub(crate) const QUEENSIDES: [BitBoard; 2] = [ +pub(crate) const QUEENSIDES: [BitBoard; Color::NUM] = [ BitBoard(0x0F0F_0F0F_0F0F_0F0F), BitBoard(0xF0F0_F0F0_F0F0_F0F0), ]; From 428ace13dcf7ef8020cc546395f9e6a22cb0cd91 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 8 Jun 2025 16:16:05 -0700 Subject: [PATCH 358/423] [moves] Remove special handling of EnPassant moves Instead of generating e.p. moves in a separate pass, check whether each capture move (left and right) targets the en passant square. If so, yield an e.p. capture otherwise just a regular capture. --- moves/src/generators/pawn.rs | 72 +++++++++++++----------------------- 1 file changed, 25 insertions(+), 47 deletions(-) diff --git a/moves/src/generators/pawn.rs b/moves/src/generators/pawn.rs index 515ab16..385e1e9 100644 --- a/moves/src/generators/pawn.rs +++ b/moves/src/generators/pawn.rs @@ -27,7 +27,6 @@ enum MoveType { DoublePushes, LeftCaptures, RightCaptures, - EnPassant, } struct PromotionIterator { @@ -62,10 +61,6 @@ impl PawnMoveGenerator { let (single_pushes, double_pushes) = Self::pushes(pawns, color, empty); let (left_captures, right_captures) = Self::captures(pawns, color, enemies | en_passant); - let en_passant = en_passant & (left_captures | right_captures); - let left_captures = left_captures & !en_passant; - let right_captures = right_captures & !en_passant; - Self { color, single_pushes, @@ -137,22 +132,6 @@ impl PawnMoveGenerator { Color::White => target.neighbor(Direction::SouthWest), Color::Black => target.neighbor(Direction::NorthEast), }, - MoveType::EnPassant => match self.color { - Color::White => { - if (self.en_passant & self.left_captures).is_populated() { - target.neighbor(Direction::SouthEast) - } else { - target.neighbor(Direction::SouthWest) - } - } - Color::Black => { - if (self.en_passant & self.left_captures).is_populated() { - target.neighbor(Direction::NorthWest) - } else { - target.neighbor(Direction::NorthEast) - } - } - }, } } @@ -165,7 +144,6 @@ impl PawnMoveGenerator { MoveType::DoublePushes => self.double_pushes, MoveType::LeftCaptures => self.left_captures, MoveType::RightCaptures => self.right_captures, - MoveType::EnPassant => self.en_passant, }; self.target_iterator = next_bitboard.occupied_squares_trailing(); @@ -229,12 +207,16 @@ impl std::iter::Iterator for PawnMoveGenerator { MoveType::DoublePushes => Some(GeneratedMove { ply: Move::double_push(origin, target), }), - MoveType::LeftCaptures | MoveType::RightCaptures => Some(GeneratedMove { - ply: Move::capture(origin, target), - }), - MoveType::EnPassant => Some(GeneratedMove { - ply: Move::en_passant_capture(origin, target), - }), + MoveType::LeftCaptures | MoveType::RightCaptures => { + let target_bitboard: BitBoard = target.into(); + Some(GeneratedMove { + ply: if (target_bitboard & self.en_passant).is_populated() { + Move::en_passant_capture(origin, target) + } else { + Move::capture(origin, target) + }, + }) + } } } else if self.next_move_type().is_some() { self.next() @@ -252,8 +234,7 @@ impl MoveType { MoveType::SinglePushes => Some(MoveType::DoublePushes), MoveType::DoublePushes => Some(MoveType::LeftCaptures), MoveType::LeftCaptures => Some(MoveType::RightCaptures), - MoveType::RightCaptures => Some(MoveType::EnPassant), - MoveType::EnPassant => None, + MoveType::RightCaptures => None, } } } @@ -261,8 +242,8 @@ impl MoveType { #[cfg(test)] mod tests { use super::*; - use crate::{assert_move_list, assert_move_list_does_not_contain, ply, Move}; - use chessfriend_board::test_board; + use crate::{assert_move_list, assert_move_list_contains, assert_move_list_does_not_contain, ply, Move}; + use chessfriend_board::{fen::FromFenStr, test_board}; use chessfriend_core::{Color, Square}; use std::collections::HashSet; @@ -370,20 +351,8 @@ mod tests { #[test] fn black_d5_left_captures() { let black_captures_board = test_board!(Black Pawn on D5, White Queen on E4); - let generated_moves: HashSet = - PawnMoveGenerator::new(&black_captures_board, Some(Color::Black)).collect(); - assert_eq!( - generated_moves, - [ - GeneratedMove { - ply: Move::capture(Square::D5, Square::E4) - }, - GeneratedMove { - ply: Move::quiet(Square::D5, Square::D4) - } - ] - .into() - ); + let generated_moves = PawnMoveGenerator::new(&black_captures_board, Some(Color::Black)); + assert_move_list!(generated_moves, [ply!(D5 x E4), ply!(D5 - D4),]); } #[test] @@ -527,7 +496,7 @@ mod tests { let generated_moves = PawnMoveGenerator::new(&board, None); - assert_move_list!(generated_moves, [ply!(E5 - E6), ply!(E5 x F6 e.p.),]); + assert_move_list!(generated_moves, [ply!(E5 - E6), ply!(E5 x F6 e.p.)]); } #[test] @@ -556,4 +525,13 @@ mod tests { assert_move_list_does_not_contain!(generated_moves, [ply!(B4 x A3 e.p.)]); } + + #[test] + fn black_en_passant_check() { + let board = Board::from_fen_str("8/8/8/2k5/2pP4/8/B7/4K3 b - d3 0 3").expect("invalid fen"); + println!("{}", board.display()); + + let generated_moves: HashSet<_> = PawnMoveGenerator::new(&board, None).collect(); + assert_move_list_contains!(generated_moves, [ply!(C4 x D3 e.p.)]); + } } From 634876822b48a9fabc5dd4ac9b7381c0406f6123 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 8 Jun 2025 16:49:55 -0700 Subject: [PATCH 359/423] [explorer] Add a load command Loads a board position from a FEN string. Plumb through setting the Zobrist state on an existing board. Rebuild the hash when setting the state. --- board/src/board.rs | 9 +++++++++ explorer/src/main.rs | 25 ++++++++++++++++++++++++- position/src/position.rs | 14 ++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/board/src/board.rs b/board/src/board.rs index a3afd64..6a93eef 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -306,6 +306,15 @@ impl Board { zobrist.set_hash_value(new_hash); } } + + pub fn zobrist_state(&self) -> Option> { + self.zobrist_hash.as_ref().map(ZobristHash::state) + } + + pub fn set_zobrist_state(&mut self, state: Arc) { + self.zobrist_hash = Some(ZobristHash::new(state)); + self.recompute_zobrist_hash(); + } } impl Board { diff --git a/explorer/src/main.rs b/explorer/src/main.rs index 9be313a..ea8ce26 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -47,6 +47,12 @@ fn command_line() -> Command { .subcommand_help_heading("COMMANDS") .help_template(PARSER_TEMPLATE) .subcommand(Command::new("fen").about("Print the current position as a FEN string")) + .subcommand( + Command::new("load") + .arg(Arg::new("fen").required(true)) + .alias("l") + .about("Load a board position from a FEN string"), + ) .subcommand( Command::new("make") .arg(Arg::new("from").required(true)) @@ -75,7 +81,7 @@ fn command_line() -> Command { .subcommand( Command::new("movement") .arg(Arg::new("square").required(true)) - .about("Show moves of a piece on a square."), + .about("Show moves of a piece on a square"), ) .subcommand( Command::new("reset") @@ -112,6 +118,7 @@ fn respond(line: &str, state: &mut State) -> anyhow::Result { let mut result = CommandResult::default(); match matches.subcommand() { + Some(("load", matches)) => result = do_load_command(state, matches)?, Some(("print", _matches)) => {} Some(("quit", _matches)) => { result.should_continue = false; @@ -184,6 +191,22 @@ fn respond(line: &str, state: &mut State) -> anyhow::Result { Ok(result) } +fn do_load_command(state: &mut State, matches: &clap::ArgMatches) -> anyhow::Result { + let fen_string = matches + .get_one::("fen") + .ok_or(CommandHandlingError::MissingArgument("fen"))?; + + let mut board = Board::from_fen_str(fen_string.as_str())?; + board.set_zobrist_state(state.zobrist.clone()); + + state.position = Position::new(board); + + Ok(CommandResult { + should_continue: true, + should_print_position: true, + }) +} + fn do_reset_command( state: &mut State, matches: &clap::ArgMatches, diff --git a/position/src/position.rs b/position/src/position.rs index 332bf65..f7b7c4a 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -2,6 +2,7 @@ mod captures; +use crate::fen::{FromFenStr, FromFenStrError}; use captures::CapturesList; use chessfriend_bitboard::BitBoard; use chessfriend_board::{ @@ -230,6 +231,10 @@ impl Position { pub fn zobrist_hash(&self) -> Option { self.board.zobrist_hash() } + + pub fn set_zobrist_state(&mut self, state: Arc) { + self.board.set_zobrist_state(state); + } } impl Position { @@ -238,6 +243,15 @@ impl Position { } } +impl FromFenStr for Position { + type Error = FromFenStrError; + + fn from_fen_str(string: &str) -> Result { + let board = Board::from_fen_str(string)?; + Ok(Position::new(board)) + } +} + impl ToFenStr for Position { type Error = ::Error; From 035a83723d5aa83b1e546174ef4cea48c071152f Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 8 Jun 2025 16:58:54 -0700 Subject: [PATCH 360/423] [position] Remove a debugging println! I accidentally committed --- position/src/position.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/position/src/position.rs b/position/src/position.rs index f7b7c4a..610834e 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -106,8 +106,6 @@ impl Position { Box::new(generator.filter(move |ply| { let active_color_before_move = test_board.active_color(); - println!("{:?} from:{} to:{}", ply, ply.origin(), ply.target()); - let ply: Move = ply.clone().into(); let record = test_board .make_move(ply, ValidateMove::No) From 651544fdd942c4d73b4b6f3ea340395fd7177e21 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 8 Jun 2025 17:16:23 -0700 Subject: [PATCH 361/423] [explorer, moves, position] Remove unused MoveBuilder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This thing isn't used by any of the "modern" code I've written. It was difficult to write, but kinda neat architecturally. It made a lot of invalid configurations of moves into build-time errors. Anyway… Remove several of the tests that relied on it, but that hadn't been updated to use the newer code either. --- explorer/src/main.rs | 20 +- moves/src/builder.rs | 428 ---------------------------------------- moves/src/lib.rs | 2 - moves/src/testing.rs | 9 - moves/tests/flags.rs | 108 ---------- moves/tests/pushes.rs | 17 -- position/src/testing.rs | 9 +- 7 files changed, 4 insertions(+), 589 deletions(-) delete mode 100644 moves/src/builder.rs delete mode 100644 moves/tests/flags.rs delete mode 100644 moves/tests/pushes.rs diff --git a/explorer/src/main.rs b/explorer/src/main.rs index ea8ce26..2e7a545 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -6,7 +6,7 @@ use chessfriend_board::ZobristState; use chessfriend_board::{fen::FromFenStr, Board}; use chessfriend_core::random::RandomNumberGenerator; use chessfriend_core::{Color, Piece, Shape, Square}; -use chessfriend_moves::{Builder as MoveBuilder, GeneratedMove, ValidateMove}; +use chessfriend_moves::GeneratedMove; use chessfriend_position::{fen::ToFenStr, PlacePieceStrategy, Position}; use clap::{Arg, Command}; use rustyline::error::ReadlineError; @@ -128,22 +128,8 @@ fn respond(line: &str, state: &mut State) -> anyhow::Result { println!("{}", state.position.to_fen_str()?); result.should_print_position = false; } - Some(("make", matches)) => { - let from_square = Square::from_algebraic_str( - matches - .get_one::("from") - .ok_or(CommandHandlingError::MissingArgument("from"))?, - )?; - - let to_square = Square::from_algebraic_str( - matches - .get_one::("to") - .ok_or(CommandHandlingError::MissingArgument("to"))?, - )?; - - let ply = MoveBuilder::new().from(from_square).to(to_square).build()?; - - state.position.make_move(ply, ValidateMove::Yes)?; + Some(("make", _matches)) => { + unimplemented!() } Some(("place", matches)) => { let color = matches diff --git a/moves/src/builder.rs b/moves/src/builder.rs deleted file mode 100644 index 53bc894..0000000 --- a/moves/src/builder.rs +++ /dev/null @@ -1,428 +0,0 @@ -// Eryn Wells - -use crate::{defs::Kind, Move, PromotionShape}; -use chessfriend_board::{en_passant::EnPassant, CastleParameters}; -use chessfriend_core::{Color, File, PlacedPiece, Rank, Square, Wing}; -use std::result::Result as StdResult; -use thiserror::Error; - -pub type Result = std::result::Result; -type EncodedMoveResult = std::result::Result; - -#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)] -pub enum Error { - #[error("no origin square")] - MissingOriginSquare, - #[error("no target square")] - MissingTargetSquare, - #[error("no capture square")] - MissingCaptureSquare, - #[error("invalid en passant square")] - InvalidEnPassantSquare, -} - -const MASK: u16 = 0b111_111; - -fn build_move_bits(origin_square: Square, target_square: Square) -> u16 { - (origin_square as u16 & MASK) << 4 | (target_square as u16 & MASK) << 10 -} - -pub trait Style { - fn origin_square(&self) -> Option { - None - } - - fn target_square(&self) -> Option { - None - } - - fn move_bits(&self) -> EncodedMoveResult { - let origin_square = self.origin_square().ok_or(Error::MissingOriginSquare)?; - let target_square = self.target_square().ok_or(Error::MissingTargetSquare)?; - - Ok(build_move_bits(origin_square, target_square)) - } - - unsafe fn move_bits_unchecked(&self) -> u16 { - let origin_square = self.origin_square().unwrap(); - let target_square = self.target_square().unwrap(); - - build_move_bits(origin_square, target_square) - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct Builder { - style: S, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct Null; - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct Push { - from: Option, - to: Option, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct DoublePush { - from: Square, - to: Square, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct Capture { - push: Push, - capture: Option, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct EnPassantCapture { - push: Push, - capture: Option, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct Promotion { - style: S, - promotion: PromotionShape, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct Castle { - color: Color, - wing: Wing, -} - -impl Style for Null {} - -impl Style for Push { - fn origin_square(&self) -> Option { - self.from - } - - fn target_square(&self) -> Option { - self.to - } -} - -impl Style for Capture { - fn origin_square(&self) -> Option { - self.push.from - } - - fn target_square(&self) -> Option { - self.push.to - } -} - -impl Style for Castle { - fn origin_square(&self) -> Option { - let parameters = CastleParameters::get(self.color, self.wing); - Some(parameters.origin.king) - } - - fn target_square(&self) -> Option { - let parameters = CastleParameters::get(self.color, self.wing); - Some(parameters.target.king) - } -} - -impl Style for DoublePush { - fn origin_square(&self) -> Option { - Some(self.from) - } - - fn target_square(&self) -> Option { - Some(self.to) - } - - fn move_bits(&self) -> StdResult { - Ok( - Kind::DoublePush as u16 - | (self.from as u16 & MASK) << 4 - | (self.to as u16 & MASK) << 10, - ) - } -} - -impl Style for EnPassantCapture { - fn origin_square(&self) -> Option { - self.push.from - } - - fn target_square(&self) -> Option { - self.push.to - } - - fn move_bits(&self) -> EncodedMoveResult { - let origin_square = self.origin_square().ok_or(Error::MissingOriginSquare)?; - let target_square = self.target_square().ok_or(Error::MissingTargetSquare)?; - - Ok(build_move_bits(origin_square, target_square)) - } -} - -impl Style for Promotion { - fn origin_square(&self) -> Option { - self.style.from - } - - fn target_square(&self) -> Option { - self.style.to - } -} - -impl Style for Promotion { - fn origin_square(&self) -> Option { - self.style.push.from - } - - fn target_square(&self) -> Option { - self.style.push.to - } -} - -impl Promotion { - fn move_bits(&self) -> StdResult { - let origin_square = self - .style - .origin_square() - .ok_or(Error::MissingOriginSquare)? as u16; - let target_square = self - .style - .target_square() - .ok_or(Error::MissingTargetSquare)? as u16; - - Ok(Kind::Promotion as u16 - | self.promotion as u16 - | (origin_square & MASK << 4) - | (target_square & MASK << 10)) - } -} - -impl Promotion { - fn move_bits(&self) -> StdResult { - let origin_square = self - .style - .origin_square() - .ok_or(Error::MissingOriginSquare)? as u16; - let target_square = self - .style - .target_square() - .ok_or(Error::MissingTargetSquare)? as u16; - - Ok(Kind::CapturePromotion as u16 - | self.promotion as u16 - | (origin_square & MASK) << 4 - | (target_square & MASK) << 10) - } -} - -impl Builder { - #[must_use] - pub fn new() -> Self { - Self::default() - } - - #[must_use] - pub fn push(piece: &PlacedPiece) -> Builder { - Builder { - style: Push { - from: Some(piece.square), - to: None, - }, - } - } - - #[must_use] - pub fn double_push(file: File, color: Color) -> Builder { - let (from, to) = match color { - Color::White => ( - Square::from_file_rank(file, Rank::TWO), - Square::from_file_rank(file, Rank::FOUR), - ), - Color::Black => ( - Square::from_file_rank(file, Rank::SEVEN), - Square::from_file_rank(file, Rank::FIVE), - ), - }; - - Builder { - style: DoublePush { from, to }, - } - } - - #[must_use] - pub fn castling(color: Color, wing: Wing) -> Builder { - Builder { - style: Castle { color, wing }, - } - } - - #[must_use] - pub fn capturing_piece(piece: &PlacedPiece, capturing: &PlacedPiece) -> Builder { - Self::push(piece).capturing_piece(capturing) - } - - #[must_use] - pub fn from(&self, square: Square) -> Builder { - Builder { - style: Push { - from: Some(square), - to: None, - }, - } - } - - #[must_use] - pub fn build(&self) -> Move { - Move(0) - } -} - -impl Default for Builder { - fn default() -> Self { - Self { style: Null } - } -} - -impl Builder { - pub fn from(&mut self, square: Square) -> &mut Self { - self.style.from = Some(square); - self - } - - pub fn to(&mut self, square: Square) -> &mut Self { - self.style.to = Some(square); - self - } - - #[must_use] - pub fn capturing_on(&self, square: Square) -> Builder { - let mut style = self.style.clone(); - style.to = Some(square); - - Builder { - style: Capture { - push: style, - capture: Some(square), - }, - } - } - - #[must_use] - pub fn capturing_en_passant_on(&self, target_square: Square) -> Builder { - match EnPassant::from_target_square(target_square) { - Some(en_passant) => { - let mut style = self.style.clone(); - style.to = Some(en_passant.target_square()); - - Builder { - style: EnPassantCapture { - push: style, - capture: Some(en_passant), - }, - } - } - None => todo!(), - } - } - - #[must_use] - pub fn capturing_piece(&self, piece: &PlacedPiece) -> Builder { - Builder { - style: Capture { - push: self.style.clone(), - capture: Some(piece.square), - }, - } - } - - #[must_use] - pub fn promoting_to(&self, shape: PromotionShape) -> Builder> { - Builder { - style: Promotion { - style: self.style.clone(), - promotion: shape, - }, - } - } - - pub fn build(&self) -> Result { - Ok(Move(Kind::Quiet as u16 | self.style.move_bits()?)) - } -} - -impl Builder { - fn bits(&self) -> u16 { - let bits = match self.style.wing { - Wing::KingSide => Kind::KingSideCastle, - Wing::QueenSide => Kind::QueenSideCastle, - }; - - bits as u16 - } - - pub fn build(&self) -> Result { - Ok(Move(self.bits() | self.style.move_bits()?)) - } -} - -impl Builder { - #[must_use] - pub fn promoting_to(self, shape: PromotionShape) -> Builder> { - Builder { - style: Promotion { - style: self.style, - promotion: shape, - }, - } - } - - pub fn build(&self) -> Result { - Ok(Move(Kind::Capture as u16 | self.style.move_bits()?)) - } -} - -impl Builder { - pub fn build(&self) -> Result { - Ok(Move(Kind::DoublePush as u16 | self.style.move_bits()?)) - } -} - -impl Builder { - /// Builds an en passant move. - /// - /// ## Safety - /// - /// This method builds without doing error checking. - #[must_use] - pub unsafe fn build_unchecked(&self) -> Move { - Move(Kind::EnPassantCapture as u16 | self.style.move_bits_unchecked()) - } - - /// Build an en passant move. - /// - /// ## Errors - /// - /// Returns an error if the target or origin squares are invalid. - pub fn build(&self) -> Result { - Ok(Move( - Kind::EnPassantCapture as u16 | self.style.move_bits()?, - )) - } -} - -impl Builder> { - pub fn build(&self) -> Result { - Ok(Move(self.style.move_bits()?)) - } -} - -impl Builder> { - pub fn build(&self) -> Result { - Ok(Move(self.style.move_bits()?)) - } -} diff --git a/moves/src/lib.rs b/moves/src/lib.rs index 4ed188c..1a46667 100644 --- a/moves/src/lib.rs +++ b/moves/src/lib.rs @@ -3,7 +3,6 @@ pub mod generators; pub mod testing; -mod builder; mod defs; mod macros; mod make_move; @@ -11,7 +10,6 @@ mod moves; mod record; mod unmake_move; -pub use builder::{Builder, Error as BuildMoveError, Result as BuildMoveResult}; pub use defs::{Kind, PromotionShape}; pub use generators::GeneratedMove; pub use make_move::{MakeMove, MakeMoveError, MakeMoveResult, ValidateMove}; diff --git a/moves/src/testing.rs b/moves/src/testing.rs index b298d5d..c4ef5a5 100644 --- a/moves/src/testing.rs +++ b/moves/src/testing.rs @@ -1,17 +1,8 @@ // Eryn Wells -use crate::BuildMoveError; - pub type TestResult = Result<(), TestError>; #[derive(Debug, Eq, PartialEq)] pub enum TestError { - BuildMove(BuildMoveError), NoLegalMoves, } - -impl From for TestError { - fn from(value: BuildMoveError) -> Self { - TestError::BuildMove(value) - } -} diff --git a/moves/tests/flags.rs b/moves/tests/flags.rs deleted file mode 100644 index f4d7530..0000000 --- a/moves/tests/flags.rs +++ /dev/null @@ -1,108 +0,0 @@ -// Eryn Wells - -use chessfriend_core::{piece, Color, File, Shape, Square, Wing}; -use chessfriend_moves::{testing::*, Builder, PromotionShape}; - -macro_rules! assert_flag { - ($move:expr, $left:expr, $right:expr, $desc:expr) => { - assert_eq!($left, $right, "{:?} -> {}", $move, stringify!($desc)) - }; -} - -macro_rules! assert_flags { - ($move:expr, $quiet:expr, $double_push:expr, $en_passant:expr, $capture:expr, $castle:expr, $promotion:expr) => { - assert_flag!($move, $move.is_quiet(), $quiet, "is_quiet"); - assert_flag!( - $move, - $move.is_double_push(), - $double_push, - "is_double_push" - ); - assert_flag!($move, $move.is_en_passant(), $en_passant, "is_en_passant"); - assert_flag!($move, $move.is_capture(), $capture, "is_capture"); - assert_flag!($move, $move.is_castle(), $castle, "is_castle"); - assert_flag!($move, $move.is_promotion(), $promotion, "is_promotion"); - }; -} - -#[test] -fn move_flags_quiet() -> TestResult { - let mv = Builder::push(&piece!(White Pawn on A4)) - .to(Square::A5) - .build()?; - assert_flags!(mv, true, false, false, false, false, false); - - Ok(()) -} - -#[test] -fn move_flags_double_push() -> TestResult { - let mv = Builder::double_push(File::C, Color::White).build()?; - assert_flags!(mv, false, true, false, false, false, false); - - Ok(()) -} - -#[test] -fn move_flags_capture() -> TestResult { - let mv = Builder::new() - .from(Square::A4) - .capturing_on(Square::B5) - .build()?; - - assert_flags!(mv, false, false, false, true, false, false); - - Ok(()) -} - -#[test] -fn move_flags_en_passant_capture() -> TestResult { - let ply = Builder::new() - .from(Square::A4) - .capturing_en_passant_on(Square::B3) - .build()?; - - assert!(ply.is_en_passant()); - assert_eq!(ply.origin_square(), Square::A4); - assert_eq!(ply.target_square(), Square::B3); - assert_eq!(ply.capture_square(), Some(Square::B4)); - - Ok(()) -} - -#[test] -fn move_flags_promotion() -> TestResult { - let ply = Builder::push(&piece!(White Pawn on H7)) - .to(Square::H8) - .promoting_to(PromotionShape::Queen) - .build()?; - - assert!(ply.is_promotion()); - assert_eq!(ply.promotion_shape(), Some(Shape::Queen)); - - Ok(()) -} - -#[test] -fn move_flags_capture_promotion() -> TestResult { - let ply = Builder::push(&piece!(White Pawn on H7)) - .to(Square::H8) - .capturing_piece(&piece!(Black Knight on G8)) - .promoting_to(PromotionShape::Queen) - .build()?; - - assert!(ply.is_capture()); - assert!(ply.is_promotion()); - assert_eq!(ply.promotion_shape(), Some(Shape::Queen)); - - Ok(()) -} - -#[test] -fn move_flags_castle() -> TestResult { - let mv = Builder::castling(Color::White, Wing::KingSide).build()?; - - assert_flags!(mv, false, false, false, false, true, false); - - Ok(()) -} diff --git a/moves/tests/pushes.rs b/moves/tests/pushes.rs deleted file mode 100644 index 7406eb0..0000000 --- a/moves/tests/pushes.rs +++ /dev/null @@ -1,17 +0,0 @@ -// Eryn Wells - -use chessfriend_core::{piece, Square}; -use chessfriend_moves::{testing::*, Builder}; - -#[test] -fn pawn_push() -> TestResult { - let mv = Builder::push(&piece!(White Pawn on A3)) - .to(Square::A4) - .build()?; - - assert!(mv.is_quiet()); - assert_eq!(mv.origin_square(), Square::A3); - assert_eq!(mv.target_square(), Square::A4); - - Ok(()) -} diff --git a/position/src/testing.rs b/position/src/testing.rs index c546a79..c5f4491 100644 --- a/position/src/testing.rs +++ b/position/src/testing.rs @@ -1,6 +1,6 @@ // Eryn Wells -use chessfriend_moves::{BuildMoveError, MakeMoveError}; +use chessfriend_moves::MakeMoveError; #[macro_export] macro_rules! assert_eq_bitboards { @@ -19,17 +19,10 @@ pub type TestResult = Result<(), TestError>; #[derive(Debug, Eq, PartialEq)] pub enum TestError { - BuildMove(BuildMoveError), MakeMove(MakeMoveError), NoLegalMoves, } -impl From for TestError { - fn from(value: BuildMoveError) -> Self { - TestError::BuildMove(value) - } -} - impl From for TestError { fn from(value: MakeMoveError) -> Self { TestError::MakeMove(value) From 5326c1ee6aeea96bc7423e343e295f6436d09f32 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 8 Jun 2025 17:16:45 -0700 Subject: [PATCH 362/423] [board] Remove some unused internal PieceSet API and tests --- board/src/piece_sets.rs | 41 ----------------------------------------- 1 file changed, 41 deletions(-) diff --git a/board/src/piece_sets.rs b/board/src/piece_sets.rs index bb6d712..063937d 100644 --- a/board/src/piece_sets.rs +++ b/board/src/piece_sets.rs @@ -138,18 +138,6 @@ impl PieceSet { } } -impl PieceSet { - pub fn color_bitboard(&self, color: Color) -> BitBoard { - self.color_occupancy[color as usize] - } - - pub fn piece_bitboard(&self, piece: Piece) -> BitBoard { - let color_occupancy = self.color_occupancy[piece.color as usize]; - let shape_occupancy = self.shape_occupancy[piece.shape as usize]; - color_occupancy & shape_occupancy - } -} - impl Hash for PieceSet { fn hash(&self, state: &mut H) { self.color_occupancy.hash(state); @@ -163,32 +151,3 @@ impl PartialEq for PieceSet { && self.shape_occupancy == other.shape_occupancy } } - -#[cfg(test)] -mod tests { - use super::*; - use chessfriend_bitboard::bitboard; - - #[test] - fn place_piece() -> Result<(), PlacePieceError> { - let mut pieces = PieceSet::default(); - - pieces.place( - Piece::king(Color::White), - Square::F5, - PlacePieceStrategy::default(), - )?; - - assert_eq!( - pieces.mailbox.get(Square::F5), - Some(Piece::king(Color::White)) - ); - assert_eq!(pieces.color_bitboard(Color::White), bitboard![F5]); - assert_eq!( - pieces.piece_bitboard(Piece::king(Color::White)), - bitboard![F5] - ); - - Ok(()) - } -} From a7d42c6c1dab7c3c5029b50e18bf445334861968 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 8 Jun 2025 17:17:05 -0700 Subject: [PATCH 363/423] [board] Remove unused imports and const variables from zobrist.rs --- board/src/zobrist.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/board/src/zobrist.rs b/board/src/zobrist.rs index 911bd26..b5a5d43 100644 --- a/board/src/zobrist.rs +++ b/board/src/zobrist.rs @@ -10,11 +10,10 @@ //! [1]: https://www.chessprogramming.org/Zobrist_Hashing use crate::{castle, Board}; -use chessfriend_core::{random::RandomNumberGenerator, Color, File, Piece, Shape, Square, Wing}; +use chessfriend_core::{random::RandomNumberGenerator, Color, File, Piece, Shape, Square}; use rand::Fill; use std::sync::Arc; -const NUM_SQUARE_PIECE_VALUES: usize = Shape::NUM * Color::NUM * Square::NUM; const NUM_CASTLING_RIGHTS_VALUES: usize = 16; type HashValue = u64; From ae0ae490935b8c186e50aeac217af8bb969f3ed9 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 8 Jun 2025 17:18:41 -0700 Subject: [PATCH 364/423] [core] Remove PlacedPiece This has been deprecated for a while now. Finally remove it! --- core/src/lib.rs | 2 +- core/src/pieces.rs | 79 ---------------------------------------------- 2 files changed, 1 insertion(+), 80 deletions(-) diff --git a/core/src/lib.rs b/core/src/lib.rs index dcb42a2..d4f5834 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -10,5 +10,5 @@ mod macros; pub use colors::Color; pub use coordinates::{Direction, File, Rank, Square, Wing}; -pub use pieces::{Piece, PlacedPiece}; +pub use pieces::Piece; pub use shapes::{Shape, Slider}; diff --git a/core/src/pieces.rs b/core/src/pieces.rs index a335aa9..f9fb63d 100644 --- a/core/src/pieces.rs +++ b/core/src/pieces.rs @@ -94,85 +94,6 @@ impl std::fmt::Display for Piece { } } -impl From for Piece { - fn from(value: PlacedPiece) -> Self { - value.piece - } -} - -impl From<&PlacedPiece> for Piece { - fn from(value: &PlacedPiece) -> Self { - value.piece - } -} - -#[deprecated] -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub struct PlacedPiece { - pub piece: Piece, - pub square: Square, -} - -macro_rules! is_shape { - ($func_name:ident, $shape:ident) => { - #[must_use] - pub fn $func_name(&self) -> bool { - self.piece().shape == Shape::$shape - } - }; -} - -impl PlacedPiece { - #[must_use] - pub const fn new(piece: Piece, square: Square) -> PlacedPiece { - PlacedPiece { piece, square } - } - - /// The [Piece] itself - #[inline] - #[must_use] - pub fn piece(&self) -> Piece { - self.piece - } - - is_shape!(is_pawn, Pawn); - is_shape!(is_knight, Knight); - is_shape!(is_bishop, Bishop); - is_shape!(is_rook, Rook); - is_shape!(is_queen, Queen); - is_shape!(is_king, King); - - #[must_use] - pub fn is_kingside_rook(&self) -> bool { - self.is_rook() - && match self.piece.color { - Color::White => self.square == Square::H1, - Color::Black => self.square == Square::H8, - } - } - - #[must_use] - pub fn is_queenside_rook(&self) -> bool { - self.is_rook() - && match self.piece.color { - Color::White => self.square == Square::A1, - Color::Black => self.square == Square::A8, - } - } -} - -impl std::fmt::Display for PlacedPiece { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}{}", self.piece, self.square) - } -} - -impl From<(&Square, &Piece)> for PlacedPiece { - fn from(value: (&Square, &Piece)) -> Self { - PlacedPiece::new(*value.1, *value.0) - } -} - #[cfg(test)] mod tests { use super::*; From 0167794346336b83aebfe7c794eeb85f2fc34b25 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 8 Jun 2025 17:19:00 -0700 Subject: [PATCH 365/423] [perft] A small Perft program --- Cargo.lock | 11 +++++++++ Cargo.toml | 2 +- perft/Cargo.toml | 11 +++++++++ perft/src/main.rs | 57 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 perft/Cargo.toml create mode 100644 perft/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index e58f1e8..c6c6f1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -295,6 +295,17 @@ dependencies = [ "libc", ] +[[package]] +name = "perft" +version = "0.1.0" +dependencies = [ + "anyhow", + "chessfriend_board", + "chessfriend_position", + "clap", + "thiserror", +] + [[package]] name = "ppv-lite86" version = "0.2.21" diff --git a/Cargo.toml b/Cargo.toml index 70558c7..3b6ebee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = [ "board", "core", "explorer", - "moves", + "moves", "perft", "position", ] resolver = "2" diff --git a/perft/Cargo.toml b/perft/Cargo.toml new file mode 100644 index 0000000..193bb43 --- /dev/null +++ b/perft/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "perft" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1.0.98" +chessfriend_board = { path = "../board" } +chessfriend_position = { path = "../position" } +clap = { version = "4.4.12", features = ["derive"] } +thiserror = "2" diff --git a/perft/src/main.rs b/perft/src/main.rs new file mode 100644 index 0000000..e113579 --- /dev/null +++ b/perft/src/main.rs @@ -0,0 +1,57 @@ +use chessfriend_position::{fen::FromFenStr, GeneratedMove, Position, ValidateMove}; +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(name = "Perft")] +struct Arguments { + depth: usize, + + #[arg(long, short, value_name = "FEN")] + fen: Option, +} + +trait Perft { + fn perft(&mut self, depth: usize) -> u64; +} + +impl Perft for Position { + fn perft(&mut self, depth: usize) -> u64 { + if depth == 0 { + return 1; + } + + let mut nodes_counted: u64 = 0; + + let legal_moves: Vec = self.all_legal_moves(None).collect(); + + for generated_ply in legal_moves { + self.make_move(generated_ply.into(), ValidateMove::No) + .expect("unable to make generated move"); + + nodes_counted += self.perft(depth - 1); + + self.unmake_last_move().expect("unable to unmake last move"); + } + + nodes_counted + } +} + +fn main() -> anyhow::Result<()> { + let args = Arguments::parse(); + let depth = args.depth; + + println!("Searching to depth {depth}"); + + let mut position = if let Some(fen) = args.fen { + Position::from_fen_str(&fen)? + } else { + Position::starting(None) + }; + + let nodes_searched = position.perft(depth); + + println!("Nodes searched: {nodes_searched}"); + + Ok(()) +} From 4b148529a17367d2a0d85016d8326db93058b0fc Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 8 Jun 2025 17:34:42 -0700 Subject: [PATCH 366/423] [bitboard] Fix the warning about shared references to mutable static data I've lived with this warning for a long time because I didn't really understand it. ``` warning: `chessfriend_core` (lib) generated 1 warning (run `cargo fix --lib -p chessfriend_core` to apply 1 suggestion) warning: creating a shared reference to mutable static is discouraged --> bitboard/src/library.rs:66:9 ``` I was able to fix this by creating a new type with a single OnceLock attribute. The OnceLock acts as a cell, making it mutable, even if self is not. So, you can declare the MoveLibraryWrapper non-mutable static, but still initialize the library inside the Cell. --- bitboard/src/library.rs | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/bitboard/src/library.rs b/bitboard/src/library.rs index c878f8f..6a60392 100644 --- a/bitboard/src/library.rs +++ b/bitboard/src/library.rs @@ -60,16 +60,8 @@ pub(crate) const LIGHT_SQUARES: BitBoard = pub(crate) const DARK_SQUARES: BitBoard = BitBoard(!LIGHT_SQUARES.0); pub(super) fn library() -> &'static MoveLibrary { - static mut MOVE_LIBRARY: OnceLock = OnceLock::new(); - - unsafe { - MOVE_LIBRARY.get_or_init(|| { - let mut library = MoveLibrary::new(); - library.init(); - - library - }) - } + static MOVE_LIBRARY: MoveLibraryWrapper = MoveLibraryWrapper::new(); + MOVE_LIBRARY.library() } macro_rules! library_getter { @@ -80,6 +72,26 @@ macro_rules! library_getter { }; } +struct MoveLibraryWrapper { + library: OnceLock, +} + +impl MoveLibraryWrapper { + const fn new() -> Self { + Self { + library: OnceLock::::new(), + } + } + + fn library(&self) -> &MoveLibrary { + self.library.get_or_init(|| { + let mut library = MoveLibrary::new(); + library.init(); + library + }) + } +} + #[derive(Debug)] pub(super) struct MoveLibrary { // Rays From 659ffb6130050f10d4f3550d41ad8744661c37c3 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 8 Jun 2025 17:34:52 -0700 Subject: [PATCH 367/423] [core] Remove an unused import --- core/src/pieces.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/pieces.rs b/core/src/pieces.rs index f9fb63d..effab94 100644 --- a/core/src/pieces.rs +++ b/core/src/pieces.rs @@ -4,7 +4,7 @@ mod display; pub use self::display::{PieceDisplay, PieceDisplayStyle}; -use crate::{Color, Shape, Square}; +use crate::{Color, Shape}; #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct Piece { From 9815a63ebbf8e59ca3368aa702759ccbdeefc7b7 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 11 Jun 2025 08:15:06 -0700 Subject: [PATCH 368/423] [explorer] Print some question marks if a move is generated without target/origin squares The move I observed in my testing was a castling move, which doesn't set target and origin squares because those are provided by the castling parameters struct from the board crate. Fix the missing squares on castle moves by looking up castling parameters and populating them. This requires Move::castle() to take a Color in addition to the Wing. Update all the call sites. --- board/src/castle/parameters.rs | 2 +- explorer/src/main.rs | 8 ++++++-- moves/src/generators/king.rs | 6 ++++-- moves/src/make_move.rs | 4 ++-- moves/src/moves.rs | 12 +++++++++--- moves/src/unmake_move.rs | 11 +++++------ 6 files changed, 27 insertions(+), 16 deletions(-) diff --git a/board/src/castle/parameters.rs b/board/src/castle/parameters.rs index 54c3512..1052ab6 100644 --- a/board/src/castle/parameters.rs +++ b/board/src/castle/parameters.rs @@ -82,7 +82,7 @@ impl Parameters { ]; #[must_use] - pub fn get(color: Color, wing: Wing) -> &'static Parameters { + pub const fn get(color: Color, wing: Wing) -> &'static Parameters { &Self::BY_COLOR[color as usize][wing as usize] } } diff --git a/explorer/src/main.rs b/explorer/src/main.rs index 2e7a545..5b5c6ed 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -234,8 +234,12 @@ fn do_moves_command( let formatted_moves: Vec = moves .iter() .map(|ply| { - let piece = state.position.get_piece(ply.origin()).unwrap(); - format!("{piece}{ply}") + let origin = ply.origin(); + if let Some(piece) = state.position.get_piece(origin) { + format!("{piece}{ply}") + } else { + format!("{ply}??") + } }) .collect(); diff --git a/moves/src/generators/king.rs b/moves/src/generators/king.rs index aa7eea0..864c07d 100644 --- a/moves/src/generators/king.rs +++ b/moves/src/generators/king.rs @@ -105,6 +105,7 @@ impl FusedIterator for KingIterator {} #[derive(Clone, Debug)] struct CastleIterator { + color: Color, kingside: Option<&'static CastleParameters>, queenside: Option<&'static CastleParameters>, } @@ -115,6 +116,7 @@ impl CastleIterator { let queenside = board.color_can_castle(Wing::QueenSide, Some(color)).ok(); Self { + color, kingside, queenside, } @@ -126,11 +128,11 @@ impl Iterator for CastleIterator { fn next(&mut self) -> Option { if let Some(_parameters) = self.kingside.take() { - return Some(Move::castle(Wing::KingSide).into()); + return Some(Move::castle(self.color, Wing::KingSide).into()); } if let Some(_parameters) = self.queenside.take() { - return Some(Move::castle(Wing::QueenSide).into()); + return Some(Move::castle(self.color, Wing::QueenSide).into()); } None diff --git a/moves/src/make_move.rs b/moves/src/make_move.rs index 59a881e..bfb7d46 100644 --- a/moves/src/make_move.rs +++ b/moves/src/make_move.rs @@ -520,7 +520,7 @@ mod tests { White King on E1, ]; - let ply = Move::castle(Wing::KingSide); + let ply = Move::castle(Color::White, Wing::KingSide); board.make_move(ply, ValidateMove::Yes)?; assert_eq!(board.active_color(), Color::Black); @@ -540,7 +540,7 @@ mod tests { White Rook on A1, ]; - let ply = Move::castle(Wing::QueenSide); + let ply = Move::castle(Color::White, Wing::QueenSide); board.make_move(ply, ValidateMove::Yes)?; assert_eq!(board.active_color(), Color::Black); diff --git a/moves/src/moves.rs b/moves/src/moves.rs index c1fda0c..31226b5 100644 --- a/moves/src/moves.rs +++ b/moves/src/moves.rs @@ -2,7 +2,8 @@ use crate::defs::{Kind, PromotionShape}; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Rank, Shape, Square, Wing}; +use chessfriend_board::CastleParameters; +use chessfriend_core::{Color, Rank, Shape, Square, Wing}; use std::fmt; #[macro_export] @@ -109,12 +110,17 @@ impl Move { } #[must_use] - pub fn castle(wing: Wing) -> Self { + pub fn castle(color: Color, wing: Wing) -> Self { let flag_bits = match wing { Wing::KingSide => Kind::KingSideCastle, Wing::QueenSide => Kind::QueenSideCastle, } as u16; - Move(flag_bits) + + let parameters = CastleParameters::get(color, wing); + let origin = parameters.origin.king; + let target = parameters.target.king; + + Move(origin_bits(origin) | target_bits(target) | flag_bits) } } diff --git a/moves/src/unmake_move.rs b/moves/src/unmake_move.rs index 7fd3fec..2c6ebed 100644 --- a/moves/src/unmake_move.rs +++ b/moves/src/unmake_move.rs @@ -398,7 +398,7 @@ mod tests { let original_castling_rights = board.castling_rights(); - let ply = Move::castle(Wing::KingSide); + let ply = Move::castle(Color::White, Wing::KingSide); let record = board.make_move(ply, ValidateMove::Yes)?; // Verify castle was made @@ -430,7 +430,7 @@ mod tests { let original_castling_rights = board.castling_rights(); - let ply = Move::castle(Wing::QueenSide); + let ply = Move::castle(Color::White, Wing::QueenSide); let record = board.make_move(ply, ValidateMove::Yes)?; // Verify castle was made @@ -455,15 +455,14 @@ mod tests { #[test] fn unmake_black_kingside_castle() -> TestResult { - let mut board = test_board![ + let mut board = test_board!(Black, [ Black King on E8, Black Rook on H8, - ]; - board.set_active_color(Color::Black); + ]); let original_castling_rights = board.castling_rights(); - let ply = Move::castle(Wing::KingSide); + let ply = Move::castle(Color::Black, Wing::KingSide); let record = board.make_move(ply, ValidateMove::Yes)?; // Verify castle was made From 3c31f900eaf249c5c8865b06fe33081de7c09361 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 11 Jun 2025 08:16:37 -0700 Subject: [PATCH 369/423] [perft] A script that checks a list of positions against known node counts A small Python script that reads a JSON list of positions and their known Perft node counts to a certain depth, then invokes the Perft program for each position and validates the output. Peter Ellis Jones shared such a JSON list on GitHub. Import that file. --- .../data/peterellisjones-perft-positions.json | 117 ++++++++++++++++++ perft/scripts/check-positions | 80 ++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 perft/data/peterellisjones-perft-positions.json create mode 100755 perft/scripts/check-positions diff --git a/perft/data/peterellisjones-perft-positions.json b/perft/data/peterellisjones-perft-positions.json new file mode 100644 index 0000000..eb48f34 --- /dev/null +++ b/perft/data/peterellisjones-perft-positions.json @@ -0,0 +1,117 @@ +[ + { + "depth":1, + "nodes":8, + "fen":"r6r/1b2k1bq/8/8/7B/8/8/R3K2R b KQ - 3 2" + }, + { + "depth":1, + "nodes":8, + "fen":"8/8/8/2k5/2pP4/8/B7/4K3 b - d3 0 3" + }, + { + "depth":1, + "nodes":19, + "fen":"r1bqkbnr/pppppppp/n7/8/8/P7/1PPPPPPP/RNBQKBNR w KQkq - 2 2" + }, + { + "depth":1, + "nodes":5, + "fen":"r3k2r/p1pp1pb1/bn2Qnp1/2qPN3/1p2P3/2N5/PPPBBPPP/R3K2R b KQkq - 3 2" + }, + { + "depth":1, + "nodes":44, + "fen":"2kr3r/p1ppqpb1/bn2Qnp1/3PN3/1p2P3/2N5/PPPBBPPP/R3K2R b KQ - 3 2" + }, + { + "depth":1, + "nodes":39, + "fen":"rnb2k1r/pp1Pbppp/2p5/q7/2B5/8/PPPQNnPP/RNB1K2R w KQ - 3 9" + }, + { + "depth":1, + "nodes":9, + "fen":"2r5/3pk3/8/2P5/8/2K5/8/8 w - - 5 4" + }, + { + "depth":3, + "nodes":62379, + "fen":"rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8" + }, + { + "depth":3, + "nodes":89890, + "fen":"r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10" + }, + { + "depth":6, + "nodes":1134888, + "fen":"3k4/3p4/8/K1P4r/8/8/8/8 b - - 0 1" + }, + { + "depth":6, + "nodes":1015133, + "fen":"8/8/4k3/8/2p5/8/B2P2K1/8 w - - 0 1" + }, + { + "depth":6, + "nodes":1440467, + "fen":"8/8/1k6/2b5/2pP4/8/5K2/8 b - d3 0 1" + }, + { + "depth":6, + "nodes":661072, + "fen":"5k2/8/8/8/8/8/8/4K2R w K - 0 1" + }, + { + "depth":6, + "nodes":803711, + "fen":"3k4/8/8/8/8/8/8/R3K3 w Q - 0 1" + }, + { + "depth":4, + "nodes":1274206, + "fen":"r3k2r/1b4bq/8/8/8/8/7B/R3K2R w KQkq - 0 1" + }, + { + "depth":4, + "nodes":1720476, + "fen":"r3k2r/8/3Q4/8/8/5q2/8/R3K2R b KQkq - 0 1" + }, + { + "depth":6, + "nodes":3821001, + "fen":"2K2r2/4P3/8/8/8/8/8/3k4 w - - 0 1" + }, + { + "depth":5, + "nodes":1004658, + "fen":"8/8/1P2K3/8/2n5/1q6/8/5k2 b - - 0 1" + }, + { + "depth":6, + "nodes":217342, + "fen":"4k3/1P6/8/8/8/8/K7/8 w - - 0 1" + }, + { + "depth":6, + "nodes":92683, + "fen":"8/P1k5/K7/8/8/8/8/8 w - - 0 1" + }, + { + "depth":6, + "nodes":2217, + "fen":"K1k5/8/P7/8/8/8/8/8 w - - 0 1" + }, + { + "depth":7, + "nodes":567584, + "fen":"8/k1P5/8/1K6/8/8/8/8 w - - 0 1" + }, + { + "depth":4, + "nodes":23527, + "fen":"8/8/2k5/5q2/5n2/8/5K2/8 b - - 0 1" + } +] \ No newline at end of file diff --git a/perft/scripts/check-positions b/perft/scripts/check-positions new file mode 100755 index 0000000..8258567 --- /dev/null +++ b/perft/scripts/check-positions @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +# Eryn Wells + +''' +New script. +''' + +import argparse +import json +import subprocess + + +def run_perft(fen, depth, expected_nodes_count): + result = subprocess.run( + [ + 'cargo', + 'run', + '--', + '--fen', fen, + str(depth) + ], + capture_output=True, + check=True, + text=True + ) + + nodes_count = 0 + for line in result.stdout.splitlines(): + if line.startswith('nodes='): + (_, nodes_count) = line.split('=') + nodes_count = int(nodes_count) + + return nodes_count + + +def parse_args(argv, *a, **kw): + parser = argparse.ArgumentParser(*a, **kw) + parser.add_argument('-c', '--continue', dest='should_continue', + action='store_true') + parser.add_argument('file') + args = parser.parse_args(argv) + return args + + +def main(argv): + args = parse_args(argv[1:], prog=argv[0]) + + items = [] + with open(args.file, 'r') as f: + items = json.load(f) + + should_continue = args.should_continue + + for item in items: + fen = item['fen'] + depth = item['depth'] + expected_nodes_count = int(item['nodes']) + + print('---') + print(f'fen={fen}') + print(f'expected-nodes={expected_nodes_count}') + + nodes_count = run_perft(fen, depth, expected_nodes_count) + print(f'nodes={nodes_count}') + + did_pass = nodes_count == expected_nodes_count + if did_pass: + print('result=PASS') + else: + print('result=FAIL') + + if not did_pass and not should_continue: + return -1 + + return 0 + + +if __name__ == '__main__': + import sys + sys.exit(main(sys.argv)) From 6a8cc0dc5b2edf1de89f3b4ee1333cc1593e9772 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 12 Jun 2025 17:37:53 -0700 Subject: [PATCH 370/423] [position] Remove unused CapturesList::last() --- position/src/position/captures.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/position/src/position/captures.rs b/position/src/position/captures.rs index f879c5b..4c7f4bb 100644 --- a/position/src/position/captures.rs +++ b/position/src/position/captures.rs @@ -8,11 +8,6 @@ use chessfriend_core::{Color, Piece}; pub(crate) struct CapturesList([Vec; Color::NUM]); impl CapturesList { - #[cfg(test)] - pub fn last(&self, color: Color) -> Option<&Piece> { - self.0[color as usize].last() - } - pub fn push(&mut self, color: Color, piece: Piece) { self.0[color as usize].push(piece); } From e9cea33c20e350f211990324025f7be8ac03e9c8 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 12 Jun 2025 17:41:31 -0700 Subject: [PATCH 371/423] [moves] Update ply! to take a color to the castle move This supports castle moves having target and origin squares. --- moves/src/generators/king.rs | 4 ++-- moves/src/moves.rs | 14 ++++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/moves/src/generators/king.rs b/moves/src/generators/king.rs index 864c07d..ed6458b 100644 --- a/moves/src/generators/king.rs +++ b/moves/src/generators/king.rs @@ -258,7 +258,7 @@ mod tests { ply!(E1 - E2), ply!(E1 - F1), ply!(E1 - F2), - ply!(0 - 0), + ply!(White 0-0), ] ); } @@ -279,7 +279,7 @@ mod tests { ply!(E1 - E2), ply!(E1 - F1), ply!(E1 - F2), - ply!(0 - 0 - 0), + ply!(White 0-0-0), ] ); } diff --git a/moves/src/moves.rs b/moves/src/moves.rs index 31226b5..27d8569 100644 --- a/moves/src/moves.rs +++ b/moves/src/moves.rs @@ -46,11 +46,17 @@ macro_rules! ply { $crate::PromotionShape::$promotion, ) }; - (0-0) => { - $crate::Move::castle(chessfriend_core::Wing::KingSide) + ($color:ident 0-0) => { + $crate::Move::castle( + chessfriend_core::Color::$color, + chessfriend_core::Wing::KingSide, + ) }; - (0-0-0) => { - $crate::Move::castle(chessfriend_core::Wing::QueenSide) + ($color:ident 0-0-0) => { + $crate::Move::castle( + chessfriend_core::Color::$color, + chessfriend_core::Wing::QueenSide, + ) }; } From df49a4950b4dd8aa25c35ecd0e9d9c96d87bf623 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 13 Jun 2025 11:27:10 -0700 Subject: [PATCH 372/423] [moves] Implement GeneratedMove::ply() Returns a copy of the generated move. This is simplier to use than ::into(). --- moves/src/generators.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/moves/src/generators.rs b/moves/src/generators.rs index bf8121d..8fbecce 100644 --- a/moves/src/generators.rs +++ b/moves/src/generators.rs @@ -30,6 +30,11 @@ impl GeneratedMove { pub fn target(&self) -> Square { self.ply.target_square() } + + #[must_use] + pub fn ply(&self) -> Move { + self.ply + } } impl std::fmt::Display for GeneratedMove { From fb182e7ac0160d45ffe4349dedd7d049f6bb6312 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 15 Jun 2025 16:24:19 -0700 Subject: [PATCH 373/423] [perft] Print the depth of the position being checked in check-positions --- perft/scripts/check-positions | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/perft/scripts/check-positions b/perft/scripts/check-positions index 8258567..e5f0626 100755 --- a/perft/scripts/check-positions +++ b/perft/scripts/check-positions @@ -58,6 +58,7 @@ def main(argv): print('---') print(f'fen={fen}') + print(f'depth={depth}') print(f'expected-nodes={expected_nodes_count}') nodes_count = run_perft(fen, depth, expected_nodes_count) @@ -71,8 +72,8 @@ def main(argv): if not did_pass and not should_continue: return -1 - - return 0 + + return 0 if __name__ == '__main__': From 3951af76cbb6213e15f386177169e49cb18b9814 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 16 Jun 2025 08:57:48 -0700 Subject: [PATCH 374/423] [core, moves, position] Implement parsing long algebraic moves UCI uses a move format it calls "long algebraic". They look like either "e2e4" for a regular move, or "h7h8q" for a promotion. Implement parsing these move strings as a two step process. First define an AlgebraicMoveComponents struct in the moves crate that implements FromStr. This struct reads out an origin square, a target square, and an optional promotion shape from a string. Then, implement a pair of methods on Position that take the move components struct and return a fully encoded Move struct with them. This process is required because the algebraic string is not enough by itself to know what kind of move was made. The current position is required to understand that. Implement Shape::is_promotable(). Add a NULL move to the Move struct. I'm not sure what this is used for yet, but the UCI spec specifically calls out a string that encodes a null move, so I added it. It may end up being unused! Do a little bit of cleanup in the core crate as well. Use deeper imports (import std::fmt instead of requring the fully qualified type path) and remove some unnecessary From implementations. This commit is also the first instance (I think) of defining an errors module in lib.rs for the core crate that holds the various error types the crate exports. --- core/src/lib.rs | 5 ++ core/src/pieces.rs | 5 +- core/src/shapes.rs | 36 +++++------ moves/src/algebraic.rs | 134 +++++++++++++++++++++++++++++++++++++++ moves/src/generators.rs | 7 +- moves/src/lib.rs | 1 + moves/src/moves.rs | 5 ++ position/src/position.rs | 63 ++++++++++++++++++ 8 files changed, 232 insertions(+), 24 deletions(-) create mode 100644 moves/src/algebraic.rs diff --git a/core/src/lib.rs b/core/src/lib.rs index d4f5834..0638410 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -12,3 +12,8 @@ pub use colors::Color; pub use coordinates::{Direction, File, Rank, Square, Wing}; pub use pieces::Piece; pub use shapes::{Shape, Slider}; + +pub mod errors { + pub use crate::coordinates::ParseSquareError; + pub use crate::shapes::ParseShapeError; +} diff --git a/core/src/pieces.rs b/core/src/pieces.rs index effab94..5291a4d 100644 --- a/core/src/pieces.rs +++ b/core/src/pieces.rs @@ -99,9 +99,8 @@ mod tests { use super::*; #[test] - fn shape_try_from() { - assert_eq!(Shape::try_from('p'), Ok(Shape::Pawn)); - assert_eq!(Shape::try_from("p"), Ok(Shape::Pawn)); + fn parse_shape() { + assert_eq!("p".parse(), Ok(Shape::Pawn)); } #[test] diff --git a/core/src/shapes.rs b/core/src/shapes.rs index fe549f6..2c6b7e9 100644 --- a/core/src/shapes.rs +++ b/core/src/shapes.rs @@ -1,6 +1,6 @@ // Eryn Wells -use std::{array, slice}; +use std::{array, fmt, slice, str::FromStr}; use thiserror::Error; #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] @@ -27,6 +27,8 @@ impl Shape { Shape::King, ]; + const PROMOTABLE_SHAPES: [Shape; 4] = [Shape::Queen, Shape::Rook, Shape::Bishop, Shape::Knight]; + pub fn iter() -> slice::Iter<'static, Self> { Shape::ALL.iter() } @@ -38,10 +40,7 @@ impl Shape { /// An iterator over the shapes that a pawn can promote to pub fn promotable() -> slice::Iter<'static, Shape> { - const PROMOTABLE_SHAPES: [Shape; 4] = - [Shape::Queen, Shape::Rook, Shape::Bishop, Shape::Knight]; - - PROMOTABLE_SHAPES.iter() + Self::PROMOTABLE_SHAPES.iter() } #[must_use] @@ -67,6 +66,11 @@ impl Shape { Shape::King => "king", } } + + #[must_use] + pub fn is_promotable(&self) -> bool { + Self::PROMOTABLE_SHAPES.contains(self) + } } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] @@ -121,32 +125,24 @@ impl TryFrom for Shape { #[derive(Clone, Copy, Debug, Error, Eq, PartialEq)] #[error("no matching piece shape for string")] -pub struct ShapeFromStrError; +pub struct ParseShapeError; -impl TryFrom<&str> for Shape { - type Error = ShapeFromStrError; +impl FromStr for Shape { + type Err = ParseShapeError; - fn try_from(value: &str) -> Result { - match value.to_lowercase().as_str() { + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { "p" | "pawn" => Ok(Shape::Pawn), "n" | "knight" => Ok(Shape::Knight), "b" | "bishop" => Ok(Shape::Bishop), "r" | "rook" => Ok(Shape::Rook), "q" | "queen" => Ok(Shape::Queen), "k" | "king" => Ok(Shape::King), - _ => Err(ShapeFromStrError), + _ => Err(ParseShapeError), } } } -impl std::str::FromStr for Shape { - type Err = ShapeFromStrError; - - fn from_str(s: &str) -> Result { - Self::try_from(s) - } -} - impl From<&Shape> for char { fn from(shape: &Shape) -> char { char::from(*shape) @@ -159,7 +155,7 @@ impl From for char { } } -impl std::fmt::Display for Shape { +impl fmt::Display for Shape { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let self_char: char = self.into(); write!(f, "{self_char}") diff --git a/moves/src/algebraic.rs b/moves/src/algebraic.rs new file mode 100644 index 0000000..f1c5936 --- /dev/null +++ b/moves/src/algebraic.rs @@ -0,0 +1,134 @@ +// Eryn Wells + +use chessfriend_core::{ + errors::{ParseShapeError, ParseSquareError}, + Shape, Square, +}; +use std::str::FromStr; +use thiserror::Error; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum AlgebraicMoveComponents { + /// An empty move + Null, + + Regular { + origin: Square, + target: Square, + promotion: Option, + }, +} + +#[derive(Clone, Debug, Error)] +pub enum ParseAlgebraicMoveComponentsError { + #[error("string not long enough to contain valid move")] + InsufficientLength, + + #[error("{0}")] + ParseSquareError(#[from] ParseSquareError), + + #[error("{0}")] + ParseShapeError(#[from] ParseShapeError), + + #[error("{0} is not a promotable shape")] + InvalidPromotionShape(Shape), +} + +impl FromStr for AlgebraicMoveComponents { + type Err = ParseAlgebraicMoveComponentsError; + + fn from_str(s: &str) -> Result { + let s = s.trim(); + + if s == "0000" { + return Ok(Self::Null); + } + + if s.len() < 4 { + return Err(ParseAlgebraicMoveComponentsError::InsufficientLength); + } + + let s = s.to_lowercase(); + let (origin_string, s) = s.split_at(2); + + let s = s.trim_start(); + let (target_string, s) = s.split_at(2); + + let promotion = if s.len() >= 1 { + let s = s.trim_start(); + let (promotion_string, _s) = s.split_at(1); + + Some(promotion_string) + } else { + None + }; + + let origin: Square = origin_string.parse()?; + let target: Square = target_string.parse()?; + + let promotion = if let Some(promotion) = promotion { + let promotion: Shape = promotion.parse()?; + if promotion.is_promotable() { + Some(promotion) + } else { + return Err(ParseAlgebraicMoveComponentsError::InvalidPromotionShape( + promotion, + )); + } + } else { + None + }; + + Ok(AlgebraicMoveComponents::Regular { + origin, + target, + promotion, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_null_move() -> Result<(), ParseAlgebraicMoveComponentsError> { + let components: AlgebraicMoveComponents = "0000".parse()?; + + assert_eq!(components, AlgebraicMoveComponents::Null); + + Ok(()) + } + + #[test] + fn parse_origin_target_move() -> Result<(), ParseAlgebraicMoveComponentsError> { + let components: AlgebraicMoveComponents = "e2e4".parse()?; + + assert_eq!( + components, + AlgebraicMoveComponents::Regular { + origin: Square::E2, + target: Square::E4, + promotion: None + } + ); + + Ok(()) + } + + #[test] + fn parse_origin_target_promotion_move() -> Result<(), ParseAlgebraicMoveComponentsError> { + let components: AlgebraicMoveComponents = "h7h8q".parse()?; + + assert_eq!( + components, + AlgebraicMoveComponents::Regular { + origin: Square::H7, + target: Square::H8, + promotion: Some(Shape::Queen), + } + ); + + Ok(()) + } +} diff --git a/moves/src/generators.rs b/moves/src/generators.rs index 8fbecce..04e6a56 100644 --- a/moves/src/generators.rs +++ b/moves/src/generators.rs @@ -13,7 +13,7 @@ pub use pawn::PawnMoveGenerator; pub use slider::{BishopMoveGenerator, QueenMoveGenerator, RookMoveGenerator}; use crate::Move; -use chessfriend_core::Square; +use chessfriend_core::{Shape, Square}; #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct GeneratedMove { @@ -31,6 +31,11 @@ impl GeneratedMove { self.ply.target_square() } + #[must_use] + pub fn promotion_shape(&self) -> Option { + self.ply.promotion_shape() + } + #[must_use] pub fn ply(&self) -> Move { self.ply diff --git a/moves/src/lib.rs b/moves/src/lib.rs index 1a46667..69d1d54 100644 --- a/moves/src/lib.rs +++ b/moves/src/lib.rs @@ -1,5 +1,6 @@ // Eryn Wells +pub mod algebraic; pub mod generators; pub mod testing; diff --git a/moves/src/moves.rs b/moves/src/moves.rs index 27d8569..db5636b 100644 --- a/moves/src/moves.rs +++ b/moves/src/moves.rs @@ -78,6 +78,11 @@ fn target_bits(square: Square) -> u16 { } impl Move { + #[must_use] + pub const fn null() -> Self { + Move(0) + } + #[must_use] pub fn quiet(origin: Square, target: Square) -> Self { Move(origin_bits(origin) | target_bits(target)) diff --git a/position/src/position.rs b/position/src/position.rs index 610834e..ff95bcd 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -11,6 +11,7 @@ use chessfriend_board::{ }; use chessfriend_core::{Color, Piece, Shape, Square}; use chessfriend_moves::{ + algebraic::AlgebraicMoveComponents, generators::{ AllPiecesMoveGenerator, BishopMoveGenerator, KingMoveGenerator, KnightMoveGenerator, PawnMoveGenerator, QueenMoveGenerator, RookMoveGenerator, @@ -222,6 +223,68 @@ impl Position { unmake_result } + + /// Build a move given its origin, target, and possible promotion. Perform + /// some minimal validation. If a move cannot be + #[must_use] + pub fn move_from_algebraic_components( + &self, + components: AlgebraicMoveComponents, + ) -> Option { + match components { + AlgebraicMoveComponents::Null => Some(Move::null()), + AlgebraicMoveComponents::Regular { + origin, + target, + promotion, + } => self.move_from_origin_target(origin, target, promotion), + } + } + + fn move_from_origin_target( + &self, + origin: Square, + target: Square, + promotion: Option, + ) -> Option { + let piece = self.get_piece(origin)?; + + let color = piece.color; + + // Pawn and King are the two most interesting shapes here, because of en + // passant, castling and so on. So, let the move generators do their + // thing and find the move that fits the parameters. For the rest of the + // pieces, do something a little more streamlined. + + match piece.shape { + Shape::Pawn => PawnMoveGenerator::new(&self.board, None) + .find(|ply| { + ply.origin() == origin + && ply.target() == target + && ply.promotion_shape() == promotion + }) + .map(std::convert::Into::into), + Shape::King => KingMoveGenerator::new(&self.board, None) + .find(|ply| ply.origin() == origin && ply.target() == target) + .map(std::convert::Into::into), + _ => { + if color != self.board.active_color() { + return None; + } + + let target_bitboard: BitBoard = target.into(); + if !(self.movement(origin) & target_bitboard).is_populated() { + return None; + } + + if self.get_piece(target).is_some() { + return Some(Move::capture(origin, target)); + } + + Some(Move::quiet(origin, target)) + } + } + } } impl Position { From f0b6cb5f08189e6308c41a83f810c7773a135268 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 16 Jun 2025 08:58:22 -0700 Subject: [PATCH 375/423] [core] Do a little cleanup in core::coordinates Import std::fmt and remove some commented out code. --- core/src/coordinates.rs | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/core/src/coordinates.rs b/core/src/coordinates.rs index aceec8c..99a252e 100644 --- a/core/src/coordinates.rs +++ b/core/src/coordinates.rs @@ -5,7 +5,7 @@ mod wings; pub use wings::Wing; use crate::Color; -use std::fmt; +use std::{fmt, str::FromStr}; use thiserror::Error; macro_rules! try_from_integer { @@ -254,19 +254,6 @@ to_square_enum!( } ); -// impl TryFrom for EnPassantTargetSquare { -// type Error = (); - -// fn try_from(value: Square) -> Result { -// let square = Self::ALL[value as usize]; -// if square as usize == value as usize { -// Ok(square) -// } else { -// Err(()) -// } -// } -// } - impl Square { /// # Safety /// @@ -391,7 +378,7 @@ impl TryFrom<&str> for Square { } } -impl std::str::FromStr for Square { +impl FromStr for Square { type Err = ParseSquareError; fn from_str(s: &str) -> Result { @@ -415,7 +402,7 @@ impl std::str::FromStr for Square { #[error("invalid rank")] pub struct ParseRankError; -impl std::str::FromStr for Rank { +impl FromStr for Rank { type Err = ParseRankError; fn from_str(s: &str) -> Result { @@ -438,7 +425,7 @@ impl std::str::FromStr for Rank { #[error("invalid file")] pub struct ParseFileError; -impl std::str::FromStr for File { +impl FromStr for File { type Err = ParseFileError; fn from_str(s: &str) -> Result { From 7744dd06f0414e8dde0b2887b1850a5a2ea743c7 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 16 Jun 2025 09:01:58 -0700 Subject: [PATCH 376/423] [position, perft] Move Perft into the position crate Move the Perft trait into the position crate, and let the perft binary call into that. Amend Position::make_move to return a bool in the Ok case that indicates whether the position has been seen before. Use this to decide whether to continue recursing during the Perft run. I haven't seen that this makes a difference in the counts returned by Perft yet. --- perft/src/main.rs | 33 ++------------------ position/src/lib.rs | 1 + position/src/perft.rs | 67 ++++++++++++++++++++++++++++++++++++++++ position/src/position.rs | 20 ++++++------ 4 files changed, 82 insertions(+), 39 deletions(-) create mode 100644 position/src/perft.rs diff --git a/perft/src/main.rs b/perft/src/main.rs index e113579..a311656 100644 --- a/perft/src/main.rs +++ b/perft/src/main.rs @@ -1,4 +1,4 @@ -use chessfriend_position::{fen::FromFenStr, GeneratedMove, Position, ValidateMove}; +use chessfriend_position::{fen::FromFenStr, perft::Perft, Position}; use clap::Parser; #[derive(Parser, Debug)] @@ -10,38 +10,11 @@ struct Arguments { fen: Option, } -trait Perft { - fn perft(&mut self, depth: usize) -> u64; -} - -impl Perft for Position { - fn perft(&mut self, depth: usize) -> u64 { - if depth == 0 { - return 1; - } - - let mut nodes_counted: u64 = 0; - - let legal_moves: Vec = self.all_legal_moves(None).collect(); - - for generated_ply in legal_moves { - self.make_move(generated_ply.into(), ValidateMove::No) - .expect("unable to make generated move"); - - nodes_counted += self.perft(depth - 1); - - self.unmake_last_move().expect("unable to unmake last move"); - } - - nodes_counted - } -} - fn main() -> anyhow::Result<()> { let args = Arguments::parse(); let depth = args.depth; - println!("Searching to depth {depth}"); + println!("depth={depth}"); let mut position = if let Some(fen) = args.fen { Position::from_fen_str(&fen)? @@ -51,7 +24,7 @@ fn main() -> anyhow::Result<()> { let nodes_searched = position.perft(depth); - println!("Nodes searched: {nodes_searched}"); + println!("nodes={nodes_searched}"); Ok(()) } diff --git a/position/src/lib.rs b/position/src/lib.rs index c575b44..d0a1ec5 100644 --- a/position/src/lib.rs +++ b/position/src/lib.rs @@ -9,5 +9,6 @@ pub use chessfriend_board::{fen, PlacePieceError, PlacePieceStrategy}; pub use chessfriend_moves::{GeneratedMove, ValidateMove}; pub use position::Position; +pub mod perft; #[macro_use] pub mod testing; diff --git a/position/src/perft.rs b/position/src/perft.rs new file mode 100644 index 0000000..20257fb --- /dev/null +++ b/position/src/perft.rs @@ -0,0 +1,67 @@ +// Eryn Wells + +use crate::{GeneratedMove, Position, ValidateMove}; + +pub trait Perft { + fn perft(&mut self, depth: usize) -> u64; +} + +impl Perft for Position { + fn perft(&mut self, depth: usize) -> u64 { + if depth == 0 { + return 1; + } + + let mut total_nodes_counted = 0u64; + + let legal_moves: Vec = self.all_legal_moves(None).collect(); + + for generated_ply in legal_moves { + let ply = generated_ply.ply(); + + let has_seen_position = self + .make_move(ply, ValidateMove::No) + .expect("unable to make generated move"); + + // Do not recursive into trees where board positions repeat. + let nodes_counted = if has_seen_position { + 1 + } else { + self.perft(depth - 1) + }; + + total_nodes_counted += nodes_counted; + + self.unmake_last_move().expect("unable to unmake last move"); + } + + total_nodes_counted + } +} + +impl Position { + fn perft_recursive(&mut self, depth: usize) -> u64 { + if depth == 0 { + return 1; + } + + let mut total_nodes_counted: u64 = 0; + + let legal_moves: Vec = self.all_legal_moves(None).collect(); + + for generated_ply in legal_moves { + let ply = generated_ply.ply(); + + self.make_move(ply, ValidateMove::No) + .expect("unable to make generated move"); + + let nodes_counted = self.perft_recursive(depth - 1); + + total_nodes_counted += nodes_counted; + + self.unmake_last_move().expect("unable to unmake last move"); + } + + total_nodes_counted + } +} diff --git a/position/src/position.rs b/position/src/position.rs index ff95bcd..8787134 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -169,29 +169,31 @@ impl Position { } impl Position { - /// Make a move on the board and record it in the move list. + /// Make a move on the board and record it in the move list. Returns `true` + /// if the board position has been seen before (i.e. it's a repetition). /// /// ## Errors /// /// Returns one of [`MakeMoveError`] if the move cannot be made. /// - pub fn make_move(&mut self, ply: Move, validate: ValidateMove) -> Result<(), MakeMoveError> { + pub fn make_move(&mut self, ply: Move, validate: ValidateMove) -> Result { let record = self.board.make_move(ply, validate)?; if let Some(captured_piece) = record.captured_piece { self.captures.push(record.color, captured_piece); } - if let Some(hash) = self.board.zobrist_hash() { - // TODO: If the hash already exists here, it's a duplicate position - // and this move results in a draw. Find a way to indicate that, - // in either Board or Position. - self.boards_seen.insert(hash); - } + let has_seen = if let Some(hash) = self.board.zobrist_hash() { + // HashSet::insert() returns true if the value does not exist in the + // set when it's called. + !self.boards_seen.insert(hash) + } else { + false + }; self.moves.push(record.clone()); - Ok(()) + Ok(has_seen) } /// Unmake the last move made on the board and remove its record from the From bd112efdf3ed22a0d12b163926ba7abb3e1b8861 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 16 Jun 2025 09:02:36 -0700 Subject: [PATCH 377/423] Reformat the members list in Cargo.toml When I added the perft crate, it added it alphabetically at the end of the line with the moves crate. Why? --- Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 3b6ebee..190988c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,8 @@ members = [ "board", "core", "explorer", - "moves", "perft", + "moves", + "perft", "position", ] resolver = "2" From 45037d6fc19e07ad03a7645f1a88578ff2418524 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 16 Jun 2025 13:49:22 -0700 Subject: [PATCH 378/423] [explorer, moves, perft] A few rustfmt changes that reorder imports --- explorer/src/main.rs | 6 +++--- moves/src/generators/pawn.rs | 4 +++- perft/src/main.rs | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/explorer/src/main.rs b/explorer/src/main.rs index 5b5c6ed..b19a44a 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -3,14 +3,14 @@ mod make_command; use chessfriend_board::ZobristState; -use chessfriend_board::{fen::FromFenStr, Board}; +use chessfriend_board::{Board, fen::FromFenStr}; use chessfriend_core::random::RandomNumberGenerator; use chessfriend_core::{Color, Piece, Shape, Square}; use chessfriend_moves::GeneratedMove; -use chessfriend_position::{fen::ToFenStr, PlacePieceStrategy, Position}; +use chessfriend_position::{PlacePieceStrategy, Position, fen::ToFenStr}; use clap::{Arg, Command}; -use rustyline::error::ReadlineError; use rustyline::DefaultEditor; +use rustyline::error::ReadlineError; use std::sync::Arc; use thiserror::Error; diff --git a/moves/src/generators/pawn.rs b/moves/src/generators/pawn.rs index 385e1e9..6d07541 100644 --- a/moves/src/generators/pawn.rs +++ b/moves/src/generators/pawn.rs @@ -242,7 +242,9 @@ impl MoveType { #[cfg(test)] mod tests { use super::*; - use crate::{assert_move_list, assert_move_list_contains, assert_move_list_does_not_contain, ply, Move}; + use crate::{ + assert_move_list, assert_move_list_contains, assert_move_list_does_not_contain, ply, Move, + }; use chessfriend_board::{fen::FromFenStr, test_board}; use chessfriend_core::{Color, Square}; use std::collections::HashSet; diff --git a/perft/src/main.rs b/perft/src/main.rs index a311656..998e28a 100644 --- a/perft/src/main.rs +++ b/perft/src/main.rs @@ -1,4 +1,4 @@ -use chessfriend_position::{fen::FromFenStr, perft::Perft, Position}; +use chessfriend_position::{Position, fen::FromFenStr, perft::Perft}; use clap::Parser; #[derive(Parser, Debug)] From 0e61598937df051ebd8bba81944d5c8f7f8d437c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 16 Jun 2025 13:49:53 -0700 Subject: [PATCH 379/423] [explorer] Remove make_command module --- explorer/src/main.rs | 2 -- explorer/src/make_command.rs | 7 +++++++ 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 explorer/src/make_command.rs diff --git a/explorer/src/main.rs b/explorer/src/main.rs index b19a44a..77ef939 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -1,7 +1,5 @@ // Eryn Wells -mod make_command; - use chessfriend_board::ZobristState; use chessfriend_board::{Board, fen::FromFenStr}; use chessfriend_core::random::RandomNumberGenerator; diff --git a/explorer/src/make_command.rs b/explorer/src/make_command.rs new file mode 100644 index 0000000..ef3d809 --- /dev/null +++ b/explorer/src/make_command.rs @@ -0,0 +1,7 @@ +// Eryn Wells + +use clap::ArgMatches; + +pub enum MakeCommandError {} + +pub struct MakeCommand {} From 4650f88e0adf5a602f01ad538bdcd1e6b4846750 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 16 Jun 2025 13:50:18 -0700 Subject: [PATCH 380/423] Bump the resolver version in the workspace Cargo.toml --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 190988c..b0eb007 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,4 +8,4 @@ members = [ "perft", "position", ] -resolver = "2" +resolver = "3" From 8dc1e859e04096bf407edce68c2d5eda186f6906 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 16 Jun 2025 19:29:57 -0700 Subject: [PATCH 381/423] [chessfriend] Empty crate The idea for this crate is that it'll be the entry point for interacting with the chess engine. --- Cargo.lock | 6 ++++++ Cargo.toml | 1 + chessfriend/Cargo.toml | 5 +++++ chessfriend/src/lib.rs | 2 ++ 4 files changed, 14 insertions(+) create mode 100644 chessfriend/Cargo.toml create mode 100644 chessfriend/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index c6c6f1f..936cbaa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,6 +68,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chessfriend" +version = "0.1.0" +dependencies = [ +] + [[package]] name = "chessfriend_bitboard" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index b0eb007..49b9a15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "bitboard", "board", + "chessfriend", "core", "explorer", "moves", diff --git a/chessfriend/Cargo.toml b/chessfriend/Cargo.toml new file mode 100644 index 0000000..25bc79e --- /dev/null +++ b/chessfriend/Cargo.toml @@ -0,0 +1,5 @@ +[package] +name = "chessfriend" +version = "0.1.0" +edition = "2024" + diff --git a/chessfriend/src/lib.rs b/chessfriend/src/lib.rs new file mode 100644 index 0000000..4e01ba5 --- /dev/null +++ b/chessfriend/src/lib.rs @@ -0,0 +1,2 @@ +// Eryn Wells + From 37cb9bcaa024c8cbfab3424b5eb5670537bccec1 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 17 Jun 2025 08:28:39 -0700 Subject: [PATCH 382/423] [board, moves] Reorganize castling_right API on Board MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Board::color_has_castling_right takes an Option which it unwraps. With that unwrapped Color, it can call… Board::color_has_castling_right_unwrapped, which performs the evaluation with a non-Option Color. Clean up some imports. --- board/src/board.rs | 19 ++++++++++++++----- board/src/castle.rs | 8 ++++---- board/src/fen.rs | 6 +++--- moves/src/make_move.rs | 8 ++++---- moves/src/unmake_move.rs | 6 +++--- 5 files changed, 28 insertions(+), 19 deletions(-) diff --git a/board/src/board.rs b/board/src/board.rs index 6a93eef..666488e 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -1,11 +1,10 @@ // Eryn Wells use crate::{ - castle, + PieceSet, castle, display::DiagramFormatter, piece_sets::{PlacePieceError, PlacePieceStrategy}, zobrist::{ZobristHash, ZobristState}, - PieceSet, }; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece, Shape, Square, Wing}; @@ -109,11 +108,16 @@ impl Board { #[must_use] pub fn active_color_has_castling_right(&self, wing: Wing) -> bool { - self.color_has_castling_right(self.active_color, wing) + self.color_has_castling_right_unwrapped(self.active_color, wing) } #[must_use] - pub fn color_has_castling_right(&self, color: Color, wing: Wing) -> bool { + pub fn color_has_castling_right(&self, color: Option, wing: Wing) -> bool { + self.color_has_castling_right_unwrapped(self.unwrap_color(color), wing) + } + + #[must_use] + pub fn color_has_castling_right_unwrapped(&self, color: Color, wing: Wing) -> bool { self.castling_rights.color_has_right(color, wing) } @@ -129,7 +133,12 @@ impl Board { self.update_zobrist_hash_castling_rights(old_rights); } - pub fn revoke_castling_right(&mut self, color: Color, wing: Wing) { + pub fn revoke_castling_right(&mut self, color: Option, wing: Wing) { + let color = self.unwrap_color(color); + self.revoke_castling_right_unwrapped(color, wing); + } + + pub fn revoke_castling_right_unwrapped(&mut self, color: Color, wing: Wing) { let old_rights = self.castling_rights; self.castling_rights.revoke(color, wing); self.update_zobrist_hash_castling_rights(old_rights); diff --git a/board/src/castle.rs b/board/src/castle.rs index c5c3423..f8f9c24 100644 --- a/board/src/castle.rs +++ b/board/src/castle.rs @@ -46,7 +46,7 @@ impl Board { let color = self.unwrap_color(color); - if !self.color_has_castling_right(color, wing) { + if !self.color_has_castling_right_unwrapped(color, wing) { return Err(CastleEvaluationError::NoRights { color, wing }); } @@ -94,7 +94,7 @@ impl Board { mod tests { use super::*; use crate::test_board; - use chessfriend_core::{piece, Color, Wing}; + use chessfriend_core::{Color, Wing, piece}; #[test] fn king_on_starting_square_can_castle() { @@ -104,8 +104,8 @@ mod tests { White Rook on H1 ); - assert!(board.color_has_castling_right(Color::White, Wing::KingSide)); - assert!(board.color_has_castling_right(Color::White, Wing::QueenSide)); + assert!(board.color_has_castling_right_unwrapped(Color::White, Wing::KingSide)); + assert!(board.color_has_castling_right_unwrapped(Color::White, Wing::QueenSide)); } #[test] diff --git a/board/src/fen.rs b/board/src/fen.rs index 12f59ed..0da38dc 100644 --- a/board/src/fen.rs +++ b/board/src/fen.rs @@ -1,8 +1,8 @@ // Eryn Wells -use crate::{piece_sets::PlacePieceStrategy, Board}; +use crate::{Board, piece_sets::PlacePieceStrategy}; use chessfriend_core::{ - coordinates::ParseSquareError, piece, Color, File, Piece, Rank, Square, Wing, + Color, File, Piece, Rank, Square, Wing, coordinates::ParseSquareError, piece, }; use std::fmt::Write; use thiserror::Error; @@ -123,7 +123,7 @@ impl ToFenStr for Board { (Color::Black, Wing::QueenSide), ] .map(|(color, castle)| { - if !self.color_has_castling_right(color, castle) { + if !self.color_has_castling_right_unwrapped(color, castle) { return ""; } diff --git a/moves/src/make_move.rs b/moves/src/make_move.rs index bfb7d46..38b03cf 100644 --- a/moves/src/make_move.rs +++ b/moves/src/make_move.rs @@ -244,7 +244,7 @@ impl MakeMoveInternal for T { // original board state is preserved. let record = MoveRecord::new(board, ply, None); - board.revoke_castling_right(active_color, wing); + board.revoke_castling_right_unwrapped(active_color, wing); self.advance_board_state(None, HalfMoveClock::Advance); @@ -378,7 +378,7 @@ mod tests { use super::*; use crate::{Move, PromotionShape}; use chessfriend_board::test_board; - use chessfriend_core::{piece, Color, Square}; + use chessfriend_core::{Color, Square, piece}; type TestResult = Result<(), MakeMoveError>; @@ -528,7 +528,7 @@ mod tests { assert_eq!(board.get_piece(Square::H1), None); assert_eq!(board.get_piece(Square::G1), Some(piece!(White King))); assert_eq!(board.get_piece(Square::F1), Some(piece!(White Rook))); - assert!(!board.color_has_castling_right(Color::White, Wing::KingSide)); + assert!(!board.color_has_castling_right_unwrapped(Color::White, Wing::KingSide)); Ok(()) } @@ -548,7 +548,7 @@ mod tests { assert_eq!(board.get_piece(Square::A1), None); assert_eq!(board.get_piece(Square::C1), Some(piece!(White King))); assert_eq!(board.get_piece(Square::D1), Some(piece!(White Rook))); - assert!(!board.color_has_castling_right(Color::White, Wing::QueenSide)); + assert!(!board.color_has_castling_right_unwrapped(Color::White, Wing::QueenSide)); Ok(()) } diff --git a/moves/src/unmake_move.rs b/moves/src/unmake_move.rs index 2c6ebed..8a477c6 100644 --- a/moves/src/unmake_move.rs +++ b/moves/src/unmake_move.rs @@ -193,7 +193,7 @@ mod tests { use super::*; use crate::{MakeMove, Move, PromotionShape, ValidateMove}; use chessfriend_board::test_board; - use chessfriend_core::{piece, Color, Square, Wing}; + use chessfriend_core::{Color, Square, Wing, piece}; type TestResult = Result<(), Box>; @@ -406,7 +406,7 @@ mod tests { assert_eq!(board.get_piece(Square::H1), None); assert_eq!(board.get_piece(Square::G1), Some(piece!(White King))); assert_eq!(board.get_piece(Square::F1), Some(piece!(White Rook))); - assert!(!board.color_has_castling_right(Color::White, Wing::KingSide)); + assert!(!board.color_has_castling_right_unwrapped(Color::White, Wing::KingSide)); board.unmake_move(&record)?; @@ -438,7 +438,7 @@ mod tests { assert_eq!(board.get_piece(Square::A1), None); assert_eq!(board.get_piece(Square::C1), Some(piece!(White King))); assert_eq!(board.get_piece(Square::D1), Some(piece!(White Rook))); - assert!(!board.color_has_castling_right(Color::White, Wing::QueenSide)); + assert!(!board.color_has_castling_right_unwrapped(Color::White, Wing::QueenSide)); board.unmake_move(&record)?; From d73630c85e8a72caae22e04b316c154cd62209cf Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 17 Jun 2025 16:17:46 -0700 Subject: [PATCH 383/423] [perft, position] Print moves and nodes counted at first level At the first level of depth, print the move and the number of nodes counted in the tree underneath that node. This behavior imitates Stockfish, and helps with debugging. Clean up the output of the Perft binary, and update the check-positions script to compensate. --- perft/scripts/check-positions | 8 +++--- perft/src/main.rs | 4 +-- position/src/perft.rs | 50 ++++++++++------------------------- 3 files changed, 20 insertions(+), 42 deletions(-) diff --git a/perft/scripts/check-positions b/perft/scripts/check-positions index e5f0626..a7b24ec 100755 --- a/perft/scripts/check-positions +++ b/perft/scripts/check-positions @@ -10,7 +10,7 @@ import json import subprocess -def run_perft(fen, depth, expected_nodes_count): +def run_perft(fen, depth): result = subprocess.run( [ 'cargo', @@ -26,8 +26,8 @@ def run_perft(fen, depth, expected_nodes_count): nodes_count = 0 for line in result.stdout.splitlines(): - if line.startswith('nodes='): - (_, nodes_count) = line.split('=') + if line.startswith('nodes '): + (_, nodes_count) = line.split(' ') nodes_count = int(nodes_count) return nodes_count @@ -61,7 +61,7 @@ def main(argv): print(f'depth={depth}') print(f'expected-nodes={expected_nodes_count}') - nodes_count = run_perft(fen, depth, expected_nodes_count) + nodes_count = run_perft(fen, depth) print(f'nodes={nodes_count}') did_pass = nodes_count == expected_nodes_count diff --git a/perft/src/main.rs b/perft/src/main.rs index 998e28a..70630ae 100644 --- a/perft/src/main.rs +++ b/perft/src/main.rs @@ -14,7 +14,7 @@ fn main() -> anyhow::Result<()> { let args = Arguments::parse(); let depth = args.depth; - println!("depth={depth}"); + println!("depth {depth}"); let mut position = if let Some(fen) = args.fen { Position::from_fen_str(&fen)? @@ -24,7 +24,7 @@ fn main() -> anyhow::Result<()> { let nodes_searched = position.perft(depth); - println!("nodes={nodes_searched}"); + println!("nodes {nodes_searched}"); Ok(()) } diff --git a/position/src/perft.rs b/position/src/perft.rs index 20257fb..57af81a 100644 --- a/position/src/perft.rs +++ b/position/src/perft.rs @@ -8,6 +8,12 @@ pub trait Perft { impl Perft for Position { fn perft(&mut self, depth: usize) -> u64 { + self.perft_recursive(depth, depth) + } +} + +impl Position { + fn perft_recursive(&mut self, depth: usize, max_depth: usize) -> u64 { if depth == 0 { return 1; } @@ -16,50 +22,22 @@ impl Perft for Position { let legal_moves: Vec = self.all_legal_moves(None).collect(); - for generated_ply in legal_moves { - let ply = generated_ply.ply(); + for ply in legal_moves { + let ply = ply.ply(); - let has_seen_position = self + let _has_seen_position = self .make_move(ply, ValidateMove::No) .expect("unable to make generated move"); - // Do not recursive into trees where board positions repeat. - let nodes_counted = if has_seen_position { - 1 - } else { - self.perft(depth - 1) - }; - - total_nodes_counted += nodes_counted; - - self.unmake_last_move().expect("unable to unmake last move"); - } - - total_nodes_counted - } -} - -impl Position { - fn perft_recursive(&mut self, depth: usize) -> u64 { - if depth == 0 { - return 1; - } - - let mut total_nodes_counted: u64 = 0; - - let legal_moves: Vec = self.all_legal_moves(None).collect(); - - for generated_ply in legal_moves { - let ply = generated_ply.ply(); - - self.make_move(ply, ValidateMove::No) - .expect("unable to make generated move"); - - let nodes_counted = self.perft_recursive(depth - 1); + let nodes_counted = self.perft_recursive(depth - 1, depth); total_nodes_counted += nodes_counted; self.unmake_last_move().expect("unable to unmake last move"); + + if depth == max_depth { + println!(" {ply} {nodes_counted}"); + } } total_nodes_counted From c7b954400475a65f3ba8cd040a7a5217bef4f7cb Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 17 Jun 2025 16:18:48 -0700 Subject: [PATCH 384/423] [moves] Revoke castling rights when King and Rook make moves of their starting squares When the King moves, revoke all rights for the moving player. When the rook moves, revoke castling rights for that side of the board, if it's moving off its starting square. --- moves/src/make_move.rs | 55 +++++++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/moves/src/make_move.rs b/moves/src/make_move.rs index 38b03cf..7b8fd33 100644 --- a/moves/src/make_move.rs +++ b/moves/src/make_move.rs @@ -2,10 +2,10 @@ use crate::{Move, MoveRecord}; use chessfriend_board::{ - castle::CastleEvaluationError, movement::Movement, Board, BoardProvider, PlacePieceError, - PlacePieceStrategy, + Board, BoardProvider, CastleParameters, PlacePieceError, PlacePieceStrategy, + castle::CastleEvaluationError, movement::Movement, }; -use chessfriend_core::{Color, Piece, Rank, Square, Wing}; +use chessfriend_core::{Color, Piece, Rank, Shape, Square, Wing}; use thiserror::Error; pub type MakeMoveResult = Result; @@ -83,6 +83,8 @@ trait MakeMoveInternal { fn advance_board_state( &mut self, + ply: &Move, + piece_moved: &Piece, en_passant_target: Option, half_move_clock: HalfMoveClock, ); @@ -151,7 +153,7 @@ impl MakeMoveInternal for T { let record = MoveRecord::new(board, ply, None); - self.advance_board_state(None, HalfMoveClock::Advance); + self.advance_board_state(&ply, &piece, None, HalfMoveClock::Advance); Ok(record) } @@ -179,7 +181,12 @@ impl MakeMoveInternal for T { _ => unreachable!(), }; - self.advance_board_state(Some(en_passant_target), HalfMoveClock::Advance); + self.advance_board_state( + &ply, + &piece, + Some(en_passant_target), + HalfMoveClock::Advance, + ); Ok(record) } @@ -221,7 +228,7 @@ impl MakeMoveInternal for T { let record = MoveRecord::new(board, ply, Some(captured_piece)); - self.advance_board_state(None, HalfMoveClock::Reset); + self.advance_board_state(&ply, &piece, None, HalfMoveClock::Reset); Ok(record) } @@ -246,7 +253,7 @@ impl MakeMoveInternal for T { board.revoke_castling_right_unwrapped(active_color, wing); - self.advance_board_state(None, HalfMoveClock::Advance); + self.advance_board_state(&ply, &king, None, HalfMoveClock::Advance); Ok(record) } @@ -280,28 +287,52 @@ impl MakeMoveInternal for T { let record = MoveRecord::new(board, ply, None); - self.advance_board_state(None, HalfMoveClock::Reset); + self.advance_board_state(&ply, &piece, None, HalfMoveClock::Reset); Ok(record) } fn advance_board_state( &mut self, + ply: &Move, + piece_moved: &Piece, en_passant_target: Option, half_move_clock: HalfMoveClock, ) { let board = self.board_mut(); - match half_move_clock { - HalfMoveClock::Reset => board.half_move_clock = 0, - HalfMoveClock::Advance => board.half_move_clock += 1, + board.set_en_passant_target_option(en_passant_target); + + match piece_moved.shape { + Shape::Rook => { + let origin = ply.origin_square(); + + if board.color_has_castling_right(None, Wing::KingSide) { + let kingside_parameters = + CastleParameters::get(board.active_color(), Wing::KingSide); + if origin == kingside_parameters.origin.rook { + board.revoke_castling_right(None, Wing::KingSide); + } + } + + let queenside_parameters = + CastleParameters::get(board.active_color(), Wing::QueenSide); + if origin == queenside_parameters.origin.rook { + board.revoke_castling_right(None, Wing::QueenSide); + } + } + Shape::King => board.revoke_all_castling_rights(), + _ => {} } let previous_active_color = board.active_color(); let active_color = previous_active_color.next(); board.set_active_color(active_color); - board.set_en_passant_target_option(en_passant_target); + match half_move_clock { + HalfMoveClock::Reset => board.half_move_clock = 0, + HalfMoveClock::Advance => board.half_move_clock += 1, + } if active_color == Color::White { board.full_move_number += 1; From 801e15fd5aabfac29585f45ba653ea27551d06f7 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 17 Jun 2025 16:24:46 -0700 Subject: [PATCH 385/423] Add style_edition to rustfmt.toml Set style edition to 2024. --- rustfmt.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rustfmt.toml b/rustfmt.toml index be6f4bf..b377055 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,3 +1,5 @@ +style_edition = "2024" + imports_layout = "HorizontalVertical" group_imports = "StdExternalCrate" From 076cdfe66f29876a1ad2c1ba01b3c64fd58565db Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 17 Jun 2025 16:42:17 -0700 Subject: [PATCH 386/423] Remove empty dependencies list from Cargo.lock --- Cargo.lock | 2 -- 1 file changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 936cbaa..337e104 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,8 +71,6 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chessfriend" version = "0.1.0" -dependencies = [ -] [[package]] name = "chessfriend_bitboard" From f3b31d5514e738c2d16807547b8b9dae8c0cd0e2 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 17 Jun 2025 16:42:35 -0700 Subject: [PATCH 387/423] [perft] Print the fen string of the board position --- perft/src/main.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/perft/src/main.rs b/perft/src/main.rs index 70630ae..dece54c 100644 --- a/perft/src/main.rs +++ b/perft/src/main.rs @@ -1,4 +1,7 @@ -use chessfriend_position::{Position, fen::FromFenStr, perft::Perft}; +use chessfriend_position::{ + Position, + fen::{FromFenStr, ToFenStr}, +}; use clap::Parser; #[derive(Parser, Debug)] @@ -14,14 +17,15 @@ fn main() -> anyhow::Result<()> { let args = Arguments::parse(); let depth = args.depth; - println!("depth {depth}"); - let mut position = if let Some(fen) = args.fen { Position::from_fen_str(&fen)? } else { Position::starting(None) }; + println!("fen \"{}\"", position.to_fen_str().unwrap()); + println!("depth {depth}"); + let nodes_searched = position.perft(depth); println!("nodes {nodes_searched}"); From 6996cbeb15305e135dc9cd4d50428307d1397c0c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 17 Jun 2025 16:42:57 -0700 Subject: [PATCH 388/423] [position] Remove the Perft trait It wasn't serving a purpose. --- position/src/perft.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/position/src/perft.rs b/position/src/perft.rs index 57af81a..a9ae169 100644 --- a/position/src/perft.rs +++ b/position/src/perft.rs @@ -2,12 +2,8 @@ use crate::{GeneratedMove, Position, ValidateMove}; -pub trait Perft { - fn perft(&mut self, depth: usize) -> u64; -} - -impl Perft for Position { - fn perft(&mut self, depth: usize) -> u64 { +impl Position { + pub fn perft(&mut self, depth: usize) -> u64 { self.perft_recursive(depth, depth) } } From bf17017694e96d094b46abd6f3cd04fb6a585323 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 18 Jun 2025 08:21:21 -0700 Subject: [PATCH 389/423] [explorer] Add several commands to help with debugging flags : Print flags for the current board position. This prints the castling rights and whether the player can castle (regardless of whether they have the right). make : Finally reimplement the make command. Change the format so it takes a move in the UCI long algebraic style. perft : Run perft to a given depth on the current board position. --- explorer/src/main.rs | 101 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 92 insertions(+), 9 deletions(-) diff --git a/explorer/src/main.rs b/explorer/src/main.rs index 77ef939..b409cd0 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -1,12 +1,14 @@ // Eryn Wells -use chessfriend_board::ZobristState; +use chessfriend_board::castle::CastleEvaluationError; use chessfriend_board::{Board, fen::FromFenStr}; +use chessfriend_board::{CastleParameters, ZobristState}; use chessfriend_core::random::RandomNumberGenerator; -use chessfriend_core::{Color, Piece, Shape, Square}; -use chessfriend_moves::GeneratedMove; +use chessfriend_core::{Color, Piece, Shape, Square, Wing}; +use chessfriend_moves::algebraic::AlgebraicMoveComponents; +use chessfriend_moves::{GeneratedMove, ValidateMove}; use chessfriend_position::{PlacePieceStrategy, Position, fen::ToFenStr}; -use clap::{Arg, Command}; +use clap::{Arg, Command, value_parser}; use rustyline::DefaultEditor; use rustyline::error::ReadlineError; use std::sync::Arc; @@ -45,6 +47,7 @@ fn command_line() -> Command { .subcommand_help_heading("COMMANDS") .help_template(PARSER_TEMPLATE) .subcommand(Command::new("fen").about("Print the current position as a FEN string")) + .subcommand(Command::new("flags").about("Print flags for the current position")) .subcommand( Command::new("load") .arg(Arg::new("fen").required(true)) @@ -53,8 +56,7 @@ fn command_line() -> Command { ) .subcommand( Command::new("make") - .arg(Arg::new("from").required(true)) - .arg(Arg::new("to").required(true)) + .arg(Arg::new("move").required(true)) .alias("m") .about("Make a move"), ) @@ -81,6 +83,14 @@ fn command_line() -> Command { .arg(Arg::new("square").required(true)) .about("Show moves of a piece on a square"), ) + .subcommand( + Command::new("perft") + .arg(Arg::new("depth") + .required(true) + .value_parser(value_parser!(usize)) + ) + .about("Run Perft on the current position to the given depth") + ) .subcommand( Command::new("reset") .subcommand(Command::new("clear").about("Reset to a cleared board")) @@ -107,6 +117,12 @@ enum CommandHandlingError<'a> { #[error("no piece on {0}")] NoPiece(Square), + + #[error("{value:?} is not a valid value for {argument_name:?}")] + ValueError { + argument_name: &'static str, + value: String, + }, } fn respond(line: &str, state: &mut State) -> anyhow::Result { @@ -116,6 +132,7 @@ fn respond(line: &str, state: &mut State) -> anyhow::Result { let mut result = CommandResult::default(); match matches.subcommand() { + Some(("flags", matches)) => result = do_flags_command(state, matches), Some(("load", matches)) => result = do_load_command(state, matches)?, Some(("print", _matches)) => {} Some(("quit", _matches)) => { @@ -126,9 +143,8 @@ fn respond(line: &str, state: &mut State) -> anyhow::Result { println!("{}", state.position.to_fen_str()?); result.should_print_position = false; } - Some(("make", _matches)) => { - unimplemented!() - } + Some(("make", matches)) => result = do_make_command(state, matches)?, + Some(("perft", matches)) => result = do_perft_command(state, matches)?, Some(("place", matches)) => { let color = matches .get_one::("color") @@ -175,6 +191,34 @@ fn respond(line: &str, state: &mut State) -> anyhow::Result { Ok(result) } +fn do_flags_command(state: &mut State, _matches: &clap::ArgMatches) -> CommandResult { + let board = &state.position.board; + + println!("Castling:"); + + for (color, wing) in [ + (Color::White, Wing::KingSide), + (Color::White, Wing::QueenSide), + (Color::Black, Wing::KingSide), + (Color::Black, Wing::QueenSide), + ] { + let has_right = board.color_has_castling_right_unwrapped(color, wing); + let can_castle = board.color_can_castle(wing, Some(color)); + + let can_castle_message = match can_castle { + Ok(_) => "ok".to_string(), + Err(error) => format!("{error}"), + }; + + println!(" {color} {wing}: {has_right}, {can_castle_message}"); + } + + CommandResult { + should_continue: true, + should_print_position: false, + } +} + fn do_load_command(state: &mut State, matches: &clap::ArgMatches) -> anyhow::Result { let fen_string = matches .get_one::("fen") @@ -191,6 +235,26 @@ fn do_load_command(state: &mut State, matches: &clap::ArgMatches) -> anyhow::Res }) } +fn do_make_command(state: &mut State, matches: &clap::ArgMatches) -> anyhow::Result { + let move_string = matches + .get_one::("move") + .ok_or(CommandHandlingError::MissingArgument("move"))?; + + let algebraic_move: AlgebraicMoveComponents = move_string.parse()?; + + let encoded_move = state + .position + .move_from_algebraic_components(algebraic_move) + .ok_or(CommandHandlingError::ValueError { + argument_name: "move", + value: move_string.to_string(), + })?; + + state.position.make_move(encoded_move, ValidateMove::Yes)?; + + Ok(CommandResult::default()) +} + fn do_reset_command( state: &mut State, matches: &clap::ArgMatches, @@ -282,6 +346,25 @@ fn do_movement_command( }) } +fn do_perft_command( + state: &mut State, + matches: &clap::ArgMatches, +) -> anyhow::Result { + let depth = *matches + .get_one::("depth") + .ok_or(CommandHandlingError::MissingArgument("depth"))?; + + let mut position = state.position.clone(); + let nodes_count = position.perft(depth); + + println!("nodes {nodes_count}"); + + Ok(CommandResult { + should_continue: true, + should_print_position: false, + }) +} + fn do_zobrist_command(state: &mut State, _matches: &clap::ArgMatches) -> CommandResult { if let Some(hash) = state.position.zobrist_hash() { println!("hash:{hash}"); From c5cc0646efc30a6723ed2b4ce6a524e77c095f45 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 18 Jun 2025 08:22:12 -0700 Subject: [PATCH 390/423] [perft] Add back the block on searching into seen positions Check if the board position has been seen and stop recursion if so. --- position/src/perft.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/position/src/perft.rs b/position/src/perft.rs index a9ae169..97b3c51 100644 --- a/position/src/perft.rs +++ b/position/src/perft.rs @@ -18,14 +18,18 @@ impl Position { let legal_moves: Vec = self.all_legal_moves(None).collect(); - for ply in legal_moves { - let ply = ply.ply(); + for generated_ply in legal_moves { + let ply = generated_ply.ply(); - let _has_seen_position = self + let has_seen_position = self .make_move(ply, ValidateMove::No) .expect("unable to make generated move"); - let nodes_counted = self.perft_recursive(depth - 1, depth); + let nodes_counted = if has_seen_position { + 1 + } else { + self.perft_recursive(depth - 1, max_depth) + }; total_nodes_counted += nodes_counted; From 9972ce94d09674c78817ebefe09f43c9ea5dd7da Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 18 Jun 2025 08:25:41 -0700 Subject: [PATCH 391/423] [moves] Revoke castling rights only for the player that moved There was a bug in the code that revokes castling rights after a king move where it revoked the rights for *all* players, rather than just the current player. Fix it. --- moves/src/make_move.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/moves/src/make_move.rs b/moves/src/make_move.rs index 7b8fd33..0ddeb1e 100644 --- a/moves/src/make_move.rs +++ b/moves/src/make_move.rs @@ -321,7 +321,10 @@ impl MakeMoveInternal for T { board.revoke_castling_right(None, Wing::QueenSide); } } - Shape::King => board.revoke_all_castling_rights(), + Shape::King => { + board.revoke_castling_right(None, Wing::KingSide); + board.revoke_castling_right(None, Wing::QueenSide); + } _ => {} } From 933924d37a1d5fe9a01040ec77ffe4e8fd08e10c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 18 Jun 2025 08:26:29 -0700 Subject: [PATCH 392/423] [board] When loading a FEN string, start with no castling rights The default value of the castle::Rights struct is with all rights granted. When loading a FEN string, start with none and add to it. --- board/src/fen.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/board/src/fen.rs b/board/src/fen.rs index 0da38dc..a125aca 100644 --- a/board/src/fen.rs +++ b/board/src/fen.rs @@ -229,9 +229,8 @@ impl FromFenStr for Board { let castling_rights = fields .next() .ok_or(FromFenStrError::MissingField(Field::CastlingRights))?; - if castling_rights == "-" { - board.revoke_all_castling_rights(); - } else { + board.revoke_all_castling_rights(); + if castling_rights != "-" { for ch in castling_rights.chars() { match ch { 'K' => board.grant_castling_right(Color::White, Wing::KingSide), From 4ce7e89cdb0ad2508d901b547dbd8f7167b3caaa Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 18 Jun 2025 23:44:40 +0000 Subject: [PATCH 393/423] [board, explorer, moves] Clean up the castling rights API Reorganize castling rights API on Board into methods named according to conventions applied to other API. Board::has_castling_right Board::has_castling_right_active Board::has_castling_right_unwrapped These all check if a color has the right to castle on a particular side (wing) of the board. The first takes an Option, the latter two operate on bare Colors: the active color, or an explicit Color. Board::grant_castling_right Board::grant_castling_right_active Board::grant_castling_right_unwrapped Grant castling rights to a color. Color arguments follow the pattern above. Board::revoke_castling_right Board::revoke_castling_right_active Board::revoke_castling_right_unwrapped Revoke castling rights from a color. Color arguments follow the pattern above. The latter two groups of methods take a new CastleRightsOption type that specifies either a single Wing or All. Rework the implementation of CastleRights to take a CastleRightsOption. Update the unit tests and make sure everything builds. --- board/src/board.rs | 70 ++++++++++++-------------------- board/src/castle.rs | 69 ++++++++++++++++++++++++++------ board/src/castle/rights.rs | 81 +++++++++++++++++++++++++------------- board/src/fen.rs | 28 ++++++++----- explorer/src/main.rs | 15 +++---- moves/src/make_move.rs | 18 ++++----- moves/src/record.rs | 4 +- moves/src/unmake_move.rs | 16 ++++---- 8 files changed, 177 insertions(+), 124 deletions(-) diff --git a/board/src/board.rs b/board/src/board.rs index 666488e..6c56346 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -1,13 +1,13 @@ // Eryn Wells use crate::{ - PieceSet, castle, + CastleRights, PieceSet, display::DiagramFormatter, piece_sets::{PlacePieceError, PlacePieceStrategy}, zobrist::{ZobristHash, ZobristState}, }; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece, Shape, Square, Wing}; +use chessfriend_core::{Color, Piece, Shape, Square}; use std::sync::Arc; pub type HalfMoveClock = u32; @@ -17,7 +17,7 @@ pub type FullMoveClock = u32; pub struct Board { active_color: Color, pieces: PieceSet, - castling_rights: castle::Rights, + castling_rights: CastleRights, en_passant_target: Option, pub half_move_clock: HalfMoveClock, pub full_move_number: FullMoveClock, @@ -92,59 +92,27 @@ impl Board { impl Board { #[must_use] - pub fn castling_rights(&self) -> castle::Rights { - self.castling_rights + pub fn castling_rights(&self) -> &CastleRights { + &self.castling_rights } - pub fn set_castling_rights(&mut self, rights: castle::Rights) { + pub(crate) fn castling_rights_mut(&mut self) -> &mut CastleRights { + &mut self.castling_rights + } + + /// Replace castling rights with new rights wholesale. + pub fn set_castling_rights(&mut self, rights: CastleRights) { if rights == self.castling_rights { return; } let old_rights = self.castling_rights; self.castling_rights = rights; + self.update_zobrist_hash_castling_rights(old_rights); } - #[must_use] - pub fn active_color_has_castling_right(&self, wing: Wing) -> bool { - self.color_has_castling_right_unwrapped(self.active_color, wing) - } - - #[must_use] - pub fn color_has_castling_right(&self, color: Option, wing: Wing) -> bool { - self.color_has_castling_right_unwrapped(self.unwrap_color(color), wing) - } - - #[must_use] - pub fn color_has_castling_right_unwrapped(&self, color: Color, wing: Wing) -> bool { - self.castling_rights.color_has_right(color, wing) - } - - pub fn grant_castling_right(&mut self, color: Color, wing: Wing) { - let old_rights = self.castling_rights; - self.castling_rights.grant(color, wing); - self.update_zobrist_hash_castling_rights(old_rights); - } - - pub fn revoke_all_castling_rights(&mut self) { - let old_rights = self.castling_rights; - self.castling_rights.revoke_all(); - self.update_zobrist_hash_castling_rights(old_rights); - } - - pub fn revoke_castling_right(&mut self, color: Option, wing: Wing) { - let color = self.unwrap_color(color); - self.revoke_castling_right_unwrapped(color, wing); - } - - pub fn revoke_castling_right_unwrapped(&mut self, color: Color, wing: Wing) { - let old_rights = self.castling_rights; - self.castling_rights.revoke(color, wing); - self.update_zobrist_hash_castling_rights(old_rights); - } - - fn update_zobrist_hash_castling_rights(&mut self, old_rights: castle::Rights) { + pub(crate) fn update_zobrist_hash_castling_rights(&mut self, old_rights: CastleRights) { let new_rights = self.castling_rights; if old_rights == new_rights { return; @@ -154,6 +122,18 @@ impl Board { zobrist.update_modifying_castling_rights(new_rights, old_rights); } } + + pub(crate) fn castling_king(&self, square: Square) -> Option { + let active_color = self.active_color(); + self.get_piece(square) + .filter(|piece| piece.color == active_color && piece.is_king()) + } + + pub(crate) fn castling_rook(&self, square: Square) -> Option { + let active_color = self.active_color(); + self.get_piece(square) + .filter(|piece| piece.color == active_color && piece.is_rook()) + } } impl Board { diff --git a/board/src/castle.rs b/board/src/castle.rs index f8f9c24..5acdaaf 100644 --- a/board/src/castle.rs +++ b/board/src/castle.rs @@ -4,10 +4,10 @@ mod parameters; mod rights; pub use parameters::Parameters; -pub use rights::Rights; +pub use rights::{CastleRightsOption, Rights}; use crate::{Board, CastleParameters}; -use chessfriend_core::{Color, Piece, Square, Wing}; +use chessfriend_core::{Color, Wing}; use thiserror::Error; #[derive(Clone, Copy, Debug, Error, Eq, PartialEq)] @@ -46,7 +46,7 @@ impl Board { let color = self.unwrap_color(color); - if !self.color_has_castling_right_unwrapped(color, wing) { + if !self.has_castling_right_unwrapped(color, wing.into()) { return Err(CastleEvaluationError::NoRights { color, wing }); } @@ -76,17 +76,60 @@ impl Board { Ok(parameters) } +} - pub(crate) fn castling_king(&self, square: Square) -> Option { - let active_color = self.active_color(); - self.get_piece(square) - .filter(|piece| piece.color == active_color && piece.is_king()) +impl Board { + #[must_use] + pub fn has_castling_right(&self, color: Option, wing: Wing) -> bool { + self.has_castling_right_unwrapped(self.unwrap_color(color), wing) } - pub(crate) fn castling_rook(&self, square: Square) -> Option { - let active_color = self.active_color(); - self.get_piece(square) - .filter(|piece| piece.color == active_color && piece.is_rook()) + #[must_use] + pub fn has_castling_right_active(&self, wing: Wing) -> bool { + self.has_castling_right_unwrapped(self.active_color(), wing) + } + + #[must_use] + pub fn has_castling_right_unwrapped(&self, color: Color, wing: Wing) -> bool { + self.castling_rights().get(color, wing.into()) + } +} + +impl Board { + pub fn grant_castling_rights(&mut self, color: Option, rights: CastleRightsOption) { + let color = self.unwrap_color(color); + self.grant_castling_rights_unwrapped(color, rights); + } + + pub fn grant_castling_rights_active(&mut self, rights: CastleRightsOption) { + self.grant_castling_rights_unwrapped(self.active_color(), rights); + } + + pub fn grant_castling_rights_unwrapped(&mut self, color: Color, rights: CastleRightsOption) { + let old_rights = *self.castling_rights(); + self.castling_rights_mut().grant(color, rights); + self.update_zobrist_hash_castling_rights(old_rights); + } +} + +impl Board { + pub fn revoke_all_castling_rights(&mut self) { + self.castling_rights_mut().revoke_all(); + } + + pub fn revoke_castling_rights(&mut self, color: Option, rights: CastleRightsOption) { + let color = self.unwrap_color(color); + self.revoke_castling_rights_unwrapped(color, rights); + } + + pub fn revoke_castling_rights_active(&mut self, rights: CastleRightsOption) { + self.revoke_castling_rights_unwrapped(self.active_color(), rights); + } + + pub fn revoke_castling_rights_unwrapped(&mut self, color: Color, rights: CastleRightsOption) { + let old_rights = *self.castling_rights(); + self.castling_rights_mut().revoke(color, rights); + self.update_zobrist_hash_castling_rights(old_rights); } } @@ -104,8 +147,8 @@ mod tests { White Rook on H1 ); - assert!(board.color_has_castling_right_unwrapped(Color::White, Wing::KingSide)); - assert!(board.color_has_castling_right_unwrapped(Color::White, Wing::QueenSide)); + assert!(board.has_castling_right_unwrapped(Color::White, Wing::KingSide)); + assert!(board.has_castling_right_unwrapped(Color::White, Wing::QueenSide)); } #[test] diff --git a/board/src/castle/rights.rs b/board/src/castle/rights.rs index b65461b..2c0a961 100644 --- a/board/src/castle/rights.rs +++ b/board/src/castle/rights.rs @@ -1,6 +1,14 @@ +// Eryn Wells + use chessfriend_core::{Color, Wing}; use std::fmt; +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CastleRightsOption { + Wing(Wing), + All, +} + #[derive(Clone, Copy, Eq, Hash, PartialEq)] pub struct Rights(u8); @@ -12,16 +20,16 @@ impl Rights { /// as long as they have not moved their king, or the rook on that side of /// the board. #[must_use] - pub fn color_has_right(self, color: Color, wing: Wing) -> bool { - (self.0 & (1 << Self::flag_offset(color, wing))) != 0 + pub fn get(self, color: Color, option: CastleRightsOption) -> bool { + (self.0 & Self::flags(color, option)) != 0 } - pub fn grant(&mut self, color: Color, wing: Wing) { - self.0 |= 1 << Self::flag_offset(color, wing); + pub fn grant(&mut self, color: Color, option: CastleRightsOption) { + self.0 |= Self::flags(color, option); } - pub fn revoke(&mut self, color: Color, wing: Wing) { - self.0 &= !(1 << Self::flag_offset(color, wing)); + pub fn revoke(&mut self, color: Color, option: CastleRightsOption) { + self.0 &= !Self::flags(color, option); } /// Revoke castling rights for all colors and all sides of the board. @@ -31,14 +39,14 @@ impl Rights { } impl Rights { - pub(crate) fn as_index(&self) -> usize { + pub(crate) fn as_index(self) -> usize { self.0 as usize } } impl Rights { - fn flag_offset(color: Color, wing: Wing) -> usize { - ((color as usize) << 1) + wing as usize + const fn flags(color: Color, option: CastleRightsOption) -> u8 { + option.as_flags() << (color as u8 * 2) } } @@ -54,36 +62,55 @@ impl Default for Rights { } } +impl CastleRightsOption { + #[must_use] + pub const fn as_flags(self) -> u8 { + match self { + Self::Wing(wing) => 1 << (wing as u8), + Self::All => (1 << Wing::KingSide as u8) | (1 << Wing::QueenSide as u8), + } + } +} + +impl From for CastleRightsOption { + fn from(value: Wing) -> Self { + Self::Wing(value) + } +} + #[cfg(test)] mod tests { use super::*; #[test] fn bitfield_offsets() { - assert_eq!(Rights::flag_offset(Color::White, Wing::KingSide), 0); - assert_eq!(Rights::flag_offset(Color::White, Wing::QueenSide), 1); - assert_eq!(Rights::flag_offset(Color::Black, Wing::KingSide), 2); - assert_eq!(Rights::flag_offset(Color::Black, Wing::QueenSide), 3); + assert_eq!(Rights::flags(Color::White, Wing::KingSide.into()), 1); + assert_eq!(Rights::flags(Color::White, Wing::QueenSide.into()), 1 << 1); + assert_eq!(Rights::flags(Color::Black, Wing::KingSide.into()), 1 << 2); + assert_eq!(Rights::flags(Color::Black, Wing::QueenSide.into()), 1 << 3); + + assert_eq!(Rights::flags(Color::White, CastleRightsOption::All), 0b11); + assert_eq!(Rights::flags(Color::Black, CastleRightsOption::All), 0b1100); } #[test] fn default_rights() { let mut rights = Rights::default(); - assert!(rights.color_has_right(Color::White, Wing::KingSide)); - assert!(rights.color_has_right(Color::White, Wing::QueenSide)); - assert!(rights.color_has_right(Color::Black, Wing::KingSide)); - assert!(rights.color_has_right(Color::Black, Wing::QueenSide)); + assert!(rights.get(Color::White, Wing::KingSide.into())); + assert!(rights.get(Color::White, Wing::QueenSide.into())); + assert!(rights.get(Color::Black, Wing::KingSide.into())); + assert!(rights.get(Color::Black, Wing::QueenSide.into())); - rights.revoke(Color::White, Wing::QueenSide); - assert!(rights.color_has_right(Color::White, Wing::KingSide)); - assert!(!rights.color_has_right(Color::White, Wing::QueenSide)); - assert!(rights.color_has_right(Color::Black, Wing::KingSide)); - assert!(rights.color_has_right(Color::Black, Wing::QueenSide)); + rights.revoke(Color::White, Wing::QueenSide.into()); + assert!(rights.get(Color::White, Wing::KingSide.into())); + assert!(!rights.get(Color::White, Wing::QueenSide.into())); + assert!(rights.get(Color::Black, Wing::KingSide.into())); + assert!(rights.get(Color::Black, Wing::QueenSide.into())); - rights.grant(Color::White, Wing::QueenSide); - assert!(rights.color_has_right(Color::White, Wing::KingSide)); - assert!(rights.color_has_right(Color::White, Wing::QueenSide)); - assert!(rights.color_has_right(Color::Black, Wing::KingSide)); - assert!(rights.color_has_right(Color::Black, Wing::QueenSide)); + rights.grant(Color::White, Wing::QueenSide.into()); + assert!(rights.get(Color::White, Wing::KingSide.into())); + assert!(rights.get(Color::White, Wing::QueenSide.into())); + assert!(rights.get(Color::Black, Wing::KingSide.into())); + assert!(rights.get(Color::Black, Wing::QueenSide.into())); } } diff --git a/board/src/fen.rs b/board/src/fen.rs index a125aca..65c468a 100644 --- a/board/src/fen.rs +++ b/board/src/fen.rs @@ -24,12 +24,16 @@ pub enum ToFenStrError { pub enum FromFenStrError { #[error("missing {0} field")] MissingField(Field), + #[error("missing piece placement")] MissingPlacement, + #[error("invalid value")] InvalidValue, + #[error("{0}")] ParseIntError(#[from] std::num::ParseIntError), + #[error("{0}")] ParseSquareError(#[from] ParseSquareError), } @@ -122,12 +126,12 @@ impl ToFenStr for Board { (Color::Black, Wing::KingSide), (Color::Black, Wing::QueenSide), ] - .map(|(color, castle)| { - if !self.color_has_castling_right_unwrapped(color, castle) { + .map(|(color, wing)| { + if !self.has_castling_right_unwrapped(color, wing) { return ""; } - match (color, castle) { + match (color, wing) { (Color::White, Wing::KingSide) => "K", (Color::White, Wing::QueenSide) => "Q", (Color::Black, Wing::KingSide) => "k", @@ -226,19 +230,23 @@ impl FromFenStr for Board { )?; board.set_active_color(active_color); + let color_wing_from_char = |ch| match ch { + 'K' => Some((Color::White, Wing::KingSide)), + 'Q' => Some((Color::White, Wing::QueenSide)), + 'k' => Some((Color::Black, Wing::KingSide)), + 'q' => Some((Color::Black, Wing::QueenSide)), + _ => None, + }; + let castling_rights = fields .next() .ok_or(FromFenStrError::MissingField(Field::CastlingRights))?; board.revoke_all_castling_rights(); if castling_rights != "-" { for ch in castling_rights.chars() { - match ch { - 'K' => board.grant_castling_right(Color::White, Wing::KingSide), - 'Q' => board.grant_castling_right(Color::White, Wing::QueenSide), - 'k' => board.grant_castling_right(Color::Black, Wing::KingSide), - 'q' => board.grant_castling_right(Color::Black, Wing::QueenSide), - _ => return Err(FromFenStrError::InvalidValue), - }; + let (color, wing) = + color_wing_from_char(ch).ok_or(FromFenStrError::InvalidValue)?; + board.grant_castling_rights_unwrapped(color, wing.into()); } } diff --git a/explorer/src/main.rs b/explorer/src/main.rs index b409cd0..4f98a63 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -1,16 +1,11 @@ // Eryn Wells -use chessfriend_board::castle::CastleEvaluationError; -use chessfriend_board::{Board, fen::FromFenStr}; -use chessfriend_board::{CastleParameters, ZobristState}; -use chessfriend_core::random::RandomNumberGenerator; -use chessfriend_core::{Color, Piece, Shape, Square, Wing}; -use chessfriend_moves::algebraic::AlgebraicMoveComponents; -use chessfriend_moves::{GeneratedMove, ValidateMove}; +use chessfriend_board::{Board, ZobristState, fen::FromFenStr}; +use chessfriend_core::{Color, Piece, Shape, Square, Wing, random::RandomNumberGenerator}; +use chessfriend_moves::{GeneratedMove, ValidateMove, algebraic::AlgebraicMoveComponents}; use chessfriend_position::{PlacePieceStrategy, Position, fen::ToFenStr}; use clap::{Arg, Command, value_parser}; -use rustyline::DefaultEditor; -use rustyline::error::ReadlineError; +use rustyline::{DefaultEditor, error::ReadlineError}; use std::sync::Arc; use thiserror::Error; @@ -202,7 +197,7 @@ fn do_flags_command(state: &mut State, _matches: &clap::ArgMatches) -> CommandRe (Color::Black, Wing::KingSide), (Color::Black, Wing::QueenSide), ] { - let has_right = board.color_has_castling_right_unwrapped(color, wing); + let has_right = board.has_castling_right_unwrapped(color, wing.into()); let can_castle = board.color_can_castle(wing, Some(color)); let can_castle_message = match can_castle { diff --git a/moves/src/make_move.rs b/moves/src/make_move.rs index 0ddeb1e..f413e72 100644 --- a/moves/src/make_move.rs +++ b/moves/src/make_move.rs @@ -3,7 +3,8 @@ use crate::{Move, MoveRecord}; use chessfriend_board::{ Board, BoardProvider, CastleParameters, PlacePieceError, PlacePieceStrategy, - castle::CastleEvaluationError, movement::Movement, + castle::{CastleEvaluationError, CastleRightsOption}, + movement::Movement, }; use chessfriend_core::{Color, Piece, Rank, Shape, Square, Wing}; use thiserror::Error; @@ -251,7 +252,7 @@ impl MakeMoveInternal for T { // original board state is preserved. let record = MoveRecord::new(board, ply, None); - board.revoke_castling_right_unwrapped(active_color, wing); + board.revoke_castling_rights_active(wing.into()); self.advance_board_state(&ply, &king, None, HalfMoveClock::Advance); @@ -307,23 +308,22 @@ impl MakeMoveInternal for T { Shape::Rook => { let origin = ply.origin_square(); - if board.color_has_castling_right(None, Wing::KingSide) { + if board.has_castling_right(None, Wing::KingSide) { let kingside_parameters = CastleParameters::get(board.active_color(), Wing::KingSide); if origin == kingside_parameters.origin.rook { - board.revoke_castling_right(None, Wing::KingSide); + board.revoke_castling_rights(None, Wing::KingSide.into()); } } let queenside_parameters = CastleParameters::get(board.active_color(), Wing::QueenSide); if origin == queenside_parameters.origin.rook { - board.revoke_castling_right(None, Wing::QueenSide); + board.revoke_castling_rights(None, Wing::QueenSide.into()); } } Shape::King => { - board.revoke_castling_right(None, Wing::KingSide); - board.revoke_castling_right(None, Wing::QueenSide); + board.revoke_castling_rights(None, CastleRightsOption::All); } _ => {} } @@ -562,7 +562,7 @@ mod tests { assert_eq!(board.get_piece(Square::H1), None); assert_eq!(board.get_piece(Square::G1), Some(piece!(White King))); assert_eq!(board.get_piece(Square::F1), Some(piece!(White Rook))); - assert!(!board.color_has_castling_right_unwrapped(Color::White, Wing::KingSide)); + assert!(!board.has_castling_right_unwrapped(Color::White, Wing::KingSide)); Ok(()) } @@ -582,7 +582,7 @@ mod tests { assert_eq!(board.get_piece(Square::A1), None); assert_eq!(board.get_piece(Square::C1), Some(piece!(White King))); assert_eq!(board.get_piece(Square::D1), Some(piece!(White Rook))); - assert!(!board.color_has_castling_right_unwrapped(Color::White, Wing::QueenSide)); + assert!(!board.has_castling_right_unwrapped(Color::White, Wing::QueenSide)); Ok(()) } diff --git a/moves/src/record.rs b/moves/src/record.rs index 47a2c3f..24472b8 100644 --- a/moves/src/record.rs +++ b/moves/src/record.rs @@ -1,7 +1,7 @@ // Eryn Wells use crate::Move; -use chessfriend_board::{board::HalfMoveClock, Board, CastleRights}; +use chessfriend_board::{Board, CastleRights, board::HalfMoveClock}; use chessfriend_core::{Color, Piece, Square}; /// A record of a move made on a board. This struct contains all the information @@ -35,7 +35,7 @@ impl MoveRecord { color: board.active_color(), ply, en_passant_target: board.en_passant_target(), - castling_rights: board.castling_rights(), + castling_rights: *board.castling_rights(), half_move_clock: board.half_move_clock, captured_piece: capture, } diff --git a/moves/src/unmake_move.rs b/moves/src/unmake_move.rs index 8a477c6..69c43b7 100644 --- a/moves/src/unmake_move.rs +++ b/moves/src/unmake_move.rs @@ -396,7 +396,7 @@ mod tests { White Rook on H1, ]; - let original_castling_rights = board.castling_rights(); + let original_castling_rights = *board.castling_rights(); let ply = Move::castle(Color::White, Wing::KingSide); let record = board.make_move(ply, ValidateMove::Yes)?; @@ -406,7 +406,7 @@ mod tests { assert_eq!(board.get_piece(Square::H1), None); assert_eq!(board.get_piece(Square::G1), Some(piece!(White King))); assert_eq!(board.get_piece(Square::F1), Some(piece!(White Rook))); - assert!(!board.color_has_castling_right_unwrapped(Color::White, Wing::KingSide)); + assert!(!board.has_castling_right_unwrapped(Color::White, Wing::KingSide)); board.unmake_move(&record)?; @@ -415,7 +415,7 @@ mod tests { assert_eq!(board.get_piece(Square::H1), Some(piece!(White Rook))); assert_eq!(board.get_piece(Square::G1), None); assert_eq!(board.get_piece(Square::F1), None); - assert_eq!(board.castling_rights(), original_castling_rights); + assert_eq!(*board.castling_rights(), original_castling_rights); assert_eq!(board.active_color(), Color::White); Ok(()) @@ -428,7 +428,7 @@ mod tests { White Rook on A1, ]; - let original_castling_rights = board.castling_rights(); + let original_castling_rights = *board.castling_rights(); let ply = Move::castle(Color::White, Wing::QueenSide); let record = board.make_move(ply, ValidateMove::Yes)?; @@ -438,7 +438,7 @@ mod tests { assert_eq!(board.get_piece(Square::A1), None); assert_eq!(board.get_piece(Square::C1), Some(piece!(White King))); assert_eq!(board.get_piece(Square::D1), Some(piece!(White Rook))); - assert!(!board.color_has_castling_right_unwrapped(Color::White, Wing::QueenSide)); + assert!(!board.has_castling_right_unwrapped(Color::White, Wing::QueenSide)); board.unmake_move(&record)?; @@ -447,7 +447,7 @@ mod tests { assert_eq!(board.get_piece(Square::A1), Some(piece!(White Rook))); assert_eq!(board.get_piece(Square::C1), None); assert_eq!(board.get_piece(Square::D1), None); - assert_eq!(board.castling_rights(), original_castling_rights); + assert_eq!(*board.castling_rights(), original_castling_rights); assert_eq!(board.active_color(), Color::White); Ok(()) @@ -460,7 +460,7 @@ mod tests { Black Rook on H8, ]); - let original_castling_rights = board.castling_rights(); + let original_castling_rights = *board.castling_rights(); let ply = Move::castle(Color::Black, Wing::KingSide); let record = board.make_move(ply, ValidateMove::Yes)?; @@ -478,7 +478,7 @@ mod tests { assert_eq!(board.get_piece(Square::H8), Some(piece!(Black Rook))); assert_eq!(board.get_piece(Square::G8), None); assert_eq!(board.get_piece(Square::F8), None); - assert_eq!(board.castling_rights(), original_castling_rights); + assert_eq!(*board.castling_rights(), original_castling_rights); assert_eq!(board.active_color(), Color::Black); Ok(()) From 0f5a538f0a8ba5b6e5b3a19ba40fca0a71566c11 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 19 Jun 2025 11:33:35 -0700 Subject: [PATCH 394/423] [explorer, perft, position] Move node count into a new PerftCounters struct --- explorer/src/main.rs | 4 +-- perft/src/main.rs | 5 ++-- position/src/perft.rs | 57 +++++++++++++++++++++++++++++++------------ 3 files changed, 47 insertions(+), 19 deletions(-) diff --git a/explorer/src/main.rs b/explorer/src/main.rs index 4f98a63..0acb2a1 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -350,9 +350,9 @@ fn do_perft_command( .ok_or(CommandHandlingError::MissingArgument("depth"))?; let mut position = state.position.clone(); - let nodes_count = position.perft(depth); + let counters = position.perft(depth); - println!("nodes {nodes_count}"); + println!("{counters}"); Ok(CommandResult { should_continue: true, diff --git a/perft/src/main.rs b/perft/src/main.rs index dece54c..d1e7f77 100644 --- a/perft/src/main.rs +++ b/perft/src/main.rs @@ -7,6 +7,7 @@ use clap::Parser; #[derive(Parser, Debug)] #[command(name = "Perft")] struct Arguments { + #[arg(long, short, value_name = "INT")] depth: usize, #[arg(long, short, value_name = "FEN")] @@ -26,9 +27,9 @@ fn main() -> anyhow::Result<()> { println!("fen \"{}\"", position.to_fen_str().unwrap()); println!("depth {depth}"); - let nodes_searched = position.perft(depth); + let counters = position.perft(depth); - println!("nodes {nodes_searched}"); + println!("\n{counters}"); Ok(()) } diff --git a/position/src/perft.rs b/position/src/perft.rs index 97b3c51..3269a04 100644 --- a/position/src/perft.rs +++ b/position/src/perft.rs @@ -1,20 +1,29 @@ // Eryn Wells +use chessfriend_moves::Move; + use crate::{GeneratedMove, Position, ValidateMove}; +use std::fmt; + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct PerftCounters { + nodes: u64, +} impl Position { - pub fn perft(&mut self, depth: usize) -> u64 { - self.perft_recursive(depth, depth) + pub fn perft(&mut self, depth: usize) -> PerftCounters { + self.perft_recursive(0, depth) } } impl Position { - fn perft_recursive(&mut self, depth: usize, max_depth: usize) -> u64 { - if depth == 0 { - return 1; - } + fn perft_recursive(&mut self, depth: usize, max_depth: usize) -> PerftCounters { + let mut counters = PerftCounters::default(); - let mut total_nodes_counted = 0u64; + if depth == max_depth { + counters.count_node(); + return counters; + } let legal_moves: Vec = self.all_legal_moves(None).collect(); @@ -25,21 +34,39 @@ impl Position { .make_move(ply, ValidateMove::No) .expect("unable to make generated move"); - let nodes_counted = if has_seen_position { - 1 + let recursive_counters = if has_seen_position { + let mut counters = PerftCounters::default(); + counters.count_node(); + counters } else { - self.perft_recursive(depth - 1, max_depth) + self.perft_recursive(depth + 1, max_depth) }; - total_nodes_counted += nodes_counted; - self.unmake_last_move().expect("unable to unmake last move"); - if depth == max_depth { - println!(" {ply} {nodes_counted}"); + counters.fold(&recursive_counters); + + if depth == 0 { + println!(" {ply}: {}", recursive_counters.nodes); } } - total_nodes_counted + counters + } +} + +impl PerftCounters { + fn count_node(&mut self) { + self.nodes += 1; + } + fn fold(&mut self, results: &Self) { + self.nodes += results.nodes; + } +} + +impl fmt::Display for PerftCounters { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "Perft Results")?; + write!(f, " Nodes: {}", self.nodes) } } From 1d8a0dc3a4ffd2c5a20935af212b71eda9523773 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 19 Jun 2025 14:27:52 -0700 Subject: [PATCH 395/423] Add a release-debug profile This profile builds binaries for release, but includes debugging information. Useful for profiling! --- Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 49b9a15..37c14db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,7 @@ members = [ "position", ] resolver = "3" + +[profile.release-debug] +inherits = "release" +debug = true From 481ae70698c3ae15ce6827750a856237457d9d5b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 19 Jun 2025 14:32:07 -0700 Subject: [PATCH 396/423] [core] Directly index the array of Squares with a given index In Square::from_index_unchecked, instead of using TryFrom to convert the index to a square, just index directly into the Square::ALL array. This function is already marked unsafe. --- core/src/coordinates.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/src/coordinates.rs b/core/src/coordinates.rs index 99a252e..308fc3e 100644 --- a/core/src/coordinates.rs +++ b/core/src/coordinates.rs @@ -115,7 +115,9 @@ macro_rules! range_bound_struct { coordinate_enum!( Direction, - [North, NorthEast, East, SouthEast, South, SouthWest, West, NorthWest] + [ + North, NorthEast, East, SouthEast, South, SouthWest, West, NorthWest + ] ); impl Direction { @@ -262,7 +264,7 @@ impl Square { #[must_use] pub unsafe fn from_index_unchecked(x: u8) -> Square { debug_assert!((x as usize) < Self::NUM); - Self::try_from(x).unwrap_unchecked() + Self::ALL[x as usize] } #[inline] From 7f25548335cfa8e081948484e9990f7f5a1c7b60 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 20 Jun 2025 14:23:57 -0700 Subject: [PATCH 397/423] [board, core, position] A simple static evaluation method for scoring positions Implement a new Evaluator struct that evaluates a Board and returns a score. This evaluation mechanism uses only a material balance function. It doesn't account for anything else. Supporting this, add a Counts struct to the internal piece set structure of a Board. This struct is responsible for keeping counts of how many pieces of each shape are on the board for each color. Export a count_piece() method on Board that returns a count of the number of pieces of a particular color and shape. Implement a newtype wrapper around i32 called Score that represents the score of a position in centipawns, i.e. hundredths of a pawn. Add piece values to the Shape enum. --- board/src/board.rs | 7 +++- board/src/fen.rs | 5 ++- board/src/piece_sets.rs | 27 ++++++++++--- board/src/piece_sets/counts.rs | 60 ++++++++++++++++++++++++++++ core/src/colors.rs | 10 ++++- core/src/lib.rs | 1 + core/src/score.rs | 71 ++++++++++++++++++++++++++++++++++ core/src/shapes.rs | 13 +++++++ position/src/evaluation.rs | 62 +++++++++++++++++++++++++++++ position/src/lib.rs | 3 +- 10 files changed, 249 insertions(+), 10 deletions(-) create mode 100644 board/src/piece_sets/counts.rs create mode 100644 core/src/score.rs create mode 100644 position/src/evaluation.rs diff --git a/board/src/board.rs b/board/src/board.rs index 6c56346..338d4aa 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -3,7 +3,7 @@ use crate::{ CastleRights, PieceSet, display::DiagramFormatter, - piece_sets::{PlacePieceError, PlacePieceStrategy}, + piece_sets::{Counter, PlacePieceError, PlacePieceStrategy}, zobrist::{ZobristHash, ZobristState}, }; use chessfriend_bitboard::BitBoard; @@ -219,6 +219,11 @@ impl Board { removed_piece } + + #[must_use] + pub fn count_piece(&self, piece: &Piece) -> Counter { + self.pieces.count(piece) + } } impl Board { diff --git a/board/src/fen.rs b/board/src/fen.rs index 65c468a..a44b735 100644 --- a/board/src/fen.rs +++ b/board/src/fen.rs @@ -9,9 +9,10 @@ use thiserror::Error; #[macro_export] macro_rules! fen { - ($fen_string:literal) => { + ($fen_string:literal) => {{ + use $crate::fen::FromFenStr; Board::from_fen_str($fen_string) - }; + }}; } #[derive(Clone, Debug, Error, Eq, PartialEq)] diff --git a/board/src/piece_sets.rs b/board/src/piece_sets.rs index 063937d..316c1d8 100644 --- a/board/src/piece_sets.rs +++ b/board/src/piece_sets.rs @@ -1,8 +1,9 @@ // Eryn Wells +mod counts; mod mailbox; -use self::mailbox::Mailbox; +use self::{counts::Counts, mailbox::Mailbox}; use chessfriend_bitboard::{BitBoard, IterationDirection}; use chessfriend_core::{Color, Piece, Shape, Square}; use std::{ @@ -11,6 +12,8 @@ use std::{ }; use thiserror::Error; +pub(crate) use counts::Counter; + #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum PlacePieceStrategy { #[default] @@ -29,6 +32,7 @@ pub enum PlacePieceError { #[derive(Clone, Debug, Default, Eq)] pub struct PieceSet { mailbox: Mailbox, + counts: Counts, color_occupancy: [BitBoard; Color::NUM], shape_occupancy: [BitBoard; Shape::NUM], } @@ -36,18 +40,21 @@ pub struct PieceSet { impl PieceSet { pub(crate) fn new(pieces: [[BitBoard; Shape::NUM]; Color::NUM]) -> Self { let mut mailbox = Mailbox::default(); + let mut counts = Counts::default(); let mut color_occupancy: [BitBoard; Color::NUM] = Default::default(); let mut shape_occupancy: [BitBoard; Shape::NUM] = Default::default(); - for (color_index, color) in Color::iter().enumerate() { + for (color_index, color) in Color::into_iter().enumerate() { for (shape_index, shape) in Shape::into_iter().enumerate() { let bitboard = pieces[color_index][shape_index]; color_occupancy[color_index] |= bitboard; shape_occupancy[shape_index] |= bitboard; + counts.increment(color, shape); + for square in bitboard.occupied_squares(&IterationDirection::default()) { - let piece = Piece::new(*color, shape); + let piece = Piece::new(color, shape); mailbox.set(piece, square); } } @@ -55,6 +62,7 @@ impl PieceSet { Self { mailbox, + counts, color_occupancy, shape_occupancy, } @@ -94,6 +102,10 @@ impl PieceSet { self.mailbox.get(square) } + pub(crate) fn count(&self, piece: &Piece) -> Counter { + self.counts.get(piece.color, piece.shape) + } + // TODO: Rename this. Maybe get_all() is better? pub(crate) fn find_pieces(&self, piece: Piece) -> BitBoard { let color_occupancy = self.color_occupancy[piece.color as usize]; @@ -120,6 +132,7 @@ impl PieceSet { self.color_occupancy[color as usize].set(square); self.shape_occupancy[shape as usize].set(square); + self.counts.increment(color, shape); self.mailbox.set(piece, square); Ok(existing_piece) @@ -127,8 +140,12 @@ impl PieceSet { pub(crate) fn remove(&mut self, square: Square) -> Option { if let Some(piece) = self.mailbox.get(square) { - self.color_occupancy[piece.color as usize].clear(square); - self.shape_occupancy[piece.shape as usize].clear(square); + let color_index = piece.color as usize; + let shape_index = piece.shape as usize; + + self.color_occupancy[color_index].clear(square); + self.shape_occupancy[shape_index].clear(square); + self.counts.decrement(piece.color, piece.shape); self.mailbox.remove(square); Some(piece) diff --git a/board/src/piece_sets/counts.rs b/board/src/piece_sets/counts.rs new file mode 100644 index 0000000..effbbe0 --- /dev/null +++ b/board/src/piece_sets/counts.rs @@ -0,0 +1,60 @@ +// Eryn Wells + +use chessfriend_core::{Color, Shape, Square}; + +pub(crate) type Counter = u8; + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub(super) struct Counts([[Counter; Shape::NUM]; Color::NUM]); + +impl Counts { + pub fn get(&self, color: Color, shape: Shape) -> Counter { + self.0[color as usize][shape as usize] + } + + pub fn increment(&mut self, color: Color, shape: Shape) { + #[allow(clippy::cast_possible_truncation)] + const SQUARE_NUM: u8 = Square::NUM as u8; + + let updated_value = self.0[color as usize][shape as usize] + 1; + if updated_value <= SQUARE_NUM { + self.0[color as usize][shape as usize] = updated_value; + } else { + unreachable!("piece count for {color} {shape} overflowed"); + } + } + + pub fn decrement(&mut self, color: Color, shape: Shape) { + let count = self.0[color as usize][shape as usize]; + if let Some(updated_count) = count.checked_sub(1) { + self.0[color as usize][shape as usize] = updated_count; + } else { + unreachable!("piece count for {color} {shape} underflowed"); + } + } + + #[cfg(test)] + fn set(&mut self, color: Color, shape: Shape, value: u8) { + self.0[color as usize][shape as usize] = value; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[should_panic(expected = "underflowed")] + fn underflow() { + let mut counts = Counts::default(); + counts.decrement(Color::White, Shape::Queen); + } + + #[test] + #[should_panic(expected = "overflowed")] + fn overflow() { + let mut counts = Counts::default(); + counts.set(Color::White, Shape::Queen, 64); + counts.increment(Color::White, Shape::Queen); + } +} diff --git a/core/src/colors.rs b/core/src/colors.rs index 5cf633b..ccf62a7 100644 --- a/core/src/colors.rs +++ b/core/src/colors.rs @@ -1,6 +1,6 @@ // Eryn Wells -use crate::Direction; +use crate::{Direction, score::ScoreInner}; use thiserror::Error; #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] @@ -56,6 +56,14 @@ impl Color { Color::Black => "black", } } + + #[must_use] + pub const fn score_factor(self) -> ScoreInner { + match self { + Color::White => 1, + Color::Black => -1, + } + } } impl std::fmt::Display for Color { diff --git a/core/src/lib.rs b/core/src/lib.rs index 0638410..f298a81 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -4,6 +4,7 @@ pub mod colors; pub mod coordinates; pub mod pieces; pub mod random; +pub mod score; pub mod shapes; mod macros; diff --git a/core/src/score.rs b/core/src/score.rs new file mode 100644 index 0000000..eee49f6 --- /dev/null +++ b/core/src/score.rs @@ -0,0 +1,71 @@ +// Eryn Wells + +use std::ops::{Add, AddAssign, Mul, Sub, SubAssign}; + +pub(crate) type ScoreInner = i32; + +/// A score for a position in centipawns. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct Score(ScoreInner); + +impl Score { + #[must_use] + pub const fn zero() -> Self { + Self(0) + } + + #[must_use] + pub const fn new(value: ScoreInner) -> Self { + Self(value) + } +} + +impl Add for Score { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Score(self.0 + rhs.0) + } +} + +impl AddAssign for Score { + fn add_assign(&mut self, rhs: Self) { + self.0 += rhs.0; + } +} + +impl Sub for Score { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + Score(self.0 - rhs.0) + } +} + +impl SubAssign for Score { + fn sub_assign(&mut self, rhs: Self) { + self.0 -= rhs.0; + } +} + +impl Mul for Score { + type Output = Score; + + fn mul(self, rhs: ScoreInner) -> Self::Output { + Score(self.0 * rhs) + } +} + +impl Mul for ScoreInner { + type Output = Score; + + fn mul(self, rhs: Score) -> Self::Output { + Score(self * rhs.0) + } +} + +impl From for Score { + fn from(value: ScoreInner) -> Self { + Score(value) + } +} diff --git a/core/src/shapes.rs b/core/src/shapes.rs index 2c6b7e9..8184f1d 100644 --- a/core/src/shapes.rs +++ b/core/src/shapes.rs @@ -3,6 +3,8 @@ use std::{array, fmt, slice, str::FromStr}; use thiserror::Error; +use crate::score::Score; + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum Shape { Pawn = 0, @@ -71,6 +73,17 @@ impl Shape { pub fn is_promotable(&self) -> bool { Self::PROMOTABLE_SHAPES.contains(self) } + + #[must_use] + pub fn score(self) -> Score { + match self { + Shape::Pawn => Score::new(100), + Shape::Knight | Shape::Bishop => Score::new(300), + Shape::Rook => Score::new(500), + Shape::Queen => Score::new(900), + Shape::King => Score::new(20000), + } + } } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] diff --git a/position/src/evaluation.rs b/position/src/evaluation.rs new file mode 100644 index 0000000..883398b --- /dev/null +++ b/position/src/evaluation.rs @@ -0,0 +1,62 @@ +// Eryn Wells + +use chessfriend_board::Board; +use chessfriend_core::{Color, Piece, Shape, score::Score}; + +struct Evaluator; + +impl Evaluator { + pub fn evaluate_symmetric_unwrapped(board: &Board, color: Color) -> Score { + let material_balance = Self::material_balance(board, color); + + let to_move_factor = color.score_factor(); + + to_move_factor * material_balance + } + + /// Evaluate a board using the symmetric evaluation algorithm defined by + /// Claude Shannon. + fn material_balance(board: &Board, color: Color) -> Score { + let other_color = color.other(); + + Shape::into_iter().fold(Score::zero(), |acc, shape| { + let (active_pieces, other_pieces) = ( + board.count_piece(&Piece::new(color, shape)) as i32, + board.count_piece(&Piece::new(other_color, shape)) as i32, + ); + + let factor = shape.score() * (active_pieces - other_pieces); + + acc + factor + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use chessfriend_board::fen; + + #[test] + fn pawn_material_balance() -> Result<(), Box> { + let board = fen!("8/8/8/8/8/3P4/8/8 w - - 0 1")?; + assert_eq!( + Evaluator::material_balance(&board, Color::White), + 100i32.into() + ); + + let board = fen!("8/8/3p4/8/8/3P4/8/8 w - - 0 1")?; + assert_eq!(Evaluator::material_balance(&board, Color::White), 0.into()); + + Ok(()) + } + + #[test] + fn starting_position_is_even() { + let board = Board::starting(None); + assert_eq!( + Evaluator::evaluate_symmetric_unwrapped(&board, Color::White), + Evaluator::evaluate_symmetric_unwrapped(&board, Color::Black) + ); + } +} diff --git a/position/src/lib.rs b/position/src/lib.rs index d0a1ec5..7ccee47 100644 --- a/position/src/lib.rs +++ b/position/src/lib.rs @@ -1,11 +1,12 @@ // Eryn Wells +mod evaluation; mod position; #[macro_use] mod macros; -pub use chessfriend_board::{fen, PlacePieceError, PlacePieceStrategy}; +pub use chessfriend_board::{PlacePieceError, PlacePieceStrategy, fen}; pub use chessfriend_moves::{GeneratedMove, ValidateMove}; pub use position::Position; From a91bb8c9838ca9eabe4ec407a041ebfb52e441a9 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 20 Jun 2025 14:24:16 -0700 Subject: [PATCH 398/423] [board] Remove the unused Mailbox::new method Just use Mailbox::default(). --- board/src/piece_sets/mailbox.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/board/src/piece_sets/mailbox.rs b/board/src/piece_sets/mailbox.rs index 5d73561..74b32bc 100644 --- a/board/src/piece_sets/mailbox.rs +++ b/board/src/piece_sets/mailbox.rs @@ -7,10 +7,6 @@ use std::iter::FromIterator; pub(crate) struct Mailbox([Option; Square::NUM]); impl Mailbox { - pub fn new() -> Self { - Self::default() - } - pub fn get(&self, square: Square) -> Option { self.0[square as usize] } @@ -46,7 +42,7 @@ impl From<[Option; Square::NUM]> for Mailbox { impl FromIterator<(Square, Piece)> for Mailbox { fn from_iter>(iter: T) -> Self { iter.into_iter() - .fold(Self::new(), |mut mailbox, (square, piece)| { + .fold(Self::default(), |mut mailbox, (square, piece)| { mailbox.set(piece, square); mailbox }) @@ -61,7 +57,7 @@ mod tests { #[test] fn iter_returns_all_pieces() { - let mut mailbox = Mailbox::new(); + let mut mailbox = Mailbox::default(); mailbox.set(piece!(White Queen), Square::C3); mailbox.set(piece!(White Rook), Square::H8); mailbox.set(piece!(Black Bishop), Square::E4); From abaf277fb48fdf5aa4452fcac07a5d5ac0001e85 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 20 Jun 2025 14:25:10 -0700 Subject: [PATCH 399/423] [core] Use the matches! macro to calculate the value of Shape::is_promotable I learned about this macro a little while ago and it's better than writing out a match block by hand, and also doesn't require static or const data, like the previous implementation did. --- core/src/shapes.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/shapes.rs b/core/src/shapes.rs index 8184f1d..0cedc7c 100644 --- a/core/src/shapes.rs +++ b/core/src/shapes.rs @@ -71,7 +71,7 @@ impl Shape { #[must_use] pub fn is_promotable(&self) -> bool { - Self::PROMOTABLE_SHAPES.contains(self) + matches!(self, Self::Knight | Self::Bishop | Self::Rook | Self::Queen) } #[must_use] From 4ae1fd62b7bf59bea637388f2cfb5efa838957d7 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 20 Jun 2025 14:25:27 -0700 Subject: [PATCH 400/423] [perft] Remove an unused Move import This was causing a warning. --- position/src/perft.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/position/src/perft.rs b/position/src/perft.rs index 3269a04..683cb67 100644 --- a/position/src/perft.rs +++ b/position/src/perft.rs @@ -1,7 +1,5 @@ // Eryn Wells -use chessfriend_moves::Move; - use crate::{GeneratedMove, Position, ValidateMove}; use std::fmt; From f84319272cb61cc759178bd930ffbcb22e54e58e Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 21 Jun 2025 21:07:26 -0700 Subject: [PATCH 401/423] [explorer, position] Make Position.board private to the crate Export a Position::board() method that returns a reference to the internal Board. --- explorer/src/main.rs | 4 ++-- position/src/position.rs | 7 ++++++- position/tests/peterellisjones.rs | 8 ++++---- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/explorer/src/main.rs b/explorer/src/main.rs index 0acb2a1..b6af7a6 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -187,7 +187,7 @@ fn respond(line: &str, state: &mut State) -> anyhow::Result { } fn do_flags_command(state: &mut State, _matches: &clap::ArgMatches) -> CommandResult { - let board = &state.position.board; + let board = state.position.board(); println!("Castling:"); @@ -390,7 +390,7 @@ fn main() -> Result<(), String> { loop { if should_print_position { println!("{}", &state.position); - println!("{} to move.", state.position.board.active_color()); + println!("{} to move.", state.position.active_color()); } let readline = editor.readline("\n? "); diff --git a/position/src/position.rs b/position/src/position.rs index 8787134..d34ce4e 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -24,7 +24,7 @@ use std::{collections::HashSet, fmt, sync::Arc}; #[must_use] #[derive(Clone, Debug, Default, Eq)] pub struct Position { - pub board: Board, + pub(crate) board: Board, pub(crate) moves: Vec, pub(crate) captures: CapturesList, @@ -48,6 +48,11 @@ impl Position { ..Default::default() } } + + #[must_use] + pub fn board(&self) -> &Board { + &self.board + } } impl Position { diff --git a/position/tests/peterellisjones.rs b/position/tests/peterellisjones.rs index eadcf4d..ea90a2f 100644 --- a/position/tests/peterellisjones.rs +++ b/position/tests/peterellisjones.rs @@ -8,7 +8,7 @@ use chessfriend_core::Color; use chessfriend_moves::{ - assert_move_list, assert_move_list_contains, assert_move_list_does_not_contain, ply, Move, + Move, assert_move_list, assert_move_list_contains, assert_move_list_does_not_contain, ply, }; use chessfriend_position::test_position; use std::collections::HashSet; @@ -107,7 +107,7 @@ fn en_passant_check_capture() { White Pawn on D4, ], D3); - assert!(pos.board.active_color_is_in_check()); + assert!(pos.board().active_color_is_in_check()); let generated_moves: HashSet<_> = pos.all_legal_moves(Some(Color::Black)).collect(); @@ -123,7 +123,7 @@ fn en_passant_check_block() { White Queen on F1, ], D3); - assert!(pos.board.active_color_is_in_check()); + assert!(pos.board().active_color_is_in_check()); let generated_moves: HashSet<_> = pos.all_legal_moves(Some(Color::Black)).collect(); @@ -139,7 +139,7 @@ fn pinned_pieces_rook_cannot_move_out_of_pin() { White King on C1, ]); - assert!(!pos.board.active_color_is_in_check()); + assert!(!pos.board().active_color_is_in_check()); let rook_moves: HashSet<_> = pos.all_legal_moves(None).collect(); From 4b96db230d269c5fddc6d115435f4abcd8bcffb1 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 21 Jun 2025 21:08:04 -0700 Subject: [PATCH 402/423] [board, moves] Derive Clone on several error types - PlacePieceError - MakeMoveError - UnmakeMoveError --- board/src/piece_sets.rs | 2 +- moves/src/make_move.rs | 2 +- moves/src/unmake_move.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/board/src/piece_sets.rs b/board/src/piece_sets.rs index 316c1d8..de43caa 100644 --- a/board/src/piece_sets.rs +++ b/board/src/piece_sets.rs @@ -21,7 +21,7 @@ pub enum PlacePieceStrategy { PreserveExisting, } -#[derive(Debug, Error, Eq, PartialEq)] +#[derive(Clone, Debug, Error, Eq, PartialEq)] pub enum PlacePieceError { #[error("cannot place piece on {square} with existing {piece}")] ExisitingPiece { piece: Piece, square: Square }, diff --git a/moves/src/make_move.rs b/moves/src/make_move.rs index f413e72..bcbca0a 100644 --- a/moves/src/make_move.rs +++ b/moves/src/make_move.rs @@ -18,7 +18,7 @@ pub enum ValidateMove { Yes, } -#[derive(Debug, Error, Eq, PartialEq)] +#[derive(Clone, Debug, Error, Eq, PartialEq)] pub enum MakeMoveError { #[error("no piece on {0}")] NoPiece(Square), diff --git a/moves/src/unmake_move.rs b/moves/src/unmake_move.rs index 69c43b7..6833608 100644 --- a/moves/src/unmake_move.rs +++ b/moves/src/unmake_move.rs @@ -7,7 +7,7 @@ use thiserror::Error; pub type UnmakeMoveResult = Result<(), UnmakeMoveError>; -#[derive(Debug, Error, Eq, PartialEq)] +#[derive(Clone, Debug, Error, Eq, PartialEq)] pub enum UnmakeMoveError { #[error("no move to unmake")] NoMove, From 54d9c3838dd7da7204fbf14c98e98438fbc3a835 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 21 Jun 2025 21:08:32 -0700 Subject: [PATCH 403/423] [position] Export Position::active_color() Passes through to the Board method. --- position/src/position.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/position/src/position.rs b/position/src/position.rs index d34ce4e..e318374 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -53,6 +53,11 @@ impl Position { pub fn board(&self) -> &Board { &self.board } + + #[must_use] + pub fn active_color(&self) -> Color { + self.board.active_color() + } } impl Position { From 9f2dc3fa7606d53b051a223c01160398b8ac899f Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 21 Jun 2025 21:09:01 -0700 Subject: [PATCH 404/423] [position] Update import ordering in position.rs --- position/src/position.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/position/src/position.rs b/position/src/position.rs index e318374..5a3ae69 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -6,18 +6,18 @@ use crate::fen::{FromFenStr, FromFenStrError}; use captures::CapturesList; use chessfriend_bitboard::BitBoard; use chessfriend_board::{ - display::DiagramFormatter, fen::ToFenStr, Board, PlacePieceError, PlacePieceStrategy, - ZobristState, + Board, PlacePieceError, PlacePieceStrategy, ZobristState, display::DiagramFormatter, + fen::ToFenStr, }; use chessfriend_core::{Color, Piece, Shape, Square}; use chessfriend_moves::{ + GeneratedMove, MakeMove, MakeMoveError, Move, MoveRecord, UnmakeMove, UnmakeMoveError, + UnmakeMoveResult, ValidateMove, algebraic::AlgebraicMoveComponents, generators::{ AllPiecesMoveGenerator, BishopMoveGenerator, KingMoveGenerator, KnightMoveGenerator, PawnMoveGenerator, QueenMoveGenerator, RookMoveGenerator, }, - GeneratedMove, MakeMove, MakeMoveError, Move, MoveRecord, UnmakeMove, UnmakeMoveError, - UnmakeMoveResult, ValidateMove, }; use std::{collections::HashSet, fmt, sync::Arc}; @@ -354,7 +354,7 @@ impl fmt::Display for Position { #[cfg(test)] mod tests { use super::*; - use crate::{test_position, Position}; + use crate::{Position, test_position}; use chessfriend_core::piece; #[test] From 80ac8ea036093307ae51277db1d96427f3883545 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 24 Jun 2025 15:18:49 -0700 Subject: [PATCH 405/423] [core] Import std::fmt and remove std:: from Display symbol spelling --- core/src/colors.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/src/colors.rs b/core/src/colors.rs index ccf62a7..53e2c1e 100644 --- a/core/src/colors.rs +++ b/core/src/colors.rs @@ -1,6 +1,7 @@ // Eryn Wells use crate::{Direction, score::ScoreInner}; +use std::fmt; use thiserror::Error; #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] @@ -66,8 +67,8 @@ impl Color { } } -impl std::fmt::Display for Color { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Display for Color { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "{}", From 4e80cc36ca9234830c3f6f47a64a0f3382168386 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 24 Jun 2025 15:20:31 -0700 Subject: [PATCH 406/423] [core] Implement Display for Score --- core/src/score.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/core/src/score.rs b/core/src/score.rs index eee49f6..a7f24cd 100644 --- a/core/src/score.rs +++ b/core/src/score.rs @@ -1,6 +1,9 @@ // Eryn Wells -use std::ops::{Add, AddAssign, Mul, Sub, SubAssign}; +use std::{ + fmt, + ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign}, +}; pub(crate) type ScoreInner = i32; @@ -69,3 +72,16 @@ impl From for Score { Score(value) } } + +impl fmt::Display for Score { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let value = self.0; + if *self == Self::MAX { + write!(f, "INF") + } else if *self == Self::MIN { + write!(f, "-INF") + } else { + write!(f, "{value}cp") + } + } +} From 1ae6d5df4887a4b9979be0cab41eeba0a23d913c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 24 Jun 2025 20:00:04 -0700 Subject: [PATCH 407/423] =?UTF-8?q?[core,=20position]=20Rename=20the=20typ?= =?UTF-8?q?e=20of=20Score's=20inner=20value=20=E2=86=92=20Value?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/src/colors.rs | 4 +-- core/src/score.rs | 65 ++++++++++++++++++++++++++++++-------- position/src/evaluation.rs | 6 ++-- 3 files changed, 57 insertions(+), 18 deletions(-) diff --git a/core/src/colors.rs b/core/src/colors.rs index 53e2c1e..e1255b7 100644 --- a/core/src/colors.rs +++ b/core/src/colors.rs @@ -1,6 +1,6 @@ // Eryn Wells -use crate::{Direction, score::ScoreInner}; +use crate::{Direction, score}; use std::fmt; use thiserror::Error; @@ -59,7 +59,7 @@ impl Color { } #[must_use] - pub const fn score_factor(self) -> ScoreInner { + pub const fn score_factor(self) -> score::Value { match self { Color::White => 1, Color::Black => -1, diff --git a/core/src/score.rs b/core/src/score.rs index a7f24cd..2dfd7c9 100644 --- a/core/src/score.rs +++ b/core/src/score.rs @@ -5,22 +5,53 @@ use std::{ ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign}, }; -pub(crate) type ScoreInner = i32; +pub(crate) type Value = i32; /// A score for a position in centipawns. #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] -pub struct Score(ScoreInner); +pub struct Score(Value); impl Score { - #[must_use] - pub const fn zero() -> Self { - Self(0) - } + pub const ZERO: Score = Score(0); + + /// The minimum possible value of a score. Notably, this is *not* the + /// minimum value for the inner integer value so negation works correctly. + /// This property is important during search, which relies on being able to + /// negate "infinity". + /// + /// ## Examples + /// + /// ``` + /// use chessfriend_core::score::Score; + /// assert_eq!(!Score::MIN, Score::MAX); + /// ``` + /// + pub const MIN: Score = Score(Value::MIN + 1); + + /// The maximum possible value of a score. + pub const MAX: Score = Score(Value::MAX); + + const CENTIPAWNS_PER_POINT: f32 = 100.0; #[must_use] - pub const fn new(value: ScoreInner) -> Self { + pub const fn new(value: Value) -> Self { Self(value) } + + /// Returns `true` if this [`Score`] is zero. + /// + /// ## Examples + /// + /// ``` + /// use chessfriend_core::score::Score; + /// assert!(Score::ZERO.is_zero()); + /// assert!(Score::new(0).is_zero()); + /// ``` + /// + #[must_use] + pub const fn is_zero(&self) -> bool { + self.0 == 0 + } } impl Add for Score { @@ -51,15 +82,15 @@ impl SubAssign for Score { } } -impl Mul for Score { - type Output = Score; +impl Mul for Score { + type Output = Self; - fn mul(self, rhs: ScoreInner) -> Self::Output { + fn mul(self, rhs: Value) -> Self::Output { Score(self.0 * rhs) } } -impl Mul for ScoreInner { +impl Mul for Value { type Output = Score; fn mul(self, rhs: Score) -> Self::Output { @@ -67,8 +98,16 @@ impl Mul for ScoreInner { } } -impl From for Score { - fn from(value: ScoreInner) -> Self { +impl Neg for Score { + type Output = Self; + + fn neg(self) -> Self::Output { + Score(-self.0) + } +} + +impl From for Score { + fn from(value: Value) -> Self { Score(value) } } diff --git a/position/src/evaluation.rs b/position/src/evaluation.rs index 883398b..b776777 100644 --- a/position/src/evaluation.rs +++ b/position/src/evaluation.rs @@ -19,10 +19,10 @@ impl Evaluator { fn material_balance(board: &Board, color: Color) -> Score { let other_color = color.other(); - Shape::into_iter().fold(Score::zero(), |acc, shape| { + Shape::into_iter().fold(Score::ZERO, |acc, shape| { let (active_pieces, other_pieces) = ( - board.count_piece(&Piece::new(color, shape)) as i32, - board.count_piece(&Piece::new(other_color, shape)) as i32, + i32::from(board.count_piece(&Piece::new(color, shape))), + i32::from(board.count_piece(&Piece::new(other_color, shape))), ); let factor = shape.score() * (active_pieces - other_pieces); From 74c0e4144f875dd0291a7ff189a0b7f3ae3b767c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 24 Jun 2025 20:04:41 -0700 Subject: [PATCH 408/423] [position] Remove the to_move_factor from symmetric evaluation Just use material balance. --- position/src/evaluation.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/position/src/evaluation.rs b/position/src/evaluation.rs index b776777..8496760 100644 --- a/position/src/evaluation.rs +++ b/position/src/evaluation.rs @@ -1,17 +1,20 @@ // Eryn Wells +use crate::Position; use chessfriend_board::Board; use chessfriend_core::{Color, Piece, Shape, score::Score}; struct Evaluator; impl Evaluator { - pub fn evaluate_symmetric_unwrapped(board: &Board, color: Color) -> Score { + pub fn evaluate_symmetric_unwrapped(position: &Position, color: Color) -> Score { + let board = &position.board; + let material_balance = Self::material_balance(board, color); - let to_move_factor = color.score_factor(); + let score = material_balance; - to_move_factor * material_balance + score } /// Evaluate a board using the symmetric evaluation algorithm defined by @@ -53,10 +56,10 @@ mod tests { #[test] fn starting_position_is_even() { - let board = Board::starting(None); + let position = Position::new(Board::starting(None)); assert_eq!( - Evaluator::evaluate_symmetric_unwrapped(&board, Color::White), - Evaluator::evaluate_symmetric_unwrapped(&board, Color::Black) + Evaluator::evaluate_symmetric_unwrapped(&position, Color::White), + Evaluator::evaluate_symmetric_unwrapped(&position, Color::Black) ); } } From 8db533cb5298e49d01cd7b599da02bcfb4e7cd95 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 27 Jun 2025 08:44:56 -0700 Subject: [PATCH 409/423] [board] Use $crate in the fen! macro so you don't have to import Board to get one back --- board/src/fen.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/board/src/fen.rs b/board/src/fen.rs index a44b735..fe418a5 100644 --- a/board/src/fen.rs +++ b/board/src/fen.rs @@ -11,7 +11,7 @@ use thiserror::Error; macro_rules! fen { ($fen_string:literal) => {{ use $crate::fen::FromFenStr; - Board::from_fen_str($fen_string) + $crate::Board::from_fen_str($fen_string) }}; } From e7fd65672d432c92ba8c4c3abe883b3d7d0e8550 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 29 Jun 2025 09:18:44 -0700 Subject: [PATCH 410/423] [bitboard, board] Make BitBoard::EMPTY and BitBoard::FULL public MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deprecate the methods. I think I'm undoing a change I made earlier. 🙃 --- bitboard/src/bitboard.rs | 6 ++++-- bitboard/src/lib.rs | 2 +- board/src/movement.rs | 2 +- board/src/sight.rs | 6 +++--- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index b9e4c1c..1897de4 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -43,13 +43,15 @@ macro_rules! moves_getter { } impl BitBoard { - const EMPTY: BitBoard = BitBoard(u64::MIN); - const FULL: BitBoard = BitBoard(u64::MAX); + pub const EMPTY: BitBoard = BitBoard(u64::MIN); + pub const FULL: BitBoard = BitBoard(u64::MAX); + #[deprecated(note = "Use BitBoard::EMPTY instead")] pub const fn empty() -> BitBoard { Self::EMPTY } + #[deprecated(note = "Use BitBoard::FULL instead")] pub const fn full() -> BitBoard { Self::FULL } diff --git a/bitboard/src/lib.rs b/bitboard/src/lib.rs index 798e51b..12a25ed 100644 --- a/bitboard/src/lib.rs +++ b/bitboard/src/lib.rs @@ -14,7 +14,7 @@ pub use direction::IterationDirection; macro_rules! bitboard { ($($sq:ident)* $(,)?) => { { - let mut bitboard = $crate::BitBoard::empty(); + let mut bitboard = $crate::BitBoard::EMPTY; $(bitboard.set(chessfriend_core::Square::$sq);)* bitboard } diff --git a/board/src/movement.rs b/board/src/movement.rs index 4fa4381..8cb8be9 100644 --- a/board/src/movement.rs +++ b/board/src/movement.rs @@ -13,7 +13,7 @@ impl Board { if let Some(piece) = self.get_piece(square) { piece.movement(square, self) } else { - BitBoard::empty() + BitBoard::EMPTY } } } diff --git a/board/src/sight.rs b/board/src/sight.rs index 81fb120..f6378b2 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -27,7 +27,7 @@ impl Board { if let Some(piece) = self.get_piece(square) { piece.sight(square, self) } else { - BitBoard::empty() + BitBoard::EMPTY } } @@ -41,7 +41,7 @@ impl Board { self.friendly_occupancy(color) .occupied_squares(&IterationDirection::default()) .map(|square| self.sight(square)) - .fold(BitBoard::empty(), BitOr::bitor) + .fold(BitBoard::EMPTY, BitOr::bitor) } pub fn active_color_opposing_sight(&self) -> BitBoard { @@ -60,7 +60,7 @@ impl Board { Some(self.friendly_sight(c)) } }) - .fold(BitBoard::empty(), BitOr::bitor) + .fold(BitBoard::EMPTY, BitOr::bitor) } } From a30553503fcb9818975e21f830cab34d8bd494e5 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 29 Jun 2025 09:23:20 -0700 Subject: [PATCH 411/423] [board, explorer, position] Clean up naming of sight and movement methods These methods have a prefix, either `sight` or `movement`, and then follow the conventions for other "trio" clusters where there's an un-suffixed method that takes an Option, a _active method that uses the active color, and a _unwrapped method that takes a bare Color. --- board/src/movement.rs | 6 +++--- board/src/sight.rs | 29 ++++++++++++++++++++++------- explorer/src/main.rs | 16 ++++++++-------- position/src/position.rs | 16 +++++++++------- 4 files changed, 42 insertions(+), 25 deletions(-) diff --git a/board/src/movement.rs b/board/src/movement.rs index 8cb8be9..2935eee 100644 --- a/board/src/movement.rs +++ b/board/src/movement.rs @@ -4,12 +4,12 @@ //! of squares a piece can move to. For all pieces except pawns, the Movement //! set is equal to the Sight set. -use crate::{sight::Sight, Board}; +use crate::{Board, sight::Sight}; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece, Rank, Shape, Square, Wing}; impl Board { - pub fn movement(&self, square: Square) -> BitBoard { + pub fn movement_piece(&self, square: Square) -> BitBoard { if let Some(piece) = self.get_piece(square) { piece.movement(square, self) } else { @@ -93,7 +93,7 @@ fn pawn_pushes(pawn: BitBoard, color: Color, occupancy: BitBoard) -> BitBoard { #[cfg(test)] mod tests { use super::pawn_pushes; - use chessfriend_bitboard::{bitboard, BitBoard}; + use chessfriend_bitboard::{BitBoard, bitboard}; use chessfriend_core::{Color, Square}; #[test] diff --git a/board/src/sight.rs b/board/src/sight.rs index f6378b2..8e5cbc6 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -23,7 +23,7 @@ use std::ops::BitOr; impl Board { /// Compute sight of the piece on the given square. - pub fn sight(&self, square: Square) -> BitBoard { + pub fn sight_piece(&self, square: Square) -> BitBoard { if let Some(piece) = self.get_piece(square) { piece.sight(square, self) } else { @@ -31,8 +31,23 @@ impl Board { } } - pub fn active_sight(&self) -> BitBoard { - self.friendly_sight(self.active_color()) + /// Calculate sight of all pieces of the given [`Color`]. If `color` is + /// `None`, calculate sight of the active color. + pub fn sight(&self, color: Option) -> BitBoard { + self.sight_unwrapped(self.unwrap_color(color)) + } + + /// Calculate sight of all pieces of the active color. + pub fn sight_active(&self) -> BitBoard { + self.sight_unwrapped(self.active_color()) + } + + /// Calculate sight of all pieces of the given [`Color`]. + pub fn sight_unwrapped(&self, color: Color) -> BitBoard { + self.friendly_occupancy(color) + .occupied_squares_leading() + .map(|square| self.sight_piece(square)) + .fold(BitBoard::EMPTY, BitOr::bitor) } /// A [`BitBoard`] of all squares the given color can see. @@ -40,7 +55,7 @@ impl Board { // TODO: Probably want to implement a caching layer here. self.friendly_occupancy(color) .occupied_squares(&IterationDirection::default()) - .map(|square| self.sight(square)) + .map(|square| self.sight_piece(square)) .fold(BitBoard::EMPTY, BitOr::bitor) } @@ -244,7 +259,7 @@ mod tests { White King on E4, ); - let sight = pos.active_sight(); + let sight = pos.sight_active(); assert_eq!(sight, bitboard![E5 F5 F4 F3 E3 D3 D4 D5]); } @@ -267,8 +282,8 @@ mod tests { mod pawn { use crate::{sight::Sight, test_board}; - use chessfriend_bitboard::{bitboard, BitBoard}; - use chessfriend_core::{piece, Square}; + use chessfriend_bitboard::{BitBoard, bitboard}; + use chessfriend_core::{Square, piece}; sight_test!(e4_pawn, piece!(White Pawn), Square::E4, bitboard![D5 F5]); diff --git a/explorer/src/main.rs b/explorer/src/main.rs index b6af7a6..cfaf102 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -65,7 +65,7 @@ fn command_line() -> Command { ) .subcommand( Command::new("sight") - .arg(Arg::new("square").required(true)) + .arg(Arg::new("square").required(false)) .about("Show sight of a piece on a square"), ) .subcommand( @@ -163,12 +163,12 @@ fn respond(line: &str, state: &mut State) -> anyhow::Result { .place_piece(piece, square, PlacePieceStrategy::default())?; } Some(("sight", matches)) => { - let square = matches - .get_one::("square") - .ok_or(CommandHandlingError::MissingArgument("square"))?; - let square = square.parse::()?; - - let sight = state.position.sight(square); + let sight = if let Some(square) = matches.get_one::("square") { + let square: Square = square.parse()?; + state.position.sight_piece(square) + } else { + state.position.sight_active() + }; let display = state.position.display().highlight(sight); println!("\n{display}"); @@ -331,7 +331,7 @@ fn do_movement_command( .get_one::("square") .ok_or(CommandHandlingError::MissingArgument("square"))?; - let movement = state.position.movement(square); + let movement = state.position.movement_piece(square); let display = state.position.display().highlight(movement); println!("\n{display}"); diff --git a/position/src/position.rs b/position/src/position.rs index 5a3ae69..d2a82f3 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -86,12 +86,14 @@ impl Position { } impl Position { - pub fn sight(&self, square: Square) -> BitBoard { - self.board.sight(square) + /// Calculate sight of a piece on the provided [`Square`]. + pub fn sight_piece(&self, square: Square) -> BitBoard { + self.board.sight_piece(square) } - pub fn movement(&self, square: Square) -> BitBoard { - self.board.movement(square) + /// Calculate movement of a piece on the provided [`Square`]. + pub fn movement_piece(&self, square: Square) -> BitBoard { + self.board.movement_piece(square) } } @@ -163,8 +165,8 @@ impl Position { } impl Position { - pub fn active_sight(&self) -> BitBoard { - self.board.active_sight() + pub fn sight_active(&self) -> BitBoard { + self.board.sight_active() } /// A [`BitBoard`] of all squares the given color can see. @@ -285,7 +287,7 @@ impl Position { } let target_bitboard: BitBoard = target.into(); - if !(self.movement(origin) & target_bitboard).is_populated() { + if !(self.movement_piece(origin) & target_bitboard).is_populated() { return None; } From e3d17219ad4648871d12b44cb3bc302094f38a00 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 29 Jun 2025 09:25:08 -0700 Subject: [PATCH 412/423] [board, position] Simplify check methods Only one check-testing method, Board::is_in_check(), that tests if the current active player is in check. It doesn't make sense to test if the non-active player is in check. --- board/src/check.rs | 18 +++++------------- position/src/position.rs | 2 +- position/tests/peterellisjones.rs | 6 +++--- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/board/src/check.rs b/board/src/check.rs index ba9f06d..6d8ceba 100644 --- a/board/src/check.rs +++ b/board/src/check.rs @@ -5,18 +5,10 @@ use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece}; impl Board { + /// Return whether the active color is in check. #[must_use] - pub fn active_color_is_in_check(&self) -> bool { - self.unwrapped_color_is_in_check(self.active_color()) - } - - #[must_use] - pub fn color_is_in_check(&self, color: Option) -> bool { - self.unwrapped_color_is_in_check(self.unwrap_color(color)) - } - - #[must_use] - pub fn unwrapped_color_is_in_check(&self, color: Color) -> bool { + pub fn is_in_check(&self) -> bool { + let color = self.active_color(); let king = self.king_bitboard(color); let opposing_sight = self.opposing_sight(color); (king & opposing_sight).is_populated() @@ -39,7 +31,7 @@ mod tests { Black Rook on F3, ); - assert!(board.unwrapped_color_is_in_check(Color::White)); + assert!(board.is_in_check()); } #[test] @@ -49,6 +41,6 @@ mod tests { Black Rook on B4, ); - assert!(!board.unwrapped_color_is_in_check(Color::White)); + assert!(!board.is_in_check()); } } diff --git a/position/src/position.rs b/position/src/position.rs index d2a82f3..1763875 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -129,7 +129,7 @@ impl Position { ); }); - let move_is_legal = !test_board.color_is_in_check(Some(active_color_before_move)); + let move_is_legal = !test_board.is_in_check(); test_board.unmake_move(&record).unwrap_or_else(|err| { panic!( diff --git a/position/tests/peterellisjones.rs b/position/tests/peterellisjones.rs index ea90a2f..06ccc45 100644 --- a/position/tests/peterellisjones.rs +++ b/position/tests/peterellisjones.rs @@ -107,7 +107,7 @@ fn en_passant_check_capture() { White Pawn on D4, ], D3); - assert!(pos.board().active_color_is_in_check()); + assert!(pos.board().is_in_check()); let generated_moves: HashSet<_> = pos.all_legal_moves(Some(Color::Black)).collect(); @@ -123,7 +123,7 @@ fn en_passant_check_block() { White Queen on F1, ], D3); - assert!(pos.board().active_color_is_in_check()); + assert!(pos.board().is_in_check()); let generated_moves: HashSet<_> = pos.all_legal_moves(Some(Color::Black)).collect(); @@ -139,7 +139,7 @@ fn pinned_pieces_rook_cannot_move_out_of_pin() { White King on C1, ]); - assert!(!pos.board().active_color_is_in_check()); + assert!(!pos.board().is_in_check()); let rook_moves: HashSet<_> = pos.all_legal_moves(None).collect(); From a904e4a5bb2163c3bcfcb25a520878cbfc189365 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 30 Jun 2025 15:37:35 -0700 Subject: [PATCH 413/423] [bitboard, board] Replace ray_in_direction! macro with a function This is simpler than writing a macro, at the expense of some overhead for calling a function. But the Rust compiler might inline it anyway! To support this change, implement BitBoard::first_occupied_square_direction, which iterates a bitboard in a direction (i.e. leading or trailing) depending on the core::Direction value passed to it. --- bitboard/src/bitboard.rs | 12 ++++++++ board/src/sight.rs | 62 +++++++++++++++++++--------------------- 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 1897de4..f896e57 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -243,6 +243,18 @@ impl BitBoard { TrailingBitScanner::new(self.0) } + #[must_use] + pub fn first_occupied_square_direction(&self, direction: Direction) -> Option { + match direction { + Direction::North | Direction::NorthEast | Direction::NorthWest | Direction::East => { + self.first_occupied_square_trailing() + } + Direction::SouthEast | Direction::South | Direction::SouthWest | Direction::West => { + self.first_occupied_square_leading() + } + } + } + #[must_use] pub fn first_occupied_square(&self, direction: &IterationDirection) -> Option { match direction { diff --git a/board/src/sight.rs b/board/src/sight.rs index 8e5cbc6..b8e366e 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -138,20 +138,6 @@ struct SightInfo { friendly_occupancy: BitBoard, } -macro_rules! ray_in_direction { - ($square:expr, $blockers:expr, $direction:ident, $first_occupied_square:tt) => {{ - let ray = BitBoard::ray($square, Direction::$direction); - let ray_blockers = ray & $blockers; - if let Some(first_occupied_square) = ray_blockers.$first_occupied_square() { - let remainder = BitBoard::ray(first_occupied_square, Direction::$direction); - let attack_ray = ray & !remainder; - attack_ray - } else { - ray - } - }}; -} - /// Compute sight of a white pawn. fn white_pawn_sight(info: &SightInfo, en_passant_square: BitBoard) -> BitBoard { let possible_squares = !info.friendly_occupancy | en_passant_square; @@ -175,15 +161,27 @@ fn knight_sight(info: &SightInfo) -> BitBoard { BitBoard::knight_moves(info.square) } +fn ray_in_direction(square: Square, blockers: BitBoard, direction: Direction) -> BitBoard { + let ray = BitBoard::ray(square, direction); + + let ray_blockers = ray & blockers; + if let Some(first_occupied_square) = ray_blockers.first_occupied_square_direction(direction) { + let remainder = BitBoard::ray(first_occupied_square, direction); + let attack_ray = ray & !remainder; + attack_ray + } else { + ray + } +} + fn bishop_sight(info: &SightInfo) -> BitBoard { let bishop = info.square; let occupancy = info.occupancy; - #[rustfmt::skip] - let sight = ray_in_direction!(bishop, occupancy, NorthEast, first_occupied_square_trailing) - | ray_in_direction!(bishop, occupancy, SouthEast, first_occupied_square_leading) - | ray_in_direction!(bishop, occupancy, SouthWest, first_occupied_square_leading) - | ray_in_direction!(bishop, occupancy, NorthWest, first_occupied_square_trailing); + let sight = ray_in_direction(bishop, occupancy, Direction::NorthEast) + | ray_in_direction(bishop, occupancy, Direction::SouthEast) + | ray_in_direction(bishop, occupancy, Direction::SouthWest) + | ray_in_direction(bishop, occupancy, Direction::NorthWest); sight } @@ -192,11 +190,10 @@ fn rook_sight(info: &SightInfo) -> BitBoard { let rook = info.square; let occupancy = info.occupancy; - #[rustfmt::skip] - let sight = ray_in_direction!(rook, occupancy, North, first_occupied_square_trailing) - | ray_in_direction!(rook, occupancy, East, first_occupied_square_trailing) - | ray_in_direction!(rook, occupancy, South, first_occupied_square_leading) - | ray_in_direction!(rook, occupancy, West, first_occupied_square_leading); + let sight = ray_in_direction(rook, occupancy, Direction::North) + | ray_in_direction(rook, occupancy, Direction::East) + | ray_in_direction(rook, occupancy, Direction::South) + | ray_in_direction(rook, occupancy, Direction::West); sight } @@ -205,15 +202,14 @@ fn queen_sight(info: &SightInfo) -> BitBoard { let queen = info.square; let occupancy = info.occupancy; - #[rustfmt::skip] - let sight = ray_in_direction!(queen, occupancy, NorthWest, first_occupied_square_trailing) - | ray_in_direction!(queen, occupancy, North, first_occupied_square_trailing) - | ray_in_direction!(queen, occupancy, NorthEast, first_occupied_square_trailing) - | ray_in_direction!(queen, occupancy, East, first_occupied_square_trailing) - | ray_in_direction!(queen, occupancy, SouthEast, first_occupied_square_leading) - | ray_in_direction!(queen, occupancy, South, first_occupied_square_leading) - | ray_in_direction!(queen, occupancy, SouthWest, first_occupied_square_leading) - | ray_in_direction!(queen, occupancy, West, first_occupied_square_leading); + let sight = ray_in_direction(queen, occupancy, Direction::NorthWest) + | ray_in_direction(queen, occupancy, Direction::North) + | ray_in_direction(queen, occupancy, Direction::NorthEast) + | ray_in_direction(queen, occupancy, Direction::East) + | ray_in_direction(queen, occupancy, Direction::SouthEast) + | ray_in_direction(queen, occupancy, Direction::South) + | ray_in_direction(queen, occupancy, Direction::SouthWest) + | ray_in_direction(queen, occupancy, Direction::West); sight } From 45183c910ca95609b1e318cd7359fe22d600a1cf Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 17:08:25 -0700 Subject: [PATCH 414/423] [bitboard] Replace some references to BitBoard::full() and BitBoard::empty() with the const values Two doc tests reference the methods instead of the const variables. Update them. --- bitboard/src/bitboard.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index f896e57..a16297d 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -160,9 +160,9 @@ impl BitBoard { /// /// ``` /// use chessfriend_bitboard::BitBoard; - /// assert_eq!(BitBoard::empty().population_count(), 0); + /// assert_eq!(BitBoard::EMPTY.population_count(), 0); /// assert_eq!(BitBoard::new(0b01011110010).population_count(), 6); - /// assert_eq!(BitBoard::full().population_count(), 64); + /// assert_eq!(BitBoard::FULL.population_count(), 64); /// ``` #[must_use] pub const fn population_count(&self) -> u32 { @@ -211,8 +211,8 @@ impl BitBoard { /// /// ``` /// use chessfriend_bitboard::BitBoard; - /// assert!(!BitBoard::empty().is_single_square(), "Empty bitboards represent no squares"); - /// assert!(!BitBoard::full().is_single_square(), "Full bitboards represent all the squares"); + /// assert!(!BitBoard::EMPTY.is_single_square(), "Empty bitboards represent no squares"); + /// assert!(!BitBoard::FULL.is_single_square(), "Full bitboards represent all the squares"); /// assert!(!BitBoard::new(0b010011110101101100).is_single_square(), "This bitboard represents a bunch of squares"); /// assert!(BitBoard::new(0b10000000000000).is_single_square()); /// ``` From 484fcf342e4e85d4734b1343f4b955ffa72a6227 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 17:09:15 -0700 Subject: [PATCH 415/423] [board] Remove a useless .into() call Clippy pointed this out to me. This .into() call serves no purpose. --- board/src/castle.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/board/src/castle.rs b/board/src/castle.rs index 5acdaaf..4ba9a4b 100644 --- a/board/src/castle.rs +++ b/board/src/castle.rs @@ -46,7 +46,7 @@ impl Board { let color = self.unwrap_color(color); - if !self.has_castling_right_unwrapped(color, wing.into()) { + if !self.has_castling_right_unwrapped(color, wing) { return Err(CastleEvaluationError::NoRights { color, wing }); } From b3ff8dec49ee2e8cddd1718da3d8801bccbd9910 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 17:09:55 -0700 Subject: [PATCH 416/423] [core] Make Shape::is_promotable() const --- core/src/shapes.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/shapes.rs b/core/src/shapes.rs index 0cedc7c..9edbbb5 100644 --- a/core/src/shapes.rs +++ b/core/src/shapes.rs @@ -70,7 +70,7 @@ impl Shape { } #[must_use] - pub fn is_promotable(&self) -> bool { + pub const fn is_promotable(&self) -> bool { matches!(self, Self::Knight | Self::Bishop | Self::Rook | Self::Queen) } From b50560692594f5c1b290634c7f72422ca329ad20 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 17:11:52 -0700 Subject: [PATCH 417/423] [core] Export Score::CENTIPAWNS_PER_POINT to the crate This constant is a conversion factor of points to the internal fixed point unit of centipawns. Points are more familiar to people because pawns are worth 1 pt. Calculate the scores of the various piece shapes with this constant. --- core/src/score.rs | 2 +- core/src/shapes.rs | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/core/src/score.rs b/core/src/score.rs index 2dfd7c9..44d0478 100644 --- a/core/src/score.rs +++ b/core/src/score.rs @@ -31,7 +31,7 @@ impl Score { /// The maximum possible value of a score. pub const MAX: Score = Score(Value::MAX); - const CENTIPAWNS_PER_POINT: f32 = 100.0; + pub(crate) const CENTIPAWNS_PER_POINT: f32 = 100.0; #[must_use] pub const fn new(value: Value) -> Self { diff --git a/core/src/shapes.rs b/core/src/shapes.rs index 9edbbb5..77126ba 100644 --- a/core/src/shapes.rs +++ b/core/src/shapes.rs @@ -75,13 +75,16 @@ impl Shape { } #[must_use] - pub fn score(self) -> Score { + pub const fn score(self) -> Score { + #[allow(clippy::cast_possible_truncation)] + const CP_PER_PT: i32 = Score::CENTIPAWNS_PER_POINT as i32; + match self { - Shape::Pawn => Score::new(100), - Shape::Knight | Shape::Bishop => Score::new(300), - Shape::Rook => Score::new(500), - Shape::Queen => Score::new(900), - Shape::King => Score::new(20000), + Shape::Pawn => Score::new(CP_PER_PT), + Shape::Knight | Shape::Bishop => Score::new(3 * CP_PER_PT), + Shape::Rook => Score::new(5 * CP_PER_PT), + Shape::Queen => Score::new(9 * CP_PER_PT), + Shape::King => Score::new(200 * CP_PER_PT), } } } From 146e4d34d3b9daf89a1347fe6ef6ee617b12216b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 17:12:34 -0700 Subject: [PATCH 418/423] [core] Fix an incorrect assertion in the Score doc test Negate with - instead of with !. --- core/src/score.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/score.rs b/core/src/score.rs index 44d0478..3528861 100644 --- a/core/src/score.rs +++ b/core/src/score.rs @@ -23,7 +23,7 @@ impl Score { /// /// ``` /// use chessfriend_core::score::Score; - /// assert_eq!(!Score::MIN, Score::MAX); + /// assert_eq!(-Score::MIN, Score::MAX); /// ``` /// pub const MIN: Score = Score(Value::MIN + 1); From b6d27356accafd3dcbedf7f3ca87ad6d0ca7beb9 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 20:19:09 -0700 Subject: [PATCH 419/423] [bitboard] Implement BitBoard::occupied_squares_direction Iterate a BitBoard in a direction (from leading or trailing edge) based on a board direction. --- bitboard/src/bitboard.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index a16297d..6eb63eb 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -233,6 +233,38 @@ impl BitBoard { } } + /// Iterate through the occupied squares in a direction specified by a + /// compass direction. This method is mose useful for bitboards of slider + /// rays so that iteration proceeds in order along the ray's direction. + /// + /// ## Examples + /// + /// ``` + /// use chessfriend_bitboard::BitBoard; + /// use chessfriend_core::{Direction, Square}; + /// + /// let ray = BitBoard::ray(Square::E4, Direction::North); + /// assert_eq!( + /// ray.occupied_squares_direction(Direction::North).collect::>(), + /// vec![Square::E5, Square::E6, Square::E7, Square::E8] + /// ); + /// ``` + /// + #[must_use] + pub fn occupied_squares_direction( + &self, + direction: Direction, + ) -> Box> { + match direction { + Direction::North | Direction::NorthEast | Direction::NorthWest | Direction::East => { + Box::new(self.occupied_squares_trailing()) + } + Direction::SouthEast | Direction::South | Direction::SouthWest | Direction::West => { + Box::new(self.occupied_squares_leading()) + } + } + } + #[must_use] pub fn occupied_squares_leading(&self) -> LeadingBitScanner { LeadingBitScanner::new(self.0) From 3a0541a2c310474a6c6c91dd0dbb1fa156ed8e67 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 20:20:04 -0700 Subject: [PATCH 420/423] [bitboard] Add a doc comment to BitBoard::first_occupied_square --- bitboard/src/bitboard.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 6eb63eb..ccee9bd 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -287,6 +287,12 @@ impl BitBoard { } } + /// Get the first occupied square in the given direction. + /// + /// ## To-Do + /// + /// - Take `direction` by value instead of reference + /// #[must_use] pub fn first_occupied_square(&self, direction: &IterationDirection) -> Option { match direction { From 3d73760146461a4d4b80289ef6e8ce06102d5464 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 15 Aug 2025 16:14:34 -0700 Subject: [PATCH 421/423] [bitboard, board] Remove BitBoard::empty() and BitBoard::full() These have been deprecated for a while. Clean up the remaining uses and remove the methods from BitBoard. --- bitboard/src/bitboard.rs | 18 ++++-------------- bitboard/src/library.rs | 18 +++++++++--------- board/src/movement.rs | 20 ++++++++++---------- board/src/sight.rs | 2 +- 4 files changed, 24 insertions(+), 34 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index ccee9bd..35ce927 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -46,16 +46,6 @@ impl BitBoard { pub const EMPTY: BitBoard = BitBoard(u64::MIN); pub const FULL: BitBoard = BitBoard(u64::MAX); - #[deprecated(note = "Use BitBoard::EMPTY instead")] - pub const fn empty() -> BitBoard { - Self::EMPTY - } - - #[deprecated(note = "Use BitBoard::FULL instead")] - pub const fn full() -> BitBoard { - Self::FULL - } - pub const fn new(bits: u64) -> BitBoard { BitBoard(bits) } @@ -109,7 +99,7 @@ impl BitBoard { /// /// ``` /// use chessfriend_bitboard::BitBoard; - /// assert!(BitBoard::empty().is_empty()); + /// assert!(BitBoard::EMPTY.is_empty()); /// assert!(!BitBoard::full().is_empty()); /// assert!(!BitBoard::new(0b1000).is_empty()); /// ``` @@ -125,7 +115,7 @@ impl BitBoard { /// /// ``` /// use chessfriend_bitboard::BitBoard; - /// assert!(!BitBoard::empty().is_populated()); + /// assert!(!BitBoard::EMPTY.is_populated()); /// assert!(BitBoard::full().is_populated()); /// assert!(BitBoard::new(0b1).is_populated()); /// ``` @@ -564,8 +554,8 @@ mod tests { let b = bitboard![B5 G7 H3]; assert_eq!(a ^ b, bitboard![B5 C5 H3]); - assert_eq!(a ^ BitBoard::empty(), a); - assert_eq!(BitBoard::empty() ^ BitBoard::empty(), BitBoard::empty()); + assert_eq!(a ^ BitBoard::EMPTY, a); + assert_eq!(BitBoard::EMPTY ^ BitBoard::EMPTY, BitBoard::EMPTY); } #[test] diff --git a/bitboard/src/library.rs b/bitboard/src/library.rs index 6a60392..3ea670c 100644 --- a/bitboard/src/library.rs +++ b/bitboard/src/library.rs @@ -110,14 +110,14 @@ pub(super) struct MoveLibrary { impl MoveLibrary { const fn new() -> MoveLibrary { MoveLibrary { - rays: [[BitBoard::empty(); Direction::NUM]; Square::NUM], - pawn_attacks: [[BitBoard::empty(); Square::NUM]; Color::NUM], - pawn_pushes: [[BitBoard::empty(); Square::NUM]; Color::NUM], - knight_moves: [BitBoard::empty(); Square::NUM], - bishop_moves: [BitBoard::empty(); Square::NUM], - rook_moves: [BitBoard::empty(); Square::NUM], - queen_moves: [BitBoard::empty(); Square::NUM], - king_moves: [BitBoard::empty(); Square::NUM], + rays: [[BitBoard::EMPTY; Direction::NUM]; Square::NUM], + pawn_attacks: [[BitBoard::EMPTY; Square::NUM]; Color::NUM], + pawn_pushes: [[BitBoard::EMPTY; Square::NUM]; Color::NUM], + knight_moves: [BitBoard::EMPTY; Square::NUM], + bishop_moves: [BitBoard::EMPTY; Square::NUM], + rook_moves: [BitBoard::EMPTY; Square::NUM], + queen_moves: [BitBoard::EMPTY; Square::NUM], + king_moves: [BitBoard::EMPTY; Square::NUM], } } @@ -238,7 +238,7 @@ impl MoveLibrary { } fn _generate_ray(sq: BitBoard, shift: fn(&BitBoard) -> BitBoard) -> BitBoard { - let mut ray = BitBoard::empty(); + let mut ray = BitBoard::EMPTY; let mut iter = shift(&sq); while !iter.is_empty() { diff --git a/board/src/movement.rs b/board/src/movement.rs index 2935eee..3ebf44c 100644 --- a/board/src/movement.rs +++ b/board/src/movement.rs @@ -41,7 +41,7 @@ impl Movement for Piece { let parameters = Board::castling_parameters(Wing::KingSide, color); parameters.target.king.into() } else { - BitBoard::empty() + BitBoard::EMPTY }; let queenside_target_square = if board @@ -51,7 +51,7 @@ impl Movement for Piece { let parameters = Board::castling_parameters(Wing::QueenSide, color); parameters.target.king.into() } else { - BitBoard::empty() + BitBoard::EMPTY }; self.sight(square, board) | kingside_target_square | queenside_target_square @@ -99,11 +99,11 @@ mod tests { #[test] fn white_pushes_empty_board() { assert_eq!( - pawn_pushes(Square::E4.into(), Color::White, BitBoard::empty()), + pawn_pushes(Square::E4.into(), Color::White, BitBoard::EMPTY), bitboard![E5] ); assert_eq!( - pawn_pushes(Square::E2.into(), Color::White, BitBoard::empty()), + pawn_pushes(Square::E2.into(), Color::White, BitBoard::EMPTY), bitboard![E3 E4] ); } @@ -111,11 +111,11 @@ mod tests { #[test] fn black_pawn_empty_board() { assert_eq!( - pawn_pushes(Square::A4.into(), Color::Black, BitBoard::empty()), + pawn_pushes(Square::A4.into(), Color::Black, BitBoard::EMPTY), bitboard![A3] ); assert_eq!( - pawn_pushes(Square::B7.into(), Color::Black, BitBoard::empty()), + pawn_pushes(Square::B7.into(), Color::Black, BitBoard::EMPTY), bitboard![B6 B5] ); } @@ -124,7 +124,7 @@ mod tests { fn white_pushes_blocker() { assert_eq!( pawn_pushes(Square::C5.into(), Color::White, bitboard![C6]), - BitBoard::empty() + BitBoard::EMPTY ); assert_eq!( pawn_pushes(Square::D2.into(), Color::White, bitboard![D4]), @@ -132,7 +132,7 @@ mod tests { ); assert_eq!( pawn_pushes(Square::D2.into(), Color::White, bitboard![D3]), - BitBoard::empty() + BitBoard::EMPTY ); } @@ -140,7 +140,7 @@ mod tests { fn black_pushes_blocker() { assert_eq!( pawn_pushes(Square::C5.into(), Color::Black, bitboard![C4]), - BitBoard::empty() + BitBoard::EMPTY ); assert_eq!( pawn_pushes(Square::D7.into(), Color::Black, bitboard![D5]), @@ -148,7 +148,7 @@ mod tests { ); assert_eq!( pawn_pushes(Square::D7.into(), Color::Black, bitboard![D6]), - BitBoard::empty() + BitBoard::EMPTY ); } } diff --git a/board/src/sight.rs b/board/src/sight.rs index b8e366e..e682cb0 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -305,7 +305,7 @@ mod tests { let piece = piece!(White Pawn); let sight = piece.sight(Square::E4, &pos); - assert_eq!(sight, BitBoard::empty()); + assert_eq!(sight, BitBoard::EMPTY); } #[test] From 182bf8112669b156faa27f63e87384e1cfff9df5 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 15 Aug 2025 16:15:09 -0700 Subject: [PATCH 422/423] [board] Fix a counter underflow in the piece set During perft runs, the PieceSet counter would occasionally underflow, causing the whole program to crash. This is because, when building a Board from a list of bitboards, Counts::increment() was only being called once, even when the bitboard had more than one piece in it. Fix the bug by incrementing during the loop that sets up the mailbox. Additionally, refactor the increment() and decrement() methods to be a little more succinct. --- board/src/piece_sets.rs | 3 +-- board/src/piece_sets/counts.rs | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/board/src/piece_sets.rs b/board/src/piece_sets.rs index de43caa..52af054 100644 --- a/board/src/piece_sets.rs +++ b/board/src/piece_sets.rs @@ -51,11 +51,10 @@ impl PieceSet { color_occupancy[color_index] |= bitboard; shape_occupancy[shape_index] |= bitboard; - counts.increment(color, shape); - for square in bitboard.occupied_squares(&IterationDirection::default()) { let piece = Piece::new(color, shape); mailbox.set(piece, square); + counts.increment(color, shape); } } } diff --git a/board/src/piece_sets/counts.rs b/board/src/piece_sets/counts.rs index effbbe0..7d3cade 100644 --- a/board/src/piece_sets/counts.rs +++ b/board/src/piece_sets/counts.rs @@ -17,20 +17,21 @@ impl Counts { const SQUARE_NUM: u8 = Square::NUM as u8; let updated_value = self.0[color as usize][shape as usize] + 1; - if updated_value <= SQUARE_NUM { - self.0[color as usize][shape as usize] = updated_value; - } else { - unreachable!("piece count for {color} {shape} overflowed"); + if updated_value > SQUARE_NUM { + let shape_name = shape.name(); + panic!("piece count for {color} {shape_name} overflowed"); } + + self.0[color as usize][shape as usize] = updated_value; } pub fn decrement(&mut self, color: Color, shape: Shape) { let count = self.0[color as usize][shape as usize]; - if let Some(updated_count) = count.checked_sub(1) { - self.0[color as usize][shape as usize] = updated_count; - } else { - unreachable!("piece count for {color} {shape} underflowed"); - } + let updated_count = count.checked_sub(1).unwrap_or_else(|| { + let shape_name = shape.name(); + panic!("piece count for {color} {shape_name} should not underflow"); + }); + self.0[color as usize][shape as usize] = updated_count; } #[cfg(test)] From dae5179947f9f7001e1a47725723d9ba0eb205d0 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 15 Aug 2025 17:06:07 -0700 Subject: [PATCH 423/423] Add a README --- README.md | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..5ca6cb2 --- /dev/null +++ b/README.md @@ -0,0 +1,113 @@ +ChessFriend +=========== + +A chess engine written in Rust. + +The project is divided into crates for major components of the engine. These +crates are collected in a Cargo workspace. All crates have the `chessfriend_` +naming prefix. The directory structure omits this prefix, and I also frequently +do when referring to them. + + + +## Engine Crates + +The engine is divided into several crates, each providing vital types and +functionality. + + + +### `core` + +A collection of types for representing core concepts in a chess game and the +engine. Types like `Color` (player or piece color), `Shape` (the shape of a +piece: knight, etc), `Piece` (a piece of a particular color and shape), and +`Score` (for scoring a board position) live here. + + + +### `bitboard` + +Implements an efficient BitBoard type. Bitboards use a 64-bit bit field to mark a +square on a board as occupied or free. + + + +### `board` + +Implements a `Board` type that represents a moment-in-time board position. FEN +parsing and production lives here. + + + +### `moves` + +The `Move` type lives here, along with routines for encoding moves in efficient +forms, parsing moves from algebraic notation, and recording moves in a game +context. Additionally, the move generators for each shape of piece are here. + + + +### `position` + +Exports the `Position` type, representing a board position within the context of +a game. As such, it also provides a move list, and methods to make and unmake +moves. + + + +## Support Crates + +These crates are for debugging and testing. + + + +### `explorer` + +This crate implements a small command-line application for "exploring" board +positions. I meant for this program to be a debugging utility so that I could +examine bitboards and other board structures live. It has grown over time to +also support more aspects of interacting with the engine. So you can also use it +to play a game! + + + +### `perft` + +A small Perft utility that executes perft to a given depth from some starting +position. + + + + + +## Building + +Build the engine in the usual Rusty way. + +```sh +$ cargo build +``` + + + + + +## Testing + +Test in the usual Rusty way. + +```sh +$ cargo test +``` + +The engine has a fairly comprehensive unit test suite, as well as a decent pile +of integration tests. + + + + + +## Authors + +This engine is built entirely by me, Eryn Wells.