[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, PieceSet,
}; };
use chessfriend_bitboard::BitBoard; 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)] #[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Board { pub struct Board {
@ -82,10 +82,12 @@ impl Board {
} }
impl Board { impl Board {
/// A [`BitBoard`] of squares occupied by pieces of all colors.
pub fn occupancy(&self) -> BitBoard { pub fn occupancy(&self) -> BitBoard {
self.pieces.occpuancy() self.pieces.occpuancy()
} }
/// A [`BitBoard`] of squares that are vacant.
pub fn vacancy(&self) -> BitBoard { pub fn vacancy(&self) -> BitBoard {
!self.occupancy() !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 { impl Board {
pub fn display(&self) -> DiagramFormatter<'_> { pub fn display(&self) -> DiagramFormatter<'_> {
DiagramFormatter::new(self) DiagramFormatter::new(self)

View file

@ -3,22 +3,5 @@
mod parameters; mod parameters;
mod rights; mod rights;
pub use parameters::Parameters;
pub use rights::Rights; 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_bitboard::BitBoard;
use chessfriend_core::{Color, Square}; use chessfriend_core::{Color, Square, Wing};
#[derive(Debug)]
pub struct Parameters { pub struct Parameters {
/// Origin squares of the king and rook. /// Origin squares of the king and rook.
origin: Squares, pub origin: Squares,
/// Target or destination squares for the king and rook. /// 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 /// The set of squares that must be clear of any pieces in order to perform
/// this castle. /// this castle.
clear: BitBoard, pub clear: BitBoard,
/// The set of squares that must not be attacked (i.e. visible to opposing /// The set of squares that must not be attacked (i.e. visible to opposing
/// pieces) in order to perform this castle. /// pieces) in order to perform this castle.
check: BitBoard, pub check: BitBoard,
} }
#[derive(Debug)] #[derive(Debug)]
pub(super) struct Squares { pub struct Squares {
pub king: Square, pub king: Square,
pub rook: Square, pub rook: Square,
} }
impl Parameters { impl Parameters {
/// Parameters for each castling move, organized by color and board-side. /// 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 { Parameters {
origin: Squares { origin: Squares {
@ -80,31 +81,8 @@ impl Parameters {
], ],
]; ];
pub fn king_origin_square(&self) -> Square { #[must_use]
self.origin.king pub fn get(color: Color, wing: Wing) -> &'static Parameters {
} &Self::BY_COLOR[color as usize][wing as usize]
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
} }
} }

View file

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

View file

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

View file

@ -10,12 +10,7 @@ mod board;
mod piece_sets; mod piece_sets;
pub use board::Board; pub use board::Board;
pub use castle::Parameters as CastleParameters;
pub use piece_sets::{PlacePieceError, PlacePieceStrategy}; pub use piece_sets::{PlacePieceError, PlacePieceStrategy};
use castle::Castle;
use en_passant::EnPassant;
use piece_sets::PieceSet; 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> // Eryn Wells <eryn@erynwells.me>
mod wings;
pub use wings::Wing;
use crate::Color; use crate::Color;
use std::fmt; use std::fmt;
use thiserror::Error; use thiserror::Error;
@ -519,7 +523,7 @@ mod tests {
fn bad_algebraic_input() { fn bad_algebraic_input() {
assert!("a0".parse::<Square>().is_err()); assert!("a0".parse::<Square>().is_err());
assert!("j3".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!("b-1".parse::<Square>().is_err());
assert!("a 1".parse::<Square>().is_err()); assert!("a 1".parse::<Square>().is_err());
assert!("".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; mod macros;
pub use colors::Color; 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 pieces::{Piece, PlacedPiece};
pub use shapes::Shape; pub use shapes::Shape;

View file

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

View file

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

View file

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

View file

@ -14,4 +14,5 @@ mod macros;
#[macro_use] #[macro_use]
mod testing; 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 { pub use {
make_move::{MakeMoveError, ValidateMove}, make_move::{MakeMoveError, ValidateMove},
position::Position, position::{CastleEvaluationError, Position},
}; };

View file

@ -1,11 +1,13 @@
// Eryn Wells <eryn@erynwells.me> // Eryn Wells <eryn@erynwells.me>
use crate::{movement::Movement, Position}; use crate::{movement::Movement, Position};
use chessfriend_board::{PlacePieceError, PlacePieceStrategy}; use chessfriend_board::{CastleParameters, PlacePieceError, PlacePieceStrategy};
use chessfriend_core::{Color, Piece, Square}; use chessfriend_core::{Color, Piece, Square, Wing};
use chessfriend_moves::Move; use chessfriend_moves::Move;
use thiserror::Error; use thiserror::Error;
use super::CastleEvaluationError;
type MakeMoveResult = Result<(), MakeMoveError>; type MakeMoveResult = Result<(), MakeMoveError>;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
@ -38,6 +40,9 @@ pub enum MakeMoveError {
#[error("{0}")] #[error("{0}")]
PlacePieceError(#[from] PlacePieceError), PlacePieceError(#[from] PlacePieceError),
#[error("{0}")]
CastleError(#[from] CastleEvaluationError),
} }
pub enum UnmakeMoveError {} pub enum UnmakeMoveError {}
@ -60,6 +65,10 @@ impl Position {
return self.make_capture_move(ply); return self.make_capture_move(ply);
} }
if let Some(wing) = ply.castle() {
return self.make_castle_move(wing);
}
Ok(()) Ok(())
} }
@ -100,6 +109,27 @@ impl Position {
Ok(()) 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 { impl Position {
@ -142,21 +172,14 @@ impl Position {
return Ok(()); return Ok(());
} }
let active_piece = self.validate_active_piece(ply)?;
let origin_square = ply.origin_square(); 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(); 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); let movement = active_piece.movement(origin_square, &self.board);
if !movement.contains(target_square) { if !movement.contains(target_square) {
return Err(MakeMoveError::NoMove { return Err(MakeMoveError::NoMove {
@ -166,6 +189,8 @@ impl Position {
}); });
} }
// TODO: En Passant capture.
if ply.is_capture() { if ply.is_capture() {
let target = ply.target_square(); let target = ply.target_square();
if let Some(captured_piece) = self.board.get_piece(target) { if let Some(captured_piece) = self.board.get_piece(target) {
@ -179,4 +204,22 @@ impl Position {
Ok(()) 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 { impl Position {
/// Return a PlacedPiece representing the rook to use for a castling move. /// Place a piece on the board.
pub(crate) fn rook_for_castle(&self, player: Color, castle: Castle) -> Option<PlacedPiece> { ///
let square = match (player, castle) { /// ## Errors
(Color::White, Castle::KingSide) => Square::H1, ///
(Color::White, Castle::QueenSide) => Square::A1, /// See [`chessfriend_board::Board::place_piece`].
(Color::Black, Castle::KingSide) => Square::H8, pub fn place_piece(
(Color::Black, Castle::QueenSide) => Square::A8, &mut self,
}; piece: Piece,
square: Square,
self.board.piece_on_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 { pub fn moves(&self) -> &Moves {
self.moves.get_or_init(|| { self.moves.get_or_init(|| {
let player_to_move = self.player_to_move(); let player_to_move = self.player_to_move();
@ -120,11 +268,6 @@ impl Position {
self.moves().moves_for_piece(piece) 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)] #[cfg(test)]
pub(crate) fn is_king_in_check(&self) -> bool { pub(crate) fn is_king_in_check(&self) -> bool {
let danger_squares = self.king_danger(self.player_to_move()); let danger_squares = self.king_danger(self.player_to_move());
@ -213,9 +356,9 @@ impl fmt::Display for Position {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::{assert_eq_bitboards, position, test_position, Position}; use crate::{test_position, Position};
use chessfriend_bitboard::bitboard; use chessfriend_bitboard::bitboard;
use chessfriend_core::piece; use chessfriend_core::{piece, Wing};
#[test] #[test]
fn piece_on_square() { fn piece_on_square() {
@ -223,22 +366,16 @@ mod tests {
Black Bishop on F7, Black Bishop on F7,
]; ];
let piece = pos.board.piece_on_square(Square::F7); let piece = pos.board.get_piece(Square::F7);
assert_eq!(piece, Some(piece!(Black Bishop on F7))); assert_eq!(piece, Some(piece!(Black Bishop)));
} }
#[test] #[test]
fn piece_in_starting_position() { fn piece_in_starting_position() {
let pos = test_position!(starting); let pos = test_position!(starting);
assert_eq!( assert_eq!(pos.board.get_piece(Square::H1), Some(piece!(White Rook)));
pos.board.piece_on_square(Square::H1), assert_eq!(pos.board.get_piece(Square::A8), Some(piece!(Black Rook)));
Some(piece!(White Rook on H1))
);
assert_eq!(
pos.board.piece_on_square(Square::A8),
Some(piece!(Black Rook on A8))
);
} }
#[test] #[test]
@ -274,38 +411,160 @@ mod tests {
White Rook on H1 White Rook on H1
); );
assert!(pos.player_can_castle(Color::White, Castle::KingSide)); let rights = pos.board.castling_rights;
assert!(pos.player_can_castle(Color::White, Castle::QueenSide)); assert!(rights.color_has_right(Color::White, Wing::KingSide));
assert!(rights.color_has_right(Color::White, Wing::QueenSide));
} }
#[test] #[test]
fn rook_for_castle() { fn friendly_sight() {
let pos = position![ 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 King on E1,
White Rook on H1, White Rook on H1,
White Rook on A1, White Rook on A1,
]; ];
let kingside_parameters = pos.board.castling_parameters(Wing::KingSide);
assert_eq!( assert_eq!(
pos.rook_for_castle(Color::White, Castle::KingSide), pos.castling_king(kingside_parameters.origin.king),
Some(piece!(White Rook on H1)) Some(piece!(White King))
); );
let queenside_parameters = pos.board.castling_parameters(Wing::QueenSide);
assert_eq!( assert_eq!(
pos.rook_for_castle(Color::White, Castle::QueenSide), pos.castling_king(queenside_parameters.origin.king),
Some(piece!(White Rook on A1)) Some(piece!(White King))
); );
} }
#[test] #[test]
fn danger_squares() { fn rook_for_castle() {
let pos = test_position!(Black, [ let pos = test_position![
White King on E1, White King on E1,
Black King on E7, White Rook on H1,
White Rook on E4, ];
]);
let danger_squares = pos.king_danger(Color::Black); let kingside_parameters = pos.board.castling_parameters(Wing::KingSide);
let expected = bitboard![D1 F1 D2 E2 F2 E3 A4 B4 C4 D4 F4 G4 H4 E5 E6 E7 E8]; assert_eq!(
assert_eq_bitboards!(danger_squares, expected); 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> // Eryn Wells <eryn@erynwells.me>
use crate::MakeMoveError; use crate::position::MakeMoveError;
use chessfriend_moves::BuildMoveError; use chessfriend_moves::BuildMoveError;
#[macro_export] #[macro_export]