diff --git a/src/board/chess_board/builder.rs b/src/board/chess_board/builder.rs index 8221d92..d82ec18 100644 --- a/src/board/chess_board/builder.rs +++ b/src/board/chess_board/builder.rs @@ -9,8 +9,9 @@ pub struct ChessBoardBuilder { castle_rights: [CastleRights; Color::NUM_VARIANTS], en_passant: Option, half_move_clock: u8, - total_plies: u32, side: Color, + // 1-based, A turn is *two* half-moves (i.e: both players have played). + turn_count: u32, } impl ChessBoardBuilder { @@ -20,8 +21,8 @@ impl ChessBoardBuilder { castle_rights: [CastleRights::NoSide; 2], en_passant: Default::default(), half_move_clock: Default::default(), - total_plies: Default::default(), side: Color::White, + turn_count: 1, } } @@ -45,8 +46,8 @@ impl ChessBoardBuilder { self } - pub fn with_total_plies(&mut self, plies: u32) -> &mut Self { - self.total_plies = plies; + pub fn with_turn_count(&mut self, count: u32) -> &mut Self { + self.turn_count = count; self } @@ -90,8 +91,8 @@ impl TryFrom for ChessBoard { castle_rights, en_passant, half_move_clock, - total_plies, side, + turn_count, } = builder; for square in Square::iter() { @@ -103,6 +104,8 @@ impl TryFrom for ChessBoard { combined_occupancy |= square; } + let total_plies = (turn_count - 1) * 2 + if side == Color::White { 0 } else { 1 }; + let board = ChessBoard { piece_occupancy, color_occupancy, @@ -146,7 +149,7 @@ mod test { builder .with_half_move_clock(board.half_move_clock()) - .with_total_plies(board.total_plies()) + .with_turn_count(board.total_plies() / 2 + 1) .with_current_player(board.current_player()); builder diff --git a/src/board/chess_board/error.rs b/src/board/chess_board/error.rs index e531f54..4265de0 100644 --- a/src/board/chess_board/error.rs +++ b/src/board/chess_board/error.rs @@ -9,7 +9,7 @@ pub enum InvalidError { InvalidPawnPosition, /// Castling rights do not match up with the state of the board. InvalidCastlingRights, - /// En-passant target square is not empty and behind an opponent's pawn. + /// En-passant target square is not empty, behind an opponent's pawn, on the correct rank. InvalidEnPassant, /// The two kings are next to each other. NeighbouringKings, @@ -21,6 +21,8 @@ pub enum InvalidError { OverlappingColors, /// The pre-computed combined occupancy boards does not match the other boards. ErroneousCombinedOccupancy, + /// Half-move clock is higher than total number of plies. + HalfMoveClockTooHigh, } impl std::fmt::Display for InvalidError { @@ -33,7 +35,7 @@ impl std::fmt::Display for InvalidError { "Castling rights do not match up with the state of the board." } Self::InvalidEnPassant => { - "En-passant target square is not empty and behind an opponent's pawn." + "En-passant target square is not empty, behind an opponent's pawn, on the correct rank." } Self::NeighbouringKings => "The two kings are next to each other.", Self::OpponentInCheck => "The opponent is currently in check.", @@ -42,6 +44,7 @@ impl std::fmt::Display for InvalidError { Self::ErroneousCombinedOccupancy => { "The pre-computed combined occupancy boards does not match the other boards." } + Self::HalfMoveClockTooHigh => "Half-move clock is higher than total number of plies.", }; write!(f, "{}", error_msg) } diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index d04fdec..879aba6 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -1,7 +1,4 @@ -use crate::{ - fen::{FenError, FromFen}, - movegen, -}; +use crate::movegen; use super::{Bitboard, CastleRights, Color, File, Move, Piece, Rank, Square}; @@ -211,6 +208,11 @@ impl ChessBoard { /// Validate the state of the board. Return Err([InvalidError]) if an issue is found. pub fn validate(&self) -> Result<(), InvalidError> { + // Make sure the clocks are in agreement. + if u32::from(self.half_move_clock()) > self.total_plies() { + return Err(InvalidError::HalfMoveClockTooHigh); + } + // Don't overlap pieces. for piece in Piece::iter() { #[allow(clippy::collapsible_if)] @@ -300,12 +302,22 @@ impl ChessBoard { } } - // The current en-passant target square must be empty, right behind an opponent's pawn. + // En-passant validation if let Some(square) = self.en_passant() { + // Must be empty if !(self.combined_occupancy() & square).is_empty() { return Err(InvalidError::InvalidEnPassant); } - let opponent_pawns = self.occupancy(Piece::Pawn, !self.current_player()); + + 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); + } + + // Must be behind a pawn + let opponent_pawns = self.occupancy(Piece::Pawn, opponent); let double_pushed_pawn = self .current_player() .backward_direction() @@ -403,102 +415,10 @@ impl Default for ChessBoard { } } -/// Return a [ChessBoard] from the given FEN string. -impl FromFen for ChessBoard { - type Err = FenError; - - fn from_fen(s: &str) -> Result { - let mut split = s.split_ascii_whitespace(); - - let piece_placement = split.next().ok_or(FenError::InvalidFen)?; - let side_to_move = split.next().ok_or(FenError::InvalidFen)?; - let castling_rights = split.next().ok_or(FenError::InvalidFen)?; - let en_passant_square = split.next().ok_or(FenError::InvalidFen)?; - let half_move_clock = split.next().ok_or(FenError::InvalidFen)?; - let full_move_counter = split.next().ok_or(FenError::InvalidFen)?; - - let castle_rights = <[CastleRights; 2]>::from_fen(castling_rights)?; - let side = Color::from_fen(side_to_move)?; - let en_passant = Option::::from_fen(en_passant_square)?; - - let half_move_clock = half_move_clock - .parse::() - .map_err(|_| FenError::InvalidFen)?; - let full_move_counter = full_move_counter - .parse::() - .map_err(|_| FenError::InvalidFen)?; - let total_plies = (full_move_counter - 1) * 2 + if side == Color::White { 0 } else { 1 }; - - let (piece_occupancy, color_occupancy, combined_occupancy) = { - let (mut pieces, mut colors, mut combined) = - ([Bitboard::EMPTY; 6], [Bitboard::EMPTY; 2], Bitboard::EMPTY); - - let mut rank: usize = 8; - for rank_str in piece_placement.split('/') { - rank -= 1; - let mut file: usize = 0; - for c in rank_str.chars() { - let color = if c.is_uppercase() { - Color::White - } else { - Color::Black - }; - let piece = match c { - digit @ '1'..='8' => { - // Unwrap is fine since this arm is only matched by digits - file += digit.to_digit(10).unwrap() as usize; - continue; - } - _ => Piece::from_fen(&c.to_string())?, - }; - let (piece_board, color_board) = - (&mut pieces[piece.index()], &mut colors[color.index()]); - - // Only need to worry about underflow since those are `usize` values. - if file >= 8 || rank >= 8 { - return Err(FenError::InvalidFen); - }; - let square = Square::new(File::from_index(file), Rank::from_index(rank)); - *piece_board |= square; - *color_board |= square; - combined |= square; - file += 1; - } - // We haven't read exactly 8 files. - if file != 8 { - return Err(FenError::InvalidFen); - } - } - // We haven't read exactly 8 ranks - if rank != 0 { - return Err(FenError::InvalidFen); - } - - (pieces, colors, combined) - }; - - let res = Self { - piece_occupancy, - color_occupancy, - combined_occupancy, - castle_rights, - en_passant, - half_move_clock, - total_plies, - side, - }; - - if let Err(err) = res.validate() { - return Err(FenError::InvalidPosition(err)); - } - - Ok(res) - } -} - #[cfg(test)] mod test { use crate::board::MoveBuilder; + use crate::fen::FromFen; use super::*; @@ -508,6 +428,18 @@ mod test { assert!(default_position.is_valid()); } + #[test] + fn invalid_half_moves_clock() { + let res = { + let mut builder = ChessBoardBuilder::new(); + builder[Square::E1] = Some((Piece::King, Color::White)); + builder[Square::E8] = Some((Piece::King, Color::Black)); + builder.with_half_move_clock(10); + TryInto::::try_into(builder) + }; + assert_eq!(res.err().unwrap(), InvalidError::HalfMoveClockTooHigh); + } + #[test] fn invalid_overlapping_pieces() { let position = ChessBoard { @@ -634,238 +566,145 @@ mod test { #[test] fn invalid_multiple_kings() { - let position = ChessBoard { - piece_occupancy: [ - // King - Square::E1 | Square::E2 | Square::E7 | Square::E8, - // Queen - Bitboard::EMPTY, - // Rook - Bitboard::EMPTY, - // Bishop - Bitboard::EMPTY, - // Knight - Bitboard::EMPTY, - // Pawn - Bitboard::EMPTY, - ], - color_occupancy: [Square::E1 | Square::E2, Square::E7 | Square::E8], - combined_occupancy: Square::E1 | Square::E2 | Square::E7 | Square::E8, - castle_rights: [CastleRights::NoSide; 2], - en_passant: None, - half_move_clock: 0, - total_plies: 0, - side: Color::White, + let res = { + let mut builder = ChessBoardBuilder::new(); + builder[Square::E1] = Some((Piece::King, Color::White)); + builder[Square::E2] = Some((Piece::King, Color::White)); + builder[Square::E7] = Some((Piece::King, Color::Black)); + builder[Square::E8] = Some((Piece::King, Color::Black)); + TryInto::::try_into(builder) }; - assert_eq!( - position.validate().err().unwrap(), - InvalidError::TooManyPieces, - ); + assert_eq!(res.err().unwrap(), InvalidError::TooManyPieces); } #[test] fn invalid_castling_rights_no_rooks() { - 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 | Square::E8, - castle_rights: [CastleRights::BothSides; 2], - en_passant: None, - half_move_clock: 0, - total_plies: 0, - side: Color::White, + let res = { + let mut builder = ChessBoardBuilder::new(); + builder[Square::E1] = Some((Piece::King, Color::White)); + builder[Square::E8] = Some((Piece::King, Color::Black)); + builder.with_castle_rights(CastleRights::BothSides, Color::White); + TryInto::::try_into(builder) }; - assert_eq!( - position.validate().err().unwrap(), - InvalidError::InvalidCastlingRights, - ); + assert_eq!(res.err().unwrap(), InvalidError::InvalidCastlingRights); } #[test] fn invalid_castling_rights_moved_king() { - let position = ChessBoard { - piece_occupancy: [ - // King - Square::E2 | Square::E7, - // Queen - Bitboard::EMPTY, - // Rook - Square::A1 | Square::A8 | Square::H1 | Square::H8, - // Bishop - Bitboard::EMPTY, - // Knight - Bitboard::EMPTY, - // Pawn - Bitboard::EMPTY, - ], - color_occupancy: [ - Square::A1 | Square::E2 | Square::H1, - Square::A8 | Square::E7 | Square::H8, - ], - combined_occupancy: Square::A1 - | Square::A8 - | Square::E2 - | Square::E7 - | Square::H1 - | Square::H8, - castle_rights: [CastleRights::BothSides; 2], - en_passant: None, - half_move_clock: 0, - total_plies: 0, - side: Color::White, + let res = { + let mut builder = ChessBoardBuilder::new(); + builder[Square::E2] = Some((Piece::King, Color::White)); + builder[Square::A1] = Some((Piece::Rook, Color::White)); + builder[Square::H1] = Some((Piece::Rook, Color::White)); + builder[Square::E7] = Some((Piece::King, Color::Black)); + builder[Square::A8] = Some((Piece::Rook, Color::Black)); + builder[Square::H8] = Some((Piece::Rook, Color::Black)); + builder.with_castle_rights(CastleRights::BothSides, Color::White); + TryInto::::try_into(builder) }; - assert_eq!( - position.validate().err().unwrap(), - InvalidError::InvalidCastlingRights, - ); + assert_eq!(res.err().unwrap(), InvalidError::InvalidCastlingRights); + } + + #[test] + fn valid_en_passant() { + let mut builder = ChessBoardBuilder::new(); + builder[Square::E1] = Some((Piece::King, Color::White)); + builder[Square::E8] = Some((Piece::King, Color::Black)); + builder[Square::A5] = Some((Piece::Pawn, Color::Black)); + builder.with_en_passant(Square::A6); + TryInto::::try_into(builder).unwrap(); + } + + #[test] + fn invalid_en_passant_not_empty() { + let res = { + let mut builder = ChessBoardBuilder::new(); + builder[Square::E1] = Some((Piece::King, Color::White)); + builder[Square::E8] = Some((Piece::King, Color::Black)); + builder[Square::A6] = Some((Piece::Rook, Color::Black)); + builder[Square::A5] = Some((Piece::Pawn, Color::Black)); + builder.with_en_passant(Square::A6); + TryInto::::try_into(builder) + }; + assert_eq!(res.err().unwrap(), InvalidError::InvalidEnPassant); + } + + #[test] + fn invalid_en_passant_not_behind_pawn() { + let res = { + let mut builder = ChessBoardBuilder::new(); + builder[Square::E1] = Some((Piece::King, Color::White)); + builder[Square::E8] = Some((Piece::King, Color::Black)); + builder[Square::A5] = Some((Piece::Rook, Color::Black)); + builder.with_en_passant(Square::A6); + TryInto::::try_into(builder) + }; + assert_eq!(res.err().unwrap(), InvalidError::InvalidEnPassant); + } + + #[test] + fn invalid_en_passant_incorrect_rank() { + let res = { + let mut builder = ChessBoardBuilder::new(); + builder[Square::E1] = Some((Piece::King, Color::White)); + builder[Square::E8] = Some((Piece::King, Color::Black)); + builder[Square::A4] = Some((Piece::Pawn, Color::Black)); + builder.with_en_passant(Square::A5); + TryInto::::try_into(builder) + }; + assert_eq!(res.err().unwrap(), InvalidError::InvalidEnPassant); } #[test] fn invalid_kings_next_to_each_other() { - let position = ChessBoard { - piece_occupancy: [ - // King - Square::E2 | Square::E3, - // Queen - Bitboard::EMPTY, - // Rook - Bitboard::EMPTY, - // Bishop - Bitboard::EMPTY, - // Knight - Bitboard::EMPTY, - // Pawn - Bitboard::EMPTY, - ], - color_occupancy: [Square::E2.into_bitboard(), Square::E3.into_bitboard()], - combined_occupancy: Square::E2 | Square::E3, - castle_rights: [CastleRights::NoSide; 2], - en_passant: None, - half_move_clock: 0, - total_plies: 0, - side: Color::White, + let res = { + let mut builder = ChessBoardBuilder::new(); + builder[Square::E1] = Some((Piece::King, Color::White)); + builder[Square::E2] = Some((Piece::King, Color::Black)); + TryInto::::try_into(builder) }; - assert_eq!( - position.validate().err().unwrap(), - InvalidError::NeighbouringKings, - ); + assert_eq!(res.err().unwrap(), InvalidError::NeighbouringKings); } #[test] fn invalid_opponent_in_check() { - let position = ChessBoard { - piece_occupancy: [ - // King - Square::E1 | Square::E8, - // Queen - Square::E7.into_bitboard(), - // Rook - Bitboard::EMPTY, - // Bishop - Bitboard::EMPTY, - // Knight - Bitboard::EMPTY, - // Pawn - Bitboard::EMPTY, - ], - color_occupancy: [Square::E1 | Square::E7, Square::E8.into_bitboard()], - combined_occupancy: Square::E1 | Square::E7 | Square::E8, - castle_rights: [CastleRights::NoSide; 2], - en_passant: None, - half_move_clock: 0, - total_plies: 0, - side: Color::White, + let res = { + let mut builder = ChessBoardBuilder::new(); + builder[Square::E1] = Some((Piece::King, Color::White)); + builder[Square::E7] = Some((Piece::Queen, Color::White)); + builder[Square::E8] = Some((Piece::King, Color::Black)); + TryInto::::try_into(builder) }; - assert_eq!( - position.validate().err().unwrap(), - InvalidError::OpponentInCheck, - ); + assert_eq!(res.err().unwrap(), InvalidError::OpponentInCheck); } #[test] fn invalid_pawn_on_first_rank() { - let position = ChessBoard { - piece_occupancy: [ - // King - Square::H1 | Square::H8, - // Queen - Bitboard::EMPTY, - // Rook - Bitboard::EMPTY, - // Bishop - Bitboard::EMPTY, - // Knight - Bitboard::EMPTY, - // Pawn - Square::A1.into_bitboard(), - ], - color_occupancy: [Square::A1 | Square::H1, Square::H8.into_bitboard()], - combined_occupancy: Square::A1 | Square::H1 | Square::H8, - castle_rights: [CastleRights::NoSide; 2], - en_passant: None, - half_move_clock: 0, - total_plies: 0, - side: Color::White, + let res = { + let mut builder = ChessBoardBuilder::new(); + builder[Square::H1] = Some((Piece::King, Color::White)); + builder[Square::A1] = Some((Piece::Pawn, Color::White)); + builder[Square::H8] = Some((Piece::King, Color::Black)); + TryInto::::try_into(builder) }; - assert_eq!( - position.validate().err().unwrap(), - InvalidError::InvalidPawnPosition, - ); + assert_eq!(res.err().unwrap(), InvalidError::InvalidPawnPosition); } #[test] fn invalid_too_many_pieces() { - let position = ChessBoard { - piece_occupancy: [ - // King - Square::H1 | Square::H8, - // Queen - Bitboard::EMPTY, - // Rook - Bitboard::EMPTY, - // Bishop - Bitboard::EMPTY, - // Knight - Bitboard::EMPTY, - // Pawn - File::B.into_bitboard() - | File::C.into_bitboard() - | File::D.into_bitboard() - | File::E.into_bitboard(), - ], - color_occupancy: [ - File::B.into_bitboard() | File::C.into_bitboard() | Square::H1, - File::D.into_bitboard() | File::E.into_bitboard() | Square::H8, - ], - combined_occupancy: File::B.into_bitboard() - | File::C.into_bitboard() - | File::D.into_bitboard() - | File::E.into_bitboard() - | Square::H1 - | Square::H8, - castle_rights: [CastleRights::NoSide; 2], - en_passant: None, - half_move_clock: 0, - total_plies: 0, - side: Color::White, + let res = { + let mut builder = ChessBoardBuilder::new(); + builder[Square::H1] = Some((Piece::King, Color::White)); + builder[Square::H8] = Some((Piece::King, Color::Black)); + for square in (File::B.into_bitboard() | File::C.into_bitboard()).into_iter() { + builder[square] = Some((Piece::Pawn, Color::White)); + } + for square in (File::F.into_bitboard() | File::G.into_bitboard()).into_iter() { + builder[square] = Some((Piece::Pawn, Color::Black)); + } + TryInto::::try_into(builder) }; - assert_eq!( - position.validate().err().unwrap(), - InvalidError::TooManyPieces, - ); + assert_eq!(res.err().unwrap(), InvalidError::TooManyPieces); } #[test] @@ -911,78 +750,6 @@ mod test { ); } - #[test] - fn fen_default_position() { - let default_position = ChessBoard::default(); - assert_eq!( - ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") - .unwrap(), - default_position - ); - } - - #[test] - fn fen_en_passant() { - // Start from default position - let mut position = ChessBoard::default(); - position.do_move( - MoveBuilder { - piece: Piece::Pawn, - start: Square::E2, - destination: Square::E4, - capture: None, - promotion: None, - en_passant: false, - double_step: true, - castling: false, - } - .into(), - ); - assert_eq!( - ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1") - .unwrap(), - position - ); - // And now c5 - position.do_move( - MoveBuilder { - piece: Piece::Pawn, - start: Square::C7, - destination: Square::C5, - capture: None, - promotion: None, - en_passant: false, - double_step: true, - castling: false, - } - .into(), - ); - assert_eq!( - ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2") - .unwrap(), - position - ); - // Finally, Nf3 - position.do_move( - MoveBuilder { - piece: Piece::Knight, - start: Square::G1, - destination: Square::F3, - capture: None, - promotion: None, - en_passant: false, - double_step: false, - castling: false, - } - .into(), - ); - assert_eq!( - ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2 ") - .unwrap(), - position - ); - } - #[test] fn do_move() { // Start from default position diff --git a/src/fen.rs b/src/fen.rs index 3034003..a7a6825 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -1,4 +1,6 @@ -use crate::board::{CastleRights, Color, File, InvalidError, Piece, Rank, Square}; +use crate::board::{ + CastleRights, ChessBoard, ChessBoardBuilder, Color, File, InvalidError, Piece, Rank, Square, +}; /// A trait to mark items that can be converted from a FEN input. pub trait FromFen: Sized { @@ -27,6 +29,13 @@ 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 { + Self::InvalidPosition(err) + } +} + /// Convert the castling rights segment of a FEN string to an array of [CastleRights]. impl FromFen for [CastleRights; 2] { type Err = FenError; @@ -108,3 +117,165 @@ impl FromFen for Piece { Ok(res) } } + +/// Return a [ChessBoard] from the given FEN string. +impl FromFen for ChessBoard { + type Err = FenError; + + fn from_fen(s: &str) -> Result { + let mut split = s.split_ascii_whitespace(); + + let piece_placement = split.next().ok_or(FenError::InvalidFen)?; + let side_to_move = split.next().ok_or(FenError::InvalidFen)?; + let castling_rights = split.next().ok_or(FenError::InvalidFen)?; + let en_passant_square = split.next().ok_or(FenError::InvalidFen)?; + let half_move_clock = split.next().ok_or(FenError::InvalidFen)?; + let full_move_counter = split.next().ok_or(FenError::InvalidFen)?; + + let mut builder = ChessBoardBuilder::new(); + + let castle_rights = <[CastleRights; 2]>::from_fen(castling_rights)?; + for color in Color::iter() { + builder.with_castle_rights(castle_rights[color.index()], color); + } + + let side = Color::from_fen(side_to_move)?; + builder.with_current_player(side); + + if let Some(square) = Option::::from_fen(en_passant_square)? { + builder.with_en_passant(square); + }; + + let half_move_clock = half_move_clock + .parse::() + .map_err(|_| FenError::InvalidFen)?; + builder.with_half_move_clock(half_move_clock); + + let full_move_counter = full_move_counter + .parse::() + .map_err(|_| FenError::InvalidFen)?; + builder.with_turn_count(full_move_counter); + + { + let mut rank: usize = 8; + for rank_str in piece_placement.split('/') { + rank -= 1; + let mut file: usize = 0; + for c in rank_str.chars() { + let color = if c.is_uppercase() { + Color::White + } else { + Color::Black + }; + let piece = match c { + digit @ '1'..='8' => { + // Unwrap is fine since this arm is only matched by digits + file += digit.to_digit(10).unwrap() as usize; + continue; + } + _ => Piece::from_fen(&c.to_string())?, + }; + + // Only need to worry about underflow since those are `usize` values. + if file >= 8 || rank >= 8 { + return Err(FenError::InvalidFen); + }; + + let square = Square::new(File::from_index(file), Rank::from_index(rank)); + + builder[square] = Some((piece, color)); + file += 1; + } + // We haven't read exactly 8 files. + if file != 8 { + return Err(FenError::InvalidFen); + } + } + // We haven't read exactly 8 ranks + if rank != 0 { + return Err(FenError::InvalidFen); + } + }; + + Ok(builder.try_into()?) + } +} + +#[cfg(test)] +mod test { + use crate::board::MoveBuilder; + + use super::*; + + #[test] + fn default_position() { + let default_position = ChessBoard::default(); + assert_eq!( + ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") + .unwrap(), + default_position + ); + } + + #[test] + fn en_passant() { + // Start from default position + let mut position = ChessBoard::default(); + position.do_move( + MoveBuilder { + piece: Piece::Pawn, + start: Square::E2, + destination: Square::E4, + capture: None, + promotion: None, + en_passant: false, + double_step: true, + castling: false, + } + .into(), + ); + assert_eq!( + ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1") + .unwrap(), + position + ); + // And now c5 + position.do_move( + MoveBuilder { + piece: Piece::Pawn, + start: Square::C7, + destination: Square::C5, + capture: None, + promotion: None, + en_passant: false, + double_step: true, + castling: false, + } + .into(), + ); + assert_eq!( + ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2") + .unwrap(), + position + ); + // Finally, Nf3 + position.do_move( + MoveBuilder { + piece: Piece::Knight, + start: Square::G1, + destination: Square::F3, + capture: None, + promotion: None, + en_passant: false, + double_step: false, + castling: false, + } + .into(), + ); + assert_eq!( + ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2 ") + .unwrap(), + position + ); + } +}