diff --git a/core/src/coordinates.rs b/core/src/coordinates.rs index a81daad..919e262 100644 --- a/core/src/coordinates.rs +++ b/core/src/coordinates.rs @@ -210,6 +210,12 @@ impl Rank { pub fn is_pawn_double_push_target_rank(&self, color: Color) -> bool { self == &Self::PAWN_DOUBLE_PUSH_TARGET_RANKS[color as usize] } + + /// Ranks where promotions happen. + #[must_use] + pub fn is_promotable_rank(&self) -> bool { + matches!(*self, Rank::ONE | Rank::EIGHT) + } } #[rustfmt::skip] diff --git a/moves/src/moves.rs b/moves/src/moves.rs index 94f79a3..30293ec 100644 --- a/moves/src/moves.rs +++ b/moves/src/moves.rs @@ -1,7 +1,6 @@ // Eryn Wells -use crate::builder::Builder; -use crate::defs::Kind; +use crate::defs::{Kind, PromotionShape}; use chessfriend_core::{Rank, Shape, Square, Wing}; use std::fmt; @@ -14,6 +13,48 @@ use std::fmt; #[derive(Clone, Copy, Eq, Hash, PartialEq)] pub struct Move(pub(crate) u16); +impl Move { + #[must_use] + pub fn quiet(origin: Square, target: Square) -> Self { + let origin_bits = (origin as u16) << 4; + let target_bits = (target as u16) << 10; + Move(origin_bits | target_bits) + } + + #[must_use] + pub fn double_push(origin: Square, target: Square) -> Self { + let origin_bits = (origin as u16) << 4; + let target_bits = (target as u16) << 10; + let flag_bits = Kind::DoublePush as u16; + Move(origin_bits | target_bits | flag_bits) + } + + #[must_use] + pub fn capture(origin: Square, target: Square) -> Self { + let origin_bits = (origin as u16) << 4; + let target_bits = (target as u16) << 10; + let flag_bits = Kind::Capture as u16; + Move(origin_bits | target_bits | flag_bits) + } + + #[must_use] + pub fn en_passant_capture(origin: Square, target: Square) -> Self { + let origin_bits = (origin as u16) << 4; + let target_bits = (target as u16) << 10; + let flag_bits = Kind::EnPassantCapture as u16; + Move(origin_bits | target_bits | flag_bits) + } + + #[must_use] + pub fn promotion(origin: Square, target: Square, shape: PromotionShape) -> Self { + let origin_bits = (origin as u16) << 4; + let target_bits = (target as u16) << 10; + let flag_bits = Kind::Promotion as u16; + let shape_bits = shape as u16; + Move(origin_bits | target_bits | flag_bits | shape_bits) + } +} + impl Move { #[must_use] #[allow(clippy::missing_panics_doc)] @@ -85,7 +126,7 @@ impl Move { } #[must_use] - pub fn promotion(&self) -> Option { + pub fn promotion_shape(&self) -> Option { if !self.is_promotion() { return None; } @@ -139,7 +180,7 @@ impl fmt::Display for Move { let transfer_char = self.transfer_char(); write!(f, "{origin}{transfer_char}{target}")?; - if let Some(promotion) = self.promotion() { + if let Some(promotion) = self.promotion_shape() { write!(f, "={promotion}")?; } else if self.is_en_passant() { write!(f, " e.p.")?; diff --git a/position/src/movement.rs b/position/src/movement.rs index a3ee154..1d26e4b 100644 --- a/position/src/movement.rs +++ b/position/src/movement.rs @@ -15,11 +15,13 @@ pub trait Movement { impl Movement for Piece { fn movement(&self, square: Square, board: &Board) -> BitBoard { + let opposing_occupancy = board.opposing_occupancy(self.color); + match self.shape { Shape::Pawn => { + let en_passant_square: BitBoard = board.en_passant_target.into(); // Pawns can only move to squares they can see to capture. - let opposing_occupancy = board.opposing_occupancy(self.color); - let sight = self.sight(square, board) & opposing_occupancy; + let sight = self.sight(square, board) & (opposing_occupancy | en_passant_square); let pushes = pawn_pushes(square.into(), self.color, board.occupancy()); sight | pushes } diff --git a/position/src/position/make_move.rs b/position/src/position/make_move.rs index abc5515..8c0597c 100644 --- a/position/src/position/make_move.rs +++ b/position/src/position/make_move.rs @@ -1,8 +1,8 @@ // Eryn Wells use crate::{movement::Movement, Position}; -use chessfriend_board::{CastleParameters, PlacePieceError, PlacePieceStrategy}; -use chessfriend_core::{Color, Piece, Square, Wing}; +use chessfriend_board::{en_passant, CastleParameters, PlacePieceError, PlacePieceStrategy}; +use chessfriend_core::{Color, Piece, Rank, Shape, Square, Wing}; use chessfriend_moves::Move; use thiserror::Error; @@ -25,12 +25,21 @@ pub enum MakeMoveError { #[error("{piece} on {square} is not of active color")] NonActiveColor { piece: Piece, square: Square }, + #[error("{0} cannot make move")] + InvalidPiece(Piece), + #[error("cannot capture piece on {0}")] InvalidCapture(Square), + #[error("cannot capture en passant on {0}")] + InvalidEnPassantCapture(Square), + #[error("no capture square")] NoCaptureSquare, + #[error("no piece to capture on {0}")] + NoCapturePiece(Square), + #[error("{piece} on {origin} cannot move to {target}")] NoMove { piece: Piece, @@ -43,6 +52,9 @@ pub enum MakeMoveError { #[error("{0}")] CastleError(#[from] CastleEvaluationError), + + #[error("cannot promote on {0}")] + InvalidPromotion(Square), } pub enum UnmakeMoveError {} @@ -61,6 +73,10 @@ impl Position { return self.make_quiet_move(ply.origin_square(), ply.target_square()); } + if ply.is_double_push() { + return self.make_double_push_move(ply); + } + if ply.is_capture() { return self.make_capture_move(ply); } @@ -69,6 +85,10 @@ impl Position { return self.make_castle_move(wing); } + if ply.is_promotion() { + return self.make_promotion_move(ply); + } + Ok(()) } @@ -91,19 +111,59 @@ impl Position { Ok(()) } + fn make_double_push_move(&mut self, ply: Move) -> MakeMoveResult { + let origin = ply.origin_square(); + let piece = self + .board + .remove_piece(origin) + .ok_or(MakeMoveError::NoPiece(origin))?; + + let target = ply.target_square(); + self.place_active_piece(piece, target)?; + + self.board.en_passant_target = match target.rank() { + Rank::FOUR => Some(Square::from_file_rank(target.file(), Rank::THREE)), + Rank::FIVE => Some(Square::from_file_rank(target.file(), Rank::SIX)), + _ => unreachable!(), + }; + + self.advance_clocks(HalfMoveClock::Advance); + + Ok(()) + } + fn make_capture_move(&mut self, ply: Move) -> MakeMoveResult { let origin_square = ply.origin_square(); + let target_square = ply.target_square(); + let piece = self.get_piece_for_move(origin_square)?; + if ply.is_en_passant() { + let en_passant_square = self + .board + .en_passant_target + .ok_or(MakeMoveError::NoCaptureSquare)?; + if target_square != en_passant_square { + return Err(MakeMoveError::InvalidEnPassantCapture(target_square)); + } + } + let capture_square = ply.capture_square().ok_or(MakeMoveError::NoCaptureSquare)?; - let captured_piece = self.get_piece_for_move(capture_square)?; + let captured_piece = self + .remove_piece(capture_square) + .ok_or(MakeMoveError::NoCapturePiece(capture_square))?; // Register the capture self.captures[piece.color as usize].push(captured_piece); self.remove_piece(origin_square).unwrap(); - let target_square = ply.target_square(); - self.place_piece(piece, target_square, PlacePieceStrategy::Replace)?; + + if let Some(promotion_shape) = ply.promotion_shape() { + let promoted_piece = Piece::new(piece.color, promotion_shape); + self.place_piece(promoted_piece, target_square, PlacePieceStrategy::Replace)?; + } else { + self.place_piece(piece, target_square, PlacePieceStrategy::Replace)?; + } self.advance_clocks(HalfMoveClock::Reset); @@ -117,12 +177,10 @@ impl Position { let parameters = self.board.castling_parameters(wing); let king = self.board.remove_piece(parameters.origin.king).unwrap(); - self.board - .place_piece(king, parameters.target.king, PlacePieceStrategy::default())?; + self.place_piece(king, parameters.target.king, PlacePieceStrategy::default())?; let rook = self.board.remove_piece(parameters.origin.rook).unwrap(); - self.board - .place_piece(rook, parameters.target.rook, PlacePieceStrategy::default())?; + self.place_piece(rook, parameters.target.rook, PlacePieceStrategy::default())?; self.board.castling_rights.revoke(active_color, wing); @@ -130,6 +188,34 @@ impl Position { Ok(()) } + + fn make_promotion_move(&mut self, ply: Move) -> MakeMoveResult { + let origin = ply.origin_square(); + + let piece = self.get_piece_for_move(origin)?; + if !piece.is_pawn() { + return Err(MakeMoveError::InvalidPiece(piece)); + } + + let target = ply.target_square(); + if !target.rank().is_promotable_rank() { + return Err(MakeMoveError::InvalidPromotion(target)); + } + + if let Some(promotion_shape) = ply.promotion_shape() { + self.remove_piece(origin); + let promoted_piece = Piece::new(piece.color, promotion_shape); + self.place_piece(promoted_piece, target, PlacePieceStrategy::PreserveExisting)?; + } else { + unreachable!( + "Cannot make a promotion move with a ply that has no promotion shape: {ply:?}", + ); + } + + self.advance_clocks(HalfMoveClock::Reset); + + Ok(()) + } } impl Position { @@ -138,8 +224,7 @@ impl Position { } fn place_active_piece(&mut self, piece: Piece, square: Square) -> MakeMoveResult { - self.board - .place_piece(piece, square, PlacePieceStrategy::PreserveExisting) + self.place_piece(piece, square, PlacePieceStrategy::PreserveExisting) .map_err(MakeMoveError::PlacePieceError) } } @@ -191,14 +276,13 @@ impl Position { // TODO: En Passant capture. - if ply.is_capture() { - let target = ply.target_square(); - if let Some(captured_piece) = self.board.get_piece(target) { + if let Some(capture_square) = ply.capture_square() { + if let Some(captured_piece) = self.board.get_piece(capture_square) { if captured_piece.color == active_piece.color { - return Err(MakeMoveError::InvalidCapture(target)); + return Err(MakeMoveError::InvalidCapture(capture_square)); } } else { - return Err(MakeMoveError::NoPiece(target)); + return Err(MakeMoveError::NoPiece(capture_square)); } } @@ -223,3 +307,105 @@ impl Position { Ok(active_piece) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{test_position, ValidateMove}; + use chessfriend_core::{piece, Color, Square}; + use chessfriend_moves::{Move, PromotionShape}; + + #[test] + fn make_quiet_move() -> MakeMoveResult { + let mut pos = test_position!(White Pawn on C2); + + let ply = Move::quiet(Square::C2, Square::C3); + pos.make_move(ply, ValidateMove::Yes)?; + + assert_eq!(pos.get_piece(Square::C2), None); + assert_eq!(pos.get_piece(Square::C3), Some(piece!(White Pawn))); + assert_eq!(pos.board.active_color, Color::Black); + assert_eq!(pos.board.half_move_clock, 1); + + pos.board.active_color = Color::White; + + let ply = Move::quiet(Square::C3, Square::C4); + pos.make_move(ply, ValidateMove::Yes)?; + + assert_eq!(pos.get_piece(Square::C3), None); + assert_eq!(pos.get_piece(Square::C4), Some(piece!(White Pawn))); + assert_eq!(pos.board.active_color, Color::Black); + assert_eq!(pos.board.half_move_clock, 2); + + Ok(()) + } + + #[test] + fn make_capture_move() { + let mut pos = test_position![ + White Bishop on C2, + Black Rook on F5, + ]; + + let ply = Move::capture(Square::C2, Square::F5); + assert_eq!(pos.make_move(ply, ValidateMove::Yes), Ok(())); + assert_eq!(pos.get_piece(Square::C2), None); + assert_eq!(pos.get_piece(Square::F5), Some(piece!(White Bishop))); + assert_eq!(pos.captures[Color::White as usize][0], piece!(Black Rook)); + assert_eq!(pos.board.active_color, Color::Black); + assert_eq!(pos.board.half_move_clock, 0); + } + + #[test] + fn make_en_passant_capture_move() -> MakeMoveResult { + let mut pos = test_position![ + Black Pawn on F4, + White Pawn on E2 + ]; + + let ply = Move::double_push(Square::E2, Square::E4); + pos.make_move(ply, ValidateMove::Yes)?; + + assert_eq!(pos.get_piece(Square::E2), None); + assert_eq!(pos.get_piece(Square::E4), Some(piece!(White Pawn))); + assert_eq!( + pos.board.en_passant_target, + Some(Square::E3), + "en passant square not set" + ); + assert_eq!(pos.board.active_color, Color::Black); + assert_eq!(pos.board.half_move_clock, 1); + + let ply = Move::en_passant_capture(Square::F4, Square::E3); + pos.make_move(ply, ValidateMove::Yes)?; + + assert_eq!(pos.get_piece(Square::F4), None); + assert_eq!(pos.get_piece(Square::E3), Some(piece!(Black Pawn))); + assert_eq!( + pos.get_piece(Square::E4), + None, + "capture target pawn not removed" + ); + assert_eq!(pos.captures[Color::Black as usize][0], piece!(White Pawn)); + + Ok(()) + } + + #[test] + fn make_promotion_move() -> MakeMoveResult { + let mut pos = test_position![ + Black Pawn on E7, + White Pawn on F7, + ]; + + let ply = Move::promotion(Square::F7, Square::F8, PromotionShape::Queen); + pos.make_move(ply, ValidateMove::Yes)?; + + assert_eq!(pos.get_piece(Square::F7), None); + assert_eq!(pos.get_piece(Square::F8), Some(piece!(White Queen))); + assert_eq!(pos.board.active_color, Color::Black); + assert_eq!(pos.board.half_move_clock, 0); + + Ok(()) + } +}