diff --git a/bamboo/src/board.rs b/bamboo/src/board.rs index 0417170..fa41d81 100644 --- a/bamboo/src/board.rs +++ b/bamboo/src/board.rs @@ -13,12 +13,16 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +//! Provides structs and functions for managing representations of a chess board. + use crate::boardfield::{Boardfield, BoardfieldOps}; use crate::error::FENParseError; use crate::piece::{PieceColor, PieceOnBoard, PieceType}; use crate::utils::{algebraic_to_boardloc}; -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone)] +#[allow(clippy::struct_excessive_bools)] +/// A struct to represent a chessboard and the pieces and state associated with it. pub struct Board { bitfield: Boardfield, // While this is a less efficient memory layout, having an array of where all the pieces are will make move generation significantly faster. @@ -38,6 +42,7 @@ pub struct Board { } impl Board { + /// Convert the current state of the board into a 41-byte packed board ID. This packed representation contains all piece data and flags associated with the chessboard and is sufficient for unique identification and compact storage of chessboards. pub fn to_board_id(&self) -> [u8; 41] { let mut res = [0u8; 41]; @@ -76,22 +81,23 @@ impl Board { } res[32] = flags_byte; - let en_passant_target: [u8; 4] = match self.en_passant_target { - Some(target) => { - (target as u32).to_le_bytes() - } - None => { - u32::MAX.to_le_bytes() - } - }; - res[33..37].copy_from_slice(&en_passant_target); - - res[37..39].copy_from_slice(&(self.halfmove_counter as u16).to_le_bytes()); - res[39..41].copy_from_slice(&(self.move_counter as u16).to_le_bytes()); + #[allow(clippy::cast_possible_truncation)] { + let en_passant_target: [u8; 4] = self.en_passant_target.map_or_else(|| u32::MAX.to_le_bytes(), |target| (target as i32).to_le_bytes()); + res[33..37].copy_from_slice(&en_passant_target); + res[37..39].copy_from_slice(&(self.halfmove_counter as u16).to_le_bytes()); + res[39..41].copy_from_slice(&(self.move_counter as u16).to_le_bytes()); + } res } + /// Create a new `Board` from a FEN string. + /// # Errors + /// This function will return an error if an invalid FEN string is passed. + /// # Panics + /// This function, while containing code that can panic, will never panic as the string is bounds checked. + #[allow(clippy::cast_possible_wrap)] + #[allow(clippy::match_on_vec_items)] pub fn from_fen(fen: &str) -> Result { let components = fen.split(' ').collect::>(); if components.len() != 6 { @@ -146,7 +152,11 @@ impl Board { file += 1; } _ if char.is_numeric() => { - let num = char.to_digit(10).unwrap() as isize; + let num; + // see above (.is_numeric()) + #[allow(clippy::unwrap_used)] { + num = char.to_digit(10).unwrap() as isize; + } if !(1..=8).contains(&num) { return Err(FENParseError::CannotSkipToOutsideOfBoard { got: num, which_is: num }); @@ -185,7 +195,7 @@ impl Board { } } -fn create_fen_piece(c: char, rank: isize, file: isize) -> Result { +const fn create_fen_piece(c: char, rank: isize, file: isize) -> Result { Ok(PieceOnBoard { loc: (rank + file * 8) - 1, value: match c { @@ -193,20 +203,20 @@ fn create_fen_piece(c: char, rank: isize, file: isize) -> Result PieceColor::Black as u8 | PieceType::Knight as u8, 'b' => PieceColor::Black as u8 | PieceType::Bishop as u8, 'q' => PieceColor::Black as u8 | PieceType::Queen as u8, - 'k' => PieceColor::Black as u8 | PieceType::Knight as u8, + 'k' => PieceColor::Black as u8 | PieceType::King as u8, 'p' => PieceColor::Black as u8 | PieceType::Pawn as u8, 'R' => PieceColor::White as u8 | PieceType::Rook as u8, 'N' => PieceColor::White as u8 | PieceType::Knight as u8, 'B' => PieceColor::White as u8 | PieceType::Bishop as u8, 'Q' => PieceColor::White as u8 | PieceType::Queen as u8, - 'K' => PieceColor::White as u8 | PieceType::Knight as u8, + 'K' => PieceColor::White as u8 | PieceType::King as u8, 'P' => PieceColor::White as u8 | PieceType::Pawn as u8, _ => return Err(FENParseError::InvalidPieceCharacter { got: c }) }, }) } -fn create_bitfield_piece(c: char) -> Result { +const fn create_bitfield_piece(c: char) -> Result { match c { 'r' => Ok(PieceColor::Black as u8 | PieceType::Rook as u8), 'n' => Ok(PieceColor::Black as u8 | PieceType::Knight as u8), @@ -227,6 +237,8 @@ fn create_bitfield_piece(c: char) -> Result { } #[cfg(test)] +#[allow(clippy::unwrap_used)] +#[allow(clippy::expect_used)] mod tests { use crate::board::{Board, create_bitfield_piece, create_fen_piece}; use crate::boardfield::BoardfieldOps; @@ -276,6 +288,7 @@ mod tests { } #[test] + #[allow(clippy::cognitive_complexity)] pub fn fen_parse_testing() { let board = Board::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1").unwrap(); diff --git a/bamboo/src/boardfield.rs b/bamboo/src/boardfield.rs index e222813..28550a4 100644 --- a/bamboo/src/boardfield.rs +++ b/bamboo/src/boardfield.rs @@ -13,15 +13,25 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +//! Operations for working with a packed square-wise representation of a chessboard. + use crate::boardloc; use crate::piece::{PieceColor, PieceType}; +/// A Boardfield is a packed, square-wise representation of a chessboard. It **does not** include any extra metadata such as castling rights. pub type Boardfield = [u8; 32]; + +/// A trait to contain the operations available on a `Boardfield` pub trait BoardfieldOps { + /// Create a new, entirely empty `Boardfield`. fn new() -> Self where Self: Sized; + /// Create a new `Boardfield` from the standard chess starting position. fn startpos() -> Self where Self: Sized; + /// Get the piece value stored at the provided boardloc. fn get_pos(&self, boardloc: isize) -> u8; + /// Set the piece stored at the provided boardloc to the given piece value. fn set_pos(&mut self, boardloc: isize, piece: u8); + /// Get the entire boardfield as a 32-length bytearray. fn get_field(&self) -> [u8; 32]; } impl BoardfieldOps for Boardfield { @@ -76,13 +86,13 @@ impl BoardfieldOps for Boardfield { field } + #[allow(clippy::cast_sign_loss)] fn get_pos(&self, boardloc: isize) -> u8 { // bf1: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 // bf2: 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 - if boardloc > 63 { - panic!("boardloc out of range"); - } + assert!(boardloc <= 63, "boardloc out of range"); + assert!(boardloc > 0, "boardloc out of range"); let field = self[(boardloc / 2) as usize]; let shift = 4 * (boardloc % 2); @@ -90,10 +100,10 @@ impl BoardfieldOps for Boardfield { (field & (0b1111 << shift)) >> shift } + #[allow(clippy::cast_sign_loss)] fn set_pos(&mut self, boardloc: isize, piece: u8) { - if boardloc > 63 { - panic!("boardloc out of range {}", boardloc); - } + assert!(boardloc <= 63, "boardloc out of range {boardloc}"); + assert!(boardloc > 0, "boardloc out of range {boardloc}"); let field = self[(boardloc / 2) as usize]; let shift = 4 * (boardloc % 2); @@ -136,6 +146,7 @@ mod tests { } #[test] + #[allow(clippy::cognitive_complexity)] fn bitfield_board() { let field = Boardfield::startpos(); diff --git a/bamboo/src/error.rs b/bamboo/src/error.rs index 4e21b3e..9aed6ae 100644 --- a/bamboo/src/error.rs +++ b/bamboo/src/error.rs @@ -13,18 +13,52 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +//! Module to contain the various types of errors used by Bamboo. + use std::error::Error; use std::fmt::{Display, Formatter}; use std::num::ParseIntError; #[derive(Debug)] +/// An error type to represent the possible errors while parsing a FEN string pub enum FENParseError { - InvalidNumberOfComponents { got: isize }, - InvalidPlayerToMove { got: String }, - EnPassantTargetParseError { e: AlgebraicNotationError }, - InvalidMoveCounter { e: ParseIntError }, - InvalidPieceCharacter { got: char }, - CannotSkipToOutsideOfBoard { got: isize, which_is: isize }, + /// A FEN string is expected to have six space-delimited components, but there were less or more than 6 components provided. + InvalidNumberOfComponents { + /// How many components were actually found in the string + got: isize + }, + + /// The Player To Move field should either be a `w`, indicating white to move, or `b`, indicating black to move, but neither were found. + InvalidPlayerToMove { + /// The actual Player To Move field found in the FEN string. + got: String + }, + + /// An error occured parsing the en passant target square. + EnPassantTargetParseError { + /// The actual error that occured + e: AlgebraicNotationError + }, + + /// An error occured parsing the fullmove or halfmove color. + InvalidMoveCounter { + /// The parsing error that occured + e: ParseIntError + }, + + /// While parsing the piece data, an unexpected character was found + InvalidPieceCharacter { + /// The unexpected character that was found + got: char + }, + + /// Chess boards are 8x8 but this FEN string attempted to place a piece outside of the board. + CannotSkipToOutsideOfBoard { + /// The skip value that was found + got: isize, + /// The location that skip value would place a piece at + which_is: isize + }, } impl Error for FENParseError {} @@ -32,29 +66,42 @@ impl Error for FENParseError {} impl Display for FENParseError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - Self::InvalidNumberOfComponents { got } => write!(f, "Invalid number of components: got {}, expected 6", got), - Self::InvalidPlayerToMove { got } => write!(f, "Invalid player to move: expected one of `wb`, got `{}`", got), - Self::EnPassantTargetParseError { e } => write!(f, "Error parsing en passant target: {}", e), - Self::InvalidMoveCounter { e } => write!(f, "Invalid move counter: {}", e), - Self::InvalidPieceCharacter { got } => write!(f, "Invalid piece character: expected one of `rnbqkpRNBQKP`, got `{}`", got), - Self::CannotSkipToOutsideOfBoard { got, which_is } => write!(f, "Cannot skip files to outside the board (tried to skip {} positions, which would be at position {})", got, which_is) + Self::InvalidNumberOfComponents { got } => write!(f, "Invalid number of components: got {got}, expected 6"), + Self::InvalidPlayerToMove { got } => write!(f, "Invalid player to move: expected one of `wb`, got `{got}`"), + Self::EnPassantTargetParseError { e } => write!(f, "Error parsing en passant target: {e}"), + Self::InvalidMoveCounter { e } => write!(f, "Invalid move counter: {e}"), + Self::InvalidPieceCharacter { got } => write!(f, "Invalid piece character: expected one of `rnbqkpRNBQKP`, got `{got}`"), + Self::CannotSkipToOutsideOfBoard { got, which_is } => write!(f, "Cannot skip files to outside the board (tried to skip {got} positions, which would be at position {which_is})") } } } #[derive(Debug)] +/// Represents an error that occured parsing an algebraic notation coordinate. pub enum AlgebraicNotationError { - InvalidRank { got: String }, - InvalidFile { got: String }, - InvalidLength { got: isize } + /// An invalid rank (1-8) was encountered + InvalidRank { + /// The rank that was provided + got: String + }, + /// An invalid file (a-h) was encountered + InvalidFile { + /// The file that was provided + got: String + }, + /// Algebraic notation is two characters long, but this string was not two characters long + InvalidLength { + /// The length of the provided string + got: isize + } } impl Error for AlgebraicNotationError {} impl Display for AlgebraicNotationError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - AlgebraicNotationError::InvalidRank { got } => write!(f, "Invalid rank: expected one of `012345678`, got `{}`", got), - AlgebraicNotationError::InvalidFile { got } => write!(f, "Invalid file: expected one of `abcdefgh`, got `{}`", got), - AlgebraicNotationError::InvalidLength { got } => write!(f, "Invalid length, expected 2, got `{}`", got) + Self::InvalidRank { got } => write!(f, "Invalid rank: expected one of `12345678`, got `{got}`"), + Self::InvalidFile { got } => write!(f, "Invalid file: expected one of `abcdefgh`, got `{got}`"), + Self::InvalidLength { got } => write!(f, "Invalid length, expected 2, got `{got}`") } } } \ No newline at end of file diff --git a/bamboo/src/lib.rs b/bamboo/src/lib.rs index ded8e48..90ccd89 100644 --- a/bamboo/src/lib.rs +++ b/bamboo/src/lib.rs @@ -13,6 +13,21 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +//! # Bamboo +//! Bamboo is a pure-rust chess engine that uses a machine learning model to provide NNUE-like board evaluation. + +#![warn(clippy::pedantic)] +#![warn(clippy::nursery)] +#![deny(missing_docs)] +#![deny(clippy::unwrap_used)] +#![deny(clippy::expect_used)] +#![deny(clippy::missing_errors_doc)] +#![deny(clippy::missing_panics_doc)] +#![deny(clippy::missing_safety_doc)] +#![allow(clippy::must_use_candidate)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::module_name_repetitions)] + pub mod boardfield; pub mod piece; pub mod board; diff --git a/bamboo/src/nn/mod.rs b/bamboo/src/nn/mod.rs index 017d3ff..18fa0b6 100644 --- a/bamboo/src/nn/mod.rs +++ b/bamboo/src/nn/mod.rs @@ -13,7 +13,10 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +//! Functions, structs, traits and macros for working with `BambooNN`. + #[derive(Default, Debug)] +/// A feed-forward machine learning model used to evaluate board positions pub struct BNNModel { } diff --git a/bamboo/src/piece.rs b/bamboo/src/piece.rs index 1f513de..8511649 100644 --- a/bamboo/src/piece.rs +++ b/bamboo/src/piece.rs @@ -13,23 +13,28 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +//! Structs and methods for working with chess pieces. + use crate::piece::PieceType::{Bishop, King, Knight, Pawn, Queen, Rook}; #[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone)] +/// Represents a piece on the chessboard. Used for the piece-based evaluation in `Board`. pub struct PieceOnBoard { + /// The boardloc this piece is at pub loc: isize, + /// The value of the piece pub value: Piece } impl PieceOnBoard { - fn empty(loc: isize) -> Self { - PieceOnBoard { + const fn empty(loc: isize) -> Self { + Self { loc, value: 0u8 } } - fn new(loc: isize, value: Piece) -> Self { - PieceOnBoard { + const fn new(loc: isize, value: Piece) -> Self { + Self { loc, value } @@ -38,37 +43,56 @@ impl PieceOnBoard { #[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone, Copy)] #[repr(u8)] +/// The type of a chess piece, used for `u8`/four-bit piece representations pub enum PieceType { + /// Represents a pawn Pawn = 1, + /// Represents a knight Knight = 2, + /// Represents a bishop Bishop = 3, + /// Represents a rook Rook = 4, + /// Represents a queen Queen = 5, + /// Represents a king King = 6 } #[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone, Copy)] #[repr(u8)] +/// Represents the color of a piece, used for `u8`/four-bit piece representations pub enum PieceColor { + /// Represents a white piece White = 8, + /// Represents a black piece Black = 0 } impl PieceColor { - pub fn invert(&self) -> Self { + /// Get the opposite of this color, e.g. for white return black and vice versa. + #[must_use] + pub const fn invert(&self) -> Self { match self { - Self::White => PieceColor::Black, - Self::Black => PieceColor::White + Self::White => Self::Black, + Self::Black => Self::White } } } +/// Represents a four-bit packed chess piece. pub type Piece = u8; + +/// A trait for the operations that can be done on a `Piece` pub trait PieceOps { + /// Determines if this piece is white. Inverse of `is_black` fn is_white(value: u8) -> bool; + /// Determines if this piece is black. Inverse of `is_black` fn is_black(value: u8) -> bool; + /// Get the `PieceType` of this piece. This returns an option, as a Piece can be any `u8`, but not all of them are actual piece types. fn get_type(value: u8) -> Option; + /// Gets the color of this piece. This effectively just checks if the fourth bit is set, and as such does not return an option. fn get_color(value: u8) -> PieceColor; } impl PieceOps for Piece { @@ -76,7 +100,7 @@ impl PieceOps for Piece { if value < 8 { false } else { - value & PieceColor::White as u8 != 0 + value & PieceColor::White as Self != 0 } } @@ -106,6 +130,7 @@ impl PieceOps for Piece { } #[cfg(test)] +#[allow(clippy::unwrap_used)] mod piece_tests { use crate::piece::{Piece, PieceColor, PieceOnBoard, PieceOps, PieceType}; @@ -159,6 +184,7 @@ mod piece_tests { } #[test] + #[allow(clippy::cognitive_complexity)] fn piece_color_checks() { assert!(!Piece::is_white(PieceType::Pawn as u8 | PieceColor::Black as u8)); assert!(Piece::is_white(PieceType::Pawn as u8 | PieceColor::White as u8)); diff --git a/bamboo/src/utils.rs b/bamboo/src/utils.rs index c96a169..5517c5b 100644 --- a/bamboo/src/utils.rs +++ b/bamboo/src/utils.rs @@ -13,8 +13,15 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +//! Provides various utility functions and macros + use crate::error::AlgebraicNotationError; +/// Convert an algebraic notation, such as `a1`, to the corresponding boardloc. +/// # Errors +/// This function will return an error if the algebraic string is invalid. +#[allow(clippy::cast_sign_loss)] +#[allow(clippy::cast_possible_wrap)] pub fn algebraic_to_boardloc(algebraic: &str) -> Result { if algebraic.chars().count() != 2 { return Err(AlgebraicNotationError::InvalidLength { got: algebraic.chars().count() as isize }) @@ -38,12 +45,19 @@ pub fn algebraic_to_boardloc(algebraic: &str) -> Result(boardloc: isize) -> &'a str { +/// Convert a 0-63 boardloc into an algebraic notation string. +/// # Panics +/// This function will **panic** if you provide an invalid boardloc. Please bounds check it first. +#[allow(clippy::cast_sign_loss)] +#[allow(clippy::cast_possible_wrap)] +pub const fn boardloc_to_algebraic<'a>(boardloc: isize) -> &'a str { BOARDLOC_TO_ALG[boardloc as usize] } +/// A macro to shorten `algebraic_to_boardloc`. This will **panic** if you provide an invalid algebraic notation. #[macro_export] macro_rules! boardloc { ($alg:expr) => { @@ -51,6 +65,7 @@ macro_rules! boardloc { }; } +/// A macro to shorten `boardloc_to_algebraic`. This will **panic** if you provide an invalid (out of boudns) boardloc. #[macro_export] macro_rules! algebraic { ($boardloc:expr) => { @@ -59,6 +74,7 @@ macro_rules! algebraic { } #[cfg(test)] +#[allow(clippy::unwrap_used)] mod tests { use crate::utils::{algebraic_to_boardloc, boardloc_to_algebraic}; @@ -73,7 +89,7 @@ mod tests { #[should_panic] fn boardloc_invalid_file() { if let Err(e) = algebraic_to_boardloc("j8") { - println!("{}", e); + println!("{e}"); panic!(); } } @@ -82,7 +98,7 @@ mod tests { #[should_panic] fn boardloc_invalid_length() { if let Err(e) = algebraic_to_boardloc("jsdf8") { - println!("{}", e); + println!("{e}"); panic!(); } } @@ -91,7 +107,7 @@ mod tests { #[should_panic] fn boardloc_invalid_rank() { if let Err(e) = algebraic_to_boardloc("a9") { - println!("{}", e); + println!("{e}"); panic!(); } }