[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:
parent
6816e350eb
commit
0c1863acb9
18 changed files with 499 additions and 258 deletions
|
@ -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};
|
||||
|
|
|
@ -5,5 +5,5 @@ mod position;
|
|||
|
||||
pub use {
|
||||
make_move::{MakeMoveError, ValidateMove},
|
||||
position::Position,
|
||||
position::{CastleEvaluationError, Position},
|
||||
};
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use crate::MakeMoveError;
|
||||
use crate::position::MakeMoveError;
|
||||
use chessfriend_moves::BuildMoveError;
|
||||
|
||||
#[macro_export]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue