From a676094dc1d744d66ebcabf69df8ca767ac70336 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:20:20 +0100 Subject: [PATCH 1/9] Use 'ChessBoardBuilder' in validation tests The various tests for overlapping can't be triggered with the builder API, so those have stayed unchanged. --- src/board/chess_board/mod.rs | 259 ++++++++--------------------------- 1 file changed, 58 insertions(+), 201 deletions(-) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index d04fdec..cb1b95c 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -634,238 +634,95 @@ 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 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] From 829362dbced2edce8d0cd5570a02e9446714a297 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:30:52 +0100 Subject: [PATCH 2/9] Add 'From' for 'FenError' --- src/fen.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/fen.rs b/src/fen.rs index 3034003..78452c2 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -27,6 +27,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; From b9cc60be9c4370374e5fd08b163d5e931e23aaeb Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:32:02 +0100 Subject: [PATCH 3/9] Use 'ChessBoardBuilder' in 'FromFen' This will allow taking this *out* of the module, now that we don't need to reach into the internals of 'ChessBoard'. --- src/board/chess_board/mod.rs | 50 +++++++++++++++--------------------- 1 file changed, 21 insertions(+), 29 deletions(-) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index cb1b95c..c502ada 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -417,22 +417,33 @@ impl FromFen for ChessBoard { 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)?; - let en_passant = Option::::from_fen(en_passant_square)?; + 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)?; - 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); + builder.with_total_plies( + (full_move_counter - 1) * 2 + if side == Color::White { 0 } else { 1 }, + ); + { let mut rank: usize = 8; for rank_str in piece_placement.split('/') { rank -= 1; @@ -451,17 +462,15 @@ impl FromFen for ChessBoard { } _ => 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; + + builder[square] = Some((piece, color)); file += 1; } // We haven't read exactly 8 files. @@ -473,26 +482,9 @@ impl FromFen for ChessBoard { 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) + Ok(builder.try_into()?) } } From c3be661719a25eda900c711d1a67d711ffc939e0 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:34:01 +0100 Subject: [PATCH 4/9] Move 'FromFen' for 'ChessBoard' into 'fen' module --- src/board/chess_board/mod.rs | 91 +----------------------------------- src/fen.rs | 89 ++++++++++++++++++++++++++++++++++- 2 files changed, 90 insertions(+), 90 deletions(-) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index c502ada..824426f 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}; @@ -403,94 +400,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 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_total_plies( - (full_move_counter - 1) * 2 + if side == Color::White { 0 } else { 1 }, - ); - - { - 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 crate::fen::FromFen; use super::*; diff --git a/src/fen.rs b/src/fen.rs index 78452c2..8baf4af 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 { @@ -115,3 +117,88 @@ 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_total_plies( + (full_move_counter - 1) * 2 + if side == Color::White { 0 } else { 1 }, + ); + + { + 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()?) + } +} From 62c2be48c49c563e79489db5adca50405f483e67 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:35:48 +0100 Subject: [PATCH 5/9] Move FEN-related tests to its module --- src/board/chess_board/mod.rs | 72 -------------------------------- src/fen.rs | 79 ++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 72 deletions(-) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index 824426f..9b25dd3 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -673,78 +673,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 8baf4af..3096c95 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -202,3 +202,82 @@ impl FromFen for ChessBoard { 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 + ); + } +} From ff7bea05085be7b758fcd30c2a83e492767d3c8f Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:54:32 +0100 Subject: [PATCH 6/9] Validate en-passant square's rank in 'ChessBoard' --- src/board/chess_board/error.rs | 4 ++-- src/board/chess_board/mod.rs | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/board/chess_board/error.rs b/src/board/chess_board/error.rs index e531f54..e6ef030 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, @@ -33,7 +33,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.", diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index 9b25dd3..122880a 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -297,12 +297,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() From 2853cec7c9b11130884f755cf61eef2d291a69fd Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:56:01 +0100 Subject: [PATCH 7/9] Add tests for en-passant validation --- src/board/chess_board/mod.rs | 50 ++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index 122880a..99d6df6 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -588,6 +588,56 @@ mod test { 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 res = { From ef3a1e4695a5089415c6a05e6cf808e60945a72a Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 22:41:01 +0100 Subject: [PATCH 8/9] Add half-move clock validation --- src/board/chess_board/error.rs | 3 +++ src/board/chess_board/mod.rs | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/board/chess_board/error.rs b/src/board/chess_board/error.rs index e6ef030..4265de0 100644 --- a/src/board/chess_board/error.rs +++ b/src/board/chess_board/error.rs @@ -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 { @@ -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 99d6df6..879aba6 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -208,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)] @@ -423,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 { From d4a1955c0f40de45f55634b7c9482668b1f236c8 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 22:46:44 +0100 Subject: [PATCH 9/9] Use turn counts in 'ChessBoardBuilder' This makes more sense from a user's perspective. --- src/board/chess_board/builder.rs | 15 +++++++++------ src/fen.rs | 4 +--- 2 files changed, 10 insertions(+), 9 deletions(-) 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/fen.rs b/src/fen.rs index 3096c95..a7a6825 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -154,9 +154,7 @@ impl FromFen for ChessBoard { let full_move_counter = full_move_counter .parse::() .map_err(|_| FenError::InvalidFen)?; - builder.with_total_plies( - (full_move_counter - 1) * 2 + if side == Color::White { 0 } else { 1 }, - ); + builder.with_turn_count(full_move_counter); { let mut rank: usize = 8;