[board, explorer, moves] Clean up the castling rights API

Reorganize castling rights API on Board into methods named according to
conventions applied to other API.

Board::has_castling_right
Board::has_castling_right_active
Board::has_castling_right_unwrapped

    These all check if a color has the right to castle on a particular side
    (wing) of the board. The first takes an Option<Color>, the latter two
    operate on bare Colors: the active color, or an explicit Color.

Board::grant_castling_right
Board::grant_castling_right_active
Board::grant_castling_right_unwrapped

    Grant castling rights to a color. Color arguments follow the pattern above.

Board::revoke_castling_right
Board::revoke_castling_right_active
Board::revoke_castling_right_unwrapped

    Revoke castling rights from a color. Color arguments follow the pattern
    above.

The latter two groups of methods take a new CastleRightsOption type that
specifies either a single Wing or All.

Rework the implementation of CastleRights to take a CastleRightsOption. Update
the unit tests and make sure everything builds.
This commit is contained in:
Eryn Wells 2025-06-18 23:44:40 +00:00
parent 933924d37a
commit 4ce7e89cdb
8 changed files with 177 additions and 124 deletions

View file

@ -1,13 +1,13 @@
// Eryn Wells <eryn@erynwells.me>
use crate::{
PieceSet, castle,
CastleRights, PieceSet,
display::DiagramFormatter,
piece_sets::{PlacePieceError, PlacePieceStrategy},
zobrist::{ZobristHash, ZobristState},
};
use chessfriend_bitboard::BitBoard;
use chessfriend_core::{Color, Piece, Shape, Square, Wing};
use chessfriend_core::{Color, Piece, Shape, Square};
use std::sync::Arc;
pub type HalfMoveClock = u32;
@ -17,7 +17,7 @@ pub type FullMoveClock = u32;
pub struct Board {
active_color: Color,
pieces: PieceSet,
castling_rights: castle::Rights,
castling_rights: CastleRights,
en_passant_target: Option<Square>,
pub half_move_clock: HalfMoveClock,
pub full_move_number: FullMoveClock,
@ -92,59 +92,27 @@ impl Board {
impl Board {
#[must_use]
pub fn castling_rights(&self) -> castle::Rights {
self.castling_rights
pub fn castling_rights(&self) -> &CastleRights {
&self.castling_rights
}
pub fn set_castling_rights(&mut self, rights: castle::Rights) {
pub(crate) fn castling_rights_mut(&mut self) -> &mut CastleRights {
&mut self.castling_rights
}
/// Replace castling rights with new rights wholesale.
pub fn set_castling_rights(&mut self, rights: CastleRights) {
if rights == self.castling_rights {
return;
}
let old_rights = self.castling_rights;
self.castling_rights = rights;
self.update_zobrist_hash_castling_rights(old_rights);
}
#[must_use]
pub fn active_color_has_castling_right(&self, wing: Wing) -> bool {
self.color_has_castling_right_unwrapped(self.active_color, wing)
}
#[must_use]
pub fn color_has_castling_right(&self, color: Option<Color>, wing: Wing) -> bool {
self.color_has_castling_right_unwrapped(self.unwrap_color(color), wing)
}
#[must_use]
pub fn color_has_castling_right_unwrapped(&self, color: Color, wing: Wing) -> bool {
self.castling_rights.color_has_right(color, wing)
}
pub fn grant_castling_right(&mut self, color: Color, wing: Wing) {
let old_rights = self.castling_rights;
self.castling_rights.grant(color, wing);
self.update_zobrist_hash_castling_rights(old_rights);
}
pub fn revoke_all_castling_rights(&mut self) {
let old_rights = self.castling_rights;
self.castling_rights.revoke_all();
self.update_zobrist_hash_castling_rights(old_rights);
}
pub fn revoke_castling_right(&mut self, color: Option<Color>, wing: Wing) {
let color = self.unwrap_color(color);
self.revoke_castling_right_unwrapped(color, wing);
}
pub fn revoke_castling_right_unwrapped(&mut self, color: Color, wing: Wing) {
let old_rights = self.castling_rights;
self.castling_rights.revoke(color, wing);
self.update_zobrist_hash_castling_rights(old_rights);
}
fn update_zobrist_hash_castling_rights(&mut self, old_rights: castle::Rights) {
pub(crate) fn update_zobrist_hash_castling_rights(&mut self, old_rights: CastleRights) {
let new_rights = self.castling_rights;
if old_rights == new_rights {
return;
@ -154,6 +122,18 @@ impl Board {
zobrist.update_modifying_castling_rights(new_rights, old_rights);
}
}
pub(crate) fn castling_king(&self, square: Square) -> Option<Piece> {
let active_color = self.active_color();
self.get_piece(square)
.filter(|piece| piece.color == active_color && piece.is_king())
}
pub(crate) fn castling_rook(&self, square: Square) -> Option<Piece> {
let active_color = self.active_color();
self.get_piece(square)
.filter(|piece| piece.color == active_color && piece.is_rook())
}
}
impl Board {

View file

@ -4,10 +4,10 @@ mod parameters;
mod rights;
pub use parameters::Parameters;
pub use rights::Rights;
pub use rights::{CastleRightsOption, Rights};
use crate::{Board, CastleParameters};
use chessfriend_core::{Color, Piece, Square, Wing};
use chessfriend_core::{Color, Wing};
use thiserror::Error;
#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
@ -46,7 +46,7 @@ impl Board {
let color = self.unwrap_color(color);
if !self.color_has_castling_right_unwrapped(color, wing) {
if !self.has_castling_right_unwrapped(color, wing.into()) {
return Err(CastleEvaluationError::NoRights { color, wing });
}
@ -76,17 +76,60 @@ impl Board {
Ok(parameters)
}
}
pub(crate) fn castling_king(&self, square: Square) -> Option<Piece> {
let active_color = self.active_color();
self.get_piece(square)
.filter(|piece| piece.color == active_color && piece.is_king())
impl Board {
#[must_use]
pub fn has_castling_right(&self, color: Option<Color>, wing: Wing) -> bool {
self.has_castling_right_unwrapped(self.unwrap_color(color), wing)
}
pub(crate) fn castling_rook(&self, square: Square) -> Option<Piece> {
let active_color = self.active_color();
self.get_piece(square)
.filter(|piece| piece.color == active_color && piece.is_rook())
#[must_use]
pub fn has_castling_right_active(&self, wing: Wing) -> bool {
self.has_castling_right_unwrapped(self.active_color(), wing)
}
#[must_use]
pub fn has_castling_right_unwrapped(&self, color: Color, wing: Wing) -> bool {
self.castling_rights().get(color, wing.into())
}
}
impl Board {
pub fn grant_castling_rights(&mut self, color: Option<Color>, rights: CastleRightsOption) {
let color = self.unwrap_color(color);
self.grant_castling_rights_unwrapped(color, rights);
}
pub fn grant_castling_rights_active(&mut self, rights: CastleRightsOption) {
self.grant_castling_rights_unwrapped(self.active_color(), rights);
}
pub fn grant_castling_rights_unwrapped(&mut self, color: Color, rights: CastleRightsOption) {
let old_rights = *self.castling_rights();
self.castling_rights_mut().grant(color, rights);
self.update_zobrist_hash_castling_rights(old_rights);
}
}
impl Board {
pub fn revoke_all_castling_rights(&mut self) {
self.castling_rights_mut().revoke_all();
}
pub fn revoke_castling_rights(&mut self, color: Option<Color>, rights: CastleRightsOption) {
let color = self.unwrap_color(color);
self.revoke_castling_rights_unwrapped(color, rights);
}
pub fn revoke_castling_rights_active(&mut self, rights: CastleRightsOption) {
self.revoke_castling_rights_unwrapped(self.active_color(), rights);
}
pub fn revoke_castling_rights_unwrapped(&mut self, color: Color, rights: CastleRightsOption) {
let old_rights = *self.castling_rights();
self.castling_rights_mut().revoke(color, rights);
self.update_zobrist_hash_castling_rights(old_rights);
}
}
@ -104,8 +147,8 @@ mod tests {
White Rook on H1
);
assert!(board.color_has_castling_right_unwrapped(Color::White, Wing::KingSide));
assert!(board.color_has_castling_right_unwrapped(Color::White, Wing::QueenSide));
assert!(board.has_castling_right_unwrapped(Color::White, Wing::KingSide));
assert!(board.has_castling_right_unwrapped(Color::White, Wing::QueenSide));
}
#[test]

View file

@ -1,6 +1,14 @@
// Eryn Wells <eryn@erynwells.me>
use chessfriend_core::{Color, Wing};
use std::fmt;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CastleRightsOption {
Wing(Wing),
All,
}
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct Rights(u8);
@ -12,16 +20,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, wing: Wing) -> bool {
(self.0 & (1 << Self::flag_offset(color, wing))) != 0
pub fn get(self, color: Color, option: CastleRightsOption) -> bool {
(self.0 & Self::flags(color, option)) != 0
}
pub fn grant(&mut self, color: Color, wing: Wing) {
self.0 |= 1 << Self::flag_offset(color, wing);
pub fn grant(&mut self, color: Color, option: CastleRightsOption) {
self.0 |= Self::flags(color, option);
}
pub fn revoke(&mut self, color: Color, wing: Wing) {
self.0 &= !(1 << Self::flag_offset(color, wing));
pub fn revoke(&mut self, color: Color, option: CastleRightsOption) {
self.0 &= !Self::flags(color, option);
}
/// Revoke castling rights for all colors and all sides of the board.
@ -31,14 +39,14 @@ impl Rights {
}
impl Rights {
pub(crate) fn as_index(&self) -> usize {
pub(crate) fn as_index(self) -> usize {
self.0 as usize
}
}
impl Rights {
fn flag_offset(color: Color, wing: Wing) -> usize {
((color as usize) << 1) + wing as usize
const fn flags(color: Color, option: CastleRightsOption) -> u8 {
option.as_flags() << (color as u8 * 2)
}
}
@ -54,36 +62,55 @@ impl Default for Rights {
}
}
impl CastleRightsOption {
#[must_use]
pub const fn as_flags(self) -> u8 {
match self {
Self::Wing(wing) => 1 << (wing as u8),
Self::All => (1 << Wing::KingSide as u8) | (1 << Wing::QueenSide as u8),
}
}
}
impl From<Wing> for CastleRightsOption {
fn from(value: Wing) -> Self {
Self::Wing(value)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bitfield_offsets() {
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);
assert_eq!(Rights::flags(Color::White, Wing::KingSide.into()), 1);
assert_eq!(Rights::flags(Color::White, Wing::QueenSide.into()), 1 << 1);
assert_eq!(Rights::flags(Color::Black, Wing::KingSide.into()), 1 << 2);
assert_eq!(Rights::flags(Color::Black, Wing::QueenSide.into()), 1 << 3);
assert_eq!(Rights::flags(Color::White, CastleRightsOption::All), 0b11);
assert_eq!(Rights::flags(Color::Black, CastleRightsOption::All), 0b1100);
}
#[test]
fn default_rights() {
let mut rights = Rights::default();
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));
assert!(rights.get(Color::White, Wing::KingSide.into()));
assert!(rights.get(Color::White, Wing::QueenSide.into()));
assert!(rights.get(Color::Black, Wing::KingSide.into()));
assert!(rights.get(Color::Black, Wing::QueenSide.into()));
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.revoke(Color::White, Wing::QueenSide.into());
assert!(rights.get(Color::White, Wing::KingSide.into()));
assert!(!rights.get(Color::White, Wing::QueenSide.into()));
assert!(rights.get(Color::Black, Wing::KingSide.into()));
assert!(rights.get(Color::Black, Wing::QueenSide.into()));
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));
rights.grant(Color::White, Wing::QueenSide.into());
assert!(rights.get(Color::White, Wing::KingSide.into()));
assert!(rights.get(Color::White, Wing::QueenSide.into()));
assert!(rights.get(Color::Black, Wing::KingSide.into()));
assert!(rights.get(Color::Black, Wing::QueenSide.into()));
}
}

View file

@ -24,12 +24,16 @@ pub enum ToFenStrError {
pub enum FromFenStrError {
#[error("missing {0} field")]
MissingField(Field),
#[error("missing piece placement")]
MissingPlacement,
#[error("invalid value")]
InvalidValue,
#[error("{0}")]
ParseIntError(#[from] std::num::ParseIntError),
#[error("{0}")]
ParseSquareError(#[from] ParseSquareError),
}
@ -122,12 +126,12 @@ impl ToFenStr for Board {
(Color::Black, Wing::KingSide),
(Color::Black, Wing::QueenSide),
]
.map(|(color, castle)| {
if !self.color_has_castling_right_unwrapped(color, castle) {
.map(|(color, wing)| {
if !self.has_castling_right_unwrapped(color, wing) {
return "";
}
match (color, castle) {
match (color, wing) {
(Color::White, Wing::KingSide) => "K",
(Color::White, Wing::QueenSide) => "Q",
(Color::Black, Wing::KingSide) => "k",
@ -226,19 +230,23 @@ impl FromFenStr for Board {
)?;
board.set_active_color(active_color);
let color_wing_from_char = |ch| match ch {
'K' => Some((Color::White, Wing::KingSide)),
'Q' => Some((Color::White, Wing::QueenSide)),
'k' => Some((Color::Black, Wing::KingSide)),
'q' => Some((Color::Black, Wing::QueenSide)),
_ => None,
};
let castling_rights = fields
.next()
.ok_or(FromFenStrError::MissingField(Field::CastlingRights))?;
board.revoke_all_castling_rights();
if castling_rights != "-" {
for ch in castling_rights.chars() {
match ch {
'K' => board.grant_castling_right(Color::White, Wing::KingSide),
'Q' => board.grant_castling_right(Color::White, Wing::QueenSide),
'k' => board.grant_castling_right(Color::Black, Wing::KingSide),
'q' => board.grant_castling_right(Color::Black, Wing::QueenSide),
_ => return Err(FromFenStrError::InvalidValue),
};
let (color, wing) =
color_wing_from_char(ch).ok_or(FromFenStrError::InvalidValue)?;
board.grant_castling_rights_unwrapped(color, wing.into());
}
}