pub mod castling_rights;
pub mod initial_position;
pub mod moves;
mod navigable;
pub mod ply_clock;
mod pseudolegal;
pub mod timer;
pub mod turn;
pub use self::{
castling_rights::CastlingRights,
initial_position::InitialPosition,
moves::{Legality, Move},
ply_clock::PlyClock,
pseudolegal::Pseudolegality,
timer::{Timer, Timers},
turn::Turn,
};
use crate::{
board::{Board, Kings, STAUNTON_PATTERN},
counter::Counter,
error::{Err, Result},
openings::default_tree,
openings::{Opening, OpeningTree},
pieces::{
Color::{self, Black, White},
Piece,
Type::{self, Bishop, King, Knight, Pawn, Rook},
},
sq,
squares::{Direction, Rank, Square},
};
use compact_str::CompactString;
use fxhash::hash;
use smallvec::SmallVec;
use std::{
fmt::Display,
hash::Hash,
io::{BufRead, BufReader},
str::FromStr,
sync::OnceLock,
};
const DIRECTIONS_AND_PIECE_TYPES: [([Direction; 4], [Type; 2]); 2] = [
(Direction::rooks(), [Type::Rook, Type::Queen]),
(Direction::bishops(), [Type::Bishop, Type::Queen]),
];
#[derive(Debug, Clone)]
pub struct ChessPosition {
pub(crate) board: Board,
pub(crate) turn: Turn,
pub(crate) castling_rights: CastlingRights,
pub(crate) en_passant_final_square: Option<Square>,
pub(crate) kings: Kings,
pub(crate) ply_clock: Option<PlyClock>,
pub(crate) move_history: Option<Vec<Move>>,
pub(crate) initial_position: InitialPosition,
pub(crate) repetition_counter: Option<Counter<usize>>,
pub(crate) timers: Option<Timers>,
pub(crate) cached_result: Option<PositionStatus>,
}
impl PartialEq for ChessPosition {
fn eq(&self, other: &Self) -> bool {
self.board == other.board
&& self.turn == other.turn
&& self.castling_rights == other.castling_rights
&& self.en_passant_final_square == other.en_passant_final_square
}
}
impl Eq for ChessPosition {}
impl Hash for ChessPosition {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.board.hash(state);
self.turn.hash(state);
self.castling_rights.hash(state);
if self.en_passant_move_exists() {
self.en_passant_final_square.hash(state);
}
}
}
impl Default for ChessPosition {
fn default() -> Self {
Self::staunton().clone()
}
}
impl Display for ChessPosition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.board.fmt(f)
}
}
impl ChessPosition {
pub(crate) fn staunton() -> &'static Self {
static STAUNTON_POSITION: OnceLock<ChessPosition> = OnceLock::new();
STAUNTON_POSITION.get_or_init(|| Self {
initial_position: InitialPosition::Standard,
board: Board::from_fen_slice(STAUNTON_PATTERN).unwrap(),
turn: Turn(White),
castling_rights: CastlingRights::new(),
en_passant_final_square: None,
kings: Kings {
white: sq!(E1),
black: sq!(E8),
},
ply_clock: Some(PlyClock::new()),
move_history: Some(Vec::new()),
repetition_counter: Some(Counter::new()),
timers: None,
cached_result: None,
})
}
pub fn fischer() -> anyhow::Result<Self> {
const PATH: &str = "src/data/fischer_fens.fen";
let idx = fastrand::u16(0..960);
let file = std::fs::File::open(PATH)?;
let buf = BufReader::new(file);
let Some(Ok(fen)) = buf.lines().nth(idx as usize) else {
return Err(Err::FileReadError(PATH).into());
};
let position = Self::from_fen(&fen)?;
Ok(position)
}
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn builder() -> PositionBuilder {
PositionBuilder::default()
}
#[must_use]
pub const fn get(&self, square: Square) -> &Option<Piece> {
self.board.get(square)
}
fn set(&mut self, square: Square, value: Option<Piece>) {
if let Some(piece) = value {
if piece.piece_type == Type::King {
self.kings.set(piece.color, square);
}
}
self.board.set(square, value);
}
pub fn set_timers(&mut self, allocated: u32, increment: u32) -> Result<()> {
if self.timers.is_some() {
return Err(Err::TimersAlreadySetError);
}
self.timers = Some(Timers::new(allocated, increment));
Ok(())
}
fn verify_en_passant_final_square(&self) -> Result<()> {
let Some(en_passant_final_square) = self.en_passant_final_square else {
return Ok(());
};
let expected: Rank = match self.turn.color() {
Color::White => Rank::Six,
Color::Black => Rank::Three,
};
let actual: Rank = en_passant_final_square.rank();
if expected != actual {
return Err(Err::EnPassantRankError(en_passant_final_square));
}
Ok(())
}
fn verify_cannot_capture_king(&self) -> Result<()> {
let other_color = self.turn.color().other();
if self.is_in_check(other_color) {
Err(Err::PlayerCanCaptureKingError(other_color))
} else {
Ok(())
}
}
#[must_use]
pub fn is_timeout(&self) -> Option<PositionStatus> {
let mut player_timed_out: Option<Color> = None;
if let Some(ref timers) = self.timers {
for color in Color::all() {
if timers.get(color).timeout() {
player_timed_out = Some(color);
break;
}
}
}
let player_timed_out: Color = player_timed_out?;
let other_player: Color = player_timed_out.other();
let opponent_piece_types: SmallVec<[Type; 16]> = self
.board
.pieces(Some(other_player))
.iter_mut()
.filter_map(|(_, pc)| {
if pc.piece_type == King {
None
} else {
Some(pc.piece_type)
}
})
.collect();
if opponent_piece_types.is_empty() {
return Some(PositionStatus::TimeoutVsInsufficient);
}
if opponent_piece_types.len() == 1 && [Knight, Bishop].contains(&opponent_piece_types[0]) {
return Some(PositionStatus::TimeoutVsInsufficient);
}
if opponent_piece_types.into_iter().all(|t| t == Knight)
&& !self
.board
.pieces(Some(player_timed_out))
.into_iter()
.any(|(_, pc)| pc.piece_type == Pawn)
{
return Some(PositionStatus::TimeoutVsInsufficient);
}
Some(PositionStatus::Timeout(player_timed_out))
}
fn update_counter(&mut self) {
if self.repetition_counter.is_some() {
let position = self.to_hashing_position();
self.repetition_counter
.as_mut()
.unwrap()
.increment(hash(&position));
}
}
#[must_use]
pub fn to_hashing_position(&self) -> Self {
Self {
board: self.board.clone(),
turn: self.turn.clone(),
kings: self.kings,
castling_rights: self.castling_rights,
en_passant_final_square: self.en_passant_final_square,
ply_clock: Some(PlyClock::new()),
move_history: None,
initial_position: InitialPosition::Unspecified,
repetition_counter: None,
timers: None,
cached_result: None,
}
}
pub fn from_fen(fen: &str) -> Result<Self> {
let mut split_fen = fen.split(' ');
let Some(piece_segment) = split_fen.next() else {
return Err(Err::EmptyFenError);
};
let board = Board::from_fen_slice(piece_segment)?;
let Some(turn_segment) = split_fen.next() else {
return Err(Err::FenMissingFieldError(fen.into(), "turn indicator"));
};
let turn = Turn::from_fen_slice(turn_segment)?;
let castling_rights = CastlingRights::from_fen(fen)?;
split_fen.next();
let Some(en_passant_segment) = split_fen.next() else {
return Err(Err::FenMissingFieldError(
fen.into(),
"en passant target square indicator",
));
};
let en_passant_final_square = Square::from_str(en_passant_segment).ok();
let ply_clock = PlyClock::from_fen(fen, Some(turn.color()))?;
let builder = PositionBuilder {
board: Some(board),
turn: Some(turn),
castling_rights: Some(castling_rights),
en_passant_final_square,
ply_clock: Some(ply_clock),
move_history: Some(Vec::new()),
initial_position: Some(InitialPosition::Unspecified),
repetition_counter: Some(Counter::new()),
timers: None,
cached_result: None,
};
let mut output: Self = builder.try_into()?;
output.initial_position = InitialPosition::from(&output);
Ok(output)
}
#[must_use]
pub fn fen(&self) -> String {
let mut output = String::new();
output.push_str(&self.board.fen_repr());
output.push(' ');
output.push(self.turn.char());
output.push(' ');
output.push_str(&self.castling_rights.fen_repr());
output.push(' ');
match self.en_passant_final_square {
Some(sq) => output.push_str(&sq.to_lowercase_string()),
None => output.push('-'),
}
if let Some(ref ply_clock) = self.ply_clock {
output.push(' ');
output.push_str(&ply_clock.halfmove_clock().to_string());
output.push(' ');
output.push_str(&ply_clock.fullmove_number().to_string());
}
output.trim_end().to_string()
}
pub fn apply_move(&mut self, m: Move) -> Result<Legality> {
if let Pseudolegality::Illegal(s) = m.is_pseudolegal(self) {
return Ok(Legality::Illegal(s));
}
let result = m.pseudolegal_is_legal(self)?;
if let Legality::Illegal(_) = result {
return Ok(result);
}
self.apply_legal_move(m)?;
Ok(result)
}
#[must_use]
pub fn is_threatened(&self, square: Square, color: Color) -> bool {
for (directions, piece_types) in DIRECTIONS_AND_PIECE_TYPES {
for direction in directions {
for sq in square.iter_dir(direction) {
if let Some(piece) = self.get(sq) {
if piece.color != color && piece_types.contains(&piece.piece_type) {
return true;
}
break;
}
}
}
}
if self.knight_pseudolegal(square, color).any(|m| {
if let Move::Standard { final_square, .. } = m {
if let Some(piece) = self.get(final_square) {
if piece.color != color && piece.piece_type == Knight {
return true;
}
}
}
false
}) {
return true;
}
if self.king_pseudolegal(square, color).any(|m| {
if let Move::Standard { final_square, .. } = m {
if let Some(piece) = self.get(final_square) {
if piece.color != color && piece.piece_type == King {
return true;
}
}
}
false
}) {
return true;
}
for sq in square.vulnerable_to_pawns(color.other()) {
let Some(sq) = sq else { break };
if let Some(piece) = self.get(sq) {
if piece.color != color && piece.piece_type == Type::Pawn {
return true;
}
}
}
let other_color = color.other();
if let Some(en_passant_final_square) = self.en_passant_final_square {
if let Some(forward_square) = square.step_dir(Direction::pawn_forward(other_color), 1) {
if forward_square == en_passant_final_square {
let rank = square.rank();
for file in square.file().adjacent() {
let Some(file) = file else { break };
let threat_square = Square(file, rank);
if let Some(threatening_piece) = self.get(threat_square) {
if threatening_piece.piece_type == Type::Pawn
&& threatening_piece.color == other_color
{
return true;
}
}
}
}
}
}
false
}
#[must_use]
pub fn is_in_check(&self, color: Color) -> bool {
self.is_threatened(self.kings.get(color), color)
}
#[must_use]
pub fn checks(&self, color: Color) -> Option<Vec<Move>> {
let square = self.kings.get(color);
let threats: Vec<_> = self.pseudolegal_threats(square, color).collect();
if threats.is_empty() {
None
} else {
Some(threats)
}
}
#[must_use]
pub fn checkmated(&self, color: Color) -> bool {
for threat in self.pseudolegal_threats(self.kings.get(color), color) {
if !threat
.can_escape(self)
.expect("Threat escapability failed to be read.")
{
return true;
}
}
false
}
#[must_use]
pub fn can_attack(&self, square: Square, color: Color) -> bool {
self.can_attack_with_rbq(square, color).unwrap_or_else(|| {
self.can_attack_with_knight(square, color)
.unwrap_or_else(|| {
self.can_attack_with_king(square, color).unwrap_or_else(|| {
self.can_attack_with_pawn(square, color).unwrap_or_default()
})
})
})
}
fn can_attack_with_pawn(&self, square: Square, color: Color) -> Option<bool> {
for sq in square.vulnerable_to_pawns(color.other()) {
let Some(sq) = sq else { break };
if let Some(piece) = self.get(sq) {
if piece.color != color && piece.piece_type == Type::Pawn {
let m = Move::Standard {
initial_square: sq,
final_square: square,
is_capture: true,
piece_color: piece.color,
piece_type: Pawn,
timer_update: None,
};
if let Legality::Legal | Legality::NeedsPromotion =
m.pseudolegal_is_legal(self).unwrap()
{
return Some(true);
}
}
}
}
let other_color = color.other();
if let Some(en_passant_final_square) = self.en_passant_final_square {
if let Some(forward_square) = square.step_dir(Direction::pawn_forward(other_color), 1) {
if forward_square == en_passant_final_square {
let rank = square.rank();
for file in square.file().adjacent() {
let Some(file) = file else { break };
let threat_square = Square(file, rank);
if let Some(threatening_piece) = self.get(threat_square) {
if threatening_piece.piece_type == Type::Pawn
&& threatening_piece.color == other_color
{
let m = Move::EnPassant {
initial_square: threat_square,
capture_square: square,
final_square: forward_square,
piece_color: other_color,
timer_update: None,
};
if m.pseudolegal_is_legal(self).unwrap() == Legality::Legal {
return Some(true);
}
}
}
}
}
}
}
None
}
fn can_attack_with_king(&self, square: Square, color: Color) -> Option<bool> {
for m in self.king_pseudolegal(square, color) {
if let Move::Standard { final_square, .. } = m {
if let Some(piece) = self.get(final_square) {
if piece.color != color && piece.piece_type == King {
let m = Move::Standard {
initial_square: final_square,
final_square: square,
is_capture: true,
piece_type: King,
piece_color: piece.color,
timer_update: None,
};
if m.pseudolegal_is_legal(self).unwrap() == Legality::Legal {
return Some(true);
}
}
}
}
}
None
}
fn can_attack_with_knight(&self, square: Square, color: Color) -> Option<bool> {
for m in self.knight_pseudolegal(square, color) {
if let Move::Standard { final_square, .. } = m {
if let Some(piece) = self.get(final_square) {
if piece.color != color && piece.piece_type == Knight {
let m = Move::Standard {
initial_square: final_square,
final_square: square,
is_capture: true,
piece_type: Knight,
piece_color: piece.color,
timer_update: None,
};
if m.pseudolegal_is_legal(self).unwrap() == Legality::Legal {
return Some(true);
}
}
}
}
}
None
}
fn can_attack_with_rbq(&self, square: Square, color: Color) -> Option<bool> {
for (directions, piece_types) in DIRECTIONS_AND_PIECE_TYPES {
for direction in directions {
for sq in square.iter_dir(direction) {
if let Some(piece) = self.get(sq) {
if piece.color != color && piece_types.contains(&piece.piece_type) {
let m = Move::Standard {
initial_square: sq,
final_square: square,
is_capture: true,
piece_color: piece.color,
piece_type: piece.piece_type,
timer_update: None,
};
if m.pseudolegal_is_legal(self).unwrap() == Legality::Legal {
return Some(true);
}
}
break;
}
}
}
}
None
}
pub fn apply_san(&mut self, san: &str) -> Result<()> {
let m = Move::read_san(san, self)?;
match m.pseudolegal_is_legal(self)? {
Legality::Legal => self.apply_legal_move(m)?,
Legality::Illegal(e) => return Err(Err::IllegalSanError(san.into(), e)),
Legality::NeedsPromotion => {
return Err(Err::IllegalSanError(
san.into(),
"Pawn must be promoted when moved to final rank.".into(),
))
}
}
Ok(())
}
pub fn apply_sans<'a, I: Iterator<Item = &'a str>>(&mut self, sans: I) -> Result<()> {
for san in sans {
self.apply_san(san)?;
}
Ok(())
}
pub fn apply_legal_move(&mut self, m: Move) -> Result<()> {
self.apply_move_to_board(m)?;
self.en_passant_final_square = m.skipped();
self.turn.alternate();
self.castling_rights.update(m)?;
self.update_counter();
if let Some(elapsed) = m.timer_update() {
let color = m.color();
if let Some(ref mut timers) = self.timers {
if let Some(ref ply_clock) = self.ply_clock {
timers.get_mut(color).update(ply_clock.total(), elapsed);
}
}
}
if let Some(ref mut ply_clock) = self.ply_clock {
ply_clock.advance(&m);
}
if let Some(ref mut move_history) = self.move_history {
move_history.push(m);
}
Ok(())
}
fn apply_move_to_board(&mut self, m: Move) -> Result<()> {
match m {
Move::Standard {
initial_square,
final_square,
..
} => {
let Some(moving_piece) = *self.get(initial_square) else {
return Err(Err::ApplyMoveError(
m,
format!("no piece at initial square {initial_square}"),
));
};
self.set(initial_square, None);
self.set(final_square, Some(moving_piece));
}
Move::EnPassant {
initial_square,
capture_square,
final_square,
..
} => {
let Some(moving_piece) = *self.get(initial_square) else {
return Err(Err::ApplyMoveError(
m,
format!("no piece at initial square {initial_square}"),
));
};
self.set(initial_square, None);
self.set(capture_square, None);
self.set(final_square, Some(moving_piece));
}
Move::Castle { color, side, .. } => {
let Some(king_initial_square) =
self.castling_rights.initial_square(King, color, None)?
else {
return Err(Err::ApplyMoveError(
m,
format!("expected initial square for {color} king"),
));
};
let king_final_square = Square::castling_king_final(color, side);
let Some(rook_initial_square) =
self.castling_rights
.initial_square(Rook, color, Some(side))?
else {
return Err(Err::ApplyMoveError(
m,
format!(
"Failed to castle because {color} {side} Rook's square is not \
identified in `initial_squares`."
),
));
};
let rook_final_square = Square::castling_rook_final(color, side);
let Some(king) = *self.get(king_initial_square) else {
return Err(Err::ApplyMoveError(
m,
format!("No piece at king's initial square {king_final_square}."),
));
};
let Some(rook) = *self.get(rook_initial_square) else {
return Err(Err::ApplyMoveError(
m,
format!("No piece at rook's initial square {rook_final_square}."),
));
};
self.set(king_initial_square, None);
self.set(rook_initial_square, None);
self.set(king_final_square, Some(king));
self.set(rook_final_square, Some(rook));
}
Move::PawnPromotion {
initial_square,
final_square,
piece_color,
new_type,
..
} => {
self.set(initial_square, None);
self.set(final_square, Some(Piece::new(new_type.into(), piece_color)));
}
};
Ok(())
}
pub fn opening(&self) -> Result<Option<&Opening>> {
let tree: &OpeningTree = default_tree();
let mut position = Self::try_from(&self.initial_position)?;
let Some(ref move_history) = self.move_history else {
return Err(Err::NoMoveHistoryError);
};
let mut cursor = tree;
let mut longest_match: Option<&Opening> = None;
for m in move_history {
let mut san = m.without_suffix(&position)?;
position.apply_legal_move(*m)?;
san.push_str(m.suffix(&position));
let Some(ref children) = cursor.children else {
break;
};
match children.get(san.as_str()) {
Some(tree) => {
cursor = tree;
if let Some(opening) = &cursor.element {
longest_match = Some(opening);
}
}
None => break,
}
}
Ok(longest_match)
}
pub fn sans(&self) -> Result<Vec<CompactString>> {
let Some(ref move_history) = self.move_history else {
return Err(Err::NoMoveHistoryError);
};
let mut position: Self = (&self.initial_position).try_into()?;
let mut output = Vec::new();
for m in move_history {
let mut san = CompactString::default();
san.push_str(&m.without_suffix(&position)?);
position.apply_legal_move(*m)?;
san.push_str(m.suffix(&position));
output.push(san);
}
Ok(output)
}
#[must_use]
pub fn is_stalemate(&self) -> bool {
let color = self.turn.color();
!self.checkmated(color) && self.stalemated_not_checkmated(color)
}
fn repetition(&self) -> Option<PositionStatus> {
match self.repetition_counter.as_ref()?.max() {
0..=2 => None,
3..=4 => Some(PositionStatus::ThreefoldRepetition),
_ => Some(PositionStatus::FivefoldRepetition),
}
}
#[must_use]
pub const fn threefold_repetition(&self) -> bool {
let Some(ref repetition_counter) = self.repetition_counter else {
return false;
};
repetition_counter.max() >= 3
}
#[must_use]
pub const fn fivefold_repetition(&self) -> bool {
let Some(ref repetition_counter) = self.repetition_counter else {
return false;
};
repetition_counter.max() >= 5
}
#[must_use]
pub fn status(&self) -> PositionStatus {
let color = self.turn.color();
let other = color.other();
self.cached_result.unwrap_or_else(|| {
if self.checkmated(color) {
PositionStatus::Checkmate(other)
} else if self.stalemated_not_checkmated(color) {
PositionStatus::Stalemate
} else if self.insufficient_material() {
PositionStatus::InsufficientMaterial
} else if let Some(status) = self.repetition() {
status
} else if let Some(status) = self.is_timeout() {
status
} else if let Some(ref ply_clock) = self.ply_clock {
if ply_clock.fifty_moves() {
PositionStatus::FiftyMoveRule
} else {
PositionStatus::InProgress
}
} else {
PositionStatus::InProgress
}
})
}
fn stalemated_not_checkmated(&self, color: Color) -> bool {
!self
.board
.pieces(Some(color))
.into_iter()
.any(|(square, Piece { piece_type, .. })| match piece_type {
Type::Queen | Type::Rook | Type::Bishop => self
.rbq_pseudolegal(square, color, piece_type.try_into().unwrap())
.any(|m| {
m.pseudolegal_is_legal(self).unwrap().as_bool()
|| m.can_legally_promote(self)
}),
Type::Pawn => self.pawn_pseudolegal(square, color).any(|m| {
m.pseudolegal_is_legal(self).unwrap().as_bool() || m.can_legally_promote(self)
}),
Type::Knight => self.knight_pseudolegal(square, color).any(|m| {
m.pseudolegal_is_legal(self).unwrap().as_bool() || m.can_legally_promote(self)
}),
Type::King => self.king_pseudolegal(square, color).any(|m| {
m.pseudolegal_is_legal(self).unwrap().as_bool() || m.can_legally_promote(self)
}),
})
}
#[must_use]
pub fn insufficient_material(&self) -> bool {
let pieces: SmallVec<[(Square, Piece); 32]> = self.board.pieces(None);
let mut white_pieces: SmallVec<[Type; 16]> = SmallVec::new();
let mut black_pieces: SmallVec<[Type; 16]> = SmallVec::new();
let mut bishop_on_white = false;
let mut bishop_on_black = false;
for (
square,
Piece {
piece_type, color, ..
},
) in pieces
{
if piece_type == King {
continue;
}
match color {
White => white_pieces.push(piece_type),
Black => black_pieces.push(piece_type),
}
if piece_type == Bishop {
match square.color() {
White => bishop_on_white = true,
Black => bishop_on_black = true,
}
}
}
let white_piece_count = white_pieces.len();
let black_piece_count = black_pieces.len();
if white_piece_count > 2 || black_piece_count > 2 {
return false;
}
if white_piece_count == 0 && black_piece_count == 0 {
return true;
}
if (white_piece_count == 0
&& black_piece_count == 1
&& [Knight, Bishop].contains(&black_pieces[0]))
|| (black_piece_count == 0
&& white_piece_count == 1
&& [Knight, Bishop].contains(&white_pieces[0]))
{
return true;
}
if white_piece_count == 1
&& black_piece_count == 1
&& white_pieces[0] == Bishop
&& black_pieces[0] == Bishop
&& !(bishop_on_black && bishop_on_white)
{
return true;
}
false
}
fn en_passant_move_exists(&self) -> bool {
if let Some(en_passant_final_square) = self.en_passant_final_square {
if self
.pseudolegal_threats(en_passant_final_square, self.turn.color().other())
.any(|m| matches!(m, Move::EnPassant { .. }))
{
return true;
}
}
false
}
}
#[derive(Default)]
pub struct PositionBuilder {
board: Option<Board>,
turn: Option<Turn>,
castling_rights: Option<CastlingRights>,
en_passant_final_square: Option<Square>,
ply_clock: Option<PlyClock>,
move_history: Option<Vec<Move>>,
initial_position: Option<InitialPosition>,
repetition_counter: Option<Counter<usize>>,
timers: Option<Timers>,
cached_result: Option<PositionStatus>,
}
impl TryFrom<PositionBuilder> for ChessPosition {
type Error = Err;
fn try_from(value: PositionBuilder) -> std::prelude::v1::Result<Self, Self::Error> {
let Some(board) = value.board else {
return Err(Err::MissingPositionField("board"));
};
let kings = Kings::try_from(&board)?;
let Some(turn) = value.turn else {
return Err(Err::MissingPositionField("turn"));
};
let Some(castling_rights) = value.castling_rights else {
return Err(Err::MissingPositionField("castling_rights"));
};
let Some(initial_position) = value.initial_position else {
return Err(Err::MissingPositionField("initial_position"));
};
let position = Self {
board,
turn,
castling_rights,
en_passant_final_square: value.en_passant_final_square,
kings,
ply_clock: value.ply_clock,
move_history: value.move_history,
initial_position,
repetition_counter: value.repetition_counter,
timers: value.timers,
cached_result: value.cached_result,
};
position.verify_cannot_capture_king()?;
position.verify_en_passant_final_square()?;
Ok(position)
}
}
#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq)]
pub enum PositionStatus {
#[default]
InProgress,
Checkmate(Color),
Resignation(Color),
Stalemate,
ThreefoldRepetition,
FivefoldRepetition,
InsufficientMaterial,
Timeout(Color),
TimeoutVsInsufficient,
FiftyMoveRule,
SeventyFiveMoveRule,
ImportedVictory(Color),
ImportedDraw,
}
impl PositionStatus {
#[must_use]
pub const fn victor(self) -> Option<Color> {
match self {
Self::Checkmate(victor)
| Self::Resignation(victor)
| Self::Timeout(victor)
| Self::ImportedVictory(victor) => Some(victor),
_ => None,
}
}
#[must_use]
pub const fn pgn_indicator(self) -> &'static str {
match self {
Self::InProgress => "*",
Self::Checkmate(victor)
| Self::Resignation(victor)
| Self::Timeout(victor)
| Self::ImportedVictory(victor) => match victor {
White => "1-0",
Black => "0-1",
},
_ => "1/2-1/2",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{pgn::Pgn, pieces::PromotedType, sq};
#[test]
fn test_fifty_move_recognition() {
let games = std::fs::read_to_string("test_data/50moves.txt").unwrap();
for game in games.split('\n') {
let mut position = ChessPosition::new();
position.apply_sans(game.split(' ')).unwrap();
assert!(position.ply_clock.unwrap().fifty_moves());
}
}
#[test]
fn test_from_fen() {
struct FenTest {
input: &'static str,
board_portion: &'static str,
turn_portion: &'static str,
castling_rights: &'static str,
}
let tests_solutions = [
FenTest {
input: "r1bqkb1r/pppp1ppp/2n2n2/4p3/3PP3/2N2N2/PPP2PPP/R1BQKB1R b KQkq d3 1 4",
board_portion: "r1bqkb1r/pppp1ppp/2n2n2/4p3/3PP3/2N2N2/PPP2PPP/R1BQKB1R",
turn_portion: "b",
castling_rights: "KQkq",
},
FenTest {
input: "rnbqkb1r/ppp1pp1p/5np1/3p4/2PP4/2N5/PP2PPPP/R1BQKBNR w Kkq d6 1 4",
board_portion: "rnbqkb1r/ppp1pp1p/5np1/3p4/2PP4/2N5/PP2PPPP/R1BQKBNR",
turn_portion: "w",
castling_rights: "Kkq",
},
];
for fen_test in tests_solutions {
let position = ChessPosition::from_fen(fen_test.input).unwrap();
assert_eq!(fen_test.board_portion, position.board.fen_repr());
assert_eq!(fen_test.turn_portion, position.turn.char().to_string());
assert_eq!(
fen_test.castling_rights,
position.castling_rights.fen_repr()
);
}
}
#[test]
fn test_threats() {
let fen = "rn1qkbnr/ppp1pp1p/8/3p1bp1/3P1B2/3Q4/PPP1PPPP/RN2KBNR w KQkq - 2 4";
let position = ChessPosition::from_fen(fen).unwrap();
assert_eq!(
vec![Move::Standard {
initial_square: sq!(F5),
final_square: sq!(D3),
is_capture: true,
piece_color: Black,
piece_type: Bishop,
timer_update: None,
}],
position
.pseudolegal_threats(sq!(D3), White)
.collect::<Vec<_>>()
);
assert_eq!(
vec![Move::Standard {
initial_square: sq!(G5),
final_square: sq!(F4),
is_capture: true,
piece_type: Pawn,
piece_color: Black,
timer_update: None,
}],
position
.pseudolegal_threats(sq!(F4), White)
.collect::<Vec<_>>()
);
assert_eq!(
Vec::<Move>::new(),
position
.pseudolegal_threats(sq!(D5), Black)
.collect::<Vec<_>>()
);
}
#[test]
fn test_builder() {
let mut builder = ChessPosition::builder();
builder.board = Some(Board::from_fen_slice("8/5pk1/p7/8/6Pp/P5qP/2Qp3K/6r1").unwrap());
builder.turn = Some(Turn::default());
builder.castling_rights = Some(CastlingRights::default());
builder.ply_clock = Some(PlyClock::new());
builder.initial_position = Some(InitialPosition::Unspecified);
let _: ChessPosition = builder.try_into().unwrap();
}
#[test]
fn test_king_tracking() {
let fen = "6k1/Bp4pp/8/7r/3Kp3/3q2P1/8/8 w - - 0 1";
let mut position = ChessPosition::from_fen(fen).unwrap();
assert_eq!(
position.kings,
Kings {
white: sq!(D4),
black: sq!(G8),
}
);
position.set(sq!(D4), None);
position.set(sq!(D5), Some(Piece::new(King, White)));
assert_eq!(
position.kings,
Kings {
white: sq!(D5),
black: sq!(G8),
}
);
}
#[test]
#[ignore]
fn test_repetitions() {
use rayon::prelude::*;
use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};
let games = std::fs::read_to_string("test_data/repetitions.txt").unwrap();
let games: Vec<&str> = games.split('\n').collect();
games.par_iter().for_each(|game| {
let mut position = ChessPosition::new();
position.apply_sans(game.split(' ')).unwrap();
assert!(
position.threefold_repetition(),
"{:#?}",
position.repetition_counter
);
});
let games = std::fs::read_to_string("test_data/not_repetitions.txt").unwrap();
let games: Vec<&str> = games.split('\n').collect();
let successes = AtomicUsize::new(0);
let failures = AtomicUsize::new(0);
games.par_iter().for_each(|game| {
let mut position = ChessPosition::new();
position.apply_sans(game.split(' ')).unwrap();
if position.threefold_repetition() {
failures.fetch_add(1, Relaxed);
println!("Failed: {game}");
} else {
successes.fetch_add(1, Relaxed);
}
});
println!("Successes: {}", successes.into_inner());
println!("Failures: {}", failures.into_inner());
}
#[test]
fn test_checks() {
let fen = "r1b4r/pp3kpp/2p1pn2/q2p4/1bPPnB2/P2BP3/1PQ2PPP/R3K2R w KQ - 3 13";
let position = ChessPosition::from_fen(fen).unwrap();
assert_eq!(
vec![Move::Standard {
initial_square: sq!(B4),
final_square: sq!(E1),
is_capture: true,
piece_color: Black,
piece_type: Bishop,
timer_update: None,
}],
position.checks(Color::White).unwrap()
);
assert_eq!(None, position.checks(Color::Black));
let fen = "r1bq3r/pp3kp1/2p1pn1p/3p4/1PPP4/P2BPPn1/2Q1K1P1/R6R w - - 0 18";
let position = ChessPosition::from_fen(fen).unwrap();
assert_eq!(
vec![Move::Standard {
initial_square: sq!(G3),
final_square: sq!(E2),
is_capture: true,
piece_type: Knight,
piece_color: Black,
timer_update: None,
}],
position.checks(Color::White).unwrap()
);
}
#[test]
fn test_en_passant_threat_detection() {
let fen = "bbqnrnkr/p1ppp1pp/1p6/5pP1/8/8/PPPPPP1P/BBQNRNKR w KQkq f6 0 3";
let position = ChessPosition::from_fen(fen).unwrap();
assert_eq!(
position
.pseudolegal_threats(sq!(F5), Black)
.collect::<Vec<_>>(),
vec![Move::EnPassant {
initial_square: sq!(G5),
capture_square: sq!(F5),
final_square: sq!(F6),
piece_color: White,
timer_update: None,
}]
);
}
#[test]
fn test_king_in_check() {
let fens = "rn1q3r/p4kbp/2p1p1b1/3pB1p1/1p1P4/1B3QP1/PPP2PP1/R3K2R b QK - 5 15
2rq2kr/p6p/4p1b1/3p2p1/Bp4P1/2b2Q2/P1P2PP1/1R2K2R w K - 2 22
6kr/p6p/3q4/2rpp1p1/1p4P1/2bb1Q2/P1P1KPP1/1R1R4 w - - 0 26
6kr/p6p/q7/2rp2p1/1p2Q1P1/2b5/P1P1KPP1/1R1R4 w - - 1 28
6kr/p6p/8/2rp2p1/1pq3P1/2b1Q3/2P1KPP1/1R1R4 w - - 2 30
6kr/p6p/8/2rp2p1/1p2q1P1/2b1QK2/2P2PP1/1R1R4 w - - 4 31
6kr/p6p/8/2r3p1/1p2p1P1/2b2K2/2P2PP1/1R1R4 w - - 0 32
7r/p2R2kp/8/2r3p1/1p2K1P1/2b5/2P2PP1/1R6 b - - 2 33
7r/R6p/6k1/4r1p1/1p2K1P1/2b5/2P2PP1/1R6 w - - 1 35
5r2/R6p/6k1/4r1p1/1p4P1/2b2K2/2P2PP1/1R6 w - - 3 36
5r2/7p/R5k1/6p1/1p4P1/2b3K1/2P1rPP1/1R6 b - - 6 37
5r2/R5kp/8/6p1/1p4P1/2b3K1/2P1rPP1/1R6 b - - 8 38
8/5k1R/8/6p1/1pr3P1/2b2PK1/6P1/8 b - - 0 45
8/6b1/6k1/6p1/5PP1/4K3/1p4P1/4r3 w - - 1 52
2b1q3/5k2/p3pQp1/1p1p3p/1P5P/2B1P1P1/8/6K1 b - - 7 44
r3kbnr/2qn1Qp1/b3p2p/p1p1P3/1p1PN3/P2B3P/1PP3PN/R1B2RK1 b qk - 0 15
r1bk2nr/6p1/1q1P1RBp/p3Q3/1p6/P2p3P/1PP3PN/R1B3K1 w - - 0 22
r1bk1Rnr/6p1/2qP2Bp/p3Q3/1p6/P2pB2P/1PP3PN/R5K1 b - - 3 23
2rqk2r/3n1pp1/3Npn1p/p1p5/1p1P4/1P1BPb2/PB3PPP/1QR2RK1 b k - 0 17"
.lines()
.map(str::trim);
for fen in fens {
let position: ChessPosition = ChessPosition::from_fen(fen).unwrap();
assert!(
Color::all()
.iter()
.any(|color| position.is_in_check(*color)),
"Did not recognize check on FEN: {fen}"
);
}
let not_in_check = "rnb1kbnr/p6p/8/qNp1p1N1/3p2p1/1P1P2P1/P1PBP1BP/R2QK2R b QKqk - 2 11
rnb1kbnr/p6p/8/1qp1p1N1/3p2p1/1P1P2P1/P1PBP1BP/R2QK2R w QKqk - 0 12
rnb1kbnr/p6p/8/1qp1p1N1/3p2p1/1P1P2P1/P1PBP1BP/1R1QK2R b Kqk - 1 12
rnb1kbnr/p7/8/1qp1p1Np/3p2p1/1P1P2P1/P1PBP1BP/1R1QK2R w Kqk h6 0 13
rnb1kbnr/p7/8/1qp1p1Np/3p2p1/1P1P2P1/PRPBP1BP/3QK2R b Kqk - 1 13
r1b1kbnr/p7/2n5/1qp1p1Np/3p2p1/1P1P2P1/PRPBP1BP/3QK2R w Kqk - 2 14
r1b1kbnr/p7/2q5/2p1p1Np/3p2p1/1P1P2P1/PRPBP2P/3QK2R w Kqk - 0 15
r1b1kbnr/p7/2q5/2p1p1Np/1P1p2p1/3P2P1/PRPBP2P/3QK2R b Kqk - 0 15
r1b1kbnr/p7/8/2p1p1Np/1P1p2p1/3P2P1/PRPBPK1P/3Q3q b qk - 1 16
r1b1kbnr/p7/8/2p1p1Np/1P1p2p1/3P2P1/PRPBPK1P/3q4 w qk - 0 17
r1b1kbnr/p7/8/2p1p1Np/1P1p2p1/3P2P1/PRP1PK1P/3qB3 b qk - 1 17
r1b1kbnr/p7/8/2p1p1Np/1P1p2p1/3P2P1/PRP1PK1P/2q1B3 w qk - 2 18
r1b1kbnr/p7/4N3/2p1p2p/1P1p2p1/3P2P1/PRP1PK1P/2q1B3 b qk - 3 18
r1b1kbnr/p7/4N3/2p1p2p/1P1p2p1/3P2P1/PqP1PK1P/4B3 w qk - 0 19"
.lines()
.map(str::trim);
for fen in not_in_check {
let position: ChessPosition = ChessPosition::from_fen(fen).unwrap();
assert!(
Color::all()
.iter()
.all(|color| !position.is_in_check(*color)),
"False check on FEN: {fen}"
);
}
}
#[test]
fn test_checkmate() {
let fens = "r2qkbnr/p1pppBpp/n1b5/6N1/8/8/PPPP1P1P/RNBQK1R1 b Qkq - 0 1
r3k2r/pp3ppp/3bpn2/8/8/8/PP1BNPPq/R3QRK1 w kq - 0 1
r1b1k2r/p2p2pp/1p6/n7/1p4n1/3NP2P/PPP2PPq/RN3RK1 w - - 0 1
5B1k/8/6Q1/8/3P4/1N5Q/P1P2PPP/R3KB1R b KQ - 0 1
r1b1k2r/p1pp1ppp/1p3n2/2b2P2/1q6/1K4P1/PPP4P/RNB5 w kq - 0 1
5r2/pp1b2kp/2p5/2n5/2B2pP1/7Q/PPP4P/2K1q3 w - - 0 1
2r3k1/K6p/1r3pp1/q7/8/3P4/8/8 w - - 0 1
7R/6R1/3p4/3P3k/1p2P3/4r3/7P/6K1 b - - 0 1
rnbqk2r/p2pnQpp/2pb4/1p2p3/4P3/1B6/PPPP1PPP/RNB1K1NR b KQkq - 0 1
1r4k1/p1p2pb1/7p/2N5/8/8/PPP2PPP/R1B1r1K1 w - - 0 1
6k1/1p3ppp/p7/B1PP4/2P5/P6P/1q4r1/1KR5 w - - 0 1
r1b1k3/3p3p/pNpb2p1/8/4P1n1/4B3/P1P1QPPq/R4RK1 w q - 0 1
r5k1/pp4Q1/2p1q2p/5p2/1P5P/P1BP4/3P1PP1/6K1 b - - 0 1
r2r1kR1/p6Q/5q1b/1p2pB2/3p1n1P/P2P4/1PPK1P2/6R1 b - - 0 1
3Q4/4kpp1/p3p3/2p1P1p1/2P3P1/8/2q2PP1/3R2K1 b - - 0 1
1kr5/1p2bp2/p2p2np/B3p1p1/1Pp3P1/P2q1P2/3K3P/2R1Q2R w - - 0 1
8/6bp/1p1nk1p1/p6r/6q1/8/8/7K w - - 0 1
r2q1bnr/ppp1kBpp/3p4/3NN3/3nP3/8/PPPP1PPP/R1Bb1RK1 b - - 0 1
6k1/6pp/2pp4/4p3/1p2P3/1RbP3P/2P5/rBK4r w - - 0 1
3RN3/p4pBp/2p3p1/1pb4k/1nP4P/1P2PBP1/5P2/6K1 b - - 0 1
8/2Q5/8/3p4/3k4/2Pp2P1/P2R1P1P/K3R3 b - - 0 1
r3r1k1/ppp2pp1/1b1p3p/4p3/1PBNP3/P2P1b2/2PQ1PqP/R4RK1 w - - 0 1
r2qr2k/ppp3Qp/2np2pB/2b1p3/2B1P3/2NP4/PPP2PPP/R4RK1 b - - 0 1"
.lines()
.map(str::trim);
for fen in fens {
let position = ChessPosition::from_fen(fen).unwrap();
assert!(
Color::all()
.into_iter()
.any(|color| position.checkmated(color)),
"Missed checkmate: {fen}"
);
}
let fens = "r1b3k1/p4Nbp/2p3p1/1p2p3/2B5/2P5/PP3PPP/3R2K1 w - - 0 1
r1b1r1k1/pppp1ppp/2n5/3RN3/2B2Q2/2P1B3/PqP2PPP/5RK1 b - - 0 1
4rr1k/p5bp/2p1p1pn/3p4/P2pP3/3P3P/1PP2PP1/R1B2RK1 w - - 0 1
r3k1nr/pppb1ppp/8/3P4/2q5/2N1BN2/PP3PPP/R3K2R w KQkq - 0 1
8/3k4/8/8/4P3/1R5P/6PK/8 b - - 0 1
r1bk4/ppp2p1p/3p4/8/2B1PB2/P1N2P2/1PP3PP/R3K2R b KQ - 0 1
2kr1b1r/ppp1pppp/3n4/8/6P1/2Pn1N1P/PP1P1P1K/R1B2R2 w - - 0 1
b7/4Kpk1/6p1/8/2B3nr/PP6/8/3R3q w - - 0 1
r1br2k1/p1p2ppp/1pp2n2/8/4N3/2N5/PPPP1PPP/R1B1RK2 b - - 0 1
r3r1k1/pp3pp1/5n1p/3pp3/1P6/PB1P1P1P/1RP2P2/4K2R w K - 0 1
1R4k1/5ppp/3p4/3P1b2/3P4/5NP1/r4PKP/8 b - - 0 1
8/8/3k4/8/4K3/8/8/8 b - - 0 1
8/8/k2K4/p4P2/P3P3/7Q/8/8 b - - 0 1
r1bq1b1r/pppp1kpp/2n5/4p3/4P3/8/PPPP1PPP/RNB1K1NR w KQ - 0 1
rn2k2r/pp3ppp/8/2pN2b1/8/3PQ2P/PPP2P1P/R3KB1R b KQkq - 0 1
6k1/p7/3p4/4b2p/7P/3p2q1/8/5K2 w - - 0 1
r1b1k2r/pp3pQ1/5n1p/q1pBp3/3pP3/2PP1N2/PP3PPP/R3K2R b KQkq - 0 1
r2q1rk1/ppp3p1/2n1b2p/4p3/2BP4/2P5/P2N1PPP/R1B1R1K1 w - - 0 1
8/8/8/7K/8/4q3/5pk1/8 w - - 0 1"
.lines()
.map(str::trim);
for fen in fens {
let position = ChessPosition::from_fen(fen).unwrap();
assert!(
Color::all()
.into_iter()
.all(|color| !position.checkmated(color)),
"False checkmate: {fen}"
);
}
}
#[test]
fn test_all_checkmates() {
let checkmates = std::fs::read_to_string("test_data/checkmates.fen").unwrap();
for checkmate in checkmates.split('\n') {
let Ok(position) = ChessPosition::from_fen(checkmate) else {
continue;
};
assert!(
Color::all()
.into_iter()
.any(|color| position.checkmated(color)),
"Failed to recognize checkmate: {checkmate}"
);
}
let naaaaaht = std::fs::read_to_string("test_data/not_checkmates.fen").unwrap();
for not_checkmate in naaaaaht.split('\n') {
let Ok(position) = ChessPosition::from_fen(not_checkmate) else {
continue;
};
for color in Color::all() {
assert!(!position.checkmated(color), "False checkmate: {position}");
}
}
}
#[test]
fn test_total_fen_import() {
for fen in std::fs::read_to_string("test_data/not_checkmates.fen")
.unwrap()
.split('\n')
.map(|s| s.to_string() + " 0 1")
{
let _ = ChessPosition::from_fen(&fen).unwrap();
}
}
#[test]
fn test_apply_legal_move() {
let game = "Nf3 d6 Nc3 c5 e3 e5 Bc4 Be7 h4 Nf6 Ng5 O-O Qf3 Nc6 Nce4 h6 Nxf6+ Bxf6 Ne4 \
Be7 Bd3 Nb4 Bb5 a6 Ba4 b5 Bb3 c4 a3 Bb7 axb4 cxb3 Nf6+ Bxf6 Qxb7 Qc8 Qxc8 Rfxc8 cxb3 Rc6 \
g3 Rac8 O-O e4 Re1 Be5 Kf1 Rc2 Rb1 f5 Ke2 Kf7 Rd1 h5 Re1 Ke6 Kd1 Kf6 Rf1 d5 f3 Bxg3 fxe4 \
dxe4 Rh1 g5 hxg5+ Kxg5 Ra1 R8c6 d3 exd3 Bd2 Rxb2 e4+ f4 e5 Kg4 Rc1 Rbc2 e6 Rxe6 Rxc2 dxc2+ \
Kxc2 Re2 Ra1 f3 Rxa6 Be1 Rg6+ Kh4 Rd6 f2 Rd4+ Kh3 Rf4 Bxd2 Kd1 Bxf4 Kxe2 Kg2";
let mut position = ChessPosition::new();
for san in game.split(' ') {
let m = Move::read_san(san, &position)
.unwrap_or_else(|e| panic!("{e} {}", position.board.fen_repr()));
position.apply_legal_move(m).unwrap();
}
}
#[test]
#[ignore]
fn test_all_apply_legal_move() {
use rayon::prelude::*;
let games = std::fs::read_to_string("test_data/standard_game_bare_moves.txt").unwrap();
let games: Vec<&str> = games.split('\n').collect();
games.par_iter().for_each(|game| {
let mut position = ChessPosition::new();
for san in game.split(' ') {
let m = Move::read_san(san, &position)
.unwrap_or_else(|e| panic!("{e} {}", position.fen()));
position.apply_legal_move(m).unwrap();
}
});
}
#[test]
#[ignore]
fn test_all_pseudolegal_is_legal() {
use rayon::prelude::*;
let games: String =
std::fs::read_to_string("test_data/standard_game_bare_moves.txt").unwrap();
let games: Vec<&str> = games.split('\n').collect();
let _: Vec<()> = games
.par_iter()
.map(|game| {
let mut position = ChessPosition::new();
for san in game.split(' ') {
position
.apply_san(san)
.unwrap_or_else(|e| panic!("Failed to read {san} on {game}: {e}"));
}
})
.collect();
}
#[test]
fn test_promotion() {
let fen = "rnbqkb1r/ppppppPp/8/8/4n3/8/PPPPP1PP/RNBQKBNR b KQkq - 0 4";
let position = ChessPosition::from_fen(fen).unwrap();
let pawn_moves: Vec<Move> = position.pseudolegal(sq!(G7), White, Pawn).collect();
assert!(
pawn_moves.clone().into_iter().all(Move::needs_promotion),
"Missed is_promotion!"
);
let best_choice = pawn_moves
.into_iter()
.find(|m| m.final_square() == sq!(H8))
.unwrap();
assert!(!best_choice
.pseudolegal_is_legal(&position)
.unwrap()
.as_bool());
let best_choice = best_choice.to_promotion(PromotedType::Queen).unwrap();
assert_eq!(
Move::PawnPromotion {
initial_square: sq!(G7),
final_square: sq!(H8),
piece_color: White,
new_type: crate::pieces::PromotedType::Queen,
is_capture: true,
timer_update: None,
},
best_choice
);
}
#[test]
#[ignore]
fn test_stalemate_recognition() {
let stalemate_fens = std::fs::read_to_string("test_data/stalemates.txt").unwrap();
for fen in stalemate_fens.split('\n').map(|s| s.to_string() + " 0 1") {
let position = ChessPosition::from_fen(&fen).unwrap();
assert!(position.is_stalemate(), "missed stalemate: {fen}");
}
let not_stalemate_fens = std::fs::read_to_string("test_data/not_stalemates.txt").unwrap();
for fen in not_stalemate_fens
.split('\n')
.map(|s| s.to_string() + " 0 1")
{
let position = ChessPosition::from_fen(&fen).unwrap();
assert!(!position.is_stalemate(), "false stalemate: {fen}");
}
}
#[test]
#[ignore]
fn test_apply_move() {
use rayon::prelude::*;
let games = std::fs::read_to_string("test_data/standard_game_bare_moves.txt").unwrap();
let games: Vec<&str> = games.split('\n').collect();
games.par_iter().for_each(|game| {
let mut moves = Vec::new();
let mut position = ChessPosition::new();
for san in game.split(' ') {
let m = Move::read_san(san, &position).unwrap();
moves.push(m);
position.apply_legal_move(m).unwrap();
}
let san_applied_fen = position.fen();
let mut position = ChessPosition::new();
for m in moves {
position.apply_move(m).unwrap();
}
assert_eq!(san_applied_fen, position.fen());
});
}
#[test]
fn test_apply_en_passant() {
let mut position = ChessPosition::new();
let m = Move::Standard {
initial_square: sq!(E2),
piece_type: Pawn,
piece_color: White,
final_square: sq!(E4),
is_capture: false,
timer_update: None,
};
position.apply_move(m).unwrap();
}
#[test]
fn test_illegal_needs_promotion() {
let pgn = r#"[Event "?"]
[Site "?"]
[Date "????.??.??"]
[Round "?"]
[White "?"]
[Black "?"]
[Result "0-1"]
1. d4 d5 2. Nf3 c6 3. g3 e6 4. Bg2 Bd6 5. O-O f5 6. Re1 Nf6 7. e3 O-O 8. Nbd2
Bd7 9. c4 Be8 10. c5 Bc7 11. a3 Bh5 12. b4 Ne4 13. Bb2 Nd7 14. a4 Ndf6 15. b5 g5
16. Ba3 Nxd2 17. Qxd2 Bxf3 18. Bxf3 h5 19. Qd1 g4 20. Bg2 h4 21. b6 Bb8 22. a5
a6 23. Qd2 Kf7 24. e4 dxe4 25. Qg5 Nd5 26. Qh5+ Kg7 27. Qxh4 Qxh4 28. gxh4 Rh8
29. Bc1 Rxh4 30. Bg5 Rxh2 31. f4 gxf3 32. Bxf3 exf3 33. Rxe6 Rg2+ 34. Kf1 Rxg5
35. Kf2 Bg3+ 36. Kxf3 Rh8 37. Rd6 Rh4 38. Rd7+ Kf6 39. Rxb7 Rf4+ 40. Ke2 Re4+
41. Kd3 Nb4+ 42. Kc4 Rgg4 43. Rd1 Be5 44. Rd7 Nd5 45. b7 Bxd4 46. Rxd5 cxd5+ 47.
Kxd5 Be5 48. c6 Rd4+ 49. Rxd4 Rxd4+ 50. Kc5 Rd1 51. Kb6 Rb1+ 52. Kxa6 f4 53. Ka7
f3 54. a6 f2 55. Ka8 f1=Q 56. a7 Qa6 57. b8=N Rxb8# 0-1"#;
println!("{pgn:?}");
let pgn = Pgn::try_from(pgn).unwrap();
let position = ChessPosition::try_from(&pgn).unwrap();
println!(
"{}",
String::try_from(&Pgn::try_from(&position).unwrap()).unwrap()
);
}
#[test]
fn test_fischer() {
let position = ChessPosition::fischer().unwrap();
position.board.color_print().unwrap();
}
}