[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

@ -7,7 +7,7 @@ use crate::{
PieceSet,
};
use chessfriend_bitboard::BitBoard;
use chessfriend_core::{Color, Piece, Shape, Square};
use chessfriend_core::{Color, Piece, Shape, Square, Wing};
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Board {
@ -82,10 +82,12 @@ impl Board {
}
impl Board {
/// A [`BitBoard`] of squares occupied by pieces of all colors.
pub fn occupancy(&self) -> BitBoard {
self.pieces.occpuancy()
}
/// A [`BitBoard`] of squares that are vacant.
pub fn vacancy(&self) -> BitBoard {
!self.occupancy()
}
@ -99,6 +101,13 @@ impl Board {
}
}
impl Board {
#[must_use]
pub fn castling_parameters(&self, wing: Wing) -> &'static castle::Parameters {
&castle::Parameters::BY_COLOR[self.active_color as usize][wing as usize]
}
}
impl Board {
pub fn display(&self) -> DiagramFormatter<'_> {
DiagramFormatter::new(self)

View file

@ -3,22 +3,5 @@
mod parameters;
mod rights;
pub use parameters::Parameters;
pub use rights::Rights;
use chessfriend_core::Color;
use parameters::Parameters;
#[repr(u8)]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Castle {
KingSide = 0,
QueenSide = 1,
}
impl Castle {
pub const ALL: [Castle; 2] = [Castle::KingSide, Castle::QueenSide];
pub fn parameters(self, color: Color) -> &'static Parameters {
&Parameters::BY_COLOR[color as usize][self as usize]
}
}

View file

@ -1,31 +1,32 @@
use chessfriend_bitboard::BitBoard;
use chessfriend_core::{Color, Square};
use chessfriend_core::{Color, Square, Wing};
#[derive(Debug)]
pub struct Parameters {
/// Origin squares of the king and rook.
origin: Squares,
pub origin: Squares,
/// Target or destination squares for the king and rook.
target: Squares,
pub target: Squares,
/// The set of squares that must be clear of any pieces in order to perform
/// this castle.
clear: BitBoard,
pub clear: BitBoard,
/// The set of squares that must not be attacked (i.e. visible to opposing
/// pieces) in order to perform this castle.
check: BitBoard,
pub check: BitBoard,
}
#[derive(Debug)]
pub(super) struct Squares {
pub struct Squares {
pub king: Square,
pub rook: Square,
}
impl Parameters {
/// Parameters for each castling move, organized by color and board-side.
pub(super) const BY_COLOR: [[Self; 2]; Color::NUM] = [
pub(crate) const BY_COLOR: [[Self; Wing::NUM]; Color::NUM] = [
[
Parameters {
origin: Squares {
@ -80,31 +81,8 @@ impl Parameters {
],
];
pub fn king_origin_square(&self) -> Square {
self.origin.king
}
pub fn rook_origin_square(&self) -> Square {
self.origin.rook
}
pub fn king_target_square(&self) -> Square {
self.target.king
}
pub fn rook_target_square(&self) -> Square {
self.target.rook
}
/// A [`BitBoard`] of the squares that must be clear of any piece in order
/// to perform this castle move.
pub fn clear_squares(&self) -> &BitBoard {
&self.clear
}
/// A [`BitBoard`] of the squares that must not be visible to opposing
/// pieces in order to perform this castle move.
pub fn check_squares(&self) -> &BitBoard {
&self.check
#[must_use]
pub fn get(color: Color, wing: Wing) -> &'static Parameters {
&Self::BY_COLOR[color as usize][wing as usize]
}
}

View file

@ -1,5 +1,4 @@
use super::Castle;
use chessfriend_core::Color;
use chessfriend_core::{Color, Wing};
use std::fmt;
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
@ -13,16 +12,16 @@ impl Rights {
/// as long as they have not moved their king, or the rook on that side of
/// the board.
#[must_use]
pub fn color_has_right(self, color: Color, castle: Castle) -> bool {
(self.0 & (1 << Self::flag_offset(color, castle))) != 0
pub fn color_has_right(self, color: Color, wing: Wing) -> bool {
(self.0 & (1 << Self::flag_offset(color, wing))) != 0
}
pub fn grant(&mut self, color: Color, castle: Castle) {
self.0 |= 1 << Self::flag_offset(color, castle);
pub fn grant(&mut self, color: Color, wing: Wing) {
self.0 |= 1 << Self::flag_offset(color, wing);
}
pub fn revoke(&mut self, color: Color, castle: Castle) {
self.0 &= !(1 << Self::flag_offset(color, castle));
pub fn revoke(&mut self, color: Color, wing: Wing) {
self.0 &= !(1 << Self::flag_offset(color, wing));
}
/// Revoke castling rights for all colors and all sides of the board.
@ -32,8 +31,8 @@ impl Rights {
}
impl Rights {
fn flag_offset(color: Color, castle: Castle) -> usize {
((color as usize) << 1) + castle as usize
fn flag_offset(color: Color, wing: Wing) -> usize {
((color as usize) << 1) + wing as usize
}
}
@ -55,30 +54,30 @@ mod tests {
#[test]
fn bitfield_offsets() {
assert_eq!(Rights::flag_offset(Color::White, Castle::KingSide), 0);
assert_eq!(Rights::flag_offset(Color::White, Castle::QueenSide), 1);
assert_eq!(Rights::flag_offset(Color::Black, Castle::KingSide), 2);
assert_eq!(Rights::flag_offset(Color::Black, Castle::QueenSide), 3);
assert_eq!(Rights::flag_offset(Color::White, Wing::KingSide), 0);
assert_eq!(Rights::flag_offset(Color::White, Wing::QueenSide), 1);
assert_eq!(Rights::flag_offset(Color::Black, Wing::KingSide), 2);
assert_eq!(Rights::flag_offset(Color::Black, Wing::QueenSide), 3);
}
#[test]
fn default_rights() {
let mut rights = Rights::default();
assert!(rights.color_has_right(Color::White, Castle::KingSide));
assert!(rights.color_has_right(Color::White, Castle::QueenSide));
assert!(rights.color_has_right(Color::Black, Castle::KingSide));
assert!(rights.color_has_right(Color::Black, Castle::QueenSide));
assert!(rights.color_has_right(Color::White, Wing::KingSide));
assert!(rights.color_has_right(Color::White, Wing::QueenSide));
assert!(rights.color_has_right(Color::Black, Wing::KingSide));
assert!(rights.color_has_right(Color::Black, Wing::QueenSide));
rights.revoke(Color::White, Castle::QueenSide);
assert!(rights.color_has_right(Color::White, Castle::KingSide));
assert!(!rights.color_has_right(Color::White, Castle::QueenSide));
assert!(rights.color_has_right(Color::Black, Castle::KingSide));
assert!(rights.color_has_right(Color::Black, Castle::QueenSide));
rights.revoke(Color::White, Wing::QueenSide);
assert!(rights.color_has_right(Color::White, Wing::KingSide));
assert!(!rights.color_has_right(Color::White, Wing::QueenSide));
assert!(rights.color_has_right(Color::Black, Wing::KingSide));
assert!(rights.color_has_right(Color::Black, Wing::QueenSide));
rights.grant(Color::White, Castle::QueenSide);
assert!(rights.color_has_right(Color::White, Castle::KingSide));
assert!(rights.color_has_right(Color::White, Castle::QueenSide));
assert!(rights.color_has_right(Color::Black, Castle::KingSide));
assert!(rights.color_has_right(Color::Black, Castle::QueenSide));
rights.grant(Color::White, Wing::QueenSide);
assert!(rights.color_has_right(Color::White, Wing::KingSide));
assert!(rights.color_has_right(Color::White, Wing::QueenSide));
assert!(rights.color_has_right(Color::Black, Wing::KingSide));
assert!(rights.color_has_right(Color::Black, Wing::QueenSide));
}
}

View file

@ -1,7 +1,9 @@
// Eryn Wells <eryn@erynwells.me>
use crate::{piece_sets::PlacePieceStrategy, Board, Castle};
use chessfriend_core::{coordinates::ParseSquareError, piece, Color, File, Piece, Rank, Square};
use crate::{piece_sets::PlacePieceStrategy, Board};
use chessfriend_core::{
coordinates::ParseSquareError, piece, Color, File, Piece, Rank, Square, Wing,
};
use std::fmt::Write;
use thiserror::Error;
@ -115,10 +117,10 @@ impl ToFenStr for Board {
.map_err(ToFenStrError::FmtError)?;
let castling = [
(Color::White, Castle::KingSide),
(Color::White, Castle::QueenSide),
(Color::Black, Castle::KingSide),
(Color::Black, Castle::QueenSide),
(Color::White, Wing::KingSide),
(Color::White, Wing::QueenSide),
(Color::Black, Wing::KingSide),
(Color::Black, Wing::QueenSide),
]
.map(|(color, castle)| {
if !self.castling_rights.color_has_right(color, castle) {
@ -126,10 +128,10 @@ impl ToFenStr for Board {
}
match (color, castle) {
(Color::White, Castle::KingSide) => "K",
(Color::White, Castle::QueenSide) => "Q",
(Color::Black, Castle::KingSide) => "k",
(Color::Black, Castle::QueenSide) => "q",
(Color::White, Wing::KingSide) => "K",
(Color::White, Wing::QueenSide) => "Q",
(Color::Black, Wing::KingSide) => "k",
(Color::Black, Wing::QueenSide) => "q",
}
})
.concat();
@ -232,10 +234,10 @@ impl FromFenStr for Board {
} else {
for ch in castling_rights.chars() {
match ch {
'K' => board.castling_rights.grant(Color::White, Castle::KingSide),
'Q' => board.castling_rights.grant(Color::White, Castle::QueenSide),
'k' => board.castling_rights.grant(Color::Black, Castle::KingSide),
'q' => board.castling_rights.grant(Color::Black, Castle::QueenSide),
'K' => board.castling_rights.grant(Color::White, Wing::KingSide),
'Q' => board.castling_rights.grant(Color::White, Wing::QueenSide),
'k' => board.castling_rights.grant(Color::Black, Wing::KingSide),
'q' => board.castling_rights.grant(Color::Black, Wing::QueenSide),
_ => return Err(FromFenStrError::InvalidValue),
};
}

View file

@ -10,12 +10,7 @@ mod board;
mod piece_sets;
pub use board::Board;
pub use castle::Parameters as CastleParameters;
pub use piece_sets::{PlacePieceError, PlacePieceStrategy};
use castle::Castle;
use en_passant::EnPassant;
use piece_sets::PieceSet;
// Used by macros.
#[allow(unused_imports)]
use piece_sets::{PlacePieceError, PlacePieceStrategy};

View file

@ -1,56 +0,0 @@
use chessfriend_bitboard::BitBoard;
use chessfriend_core::{Color, Piece, Shape, Square};
/// A collection of bitboards that organize pieces by color.
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
pub(super) struct ByColor(BitBoard, [BitBoard; Color::NUM]);
/// A collection of bitboards that organize pieces first by color and then by piece type.
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
pub(super) struct ByColorAndShape([[BitBoard; Shape::NUM]; Color::NUM]);
impl ByColor {
pub(super) fn new(all_pieces: BitBoard, bitboards_by_color: [BitBoard; Color::NUM]) -> Self {
ByColor(all_pieces, bitboards_by_color)
}
pub(crate) fn all(&self) -> BitBoard {
self.0
}
pub(crate) fn bitboard(&self, color: Color) -> BitBoard {
self.1[color as usize]
}
pub(super) fn set_square(&mut self, square: Square, color: Color) {
self.0.set(square);
self.1[color as usize].set(square);
}
pub(super) fn clear_square(&mut self, square: Square, color: Color) {
self.0.clear(square);
self.1[color as usize].clear(square);
}
}
impl ByColorAndShape {
pub(super) fn new(bitboards: [[BitBoard; Shape::NUM]; Color::NUM]) -> Self {
Self(bitboards)
}
pub(super) fn bitboard_for_piece(&self, piece: Piece) -> BitBoard {
self.0[piece.color as usize][piece.shape as usize]
}
pub(super) fn bitboard_for_piece_mut(&mut self, piece: Piece) -> &mut BitBoard {
&mut self.0[piece.color as usize][piece.shape as usize]
}
pub(super) fn set_square(&mut self, square: Square, piece: Piece) {
self.bitboard_for_piece_mut(piece).set(square);
}
pub(super) fn clear_square(&mut self, square: Square, piece: Piece) {
self.bitboard_for_piece_mut(piece).clear(square);
}
}

View file

@ -1,5 +1,9 @@
// Eryn Wells <eryn@erynwells.me>
mod wings;
pub use wings::Wing;
use crate::Color;
use std::fmt;
use thiserror::Error;
@ -519,7 +523,7 @@ mod tests {
fn bad_algebraic_input() {
assert!("a0".parse::<Square>().is_err());
assert!("j3".parse::<Square>().is_err());
assert!("a11".parse::<Square>().is_err());
assert!("a9".parse::<Square>().is_err());
assert!("b-1".parse::<Square>().is_err());
assert!("a 1".parse::<Square>().is_err());
assert!("".parse::<Square>().is_err());

View file

@ -0,0 +1,22 @@
// Eryn Wells <eryn@erynwells.me>
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Wing {
KingSide = 0,
QueenSide = 1,
}
impl Wing {
pub const NUM: usize = 2;
pub const ALL: [Wing; Self::NUM] = [Self::KingSide, Self::QueenSide];
}
impl std::fmt::Display for Wing {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Wing::KingSide => write!(f, "kingside"),
Wing::QueenSide => write!(f, "queenside"),
}
}
}

View file

@ -8,6 +8,6 @@ pub mod shapes;
mod macros;
pub use colors::Color;
pub use coordinates::{Direction, File, Rank, Square};
pub use coordinates::{Direction, File, Rank, Square, Wing};
pub use pieces::{Piece, PlacedPiece};
pub use shapes::Shape;

View file

@ -1,8 +1,8 @@
// Eryn Wells <eryn@erynwells.me>
use crate::{defs::Kind, Move, PromotionShape};
use chessfriend_board::{castle, en_passant::EnPassant};
use chessfriend_core::{Color, File, PlacedPiece, Rank, Square};
use chessfriend_board::{en_passant::EnPassant, CastleParameters};
use chessfriend_core::{Color, File, PlacedPiece, Rank, Square, Wing};
use std::result::Result as StdResult;
use thiserror::Error;
@ -92,7 +92,7 @@ pub struct Promotion<S> {
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Castle {
color: Color,
castle: castle::Castle,
wing: Wing,
}
impl Style for Null {}
@ -119,13 +119,13 @@ impl Style for Capture {
impl Style for Castle {
fn origin_square(&self) -> Option<Square> {
let parameters = self.castle.parameters(self.color);
Some(parameters.king_origin_square())
let parameters = CastleParameters::get(self.color, self.wing);
Some(parameters.origin.king)
}
fn target_square(&self) -> Option<Square> {
let parameters = self.castle.parameters(self.color);
Some(parameters.king_target_square())
let parameters = CastleParameters::get(self.color, self.wing);
Some(parameters.target.king)
}
}
@ -255,9 +255,9 @@ impl Builder<Null> {
}
#[must_use]
pub fn castling(color: Color, castle: castle::Castle) -> Builder<Castle> {
pub fn castling(color: Color, wing: Wing) -> Builder<Castle> {
Builder {
style: Castle { color, castle },
style: Castle { color, wing },
}
}
@ -357,9 +357,9 @@ impl Builder<Push> {
impl Builder<Castle> {
fn bits(&self) -> u16 {
let bits = match self.style.castle {
castle::Castle::KingSide => Kind::KingSideCastle,
castle::Castle::QueenSide => Kind::QueenSideCastle,
let bits = match self.style.wing {
Wing::KingSide => Kind::KingSideCastle,
Wing::QueenSide => Kind::QueenSideCastle,
};
bits as u16
@ -403,6 +403,11 @@ impl Builder<EnPassantCapture> {
Move(Kind::EnPassantCapture as u16 | self.style.move_bits_unchecked())
}
/// Build an en passant move.
///
/// ## Errors
///
/// Returns an error if the target or origin squares are invalid.
pub fn build(&self) -> Result {
Ok(Move(
Kind::EnPassantCapture as u16 | self.style.move_bits()?,

View file

@ -2,8 +2,7 @@
use crate::builder::Builder;
use crate::defs::Kind;
use chessfriend_board::castle::Castle;
use chessfriend_core::{Rank, Shape, Square};
use chessfriend_core::{Rank, Shape, Square, Wing};
use std::fmt;
/// A single player's move. In game theory parlance, this is a "ply".
@ -62,10 +61,10 @@ impl Move {
}
#[must_use]
pub fn castle(&self) -> Option<Castle> {
pub fn castle(&self) -> Option<Wing> {
match self.flags() {
0b0010 => Some(Castle::KingSide),
0b0011 => Some(Castle::QueenSide),
0b0010 => Some(Wing::KingSide),
0b0011 => Some(Wing::QueenSide),
_ => None,
}
}
@ -77,7 +76,7 @@ impl Move {
#[must_use]
pub fn is_en_passant(&self) -> bool {
self.0 == Kind::EnPassantCapture as u16
self.flags() == Kind::EnPassantCapture as u16
}
#[must_use]
@ -130,8 +129,8 @@ impl fmt::Display for Move {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(castle) = self.castle() {
return match castle {
Castle::KingSide => write!(f, "{KINGSIDE_CASTLE_STR}"),
Castle::QueenSide => write!(f, "{QUEENSIDE_CASTLE_STR}"),
Wing::KingSide => write!(f, "{KINGSIDE_CASTLE_STR}"),
Wing::QueenSide => write!(f, "{QUEENSIDE_CASTLE_STR}"),
};
}

View file

@ -1,7 +1,6 @@
// Eryn Wells <eryn@erynwells.me>
use chessfriend_board::castle::Castle;
use chessfriend_core::{piece, Color, File, Shape, Square};
use chessfriend_core::{piece, Color, File, Shape, Square, Wing};
use chessfriend_moves::{testing::*, Builder, PromotionShape};
macro_rules! assert_flag {
@ -58,51 +57,50 @@ fn move_flags_capture() -> TestResult {
#[test]
fn move_flags_en_passant_capture() -> TestResult {
let mv = unsafe {
Builder::new()
.from(Square::A4)
.capturing_en_passant_on(Square::B3)
.build_unchecked()
};
let ply = Builder::new()
.from(Square::A4)
.capturing_en_passant_on(Square::B3)
.build()?;
assert_flags!(mv, false, false, true, true, false, false);
assert_eq!(mv.origin_square(), Square::A4);
assert_eq!(mv.target_square(), Square::B3);
assert_eq!(mv.capture_square(), Some(Square::B4));
assert!(ply.is_en_passant());
assert_eq!(ply.origin_square(), Square::A4);
assert_eq!(ply.target_square(), Square::B3);
assert_eq!(ply.capture_square(), Some(Square::B4));
Ok(())
}
#[test]
fn move_flags_promotion() -> TestResult {
let mv = Builder::push(&piece!(White Pawn on H7))
let ply = Builder::push(&piece!(White Pawn on H7))
.to(Square::H8)
.promoting_to(PromotionShape::Queen)
.build()?;
assert_flags!(mv, false, false, false, false, false, true);
assert_eq!(mv.promotion(), Some(Shape::Queen));
assert!(ply.is_promotion());
assert_eq!(ply.promotion(), Some(Shape::Queen));
Ok(())
}
#[test]
fn move_flags_capture_promotion() -> TestResult {
let mv = Builder::push(&piece!(White Pawn on H7))
let ply = Builder::push(&piece!(White Pawn on H7))
.to(Square::H8)
.capturing_piece(&piece!(Black Knight on G8))
.promoting_to(PromotionShape::Queen)
.build()?;
assert_flags!(mv, false, false, false, true, false, true);
assert_eq!(mv.promotion(), Some(Shape::Queen));
assert!(ply.is_capture());
assert!(ply.is_promotion());
assert_eq!(ply.promotion(), Some(Shape::Queen));
Ok(())
}
#[test]
fn move_flags_castle() -> TestResult {
let mv = Builder::castling(Color::White, Castle::KingSide).build()?;
let mv = Builder::castling(Color::White, Wing::KingSide).build()?;
assert_flags!(mv, false, false, false, false, true, false);

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]