From 388c26f4ac6e005f4e844e2b35e91859a017b75d Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 3 Apr 2024 20:00:57 +0100 Subject: [PATCH 1/8] Use 'writeln' in magic seed generation --- src/movegen/wizardry/mod.rs | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs index 00645d8..f2794aa 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -197,6 +197,8 @@ pub(crate) const ROOK_SEED: [u64; 64] = [ #[cfg(test)] mod test { + use std::fmt::Write as _; + use super::*; // A simple XOR-shift RNG implementation. @@ -238,20 +240,25 @@ mod test { Some((prefix, mid, suffix)) } - fn array_string(piece_type: &str, values: &[Magic]) -> String { - let mut res = format!( - "/// A set of magic numbers for {} move generation.\n", + fn array_string(piece_type: &str, values: &[Magic]) -> Result { + let mut res = String::new(); + + writeln!( + &mut res, + "/// A set of magic numbers for {} move generation.", piece_type - ); - res.push_str(&format!( - "pub(crate) const {}_SEED: [u64; 64] = [\n", + )?; + writeln!( + &mut res, + "pub(crate) const {}_SEED: [u64; 64] = [", piece_type.to_uppercase() - )); + )?; for magic in values { - res.push_str(&format!(" {},\n", magic.magic)); + writeln!(&mut res, " {},", magic.magic)?; } - res.push_str("];\n"); - res + writeln!(&mut res, "];")?; + + Ok(res) } #[test] @@ -264,8 +271,8 @@ mod test { let original_text = std::fs::read_to_string(file!()).unwrap(); - let bishop_array = array_string("bishop", &bishop_magics[..]); - let rook_array = array_string("rook", &rook_magics[..]); + let bishop_array = array_string("bishop", &bishop_magics[..]).unwrap(); + let rook_array = array_string("rook", &rook_magics[..]).unwrap(); let new_text = { let start_marker = "// region:sourcegen\n"; From 6f161d067d577a1f4ae2a986a9f013b70a708608 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 3 Apr 2024 20:07:50 +0100 Subject: [PATCH 2/8] Use 'ChessBoardBuilder' in more validation tests --- src/board/chess_board/mod.rs | 116 +++++++++-------------------------- 1 file changed, 28 insertions(+), 88 deletions(-) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index 9ceb673..7fc43af 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -463,28 +463,13 @@ mod test { #[test] fn invalid_overlapping_pieces() { - let position = ChessBoard { - piece_occupancy: [ - // King - Square::E1 | Square::E8, - // Queen - Square::E1 | Square::E8, - // Rook - Bitboard::EMPTY, - // Bishop - Bitboard::EMPTY, - // Knight - Bitboard::EMPTY, - // Pawn - Bitboard::EMPTY, - ], - color_occupancy: [Square::E1.into_bitboard(), Square::E8.into_bitboard()], - combined_occupancy: Square::E1 | Square::E8, - castle_rights: [CastleRights::NoSide; 2], - en_passant: None, - half_move_clock: 0, - total_plies: 0, - side: Color::White, + let position = { + let mut builder = ChessBoardBuilder::new(); + builder[Square::E1] = Some((Piece::King, Color::White)); + builder[Square::E8] = Some((Piece::King, Color::Black)); + let mut board: ChessBoard = builder.try_into().unwrap(); + *board.piece_occupancy_mut(Piece::Queen) |= Square::E1.into_bitboard(); + board }; assert_eq!( position.validate().err().unwrap(), @@ -494,28 +479,13 @@ mod test { #[test] fn invalid_overlapping_colors() { - let position = ChessBoard { - piece_occupancy: [ - // King - Square::E1 | Square::E8, - // Queen - Bitboard::EMPTY, - // Rook - Bitboard::EMPTY, - // Bishop - Bitboard::EMPTY, - // Knight - Bitboard::EMPTY, - // Pawn - Bitboard::EMPTY, - ], - color_occupancy: [Square::E1 | Square::E8, Square::E1 | Square::E8], - combined_occupancy: Square::E1 | Square::E8, - castle_rights: [CastleRights::NoSide; 2], - en_passant: None, - half_move_clock: 0, - total_plies: 0, - side: Color::White, + let position = { + let mut builder = ChessBoardBuilder::new(); + builder[Square::E1] = Some((Piece::King, Color::White)); + builder[Square::E8] = Some((Piece::King, Color::Black)); + let mut board: ChessBoard = builder.try_into().unwrap(); + *board.color_occupancy_mut(Color::White) |= Square::E8.into_bitboard(); + board }; assert_eq!( position.validate().err().unwrap(), @@ -525,28 +495,13 @@ mod test { #[test] fn invalid_combined_does_not_equal_pieces() { - let position = ChessBoard { - piece_occupancy: [ - // King - Square::E1 | Square::E8, - // Queen - Bitboard::EMPTY, - // Rook - Bitboard::EMPTY, - // Bishop - Bitboard::EMPTY, - // Knight - Bitboard::EMPTY, - // Pawn - Bitboard::EMPTY, - ], - color_occupancy: [Square::E1.into_bitboard(), Square::E8.into_bitboard()], - combined_occupancy: Square::E1.into_bitboard(), - castle_rights: [CastleRights::NoSide; 2], - en_passant: None, - half_move_clock: 0, - total_plies: 0, - side: Color::White, + let position = { + let mut builder = ChessBoardBuilder::new(); + builder[Square::E1] = Some((Piece::King, Color::White)); + builder[Square::E8] = Some((Piece::King, Color::Black)); + let mut board: ChessBoard = builder.try_into().unwrap(); + *board.piece_occupancy_mut(Piece::Pawn) |= Square::E2.into_bitboard(); + board }; assert_eq!( position.validate().err().unwrap(), @@ -556,28 +511,13 @@ mod test { #[test] fn invalid_combined_does_not_equal_colors() { - let position = ChessBoard { - piece_occupancy: [ - // King - Square::E1 | Square::E8, - // Queen - Bitboard::EMPTY, - // Rook - Bitboard::EMPTY, - // Bishop - Bitboard::EMPTY, - // Knight - Bitboard::EMPTY, - // Pawn - Bitboard::EMPTY, - ], - color_occupancy: [Square::E1 | Square::H1, Square::E8 | Square::H8], - combined_occupancy: Square::E1 | Square::E8, - castle_rights: [CastleRights::NoSide; 2], - en_passant: None, - half_move_clock: 0, - total_plies: 0, - side: Color::White, + let position = { + let mut builder = ChessBoardBuilder::new(); + builder[Square::E1] = Some((Piece::King, Color::White)); + builder[Square::E8] = Some((Piece::King, Color::Black)); + let mut board: ChessBoard = builder.try_into().unwrap(); + *board.color_occupancy_mut(Color::Black) |= Square::E2.into_bitboard(); + board }; assert_eq!( position.validate().err().unwrap(), From 7d9c5edb994f9ba4b887471d83bd01b862ea77ef Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 3 Apr 2024 20:15:17 +0100 Subject: [PATCH 3/8] Rename 'ValidationError' This is a better, clearer name. --- src/board/chess_board/builder.rs | 4 +- src/board/chess_board/error.rs | 6 +-- src/board/chess_board/mod.rs | 70 ++++++++++++++++---------------- src/fen.rs | 10 ++--- 4 files changed, 45 insertions(+), 45 deletions(-) diff --git a/src/board/chess_board/builder.rs b/src/board/chess_board/builder.rs index d16c881..0304af5 100644 --- a/src/board/chess_board/builder.rs +++ b/src/board/chess_board/builder.rs @@ -1,4 +1,4 @@ -use crate::board::{Bitboard, CastleRights, ChessBoard, Color, InvalidError, Piece, Square}; +use crate::board::{Bitboard, CastleRights, ChessBoard, Color, Piece, Square, ValidationError}; /// Build a [ChessBoard] one piece at a time. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] @@ -80,7 +80,7 @@ impl std::ops::IndexMut for ChessBoardBuilder { } impl TryFrom for ChessBoard { - type Error = InvalidError; + type Error = ValidationError; fn try_from(builder: ChessBoardBuilder) -> Result { let mut piece_occupancy: [Bitboard; Piece::NUM_VARIANTS] = Default::default(); diff --git a/src/board/chess_board/error.rs b/src/board/chess_board/error.rs index 7b570a4..fe8e4c0 100644 --- a/src/board/chess_board/error.rs +++ b/src/board/chess_board/error.rs @@ -1,6 +1,6 @@ /// A singular type for all errors that could happen during [ChessBoard::is_valid]. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum InvalidError { +pub enum ValidationError { /// Too many pieces. TooManyPieces, /// Missing king. @@ -27,7 +27,7 @@ pub enum InvalidError { IncoherentPlieCount, } -impl std::fmt::Display for InvalidError { +impl std::fmt::Display for ValidationError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let error_msg = match self { Self::TooManyPieces => "Too many pieces.", @@ -53,4 +53,4 @@ impl std::fmt::Display for InvalidError { } } -impl std::error::Error for InvalidError {} +impl std::error::Error for ValidationError {} diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index 7fc43af..edf92a2 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -206,16 +206,16 @@ impl ChessBoard { self.validate().is_ok() } - /// Validate the state of the board. Return Err([InvalidError]) if an issue is found. - pub fn validate(&self) -> Result<(), InvalidError> { + /// Validate the state of the board. Return Err([ValidationError]) if an issue is found. + pub fn validate(&self) -> Result<(), ValidationError> { // The current plie count should be odd on white's turn, and vice-versa. if self.total_plies() % 2 != self.current_player().index() as u32 { - return Err(InvalidError::IncoherentPlieCount); + return Err(ValidationError::IncoherentPlieCount); } // Make sure the clocks are in agreement. if self.half_move_clock() > self.total_plies() { - return Err(InvalidError::HalfMoveClockTooHigh); + return Err(ValidationError::HalfMoveClockTooHigh); } // Don't overlap pieces. @@ -224,7 +224,7 @@ impl ChessBoard { for other in Piece::iter() { if piece != other { if !(self.piece_occupancy(piece) & self.piece_occupancy(other)).is_empty() { - return Err(InvalidError::OverlappingPieces); + return Err(ValidationError::OverlappingPieces); } } } @@ -232,7 +232,7 @@ impl ChessBoard { // Don't overlap colors. if !(self.color_occupancy(Color::White) & self.color_occupancy(Color::Black)).is_empty() { - return Err(InvalidError::OverlappingColors); + return Err(ValidationError::OverlappingColors); } // Calculate the union of all pieces. @@ -241,12 +241,12 @@ impl ChessBoard { // Ensure that the pre-computed version is accurate. if combined != self.combined_occupancy() { - return Err(InvalidError::ErroneousCombinedOccupancy); + return Err(ValidationError::ErroneousCombinedOccupancy); } // Ensure that all pieces belong to a color, and no color has pieces that don't exist. if combined != (self.color_occupancy(Color::White) | self.color_occupancy(Color::Black)) { - return Err(InvalidError::ErroneousCombinedOccupancy); + return Err(ValidationError::ErroneousCombinedOccupancy); } for color in Color::iter() { @@ -260,18 +260,18 @@ impl ChessBoard { _ => count <= 10, }; if !possible { - return Err(InvalidError::TooManyPieces); + return Err(ValidationError::TooManyPieces); } } // Check that we have a king if self.occupancy(Piece::King, color).count() != 1 { - return Err(InvalidError::MissingKing); + return Err(ValidationError::MissingKing); } // Check that don't have too many pieces in total if self.color_occupancy(color).count() > 16 { - return Err(InvalidError::TooManyPieces); + return Err(ValidationError::TooManyPieces); } } @@ -280,7 +280,7 @@ impl ChessBoard { & (Rank::First.into_bitboard() | Rank::Eighth.into_bitboard())) .is_empty() { - return Err(InvalidError::InvalidPawnPosition); + return Err(ValidationError::InvalidPawnPosition); } // Verify that rooks and kings that are allowed to castle have not been moved. @@ -296,14 +296,14 @@ impl ChessBoard { let expected_rooks = castle_rights.unmoved_rooks(color); // We must check the intersection, in case there are more than 2 rooks on the board. if (expected_rooks & actual_rooks) != expected_rooks { - return Err(InvalidError::InvalidCastlingRights); + return Err(ValidationError::InvalidCastlingRights); } let actual_king = self.occupancy(Piece::King, color); let expected_king = Square::new(File::E, color.first_rank()); // We have checked that there is exactly one king, no need for intersecting the sets. if actual_king != expected_king.into_bitboard() { - return Err(InvalidError::InvalidCastlingRights); + return Err(ValidationError::InvalidCastlingRights); } } @@ -311,14 +311,14 @@ impl ChessBoard { if let Some(square) = self.en_passant() { // Must be empty if !(self.combined_occupancy() & square).is_empty() { - return Err(InvalidError::InvalidEnPassant); + return Err(ValidationError::InvalidEnPassant); } let opponent = !self.current_player(); // Must be on the opponent's third rank if (square & opponent.third_rank().into_bitboard()).is_empty() { - return Err(InvalidError::InvalidEnPassant); + return Err(ValidationError::InvalidEnPassant); } // Must be behind a pawn @@ -328,7 +328,7 @@ impl ChessBoard { .backward_direction() .move_board(square.into_bitboard()); if (opponent_pawns & double_pushed_pawn).is_empty() { - return Err(InvalidError::InvalidEnPassant); + return Err(ValidationError::InvalidEnPassant); } } @@ -337,12 +337,12 @@ impl ChessBoard { let black_king = self.occupancy(Piece::King, Color::Black); // Unwrap is fine, we already checked that there is exactly one king of each color if !(movegen::king_moves(white_king.try_into().unwrap()) & black_king).is_empty() { - return Err(InvalidError::NeighbouringKings); + return Err(ValidationError::NeighbouringKings); } // Check that the opponent is not currently in check. if !self.compute_checkers(!self.current_player()).is_empty() { - return Err(InvalidError::OpponentInCheck); + return Err(ValidationError::OpponentInCheck); } Ok(()) @@ -445,7 +445,7 @@ mod test { }; assert_eq!( position.validate().err().unwrap(), - InvalidError::IncoherentPlieCount, + ValidationError::IncoherentPlieCount, ); } @@ -458,7 +458,7 @@ mod test { builder.with_half_move_clock(10); TryInto::::try_into(builder) }; - assert_eq!(res.err().unwrap(), InvalidError::HalfMoveClockTooHigh); + assert_eq!(res.err().unwrap(), ValidationError::HalfMoveClockTooHigh); } #[test] @@ -473,7 +473,7 @@ mod test { }; assert_eq!( position.validate().err().unwrap(), - InvalidError::OverlappingPieces, + ValidationError::OverlappingPieces, ); } @@ -489,7 +489,7 @@ mod test { }; assert_eq!( position.validate().err().unwrap(), - InvalidError::OverlappingColors, + ValidationError::OverlappingColors, ); } @@ -505,7 +505,7 @@ mod test { }; assert_eq!( position.validate().err().unwrap(), - InvalidError::ErroneousCombinedOccupancy, + ValidationError::ErroneousCombinedOccupancy, ); } @@ -521,7 +521,7 @@ mod test { }; assert_eq!( position.validate().err().unwrap(), - InvalidError::ErroneousCombinedOccupancy, + ValidationError::ErroneousCombinedOccupancy, ); } @@ -535,7 +535,7 @@ mod test { builder[Square::E8] = Some((Piece::King, Color::Black)); TryInto::::try_into(builder) }; - assert_eq!(res.err().unwrap(), InvalidError::TooManyPieces); + assert_eq!(res.err().unwrap(), ValidationError::TooManyPieces); } #[test] @@ -547,7 +547,7 @@ mod test { builder.with_castle_rights(CastleRights::BothSides, Color::White); TryInto::::try_into(builder) }; - assert_eq!(res.err().unwrap(), InvalidError::InvalidCastlingRights); + assert_eq!(res.err().unwrap(), ValidationError::InvalidCastlingRights); } #[test] @@ -563,7 +563,7 @@ mod test { builder.with_castle_rights(CastleRights::BothSides, Color::White); TryInto::::try_into(builder) }; - assert_eq!(res.err().unwrap(), InvalidError::InvalidCastlingRights); + assert_eq!(res.err().unwrap(), ValidationError::InvalidCastlingRights); } #[test] @@ -587,7 +587,7 @@ mod test { builder.with_en_passant(Square::A6); TryInto::::try_into(builder) }; - assert_eq!(res.err().unwrap(), InvalidError::InvalidEnPassant); + assert_eq!(res.err().unwrap(), ValidationError::InvalidEnPassant); } #[test] @@ -600,7 +600,7 @@ mod test { builder.with_en_passant(Square::A6); TryInto::::try_into(builder) }; - assert_eq!(res.err().unwrap(), InvalidError::InvalidEnPassant); + assert_eq!(res.err().unwrap(), ValidationError::InvalidEnPassant); } #[test] @@ -613,7 +613,7 @@ mod test { builder.with_en_passant(Square::A5); TryInto::::try_into(builder) }; - assert_eq!(res.err().unwrap(), InvalidError::InvalidEnPassant); + assert_eq!(res.err().unwrap(), ValidationError::InvalidEnPassant); } #[test] @@ -624,7 +624,7 @@ mod test { builder[Square::E2] = Some((Piece::King, Color::Black)); TryInto::::try_into(builder) }; - assert_eq!(res.err().unwrap(), InvalidError::NeighbouringKings); + assert_eq!(res.err().unwrap(), ValidationError::NeighbouringKings); } #[test] @@ -636,7 +636,7 @@ mod test { builder[Square::E8] = Some((Piece::King, Color::Black)); TryInto::::try_into(builder) }; - assert_eq!(res.err().unwrap(), InvalidError::OpponentInCheck); + assert_eq!(res.err().unwrap(), ValidationError::OpponentInCheck); } #[test] @@ -648,7 +648,7 @@ mod test { builder[Square::H8] = Some((Piece::King, Color::Black)); TryInto::::try_into(builder) }; - assert_eq!(res.err().unwrap(), InvalidError::InvalidPawnPosition); + assert_eq!(res.err().unwrap(), ValidationError::InvalidPawnPosition); } #[test] @@ -665,7 +665,7 @@ mod test { } TryInto::::try_into(builder) }; - assert_eq!(res.err().unwrap(), InvalidError::TooManyPieces); + assert_eq!(res.err().unwrap(), ValidationError::TooManyPieces); } #[test] diff --git a/src/fen.rs b/src/fen.rs index 90f6dd1..41a0696 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -1,5 +1,5 @@ use crate::board::{ - CastleRights, ChessBoard, ChessBoardBuilder, Color, File, InvalidError, Piece, Rank, Square, + CastleRights, ChessBoard, ChessBoardBuilder, Color, File, Piece, Rank, Square, ValidationError, }; /// A trait to mark items that can be converted from a FEN input. @@ -15,7 +15,7 @@ pub enum FenError { /// Invalid FEN input. InvalidFen, /// Invalid chess position. - InvalidPosition(InvalidError), + InvalidPosition(ValidationError), } impl std::fmt::Display for FenError { @@ -29,9 +29,9 @@ impl std::fmt::Display for FenError { impl std::error::Error for FenError {} -/// Allow converting a [InvalidError] into [FenError], for use with the '?' operator. -impl From for FenError { - fn from(err: InvalidError) -> Self { +/// Allow converting a [ValidationError] into [FenError], for use with the '?' operator. +impl From for FenError { + fn from(err: ValidationError) -> Self { Self::InvalidPosition(err) } } From 753f1590d1cb3d509229f1628dc344b4522fc203 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 3 Apr 2024 20:32:59 +0100 Subject: [PATCH 4/8] Add 'Panics' section to 'from_index' methods --- src/board/castle_rights.rs | 4 ++++ src/board/color.rs | 4 ++++ src/board/file.rs | 4 ++++ src/board/piece.rs | 4 ++++ src/board/rank.rs | 4 ++++ src/board/square.rs | 4 ++++ 6 files changed, 24 insertions(+) diff --git a/src/board/castle_rights.rs b/src/board/castle_rights.rs index b34d952..5b06544 100644 --- a/src/board/castle_rights.rs +++ b/src/board/castle_rights.rs @@ -18,6 +18,10 @@ impl CastleRights { pub const NUM_VARIANTS: usize = 4; /// Convert from a castle rights index into a [CastleRights] type. + /// + /// # Panics + /// + /// Panics if the index is out of bounds. #[inline(always)] pub fn from_index(index: usize) -> Self { assert!(index < Self::NUM_VARIANTS); diff --git a/src/board/color.rs b/src/board/color.rs index 66b21b3..17ebcb5 100644 --- a/src/board/color.rs +++ b/src/board/color.rs @@ -19,6 +19,10 @@ impl Color { } /// Convert from a color index into a [Color] type. + /// + /// # Panics + /// + /// Panics if the index is out of bounds. #[inline(always)] pub fn from_index(index: usize) -> Self { assert!(index < Self::NUM_VARIANTS); diff --git a/src/board/file.rs b/src/board/file.rs index 1475e9a..5398660 100644 --- a/src/board/file.rs +++ b/src/board/file.rs @@ -35,6 +35,10 @@ impl File { } /// Convert from a file index into a [File] type. + /// + /// # Panics + /// + /// Panics if the index is out of bounds. #[inline(always)] pub fn from_index(index: usize) -> Self { assert!(index < Self::NUM_VARIANTS); diff --git a/src/board/piece.rs b/src/board/piece.rs index 58f989a..76d9df9 100644 --- a/src/board/piece.rs +++ b/src/board/piece.rs @@ -28,6 +28,10 @@ impl Piece { } /// Convert from a piece index into a [Piece] type. + /// + /// # Panics + /// + /// Panics if the index is out of bounds. #[inline(always)] pub fn from_index(index: usize) -> Self { assert!(index < Self::NUM_VARIANTS); diff --git a/src/board/rank.rs b/src/board/rank.rs index f448df5..f716bde 100644 --- a/src/board/rank.rs +++ b/src/board/rank.rs @@ -35,6 +35,10 @@ impl Rank { } /// Convert from a rank index into a [Rank] type. + /// + /// # Panics + /// + /// Panics if the index is out of bounds. #[inline(always)] pub fn from_index(index: usize) -> Self { assert!(index < Self::NUM_VARIANTS); diff --git a/src/board/square.rs b/src/board/square.rs index 958c3c9..afbd9bf 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -39,6 +39,10 @@ impl Square { ]; /// Construct a [Square] from a [File] and [Rank]. + /// + /// # Panics + /// + /// Panics if the index is out of bounds. #[inline(always)] pub fn new(file: File, rank: Rank) -> Self { // SAFETY: we know the value is in-bounds From 8e688a0cac264339791e22db14dbef05243ef977 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 3 Apr 2024 20:44:15 +0100 Subject: [PATCH 5/8] Add 'try_from_index' implementations --- src/board/castle_rights.rs | 15 ++++++++++++--- src/board/color.rs | 15 ++++++++++++--- src/board/file.rs | 14 +++++++++++--- src/board/piece.rs | 15 ++++++++++++--- src/board/rank.rs | 14 +++++++++++--- src/board/square.rs | 15 ++++++++++++--- 6 files changed, 70 insertions(+), 18 deletions(-) diff --git a/src/board/castle_rights.rs b/src/board/castle_rights.rs index 5b06544..5bd9d91 100644 --- a/src/board/castle_rights.rs +++ b/src/board/castle_rights.rs @@ -24,9 +24,18 @@ impl CastleRights { /// Panics if the index is out of bounds. #[inline(always)] pub fn from_index(index: usize) -> Self { - assert!(index < Self::NUM_VARIANTS); - // SAFETY: we know the value is in-bounds - unsafe { Self::from_index_unchecked(index) } + Self::try_from_index(index).expect("index out of bouds") + } + + /// Convert from a castle rights index into a [CastleRights] type. Returns [None] if the index + /// is out of bounds. + pub fn try_from_index(index: usize) -> Option { + if index < Self::NUM_VARIANTS { + // SAFETY: we know the value is in-bounds + Some(unsafe { Self::from_index_unchecked(index) }) + } else { + None + } } /// Convert from a castle rights index into a [CastleRights] type, no bounds checking. diff --git a/src/board/color.rs b/src/board/color.rs index 17ebcb5..e41d3c5 100644 --- a/src/board/color.rs +++ b/src/board/color.rs @@ -25,9 +25,18 @@ impl Color { /// Panics if the index is out of bounds. #[inline(always)] pub fn from_index(index: usize) -> Self { - assert!(index < Self::NUM_VARIANTS); - // SAFETY: we know the value is in-bounds - unsafe { Self::from_index_unchecked(index) } + Self::try_from_index(index).expect("index out of bouds") + } + + /// Convert from a color index into a [Color] type. Returns [None] if the index is out of + /// bounds. + pub fn try_from_index(index: usize) -> Option { + if index < Self::NUM_VARIANTS { + // SAFETY: we know the value is in-bounds + Some(unsafe { Self::from_index_unchecked(index) }) + } else { + None + } } /// Convert from a color index into a [Color] type, no bounds checking. diff --git a/src/board/file.rs b/src/board/file.rs index 5398660..1641498 100644 --- a/src/board/file.rs +++ b/src/board/file.rs @@ -41,9 +41,17 @@ impl File { /// Panics if the index is out of bounds. #[inline(always)] pub fn from_index(index: usize) -> Self { - assert!(index < Self::NUM_VARIANTS); - // SAFETY: we know the value is in-bounds - unsafe { Self::from_index_unchecked(index) } + Self::try_from_index(index).expect("index out of bouds") + } + + /// Convert from a file index into a [File] type. Returns [None] if the index is out of bounds. + pub fn try_from_index(index: usize) -> Option { + if index < Self::NUM_VARIANTS { + // SAFETY: we know the value is in-bounds + Some(unsafe { Self::from_index_unchecked(index) }) + } else { + None + } } /// Convert from a file index into a [File] type, no bounds checking. diff --git a/src/board/piece.rs b/src/board/piece.rs index 76d9df9..f6fdce4 100644 --- a/src/board/piece.rs +++ b/src/board/piece.rs @@ -34,9 +34,18 @@ impl Piece { /// Panics if the index is out of bounds. #[inline(always)] pub fn from_index(index: usize) -> Self { - assert!(index < Self::NUM_VARIANTS); - // SAFETY: we know the value is in-bounds - unsafe { Self::from_index_unchecked(index) } + Self::try_from_index(index).expect("index out of bouds") + } + + /// Convert from a piece index into a [Piece] type. Returns [None] if the index is out of + /// bounds. + pub fn try_from_index(index: usize) -> Option { + if index < Self::NUM_VARIANTS { + // SAFETY: we know the value is in-bounds + Some(unsafe { Self::from_index_unchecked(index) }) + } else { + None + } } /// Convert from a piece index into a [Piece] type, no bounds checking. diff --git a/src/board/rank.rs b/src/board/rank.rs index f716bde..1632229 100644 --- a/src/board/rank.rs +++ b/src/board/rank.rs @@ -41,9 +41,17 @@ impl Rank { /// Panics if the index is out of bounds. #[inline(always)] pub fn from_index(index: usize) -> Self { - assert!(index < Self::NUM_VARIANTS); - // SAFETY: we know the value is in-bounds - unsafe { Self::from_index_unchecked(index) } + Self::try_from_index(index).expect("index out of bouds") + } + + /// Convert from a rank index into a [Rank] type. Returns [None] if the index is out of bounds. + pub fn try_from_index(index: usize) -> Option { + if index < Self::NUM_VARIANTS { + // SAFETY: we know the value is in-bounds + Some(unsafe { Self::from_index_unchecked(index) }) + } else { + None + } } /// Convert from a rank index into a [Rank] type, no bounds checking. diff --git a/src/board/square.rs b/src/board/square.rs index afbd9bf..b5de25b 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -57,9 +57,18 @@ impl Square { /// Convert from a square index into a [Square] type. #[inline(always)] pub fn from_index(index: usize) -> Self { - assert!(index < Self::NUM_VARIANTS); - // SAFETY: we know the value is in-bounds - unsafe { Self::from_index_unchecked(index) } + Self::try_from_index(index).expect("index out of bouds") + } + + /// Convert from a square index into a [Square] type. Returns [None] if the index is out of + /// bounds. + pub fn try_from_index(index: usize) -> Option { + if index < Self::NUM_VARIANTS { + // SAFETY: we know the value is in-bounds + Some(unsafe { Self::from_index_unchecked(index) }) + } else { + None + } } /// Convert from a square index into a [Square] type, no bounds checking. From d74605ba5c34b1dbc2424cebc077b99d30378c3b Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 3 Apr 2024 21:14:31 +0100 Subject: [PATCH 6/8] Use 'NUM_VARIANTS' where appropriate --- src/board/bitboard/mod.rs | 6 +++--- src/board/chess_board/builder.rs | 6 +++--- src/board/chess_board/mod.rs | 4 ++-- src/fen.rs | 6 +++--- src/movegen/moves.rs | 22 ++++++++++++---------- src/movegen/wizardry/mod.rs | 6 +++--- 6 files changed, 26 insertions(+), 24 deletions(-) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index b0ec90a..9059235 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -1,4 +1,4 @@ -use super::Square; +use super::{File, Rank, Square}; use crate::utils::static_assert; mod error; @@ -21,7 +21,7 @@ impl Bitboard { pub const ALL: Bitboard = Bitboard(u64::MAX); /// Array of bitboards representing the eight ranks, in order from rank 1 to rank 8. - pub const RANKS: [Self; 8] = [ + pub const RANKS: [Self; Rank::NUM_VARIANTS] = [ Bitboard(0b00000001_00000001_00000001_00000001_00000001_00000001_00000001_00000001), Bitboard(0b00000010_00000010_00000010_00000010_00000010_00000010_00000010_00000010), Bitboard(0b00000100_00000100_00000100_00000100_00000100_00000100_00000100_00000100), @@ -33,7 +33,7 @@ impl Bitboard { ]; /// Array of bitboards representing the eight files, in order from file A to file H. - pub const FILES: [Self; 8] = [ + pub const FILES: [Self; File::NUM_VARIANTS] = [ Bitboard(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_11111111), Bitboard(0b00000000_00000000_00000000_00000000_00000000_00000000_11111111_00000000), Bitboard(0b00000000_00000000_00000000_00000000_00000000_11111111_00000000_00000000), diff --git a/src/board/chess_board/builder.rs b/src/board/chess_board/builder.rs index 0304af5..679b3b7 100644 --- a/src/board/chess_board/builder.rs +++ b/src/board/chess_board/builder.rs @@ -4,7 +4,7 @@ use crate::board::{Bitboard, CastleRights, ChessBoard, Color, Piece, Square, Val #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct ChessBoardBuilder { /// The list of [Piece] on the board. Indexed by [Square::index]. - pieces: [Option<(Piece, Color)>; 64], + pieces: [Option<(Piece, Color)>; Square::NUM_VARIANTS], // Same fields as [ChessBoard]. castle_rights: [CastleRights; Color::NUM_VARIANTS], en_passant: Option, @@ -17,8 +17,8 @@ pub struct ChessBoardBuilder { impl ChessBoardBuilder { pub fn new() -> Self { Self { - pieces: [None; 64], - castle_rights: [CastleRights::NoSide; 2], + pieces: [None; Square::NUM_VARIANTS], + castle_rights: [CastleRights::NoSide; Color::NUM_VARIANTS], en_passant: Default::default(), half_move_clock: Default::default(), side: Color::White, diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index edf92a2..63dbe51 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -411,7 +411,7 @@ impl Default for ChessBoard { | Rank::Second.into_bitboard() | Rank::Seventh.into_bitboard() | Rank::Eighth.into_bitboard(), - castle_rights: [CastleRights::BothSides; 2], + castle_rights: [CastleRights::BothSides; Color::NUM_VARIANTS], en_passant: None, half_move_clock: 0, total_plies: 0, @@ -699,7 +699,7 @@ mod test { | Square::F3 | Square::G1 | Square::H2, - castle_rights: [CastleRights::NoSide; 2], + castle_rights: [CastleRights::NoSide; Color::NUM_VARIANTS], en_passant: None, half_move_clock: 0, total_plies: 0, diff --git a/src/fen.rs b/src/fen.rs index 41a0696..03c60c1 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -37,7 +37,7 @@ impl From for FenError { } /// Convert the castling rights segment of a FEN string to an array of [CastleRights]. -impl FromFen for [CastleRights; 2] { +impl FromFen for [CastleRights; Color::NUM_VARIANTS] { type Err = FenError; fn from_fen(s: &str) -> Result { @@ -45,7 +45,7 @@ impl FromFen for [CastleRights; 2] { return Err(FenError::InvalidFen); } - let mut res = [CastleRights::NoSide; 2]; + let mut res = [CastleRights::NoSide; Color::NUM_VARIANTS]; if s == "-" { return Ok(res); @@ -134,7 +134,7 @@ impl FromFen for ChessBoard { let mut builder = ChessBoardBuilder::new(); - let castle_rights = <[CastleRights; 2]>::from_fen(castling_rights)?; + let castle_rights = <[CastleRights; Color::NUM_VARIANTS]>::from_fen(castling_rights)?; for color in Color::iter() { builder.with_castle_rights(castle_rights[color.index()], color); } diff --git a/src/movegen/moves.rs b/src/movegen/moves.rs index 9840083..d46a733 100644 --- a/src/movegen/moves.rs +++ b/src/movegen/moves.rs @@ -13,12 +13,12 @@ use crate::{ // A pre-rolled RNG for magic bitboard generation, using pre-determined values. struct PreRolledRng { - numbers: [u64; 64], + numbers: [u64; Square::NUM_VARIANTS], current_index: usize, } impl PreRolledRng { - pub fn new(numbers: [u64; 64]) -> Self { + pub fn new(numbers: [u64; Square::NUM_VARIANTS]) -> Self { Self { numbers, current_index: 0, @@ -39,7 +39,8 @@ impl RandGen for PreRolledRng { /// Compute the set of possible non-attack moves for a pawn on a [Square], given its [Color] and /// set of blockers. pub fn pawn_quiet_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard { - static PAWN_MOVES: OnceLock<[[Bitboard; 64]; 2]> = OnceLock::new(); + static PAWN_MOVES: OnceLock<[[Bitboard; Square::NUM_VARIANTS]; Color::NUM_VARIANTS]> = + OnceLock::new(); // If there is a piece in front of the pawn, it can't advance if !(color.backward_direction().move_board(blockers) & square).is_empty() { @@ -47,7 +48,7 @@ pub fn pawn_quiet_moves(color: Color, square: Square, blockers: Bitboard) -> Bit } PAWN_MOVES.get_or_init(|| { - let mut res = [[Bitboard::EMPTY; 64]; 2]; + let mut res = [[Bitboard::EMPTY; Square::NUM_VARIANTS]; Color::NUM_VARIANTS]; for color in Color::iter() { for square in Square::iter() { res[color.index()][square.index()] = @@ -60,10 +61,11 @@ pub fn pawn_quiet_moves(color: Color, square: Square, blockers: Bitboard) -> Bit /// Compute the set of possible attacks for a pawn on a [Square], given its [Color]. pub fn pawn_attacks(color: Color, square: Square) -> Bitboard { - static PAWN_ATTACKS: OnceLock<[[Bitboard; 64]; 2]> = OnceLock::new(); + static PAWN_ATTACKS: OnceLock<[[Bitboard; Square::NUM_VARIANTS]; Color::NUM_VARIANTS]> = + OnceLock::new(); PAWN_ATTACKS.get_or_init(|| { - let mut res = [[Bitboard::EMPTY; 64]; 2]; + let mut res = [[Bitboard::EMPTY; Square::NUM_VARIANTS]; Color::NUM_VARIANTS]; for color in Color::iter() { for square in Square::iter() { res[color.index()][square.index()] = naive::pawn_captures(color, square); @@ -81,9 +83,9 @@ pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard /// Compute the set of possible moves for a knight on a [Square]. pub fn knight_moves(square: Square) -> Bitboard { - static KNIGHT_MOVES: OnceLock<[Bitboard; 64]> = OnceLock::new(); + static KNIGHT_MOVES: OnceLock<[Bitboard; Square::NUM_VARIANTS]> = OnceLock::new(); KNIGHT_MOVES.get_or_init(|| { - let mut res = [Bitboard::EMPTY; 64]; + let mut res = [Bitboard::EMPTY; Square::NUM_VARIANTS]; for square in Square::iter() { res[square.index()] = naive::knight_moves(square) } @@ -122,9 +124,9 @@ pub fn queen_moves(square: Square, blockers: Bitboard) -> Bitboard { /// Compute the set of possible moves for a king on a [Square]. pub fn king_moves(square: Square) -> Bitboard { - static KING_MOVES: OnceLock<[Bitboard; 64]> = OnceLock::new(); + static KING_MOVES: OnceLock<[Bitboard; Square::NUM_VARIANTS]> = OnceLock::new(); KING_MOVES.get_or_init(|| { - let mut res = [Bitboard::EMPTY; 64]; + let mut res = [Bitboard::EMPTY; Square::NUM_VARIANTS]; for square in Square::iter() { res[square.index()] = naive::king_moves(square) } diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs index f2794aa..6918d09 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -59,7 +59,7 @@ impl MagicMoves { // region:sourcegen /// A set of magic numbers for bishop move generation. -pub(crate) const BISHOP_SEED: [u64; 64] = [ +pub(crate) const BISHOP_SEED: [u64; Square::NUM_VARIANTS] = [ 4908958787341189172, 1157496606860279808, 289395876198088778, @@ -127,7 +127,7 @@ pub(crate) const BISHOP_SEED: [u64; 64] = [ ]; /// A set of magic numbers for rook move generation. -pub(crate) const ROOK_SEED: [u64; 64] = [ +pub(crate) const ROOK_SEED: [u64; Square::NUM_VARIANTS] = [ 2341871943948451840, 18015635528220736, 72066665545773824, @@ -250,7 +250,7 @@ mod test { )?; writeln!( &mut res, - "pub(crate) const {}_SEED: [u64; 64] = [", + "pub(crate) const {}_SEED: [u64; Square::NUM_VARIANTS] = [", piece_type.to_uppercase() )?; for magic in values { From b0e9e3cbcce686a466b0596df1e6704e52920c7f Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 3 Apr 2024 21:16:59 +0100 Subject: [PATCH 7/8] Add explicit 'rustfmt' configuration --- rustfmt.toml | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 rustfmt.toml diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..e69de29 From cb06fc10c8d7109eb61c942ec61f41aae96e967e Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 3 Apr 2024 21:25:45 +0100 Subject: [PATCH 8/8] Fix broken link in documentation --- src/board/chess_board/error.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/board/chess_board/error.rs b/src/board/chess_board/error.rs index fe8e4c0..bfc3bdc 100644 --- a/src/board/chess_board/error.rs +++ b/src/board/chess_board/error.rs @@ -1,4 +1,4 @@ -/// A singular type for all errors that could happen during [ChessBoard::is_valid]. +/// A singular type for all errors that could happen during [crate::board::ChessBoard::is_valid]. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ValidationError { /// Too many pieces.