[board, moves, position] Move make_move routines to moves crate

Declare a MakeMove trait and export it from chessfriend_moves. Declare a
BoardProvider trait that both Board and Position implement.

Implement the MakeMove trait for all types that implement BoardProvider, and move
all the move making code to the moves crate.

This change makes it possible to make moves directly on a Board, rather than
requiring a Position. The indirection of declaring and implementing the trait
in the moves crate is required because chessfriend_board is a dependency of
chessfriend_moves. So, it would be a layering violation for Board to implement
make_move() directly. The board crate cannot link the moves crate because that
would introduce a circular dependency.
This commit is contained in:
Eryn Wells 2025-05-31 19:01:20 -07:00
parent ecde338602
commit 40e8e055f9
7 changed files with 202 additions and 163 deletions

View file

@ -5,11 +5,13 @@ pub mod testing;
mod builder;
mod defs;
mod make_move;
mod moves;
mod record;
pub use builder::{Builder, Error as BuildMoveError, Result as BuildMoveResult};
pub use defs::{Kind, PromotionShape};
pub use generators::GeneratedMove;
pub use make_move::{MakeMove, MakeMoveError, ValidateMove};
pub use moves::Move;
pub use record::MoveRecord;

534
moves/src/make_move.rs Normal file
View file

@ -0,0 +1,534 @@
// Eryn Wells <eryn@erynwells.me>
use crate::{Move, MoveRecord};
use chessfriend_board::{
castle::CastleEvaluationError, movement::Movement, Board, BoardProvider, PlacePieceError,
PlacePieceStrategy,
};
use chessfriend_core::{Color, Piece, Rank, Square, Wing};
use thiserror::Error;
type MakeMoveResult = Result<MoveRecord, MakeMoveError>;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum ValidateMove {
#[default]
No,
Yes,
}
#[derive(Debug, Error, Eq, PartialEq)]
pub enum MakeMoveError {
#[error("no piece on {0}")]
NoPiece(Square),
#[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,
origin: Square,
target: Square,
},
#[error("{0}")]
PlacePieceError(#[from] PlacePieceError),
#[error("{0}")]
CastleError(#[from] CastleEvaluationError),
#[error("cannot promote on {0}")]
InvalidPromotion(Square),
#[error("move to {0} requires promotion")]
PromotionRequired(Square),
}
pub trait MakeMove {
fn make_move(&mut self, ply: Move, validate: ValidateMove) -> MakeMoveResult;
}
trait MakeMoveInternal {
fn make_quiet_move(&mut self, ply: Move) -> MakeMoveResult;
fn make_double_push_move(&mut self, ply: Move) -> MakeMoveResult;
fn make_capture_move(&mut self, ply: Move) -> MakeMoveResult;
fn make_castle_move(&mut self, ply: Move, wing: Wing) -> MakeMoveResult;
fn make_promotion_move(&mut self, ply: Move) -> MakeMoveResult;
fn validate_move(&self, ply: Move, validate: ValidateMove) -> Result<(), MakeMoveError>;
fn validate_active_piece(&self, ply: Move) -> Result<Piece, MakeMoveError>;
fn advance_clocks(&mut self, half_move_clock: HalfMoveClock);
}
impl<T: BoardProvider> MakeMove for T {
/// Make a move in the position.
///
/// ## Errors
///
/// If `validate` is [`ValidateMove::Yes`], perform validation of move correctness prior to
/// applying the move. See [`Position::validate_move`].
fn make_move(
&mut self,
ply: Move,
validate: ValidateMove,
) -> Result<MoveRecord, MakeMoveError> {
if ply.is_quiet() {
self.validate_move(ply, validate)?;
return self.make_quiet_move(ply);
}
if ply.is_double_push() {
self.validate_move(ply, validate)?;
return self.make_double_push_move(ply);
}
if ply.is_capture() {
self.validate_move(ply, validate)?;
return self.make_capture_move(ply);
}
if let Some(wing) = ply.castle_wing() {
return self.make_castle_move(ply, wing);
}
if ply.is_promotion() {
self.validate_move(ply, validate)?;
return self.make_promotion_move(ply);
}
unreachable!();
}
}
impl<T: BoardProvider> MakeMoveInternal for T {
fn make_quiet_move(&mut self, ply: Move) -> MakeMoveResult {
let board = self.board_mut();
let origin = ply.origin_square();
let piece = board
.get_piece(origin)
.ok_or(MakeMoveError::NoPiece(origin))?;
let target = ply.target_square();
if piece.is_pawn() && target.rank().is_promotable_rank() {
return Err(MakeMoveError::PromotionRequired(target));
}
board
.place_piece(piece, target, PlacePieceStrategy::PreserveExisting)
.map_err(MakeMoveError::PlacePieceError)?;
board.remove_piece(origin);
let record = MoveRecord::new(board, ply, None);
self.advance_clocks(HalfMoveClock::Advance);
Ok(record)
}
fn make_double_push_move(&mut self, ply: Move) -> MakeMoveResult {
let board = self.board_mut();
let origin = ply.origin_square();
let piece = board
.remove_piece(origin)
.ok_or(MakeMoveError::NoPiece(origin))?;
let target = ply.target_square();
board
.place_piece(piece, target, PlacePieceStrategy::PreserveExisting)
.map_err(MakeMoveError::PlacePieceError)?;
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!(),
};
let record = MoveRecord::new(board, ply, None);
self.advance_clocks(HalfMoveClock::Advance);
Ok(record)
}
fn make_capture_move(&mut self, ply: Move) -> MakeMoveResult {
let origin_square = ply.origin_square();
let target_square = ply.target_square();
let board = self.board_mut();
let piece = board
.get_piece(origin_square)
.ok_or(MakeMoveError::NoPiece(origin_square))?;
if ply.is_en_passant() {
let en_passant_square = board
.en_passant_target
.ok_or(MakeMoveError::NoCaptureSquare)?;
if target_square != en_passant_square {
return Err(MakeMoveError::InvalidEnPassantCapture(target_square));
}
}
let board = self.board_mut();
let capture_square = ply.capture_square().ok_or(MakeMoveError::NoCaptureSquare)?;
let captured_piece = board
.remove_piece(capture_square)
.ok_or(MakeMoveError::NoCapturePiece(capture_square))?;
board.remove_piece(origin_square).unwrap();
if let Some(promotion_shape) = ply.promotion_shape() {
let promoted_piece = Piece::new(piece.color, promotion_shape);
board.place_piece(promoted_piece, target_square, PlacePieceStrategy::Replace)?;
} else {
board.place_piece(piece, target_square, PlacePieceStrategy::Replace)?;
}
let record = MoveRecord::new(board, ply, Some(captured_piece));
self.advance_clocks(HalfMoveClock::Reset);
Ok(record)
}
fn make_castle_move(&mut self, ply: Move, wing: Wing) -> MakeMoveResult {
let board = self.board_mut();
board.color_can_castle(wing, None)?;
let active_color = board.active_color;
let parameters = Board::castling_parameters(wing, active_color);
let king = board.remove_piece(parameters.origin.king).unwrap();
board.place_piece(king, parameters.target.king, PlacePieceStrategy::default())?;
let rook = board.remove_piece(parameters.origin.rook).unwrap();
board.place_piece(rook, parameters.target.rook, PlacePieceStrategy::default())?;
board.castling_rights.revoke(active_color, wing);
let record = MoveRecord::new(board, ply, None);
self.advance_clocks(HalfMoveClock::Advance);
Ok(record)
}
fn make_promotion_move(&mut self, ply: Move) -> MakeMoveResult {
let board = self.board_mut();
let origin = ply.origin_square();
let piece = board
.get_piece(origin)
.ok_or(MakeMoveError::NoPiece(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() {
board.remove_piece(origin);
let promoted_piece = Piece::new(piece.color, promotion_shape);
board.place_piece(promoted_piece, target, PlacePieceStrategy::PreserveExisting)?;
} else {
unreachable!(
"Cannot make a promotion move with a ply that has no promotion shape: {ply:?}",
);
}
let record = MoveRecord::new(board, ply, None);
self.advance_clocks(HalfMoveClock::Reset);
Ok(record)
}
fn advance_clocks(&mut self, half_move_clock: HalfMoveClock) {
let board = self.board_mut();
match half_move_clock {
HalfMoveClock::Reset => board.half_move_clock = 0,
HalfMoveClock::Advance => board.half_move_clock += 1,
}
board.active_color = board.active_color.next();
if board.active_color == Color::White {
board.full_move_number += 1;
}
}
fn validate_move(&self, ply: Move, validate: ValidateMove) -> Result<(), MakeMoveError> {
if validate == ValidateMove::No {
return Ok(());
}
let active_piece = self.validate_active_piece(ply)?;
let board = self.board();
let origin_square = ply.origin_square();
let target_square = ply.target_square();
// Pawns can see squares they can't move to. So, calculating valid
// squares requires a concept that includes Sight, but adds pawn pushes.
// In ChessFriend, that concept is Movement.
let movement = active_piece.movement(origin_square, board);
if !movement.contains(target_square) {
return Err(MakeMoveError::NoMove {
piece: active_piece,
origin: origin_square,
target: target_square,
});
}
// TODO: En Passant capture.
if let Some(capture_square) = ply.capture_square() {
if let Some(captured_piece) = board.get_piece(capture_square) {
if captured_piece.color == active_piece.color {
return Err(MakeMoveError::InvalidCapture(capture_square));
}
} else {
return Err(MakeMoveError::NoPiece(capture_square));
}
}
Ok(())
}
fn validate_active_piece(&self, ply: Move) -> Result<Piece, MakeMoveError> {
let origin_square = ply.origin_square();
let board = self.board();
let active_piece = board
.get_piece(origin_square)
.ok_or(MakeMoveError::NoPiece(origin_square))?;
if active_piece.color != board.active_color {
return Err(MakeMoveError::NonActiveColor {
piece: active_piece,
square: origin_square,
});
}
Ok(active_piece)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
enum HalfMoveClock {
Reset,
#[default]
Advance,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Move, PromotionShape};
use chessfriend_board::test_board;
use chessfriend_core::{piece, Color, Square};
type TestResult = Result<(), MakeMoveError>;
#[test]
fn make_quiet_move() -> TestResult {
let mut board = test_board!(White Pawn on C2);
let ply = Move::quiet(Square::C2, Square::C3);
board.make_move(ply, ValidateMove::Yes)?;
assert_eq!(board.get_piece(Square::C2), None);
assert_eq!(board.get_piece(Square::C3), Some(piece!(White Pawn)));
assert_eq!(board.active_color, Color::Black);
assert_eq!(board.half_move_clock, 1);
board.active_color = Color::White;
let ply = Move::quiet(Square::C3, Square::C4);
board.make_move(ply, ValidateMove::Yes)?;
assert_eq!(board.get_piece(Square::C3), None);
assert_eq!(board.get_piece(Square::C4), Some(piece!(White Pawn)));
assert_eq!(board.active_color, Color::Black);
assert_eq!(board.half_move_clock, 2);
Ok(())
}
#[test]
fn make_invalid_quiet_pawn_move() {
let mut board = test_board!(White Pawn on C2);
let ply = Move::quiet(Square::C2, Square::D2);
let result = board.make_move(ply, ValidateMove::Yes);
assert!(result.is_err());
assert_eq!(board.get_piece(Square::C2), Some(piece!(White Pawn)));
assert_eq!(board.get_piece(Square::D2), None);
assert_eq!(board.active_color, Color::White);
assert_eq!(board.half_move_clock, 0);
}
#[test]
fn make_capture_move() -> TestResult {
let mut board = test_board![
White Bishop on C2,
Black Rook on F5,
];
let ply = Move::capture(Square::C2, Square::F5);
let result = board.make_move(ply, ValidateMove::Yes)?;
assert_eq!(result.captured_piece, Some(piece!(Black Rook)));
assert_eq!(board.get_piece(Square::C2), None);
assert_eq!(board.get_piece(Square::F5), Some(piece!(White Bishop)));
assert_eq!(board.active_color, Color::Black);
assert_eq!(board.half_move_clock, 0);
Ok(())
}
#[test]
fn make_en_passant_capture_move() -> TestResult {
let mut board = test_board![
Black Pawn on F4,
White Pawn on E2
];
let ply = Move::double_push(Square::E2, Square::E4);
board.make_move(ply, ValidateMove::Yes)?;
assert_eq!(board.get_piece(Square::E2), None);
assert_eq!(board.get_piece(Square::E4), Some(piece!(White Pawn)));
assert_eq!(
board.en_passant_target,
Some(Square::E3),
"en passant square not set"
);
assert_eq!(board.active_color, Color::Black);
assert_eq!(board.half_move_clock, 1);
let ply = Move::en_passant_capture(Square::F4, Square::E3);
let result = board.make_move(ply, ValidateMove::Yes)?;
assert_eq!(result.captured_piece, Some(piece!(White Pawn)));
assert_eq!(board.get_piece(Square::F4), None);
assert_eq!(board.get_piece(Square::E3), Some(piece!(Black Pawn)));
assert_eq!(
board.get_piece(Square::E4),
None,
"capture target pawn not removed"
);
Ok(())
}
#[test]
fn make_last_rank_quiet_move_without_promotion() {
let mut board = test_board!(
White Pawn on A7
);
let ply = Move::quiet(Square::A7, Square::A8);
let result = board.make_move(ply, ValidateMove::Yes);
assert!(result.is_err());
assert_eq!(board.active_color, Color::White);
assert_eq!(board.get_piece(Square::A7), Some(piece!(White Pawn)));
assert_eq!(board.get_piece(Square::A8), None);
assert_eq!(board.half_move_clock, 0);
}
#[test]
fn make_promotion_move() -> TestResult {
let mut board = test_board![
Black Pawn on E7,
White Pawn on F7,
];
let ply = Move::promotion(Square::F7, Square::F8, PromotionShape::Queen);
board.make_move(ply, ValidateMove::Yes)?;
assert_eq!(board.get_piece(Square::F7), None);
assert_eq!(board.get_piece(Square::F8), Some(piece!(White Queen)));
assert_eq!(board.active_color, Color::Black);
assert_eq!(board.half_move_clock, 0);
Ok(())
}
#[test]
fn make_white_kingside_castle() -> TestResult {
let mut board = test_board![
White Rook on H1,
White King on E1,
];
let ply = Move::castle(Wing::KingSide);
board.make_move(ply, ValidateMove::Yes)?;
assert_eq!(board.active_color, Color::Black);
assert_eq!(board.get_piece(Square::E1), None);
assert_eq!(board.get_piece(Square::H1), None);
assert_eq!(board.get_piece(Square::G1), Some(piece!(White King)));
assert_eq!(board.get_piece(Square::F1), Some(piece!(White Rook)));
assert!(!board
.castling_rights
.color_has_right(Color::White, Wing::KingSide));
Ok(())
}
#[test]
fn make_white_queenside_castle() -> TestResult {
let mut board = test_board![
White King on E1,
White Rook on A1,
];
let ply = Move::castle(Wing::QueenSide);
board.make_move(ply, ValidateMove::Yes)?;
assert_eq!(board.active_color, Color::Black);
assert_eq!(board.get_piece(Square::E1), None);
assert_eq!(board.get_piece(Square::A1), None);
assert_eq!(board.get_piece(Square::C1), Some(piece!(White King)));
assert_eq!(board.get_piece(Square::D1), Some(piece!(White Rook)));
assert!(!board
.castling_rights
.color_has_right(Color::White, Wing::QueenSide));
Ok(())
}
}