[board, core, moves, position] Implement castling

Implement a new method on Position that evaluates whether the active color can castle
on a given wing of the board. Then, implement making a castling move in the position.

Make a new Wing enum in the core crate to specify kingside or queenside. Replace the
Castle enum from the board crate with this one. This caused a lot of churn...

Along the way fix a bunch of tests.

Note: there's still no way to actually make a castling move in explorer.
This commit is contained in:
Eryn Wells 2025-05-19 16:50:30 -07:00
parent 6816e350eb
commit 0c1863acb9
18 changed files with 499 additions and 258 deletions

View file

@ -14,4 +14,5 @@ mod macros;
#[macro_use]
mod testing;
pub use position::{MakeMoveError, MoveBuilder as MakeMoveBuilder, Position};
pub use chessfriend_board::{fen, PlacePieceError, PlacePieceStrategy};
pub use position::{CastleEvaluationError, Position, ValidateMove};

View file

@ -5,5 +5,5 @@ mod position;
pub use {
make_move::{MakeMoveError, ValidateMove},
position::Position,
position::{CastleEvaluationError, Position},
};

View file

@ -1,11 +1,13 @@
// Eryn Wells <eryn@erynwells.me>
use crate::{movement::Movement, Position};
use chessfriend_board::{PlacePieceError, PlacePieceStrategy};
use chessfriend_core::{Color, Piece, Square};
use chessfriend_board::{CastleParameters, PlacePieceError, PlacePieceStrategy};
use chessfriend_core::{Color, Piece, Square, Wing};
use chessfriend_moves::Move;
use thiserror::Error;
use super::CastleEvaluationError;
type MakeMoveResult = Result<(), MakeMoveError>;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
@ -38,6 +40,9 @@ pub enum MakeMoveError {
#[error("{0}")]
PlacePieceError(#[from] PlacePieceError),
#[error("{0}")]
CastleError(#[from] CastleEvaluationError),
}
pub enum UnmakeMoveError {}
@ -60,6 +65,10 @@ impl Position {
return self.make_capture_move(ply);
}
if let Some(wing) = ply.castle() {
return self.make_castle_move(wing);
}
Ok(())
}
@ -100,6 +109,27 @@ impl Position {
Ok(())
}
fn make_castle_move(&mut self, wing: Wing) -> MakeMoveResult {
self.active_color_can_castle(wing)?;
let active_color = self.board.active_color;
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())?;
let rook = self.board.remove_piece(parameters.origin.rook).unwrap();
self.board
.place_piece(rook, parameters.target.rook, PlacePieceStrategy::default())?;
self.board.castling_rights.revoke(active_color, wing);
self.advance_clocks(HalfMoveClock::Advance);
Ok(())
}
}
impl Position {
@ -142,21 +172,14 @@ impl Position {
return Ok(());
}
let active_piece = self.validate_active_piece(ply)?;
let origin_square = ply.origin_square();
let active_piece = self
.board
.get_piece(origin_square)
.ok_or(MakeMoveError::NoPiece(origin_square))?;
if active_piece.color != self.board.active_color {
return Err(MakeMoveError::NonActiveColor {
piece: active_piece,
square: 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, &self.board);
if !movement.contains(target_square) {
return Err(MakeMoveError::NoMove {
@ -166,6 +189,8 @@ 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) {
@ -179,4 +204,22 @@ impl Position {
Ok(())
}
fn validate_active_piece(&self, ply: Move) -> Result<Piece, MakeMoveError> {
let origin_square = ply.origin_square();
let active_piece = self
.board
.get_piece(origin_square)
.ok_or(MakeMoveError::NoPiece(origin_square))?;
if active_piece.color != self.board.active_color {
return Err(MakeMoveError::NonActiveColor {
piece: active_piece,
square: origin_square,
});
}
Ok(active_piece)
}
}

View file

@ -38,20 +38,168 @@ impl Position {
}
}
/*
impl Position {
/// Return a PlacedPiece representing the rook to use for a castling move.
pub(crate) fn rook_for_castle(&self, player: Color, castle: Castle) -> Option<PlacedPiece> {
let square = match (player, castle) {
(Color::White, Castle::KingSide) => Square::H1,
(Color::White, Castle::QueenSide) => Square::A1,
(Color::Black, Castle::KingSide) => Square::H8,
(Color::Black, Castle::QueenSide) => Square::A8,
};
self.board.piece_on_square(square)
/// Place a piece on the board.
///
/// ## Errors
///
/// See [`chessfriend_board::Board::place_piece`].
pub fn place_piece(
&mut self,
piece: Piece,
square: Square,
strategy: PlacePieceStrategy,
) -> Result<(), PlacePieceError> {
self.board.place_piece(piece, square, strategy)
}
#[must_use]
pub fn get_piece(&self, square: Square) -> Option<Piece> {
self.board.get_piece(square)
}
pub fn remove_piece(&mut self, square: Square) -> Option<Piece> {
self.board.remove_piece(square)
}
}
impl Position {
pub fn sight(&self, square: Square) -> BitBoard {
if let Some(piece) = self.get_piece(square) {
piece.sight(square, &self.board)
} else {
BitBoard::empty()
}
}
pub fn movement(&self, square: Square) -> BitBoard {
if let Some(piece) = self.get_piece(square) {
piece.movement(square, &self.board)
} else {
BitBoard::empty()
}
}
}
impl Position {
pub fn active_sight(&self) -> BitBoard {
self.friendly_sight(self.board.active_color)
}
/// A [`BitBoard`] of all squares the given color can see.
pub fn friendly_sight(&self, color: Color) -> BitBoard {
// TODO: Probably want to implement a caching layer here.
self.board
.friendly_occupancy(color)
.occupied_squares(&IterationDirection::default())
.map(|square| self.sight(square))
.fold(BitBoard::empty(), BitOr::bitor)
}
/// A [`BitBoard`] of all squares visible by colors that oppose the given color.
pub fn opposing_sight(&self) -> BitBoard {
// TODO: Probably want to implement a caching layer here.
let active_color = self.board.active_color;
Color::ALL
.into_iter()
.filter_map(|c| {
if c == active_color {
None
} else {
Some(self.friendly_sight(c))
}
})
.fold(BitBoard::empty(), BitOr::bitor)
}
}
#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
pub enum CastleEvaluationError {
#[error("{color} does not have the right to castle {wing}")]
NoRights { color: Color, wing: Wing },
#[error("no king")]
NoKing,
#[error("no rook")]
NoRook,
#[error("castling path is not clear")]
ObstructingPieces,
#[error("opposing pieces check castling path")]
CheckingPieces,
}
impl Position {
/// Evaluates whether the active color can castle toward the given wing of the board in the
/// current position.
///
/// ## Errors
///
/// Returns an error indicating why the active color cannot castle.
pub fn active_color_can_castle(&self, wing: Wing) -> Result<(), CastleEvaluationError> {
// TODO: Cache this result. It's expensive!
let active_color = self.board.active_color;
if !self
.board
.castling_rights
.color_has_right(active_color, wing)
{
return Err(CastleEvaluationError::NoRights {
color: active_color,
wing,
});
}
let parameters = self.board.castling_parameters(wing);
if self.castling_king(parameters.origin.king).is_none() {
return Err(CastleEvaluationError::NoKing);
}
if self.castling_rook(parameters.origin.rook).is_none() {
return Err(CastleEvaluationError::NoRook);
}
// All squares must be clear.
let has_obstructing_pieces = (self.board.occupancy() & parameters.clear).is_populated();
if has_obstructing_pieces {
return Err(CastleEvaluationError::ObstructingPieces);
}
// King cannot pass through check.
let opposing_sight = self.opposing_sight();
let opposing_pieces_can_see_castling_path =
(parameters.check & opposing_sight).is_populated();
if opposing_pieces_can_see_castling_path {
return Err(CastleEvaluationError::CheckingPieces);
}
Ok(())
}
fn castling_king(&self, square: Square) -> Option<Piece> {
self.get_piece(square).and_then(|piece| {
if piece.color == self.board.active_color && piece.is_king() {
Some(piece)
} else {
None
}
})
}
fn castling_rook(&self, square: Square) -> Option<Piece> {
self.get_piece(square).and_then(|piece| {
if piece.color == self.board.active_color && piece.is_rook() {
Some(piece)
} else {
None
}
})
}
}
/*
impl Position {
pub fn moves(&self) -> &Moves {
self.moves.get_or_init(|| {
let player_to_move = self.player_to_move();
@ -120,11 +268,6 @@ impl Position {
self.moves().moves_for_piece(piece)
}
#[cfg(test)]
pub(crate) fn sight_of_piece(&self, piece: &PlacedPiece) -> BitBoard {
piece.sight(&self.board, self._en_passant_target_square())
}
#[cfg(test)]
pub(crate) fn is_king_in_check(&self) -> bool {
let danger_squares = self.king_danger(self.player_to_move());
@ -213,9 +356,9 @@ impl fmt::Display for Position {
#[cfg(test)]
mod tests {
use super::*;
use crate::{assert_eq_bitboards, position, test_position, Position};
use crate::{test_position, Position};
use chessfriend_bitboard::bitboard;
use chessfriend_core::piece;
use chessfriend_core::{piece, Wing};
#[test]
fn piece_on_square() {
@ -223,22 +366,16 @@ mod tests {
Black Bishop on F7,
];
let piece = pos.board.piece_on_square(Square::F7);
assert_eq!(piece, Some(piece!(Black Bishop on F7)));
let piece = pos.board.get_piece(Square::F7);
assert_eq!(piece, Some(piece!(Black Bishop)));
}
#[test]
fn piece_in_starting_position() {
let pos = test_position!(starting);
assert_eq!(
pos.board.piece_on_square(Square::H1),
Some(piece!(White Rook on H1))
);
assert_eq!(
pos.board.piece_on_square(Square::A8),
Some(piece!(Black Rook on A8))
);
assert_eq!(pos.board.get_piece(Square::H1), Some(piece!(White Rook)));
assert_eq!(pos.board.get_piece(Square::A8), Some(piece!(Black Rook)));
}
#[test]
@ -274,38 +411,160 @@ mod tests {
White Rook on H1
);
assert!(pos.player_can_castle(Color::White, Castle::KingSide));
assert!(pos.player_can_castle(Color::White, Castle::QueenSide));
let rights = pos.board.castling_rights;
assert!(rights.color_has_right(Color::White, Wing::KingSide));
assert!(rights.color_has_right(Color::White, Wing::QueenSide));
}
#[test]
fn rook_for_castle() {
let pos = position![
fn friendly_sight() {
let pos = test_position!(
White King on E4,
);
let sight = pos.active_sight();
assert_eq!(sight, bitboard![E5 F5 F4 F3 E3 D3 D4 D5]);
}
#[test]
fn opposing_sight() {
let pos = test_position!(
White King on E4,
Black Rook on E7,
);
let sight = pos.opposing_sight();
assert_eq!(sight, bitboard![A7 B7 C7 D7 F7 G7 H7 E8 E6 E5 E4]);
}
#[test]
fn king_for_castle() {
let pos = test_position![
White King on E1,
White Rook on H1,
White Rook on A1,
];
let kingside_parameters = pos.board.castling_parameters(Wing::KingSide);
assert_eq!(
pos.rook_for_castle(Color::White, Castle::KingSide),
Some(piece!(White Rook on H1))
pos.castling_king(kingside_parameters.origin.king),
Some(piece!(White King))
);
let queenside_parameters = pos.board.castling_parameters(Wing::QueenSide);
assert_eq!(
pos.rook_for_castle(Color::White, Castle::QueenSide),
Some(piece!(White Rook on A1))
pos.castling_king(queenside_parameters.origin.king),
Some(piece!(White King))
);
}
#[test]
fn danger_squares() {
let pos = test_position!(Black, [
fn rook_for_castle() {
let pos = test_position![
White King on E1,
Black King on E7,
White Rook on E4,
]);
White Rook on H1,
];
let danger_squares = pos.king_danger(Color::Black);
let expected = bitboard![D1 F1 D2 E2 F2 E3 A4 B4 C4 D4 F4 G4 H4 E5 E6 E7 E8];
assert_eq_bitboards!(danger_squares, expected);
let kingside_parameters = pos.board.castling_parameters(Wing::KingSide);
assert_eq!(
pos.castling_rook(kingside_parameters.origin.rook),
Some(piece!(White Rook))
);
let pos = test_position![
White King on E1,
White Rook on A1,
];
let queenside_parameters = pos.board.castling_parameters(Wing::QueenSide);
assert_eq!(
pos.castling_rook(queenside_parameters.origin.rook),
Some(piece!(White Rook))
);
}
#[test]
fn white_can_castle() {
let pos = test_position![
White King on E1,
White Rook on H1,
White Rook on A1,
];
assert_eq!(pos.active_color_can_castle(Wing::KingSide), Ok(()));
assert_eq!(pos.active_color_can_castle(Wing::QueenSide), Ok(()));
}
#[test]
fn white_cannot_castle_missing_king() {
let pos = test_position![
White King on E2,
White Rook on H1,
White Rook on A1,
];
assert_eq!(
pos.active_color_can_castle(Wing::KingSide),
Err(CastleEvaluationError::NoKing)
);
assert_eq!(
pos.active_color_can_castle(Wing::QueenSide),
Err(CastleEvaluationError::NoKing)
);
}
#[test]
fn white_cannot_castle_missing_rook() {
let pos = test_position![
White King on E1,
White Rook on A1,
];
assert_eq!(
pos.active_color_can_castle(Wing::KingSide),
Err(CastleEvaluationError::NoRook)
);
let pos = test_position![
White King on E1,
White Rook on H1,
];
assert_eq!(
pos.active_color_can_castle(Wing::QueenSide),
Err(CastleEvaluationError::NoRook)
);
}
#[test]
fn white_cannot_castle_obstructing_piece() {
let pos = test_position![
White King on E1,
White Bishop on F1,
White Rook on H1,
White Rook on A1,
];
assert_eq!(
pos.active_color_can_castle(Wing::KingSide),
Err(CastleEvaluationError::ObstructingPieces)
);
assert_eq!(pos.active_color_can_castle(Wing::QueenSide), Ok(()));
}
#[test]
fn white_cannot_castle_checking_pieces() {
let pos = test_position![
White King on E1,
White Rook on H1,
White Rook on A1,
Black Queen on C6,
];
assert_eq!(pos.active_color_can_castle(Wing::KingSide), Ok(()));
assert_eq!(
pos.active_color_can_castle(Wing::QueenSide),
Err(CastleEvaluationError::CheckingPieces)
);
}
}

View file

@ -1,6 +1,6 @@
// Eryn Wells <eryn@erynwells.me>
use crate::MakeMoveError;
use crate::position::MakeMoveError;
use chessfriend_moves::BuildMoveError;
#[macro_export]