[position, moves] Implement some castling tests

Add white castling for both wings. Found some bugs in how king sight is computed
while writing these.

In order for the king to perform the castle, the Movement bitboard needs to return
the two squares the king can castle to. That means anytime movement is calculated
for the king, the (relatively expensive) castling evaluation needs to happen.

Write two new helper static functions to create a Move for castling and promotion
moves. Since structs cannot have any functions with the same name, the two methods
that return properties related to those moves (Move::castle and Move::promotion)
need to be renamed. They're now called Move::castle_wing and Move::promotion_shape.
This commit is contained in:
Eryn Wells 2025-05-21 08:25:49 -07:00
parent 7c9c5484ba
commit feaa81bbd8
5 changed files with 99 additions and 27 deletions

View file

@ -13,45 +13,52 @@ use std::fmt;
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct Move(pub(crate) u16);
fn origin_bits(square: Square) -> u16 {
(square as u16) << 4
}
fn target_bits(square: Square) -> u16 {
(square as u16) << 10
}
impl Move {
#[must_use]
pub fn quiet(origin: Square, target: Square) -> Self {
let origin_bits = (origin as u16) << 4;
let target_bits = (target as u16) << 10;
Move(origin_bits | target_bits)
Move(origin_bits(origin) | target_bits(target))
}
#[must_use]
pub fn double_push(origin: Square, target: Square) -> Self {
let origin_bits = (origin as u16) << 4;
let target_bits = (target as u16) << 10;
let flag_bits = Kind::DoublePush as u16;
Move(origin_bits | target_bits | flag_bits)
Move(origin_bits(origin) | target_bits(target) | flag_bits)
}
#[must_use]
pub fn capture(origin: Square, target: Square) -> Self {
let origin_bits = (origin as u16) << 4;
let target_bits = (target as u16) << 10;
let flag_bits = Kind::Capture as u16;
Move(origin_bits | target_bits | flag_bits)
Move(origin_bits(origin) | target_bits(target) | flag_bits)
}
#[must_use]
pub fn en_passant_capture(origin: Square, target: Square) -> Self {
let origin_bits = (origin as u16) << 4;
let target_bits = (target as u16) << 10;
let flag_bits = Kind::EnPassantCapture as u16;
Move(origin_bits | target_bits | flag_bits)
Move(origin_bits(origin) | target_bits(target) | flag_bits)
}
#[must_use]
pub fn promotion(origin: Square, target: Square, shape: PromotionShape) -> Self {
let origin_bits = (origin as u16) << 4;
let target_bits = (target as u16) << 10;
let flag_bits = Kind::Promotion as u16;
let shape_bits = shape as u16;
Move(origin_bits | target_bits | flag_bits | shape_bits)
Move(origin_bits(origin) | target_bits(target) | flag_bits | shape_bits)
}
#[must_use]
pub fn castle(origin: Square, target: Square, wing: Wing) -> Self {
let flag_bits = match wing {
Wing::KingSide => Kind::KingSideCastle,
Wing::QueenSide => Kind::QueenSideCastle,
} as u16;
Move(origin_bits(origin) | target_bits(target) | flag_bits)
}
}
@ -102,7 +109,7 @@ impl Move {
}
#[must_use]
pub fn castle(&self) -> Option<Wing> {
pub fn castle_wing(&self) -> Option<Wing> {
match self.flags() {
0b0010 => Some(Wing::KingSide),
0b0011 => Some(Wing::QueenSide),
@ -168,7 +175,7 @@ const QUEENSIDE_CASTLE_STR: &str = "0-0-0";
impl fmt::Display for Move {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(castle) = self.castle() {
if let Some(castle) = self.castle_wing() {
return match castle {
Wing::KingSide => write!(f, "{KINGSIDE_CASTLE_STR}"),
Wing::QueenSide => write!(f, "{QUEENSIDE_CASTLE_STR}"),

View file

@ -78,7 +78,7 @@ fn move_flags_promotion() -> TestResult {
.build()?;
assert!(ply.is_promotion());
assert_eq!(ply.promotion(), Some(Shape::Queen));
assert_eq!(ply.promotion_shape(), Some(Shape::Queen));
Ok(())
}
@ -93,7 +93,7 @@ fn move_flags_capture_promotion() -> TestResult {
assert!(ply.is_capture());
assert!(ply.is_promotion());
assert_eq!(ply.promotion(), Some(Shape::Queen));
assert_eq!(ply.promotion_shape(), Some(Shape::Queen));
Ok(())
}

View file

@ -4,17 +4,17 @@
//! of squares a piece can move to. For all pieces except pawns, the Movement
//! set is equal to the Sight set.
use crate::sight::Sight;
use crate::{sight::Sight, Position};
use chessfriend_bitboard::BitBoard;
use chessfriend_board::Board;
use chessfriend_core::{Color, Piece, Rank, Shape, Square};
use chessfriend_core::{Color, Piece, Rank, Shape, Square, Wing};
pub trait Movement {
fn movement(&self, square: Square, board: &Board) -> BitBoard;
fn movement(&self, square: Square, position: &Position) -> BitBoard;
}
impl Movement for Piece {
fn movement(&self, square: Square, board: &Board) -> BitBoard {
fn movement(&self, square: Square, position: &Position) -> BitBoard {
let board = &position.board;
let opposing_occupancy = board.opposing_occupancy(self.color);
match self.shape {
@ -25,6 +25,25 @@ impl Movement for Piece {
let pushes = pawn_pushes(square.into(), self.color, board.occupancy());
sight | pushes
}
Shape::King => {
let kingside_target_square =
if position.active_color_can_castle(Wing::KingSide).is_ok() {
let parameters = board.castling_parameters(Wing::KingSide);
parameters.target.king.into()
} else {
BitBoard::empty()
};
let queenside_target_square =
if position.active_color_can_castle(Wing::QueenSide).is_ok() {
let parameters = board.castling_parameters(Wing::QueenSide);
parameters.target.king.into()
} else {
BitBoard::empty()
};
self.sight(square, board) | kingside_target_square | queenside_target_square
}
_ => self.sight(square, board),
}
}

View file

@ -81,7 +81,7 @@ impl Position {
return self.make_capture_move(ply);
}
if let Some(wing) = ply.castle() {
if let Some(wing) = ply.castle_wing() {
return self.make_castle_move(wing);
}
@ -265,7 +265,7 @@ impl Position {
// 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);
if !movement.contains(target_square) {
return Err(MakeMoveError::NoMove {
piece: active_piece,
@ -408,4 +408,50 @@ mod tests {
Ok(())
}
#[test]
fn make_white_kingside_castle() -> MakeMoveResult {
let mut pos = test_position![
White Rook on H1,
White King on E1,
];
let ply = Move::castle(Square::E1, Square::G1, Wing::KingSide);
pos.make_move(ply, ValidateMove::Yes)?;
assert_eq!(pos.board.active_color, Color::Black);
assert_eq!(pos.get_piece(Square::E1), None);
assert_eq!(pos.get_piece(Square::H1), None);
assert_eq!(pos.get_piece(Square::G1), Some(piece!(White King)));
assert_eq!(pos.get_piece(Square::F1), Some(piece!(White Rook)));
assert!(!pos
.board
.castling_rights
.color_has_right(Color::White, Wing::KingSide));
Ok(())
}
#[test]
fn make_white_queenside_castle() -> MakeMoveResult {
let mut pos = test_position![
White King on E1,
White Rook on A1,
];
let ply = Move::castle(Square::E1, Square::C1, Wing::QueenSide);
pos.make_move(ply, ValidateMove::Yes)?;
assert_eq!(pos.board.active_color, Color::Black);
assert_eq!(pos.get_piece(Square::E1), None);
assert_eq!(pos.get_piece(Square::A1), None);
assert_eq!(pos.get_piece(Square::C1), Some(piece!(White King)));
assert_eq!(pos.get_piece(Square::D1), Some(piece!(White Rook)));
assert!(!pos
.board
.castling_rights
.color_has_right(Color::White, Wing::QueenSide));
Ok(())
}
}

View file

@ -73,7 +73,7 @@ impl Position {
pub fn movement(&self, square: Square) -> BitBoard {
if let Some(piece) = self.get_piece(square) {
piece.movement(square, &self.board)
piece.movement(square, self)
} else {
BitBoard::empty()
}