From d910ff708e17c91302391c3ff2b78c295a24fd85 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 15:43:47 -0800 Subject: [PATCH 1/8] Remove the move list argument from MoveList::quiet_moves and capture_moves Produce an iterator of Moves in MoveList::moves --- position/src/move_generator/move_set.rs | 53 +++++++++++++------------ 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/position/src/move_generator/move_set.rs b/position/src/move_generator/move_set.rs index bfd9f75..fd62e85 100644 --- a/position/src/move_generator/move_set.rs +++ b/position/src/move_generator/move_set.rs @@ -1,10 +1,10 @@ // Eryn Wells -use crate::Move; +use crate::{Move, MoveBuilder}; use chessfriend_bitboard::BitBoard; -use chessfriend_core::PlacedPiece; +use chessfriend_core::{Color, Piece, PlacedPiece, Shape}; -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Default, Eq, PartialEq)] struct BitBoardSet { quiet: BitBoard, captures: BitBoard, @@ -34,10 +34,7 @@ impl MoveSet { pub(super) fn new(piece: PlacedPiece) -> MoveSet { MoveSet { piece, - bitboards: BitBoardSet { - quiet: BitBoard::empty(), - captures: BitBoard::empty(), - }, + bitboards: BitBoardSet::default(), move_lists: MoveListSet { quiet: Vec::new(), captures: Vec::new(), @@ -45,25 +42,13 @@ impl MoveSet { } } - pub(super) fn quiet_moves( - mut self, - bitboard: BitBoard, - move_list: impl Iterator, - ) -> MoveSet { + pub(super) fn quiet_moves(mut self, bitboard: BitBoard) -> MoveSet { self.bitboards.quiet = bitboard; - self.move_lists.quiet = move_list.collect(); - self } - pub(super) fn capture_moves( - mut self, - bitboard: BitBoard, - move_list: impl Iterator, - ) -> MoveSet { + pub(super) fn capture_moves(mut self, bitboard: BitBoard) -> MoveSet { self.bitboards.captures = bitboard; - self.move_lists.captures = move_list.collect(); - self } @@ -72,10 +57,26 @@ impl MoveSet { self.bitboards.captures | self.bitboards.quiet } - pub(super) fn moves(&self) -> impl Iterator { - self.move_lists - .captures - .iter() - .chain(self.move_lists.quiet.iter()) + pub(super) fn moves(&self) -> impl Iterator + '_ { + let piece = self.piece.piece(); + let from_square = self.piece.square(); + + self.bitboards + .quiet + .occupied_squares() + .map(move |to_square| MoveBuilder::new(*piece, from_square, to_square).build()) + .chain( + self.bitboards + .captures + .occupied_squares() + .map(move |to_square| { + MoveBuilder::new(*piece, from_square, to_square) + .capturing(PlacedPiece::new( + Piece::new(Color::White, Shape::Pawn), + to_square, + )) + .build() + }), + ) } } From cd3cb82192568ae3b83747e7398bbc8c1a4b78e8 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 15:47:32 -0800 Subject: [PATCH 2/8] Add an assert_move_list! macro to help with verifying move lists --- position/src/lib.rs | 4 ++++ position/src/tests.rs | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 position/src/tests.rs diff --git a/position/src/lib.rs b/position/src/lib.rs index 015b10b..9c58540 100644 --- a/position/src/lib.rs +++ b/position/src/lib.rs @@ -11,5 +11,9 @@ mod sight; #[macro_use] mod macros; +#[cfg(test)] +#[macro_use] +mod tests; + pub use position::{MoveBuilder as MakeMoveBuilder, Position, PositionBuilder}; pub use r#move::{Castle, MakeMoveError, Move, MoveBuilder}; diff --git a/position/src/tests.rs b/position/src/tests.rs new file mode 100644 index 0000000..8663afa --- /dev/null +++ b/position/src/tests.rs @@ -0,0 +1,19 @@ +// Eryn Wells + +#[macro_export] +macro_rules! assert_move_list { + ($generated:expr, $expected:expr, $position:expr) => { + assert_eq!( + $generated, + $expected, + "Difference: {:?}", + $generated + .symmetric_difference(&$expected) + .map(|mv| format!( + "{}", + $crate::r#move::AlgebraicMoveFormatter::new(mv, &$position) + )) + .collect::>() + ) + }; +} From 5e3ef9d21e52577e38a68fc57bee1dc7fbc0ef4c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 15:48:11 -0800 Subject: [PATCH 3/8] Remove the move lists from bishop, knight, queen, and rook move set construction These are the easy ones. --- position/src/move_generator/bishop.rs | 12 ++++-------- position/src/move_generator/knight.rs | 20 +++++--------------- position/src/move_generator/queen.rs | 14 +++++--------- position/src/move_generator/rook.rs | 12 ++++-------- 4 files changed, 18 insertions(+), 40 deletions(-) diff --git a/position/src/move_generator/bishop.rs b/position/src/move_generator/bishop.rs index 48fd833..3eb1405 100644 --- a/position/src/move_generator/bishop.rs +++ b/position/src/move_generator/bishop.rs @@ -42,16 +42,12 @@ impl MoveGeneratorInternal for ClassicalMoveGenerator { update_moves_with_ray!(SouthEast, occupied_squares); update_moves_with_ray!(SouthWest, occupied_squares); - let quiet_moves_bb = all_moves & (empty_squares | !friendly_pieces); - let capture_moves_bb = all_moves & opposing_pieces; - - let map_to_move = |sq| MoveBuilder::new(*piece, square, sq).build(); - let quiet_moves = quiet_moves_bb.occupied_squares().map(map_to_move); - let capture_moves = capture_moves_bb.occupied_squares().map(map_to_move); + let quiet_moves = all_moves & (empty_squares | !friendly_pieces); + let capture_moves = all_moves & opposing_pieces; MoveSet::new(placed_piece) - .quiet_moves(quiet_moves_bb, quiet_moves) - .capture_moves(capture_moves_bb, capture_moves) + .quiet_moves(quiet_moves) + .capture_moves(capture_moves) } } diff --git a/position/src/move_generator/knight.rs b/position/src/move_generator/knight.rs index 751217a..f2e3fcd 100644 --- a/position/src/move_generator/knight.rs +++ b/position/src/move_generator/knight.rs @@ -17,22 +17,12 @@ impl MoveGeneratorInternal for KnightMoveGenerator { let empty_squares = position.empty_squares(); let knight_moves = BitBoard::knight_moves(placed_piece.square()); - let quiet_moves_bb = knight_moves & empty_squares; - let capture_moves_bb = knight_moves & opposing_pieces; - - let quiet_moves = quiet_moves_bb.occupied_squares().map(|to_sq| { - MoveBuilder::new(*placed_piece.piece(), placed_piece.square(), to_sq).build() - }); - let capture_moves = capture_moves_bb.occupied_squares().map(|to_sq| { - let captured_piece = position.piece_on_square(to_sq).unwrap(); - MoveBuilder::new(*placed_piece.piece(), placed_piece.square(), to_sq) - .capturing(captured_piece) - .build() - }); + let quiet_moves = knight_moves & empty_squares; + let capture_moves = knight_moves & opposing_pieces; MoveSet::new(placed_piece) - .quiet_moves(quiet_moves_bb, quiet_moves) - .capture_moves(capture_moves_bb, capture_moves) + .quiet_moves(quiet_moves) + .capture_moves(capture_moves) } } @@ -72,7 +62,7 @@ mod tests { MoveBuilder::new(piece!(White Knight), Square::E4, Square::F6).build(), ]; - let mut generated_moves: HashSet = generator.iter().cloned().collect(); + let mut generated_moves: HashSet = generator.iter().collect(); for ex_move in expected_moves { assert!( diff --git a/position/src/move_generator/queen.rs b/position/src/move_generator/queen.rs index f38da4a..737686c 100644 --- a/position/src/move_generator/queen.rs +++ b/position/src/move_generator/queen.rs @@ -1,7 +1,7 @@ // Eryn Wells use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use crate::{MoveBuilder, Position}; +use crate::Position; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Direction, Piece, PlacedPiece}; @@ -48,16 +48,12 @@ impl MoveGeneratorInternal for ClassicalMoveGenerator { update_moves_with_ray!(SouthWest, occupied_squares); update_moves_with_ray!(West, occupied_squares); - let quiet_moves_bb = all_moves & (empty_squares | !friendly_pieces); - let capture_moves_bb = all_moves & opposing_pieces; - - let map_to_move = |sq| MoveBuilder::new(*piece, square, sq).build(); - let quiet_moves = quiet_moves_bb.occupied_squares().map(map_to_move); - let capture_moves = capture_moves_bb.occupied_squares().map(map_to_move); + let quiet_moves = all_moves & (empty_squares | !friendly_pieces); + let capture_moves = all_moves & opposing_pieces; MoveSet::new(placed_piece) - .quiet_moves(quiet_moves_bb, quiet_moves) - .capture_moves(capture_moves_bb, capture_moves) + .quiet_moves(quiet_moves) + .capture_moves(capture_moves) } } diff --git a/position/src/move_generator/rook.rs b/position/src/move_generator/rook.rs index 71e17e7..4d74c11 100644 --- a/position/src/move_generator/rook.rs +++ b/position/src/move_generator/rook.rs @@ -44,16 +44,12 @@ impl MoveGeneratorInternal for ClassicalMoveGenerator { update_moves_with_ray!(South, occupied_squares); update_moves_with_ray!(West, occupied_squares); - let quiet_moves_bb = all_moves & (empty_squares | !friendly_pieces); - let capture_moves_bb = all_moves & opposing_pieces; - - let map_to_move = |sq| MoveBuilder::new(*piece, square, sq).build(); - let quiet_moves = quiet_moves_bb.occupied_squares().map(map_to_move); - let capture_moves = capture_moves_bb.occupied_squares().map(map_to_move); + let quiet_moves = all_moves & (empty_squares | !friendly_pieces); + let capture_moves = all_moves & opposing_pieces; MoveSet::new(placed_piece) - .quiet_moves(quiet_moves_bb, quiet_moves) - .capture_moves(capture_moves_bb, capture_moves) + .quiet_moves(quiet_moves) + .capture_moves(capture_moves) } } From 296a57d7ac586783d8d9b052dabafeb0343f18dc Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 15:50:00 -0800 Subject: [PATCH 4/8] Remove move list arguments from king and pawn move set constuction These are harder. --- position/src/move_generator/king.rs | 66 +++++++++-------------------- position/src/move_generator/pawn.rs | 46 +++++++++----------- 2 files changed, 40 insertions(+), 72 deletions(-) diff --git a/position/src/move_generator/king.rs b/position/src/move_generator/king.rs index 1f35af5..bd65151 100644 --- a/position/src/move_generator/king.rs +++ b/position/src/move_generator/king.rs @@ -44,12 +44,17 @@ impl MoveGeneratorInternal for KingMoveGenerator { let color = piece.color(); let square = placed_piece.square(); - let empty_squares = position.empty_squares(); - let opposing_pieces = position.bitboard_for_color(color.other()); + let safe_squares = !position.king_danger(); + let all_king_moves = BitBoard::king_moves(square); - let all_moves = BitBoard::king_moves(square); - let quiet_moves_bb = all_moves & empty_squares; - let capture_moves_bb = all_moves & opposing_pieces; + let empty_squares = position.empty_squares(); + let safe_empty_squares = empty_squares & safe_squares; + + let opposing_pieces = position.bitboard_for_color(color.other()); + let opposing_pieces_on_safe_squares = opposing_pieces & safe_squares; + + 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. @@ -66,15 +71,15 @@ impl MoveGeneratorInternal for KingMoveGenerator { let capture_moves = capture_moves_bb.occupied_squares().map(map_to_move); MoveSet::new(placed_piece) - .quiet_moves(quiet_moves_bb, quiet_moves) - .capture_moves(capture_moves_bb, capture_moves) + .quiet_moves(quiet_moves) + .capture_moves(capture_moves) } } #[cfg(test)] mod tests { use super::*; - use crate::{position, r#move::AlgebraicMoveFormatter, PositionBuilder}; + use crate::{assert_move_list, position, r#move::AlgebraicMoveFormatter, PositionBuilder}; use chessfriend_bitboard::bitboard; use chessfriend_core::{piece, Square}; use std::collections::HashSet; @@ -90,7 +95,7 @@ mod tests { bitboard![E5, F5, F4, F3, E3, D3, D4, D5] ); - let expected_moves = [ + let expected_moves: HashSet = HashSet::from_iter([ MoveBuilder::new(piece!(White King), Square::E4, Square::D5).build(), MoveBuilder::new(piece!(White King), Square::E4, Square::E5).build(), MoveBuilder::new(piece!(White King), Square::E4, Square::F5).build(), @@ -99,23 +104,11 @@ mod tests { MoveBuilder::new(piece!(White King), Square::E4, Square::E3).build(), MoveBuilder::new(piece!(White King), Square::E4, Square::D3).build(), MoveBuilder::new(piece!(White King), Square::E4, Square::D4).build(), - ]; + ]); - let mut generated_moves: HashSet = generator.iter().cloned().collect(); + let generated_moves: HashSet = generator.iter().collect(); - for ex_move in expected_moves { - assert!( - generated_moves.remove(&ex_move), - "{:#?} was not generated", - &ex_move - ); - } - - assert!( - generated_moves.is_empty(), - "Moves unexpectedly present: {:#?}", - generated_moves - ); + assert_move_list!(generated_moves, expected_moves, pos); } #[test] @@ -138,7 +131,7 @@ mod tests { MoveBuilder::new(piece!(White King), Square::A1, Square::B2).build(), ]; - let mut generated_moves: HashSet = generator.iter().cloned().collect(); + let mut generated_moves: HashSet = generator.iter().collect(); for ex_move in expected_moves { assert!( @@ -167,27 +160,10 @@ mod tests { assert!(pos.is_king_in_check()); let generator = KingMoveGenerator::new(&pos, Color::Black); - let generated_moves: HashSet = generator.iter().cloned().collect(); + let generated_moves = generator.bitboard(); - let king = piece!(Black King); - let from_square = Square::E7; - let expected_moves = HashSet::from_iter([ - MoveBuilder::new(king, from_square, Square::D6).build(), - MoveBuilder::new(king, from_square, Square::D7).build(), - MoveBuilder::new(king, from_square, Square::D8).build(), - MoveBuilder::new(king, from_square, Square::F6).build(), - MoveBuilder::new(king, from_square, Square::F7).build(), - MoveBuilder::new(king, from_square, Square::F8).build(), - ]); + let expected_moves = bitboard![F8, F7, F6, D6, D7, D8]; - assert_eq!( - generated_moves, - expected_moves, - "Difference: {:?}", - generated_moves - .symmetric_difference(&expected_moves) - .map(|mv| format!("{}", AlgebraicMoveFormatter::new(mv, &pos))) - .collect::>() - ); + assert_eq!(generated_moves, expected_moves); } } diff --git a/position/src/move_generator/pawn.rs b/position/src/move_generator/pawn.rs index 8b9b4cf..401366e 100644 --- a/position/src/move_generator/pawn.rs +++ b/position/src/move_generator/pawn.rs @@ -1,7 +1,7 @@ // Eryn Wells use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use crate::{MoveBuilder, Position}; +use crate::Position; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece, PlacedPiece, Rank, Square}; @@ -16,24 +16,12 @@ impl MoveGeneratorInternal for PawnMoveGenerator { } fn move_set_for_piece(position: &Position, placed_piece: PlacedPiece) -> MoveSet { - let from_square = placed_piece.square(); - - let captures_bitboard = Self::attacks(position, placed_piece); - let quiet_moves_bitboard = Self::pushes(position, placed_piece); - - let quiet_moves = quiet_moves_bitboard.occupied_squares().map(|to_square| { - MoveBuilder::new(*placed_piece.piece(), from_square, to_square).build() - }); - let capture_moves = captures_bitboard.occupied_squares().map(|to_square| { - let captured_piece = position.piece_on_square(to_square).unwrap(); - MoveBuilder::new(*placed_piece.piece(), from_square, to_square) - .capturing(captured_piece) - .build() - }); + let capture_moves = Self::attacks(position, placed_piece); + let quiet_moves = Self::pushes(position, placed_piece); MoveSet::new(placed_piece) - .quiet_moves(quiet_moves_bitboard, quiet_moves) - .capture_moves(captures_bitboard, capture_moves) + .quiet_moves(quiet_moves) + .capture_moves(capture_moves) } } @@ -71,7 +59,10 @@ impl PawnMoveGenerator { #[cfg(test)] mod tests { use super::*; - use crate::{position::DiagramFormatter, test_position, Move}; + use crate::{ + assert_move_list, position::DiagramFormatter, r#move::AlgebraicMoveFormatter, + test_position, Move, MoveBuilder, + }; use chessfriend_core::{piece, Square}; use std::collections::HashSet; @@ -86,7 +77,7 @@ mod tests { MoveBuilder::new(piece!(White Pawn), Square::E2, Square::E4).build(), ]); - let generated_moves: HashSet = generator.iter().cloned().collect(); + let generated_moves: HashSet = generator.iter().collect(); assert_eq!(generated_moves, expected_moves); } @@ -104,9 +95,9 @@ mod tests { ) .build()]); - let generated_moves: HashSet = generator.iter().cloned().collect(); + let generated_moves: HashSet = generator.iter().collect(); - assert_eq!(generated_moves, expected_moves); + assert_move_list!(generated_moves, expected_moves, pos); } #[test] @@ -127,9 +118,9 @@ mod tests { ) .build()]); - let generated_moves: HashSet = generator.iter().cloned().collect(); + let generated_moves: HashSet = generator.iter().collect(); - assert_eq!(generated_moves, expected_moves); + assert_move_list!(generated_moves, expected_moves, pos); } #[test] @@ -141,9 +132,10 @@ mod tests { let generator = PawnMoveGenerator::new(&pos, Color::White); - let generated_moves: HashSet = generator.iter().cloned().collect(); + let generated_moves: HashSet = generator.iter().collect(); + let expected_moves: HashSet = HashSet::new(); - assert_eq!(generated_moves, HashSet::new()); + assert_move_list!(generated_moves, expected_moves, pos); } #[test] @@ -163,7 +155,7 @@ mod tests { .build()], ); - let generated_moves: HashSet = generator.iter().cloned().collect(); + let generated_moves: HashSet = generator.iter().collect(); assert_eq!(generated_moves, expected_moves); } @@ -188,7 +180,7 @@ mod tests { .build(), ]); - let generated_moves: HashSet = generator.iter().cloned().collect(); + let generated_moves: HashSet = generator.iter().collect(); assert_eq!( generated_moves, expected_moves, From 2d5710ccb10c80f61cc3240c99e7e965dfafde04 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 15:58:36 -0800 Subject: [PATCH 5/8] Clean up Pawn::pushes a little bit --- position/src/move_generator/pawn.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/position/src/move_generator/pawn.rs b/position/src/move_generator/pawn.rs index 401366e..1797603 100644 --- a/position/src/move_generator/pawn.rs +++ b/position/src/move_generator/pawn.rs @@ -30,13 +30,11 @@ impl PawnMoveGenerator { let square = piece.square(); let bitboard: BitBoard = square.into(); - let empty_squares = position.empty_squares(); + let starting_rank = Rank::PAWN_STARTING_RANKS[piece.color() as usize]; + let empty_squares = position.empty_squares(); let mut moves = bitboard.shift_north_one() & empty_squares; - if !(bitboard - & BitBoard::rank(Rank::PAWN_STARTING_RANKS[piece.color() as usize].as_index())) - .is_empty() - { + if !(bitboard & BitBoard::rank(starting_rank.as_index())).is_empty() { moves |= moves.shift_north_one() & empty_squares; } From ea22f7c5c722ded57808e050c1273265e91b4002 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 15:58:47 -0800 Subject: [PATCH 6/8] Clean up some test imports --- position/src/move_generator/pawn.rs | 5 +---- position/src/move_generator/rook.rs | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/position/src/move_generator/pawn.rs b/position/src/move_generator/pawn.rs index 1797603..728ba05 100644 --- a/position/src/move_generator/pawn.rs +++ b/position/src/move_generator/pawn.rs @@ -57,10 +57,7 @@ impl PawnMoveGenerator { #[cfg(test)] mod tests { use super::*; - use crate::{ - assert_move_list, position::DiagramFormatter, r#move::AlgebraicMoveFormatter, - test_position, Move, MoveBuilder, - }; + use crate::{assert_move_list, position::DiagramFormatter, test_position, Move, MoveBuilder}; use chessfriend_core::{piece, Square}; use std::collections::HashSet; diff --git a/position/src/move_generator/rook.rs b/position/src/move_generator/rook.rs index 4d74c11..72f237b 100644 --- a/position/src/move_generator/rook.rs +++ b/position/src/move_generator/rook.rs @@ -1,7 +1,7 @@ // Eryn Wells use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; -use crate::{MoveBuilder, Position}; +use crate::Position; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Direction, Piece, PlacedPiece}; From 83a4e47e56452b0fc1b0faf8fc6f817c13cdaaad Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 15:59:16 -0800 Subject: [PATCH 7/8] MoveGenerator::iter() returns an iterator of moves-by-value --- position/src/move_generator.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/position/src/move_generator.rs b/position/src/move_generator.rs index 2202104..eef5fcf 100644 --- a/position/src/move_generator.rs +++ b/position/src/move_generator.rs @@ -57,7 +57,7 @@ macro_rules! move_generator_declaration { }; ($name:ident, getters) => { impl $name { - pub(super) fn iter(&self) -> impl Iterator + '_ { + pub(super) fn iter(&self) -> impl Iterator + '_ { self.move_sets.values().map(|set| set.moves()).flatten() } @@ -141,7 +141,6 @@ impl Moves { .chain(self.rook_moves.iter()) .chain(self.queen_moves.iter()) .chain(self.king_moves.iter()) - .cloned() } } From 1d7dada9878cd298e7a283a3a79c4c6bb0cc0971 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 29 Jan 2024 14:44:48 -0800 Subject: [PATCH 8/8] [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!(