From 0c1863acb93e6d9176f31b5aadc72c1f208d0afd Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 19 May 2025 16:50:30 -0700 Subject: [PATCH] [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]