From 1d7dada9878cd298e7a283a3a79c4c6bb0cc0971 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 29 Jan 2024 14:44:48 -0800 Subject: [PATCH] [bitboard, core, position] Implement proper castle move generation Add a field to MoveSet called special that flags special moves that should be generated in the iter() method. This field is a u8. It only tracks castles in the first and second bits (kingside and queenside, respectively). The move iterator chains two maps over Option<()> that produce the kingside and queenside castle moves. With that done, finish the implementation of Position::player_can_castle by adding checks for whether the squares between the rook and king are clear, and that the king would not pass through a check. This is done with BitBoards! Finally, implement some logic in PositionBuilder that updates the position's castling flags based on the positions of king and rooks. Supporting changes: - Add Color:ALL and iterate on that slice - Add Castle::ALL and iterator on that slice - Add a CastlingParameters struct that contains BitBoard properties that describe squares that should be clear of pieces and squares that should not be attacked. --- core/src/colors.rs | 6 +- position/src/move.rs | 31 ++++ position/src/move_generator/king.rs | 136 ++++++++++++------ position/src/move_generator/move_set.rs | 51 ++++++- .../src/position/builders/position_builder.rs | 26 +++- position/src/position/position.rs | 44 ++++-- 6 files changed, 234 insertions(+), 60 deletions(-) diff --git a/core/src/colors.rs b/core/src/colors.rs index 4e94fed..55757c1 100644 --- a/core/src/colors.rs +++ b/core/src/colors.rs @@ -10,8 +10,10 @@ pub enum Color { } impl Color { - pub fn iter() -> impl Iterator { - [Color::White, Color::Black].into_iter() + pub const ALL: [Color; 2] = [Color::White, Color::Black]; + + pub fn iter() -> impl Iterator { + Color::ALL.iter() } pub fn other(&self) -> Color { diff --git a/position/src/move.rs b/position/src/move.rs index 0469e82..7bd7052 100644 --- a/position/src/move.rs +++ b/position/src/move.rs @@ -16,6 +16,7 @@ pub enum MakeMoveError { } mod castle { + use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Square}; #[repr(u16)] @@ -25,12 +26,19 @@ mod castle { QueenSide = 0b11, } + pub(crate) struct CastlingParameters { + clear_squares: BitBoard, + check_squares: BitBoard, + } + pub(crate) struct Squares { pub king: Square, pub rook: Square, } impl Castle { + pub const ALL: [Castle; 2] = [Castle::KingSide, Castle::QueenSide]; + const STARTING_SQUARES: [[Squares; 2]; 2] = [ [ Squares { @@ -91,6 +99,29 @@ mod castle { Castle::QueenSide => 1, } } + + pub(crate) fn parameters(&self) -> CastlingParameters { + match self { + Castle::KingSide => CastlingParameters { + clear_squares: BitBoard::new(0b01100000), + check_squares: BitBoard::new(0b01110000), + }, + Castle::QueenSide => CastlingParameters { + clear_squares: BitBoard::new(0b00001110), + check_squares: BitBoard::new(0b00011100), + }, + } + } + } + + impl CastlingParameters { + pub fn clear_squares(&self) -> &BitBoard { + &self.clear_squares + } + + pub fn check_squares(&self) -> &BitBoard { + &self.check_squares + } } } diff --git a/position/src/move_generator/king.rs b/position/src/move_generator/king.rs index bd65151..307cbc4 100644 --- a/position/src/move_generator/king.rs +++ b/position/src/move_generator/king.rs @@ -12,28 +12,6 @@ move_generator_declaration!(KingMoveGenerator, struct); move_generator_declaration!(KingMoveGenerator, new); move_generator_declaration!(KingMoveGenerator, getters); -impl KingMoveGenerator { - #[allow(unused_variables)] - fn king_side_castle(position: &Position, color: Color) -> Option { - if !position.player_has_right_to_castle(color, Castle::KingSide) { - return None; - } - - // TODO: Implement king side castle. - None - } - - #[allow(unused_variables)] - fn queen_side_castle(position: &Position, color: Color) -> Option { - if !position.player_has_right_to_castle(color, Castle::QueenSide) { - return None; - } - - // TODO: Implement queen side castle. - None - } -} - impl MoveGeneratorInternal for KingMoveGenerator { fn piece(color: Color) -> Piece { Piece::king(color) @@ -44,7 +22,7 @@ impl MoveGeneratorInternal for KingMoveGenerator { let color = piece.color(); let square = placed_piece.square(); - let safe_squares = !position.king_danger(); + let safe_squares = !position.king_danger(color); let all_king_moves = BitBoard::king_moves(square); let empty_squares = position.empty_squares(); @@ -56,30 +34,25 @@ impl MoveGeneratorInternal for KingMoveGenerator { let quiet_moves = all_king_moves & safe_empty_squares; let capture_moves = all_king_moves & opposing_pieces_on_safe_squares; - // TODO: Handle checks. Prevent moving a king to a square attacked by a - // piece of the opposite color. - - let map_to_move = |sq| MoveBuilder::new(*piece, square, sq).build(); - - let king_side_castle = Self::king_side_castle(position, color); - let queen_side_castle = Self::queen_side_castle(position, color); - let quiet_moves = quiet_moves_bb - .occupied_squares() - .map(map_to_move) - .chain(king_side_castle.iter().cloned()) - .chain(queen_side_castle.iter().cloned()); - let capture_moves = capture_moves_bb.occupied_squares().map(map_to_move); - - MoveSet::new(placed_piece) + let mut move_set = MoveSet::new(placed_piece) .quiet_moves(quiet_moves) - .capture_moves(capture_moves) + .capture_moves(capture_moves); + + if position.player_can_castle(color, Castle::KingSide) { + move_set.kingside_castle(); + } + if position.player_can_castle(color, Castle::QueenSide) { + move_set.queenside_castle(); + } + + move_set } } #[cfg(test)] mod tests { use super::*; - use crate::{assert_move_list, position, r#move::AlgebraicMoveFormatter, PositionBuilder}; + use crate::{assert_move_list, position, test_position, PositionBuilder}; use chessfriend_bitboard::bitboard; use chessfriend_core::{piece, Square}; use std::collections::HashSet; @@ -166,4 +139,87 @@ mod tests { assert_eq!(generated_moves, expected_moves); } + + #[test] + fn white_king_unobstructed_castles() { + let pos = test_position!( + White King on E1, + White Rook on A1, + White Rook on H1, + ); + + assert!(pos.player_can_castle(Color::White, Castle::KingSide)); + assert!(pos.player_can_castle(Color::White, Castle::QueenSide)); + + let generator = KingMoveGenerator::new(&pos, Color::White); + let generated_moves: HashSet = generator.iter().collect(); + + let king = piece!(White King); + assert!(generated_moves.contains( + &MoveBuilder::new(king, Square::E1, Square::G1) + .castle(Castle::KingSide) + .build() + )); + assert!(generated_moves.contains( + &MoveBuilder::new(king, Square::E1, Square::C1) + .castle(Castle::QueenSide) + .build() + )); + } + + #[test] + fn white_king_obstructed_queenside_castle() { + let pos = test_position!( + White King on E1, + White Knight on B1, + White Rook on A1, + White Rook on H1, + ); + + assert!(pos.player_can_castle(Color::White, Castle::KingSide)); + assert!(!pos.player_can_castle(Color::White, Castle::QueenSide)); + + let generator = KingMoveGenerator::new(&pos, Color::White); + let generated_moves: HashSet = generator.iter().collect(); + + let king = piece!(White King); + assert!(generated_moves.contains( + &MoveBuilder::new(king, Square::E1, Square::G1) + .castle(Castle::KingSide) + .build() + )); + assert!(!generated_moves.contains( + &MoveBuilder::new(king, Square::E1, Square::C1) + .castle(Castle::QueenSide) + .build() + )); + } + + #[test] + fn white_king_obstructed_kingside_castle() { + let pos = test_position!( + White King on E1, + White Rook on A1, + White Knight on G1, + White Rook on H1, + ); + + assert!(!pos.player_can_castle(Color::White, Castle::KingSide)); + assert!(pos.player_can_castle(Color::White, Castle::QueenSide)); + + let generator = KingMoveGenerator::new(&pos, Color::White); + let generated_moves: HashSet = generator.iter().collect(); + + let king = piece!(White King); + assert!(!generated_moves.contains( + &MoveBuilder::new(king, Square::E1, Square::G1) + .castle(Castle::KingSide) + .build() + )); + assert!(generated_moves.contains( + &MoveBuilder::new(king, Square::E1, Square::C1) + .castle(Castle::QueenSide) + .build() + )); + } } diff --git a/position/src/move_generator/move_set.rs b/position/src/move_generator/move_set.rs index fd62e85..cf405f1 100644 --- a/position/src/move_generator/move_set.rs +++ b/position/src/move_generator/move_set.rs @@ -1,6 +1,6 @@ // Eryn Wells -use crate::{Move, MoveBuilder}; +use crate::{r#move::Castle, Move, MoveBuilder}; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece, PlacedPiece, Shape}; @@ -27,7 +27,7 @@ impl MoveListSet { pub(crate) struct MoveSet { piece: PlacedPiece, bitboards: BitBoardSet, - move_lists: MoveListSet, + special: u8, } impl MoveSet { @@ -35,10 +35,7 @@ impl MoveSet { MoveSet { piece, bitboards: BitBoardSet::default(), - move_lists: MoveListSet { - quiet: Vec::new(), - captures: Vec::new(), - }, + special: 0, } } @@ -52,6 +49,16 @@ impl MoveSet { self } + pub(super) fn kingside_castle(&mut self) -> &mut MoveSet { + self.special |= 0b1; + self + } + + pub(super) fn queenside_castle(&mut self) -> &mut MoveSet { + self.special |= 0b10; + self + } + /// Return a BitBoard representing all possible moves. pub(super) fn bitboard(&self) -> BitBoard { self.bitboards.captures | self.bitboards.quiet @@ -78,5 +85,37 @@ impl MoveSet { .build() }), ) + .chain( + if (self.special & 0b1) != 0 { + Some(()) + } else { + None + } + .map(|()| { + MoveBuilder::new( + *piece, + from_square, + Castle::KingSide.target_squares(piece.color()).king, + ) + .castle(Castle::KingSide) + .build() + }), + ) + .chain( + if (self.special & 0b10) != 0 { + Some(()) + } else { + None + } + .map(|()| { + MoveBuilder::new( + *piece, + from_square, + Castle::QueenSide.target_squares(piece.color()).king, + ) + .castle(Castle::QueenSide) + .build() + }), + ) } } diff --git a/position/src/position/builders/position_builder.rs b/position/src/position/builders/position_builder.rs index 369014c..5b0a4e0 100644 --- a/position/src/position/builders/position_builder.rs +++ b/position/src/position/builders/position_builder.rs @@ -61,9 +61,11 @@ impl Builder { pub fn place_piece(&mut self, piece: PlacedPiece) -> &mut Self { let square = piece.square(); + let shape = piece.shape(); - if piece.shape() == Shape::King { - let color_index: usize = piece.color() as usize; + if shape == Shape::King { + let color = piece.color(); + let color_index: usize = color as usize; self.pieces.remove(&self.kings[color_index]); self.kings[color_index] = square; } @@ -81,9 +83,27 @@ impl Builder { .filter(Self::is_piece_placement_valid), ); + let mut flags = self.flags; + + for color in Color::ALL { + for castle in Castle::ALL { + let starting_squares = castle.starting_squares(color); + let has_rook_on_starting_square = self + .pieces + .get(&starting_squares.rook) + .is_some_and(|piece| piece.shape() == Shape::Rook); + let king_is_on_starting_square = + self.kings[color as usize] == starting_squares.king; + + if !king_is_on_starting_square || !has_rook_on_starting_square { + flags.clear_player_has_right_to_castle_flag(color, castle); + } + } + } + Position::new( self.player_to_move, - self.flags, + flags, pieces, None, self.ply_counter, diff --git a/position/src/position/position.rs b/position/src/position/position.rs index 6a09226..3ee09f1 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -107,7 +107,17 @@ impl Position { return false; } - // TODO: Perform a real check that the player can castle. + let castling_parameters = castle.parameters(); + + let all_pieces = self.occupied_squares(); + if !(all_pieces & castling_parameters.clear_squares()).is_empty() { + return false; + } + + let danger_squares = self.king_danger(player); + if !(danger_squares & castling_parameters.check_squares()).is_empty() { + return false; + } true } @@ -159,7 +169,7 @@ impl Position { pub fn piece_on_square(&self, sq: Square) -> Option { for color in Color::iter() { for shape in Shape::iter() { - let piece = Piece::new(color, *shape); + let piece = Piece::new(*color, *shape); if self.pieces.bitboard_for_piece(&piece).is_set(sq) { return Some(PlacedPiece::new(piece, sq)); } @@ -212,19 +222,16 @@ impl Position { /// A bitboard representing the squares where a king of the given color will /// be in danger. The king cannot move to these squares. - pub(crate) fn king_danger(&self) -> BitBoard { + pub(crate) fn king_danger(&self, color: Color) -> BitBoard { let pieces_without_king = { let mut cloned_pieces = self.pieces.clone(); - let placed_king = PlacedPiece::new( - Piece::king(self.color_to_move), - self.king_square(self.color_to_move), - ); + let placed_king = PlacedPiece::new(Piece::king(color), self.king_square(color)); cloned_pieces.remove_piece(&placed_king); cloned_pieces }; - self._sight_of_player(self.color_to_move.other(), &pieces_without_king) + self._sight_of_player(color.other(), &pieces_without_king) } pub(crate) fn is_king_in_check(&self) -> bool { @@ -363,6 +370,25 @@ mod tests { assert!(!pos.is_king_in_check()); } + #[test] + fn king_not_on_starting_square_cannot_castle() { + let pos = test_position!(White King on E4); + assert!(!pos.player_can_castle(Color::White, Castle::KingSide)); + assert!(!pos.player_can_castle(Color::White, Castle::QueenSide)); + } + + #[test] + fn king_on_starting_square_can_castle() { + let pos = test_position!( + White King on E1, + White Rook on A1, + White Rook on H1 + ); + + assert!(pos.player_can_castle(Color::White, Castle::KingSide)); + assert!(pos.player_can_castle(Color::White, Castle::QueenSide)); + } + #[test] fn rook_for_castle() { let pos = position![ @@ -390,7 +416,7 @@ mod tests { .to_move(Color::Black) .build(); - let danger_squares = pos.king_danger(); + let danger_squares = pos.king_danger(Color::Black); let expected = bitboard![D1, F1, D2, E2, F2, E3, A4, B4, C4, D4, F4, G4, H4, E5, E6, E7, E8]; assert_eq!(