diff --git a/Cargo.lock b/Cargo.lock index 6e580bf..53be692 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]] @@ -88,6 +87,7 @@ version = "0.1.0" dependencies = [ "chessfriend_bitboard", "chessfriend_core", + "chessfriend_moves", ] [[package]] @@ -172,6 +172,7 @@ name = "explorer" version = "0.1.0" dependencies = [ "chessfriend_core", + "chessfriend_moves", "chessfriend_position", "clap", "rustyline", 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/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/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); } 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/builder.rs b/moves/src/builder.rs new file mode 100644 index 0000000..5f02364 --- /dev/null +++ b/moves/src/builder.rs @@ -0,0 +1,395 @@ +// Eryn Wells + +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 { + MissingOriginSquare, + MissingTargetSquare, + MissingCaptureSquare, + InvalidEnPassantSquare, +} + +pub trait Style { + fn origin_square(&self) -> Option { + None + } + + fn target_square(&self) -> Option { + None + } + + 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(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 + } +} + +#[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, + 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 { + 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 = 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 { + 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) -> 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)) + } +} + +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 push(piece: &PlacedPiece) -> Builder { + Builder { + style: Push { + from: Some(piece.square()), + to: None, + }, + } + } + + 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(color: Color, castle: castle::Castle) -> Builder { + Builder { + style: Castle { color, castle }, + } + } + + pub fn capturing_piece(piece: &PlacedPiece, capturing: &PlacedPiece) -> Builder { + Self::push(piece).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) -> &mut Self { + self.style.from = Some(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.clone(); + style.to = Some(square); + + Builder { + style: Capture { + push: style, + capture: Some(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()); + + Builder { + style: EnPassantCapture { + push: style, + capture: Some(en_passant), + }, + } + } + None => todo!(), + } + } + + pub fn capturing_piece(&self, piece: &PlacedPiece) -> Builder { + Builder { + style: Capture { + push: self.style.clone(), + capture: Some(piece.square()), + }, + } + } + + 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.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) -> Result { + Ok(Move(self.bits() | self.style.into_move_bits()?)) + } +} + +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) -> 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()?, + )) + } +} + +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 new file mode 100644 index 0000000..731f25f --- /dev/null +++ b/moves/src/castle.rs @@ -0,0 +1,121 @@ +// Eryn Wells + +use chessfriend_bitboard::BitBoard; +use chessfriend_core::{Color, Square}; + +#[repr(u8)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Castle { + KingSide = 0, + QueenSide = 1, +} + +pub struct Parameters { + /// 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, +} + +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)] +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]; 2] = [ + [ + Parameters { + 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), + }, + Parameters { + 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), + }, + ], + [ + Parameters { + 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), + }, + Parameters { + 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), + }, + ], + ]; + + pub fn parameters(&self, color: Color) -> &'static Parameters { + &Castle::PARAMETERS[color as usize][*self as usize] + } +} diff --git a/moves/src/defs.rs b/moves/src/defs.rs new file mode 100644 index 0000000..337f30e --- /dev/null +++ b/moves/src/defs.rs @@ -0,0 +1,34 @@ +// Eryn Wells + +use chessfriend_core::Shape; + +pub(crate) enum Kind { + Quiet = 0b0000, + DoublePush = 0b0001, + KingSideCastle = 0b0010, + QueenSideCastle = 0b0011, + 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/en_passant.rs b/moves/src/en_passant.rs new file mode 100644 index 0000000..49e88a6 --- /dev/null +++ b/moves/src/en_passant.rs @@ -0,0 +1,51 @@ +// 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_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, + } + } + + /// 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/moves/src/lib.rs b/moves/src/lib.rs new file mode 100644 index 0000000..297f82e --- /dev/null +++ b/moves/src/lib.rs @@ -0,0 +1,15 @@ +// Eryn Wells + +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 new file mode 100644 index 0000000..5348306 --- /dev/null +++ b/moves/src/moves.rs @@ -0,0 +1,135 @@ +// Eryn Wells + +use crate::{castle::Castle, defs::Kind}; +use chessfriend_core::{Rank, Shape, Square}; +use std::fmt; + +/// 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 origin_square(&self) -> Square { + ((self.0 >> 4) & 0b111111).try_into().unwrap() + } + + pub fn target_square(&self) -> Square { + (self.0 >> 10).try_into().unwrap() + } + + pub fn capture_square(&self) -> Option { + 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!(), + }); + } + + if self.is_capture() { + return Some(self.target_square()); + } + + None + } + + pub fn is_quiet(&self) -> bool { + self.flags() == Kind::Quiet as u16 + } + + pub fn is_double_push(&self) -> bool { + self.flags() == Kind::DoublePush as u16 + } + + 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 + } + + 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 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("Move") + .field(&format_args!("{:08b}", &self.0)) + .finish() + } +} 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 new file mode 100644 index 0000000..6d001cb --- /dev/null +++ b/moves/tests/flags.rs @@ -0,0 +1,109 @@ +// Eryn Wells + +use chessfriend_core::{piece, Color, File, Shape, Square}; +use chessfriend_moves::{testing::*, Builder, 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() -> 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 mv = unsafe { + Builder::new() + .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::A4); + assert_eq!(mv.target_square(), Square::B3); + assert_eq!(mv.capture_square(), Some(Square::B4)); + + Ok(()) +} + +#[test] +fn move_flags_promotion() -> TestResult { + let mv = 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)); + + Ok(()) +} + +#[test] +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)) + .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() -> TestResult { + let mv = Builder::castling(Color::White, 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..7406eb0 --- /dev/null +++ b/moves/tests/pushes.rs @@ -0,0 +1,17 @@ +// 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/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/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; diff --git a/position/src/fen.rs b/position/src/fen.rs index d6c3568..fde920c 100644 --- a/position/src/fen.rs +++ b/position/src/fen.rs @@ -1,9 +1,11 @@ // 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, EnPassant}; use std::fmt::Write; +#[macro_export] macro_rules! fen { ($fen_string:literal) => { Position::from_fen_str($fen_string) @@ -95,11 +97,8 @@ impl ToFen for Position { write!( fen_string, " {}", - if let Some(en_passant_square) = self.en_passant_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))?; @@ -197,7 +196,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/lib.rs b/position/src/lib.rs index 5776e05..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 tests; +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/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.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); - } -} diff --git a/position/src/move_generator.rs b/position/src/move_generator.rs index e6884d4..c4271ce 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 { @@ -53,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 { @@ -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()) } @@ -77,7 +80,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..3443d46 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); @@ -57,59 +58,64 @@ 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); assert_eq!( - generator.bitboard(), + 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([ - 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); - 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}" ); + 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!( @@ -124,6 +130,8 @@ mod tests { "Moves unexpectedly present: {:#?}", generated_moves ); + + Ok(()) } #[test] @@ -138,7 +146,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]; @@ -146,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, @@ -159,21 +167,16 @@ 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::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(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, @@ -187,21 +190,16 @@ 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::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(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, @@ -215,16 +213,11 @@ 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::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(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 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 6cefb7a..61adb03 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_core::{PlacedPiece, Square}; +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,21 +30,53 @@ impl MoveSet { MoveSet { piece, bitboards: BitBoardSet::default(), - special: 0, + special: None, } } - 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 castle { - Castle::KingSide => (self.special & 0b1) != 0, - Castle::QueenSide => (self.special & 0b10) != 0, + 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 @@ -50,72 +88,103 @@ 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.piece(); - let from_square = self.piece.square(); + 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() - .map(move |to_square| MoveBuilder::new(*piece, from_square, to_square).build()) + .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() - .map(move |to_square| { - MoveBuilder::new(*piece, from_square, to_square) - .capturing(PlacedPiece::new( - Piece::new(Color::White, Shape::Pawn), - to_square, - )) + .filter_map(|to_square| { + MoveBuilder::push(piece) + .capturing_on(to_square) .build() + .ok() }), ) - .chain( - if (self.special & 0b1) != 0 { - Some(()) + .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 } - .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() - }), - ) + } + _ => 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 c6e7a5a..1a07c4c 100644 --- a/position/src/move_generator/pawn.rs +++ b/position/src/move_generator/pawn.rs @@ -4,11 +4,20 @@ 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::{EnPassant, Move}; +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 { @@ -24,14 +33,61 @@ 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, &push_mask, &capture_mask) + { + move_set.en_passant(en_passant); + } + + move_set } } impl PawnMoveGenerator { - fn pushes(position: &Position, piece: PlacedPiece) -> BitBoard { + pub(super) fn new( + position: &Position, + 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) + } else { + std::collections::BTreeMap::new() + }; + + Self { + color: player_to_move, + 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 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) + }, + )); + + moves_for_pieces + } + + fn pushes(position: &Position, piece: &PlacedPiece) -> BitBoard { let color = piece.color(); let square = piece.square(); let bitboard: BitBoard = square.into(); @@ -59,62 +115,109 @@ 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( + position: &Position, + piece: &PlacedPiece, + push_mask: &BitBoard, + capture_mask: &BitBoard, + ) -> Option { + match position.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 position.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, position::DiagramFormatter, test_position, Move, MoveBuilder}; - use chessfriend_core::{piece, Color, Piece, Square}; + 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; #[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); + assert_move_list!(generated_moves, expected_moves, pos); + + 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, @@ -124,16 +227,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] @@ -145,14 +247,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, @@ -161,20 +263,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, @@ -184,21 +285,68 @@ 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::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 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] ); } diff --git a/position/src/move_generator/tests/peterellisjones.rs b/position/src/move_generator/tests/peterellisjones.rs index 3828970..4a12b92 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, @@ -139,21 +145,23 @@ fn en_passant_check_capture() { 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::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::D3) + .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, @@ -163,21 +171,23 @@ fn en_passant_check_block() { 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::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::D3) + .build()? ), "Valid moves: {:?}", formatted_move_list!(generated_moves, pos) ); + + Ok(()) } #[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, @@ -190,21 +200,21 @@ 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(()) } #[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, @@ -212,15 +222,17 @@ fn en_passant_discovered_check() { White Queen on H4, ], D3); - let generated_moves = pos.moves().iter().collect::>(); + let generated_moves: HashSet<_> = 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::D3) + .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 c926c08..307bae6 100644 --- a/position/src/position/builders/move_builder.rs +++ b/position/src/position/builders/move_builder.rs @@ -1,12 +1,19 @@ // Eryn Wells -use crate::{ - position::flags::Flags, - r#move::{AlgebraicMoveFormatter, 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, EnPassant, 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)] @@ -27,7 +34,7 @@ pub enum ValidatedMove { captured_piece: Option, promotion: Option, flags: Flags, - en_passant_square: Option, + en_passant: Option, increment_ply: bool, }, Castle { @@ -55,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 @@ -76,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)); } } } @@ -87,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)?; @@ -100,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 { @@ -142,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 => 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), } + .and_then(EnPassant::from_target_square) } else { None }; @@ -154,13 +162,13 @@ 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(), flags, - en_passant_square, + en_passant, increment_ply: !(mv.is_capture() || piece.is_pawn()), }, }) @@ -183,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(); @@ -210,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, ) @@ -223,18 +231,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(), @@ -261,15 +270,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 chessfriend_core::piece; + use crate::testing::*; + use crate::{position, test_position}; + use chessfriend_core::{piece, File}; + 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!( @@ -281,25 +299,32 @@ mod tests { } #[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(); + fn move_white_pawn_two_squares() -> TestResult { + 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)) ); - assert_eq!(new_position.en_passant_square(), Some(Square::E3)); + + 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) + ); Ok(()) } #[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, @@ -307,13 +332,10 @@ mod tests { 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 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!( @@ -329,15 +351,14 @@ mod tests { } #[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}"); + fn en_passant_capture() -> TestResult { + let pos = test_position!(Black, [ + White Pawn on B5, + Black Pawn on A7, + ]); + + let black_pawn_move = MoveBuilder::double_push(File::A, Color::Black).build()?; - 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()); @@ -353,10 +374,10 @@ 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 en_passant_capture = Builder::::new(&en_passant_position) + let white_pawn_capture = MoveBuilder::push(&piece!(White Pawn on B5)) + .capturing_en_passant_on(Square::A6) + .build()?; + 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 5011f21..08fc193 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, 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_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 } @@ -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); @@ -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/flags.rs b/position/src/position/flags.rs index 47e49bf..19d3165 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 { @@ -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 01de793..a6fc369 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -5,11 +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::{Castle, EnPassant}; use std::{cell::OnceCell, fmt}; #[derive(Clone, Debug, Eq)] @@ -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, @@ -92,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() { @@ -182,12 +182,16 @@ impl Position { Pieces::new(&self, color) } - pub fn en_passant_square(&self) -> Option { - self.en_passant_square + pub fn has_en_passant_square(&self) -> bool { + self.en_passant.is_some() + } + + pub fn en_passant(&self) -> Option { + 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() @@ -202,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) @@ -214,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 @@ -295,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, @@ -332,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); } } @@ -343,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, @@ -356,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 } } @@ -368,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/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); diff --git a/position/src/testing.rs b/position/src/testing.rs new file mode 100644 index 0000000..1693110 --- /dev/null +++ b/position/src/testing.rs @@ -0,0 +1,58 @@ +// Eryn Wells + +use crate::MakeMoveError; +use chessfriend_moves::BuildMoveError; + +#[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::>() - }; -}