From 5efb818782aeae3cb972e591260d99377658c0fb Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 25 Jun 2021 16:14:47 +0200 Subject: [PATCH 1/6] Factor out "physical" board state into a separate private struct that guarantees a valid board --- src/board.rs | 575 ++++++++++++++++++++++++++++++++----------- src/board_builder.rs | 4 + 2 files changed, 435 insertions(+), 144 deletions(-) diff --git a/src/board.rs b/src/board.rs index ff5031227..040b25b1e 100644 --- a/src/board.rs +++ b/src/board.rs @@ -20,12 +20,355 @@ use std::hash::{Hash, Hasher}; use std::mem; use std::str::FromStr; +mod board_pieces { + use super::*; + + const MAX_PIECES_PER_COLOR: u32 = 16; + + /// Internal representation of "physical" board state + /// + /// Guarantees: + /// - always consistent pieces, combined and color_combined + /// - Number of pieces per player always <= MAX_PIECES_PER_COLOR + /// + /// BoardPieces must only be modified through the unsafe functions to mark places where + /// that happens. `PieceHandle` and friends provide a safe interface. + #[derive(Copy, Clone, PartialEq, Eq, Debug)] + pub struct BoardPieces { + pieces: [BitBoard; NUM_PIECES], + color_combined: [BitBoard; NUM_COLORS], + combined: BitBoard, + } + + impl BoardPieces { + pub const fn new() -> Self { + Self { + pieces: [EMPTY; NUM_PIECES], + color_combined: [EMPTY; NUM_COLORS], + combined: EMPTY, + } + } + + #[inline(always)] + pub fn pieces(&self, piece: Piece) -> &BitBoard { + &self.pieces[piece.to_index()] + } + + #[inline(always)] + pub fn combined(&self) -> &BitBoard { + &self.combined + } + + #[inline(always)] + pub fn color_combined(&self, color: Color) -> &BitBoard { + &self.color_combined[color.to_index()] + } + + #[inline] + pub fn piece_on(&self, square: Square) -> Option { + let opp = BitBoard::from_square(square); + + if self.combined() & opp == EMPTY { + None + } else { + // TODO: investigate branchless version? + //naive algorithm + /* + for p in ALL_PIECES { + if self.pieces(*p) & opp { + return p; + } + } */ + if (self.pieces(Piece::Pawn) ^ self.pieces(Piece::Knight) ^ self.pieces(Piece::Bishop)) + & opp + != EMPTY + { + if self.pieces(Piece::Pawn) & opp != EMPTY { + Some(Piece::Pawn) + } else if self.pieces(Piece::Knight) & opp != EMPTY { + Some(Piece::Knight) + } else { + Some(Piece::Bishop) + } + } else { + if self.pieces(Piece::Rook) & opp != EMPTY { + Some(Piece::Rook) + } else if self.pieces(Piece::Queen) & opp != EMPTY { + Some(Piece::Queen) + } else { + Some(Piece::King) + } + } + } + } + + /// SAFETY: + /// - There must be a piece of given type and color at src + /// - dst must be empty + #[inline(always)] + unsafe fn move_unchecked(&mut self, src: Square, dst: Square, piece: Piece, color: Color) { + let dst_bb = BitBoard::from_square(dst); + let src_bb = BitBoard::from_square(src); + let move_bb = src_bb | dst_bb; + + self.pieces[piece.to_index()] ^= move_bb; + self.color_combined[color.to_index()] ^= move_bb; + self.combined ^= move_bb; + } + + /// SAFETY: There must be a piece of type old at given square + #[inline(always)] + unsafe fn change_piece_unchecked(&mut self, square: Square, old: Piece, new: Piece) { + let bb = BitBoard::from_square(square); + self.pieces[old.to_index()] ^= bb; + self.pieces[new.to_index()] ^= bb; + } + + /// SAFETY: + /// - There must be a piece at given square + /// - total number of pieces of target color must not exceed MAX_PIECES_PER_COLOR + #[inline(always)] + unsafe fn toggle_color_unchecked(&mut self, square: Square) { + let bb = BitBoard::from_square(square); + self.color_combined[0] ^= bb; + self.color_combined[1] ^= bb; + } + + /// SAFETY: + /// - If the square is empty the total number of pieces of the given color must not + /// exceed MAX_PIECES_PER_COLOR after this operation + /// - Other wise the square must be occupied by the given piece and color + #[inline(always)] + unsafe fn toggle_piece_unchecked(&mut self, square: Square, piece: Piece, color: Color) { + let bb = BitBoard::from_square(square); + self.pieces[piece.to_index()] ^= bb; + self.color_combined[color.to_index()] ^= bb; + self.combined ^= bb; + } + + /// Returns the correct color if the square is occupied + #[inline(always)] + fn color_of_occupied(&self, square: Square) -> Color { + match self.color_combined[0] & BitBoard::from_square(square) == EMPTY { + false => Color::White, + true => Color::Black, + } + } + + /// Return a PieceHandle to the piece at the given square if there is one + #[inline] + pub fn get_piece_handle(&mut self, square: Square) -> Option { + let piece = self.piece_on(square)?; + let color = self.color_of_occupied(square); + Some(PieceHandle{ + board_pieces: self, + square, + piece, + color + }) + } + + /// Return a PieceHandle to the piece at the given square if it has the given color and type + #[inline(always)] + pub fn get_known_piece_handle(&mut self, square: Square, color: Color, piece: Piece) -> Option { + let bb = BitBoard::from_square(square); + if self.pieces(piece) & bb == EMPTY || self.color_combined(color) & bb == EMPTY { + None + } else { + Some(PieceHandle{ + board_pieces: self, + square, + piece, + color + }) + } + } + + /// Return a handle to the square. Inspired by HashMap's entry API + #[inline] + pub fn get_square_handle(&mut self, square: Square) -> SquareHandle { + if let Some(piece) = self.piece_on(square) { + let color = self.color_of_occupied(square); + SquareHandle::Occupied(PieceHandle{ + board_pieces: self, + square, + piece, + color + }) + } else { + SquareHandle::Empty(EmptySquare{ + board_pieces: self, + square + }) + } + } + } + + impl TryFrom<[Option<(Piece, Color)>; 64]> for BoardPieces { + type Error = Error; + fn try_from(pieces: [Option<(Piece, Color)>; 64]) -> Result { + let black = pieces.iter().filter(|f| matches!(f, Some((_, Color::Black)) )).count(); + let white = pieces.iter().filter(|f| matches!(f, Some((_, Color::White)) )).count(); + if black as u32 > MAX_PIECES_PER_COLOR || white as u32 > MAX_PIECES_PER_COLOR { + Err(Error::InvalidBoard) + } else { + let mut result = Self::new(); + for (sq, (piece, color)) in pieces.iter().zip(ALL_SQUARES.iter()).filter_map(|(f, &sq)| f.map(|f| (sq, f))) { + unsafe { + result.toggle_piece_unchecked(sq, piece, color) + } + } + Ok(result) + } + } + } + + /// This handle guarantees that it points to an occupied square with the given piece and color + pub struct PieceHandle<'a> { + board_pieces: &'a mut BoardPieces, + square: Square, + piece: Piece, + color: Color + } + + impl<'a> PieceHandle<'a> { + #[inline(always)] + pub fn square(&self) -> Square { + self.square + } + + #[inline(always)] + pub fn piece(&self) -> Piece { + self.piece + } + + #[inline(always)] + pub fn color(&self) -> Color { + self.color + } + + /// panics if dst is not empty + #[inline] + pub fn move_to(&mut self, dst: Square) { + let dst_bb = BitBoard::from_square(dst); + assert_eq!(self.board_pieces.combined & dst_bb, EMPTY); + + unsafe { + self.board_pieces.move_unchecked(self.square, dst, self.piece, self.color) + } + self.square = dst; + } + + #[inline] + pub fn move_and_capture(&mut self, dst: Square) -> Option<(Piece, Color)> { + let mut captured = None; + + if let Some(captured_piece) = self.board_pieces.piece_on(dst) { + let captured_color = self.board_pieces.color_of_occupied(dst); + unsafe { + self.board_pieces.toggle_piece_unchecked(dst, captured_piece, captured_color); + } + captured = Some((captured_piece, captured_color)); + } + + unsafe { + // the destination is empty because we removed the captured piece + self.board_pieces.move_unchecked(self.square, dst, self.piece, self.color) + } + + captured + } + + #[inline] + pub fn replace_piece(&mut self, new: Piece) { + unsafe { + self.board_pieces.change_piece_unchecked(self.square, self.piece, new); + } + self.piece = new; + } + + #[inline] + pub fn set_color(mut self, color: Color) -> Result { + if self.color == color { + Ok(self) + } else { + if self.board_pieces.color_combined(color).popcnt() + 1 <= MAX_PIECES_PER_COLOR { + unsafe { + self.board_pieces.toggle_color_unchecked(self.square); + Ok(self) + } + } else { + Err(self) + } + } + } + + #[inline] + pub fn remove(mut self) -> EmptySquare<'a> { + unsafe { + self.board_pieces.toggle_piece_unchecked(self.square, self.piece, self.color); + } + EmptySquare { + board_pieces: self.board_pieces, + square: self.square + } + } + } + + pub struct EmptySquare<'a> { + board_pieces: &'a mut BoardPieces, + square: Square, + } + + impl<'a> EmptySquare<'a> { + pub fn place_piece(mut self, piece: Piece, color: Color) -> Result, EmptySquare<'a>> { + if self.board_pieces.color_combined(color).popcnt() + 1 <= MAX_PIECES_PER_COLOR { + unsafe { + self.board_pieces.toggle_piece_unchecked(self.square, piece, color); + } + Ok(PieceHandle { + board_pieces: self.board_pieces, + square: self.square, + piece, + color + }) + } else { + Err(self) + } + } + } + + pub enum SquareHandle<'a> { + Empty(EmptySquare<'a>), + Occupied(PieceHandle<'a>) + } + + impl<'a> SquareHandle<'a> { + pub fn set_piece(self, color: Color, piece: Piece) -> Result, SquareHandle<'a>> { + match self { + SquareHandle::Occupied(piece_handle) => { + match piece_handle.set_color(color) { + Ok(mut piece_handle) => { + piece_handle.replace_piece(piece); + Ok(piece_handle) + }, + Err(piece_handle) => { + Err(SquareHandle::Occupied(piece_handle)) + } + } + }, + SquareHandle::Empty(empty) => empty.place_piece(piece, color).map_err(SquareHandle::Empty) + } + } + } +} + +use board_pieces::BoardPieces; + /// A representation of a chess board. That's why you're here, right? #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub struct Board { - pieces: [BitBoard; NUM_PIECES], - color_combined: [BitBoard; NUM_COLORS], - combined: BitBoard, + board_pieces: BoardPieces, side_to_move: Color, castle_rights: [CastleRights; NUM_COLORS], pinned: BitBoard, @@ -62,9 +405,7 @@ impl Board { /// Note: This does NOT give you the initial position. Just a blank slate. fn new() -> Board { Board { - pieces: [EMPTY; NUM_PIECES], - color_combined: [EMPTY; NUM_COLORS], - combined: EMPTY, + board_pieces: BoardPieces::new(), side_to_move: Color::White, castle_rights: [CastleRights::NoRights; NUM_COLORS], pinned: EMPTY, @@ -184,7 +525,7 @@ impl Board { /// ``` #[inline] pub fn combined(&self) -> &BitBoard { - &self.combined + &self.board_pieces.combined() } /// Grab the "color combined" `BitBoard`. This is a `BitBoard` with every piece of a particular @@ -206,7 +547,7 @@ impl Board { /// ``` #[inline] pub fn color_combined(&self, color: Color) -> &BitBoard { - unsafe { self.color_combined.get_unchecked(color.to_index()) } + self.board_pieces.color_combined(color) } /// Give me the `Square` the `color` king is on. @@ -241,7 +582,7 @@ impl Board { /// ``` #[inline] pub fn pieces(&self, piece: Piece) -> &BitBoard { - unsafe { self.pieces.get_unchecked(piece.to_index()) } + self.board_pieces.pieces(piece) } /// Grab the `CastleRights` for a particular side. @@ -432,16 +773,6 @@ impl Board { self.remove_castle_rights(color, remove); } - /// Add or remove a piece from the bitboards in this struct. - fn xor(&mut self, piece: Piece, bb: BitBoard, color: Color) { - unsafe { - *self.pieces.get_unchecked_mut(piece.to_index()) ^= bb; - *self.color_combined.get_unchecked_mut(color.to_index()) ^= bb; - self.combined ^= bb; - self.hash ^= Zobrist::piece(piece, bb.to_square(), color); - } - } - /// For a chess UI: set a piece on a particular square. /// /// ``` @@ -463,19 +794,10 @@ impl Board { #[inline] pub fn set_piece(&self, piece: Piece, color: Color, square: Square) -> Option { let mut result = *self; - let square_bb = BitBoard::from_square(square); - match self.piece_on(square) { - None => result.xor(piece, square_bb, color), - Some(x) => { - // remove x from the bitboard - if self.color_combined(Color::White) & square_bb == square_bb { - result.xor(x, square_bb, Color::White); - } else { - result.xor(x, square_bb, Color::Black); - } - // add piece to the bitboard - result.xor(piece, square_bb, color); - } + + if result.board_pieces.get_square_handle(square).set_piece(color, piece).is_err() { + // too many pieces of this color + return None; } // If setting this piece down leaves my opponent in check, and it's my move, then the @@ -512,16 +834,11 @@ impl Board { #[inline] pub fn clear_square(&self, square: Square) -> Option { let mut result = *self; - let square_bb = BitBoard::from_square(square); - match self.piece_on(square) { + match result.board_pieces.get_piece_handle(square) { None => {} Some(x) => { // remove x from the bitboard - if self.color_combined(Color::White) & square_bb == square_bb { - result.xor(x, square_bb, Color::White); - } else { - result.xor(x, square_bb, Color::Black); - } + x.remove(); } } @@ -723,38 +1040,7 @@ impl Board { /// ``` #[inline] pub fn piece_on(&self, square: Square) -> Option { - let opp = BitBoard::from_square(square); - if self.combined() & opp == EMPTY { - None - } else { - //naiive algorithm - /* - for p in ALL_PIECES { - if self.pieces(*p) & opp { - return p; - } - } */ - if (self.pieces(Piece::Pawn) ^ self.pieces(Piece::Knight) ^ self.pieces(Piece::Bishop)) - & opp - != EMPTY - { - if self.pieces(Piece::Pawn) & opp != EMPTY { - Some(Piece::Pawn) - } else if self.pieces(Piece::Knight) & opp != EMPTY { - Some(Piece::Knight) - } else { - Some(Piece::Bishop) - } - } else { - if self.pieces(Piece::Rook) & opp != EMPTY { - Some(Piece::Rook) - } else if self.pieces(Piece::Queen) & opp != EMPTY { - Some(Piece::Queen) - } else { - Some(Piece::King) - } - } - } + self.board_pieces.piece_on(square) } /// What color piece is on a particular square? @@ -898,88 +1184,93 @@ impl Board { let move_bb = source_bb ^ dest_bb; let moved = self.piece_on(source).unwrap(); - result.xor(moved, source_bb, self.side_to_move); - result.xor(moved, dest_bb, self.side_to_move); - if let Some(captured) = self.piece_on(dest) { - result.xor(captured, dest_bb, !self.side_to_move); - } + let ksq = (result.pieces(Piece::King) & result.color_combined(!result.side_to_move)).to_square(); #[allow(deprecated)] - result.remove_their_castle_rights(CastleRights::square_to_castle_rights( + result.remove_their_castle_rights(CastleRights::square_to_castle_rights( !self.side_to_move, dest, )); #[allow(deprecated)] - result.remove_my_castle_rights(CastleRights::square_to_castle_rights( + result.remove_my_castle_rights(CastleRights::square_to_castle_rights( self.side_to_move, source, )); - let opp_king = result.pieces(Piece::King) & result.color_combined(!result.side_to_move); - - let castles = moved == Piece::King && (move_bb & get_castle_moves()) == move_bb; - - let ksq = opp_king.to_square(); - - const CASTLE_ROOK_START: [File; 8] = [ - File::A, - File::A, - File::A, - File::A, - File::H, - File::H, - File::H, - File::H, - ]; - const CASTLE_ROOK_END: [File; 8] = [ - File::D, - File::D, - File::D, - File::D, - File::F, - File::F, - File::F, - File::F, - ]; - - if moved == Piece::Knight { - result.checkers ^= get_knight_moves(ksq) & dest_bb; - } else if moved == Piece::Pawn { - if let Some(Piece::Knight) = m.get_promotion() { - result.xor(Piece::Pawn, dest_bb, self.side_to_move); - result.xor(Piece::Knight, dest_bb, self.side_to_move); + // extra scope to signal that piece_handle is dropped latest at the end + { + let mut piece_handle = result.board_pieces.get_piece_handle(source).unwrap(); + assert_eq!(self.side_to_move, piece_handle.color()); + + if let Some((_captured_piece, _captured_color)) = piece_handle.move_and_capture(dest) { + // TODO: Assert we captured the correct color? + } + + let castles = piece_handle.piece() == Piece::King && (move_bb & get_castle_moves()) == move_bb; + + + const CASTLE_ROOK_START: [File; 8] = [ + File::A, + File::A, + File::A, + File::A, + File::H, + File::H, + File::H, + File::H, + ]; + const CASTLE_ROOK_END: [File; 8] = [ + File::D, + File::D, + File::D, + File::D, + File::F, + File::F, + File::F, + File::F, + ]; + + if moved == Piece::Knight { result.checkers ^= get_knight_moves(ksq) & dest_bb; - } else if let Some(promotion) = m.get_promotion() { - result.xor(Piece::Pawn, dest_bb, self.side_to_move); - result.xor(promotion, dest_bb, self.side_to_move); - } else if (source_bb & get_pawn_source_double_moves()) != EMPTY - && (dest_bb & get_pawn_dest_double_moves()) != EMPTY - { - result.set_ep(dest); - result.checkers ^= get_pawn_attacks(ksq, !result.side_to_move, dest_bb); - } else if Some(dest.ubackward(self.side_to_move)) == self.en_passant { - result.xor( - Piece::Pawn, - BitBoard::from_square(dest.ubackward(self.side_to_move)), - !self.side_to_move, - ); - result.checkers ^= get_pawn_attacks(ksq, !result.side_to_move, dest_bb); - } else { - result.checkers ^= get_pawn_attacks(ksq, !result.side_to_move, dest_bb); + } else if moved == Piece::Pawn { + if let Some(Piece::Knight) = m.get_promotion() { + piece_handle.replace_piece(Piece::Knight); + result.checkers ^= get_knight_moves(ksq) & dest_bb; + } else if let Some(promotion) = m.get_promotion() { + piece_handle.replace_piece(promotion); + } else if (source_bb & get_pawn_source_double_moves()) != EMPTY + && (dest_bb & get_pawn_dest_double_moves()) != EMPTY + { + result.set_ep(dest); + result.checkers ^= get_pawn_attacks(ksq, !result.side_to_move, dest_bb); + } else if Some(dest.ubackward(self.side_to_move)) == self.en_passant { + drop(piece_handle); + + // remove opponents en passant pawn + result.board_pieces.get_known_piece_handle(self.en_passant.unwrap(), !result.side_to_move, Piece::Pawn).unwrap().remove(); + + result.checkers ^= get_pawn_attacks(ksq, !result.side_to_move, dest_bb); + } else { + result.checkers ^= get_pawn_attacks(ksq, !result.side_to_move, dest_bb); + } + } else if castles { + drop(piece_handle); + + let my_backrank = self.side_to_move.to_my_backrank(); + let index = dest.get_file().to_index(); + let start = BitBoard::set(my_backrank, unsafe { + *CASTLE_ROOK_START.get_unchecked(index) + }); + let end = BitBoard::set(my_backrank, unsafe { + *CASTLE_ROOK_END.get_unchecked(index) + }); + + let mut rook = result.board_pieces.get_known_piece_handle(start.to_square(), self.side_to_move, Piece::Rook).unwrap(); + rook.move_to(end.to_square()); } - } else if castles { - let my_backrank = self.side_to_move.to_my_backrank(); - let index = dest.get_file().to_index(); - let start = BitBoard::set(my_backrank, unsafe { - *CASTLE_ROOK_START.get_unchecked(index) - }); - let end = BitBoard::set(my_backrank, unsafe { - *CASTLE_ROOK_END.get_unchecked(index) - }); - result.xor(Piece::Rook, start, self.side_to_move); - result.xor(Piece::Rook, end, self.side_to_move); } + // now, lets see if we're in check or pinned let attackers = result.color_combined(result.side_to_move) & ((get_bishop_rays(ksq) @@ -1056,11 +1347,7 @@ impl TryFrom<&BoardBuilder> for Board { fn try_from(fen: &BoardBuilder) -> Result { let mut board = Board::new(); - for sq in ALL_SQUARES.iter() { - if let Some((piece, color)) = fen[*sq] { - board.xor(piece, BitBoard::from_square(*sq), color); - } - } + board.board_pieces = BoardPieces::try_from(*fen.pieces())?; board.side_to_move = fen.get_side_to_move(); diff --git a/src/board_builder.rs b/src/board_builder.rs index da2f67953..d52f24001 100644 --- a/src/board_builder.rs +++ b/src/board_builder.rs @@ -259,6 +259,10 @@ impl BoardBuilder { self.en_passant = file; self } + + pub(crate) fn pieces(&self) -> &[Option<(Piece, Color)>; 64] { + &self.pieces + } } impl Index for BoardBuilder { From ba1db23b52d05d99e4fcc06f5d099296e8509268 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 7 Jul 2021 20:55:59 +0200 Subject: [PATCH 2/6] Add more (safety) comments, rename methods and do not use unsafe in non-performance critical parts. --- src/bitboard.rs | 28 ++++++++++- src/board.rs | 130 +++++++++++++++++++++++++++++++++++++----------- 2 files changed, 128 insertions(+), 30 deletions(-) diff --git a/src/bitboard.rs b/src/bitboard.rs index 4d90dcc75..5a26e06cd 100644 --- a/src/bitboard.rs +++ b/src/bitboard.rs @@ -302,7 +302,7 @@ impl BitBoard { /// Count the number of `Squares` set in this `BitBoard` #[inline] - pub fn popcnt(&self) -> u32 { + pub const fn popcnt(&self) -> u32 { self.0.count_ones() } @@ -317,6 +317,32 @@ impl BitBoard { pub fn to_size(&self, rightshift: u8) -> usize { (self.0 >> rightshift) as usize } + + pub(crate) const fn are_disjoint(boards: &[Self]) -> bool { + // trades readability for const + let mut disjoint = true; + let mut i = 0; + while i + 1 < boards.len() { + let mut j = i + 1; + while j < boards.len() { + disjoint |= boards[i].0 & boards[j].0 == EMPTY.0; + j += 1; + } + i += 1; + } + disjoint + } + + pub(crate) const fn combine(boards: &[Self]) -> BitBoard { + // trades readability for const + let mut uni = EMPTY; + let mut i = 0; + while i < boards.len() { + uni.0 |= boards[i].0; + i += 1; + } + uni + } } /// For the `BitBoard`, iterate over every `Square` set. diff --git a/src/board.rs b/src/board.rs index 040b25b1e..51901c0b3 100644 --- a/src/board.rs +++ b/src/board.rs @@ -31,8 +31,10 @@ mod board_pieces { /// - always consistent pieces, combined and color_combined /// - Number of pieces per player always <= MAX_PIECES_PER_COLOR /// - /// BoardPieces must only be modified through the unsafe functions to mark places where - /// that happens. `PieceHandle` and friends provide a safe interface. + /// Unsafe code can rely on these invariants. BoardPieces must only be modified through the + /// unsafe functions to mark places where that happens. `PieceHandle` and friends provide a safe + /// interface. Use the `grab_*` methods to obtain them. You can understand "grab" here as a + /// domain specific "lock" replacement. #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub struct BoardPieces { pieces: [BitBoard; NUM_PIECES], @@ -49,6 +51,16 @@ mod board_pieces { } } + // this function must ALWAYS return true. Otherwise it might result in unsound code + // elsewhere. + const fn is_consistent(&self) -> bool { + BitBoard::combine(&self.pieces).0 == self.combined.0 && + BitBoard::combine(&self.color_combined).0 == self.combined.0 && + BitBoard::are_disjoint(&self.pieces) && BitBoard::are_disjoint(&self.color_combined) && + self.color_combined[0].popcnt() <= MAX_PIECES_PER_COLOR && + self.color_combined[1].popcnt() <= MAX_PIECES_PER_COLOR + } + #[inline(always)] pub fn pieces(&self, piece: Piece) -> &BitBoard { &self.pieces[piece.to_index()] @@ -66,6 +78,8 @@ mod board_pieces { #[inline] pub fn piece_on(&self, square: Square) -> Option { + debug_assert!(self.is_consistent()); + let opp = BitBoard::from_square(square); if self.combined() & opp == EMPTY { @@ -107,6 +121,9 @@ mod board_pieces { /// - dst must be empty #[inline(always)] unsafe fn move_unchecked(&mut self, src: Square, dst: Square, piece: Piece, color: Color) { + debug_assert!(self.is_consistent()); + debug_assert!(self.grab_known_piece_handle(src, color, piece).is_some()); + let dst_bb = BitBoard::from_square(dst); let src_bb = BitBoard::from_square(src); let move_bb = src_bb | dst_bb; @@ -114,14 +131,21 @@ mod board_pieces { self.pieces[piece.to_index()] ^= move_bb; self.color_combined[color.to_index()] ^= move_bb; self.combined ^= move_bb; + + debug_assert!(self.is_consistent()); } /// SAFETY: There must be a piece of type old at given square #[inline(always)] unsafe fn change_piece_unchecked(&mut self, square: Square, old: Piece, new: Piece) { + debug_assert!(self.is_consistent()); + debug_assert_eq!(self.piece_on(square), Some(old)); + let bb = BitBoard::from_square(square); self.pieces[old.to_index()] ^= bb; self.pieces[new.to_index()] ^= bb; + + debug_assert!(self.is_consistent()); } /// SAFETY: @@ -129,9 +153,15 @@ mod board_pieces { /// - total number of pieces of target color must not exceed MAX_PIECES_PER_COLOR #[inline(always)] unsafe fn toggle_color_unchecked(&mut self, square: Square) { + debug_assert!(self.is_consistent()); + debug_assert!(self.grab_piece_handle(square).is_some()); + let bb = BitBoard::from_square(square); self.color_combined[0] ^= bb; self.color_combined[1] ^= bb; + + // also checks color counts + debug_assert!(self.is_consistent()); } /// SAFETY: @@ -140,10 +170,14 @@ mod board_pieces { /// - Other wise the square must be occupied by the given piece and color #[inline(always)] unsafe fn toggle_piece_unchecked(&mut self, square: Square, piece: Piece, color: Color) { + debug_assert!(self.is_consistent()); + let bb = BitBoard::from_square(square); self.pieces[piece.to_index()] ^= bb; self.color_combined[color.to_index()] ^= bb; self.combined ^= bb; + + debug_assert!(self.is_consistent()); } /// Returns the correct color if the square is occupied @@ -157,7 +191,7 @@ mod board_pieces { /// Return a PieceHandle to the piece at the given square if there is one #[inline] - pub fn get_piece_handle(&mut self, square: Square) -> Option { + pub fn grab_piece_handle(&mut self, square: Square) -> Option { let piece = self.piece_on(square)?; let color = self.color_of_occupied(square); Some(PieceHandle{ @@ -168,9 +202,10 @@ mod board_pieces { }) } - /// Return a PieceHandle to the piece at the given square if it has the given color and type + /// Return a PieceHandle to the piece at the given square if it has the given color and + /// type. This is faster than getting the piece handle just by square. #[inline(always)] - pub fn get_known_piece_handle(&mut self, square: Square, color: Color, piece: Piece) -> Option { + pub fn grab_known_piece_handle(&mut self, square: Square, color: Color, piece: Piece) -> Option { let bb = BitBoard::from_square(square); if self.pieces(piece) & bb == EMPTY || self.color_combined(color) & bb == EMPTY { None @@ -186,7 +221,7 @@ mod board_pieces { /// Return a handle to the square. Inspired by HashMap's entry API #[inline] - pub fn get_square_handle(&mut self, square: Square) -> SquareHandle { + pub fn grab_square_handle(&mut self, square: Square) -> SquareHandle { if let Some(piece) = self.piece_on(square) { let color = self.color_of_occupied(square); SquareHandle::Occupied(PieceHandle{ @@ -202,28 +237,51 @@ mod board_pieces { }) } } + + /// Return a handle to an empty square + #[inline] + pub fn grab_empty_square(&mut self, square: Square) -> Option { + if self.combined & BitBoard::from_square(square) == EMPTY { + Some(EmptySquare{ + board_pieces: self, + square + }) + } else { + None + } + } } impl TryFrom<[Option<(Piece, Color)>; 64]> for BoardPieces { type Error = Error; fn try_from(pieces: [Option<(Piece, Color)>; 64]) -> Result { - let black = pieces.iter().filter(|f| matches!(f, Some((_, Color::Black)) )).count(); - let white = pieces.iter().filter(|f| matches!(f, Some((_, Color::White)) )).count(); - if black as u32 > MAX_PIECES_PER_COLOR || white as u32 > MAX_PIECES_PER_COLOR { - Err(Error::InvalidBoard) - } else { - let mut result = Self::new(); - for (sq, (piece, color)) in pieces.iter().zip(ALL_SQUARES.iter()).filter_map(|(f, &sq)| f.map(|f| (sq, f))) { - unsafe { - result.toggle_piece_unchecked(sq, piece, color) - } + let mut board_pieces = Self::new(); + + let mut color_counts = [0u32; NUM_COLORS]; + + for (sq, (piece, color)) in pieces.iter().zip(ALL_SQUARES.iter()).filter_map(|(f, &sq)| f.map(|f| (sq, f))) { + color_counts[color.to_index()] += 1; + if color_counts[color.to_index()] >= MAX_PIECES_PER_COLOR { + return Err(Error::InvalidBoard); } - Ok(result) + + // this could be replaced by unsafe code because + // a. there can be no double occupations due to the input format + // b. we checked the color count before hand + let placed = board_pieces.grab_empty_square(sq) + .expect("Square not empty. This is a bug") + .place_piece(piece, color); + + // rather panic than silently dropping an internal logic error + assert!(placed.is_ok()); } + + Ok(board_pieces) } } /// This handle guarantees that it points to an occupied square with the given piece and color + /// It provides a safe interface to modify `BoardPieces`. pub struct PieceHandle<'a> { board_pieces: &'a mut BoardPieces, square: Square, @@ -254,6 +312,8 @@ mod board_pieces { assert_eq!(self.board_pieces.combined & dst_bb, EMPTY); unsafe { + // SAFETY: PieceHandle always points to an occupied field and we asserted that + // the destination is empty. self.board_pieces.move_unchecked(self.square, dst, self.piece, self.color) } self.square = dst; @@ -266,13 +326,14 @@ mod board_pieces { if let Some(captured_piece) = self.board_pieces.piece_on(dst) { let captured_color = self.board_pieces.color_of_occupied(dst); unsafe { + // SAFETY: we known that the piece we are about to capture is there self.board_pieces.toggle_piece_unchecked(dst, captured_piece, captured_color); } captured = Some((captured_piece, captured_color)); } unsafe { - // the destination is empty because we removed the captured piece + // SAFETY: the destination is empty because we removed the captured piece self.board_pieces.move_unchecked(self.square, dst, self.piece, self.color) } @@ -282,18 +343,21 @@ mod board_pieces { #[inline] pub fn replace_piece(&mut self, new: Piece) { unsafe { + // SAFETY: self is always a valid piece self.board_pieces.change_piece_unchecked(self.square, self.piece, new); } self.piece = new; } - #[inline] - pub fn set_color(mut self, color: Color) -> Result { + /// Replace color with the requested one. Fails if there is already the maximum number of + /// pieces of the requested color. + pub fn with_color(self, color: Color) -> Result { if self.color == color { Ok(self) } else { if self.board_pieces.color_combined(color).popcnt() + 1 <= MAX_PIECES_PER_COLOR { unsafe { + // SAFETY: self is always a valid piece and we just checked the color count self.board_pieces.toggle_color_unchecked(self.square); Ok(self) } @@ -304,8 +368,9 @@ mod board_pieces { } #[inline] - pub fn remove(mut self) -> EmptySquare<'a> { + pub fn remove(self) -> EmptySquare<'a> { unsafe { + // SAFETY: self is always a valid piece self.board_pieces.toggle_piece_unchecked(self.square, self.piece, self.color); } EmptySquare { @@ -321,9 +386,14 @@ mod board_pieces { } impl<'a> EmptySquare<'a> { - pub fn place_piece(mut self, piece: Piece, color: Color) -> Result, EmptySquare<'a>> { + /// Places a piece of the given type and color at the square. + /// + /// Returns a `PieceHandle` on success and an unchaged `EmptySquare` if there is + /// already the maximum number of pieces of the requested color. + pub fn place_piece(self, piece: Piece, color: Color) -> Result, EmptySquare<'a>> { if self.board_pieces.color_combined(color).popcnt() + 1 <= MAX_PIECES_PER_COLOR { unsafe { + // SAFETY: self is always empty and we just checked the max color count self.board_pieces.toggle_piece_unchecked(self.square, piece, color); } Ok(PieceHandle { @@ -344,10 +414,12 @@ mod board_pieces { } impl<'a> SquareHandle<'a> { - pub fn set_piece(self, color: Color, piece: Piece) -> Result, SquareHandle<'a>> { + /// places a piece of the given color and type at the square. Fails if there is + /// already the maximum number of pieces of the requested color. + pub fn place_piece(self, color: Color, piece: Piece) -> Result, SquareHandle<'a>> { match self { SquareHandle::Occupied(piece_handle) => { - match piece_handle.set_color(color) { + match piece_handle.with_color(color) { Ok(mut piece_handle) => { piece_handle.replace_piece(piece); Ok(piece_handle) @@ -795,7 +867,7 @@ impl Board { pub fn set_piece(&self, piece: Piece, color: Color, square: Square) -> Option { let mut result = *self; - if result.board_pieces.get_square_handle(square).set_piece(color, piece).is_err() { + if result.board_pieces.grab_square_handle(square).place_piece(color, piece).is_err() { // too many pieces of this color return None; } @@ -834,7 +906,7 @@ impl Board { #[inline] pub fn clear_square(&self, square: Square) -> Option { let mut result = *self; - match result.board_pieces.get_piece_handle(square) { + match result.board_pieces.grab_piece_handle(square) { None => {} Some(x) => { // remove x from the bitboard @@ -1200,7 +1272,7 @@ impl Board { // extra scope to signal that piece_handle is dropped latest at the end { - let mut piece_handle = result.board_pieces.get_piece_handle(source).unwrap(); + let mut piece_handle = result.board_pieces.grab_piece_handle(source).unwrap(); assert_eq!(self.side_to_move, piece_handle.color()); if let Some((_captured_piece, _captured_color)) = piece_handle.move_and_capture(dest) { @@ -1248,7 +1320,7 @@ impl Board { drop(piece_handle); // remove opponents en passant pawn - result.board_pieces.get_known_piece_handle(self.en_passant.unwrap(), !result.side_to_move, Piece::Pawn).unwrap().remove(); + result.board_pieces.grab_known_piece_handle(self.en_passant.unwrap(), !result.side_to_move, Piece::Pawn).unwrap().remove(); result.checkers ^= get_pawn_attacks(ksq, !result.side_to_move, dest_bb); } else { @@ -1266,7 +1338,7 @@ impl Board { *CASTLE_ROOK_END.get_unchecked(index) }); - let mut rook = result.board_pieces.get_known_piece_handle(start.to_square(), self.side_to_move, Piece::Rook).unwrap(); + let mut rook = result.board_pieces.grab_known_piece_handle(start.to_square(), self.side_to_move, Piece::Rook).unwrap(); rook.move_to(end.to_square()); } } From 090fd061abf6782dc93d1ecf6a6ff577fafdfb64 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 7 Jul 2021 21:00:43 +0200 Subject: [PATCH 3/6] Use MAX_PIECES_FOR_COLOR for move list length --- src/board.rs | 3 ++- src/movegen/movegen.rs | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/board.rs b/src/board.rs index 51901c0b3..e77c7e636 100644 --- a/src/board.rs +++ b/src/board.rs @@ -23,7 +23,7 @@ use std::str::FromStr; mod board_pieces { use super::*; - const MAX_PIECES_PER_COLOR: u32 = 16; + pub const MAX_PIECES_PER_COLOR: u32 = 16; /// Internal representation of "physical" board state /// @@ -436,6 +436,7 @@ mod board_pieces { } use board_pieces::BoardPieces; +pub(crate) use board_pieces::MAX_PIECES_PER_COLOR; /// A representation of a chess board. That's why you're here, right? #[derive(Copy, Clone, PartialEq, Eq, Debug)] diff --git a/src/movegen/movegen.rs b/src/movegen/movegen.rs index 8706bc38f..e5a82831b 100644 --- a/src/movegen/movegen.rs +++ b/src/movegen/movegen.rs @@ -1,5 +1,5 @@ use crate::bitboard::{BitBoard, EMPTY}; -use crate::board::Board; +use crate::board::{Board, MAX_PIECES_PER_COLOR}; use crate::chess_move::ChessMove; use crate::magic::between; use crate::movegen::piece_type::*; @@ -27,7 +27,10 @@ impl SquareAndBitBoard { } } -pub type MoveList = NoDrop>; +/// MAX_PIECES_PER_COLOR + 2 for the en passant moves +const MAX_MOVE_LIST_LEN: usize = MAX_PIECES_PER_COLOR as usize + 2; + +pub type MoveList = NoDrop>; /// An incremental move generator /// From 46b80d5096607526c4e5f4fbbbf8b03480a78a5a Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 8 Jul 2021 01:11:37 +0200 Subject: [PATCH 4/6] Fix bugs in board_pieces --- src/board.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/board.rs b/src/board.rs index e77c7e636..24d34a60c 100644 --- a/src/board.rs +++ b/src/board.rs @@ -261,7 +261,7 @@ mod board_pieces { for (sq, (piece, color)) in pieces.iter().zip(ALL_SQUARES.iter()).filter_map(|(f, &sq)| f.map(|f| (sq, f))) { color_counts[color.to_index()] += 1; - if color_counts[color.to_index()] >= MAX_PIECES_PER_COLOR { + if color_counts[color.to_index()] > MAX_PIECES_PER_COLOR { return Err(Error::InvalidBoard); } @@ -314,9 +314,9 @@ mod board_pieces { unsafe { // SAFETY: PieceHandle always points to an occupied field and we asserted that // the destination is empty. - self.board_pieces.move_unchecked(self.square, dst, self.piece, self.color) + self.board_pieces.move_unchecked(self.square, dst, self.piece, self.color); + self.square = dst; } - self.square = dst; } #[inline] @@ -334,7 +334,8 @@ mod board_pieces { unsafe { // SAFETY: the destination is empty because we removed the captured piece - self.board_pieces.move_unchecked(self.square, dst, self.piece, self.color) + self.board_pieces.move_unchecked(self.square, dst, self.piece, self.color); + self.square = dst; } captured @@ -345,13 +346,13 @@ mod board_pieces { unsafe { // SAFETY: self is always a valid piece self.board_pieces.change_piece_unchecked(self.square, self.piece, new); + self.piece = new; } - self.piece = new; } /// Replace color with the requested one. Fails if there is already the maximum number of /// pieces of the requested color. - pub fn with_color(self, color: Color) -> Result { + pub fn with_color(mut self, color: Color) -> Result { if self.color == color { Ok(self) } else { @@ -359,8 +360,9 @@ mod board_pieces { unsafe { // SAFETY: self is always a valid piece and we just checked the color count self.board_pieces.toggle_color_unchecked(self.square); - Ok(self) + self.color = color; } + Ok(self) } else { Err(self) } From e301d959a4055ef5a42424b2251812ae8ac79140 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 8 Jul 2021 01:14:26 +0200 Subject: [PATCH 5/6] Implement hash correctly and include it in board_pieces --- src/board.rs | 41 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/src/board.rs b/src/board.rs index 24d34a60c..8ce74d853 100644 --- a/src/board.rs +++ b/src/board.rs @@ -40,6 +40,7 @@ mod board_pieces { pieces: [BitBoard; NUM_PIECES], color_combined: [BitBoard; NUM_COLORS], combined: BitBoard, + hash: u64 } impl BoardPieces { @@ -48,6 +49,7 @@ mod board_pieces { pieces: [EMPTY; NUM_PIECES], color_combined: [EMPTY; NUM_COLORS], combined: EMPTY, + hash: 0 } } @@ -76,6 +78,24 @@ mod board_pieces { &self.color_combined[color.to_index()] } + #[inline(always)] + pub fn hash(&self) -> u64 { + debug_assert_eq!(self.hash, self.calc_hash()); + self.hash + } + + fn calc_hash(&self) -> u64 { + let mut hash = 0; + for piece in ALL_PIECES { + for color in ALL_COLORS { + for square in self.color_combined[color.to_index()] & self.pieces[piece.to_index()] { + hash ^= Zobrist::piece(piece, square, color); + } + } + } + hash + } + #[inline] pub fn piece_on(&self, square: Square) -> Option { debug_assert!(self.is_consistent()); @@ -131,6 +151,8 @@ mod board_pieces { self.pieces[piece.to_index()] ^= move_bb; self.color_combined[color.to_index()] ^= move_bb; self.combined ^= move_bb; + self.hash ^= Zobrist::piece(piece, src, color); + self.hash ^= Zobrist::piece(piece, dst, color); debug_assert!(self.is_consistent()); } @@ -145,6 +167,10 @@ mod board_pieces { self.pieces[old.to_index()] ^= bb; self.pieces[new.to_index()] ^= bb; + let color = self.color_of_occupied(square); + self.hash ^= Zobrist::piece(old, square, color); + self.hash ^= Zobrist::piece(new, square, color); + debug_assert!(self.is_consistent()); } @@ -160,6 +186,13 @@ mod board_pieces { self.color_combined[0] ^= bb; self.color_combined[1] ^= bb; + // we require the piece for adjusting the hash + // One could use unsafe here but color changing is not a function used very often... + let piece = self.piece_on(square).unwrap(); + + self.hash ^= Zobrist::piece(piece, square, Color::Black); + self.hash ^= Zobrist::piece(piece, square, Color::White); + // also checks color counts debug_assert!(self.is_consistent()); } @@ -177,6 +210,8 @@ mod board_pieces { self.color_combined[color.to_index()] ^= bb; self.combined ^= bb; + self.hash ^= Zobrist::piece(piece, square, color); + debug_assert!(self.is_consistent()); } @@ -448,7 +483,6 @@ pub struct Board { castle_rights: [CastleRights; NUM_COLORS], pinned: BitBoard, checkers: BitBoard, - hash: u64, en_passant: Option, } @@ -471,7 +505,7 @@ impl Default for Board { impl Hash for Board { fn hash(&self, state: &mut H) { - self.hash.hash(state); + self.board_pieces.hash().hash(state); } } @@ -485,7 +519,6 @@ impl Board { castle_rights: [CastleRights::NoRights; NUM_COLORS], pinned: EMPTY, checkers: EMPTY, - hash: 0, en_passant: None, } } @@ -1074,7 +1107,7 @@ impl Board { /// Get a hash of the board. #[inline] pub fn get_hash(&self) -> u64 { - self.hash + self.board_pieces.hash() ^ if let Some(ep) = self.en_passant { Zobrist::en_passant(ep.get_file(), !self.side_to_move) } else { From a8d0ad292244ae469160894d512586dd7a6233a0 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 8 Jul 2021 01:17:24 +0200 Subject: [PATCH 6/6] Flag PieceType::legals as unsafe and document its soundness requirements --- src/movegen/movegen.rs | 47 +++++++++++-------- src/movegen/piece_type.rs | 97 ++++++++++++++++++--------------------- 2 files changed, 74 insertions(+), 70 deletions(-) diff --git a/src/movegen/movegen.rs b/src/movegen/movegen.rs index e5a82831b..8d1fbb407 100644 --- a/src/movegen/movegen.rs +++ b/src/movegen/movegen.rs @@ -94,28 +94,39 @@ pub struct MoveGen { } impl MoveGen { + /// panics if there is not exactly one king for the moving side #[inline(always)] fn enumerate_moves(board: &Board) -> MoveList { let checkers = *board.checkers(); let mask = !board.color_combined(board.side_to_move()); - let mut movelist = NoDrop::new(ArrayVec::<[SquareAndBitBoard; 18]>::new()); - - if checkers == EMPTY { - PawnType::legals::(&mut movelist, &board, mask); - KnightType::legals::(&mut movelist, &board, mask); - BishopType::legals::(&mut movelist, &board, mask); - RookType::legals::(&mut movelist, &board, mask); - QueenType::legals::(&mut movelist, &board, mask); - KingType::legals::(&mut movelist, &board, mask); - } else if checkers.popcnt() == 1 { - PawnType::legals::(&mut movelist, &board, mask); - KnightType::legals::(&mut movelist, &board, mask); - BishopType::legals::(&mut movelist, &board, mask); - RookType::legals::(&mut movelist, &board, mask); - QueenType::legals::(&mut movelist, &board, mask); - KingType::legals::(&mut movelist, &board, mask); - } else { - KingType::legals::(&mut movelist, &board, mask); + let mut movelist = NoDrop::new(ArrayVec::<[SquareAndBitBoard; MAX_MOVE_LIST_LEN]>::new()); + + assert_eq!((board.pieces(Piece::King) & board.color_combined(board.side_to_move())).popcnt(), 1); + assert_eq!(PawnType::EXTRA_MOVES + KnightType::EXTRA_MOVES + BishopType::EXTRA_MOVES + RookType::EXTRA_MOVES + QueenType::EXTRA_MOVES + KingType::EXTRA_MOVES + MAX_PIECES_PER_COLOR as usize, + movelist.capacity()); + unsafe { + // SAFETY: + // Board guarantees that there are maximally MAX_PIECES_PER_COLOR pieces per color + // We assert above that there is exactly one king + // We assert that the move list is long enough + + if checkers == EMPTY { + PawnType::legals::(&mut movelist, &board, mask); + KnightType::legals::(&mut movelist, &board, mask); + BishopType::legals::(&mut movelist, &board, mask); + RookType::legals::(&mut movelist, &board, mask); + QueenType::legals::(&mut movelist, &board, mask); + KingType::legals::(&mut movelist, &board, mask); + } else if checkers.popcnt() == 1 { + PawnType::legals::(&mut movelist, &board, mask); + KnightType::legals::(&mut movelist, &board, mask); + BishopType::legals::(&mut movelist, &board, mask); + RookType::legals::(&mut movelist, &board, mask); + QueenType::legals::(&mut movelist, &board, mask); + KingType::legals::(&mut movelist, &board, mask); + } else { + KingType::legals::(&mut movelist, &board, mask); + } } movelist diff --git a/src/movegen/piece_type.rs b/src/movegen/piece_type.rs index d4e456262..57bbdf0aa 100644 --- a/src/movegen/piece_type.rs +++ b/src/movegen/piece_type.rs @@ -11,13 +11,19 @@ use crate::magic::{ line, }; + +/// Helper trait to generate moves for different pieces. pub trait PieceType { - fn is(piece: Piece) -> bool; - fn into_piece() -> Piece; - #[inline(always)] + const PIECE: Piece; + const EXTRA_MOVES: usize = 0; + fn pseudo_legals(src: Square, color: Color, combined: BitBoard, mask: BitBoard) -> BitBoard; + #[inline(always)] - fn legals(movelist: &mut MoveList, board: &Board, mask: BitBoard) + /// Safety: + /// movelist.remaining_capacity() >= (board.pieces(Self::PIECE) & board.color_combined(board.side_to_move())).popcnt() + if Self::EXTRA_MOVES + /// (board.pieces(Piece::King) & board.color_combined(board.side_to_move())).popcnt() == 1 + unsafe fn legals(movelist: &mut MoveList, board: &Board, mask: BitBoard) where T: CheckType, { @@ -26,7 +32,8 @@ pub trait PieceType { let my_pieces = board.color_combined(color); let ksq = board.king_square(color); - let pieces = board.pieces(Self::into_piece()) & my_pieces; + // SAFETY: we can add a move for each bit set here by requirement + let pieces = board.pieces(Self::PIECE) & my_pieces; let pinned = board.pinned(); let checkers = board.checkers(); @@ -40,6 +47,8 @@ pub trait PieceType { let moves = Self::pseudo_legals(src, color, *combined, mask) & check_mask; if moves != EMPTY { unsafe { + // SAFETY: Unpinned subset of pieces + debug_assert_ne!(movelist.remaining_capacity(), 0); movelist.push_unchecked(SquareAndBitBoard::new(src, moves, false)); } } @@ -50,6 +59,8 @@ pub trait PieceType { let moves = Self::pseudo_legals(src, color, *combined, mask) & line(src, ksq); if moves != EMPTY { unsafe { + // SAFETY: Pinned subset of pieces (disjoint to previous) + debug_assert_ne!(movelist.remaining_capacity(), 0); movelist.push_unchecked(SquareAndBitBoard::new(src, moves, false)); } } @@ -114,13 +125,8 @@ impl PawnType { } impl PieceType for PawnType { - fn is(piece: Piece) -> bool { - piece == Piece::Pawn - } - - fn into_piece() -> Piece { - Piece::Pawn - } + const PIECE: Piece = Piece::Pawn; + const EXTRA_MOVES: usize = 2; #[inline(always)] fn pseudo_legals(src: Square, color: Color, combined: BitBoard, mask: BitBoard) -> BitBoard { @@ -128,7 +134,7 @@ impl PieceType for PawnType { } #[inline(always)] - fn legals(movelist: &mut MoveList, board: &Board, mask: BitBoard) + unsafe fn legals(movelist: &mut MoveList, board: &Board, mask: BitBoard) where T: CheckType, { @@ -137,7 +143,7 @@ impl PieceType for PawnType { let my_pieces = board.color_combined(color); let ksq = board.king_square(color); - let pieces = board.pieces(Self::into_piece()) & my_pieces; + let pieces = board.pieces(Self::PIECE) & my_pieces; let pinned = board.pinned(); let checkers = board.checkers(); @@ -151,6 +157,8 @@ impl PieceType for PawnType { let moves = Self::pseudo_legals(src, color, *combined, mask) & check_mask; if moves != EMPTY { unsafe { + debug_assert_ne!(movelist.remaining_capacity(), 0); + // SAFETY: unpinned subset of pieces movelist.push_unchecked(SquareAndBitBoard::new( src, moves, @@ -165,6 +173,8 @@ impl PieceType for PawnType { let moves = Self::pseudo_legals(src, color, *combined, mask) & line(ksq, src); if moves != EMPTY { unsafe { + debug_assert_ne!(movelist.remaining_capacity(), 0); + // SAFETY: pinned subset of pieces movelist.push_unchecked(SquareAndBitBoard::new( src, moves, @@ -179,10 +189,14 @@ impl PieceType for PawnType { let ep_sq = board.en_passant().unwrap(); let rank = get_rank(ep_sq.get_rank()); let files = get_adjacent_files(ep_sq.get_file()); + // (files & rank).popcnt() <= 2 for src in rank & files & pieces { let dest = ep_sq.uforward(color); if PawnType::legal_ep_move(board, src, dest) { unsafe { + // SAFETY: we require two extra slots for en passant moves. + // There are max two values for src + debug_assert_ne!(movelist.remaining_capacity(), 0); movelist.push_unchecked(SquareAndBitBoard::new( src, BitBoard::from_square(dest), @@ -196,13 +210,7 @@ impl PieceType for PawnType { } impl PieceType for BishopType { - fn is(piece: Piece) -> bool { - piece == Piece::Bishop - } - - fn into_piece() -> Piece { - Piece::Bishop - } + const PIECE: Piece = Piece::Bishop; #[inline(always)] fn pseudo_legals(src: Square, _color: Color, combined: BitBoard, mask: BitBoard) -> BitBoard { @@ -211,13 +219,7 @@ impl PieceType for BishopType { } impl PieceType for KnightType { - fn is(piece: Piece) -> bool { - piece == Piece::Knight - } - - fn into_piece() -> Piece { - Piece::Knight - } + const PIECE: Piece = Piece::Knight; #[inline(always)] fn pseudo_legals(src: Square, _color: Color, _combined: BitBoard, mask: BitBoard) -> BitBoard { @@ -225,7 +227,7 @@ impl PieceType for KnightType { } #[inline(always)] - fn legals(movelist: &mut MoveList, board: &Board, mask: BitBoard) + unsafe fn legals(movelist: &mut MoveList, board: &Board, mask: BitBoard) where T: CheckType, { @@ -234,7 +236,9 @@ impl PieceType for KnightType { let my_pieces = board.color_combined(color); let ksq = board.king_square(color); - let pieces = board.pieces(Self::into_piece()) & my_pieces; + // we can add a move for each set bit in pieces + let pieces = board.pieces(Self::PIECE) & my_pieces; + let pinned = board.pinned(); let checkers = board.checkers(); @@ -245,6 +249,8 @@ impl PieceType for KnightType { let moves = Self::pseudo_legals(src, color, *combined, mask & check_mask); if moves != EMPTY { unsafe { + // SAFETY: moves for subset of pieces + debug_assert_ne!(movelist.remaining_capacity(), 0); movelist.push_unchecked(SquareAndBitBoard::new(src, moves, false)); } } @@ -254,6 +260,8 @@ impl PieceType for KnightType { let moves = Self::pseudo_legals(src, color, *combined, mask); if moves != EMPTY { unsafe { + // SAFETY: moves for subset of pieces + debug_assert_ne!(movelist.remaining_capacity(), 0); movelist.push_unchecked(SquareAndBitBoard::new(src, moves, false)); } } @@ -263,13 +271,7 @@ impl PieceType for KnightType { } impl PieceType for RookType { - fn is(piece: Piece) -> bool { - piece == Piece::Rook - } - - fn into_piece() -> Piece { - Piece::Rook - } + const PIECE: Piece = Piece::Rook; #[inline(always)] fn pseudo_legals(src: Square, _color: Color, combined: BitBoard, mask: BitBoard) -> BitBoard { @@ -278,13 +280,7 @@ impl PieceType for RookType { } impl PieceType for QueenType { - fn is(piece: Piece) -> bool { - piece == Piece::Queen - } - - fn into_piece() -> Piece { - Piece::Queen - } + const PIECE: Piece = Piece::Queen; #[inline(always)] fn pseudo_legals(src: Square, _color: Color, combined: BitBoard, mask: BitBoard) -> BitBoard { @@ -331,13 +327,7 @@ impl KingType { } impl PieceType for KingType { - fn is(piece: Piece) -> bool { - piece == Piece::King - } - - fn into_piece() -> Piece { - Piece::King - } + const PIECE: Piece = Piece::King; #[inline(always)] fn pseudo_legals(src: Square, _color: Color, _combined: BitBoard, mask: BitBoard) -> BitBoard { @@ -345,7 +335,7 @@ impl PieceType for KingType { } #[inline(always)] - fn legals(movelist: &mut MoveList, board: &Board, mask: BitBoard) + unsafe fn legals(movelist: &mut MoveList, board: &Board, mask: BitBoard) where T: CheckType, { @@ -397,6 +387,9 @@ impl PieceType for KingType { } if moves != EMPTY { unsafe { + // SAFETY: we require that there is exactly one king of our color -> we have the + // space to add his moves + debug_assert_ne!(movelist.remaining_capacity(), 0); movelist.push_unchecked(SquareAndBitBoard::new(ksq, moves, false)); } }