[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:
Eryn Wells 2025-05-20 19:29:02 -07:00
parent 6591619e32
commit 039fd2b080
4 changed files with 257 additions and 22 deletions

View file

@ -210,6 +210,12 @@ impl Rank {
pub fn is_pawn_double_push_target_rank(&self, color: Color) -> bool { pub fn is_pawn_double_push_target_rank(&self, color: Color) -> bool {
self == &Self::PAWN_DOUBLE_PUSH_TARGET_RANKS[color as usize] 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] #[rustfmt::skip]

View file

@ -1,7 +1,6 @@
// Eryn Wells <eryn@erynwells.me> // Eryn Wells <eryn@erynwells.me>
use crate::builder::Builder; use crate::defs::{Kind, PromotionShape};
use crate::defs::Kind;
use chessfriend_core::{Rank, Shape, Square, Wing}; use chessfriend_core::{Rank, Shape, Square, Wing};
use std::fmt; use std::fmt;
@ -14,6 +13,48 @@ use std::fmt;
#[derive(Clone, Copy, Eq, Hash, PartialEq)] #[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct Move(pub(crate) u16); 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 { impl Move {
#[must_use] #[must_use]
#[allow(clippy::missing_panics_doc)] #[allow(clippy::missing_panics_doc)]
@ -85,7 +126,7 @@ impl Move {
} }
#[must_use] #[must_use]
pub fn promotion(&self) -> Option<Shape> { pub fn promotion_shape(&self) -> Option<Shape> {
if !self.is_promotion() { if !self.is_promotion() {
return None; return None;
} }
@ -139,7 +180,7 @@ impl fmt::Display for Move {
let transfer_char = self.transfer_char(); let transfer_char = self.transfer_char();
write!(f, "{origin}{transfer_char}{target}")?; write!(f, "{origin}{transfer_char}{target}")?;
if let Some(promotion) = self.promotion() { if let Some(promotion) = self.promotion_shape() {
write!(f, "={promotion}")?; write!(f, "={promotion}")?;
} else if self.is_en_passant() { } else if self.is_en_passant() {
write!(f, " e.p.")?; write!(f, " e.p.")?;

View file

@ -15,11 +15,13 @@ pub trait Movement {
impl Movement for Piece { impl Movement for Piece {
fn movement(&self, square: Square, board: &Board) -> BitBoard { fn movement(&self, square: Square, board: &Board) -> BitBoard {
let opposing_occupancy = board.opposing_occupancy(self.color);
match self.shape { match self.shape {
Shape::Pawn => { Shape::Pawn => {
let en_passant_square: BitBoard = board.en_passant_target.into();
// Pawns can only move to squares they can see to capture. // 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 | en_passant_square);
let sight = self.sight(square, board) & opposing_occupancy;
let pushes = pawn_pushes(square.into(), self.color, board.occupancy()); let pushes = pawn_pushes(square.into(), self.color, board.occupancy());
sight | pushes sight | pushes
} }

View file

@ -1,8 +1,8 @@
// Eryn Wells <eryn@erynwells.me> // Eryn Wells <eryn@erynwells.me>
use crate::{movement::Movement, Position}; use crate::{movement::Movement, Position};
use chessfriend_board::{CastleParameters, PlacePieceError, PlacePieceStrategy}; use chessfriend_board::{en_passant, CastleParameters, PlacePieceError, PlacePieceStrategy};
use chessfriend_core::{Color, Piece, Square, Wing}; use chessfriend_core::{Color, Piece, Rank, Shape, Square, Wing};
use chessfriend_moves::Move; use chessfriend_moves::Move;
use thiserror::Error; use thiserror::Error;
@ -25,12 +25,21 @@ pub enum MakeMoveError {
#[error("{piece} on {square} is not of active color")] #[error("{piece} on {square} is not of active color")]
NonActiveColor { piece: Piece, square: Square }, NonActiveColor { piece: Piece, square: Square },
#[error("{0} cannot make move")]
InvalidPiece(Piece),
#[error("cannot capture piece on {0}")] #[error("cannot capture piece on {0}")]
InvalidCapture(Square), InvalidCapture(Square),
#[error("cannot capture en passant on {0}")]
InvalidEnPassantCapture(Square),
#[error("no capture square")] #[error("no capture square")]
NoCaptureSquare, NoCaptureSquare,
#[error("no piece to capture on {0}")]
NoCapturePiece(Square),
#[error("{piece} on {origin} cannot move to {target}")] #[error("{piece} on {origin} cannot move to {target}")]
NoMove { NoMove {
piece: Piece, piece: Piece,
@ -43,6 +52,9 @@ pub enum MakeMoveError {
#[error("{0}")] #[error("{0}")]
CastleError(#[from] CastleEvaluationError), CastleError(#[from] CastleEvaluationError),
#[error("cannot promote on {0}")]
InvalidPromotion(Square),
} }
pub enum UnmakeMoveError {} pub enum UnmakeMoveError {}
@ -61,6 +73,10 @@ impl Position {
return self.make_quiet_move(ply.origin_square(), ply.target_square()); 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() { if ply.is_capture() {
return self.make_capture_move(ply); return self.make_capture_move(ply);
} }
@ -69,6 +85,10 @@ impl Position {
return self.make_castle_move(wing); return self.make_castle_move(wing);
} }
if ply.is_promotion() {
return self.make_promotion_move(ply);
}
Ok(()) Ok(())
} }
@ -91,19 +111,59 @@ impl Position {
Ok(()) 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 { fn make_capture_move(&mut self, ply: Move) -> MakeMoveResult {
let origin_square = ply.origin_square(); let origin_square = ply.origin_square();
let target_square = ply.target_square();
let piece = self.get_piece_for_move(origin_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 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 // Register the capture
self.captures[piece.color as usize].push(captured_piece); self.captures[piece.color as usize].push(captured_piece);
self.remove_piece(origin_square).unwrap(); 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); self.advance_clocks(HalfMoveClock::Reset);
@ -117,12 +177,10 @@ impl Position {
let parameters = self.board.castling_parameters(wing); let parameters = self.board.castling_parameters(wing);
let king = self.board.remove_piece(parameters.origin.king).unwrap(); let king = self.board.remove_piece(parameters.origin.king).unwrap();
self.board self.place_piece(king, parameters.target.king, PlacePieceStrategy::default())?;
.place_piece(king, parameters.target.king, PlacePieceStrategy::default())?;
let rook = self.board.remove_piece(parameters.origin.rook).unwrap(); let rook = self.board.remove_piece(parameters.origin.rook).unwrap();
self.board self.place_piece(rook, parameters.target.rook, PlacePieceStrategy::default())?;
.place_piece(rook, parameters.target.rook, PlacePieceStrategy::default())?;
self.board.castling_rights.revoke(active_color, wing); self.board.castling_rights.revoke(active_color, wing);
@ -130,6 +188,34 @@ impl Position {
Ok(()) 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 { impl Position {
@ -138,8 +224,7 @@ impl Position {
} }
fn place_active_piece(&mut self, piece: Piece, square: Square) -> MakeMoveResult { fn place_active_piece(&mut self, piece: Piece, square: Square) -> MakeMoveResult {
self.board self.place_piece(piece, square, PlacePieceStrategy::PreserveExisting)
.place_piece(piece, square, PlacePieceStrategy::PreserveExisting)
.map_err(MakeMoveError::PlacePieceError) .map_err(MakeMoveError::PlacePieceError)
} }
} }
@ -191,14 +276,13 @@ impl Position {
// TODO: En Passant capture. // TODO: En Passant capture.
if ply.is_capture() { if let Some(capture_square) = ply.capture_square() {
let target = ply.target_square(); if let Some(captured_piece) = self.board.get_piece(capture_square) {
if let Some(captured_piece) = self.board.get_piece(target) {
if captured_piece.color == active_piece.color { if captured_piece.color == active_piece.color {
return Err(MakeMoveError::InvalidCapture(target)); return Err(MakeMoveError::InvalidCapture(capture_square));
} }
} else { } else {
return Err(MakeMoveError::NoPiece(target)); return Err(MakeMoveError::NoPiece(capture_square));
} }
} }
@ -223,3 +307,105 @@ impl Position {
Ok(active_piece) 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(())
}
}