diff --git a/goldchess/src/color.rs b/goldchess/src/color.rs index bca2ad5..75fef00 100644 --- a/goldchess/src/color.rs +++ b/goldchess/src/color.rs @@ -7,6 +7,16 @@ pub enum Color { impl Color { pub const ALL: [Color; 2] = [Color::White, Color::Black]; + pub const NUM_COLORS: usize = Color::ALL.len(); + + /// Get the index of the color. + #[must_use] + pub const fn as_index(self) -> usize { + match self { + Color::White => 0, + Color::Black => 1, + } + } } impl std::ops::Not for Color { @@ -19,3 +29,12 @@ impl std::ops::Not for Color { } } } + +impl std::fmt::Display for Color { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Color::White => write!(f, "w"), + Color::Black => write!(f, "b"), + } + } +} diff --git a/goldchess/src/error.rs b/goldchess/src/error.rs index 0eb5ad8..d54e6ee 100644 --- a/goldchess/src/error.rs +++ b/goldchess/src/error.rs @@ -13,6 +13,8 @@ pub enum Error { InvalidRank(u8), #[error("Invalid rank notation: {0}")] InvalidRankChar(char), + #[error("Invalid FEN notation: {0}")] + InvalidFen(String), } /// A specialized [`Result`] type for this crate, used by all fallible functions. diff --git a/goldchess/src/file.rs b/goldchess/src/file.rs index a95e773..d983b28 100644 --- a/goldchess/src/file.rs +++ b/goldchess/src/file.rs @@ -58,6 +58,16 @@ impl File { pub const F: File = File(5); pub const G: File = File(6); pub const H: File = File(7); + pub const ALL: [File; 8] = [ + File::A, + File::B, + File::C, + File::D, + File::E, + File::F, + File::G, + File::H, + ]; } impl From for u8 { diff --git a/goldchess/src/lib.rs b/goldchess/src/lib.rs index 34b18b6..a3e2c1b 100644 --- a/goldchess/src/lib.rs +++ b/goldchess/src/lib.rs @@ -13,6 +13,7 @@ mod error; mod file; mod generated_tables; mod piece; +mod position; mod rank; mod square; @@ -22,5 +23,6 @@ pub use color::Color; pub use error::{Error, Result}; pub use file::File; pub use piece::Piece; +pub use position::Position; pub use rank::Rank; pub use square::Square; diff --git a/goldchess/src/piece.rs b/goldchess/src/piece.rs index c8fe06c..792c7ba 100644 --- a/goldchess/src/piece.rs +++ b/goldchess/src/piece.rs @@ -10,6 +10,7 @@ pub enum Piece { } impl Piece { + /// All possible pieces, in order of their [`Self::as_index`] value. pub const ALL: [Piece; 6] = [ Piece::Pawn, Piece::Knight, @@ -18,4 +19,33 @@ impl Piece { Piece::Queen, Piece::King, ]; + + /// The number of distinct piece types. + pub const NUM_PIECES: usize = Piece::ALL.len(); + + /// Get the index of the piece. + #[must_use] + pub const fn as_index(self) -> usize { + match self { + Piece::Pawn => 0, + Piece::Knight => 1, + Piece::Bishop => 2, + Piece::Rook => 3, + Piece::Queen => 4, + Piece::King => 5, + } + } + + /// Get the (upper case) character representation of the piece. + #[must_use] + pub const fn as_char(self) -> char { + match self { + Piece::Pawn => 'P', + Piece::Knight => 'N', + Piece::Bishop => 'B', + Piece::Rook => 'R', + Piece::Queen => 'Q', + Piece::King => 'K', + } + } } diff --git a/goldchess/src/position.rs b/goldchess/src/position.rs new file mode 100644 index 0000000..5f77f14 --- /dev/null +++ b/goldchess/src/position.rs @@ -0,0 +1,244 @@ +use std::str::FromStr; + +use crate::{Bitboard, CastleRights, Color, Error, File, Piece, Rank, Square}; + +#[derive(Debug, Clone)] +pub struct Position { + pieces: [Bitboard; Piece::NUM_PIECES], + color_combined: [Bitboard; Color::NUM_COLORS], + side_to_move: Color, + castle_rights: [CastleRights; Color::NUM_COLORS], + en_passant: Option, + halfmove_clock: u8, +} + +impl Position { + #[must_use] + pub fn piece_on(&self, sq: Square) -> Option { + Piece::ALL + .into_iter() + .find(|&piece| self.pieces[piece.as_index()] & sq != Bitboard::EMPTY) + } + + #[must_use] + pub fn color_on(&self, sq: Square) -> Option { + Color::ALL + .into_iter() + .find(|&color| self.color_combined[color.as_index()] & sq != Bitboard::EMPTY) + } + + pub const STARTING_POSITION_FEN: &str = + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; +} + +impl Default for Position { + fn default() -> Self { + Position::from_str(Position::STARTING_POSITION_FEN).unwrap() + } +} + +impl FromStr for Position { + type Err = Error; + + fn from_str(value: &str) -> Result { + let mut board = [None; Square::NUM_SQUARES]; + let invalid_fen = || Error::InvalidFen(value.to_string()); + + let tokens: Vec<&str> = value.split(' ').collect(); + if tokens.len() < 4 { + return Err(invalid_fen()); + } + + let pieces = tokens[0]; + let side = tokens[1]; + let castles = tokens[2]; + let ep = tokens[3]; + let halfmove = tokens[4]; + let mut cur_rank = Rank::R8; + let mut cur_file = File::A; + + for x in pieces.chars() { + match x { + '/' => { + cur_rank = cur_rank + .down() + .ok_or_else(|| Error::InvalidFen(value.to_string()))?; + cur_file = File::A; + } + '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' => { + cur_file = File::new( + u8::try_from(cur_file.0 as usize + (x as usize) - ('0' as usize)) + .map_err(|_| invalid_fen())? + % 8, + ) + .map_err(|_| invalid_fen())?; + } + x => { + let (piece, color) = match x { + 'r' => (Piece::Rook, Color::Black), + 'R' => (Piece::Rook, Color::White), + 'n' => (Piece::Knight, Color::Black), + 'N' => (Piece::Knight, Color::White), + 'b' => (Piece::Bishop, Color::Black), + 'B' => (Piece::Bishop, Color::White), + 'q' => (Piece::Queen, Color::Black), + 'Q' => (Piece::Queen, Color::White), + 'k' => (Piece::King, Color::Black), + 'K' => (Piece::King, Color::White), + 'p' => (Piece::Pawn, Color::Black), + 'P' => (Piece::Pawn, Color::White), + _ => { + return Err(invalid_fen()); + } + }; + board[Square::make_square(cur_rank, cur_file).as_index()] = + Some((piece, color)); + cur_file = cur_file.right().unwrap_or(File::A); + } + } + } + let side_to_move = match side { + "w" | "W" => Color::White, + "b" | "B" => Color::Black, + _ => { + return Err(invalid_fen()); + } + }; + + let mut castle_rights = [CastleRights::None; Color::NUM_COLORS]; + if castles.contains('K') && castles.contains('Q') { + castle_rights[Color::White.as_index()] = CastleRights::Both; + } else if castles.contains('K') { + castle_rights[Color::White.as_index()] = CastleRights::KingSide; + } else if castles.contains('Q') { + castle_rights[Color::White.as_index()] = CastleRights::QueenSide; + } + + if castles.contains('k') && castles.contains('q') { + castle_rights[Color::Black.as_index()] = CastleRights::Both; + } else if castles.contains('k') { + castle_rights[Color::Black.as_index()] = CastleRights::KingSide; + } else if castles.contains('q') { + castle_rights[Color::Black.as_index()] = CastleRights::QueenSide; + } + + let ep_square = Square::from_str(ep).ok(); + + let halfmove_clock = halfmove.parse::().map_err(|_| invalid_fen())?; + + let mut pieces = [Bitboard(0); Piece::NUM_PIECES]; + let mut color_combined = [Bitboard(0); Color::NUM_COLORS]; + for (&piece, sq) in board.iter().zip(Square::ALL_SQUARES) { + if let Some((piece, color)) = piece { + pieces[piece.as_index()] |= sq; + color_combined[color.as_index()] |= sq; + } + } + + Ok(Position { + pieces, + color_combined, + side_to_move, + castle_rights, + en_passant: ep_square, + halfmove_clock, + }) + } +} + +impl std::fmt::Display for Position { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for rank in Rank::ALL.into_iter().rev() { + let mut empty = 0; + for file in File::ALL { + let sq = Square::make_square(rank, file); + match self.piece_on(sq) { + None => empty += 1, + Some(piece) => { + if empty > 0 { + write!(f, "{empty}")?; + empty = 0; + } + let color = self.color_on(sq).unwrap(); + let c = piece.as_char(); + write!( + f, + "{}", + if color == Color::Black { + c.to_ascii_lowercase() + } else { + c + } + )?; + } + } + } + if empty > 0 { + write!(f, "{empty}")?; + } + if rank != Rank::R1 { + write!(f, "/")?; + } + } + + write!(f, " {} ", self.side_to_move)?; + + match self.castle_rights { + [CastleRights::None, CastleRights::None] => write!(f, "-")?, + [white, black] => { + match white { + CastleRights::KingSide => write!(f, "K")?, + CastleRights::QueenSide => write!(f, "Q")?, + CastleRights::Both => write!(f, "KQ")?, + CastleRights::None => {} + } + + match black { + CastleRights::KingSide => write!(f, "k")?, + CastleRights::QueenSide => write!(f, "q")?, + CastleRights::Both => write!(f, "kq")?, + CastleRights::None => {} + } + } + } + + if let Some(ep) = self.en_passant { + write!(f, " {ep}")?; + } else { + write!(f, " -")?; + } + + write!(f, " {}", self.halfmove_clock)?; + write!(f, " 1") // Full move clock, not stored here so just write 1 to be FEN-compliant. + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_from_str() { + for fen in [ + "2n1k3/8/8/8/8/8/8/2N1K3 w - - 0 1", + "5B2/6P1/1p6/8/1N6/kP6/2K5/8 w - - 0 1", + "6R1/P2k4/r7/5N1P/r7/p7/7K/8 w - - 0 1", + "7K/8/k1P5/7p/8/8/8/8 w - - 0 1", + "7k/8/8/8/8/8/8/7K w - - 0 1", + "8/5k2/3p4/1pPPp2p/pP2Pp1P/P4P1K/8/8 b - b6 99 1", + "8/8/7p/3KNN1k/2p4p/8/3P2p1/8 w - - 0 1", + "r4rk1/1pp2ppp/3b4/3P4/3p4/3B4/1PP2PPP/R4RK1 b - - 0 1", + "rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b - - 1 1", + "rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 1", + "rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b Kkq - 1 1", + "rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R w kq - 10 1", + "rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 1", + "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1", + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", + ] { + let pos = Position::from_str(fen).unwrap(); + assert_eq!(pos.to_string(), fen); + println!("{pos}"); + } + } +} diff --git a/goldchess/src/rank.rs b/goldchess/src/rank.rs index 2638d82..ebeb93b 100644 --- a/goldchess/src/rank.rs +++ b/goldchess/src/rank.rs @@ -58,6 +58,16 @@ impl Rank { pub const R6: Rank = Rank(5); pub const R7: Rank = Rank(6); pub const R8: Rank = Rank(7); + pub const ALL: [Rank; 8] = [ + Rank::R1, + Rank::R2, + Rank::R3, + Rank::R4, + Rank::R5, + Rank::R6, + Rank::R7, + Rank::R8, + ]; } impl From for u8 { diff --git a/goldchess/src/square.rs b/goldchess/src/square.rs index 20d378e..7bfa661 100644 --- a/goldchess/src/square.rs +++ b/goldchess/src/square.rs @@ -36,6 +36,13 @@ impl Square { } } + /// Create a new square from a [`Rank`] and [`File`]. + #[must_use] + pub const fn make_square(rank: Rank, file: File) -> Self { + // SAFETY: rank and file are valid, so the square is valid. + unsafe { Self::new_unchecked(rank.0 * 8 + file.0 + 1) } + } + #[must_use] pub const fn file(self) -> File { // SAFETY: The modulo operation ensures that the result is always in 0..8. @@ -47,6 +54,11 @@ impl Square { // SAFETY: self is a valid square index, so (self.0 - 1) / 8 is a valid rank index. unsafe { Rank::new_unchecked((self.0.get() - 1) / 8) } } + + /// Get the square index as a `usize`. + pub const fn as_index(self) -> usize { + self.0.get() as usize - 1 + } } #[allow(dead_code)] @@ -181,11 +193,12 @@ impl Square { Self::G8, Self::H8, ]; + pub const NUM_SQUARES: usize = Self::ALL_SQUARES.len(); } impl std::fmt::Display for Square { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}{}", (b'A' + self.file().0) as char, self.rank().0 + 1) + write!(f, "{}{}", (b'a' + self.file().0) as char, self.rank().0 + 1) } } @@ -235,7 +248,6 @@ impl FromStr for Square { fn from_str(s: &str) -> Result { let file = File::from_str(&s[0..1])?; let rank = Rank::from_str(&s[1..2])?; - // SAFETY: file and rank are valid, so the square is valid. - Ok(unsafe { Self::new_unchecked(rank.0 * 8 + file.0 + 1) }) + Ok(Square::make_square(rank, file)) } }