2022-07-29 19:27:17 +02:00
|
|
|
use crate::{
|
|
|
|
fen::{FenError, FromFen},
|
|
|
|
movegen,
|
|
|
|
};
|
2022-07-28 21:38:50 +02:00
|
|
|
|
2022-07-28 21:45:04 +02:00
|
|
|
use super::{Bitboard, CastleRights, Color, File, Move, Piece, Rank, Square};
|
2022-07-28 21:19:40 +02:00
|
|
|
|
2024-04-01 22:19:55 +02:00
|
|
|
mod builder;
|
|
|
|
pub use builder::*;
|
|
|
|
|
2024-04-01 21:21:05 +02:00
|
|
|
mod error;
|
|
|
|
pub use error::*;
|
|
|
|
|
2022-07-28 21:19:40 +02:00
|
|
|
/// Represent an on-going chess game.
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
|
|
pub struct ChessBoard {
|
|
|
|
/// A [Bitboard] of occupancy for each piece type, discarding color. Indexed by [Piece::index].
|
|
|
|
piece_occupancy: [Bitboard; Piece::NUM_VARIANTS],
|
|
|
|
/// A [Bitboard] of occupancy for each color, discarding piece type. Indexed by [Piece::index].
|
|
|
|
color_occupancy: [Bitboard; Color::NUM_VARIANTS],
|
|
|
|
/// A [Bitboard] representing all squares currently occupied by a piece.
|
|
|
|
combined_occupancy: Bitboard,
|
|
|
|
/// The allowed [CastleRights] for either color. Indexed by [Color::index].
|
|
|
|
castle_rights: [CastleRights; Color::NUM_VARIANTS],
|
|
|
|
/// A potential en-passant attack.
|
|
|
|
/// Either `None` if no double-step pawn move was made in the previous half-turn, or
|
|
|
|
/// `Some(target_square)` if a double-step move was made.
|
|
|
|
en_passant: Option<Square>,
|
|
|
|
/// The number of half-turns without either a pawn push or capture.
|
|
|
|
half_move_clock: u8, // Should never go higher than 50.
|
|
|
|
/// The number of half-turns so far.
|
|
|
|
total_plies: u32, // Should be plenty.
|
|
|
|
/// The current player turn.
|
|
|
|
side: Color,
|
|
|
|
}
|
|
|
|
|
2022-07-28 21:19:40 +02:00
|
|
|
/// The state which can't be reversed when doing/un-doing a [Move].
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
|
|
pub struct NonReversibleState {
|
|
|
|
castle_rights: [CastleRights; Color::NUM_VARIANTS],
|
|
|
|
en_passant: Option<Square>,
|
|
|
|
half_move_clock: u8, // Should never go higher than 50.
|
|
|
|
}
|
|
|
|
|
2022-07-28 21:19:40 +02:00
|
|
|
impl ChessBoard {
|
|
|
|
/// Which player's turn is it.
|
|
|
|
#[inline(always)]
|
|
|
|
pub fn current_player(&self) -> Color {
|
|
|
|
self.side
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Return the [Square] currently occupied by a pawn that can be captured en-passant, or `None`
|
|
|
|
#[inline(always)]
|
|
|
|
pub fn en_passant(&self) -> Option<Square> {
|
|
|
|
self.en_passant
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Return the [CastleRights] for the given [Color].
|
|
|
|
#[inline(always)]
|
|
|
|
pub fn castle_rights(&self, color: Color) -> CastleRights {
|
|
|
|
self.castle_rights[color.index()]
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Return the [CastleRights] for the given [Color]. Allow mutations.
|
|
|
|
#[inline(always)]
|
|
|
|
fn castle_rights_mut(&mut self, color: Color) -> &mut CastleRights {
|
|
|
|
&mut self.castle_rights[color.index()]
|
|
|
|
}
|
|
|
|
|
2024-04-01 02:29:39 +02:00
|
|
|
/// Get the [Bitboard] representing all pieces of the given [Piece] and [Color] type.
|
|
|
|
#[inline(always)]
|
|
|
|
pub fn occupancy(&self, piece: Piece, color: Color) -> Bitboard {
|
|
|
|
self.piece_occupancy(piece) & self.color_occupancy(color)
|
|
|
|
}
|
|
|
|
|
2022-07-28 21:19:40 +02:00
|
|
|
/// Get the [Bitboard] representing all pieces of the given [Piece] type, discarding color.
|
|
|
|
#[inline(always)]
|
|
|
|
pub fn piece_occupancy(&self, piece: Piece) -> Bitboard {
|
|
|
|
self.piece_occupancy[piece.index()]
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Get the [Bitboard] representing all pieces of the given [Piece] type, discarding color.
|
|
|
|
/// Allow mutating the state.
|
|
|
|
#[inline(always)]
|
|
|
|
fn piece_occupancy_mut(&mut self, piece: Piece) -> &mut Bitboard {
|
|
|
|
&mut self.piece_occupancy[piece.index()]
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Get the [Bitboard] representing all colors of the given [Color] type, discarding piece
|
|
|
|
/// type.
|
|
|
|
#[inline(always)]
|
|
|
|
pub fn color_occupancy(&self, color: Color) -> Bitboard {
|
|
|
|
self.color_occupancy[color.index()]
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Get the [Bitboard] representing all colors of the given [Color] type, discarding piece
|
|
|
|
/// type. Allow mutating the state.
|
|
|
|
#[inline(always)]
|
|
|
|
fn color_occupancy_mut(&mut self, color: Color) -> &mut Bitboard {
|
|
|
|
&mut self.color_occupancy[color.index()]
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Get the [Bitboard] representing all pieces on the board.
|
|
|
|
#[inline(always)]
|
|
|
|
pub fn combined_occupancy(&self) -> Bitboard {
|
|
|
|
self.combined_occupancy
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Return the number of half-turns without either a pawn push or a capture.
|
|
|
|
#[inline(always)]
|
|
|
|
pub fn half_move_clock(&self) -> u8 {
|
|
|
|
self.half_move_clock
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Return the total number of plies (i.e: half-turns) played so far.
|
|
|
|
#[inline(always)]
|
|
|
|
pub fn total_plies(&self) -> u32 {
|
|
|
|
self.total_plies
|
|
|
|
}
|
2022-07-28 21:19:40 +02:00
|
|
|
|
2022-07-30 12:02:41 +02:00
|
|
|
/// Return the [Bitboard] corresponding to all the opponent's pieces threatening the current
|
|
|
|
/// player's king.
|
|
|
|
#[inline(always)]
|
|
|
|
pub fn checkers(&self) -> Bitboard {
|
|
|
|
self.compute_checkers(self.current_player())
|
|
|
|
}
|
|
|
|
|
2022-07-28 21:19:40 +02:00
|
|
|
/// Quickly do and undo a move on the [Bitboard]s that are part of the [ChessBoard] state. Does
|
|
|
|
/// not account for all non-revertible changes such as en-passant state or half-move clock.
|
|
|
|
#[inline(always)]
|
|
|
|
fn xor(&mut self, color: Color, piece: Piece, start_end: Bitboard) {
|
|
|
|
*self.piece_occupancy_mut(piece) ^= start_end;
|
|
|
|
*self.color_occupancy_mut(color) ^= start_end;
|
|
|
|
self.combined_occupancy ^= start_end;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Play the given [Move], returning all non-revertible state (e.g: en-passant, etc...).
|
|
|
|
#[inline(always)]
|
|
|
|
pub fn do_move(&mut self, chess_move: Move) -> NonReversibleState {
|
|
|
|
// Save non-revertible state
|
|
|
|
let state = NonReversibleState {
|
|
|
|
castle_rights: self.castle_rights,
|
|
|
|
en_passant: self.en_passant,
|
|
|
|
half_move_clock: self.half_move_clock,
|
|
|
|
};
|
|
|
|
|
|
|
|
// Non-revertible state modification
|
|
|
|
if chess_move.capture().is_some() || chess_move.piece() == Piece::Pawn {
|
|
|
|
self.half_move_clock = 0;
|
|
|
|
} else {
|
|
|
|
self.half_move_clock += 1;
|
|
|
|
}
|
|
|
|
if chess_move.is_double_step() {
|
|
|
|
let target_square = Square::new(
|
|
|
|
chess_move.destination().file(),
|
|
|
|
self.current_player().third_rank(),
|
|
|
|
);
|
|
|
|
self.en_passant = Some(target_square);
|
|
|
|
} else {
|
|
|
|
self.en_passant = None;
|
|
|
|
}
|
|
|
|
if chess_move.is_castling() || chess_move.piece() == Piece::King {
|
|
|
|
*self.castle_rights_mut(self.current_player()) = CastleRights::NoSide;
|
|
|
|
}
|
|
|
|
if chess_move.piece() == Piece::Rook {
|
|
|
|
let castle_rights = self.castle_rights_mut(self.current_player());
|
|
|
|
*castle_rights = match chess_move.start().file() {
|
|
|
|
File::A => castle_rights.without_queen_side(),
|
|
|
|
File::H => castle_rights.without_king_side(),
|
|
|
|
_ => *castle_rights,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Revertible state modification
|
|
|
|
self.xor(
|
|
|
|
self.current_player(),
|
|
|
|
chess_move.piece(),
|
|
|
|
chess_move.start() | chess_move.destination(),
|
|
|
|
);
|
|
|
|
self.total_plies += 1;
|
|
|
|
self.side = !self.side;
|
|
|
|
|
|
|
|
state
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Reverse the effect of playing the given [Move], and return to the given
|
|
|
|
/// [NonReversibleState].
|
|
|
|
#[inline(always)]
|
|
|
|
pub fn undo_move(&mut self, chess_move: Move, previous: NonReversibleState) {
|
|
|
|
// Restore non-revertible state
|
|
|
|
self.castle_rights = previous.castle_rights;
|
|
|
|
self.en_passant = previous.en_passant;
|
|
|
|
self.half_move_clock = previous.half_move_clock;
|
|
|
|
|
|
|
|
// Restore revertible state
|
|
|
|
self.xor(
|
|
|
|
// The move was applied at the turn *before* the current player
|
|
|
|
!self.current_player(),
|
|
|
|
chess_move.piece(),
|
|
|
|
chess_move.start() | chess_move.destination(),
|
|
|
|
);
|
|
|
|
self.total_plies -= 1;
|
|
|
|
self.side = !self.side;
|
|
|
|
}
|
2022-07-28 21:37:47 +02:00
|
|
|
|
|
|
|
/// Return true if the current state of the board looks valid, false if something is definitely
|
|
|
|
/// wrong.
|
|
|
|
pub fn is_valid(&self) -> bool {
|
2024-04-01 21:21:05 +02:00
|
|
|
self.validate().is_ok()
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Validate the state of the board. Return Err([InvalidError]) if an issue is found.
|
|
|
|
pub fn validate(&self) -> Result<(), InvalidError> {
|
2022-07-28 21:37:47 +02:00
|
|
|
// Don't overlap pieces.
|
|
|
|
for piece in Piece::iter() {
|
|
|
|
#[allow(clippy::collapsible_if)]
|
|
|
|
for other in Piece::iter() {
|
|
|
|
if piece != other {
|
|
|
|
if !(self.piece_occupancy(piece) & self.piece_occupancy(other)).is_empty() {
|
2024-04-01 21:21:05 +02:00
|
|
|
return Err(InvalidError::OverlappingPieces);
|
2022-07-28 21:37:47 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Don't overlap colors.
|
|
|
|
if !(self.color_occupancy(Color::White) & self.color_occupancy(Color::Black)).is_empty() {
|
2024-04-01 21:21:05 +02:00
|
|
|
return Err(InvalidError::OverlappingColors);
|
2022-07-28 21:37:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Calculate the union of all pieces.
|
|
|
|
let combined =
|
|
|
|
Piece::iter().fold(Bitboard::EMPTY, |board, p| board | self.piece_occupancy(p));
|
|
|
|
|
|
|
|
// Ensure that the pre-computed version is accurate.
|
|
|
|
if combined != self.combined_occupancy() {
|
2024-04-01 21:21:05 +02:00
|
|
|
return Err(InvalidError::ErroneousCombinedOccupancy);
|
2022-07-28 21:37:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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)) {
|
2024-04-01 21:21:05 +02:00
|
|
|
return Err(InvalidError::ErroneousCombinedOccupancy);
|
2022-07-28 21:37:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
for color in Color::iter() {
|
2022-08-12 16:17:03 +02:00
|
|
|
for piece in Piece::iter() {
|
|
|
|
// Check that we have the expected number of piecese.
|
|
|
|
let count = self.occupancy(piece, color).count();
|
|
|
|
let possible = match piece {
|
2024-04-01 21:21:05 +02:00
|
|
|
Piece::King => count <= 1,
|
2022-08-12 16:17:03 +02:00
|
|
|
Piece::Pawn => count <= 8,
|
|
|
|
Piece::Queen => count <= 9,
|
|
|
|
_ => count <= 10,
|
|
|
|
};
|
|
|
|
if !possible {
|
2024-04-01 21:21:05 +02:00
|
|
|
return Err(InvalidError::TooManyPieces);
|
2022-08-12 16:17:03 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-01 21:21:05 +02:00
|
|
|
// Check that we have a king
|
|
|
|
if self.occupancy(Piece::King, color).count() != 1 {
|
|
|
|
return Err(InvalidError::MissingKing);
|
|
|
|
}
|
|
|
|
|
2022-08-12 16:17:03 +02:00
|
|
|
// Check that don't have too many pieces in total
|
|
|
|
if self.color_occupancy(color).count() > 16 {
|
2024-04-01 21:21:05 +02:00
|
|
|
return Err(InvalidError::TooManyPieces);
|
2022-07-28 21:37:47 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-12 16:17:51 +02:00
|
|
|
// Check that pawns aren't in first/last rank.
|
|
|
|
if !(self.piece_occupancy(Piece::Pawn)
|
|
|
|
& (Rank::First.into_bitboard() | Rank::Eighth.into_bitboard()))
|
|
|
|
.is_empty()
|
|
|
|
{
|
2024-04-01 21:21:05 +02:00
|
|
|
return Err(InvalidError::InvalidPawnPosition);
|
2022-08-12 16:17:51 +02:00
|
|
|
}
|
|
|
|
|
2022-07-28 21:37:47 +02:00
|
|
|
// Verify that rooks and kings that are allowed to castle have not been moved.
|
|
|
|
for color in Color::iter() {
|
|
|
|
let castle_rights = self.castle_rights(color);
|
|
|
|
|
|
|
|
// Nothing to check if there are no castlings allowed.
|
|
|
|
if castle_rights == CastleRights::NoSide {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2024-04-01 02:29:39 +02:00
|
|
|
let actual_rooks = self.occupancy(Piece::Rook, color);
|
2022-07-28 21:37:47 +02:00
|
|
|
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 {
|
2024-04-01 21:21:05 +02:00
|
|
|
return Err(InvalidError::InvalidCastlingRights);
|
2022-07-28 21:37:47 +02:00
|
|
|
}
|
|
|
|
|
2024-04-01 02:29:39 +02:00
|
|
|
let actual_king = self.occupancy(Piece::King, color);
|
2022-07-28 21:37:47 +02:00
|
|
|
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() {
|
2024-04-01 21:21:05 +02:00
|
|
|
return Err(InvalidError::InvalidCastlingRights);
|
2022-07-28 21:37:47 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// The current en-passant target square must be empty, right behind an opponent's pawn.
|
|
|
|
if let Some(square) = self.en_passant() {
|
|
|
|
if !(self.combined_occupancy() & square).is_empty() {
|
2024-04-01 21:21:05 +02:00
|
|
|
return Err(InvalidError::InvalidEnPassant);
|
2022-07-28 21:37:47 +02:00
|
|
|
}
|
2024-04-01 02:29:39 +02:00
|
|
|
let opponent_pawns = self.occupancy(Piece::Pawn, !self.current_player());
|
2022-07-28 21:37:47 +02:00
|
|
|
let double_pushed_pawn = self
|
|
|
|
.current_player()
|
|
|
|
.backward_direction()
|
|
|
|
.move_board(square.into_bitboard());
|
|
|
|
if (opponent_pawns & double_pushed_pawn).is_empty() {
|
2024-04-01 21:21:05 +02:00
|
|
|
return Err(InvalidError::InvalidEnPassant);
|
2022-07-28 21:37:47 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-29 19:27:17 +02:00
|
|
|
// Check that kings don't touch each other.
|
2024-04-01 02:29:39 +02:00
|
|
|
let white_king = self.occupancy(Piece::King, Color::White);
|
|
|
|
let black_king = self.occupancy(Piece::King, Color::Black);
|
2022-07-29 19:27:17 +02:00
|
|
|
// 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() {
|
2024-04-01 21:21:05 +02:00
|
|
|
return Err(InvalidError::NeighbouringKings);
|
2022-07-29 19:27:17 +02:00
|
|
|
}
|
|
|
|
|
2022-07-30 11:58:18 +02:00
|
|
|
// Check that the opponent is not currently in check.
|
|
|
|
if !self.compute_checkers(!self.current_player()).is_empty() {
|
2024-04-01 21:21:05 +02:00
|
|
|
return Err(InvalidError::OpponentInCheck);
|
2022-07-30 11:58:18 +02:00
|
|
|
}
|
2022-07-28 21:37:47 +02:00
|
|
|
|
2024-04-01 21:21:05 +02:00
|
|
|
Ok(())
|
2022-07-28 21:37:47 +02:00
|
|
|
}
|
2022-07-30 11:58:18 +02:00
|
|
|
|
|
|
|
/// Compute all pieces that are currently threatening the given [Color]'s king.
|
|
|
|
fn compute_checkers(&self, color: Color) -> Bitboard {
|
|
|
|
// Unwrap is fine, there should always be exactly one king per color
|
|
|
|
let king = (self.occupancy(Piece::King, color)).try_into().unwrap();
|
|
|
|
|
|
|
|
let opponent = !color;
|
|
|
|
|
|
|
|
// No need to remove our pieces from the generated moves, we just want to check if we
|
|
|
|
// intersect with the opponent's pieces, rather than generate only valid moves.
|
|
|
|
let bishops = {
|
|
|
|
let queens = self.occupancy(Piece::Queen, opponent);
|
|
|
|
let bishops = self.occupancy(Piece::Bishop, opponent);
|
|
|
|
let bishop_attacks = movegen::bishop_moves(king, self.combined_occupancy());
|
|
|
|
(queens | bishops) & bishop_attacks
|
|
|
|
};
|
|
|
|
let rooks = {
|
|
|
|
let queens = self.occupancy(Piece::Queen, opponent);
|
|
|
|
let rooks = self.occupancy(Piece::Rook, opponent);
|
|
|
|
let rook_attacks = movegen::rook_moves(king, self.combined_occupancy());
|
|
|
|
(queens | rooks) & rook_attacks
|
|
|
|
};
|
|
|
|
let knights = {
|
|
|
|
let knights = self.occupancy(Piece::Knight, opponent);
|
|
|
|
let knight_attacks = movegen::knight_moves(king);
|
|
|
|
knights & knight_attacks
|
|
|
|
};
|
|
|
|
let pawns = {
|
|
|
|
let pawns = self.occupancy(Piece::Pawn, opponent);
|
|
|
|
let pawn_attacks = movegen::pawn_attacks(color, king);
|
|
|
|
pawns & pawn_attacks
|
|
|
|
};
|
|
|
|
|
|
|
|
bishops | rooks | knights | pawns
|
|
|
|
}
|
2022-07-28 21:19:40 +02:00
|
|
|
}
|
2022-07-28 21:45:04 +02:00
|
|
|
|
|
|
|
/// Use the starting position as a default value, corresponding to the
|
|
|
|
/// "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" FEN string
|
|
|
|
impl Default for ChessBoard {
|
|
|
|
fn default() -> Self {
|
|
|
|
Self {
|
|
|
|
piece_occupancy: [
|
|
|
|
// King
|
|
|
|
Square::E1 | Square::E8,
|
|
|
|
// Queen
|
|
|
|
Square::D1 | Square::D8,
|
|
|
|
// Rook
|
|
|
|
Square::A1 | Square::A8 | Square::H1 | Square::H8,
|
|
|
|
// Bishop
|
|
|
|
Square::C1 | Square::C8 | Square::F1 | Square::F8,
|
|
|
|
// Knight
|
|
|
|
Square::B1 | Square::B8 | Square::G1 | Square::G8,
|
|
|
|
// Pawn
|
|
|
|
Rank::Second.into_bitboard() | Rank::Seventh.into_bitboard(),
|
|
|
|
],
|
|
|
|
color_occupancy: [
|
|
|
|
Rank::First.into_bitboard() | Rank::Second.into_bitboard(),
|
|
|
|
Rank::Seventh.into_bitboard() | Rank::Eighth.into_bitboard(),
|
|
|
|
],
|
|
|
|
combined_occupancy: Rank::First.into_bitboard()
|
|
|
|
| Rank::Second.into_bitboard()
|
|
|
|
| Rank::Seventh.into_bitboard()
|
|
|
|
| Rank::Eighth.into_bitboard(),
|
|
|
|
castle_rights: [CastleRights::BothSides; 2],
|
|
|
|
en_passant: None,
|
|
|
|
half_move_clock: 0,
|
|
|
|
total_plies: 0,
|
|
|
|
side: Color::White,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-07-28 21:37:47 +02:00
|
|
|
|
2022-07-28 21:38:50 +02:00
|
|
|
/// Return a [ChessBoard] from the given FEN string.
|
|
|
|
impl FromFen for ChessBoard {
|
|
|
|
type Err = FenError;
|
|
|
|
|
|
|
|
fn from_fen(s: &str) -> Result<Self, Self::Err> {
|
|
|
|
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::<Square>::from_fen(en_passant_square)?;
|
|
|
|
|
|
|
|
let half_move_clock = half_move_clock
|
|
|
|
.parse::<u8>()
|
|
|
|
.map_err(|_| FenError::InvalidFen)?;
|
|
|
|
let full_move_counter = full_move_counter
|
|
|
|
.parse::<u32>()
|
|
|
|
.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,
|
|
|
|
};
|
|
|
|
|
2024-04-01 21:28:16 +02:00
|
|
|
if let Err(err) = res.validate() {
|
|
|
|
return Err(FenError::InvalidPosition(err));
|
2022-07-28 21:38:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
Ok(res)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-28 21:37:47 +02:00
|
|
|
#[cfg(test)]
|
|
|
|
mod test {
|
2022-07-28 21:38:50 +02:00
|
|
|
use crate::board::MoveBuilder;
|
|
|
|
|
2022-07-28 21:37:47 +02:00
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn valid() {
|
|
|
|
let default_position = ChessBoard::default();
|
|
|
|
assert!(default_position.is_valid());
|
|
|
|
}
|
|
|
|
|
|
|
|
#[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,
|
|
|
|
};
|
2024-04-01 21:42:28 +02:00
|
|
|
assert_eq!(
|
|
|
|
position.validate().err().unwrap(),
|
|
|
|
InvalidError::OverlappingPieces,
|
|
|
|
);
|
2022-07-28 21:37:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[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,
|
|
|
|
};
|
2024-04-01 21:42:28 +02:00
|
|
|
assert_eq!(
|
|
|
|
position.validate().err().unwrap(),
|
|
|
|
InvalidError::OverlappingColors,
|
|
|
|
);
|
2022-07-28 21:37:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[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,
|
|
|
|
};
|
2024-04-01 21:42:28 +02:00
|
|
|
assert_eq!(
|
|
|
|
position.validate().err().unwrap(),
|
|
|
|
InvalidError::ErroneousCombinedOccupancy,
|
|
|
|
);
|
2022-07-28 21:37:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[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,
|
|
|
|
};
|
2024-04-01 21:42:28 +02:00
|
|
|
assert_eq!(
|
|
|
|
position.validate().err().unwrap(),
|
|
|
|
InvalidError::ErroneousCombinedOccupancy,
|
|
|
|
);
|
2022-07-28 21:37:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn invalid_multiple_kings() {
|
2024-04-01 22:20:20 +02:00
|
|
|
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::<ChessBoard>::try_into(builder)
|
2022-07-28 21:37:47 +02:00
|
|
|
};
|
2024-04-01 22:20:20 +02:00
|
|
|
assert_eq!(res.err().unwrap(), InvalidError::TooManyPieces);
|
2022-07-28 21:37:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn invalid_castling_rights_no_rooks() {
|
2024-04-01 22:20:20 +02:00
|
|
|
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::<ChessBoard>::try_into(builder)
|
2022-07-28 21:37:47 +02:00
|
|
|
};
|
2024-04-01 22:20:20 +02:00
|
|
|
assert_eq!(res.err().unwrap(), InvalidError::InvalidCastlingRights);
|
2022-07-28 21:37:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn invalid_castling_rights_moved_king() {
|
2024-04-01 22:20:20 +02:00
|
|
|
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::<ChessBoard>::try_into(builder)
|
2022-07-29 19:27:17 +02:00
|
|
|
};
|
2024-04-01 22:20:20 +02:00
|
|
|
assert_eq!(res.err().unwrap(), InvalidError::InvalidCastlingRights);
|
2022-07-29 19:27:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn invalid_kings_next_to_each_other() {
|
2024-04-01 22:20:20 +02:00
|
|
|
let res = {
|
|
|
|
let mut builder = ChessBoardBuilder::new();
|
|
|
|
builder[Square::E1] = Some((Piece::King, Color::White));
|
|
|
|
builder[Square::E2] = Some((Piece::King, Color::Black));
|
|
|
|
TryInto::<ChessBoard>::try_into(builder)
|
2022-07-30 11:58:18 +02:00
|
|
|
};
|
2024-04-01 22:20:20 +02:00
|
|
|
assert_eq!(res.err().unwrap(), InvalidError::NeighbouringKings);
|
2022-07-30 11:58:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn invalid_opponent_in_check() {
|
2024-04-01 22:20:20 +02:00
|
|
|
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::<ChessBoard>::try_into(builder)
|
2022-07-28 21:37:47 +02:00
|
|
|
};
|
2024-04-01 22:20:20 +02:00
|
|
|
assert_eq!(res.err().unwrap(), InvalidError::OpponentInCheck);
|
2022-07-28 21:37:47 +02:00
|
|
|
}
|
2022-07-28 21:38:50 +02:00
|
|
|
|
2022-08-12 16:17:51 +02:00
|
|
|
#[test]
|
|
|
|
fn invalid_pawn_on_first_rank() {
|
2024-04-01 22:20:20 +02:00
|
|
|
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::<ChessBoard>::try_into(builder)
|
2022-08-12 16:17:51 +02:00
|
|
|
};
|
2024-04-01 22:20:20 +02:00
|
|
|
assert_eq!(res.err().unwrap(), InvalidError::InvalidPawnPosition);
|
2022-08-12 16:17:51 +02:00
|
|
|
}
|
|
|
|
|
2022-08-12 16:17:03 +02:00
|
|
|
#[test]
|
|
|
|
fn invalid_too_many_pieces() {
|
2024-04-01 22:20:20 +02:00
|
|
|
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::<ChessBoard>::try_into(builder)
|
2022-08-12 16:17:03 +02:00
|
|
|
};
|
2024-04-01 22:20:20 +02:00
|
|
|
assert_eq!(res.err().unwrap(), InvalidError::TooManyPieces);
|
2022-08-12 16:17:03 +02:00
|
|
|
}
|
|
|
|
|
2022-07-30 12:02:41 +02:00
|
|
|
#[test]
|
|
|
|
fn checkers() {
|
|
|
|
let position = ChessBoard {
|
|
|
|
piece_occupancy: [
|
|
|
|
// King
|
|
|
|
Square::E2 | Square::E8,
|
|
|
|
// Queen
|
|
|
|
Square::E7 | Square::H2,
|
|
|
|
// Rook
|
|
|
|
Square::A2 | Square::E1,
|
|
|
|
// Bishop
|
|
|
|
Square::D3 | Square::F3,
|
|
|
|
// Knight
|
|
|
|
Square::C1 | Square::G1,
|
|
|
|
// Pawn
|
|
|
|
Bitboard::EMPTY,
|
|
|
|
],
|
|
|
|
color_occupancy: [
|
|
|
|
Square::C1 | Square::D3 | Square::E1 | Square::E2 | Square::H2,
|
|
|
|
Square::A2 | Square::E7 | Square::E8 | Square::F3 | Square::G1,
|
|
|
|
],
|
|
|
|
combined_occupancy: Square::A2
|
|
|
|
| Square::C1
|
|
|
|
| Square::D3
|
|
|
|
| Square::E1
|
|
|
|
| Square::E2
|
|
|
|
| Square::E7
|
|
|
|
| Square::E8
|
|
|
|
| Square::F3
|
|
|
|
| Square::G1
|
|
|
|
| Square::H2,
|
|
|
|
castle_rights: [CastleRights::NoSide; 2],
|
|
|
|
en_passant: None,
|
|
|
|
half_move_clock: 0,
|
|
|
|
total_plies: 0,
|
|
|
|
side: Color::White,
|
|
|
|
};
|
|
|
|
assert_eq!(
|
|
|
|
position.checkers(),
|
|
|
|
Square::A2 | Square::E7 | Square::F3 | Square::G1
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-07-28 21:38:50 +02:00
|
|
|
#[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
|
|
|
|
);
|
|
|
|
}
|
2022-07-28 21:39:29 +02:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn do_move() {
|
|
|
|
// Start from default position
|
|
|
|
let mut position = ChessBoard::default();
|
|
|
|
// Modify it to account for e4 move
|
|
|
|
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!(
|
|
|
|
position,
|
|
|
|
ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1")
|
|
|
|
.unwrap()
|
|
|
|
);
|
|
|
|
// 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!(
|
|
|
|
position,
|
|
|
|
ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2")
|
|
|
|
.unwrap()
|
|
|
|
);
|
|
|
|
// 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!(
|
|
|
|
position,
|
|
|
|
ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2 ")
|
|
|
|
.unwrap()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn do_move_and_undo() {
|
|
|
|
// Start from default position
|
|
|
|
let mut position = ChessBoard::default();
|
|
|
|
// Modify it to account for e4 move
|
|
|
|
let move_1 = MoveBuilder {
|
|
|
|
piece: Piece::Pawn,
|
|
|
|
start: Square::E2,
|
|
|
|
destination: Square::E4,
|
|
|
|
capture: None,
|
|
|
|
promotion: None,
|
|
|
|
en_passant: false,
|
|
|
|
double_step: true,
|
|
|
|
castling: false,
|
|
|
|
}
|
|
|
|
.into();
|
|
|
|
let state_1 = position.do_move(move_1);
|
|
|
|
// And now c5
|
|
|
|
let move_2 = MoveBuilder {
|
|
|
|
piece: Piece::Pawn,
|
|
|
|
start: Square::C7,
|
|
|
|
destination: Square::C5,
|
|
|
|
capture: None,
|
|
|
|
promotion: None,
|
|
|
|
en_passant: false,
|
|
|
|
double_step: true,
|
|
|
|
castling: false,
|
|
|
|
}
|
|
|
|
.into();
|
|
|
|
let state_2 = position.do_move(move_2);
|
|
|
|
// Finally, Nf3
|
|
|
|
let move_3 = MoveBuilder {
|
|
|
|
piece: Piece::Knight,
|
|
|
|
start: Square::G1,
|
|
|
|
destination: Square::F3,
|
|
|
|
capture: None,
|
|
|
|
promotion: None,
|
|
|
|
en_passant: false,
|
|
|
|
double_step: false,
|
|
|
|
castling: false,
|
|
|
|
}
|
|
|
|
.into();
|
|
|
|
let state_3 = position.do_move(move_3);
|
|
|
|
// Now revert each move one-by-one
|
|
|
|
position.undo_move(move_3, state_3);
|
|
|
|
assert_eq!(
|
|
|
|
position,
|
|
|
|
ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2")
|
|
|
|
.unwrap()
|
|
|
|
);
|
|
|
|
position.undo_move(move_2, state_2);
|
|
|
|
assert_eq!(
|
|
|
|
position,
|
|
|
|
ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1")
|
|
|
|
.unwrap()
|
|
|
|
);
|
|
|
|
position.undo_move(move_1, state_1);
|
|
|
|
assert_eq!(
|
|
|
|
position,
|
|
|
|
ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
|
|
|
|
.unwrap()
|
|
|
|
);
|
|
|
|
}
|
2022-07-28 21:37:47 +02:00
|
|
|
}
|