[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);
}
}