[board, moves, position] Implement KingMoveGenerator

Implement a move generator that emits moves for the king(s) of a particular color.
There will, of course, only ever be one king per side in any valid board, but
this iterator can (in theory) handle multiple kings on the board. This iterator
is almost entirely copypasta of the SliderMoveGenerator. The major difference is
castling.

Castle moves are emitted by a helper CastleIterator type. This struct collects
information about whether the given color can castle on each side of the board
and then emits moves for each side, if indicated.

Do some light refactoring of the castle-related methods on Board to accommodate
this move generator. Remove the dependency on internal state and rename the
"can_castle" method to color_can_castle.

In order to facilitate creating castling moves without relying on Board, remove
the origin and target squares from the encoded castling move. Code that makes
a castling move already looks up castling parameters to move the king and rook to
the right squares, so encoding those squares was redundant. This change
necessitated some updates to position.

Lastly, bring in a handful of unit tests courtesy of Claude. Apparently, it's my
new best coding friend. 🙃
This commit is contained in:
Eryn Wells 2025-05-26 23:37:33 -07:00
parent f005d94fc2
commit eb6f2000a9
9 changed files with 427 additions and 53 deletions

View file

@ -7,7 +7,7 @@ use crate::{
PieceSet,
};
use chessfriend_bitboard::BitBoard;
use chessfriend_core::{Color, Piece, Shape, Square, Wing};
use chessfriend_core::{Color, Piece, Shape, Square};
pub type HalfMoveClock = u32;
pub type FullMoveClock = u32;
@ -131,12 +131,9 @@ impl Board {
pub fn queens(&self, color: Color) -> BitBoard {
self.find_pieces(Piece::queen(color))
}
}
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]
pub fn kings(&self, color: Color) -> BitBoard {
self.find_pieces(Piece::king(color))
}
}

View file

@ -6,7 +6,7 @@ mod rights;
pub use parameters::Parameters;
pub use rights::Rights;
use crate::Board;
use crate::{Board, CastleParameters};
use chessfriend_core::{Color, Piece, Square, Wing};
use thiserror::Error;
@ -25,26 +25,32 @@ pub enum CastleEvaluationError {
}
impl Board {
#[must_use]
pub fn castling_parameters(wing: Wing, color: Color) -> &'static CastleParameters {
&CastleParameters::BY_COLOR[color as usize][wing as usize]
}
/// 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> {
pub fn color_can_castle(
&self,
wing: Wing,
color: Option<Color>,
) -> Result<&'static CastleParameters, CastleEvaluationError> {
// TODO: Cache this result. It's expensive!
// TODO: Does this actually need to rely on internal state, i.e. active_color?
let active_color = self.active_color;
let color = self.unwrap_color(color);
if !self.castling_rights.color_has_right(active_color, wing) {
return Err(CastleEvaluationError::NoRights {
color: active_color,
wing,
});
if !self.castling_rights.color_has_right(color, wing) {
return Err(CastleEvaluationError::NoRights { color, wing });
}
let parameters = self.castling_parameters(wing);
let parameters = Self::castling_parameters(wing, color);
if self.castling_king(parameters.origin.king).is_none() {
return Err(CastleEvaluationError::NoKing);
@ -61,14 +67,14 @@ impl Board {
}
// King cannot pass through check.
let opposing_sight = self.opposing_sight(active_color);
let opposing_sight = self.opposing_sight(color);
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(())
Ok(parameters)
}
pub(crate) fn castling_king(&self, square: Square) -> Option<Piece> {
@ -111,13 +117,13 @@ mod tests {
White Rook on A1,
];
let kingside_parameters = pos.castling_parameters(Wing::KingSide);
let kingside_parameters = Board::castling_parameters(Wing::KingSide, Color::White);
assert_eq!(
pos.castling_king(kingside_parameters.origin.king),
Some(piece!(White King))
);
let queenside_parameters = pos.castling_parameters(Wing::QueenSide);
let queenside_parameters = Board::castling_parameters(Wing::QueenSide, Color::White);
assert_eq!(
pos.castling_king(queenside_parameters.origin.king),
Some(piece!(White King))
@ -131,7 +137,7 @@ mod tests {
White Rook on H1,
];
let kingside_parameters = pos.castling_parameters(Wing::KingSide);
let kingside_parameters = Board::castling_parameters(Wing::KingSide, Color::White);
assert_eq!(
pos.castling_rook(kingside_parameters.origin.rook),
Some(piece!(White Rook))
@ -142,7 +148,7 @@ mod tests {
White Rook on A1,
];
let queenside_parameters = pos.castling_parameters(Wing::QueenSide);
let queenside_parameters = Board::castling_parameters(Wing::QueenSide, Color::White);
assert_eq!(
pos.castling_rook(queenside_parameters.origin.rook),
Some(piece!(White Rook))
@ -150,15 +156,17 @@ mod tests {
}
#[test]
fn white_can_castle() {
fn white_can_castle() -> Result<(), CastleEvaluationError> {
let pos = test_board![
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(()));
pos.color_can_castle(Wing::KingSide, None)?;
pos.color_can_castle(Wing::QueenSide, None)?;
Ok(())
}
#[test]
@ -170,11 +178,11 @@ mod tests {
];
assert_eq!(
pos.active_color_can_castle(Wing::KingSide),
pos.color_can_castle(Wing::KingSide, None),
Err(CastleEvaluationError::NoKing)
);
assert_eq!(
pos.active_color_can_castle(Wing::QueenSide),
pos.color_can_castle(Wing::QueenSide, None),
Err(CastleEvaluationError::NoKing)
);
}
@ -187,7 +195,7 @@ mod tests {
];
assert_eq!(
pos.active_color_can_castle(Wing::KingSide),
pos.color_can_castle(Wing::KingSide, None),
Err(CastleEvaluationError::NoRook)
);
@ -197,7 +205,7 @@ mod tests {
];
assert_eq!(
pos.active_color_can_castle(Wing::QueenSide),
pos.color_can_castle(Wing::QueenSide, None),
Err(CastleEvaluationError::NoRook)
);
}
@ -212,10 +220,10 @@ mod tests {
];
assert_eq!(
pos.active_color_can_castle(Wing::KingSide),
pos.color_can_castle(Wing::KingSide, None),
Err(CastleEvaluationError::ObstructingPieces)
);
assert_eq!(pos.active_color_can_castle(Wing::QueenSide), Ok(()));
assert!(pos.color_can_castle(Wing::QueenSide, None).is_ok());
}
#[test]
@ -227,9 +235,9 @@ mod tests {
Black Queen on C6,
];
assert_eq!(pos.active_color_can_castle(Wing::KingSide), Ok(()));
assert!(pos.color_can_castle(Wing::KingSide, None).is_ok());
assert_eq!(
pos.active_color_can_castle(Wing::QueenSide),
pos.color_can_castle(Wing::QueenSide, None),
Err(CastleEvaluationError::CheckingPieces)
);
}

View file

@ -1,7 +1,7 @@
use chessfriend_bitboard::BitBoard;
use chessfriend_core::{Color, Square, Wing};
#[derive(Debug)]
#[derive(Debug, Eq, PartialEq)]
pub struct Parameters {
/// Origin squares of the king and rook.
pub origin: Squares,
@ -18,7 +18,7 @@ pub struct Parameters {
pub check: BitBoard,
}
#[derive(Debug)]
#[derive(Debug, Eq, PartialEq)]
pub struct Squares {
pub king: Square,
pub rook: Square,

View file

@ -24,7 +24,8 @@ pub trait Movement {
impl Movement for Piece {
fn movement(&self, square: Square, board: &Board) -> BitBoard {
let opposing_occupancy = board.opposing_occupancy(self.color);
let color = self.color;
let opposing_occupancy = board.opposing_occupancy(color);
match self.shape {
Shape::Pawn => {
@ -36,20 +37,22 @@ impl Movement for Piece {
}
Shape::King => {
let kingside_target_square =
if board.active_color_can_castle(Wing::KingSide).is_ok() {
let parameters = board.castling_parameters(Wing::KingSide);
if board.color_can_castle(Wing::KingSide, Some(color)).is_ok() {
let parameters = Board::castling_parameters(Wing::KingSide, color);
parameters.target.king.into()
} else {
BitBoard::empty()
};
let queenside_target_square =
if board.active_color_can_castle(Wing::QueenSide).is_ok() {
let parameters = board.castling_parameters(Wing::QueenSide);
parameters.target.king.into()
} else {
BitBoard::empty()
};
let queenside_target_square = if board
.color_can_castle(Wing::QueenSide, Some(self.color))
.is_ok()
{
let parameters = Board::castling_parameters(Wing::QueenSide, color);
parameters.target.king.into()
} else {
BitBoard::empty()
};
self.sight(square, board) | kingside_target_square | queenside_target_square
}

View file

@ -114,6 +114,7 @@ impl Board {
sight_method!(bishop_sight);
sight_method!(rook_sight);
sight_method!(queen_sight);
sight_method!(king_sight);
}
struct SightInfo {