From eb6f2000a97617d129c781f56432c7061e300d6c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 26 May 2025 23:37:33 -0700 Subject: [PATCH] [board, moves, position] Implement KingMoveGenerator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. 🙃 --- board/src/board.rs | 9 +- board/src/castle.rs | 60 ++--- board/src/castle/parameters.rs | 4 +- board/src/movement.rs | 23 +- board/src/sight.rs | 1 + moves/src/generators.rs | 2 + moves/src/generators/king.rs | 361 +++++++++++++++++++++++++++++ moves/src/moves.rs | 4 +- position/src/position/make_move.rs | 16 +- 9 files changed, 427 insertions(+), 53 deletions(-) create mode 100644 moves/src/generators/king.rs diff --git a/board/src/board.rs b/board/src/board.rs index e5e0cbc..4746e95 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -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)) } } diff --git a/board/src/castle.rs b/board/src/castle.rs index 6581676..ef3a901 100644 --- a/board/src/castle.rs +++ b/board/src/castle.rs @@ -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, + ) -> 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 { @@ -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) ); } diff --git a/board/src/castle/parameters.rs b/board/src/castle/parameters.rs index bc4e238..54c3512 100644 --- a/board/src/castle/parameters.rs +++ b/board/src/castle/parameters.rs @@ -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, diff --git a/board/src/movement.rs b/board/src/movement.rs index a1387db..f386579 100644 --- a/board/src/movement.rs +++ b/board/src/movement.rs @@ -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 } diff --git a/board/src/sight.rs b/board/src/sight.rs index 8865097..8518300 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -114,6 +114,7 @@ impl Board { sight_method!(bishop_sight); sight_method!(rook_sight); sight_method!(queen_sight); + sight_method!(king_sight); } struct SightInfo { diff --git a/moves/src/generators.rs b/moves/src/generators.rs index d5c9e37..a3b6fcf 100644 --- a/moves/src/generators.rs +++ b/moves/src/generators.rs @@ -1,5 +1,6 @@ // Eryn Wells +mod king; mod knight; mod pawn; mod slider; @@ -7,6 +8,7 @@ mod slider; #[cfg(test)] mod testing; +pub use king::KingMoveGenerator; pub use knight::KnightMoveGenerator; pub use pawn::PawnMoveGenerator; pub use slider::{BishopMoveGenerator, QueenMoveGenerator, RookMoveGenerator}; diff --git a/moves/src/generators/king.rs b/moves/src/generators/king.rs new file mode 100644 index 0000000..b091b3d --- /dev/null +++ b/moves/src/generators/king.rs @@ -0,0 +1,361 @@ +// Eryn Wells + +use crate::{GeneratedMove, Move}; +use chessfriend_bitboard::{bit_scanner::TrailingBitScanner, BitBoard}; +use chessfriend_board::{Board, CastleParameters}; +use chessfriend_core::{Color, Square, Wing}; + +#[must_use] +pub struct KingMoveGenerator { + kings: Vec, + next_kings_index: usize, + current_king: Option, + castle_iterator: CastleIterator, + friends: BitBoard, + enemies: BitBoard, +} + +impl KingMoveGenerator { + pub fn new(board: &Board, color: Option) -> Self { + let color = board.unwrap_color(color); + + let friends = board.friendly_occupancy(color); + let enemies = board.enemies(color); + + let kings: Vec = board + .kings(color) + .occupied_squares_trailing() + .map(|king| KingIterator { + origin: king, + moves: board + .king_sight(king, Some(color)) + .occupied_squares_trailing(), + }) + .collect(); + + Self { + kings, + next_kings_index: 0, + current_king: None, + castle_iterator: CastleIterator::new(board, color), + friends, + enemies, + } + } +} + +impl Iterator for KingMoveGenerator { + type Item = GeneratedMove; + + fn next(&mut self) -> Option { + loop { + if self.current_king.is_none() { + if self.next_kings_index < self.kings.len() { + self.current_king = Some(self.kings[self.next_kings_index].clone()); + self.next_kings_index += 1; + } else { + break; + } + } + + if let Some(current_king) = self.current_king.as_mut() { + if let Some(target) = current_king.next() { + let target_bitboard: BitBoard = target.into(); + + let is_targeting_friendly_piece = + (target_bitboard & self.friends).is_populated(); + if is_targeting_friendly_piece { + continue; + } + + let is_targeting_enemy_piece = (target_bitboard & self.enemies).is_populated(); + if is_targeting_enemy_piece { + return Some(Move::capture(current_king.origin, target).into()); + } + + return Some(Move::quiet(current_king.origin, target).into()); + } + + self.current_king = None; + } + } + + self.castle_iterator.next() + } +} + +#[derive(Clone, Debug)] +struct KingIterator { + origin: Square, + moves: TrailingBitScanner, +} + +impl Iterator for KingIterator { + type Item = Square; + + fn next(&mut self) -> Option { + self.moves.next() + } +} + +#[derive(Clone, Debug)] +struct CastleIterator { + kingside: Option<&'static CastleParameters>, + queenside: Option<&'static CastleParameters>, +} + +impl CastleIterator { + fn new(board: &Board, color: Color) -> Self { + let kingside = board.color_can_castle(Wing::KingSide, Some(color)).ok(); + let queenside = board.color_can_castle(Wing::QueenSide, Some(color)).ok(); + + Self { + kingside, + queenside, + } + } +} + +impl Iterator for CastleIterator { + type Item = GeneratedMove; + + fn next(&mut self) -> Option { + if let Some(_parameters) = self.kingside.take() { + return Some(Move::castle(Wing::KingSide).into()); + } + + if let Some(_parameters) = self.queenside.take() { + return Some(Move::castle(Wing::QueenSide).into()); + } + + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{assert_move_list, ply}; + use chessfriend_board::test_board; + + #[test] + fn white_king_center_square_ai_claude() { + let board = test_board!(White King on E4); + assert_move_list!( + KingMoveGenerator::new(&board, None), + [ + // All 8 adjacent squares + ply!(E4 - D3), // Southwest + ply!(E4 - D4), // West + ply!(E4 - D5), // Northwest + ply!(E4 - E3), // South + ply!(E4 - E5), // North + ply!(E4 - F3), // Southeast + ply!(E4 - F4), // East + ply!(E4 - F5), // Northeast + ] + ); + } + + #[test] + fn white_king_corner_square_ai_claude() { + let board = test_board!(White King on A1); + assert_move_list!( + KingMoveGenerator::new(&board, None), + [ + // Only 3 possible moves from corner + ply!(A1 - A2), // North + ply!(A1 - B1), // East + ply!(A1 - B2), // Northeast + ] + ); + } + + #[test] + fn white_king_edge_square_ai_claude() { + let board = test_board!(White King on E1); + assert_move_list!( + KingMoveGenerator::new(&board, None), + [ + // 5 possible moves from edge + ply!(E1 - D1), // West + ply!(E1 - D2), // Northwest + ply!(E1 - E2), // North + ply!(E1 - F1), // East + ply!(E1 - F2), // Northeast + ] + ); + } + + #[test] + fn white_king_with_captures_ai_claude() { + let board = test_board!( + White King on D4, + Black Pawn on C5, // Can capture + Black Knight on E5, // Can capture + Black Bishop on D3 // Can capture + ); + assert_move_list!( + KingMoveGenerator::new(&board, None), + [ + // Regular moves + ply!(D4 - C3), + ply!(D4 - C4), + ply!(D4 - D5), + ply!(D4 - E3), + ply!(D4 - E4), + // Captures + ply!(D4 x C5), // Capture pawn + ply!(D4 x E5), // Capture knight + ply!(D4 x D3), // Capture bishop + ] + ); + } + + #[test] + fn white_king_blocked_by_friendly_pieces_ai_claude() { + let board = test_board!( + White King on D4, + White Pawn on C4, // Blocks west + White Knight on D5, // Blocks north + White Bishop on E3 // Blocks southeast + ); + assert_move_list!( + KingMoveGenerator::new(&board, None), + [ + ply!(D4 - C3), + ply!(D4 - C5), + ply!(D4 - D3), + ply!(D4 - E4), + ply!(D4 - E5), + // Cannot move to C4, D5, E3 (friendly pieces) + ] + ); + } + + #[test] + fn white_king_castling_kingside_ai_claude() { + let board = test_board!( + White King on E1, + White Rook on H1 + // Assuming squares F1, G1 are empty and king/rook haven't moved + ); + assert_move_list!( + KingMoveGenerator::new(&board, None), + [ + // Regular king moves + ply!(E1 - D1), + ply!(E1 - D2), + ply!(E1 - E2), + ply!(E1 - F1), + ply!(E1 - F2), + ply!(0 - 0), + ] + ); + } + + #[test] + fn white_king_castling_queenside_ai_claude() { + let board = test_board!( + White King on E1, + White Rook on A1 + // Assuming squares B1, C1, D1 are empty and king/rook haven't moved + ); + assert_move_list!( + KingMoveGenerator::new(&board, None), + [ + // Regular king moves + ply!(E1 - D1), + ply!(E1 - D2), + ply!(E1 - E2), + ply!(E1 - F1), + ply!(E1 - F2), + ply!(0 - 0 - 0), + ] + ); + } + + #[test] + fn white_king_no_castling_through_check_ai_claude() { + let board = test_board!( + White King on E1, + White Rook on H1, + Black Rook on F8 // Attacks F1, preventing kingside castling + ); + assert_move_list!( + KingMoveGenerator::new(&board, None), + [ + ply!(E1 - D1), + ply!(E1 - D2), + ply!(E1 - E2), + ply!(E1 - F1), + ply!(E1 - F2), + // No castling moves - cannot castle through check + ] + ); + } + + #[test] + fn white_king_castling_blocked_by_pieces_ai_claude() { + let board = test_board!( + White King on E1, + White Rook on H1, + White Knight on G1, // Blocks kingside castling + ); + assert_move_list!( + KingMoveGenerator::new(&board, None), + [ + ply!(E1 - D1), + ply!(E1 - D2), + ply!(E1 - E2), + ply!(E1 - F1), + ply!(E1 - F2), + // No castling - path blocked by knight + ] + ); + } + + #[test] + fn black_king_movement_ai_claude() { + let board = test_board!(Black King on E8); + assert_move_list!( + KingMoveGenerator::new(&board, Some(Color::Black)), + [ + ply!(E8 - D7), + ply!(E8 - D8), + ply!(E8 - E7), + ply!(E8 - F7), + ply!(E8 - F8), + ] + ); + } + + #[test] + fn white_king_surrounded_by_enemies_ai_claude() { + let board = test_board!( + White King on E4, + Black Pawn on D3, + Black Pawn on D4, + Black Pawn on D5, + Black Pawn on E3, + Black Pawn on E5, + Black Pawn on F3, + Black Pawn on F4, + Black Pawn on F5 + ); + assert_move_list!( + KingMoveGenerator::new(&board, None), + [ + // Can capture all surrounding enemy pieces + ply!(E4 x D3), + ply!(E4 x D4), + ply!(E4 x D5), + ply!(E4 x E3), + ply!(E4 x E5), + ply!(E4 x F3), + ply!(E4 x F4), + ply!(E4 x F5), + ] + ); + } +} diff --git a/moves/src/moves.rs b/moves/src/moves.rs index d3fd940..59ac237 100644 --- a/moves/src/moves.rs +++ b/moves/src/moves.rs @@ -108,12 +108,12 @@ impl Move { } #[must_use] - pub fn castle(origin: Square, target: Square, wing: Wing) -> Self { + pub fn castle(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) + Move(flag_bits) } } diff --git a/position/src/position/make_move.rs b/position/src/position/make_move.rs index 3c3ad3b..e944e3f 100644 --- a/position/src/position/make_move.rs +++ b/position/src/position/make_move.rs @@ -2,7 +2,7 @@ use crate::{move_record::MoveRecord, Position}; use chessfriend_board::{ - castle::CastleEvaluationError, movement::Movement, PlacePieceError, PlacePieceStrategy, + castle::CastleEvaluationError, movement::Movement, Board, PlacePieceError, PlacePieceStrategy, }; use chessfriend_core::{Color, Piece, Rank, Square, Wing}; use chessfriend_moves::Move; @@ -79,17 +79,18 @@ impl Position { ply: Move, validate: ValidateMove, ) -> MakeMoveResult { - self.validate_move(ply, validate)?; - if ply.is_quiet() { + self.validate_move(ply, validate)?; return self.make_quiet_move(ply); } if ply.is_double_push() { + self.validate_move(ply, validate)?; return self.make_double_push_move(ply); } if ply.is_capture() { + self.validate_move(ply, validate)?; return self.make_capture_move(ply); } @@ -98,6 +99,7 @@ impl Position { } if ply.is_promotion() { + self.validate_move(ply, validate)?; return self.make_promotion_move(ply); } @@ -189,10 +191,10 @@ impl Position { } fn make_castle_move(&mut self, ply: Move, wing: Wing) -> MakeMoveResult { - self.board.active_color_can_castle(wing)?; + self.board.color_can_castle(wing, None)?; let active_color = self.board.active_color; - let parameters = self.board.castling_parameters(wing); + let parameters = Board::castling_parameters(wing, active_color); let king = self.board.remove_piece(parameters.origin.king).unwrap(); self.place_piece(king, parameters.target.king, PlacePieceStrategy::default())?; @@ -490,7 +492,7 @@ mod tests { White King on E1, ]; - let ply = Move::castle(Square::E1, Square::G1, Wing::KingSide); + let ply = Move::castle(Wing::KingSide); pos.make_move(ply, ValidateMove::Yes)?; assert_eq!(pos.board.active_color, Color::Black); @@ -513,7 +515,7 @@ mod tests { White Rook on A1, ]; - let ply = Move::castle(Square::E1, Square::C1, Wing::QueenSide); + let ply = Move::castle(Wing::QueenSide); pos.make_move(ply, ValidateMove::Yes)?; assert_eq!(pos.board.active_color, Color::Black);