From dbca7b4f883b56c12ecf0f4b11f720c6bb0d5287 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 21 May 2025 10:08:59 -0700 Subject: [PATCH] [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, [