[position, board, core, moves] Implement a bunch of make_move code
Implement making double push and promotion moves. Then write several tests to exercise these. Add convenient static functions to the Move struct to build moves quickly, without using the Builder. Add a is_promotable_rank() method to Rank to check that a rank can be used for promotion moves. The tests found and fixed a bug in pawn movement where the en passant square was being discarded when deciding whether an e.p. move can be made.
This commit is contained in:
parent
6591619e32
commit
039fd2b080
4 changed files with 257 additions and 22 deletions
|
@ -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]
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
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<Shape> {
|
||||
pub fn promotion_shape(&self) -> Option<Shape> {
|
||||
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.")?;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue