#![doc = include_str!("../documentation/board/README.md")]
use super::*;
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub enum GameResult {
Win(Color),
Draw,
InProgress,
}
impl GameResult {
pub fn is_win(&self) -> bool {
matches!(self, Self::Win(_))
}
pub fn is_draw(&self) -> bool {
matches!(self, Self::Draw)
}
pub fn is_in_progress(&self) -> bool {
matches!(self, Self::InProgress)
}
pub fn winner(&self) -> Option<Color> {
match self {
Self::Win(color) => Some(*color),
_ => None,
}
}
}
impl fmt::Display for GameResult {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Win(Color::White) => write!(f, "1-0"),
Self::Win(Color::Black) => write!(f, "0-1"),
Self::Draw => write!(f, "1/2-1/2"),
Self::InProgress => write!(f, "*"),
}
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug)]
pub struct Board {
position: ChessPosition,
stack: Vec<(ChessPosition, ValidOrNullMove)>,
repetition_table: RepetitionTable,
#[cfg(feature = "extras")]
evaluator: Evaluator,
}
impl Board {
#[inline]
pub fn new() -> Self {
ChessPosition::from_str(STARTING_POSITION_FEN)
.unwrap()
.into()
}
#[inline]
pub fn shallow_clone(&self) -> Self {
Self {
position: self.position.clone(),
repetition_table: self.repetition_table.clone(),
..Default::default()
}
}
pub fn set_fen(&mut self, fen: &str) -> Result<()> {
self.position.set_fen(fen)?;
self.clear_stack();
self.update_repetition_table();
Ok(())
}
pub fn from_fen(fen: &str) -> Result<Self> {
let mut board = Self::new();
board.set_fen(fen)?;
Ok(board)
}
#[inline]
pub fn get_position(&self) -> &ChessPosition {
&self.position
}
#[cfg(feature = "extras")]
#[inline]
pub fn get_evaluator(&self) -> &Evaluator {
&self.evaluator
}
#[cfg(feature = "extras")]
#[inline]
pub fn get_evaluator_mut(&mut self) -> &mut Evaluator {
&mut self.evaluator
}
#[inline]
pub fn reset(&mut self) {
self.set_fen(STARTING_POSITION_FEN).unwrap();
}
#[deprecated(
note = "This method is unstable and may contain bugs. Hence, it is recommended not to use this method."
)]
pub fn flip_vertical(&mut self) {
#[expect(deprecated)]
self.position.flip_vertical();
self.clear_stack();
self.update_repetition_table();
}
#[deprecated(
note = "This method is unstable and may contain bugs. Hence, it is recommended not to use this method."
)]
pub fn flip_horizontal(&mut self) {
#[expect(deprecated)]
self.position.flip_horizontal();
self.clear_stack();
self.update_repetition_table();
}
#[inline]
pub fn to_board_string(&self, use_unicode: bool, colored_board: bool) -> String {
self.position.to_board_string(
self.stack
.last()
.map_or(ValidOrNullMove::NullMove, |(_, m)| *m),
use_unicode,
colored_board,
)
}
#[inline]
pub fn to_unicode_string(&self, colored_board: bool) -> String {
self.position.to_unicode_string(
self.stack
.last()
.map_or(ValidOrNullMove::NullMove, |(_, m)| *m),
colored_board,
)
}
pub fn result(&self) -> GameResult {
if self.is_other_draw() {
return GameResult::Draw;
}
match self.status() {
BoardStatus::Checkmate => GameResult::Win(!self.turn()),
BoardStatus::Stalemate => GameResult::Draw,
BoardStatus::Ongoing => GameResult::InProgress,
}
}
#[inline]
pub fn get_num_moves(&self) -> NumMoves {
self.stack.len() as NumMoves
}
#[inline]
pub fn clear_stack(&mut self) {
self.stack.clear();
}
pub fn update_repetition_table(&mut self) {
self.repetition_table.clear();
for (position, _) in &self.stack {
self.repetition_table.insert(position.get_hash())
}
self.repetition_table.insert(self.get_hash())
}
#[inline]
pub fn get_num_repetitions(&self) -> u8 {
self.repetition_table.get_repetition(self.get_hash())
}
#[inline]
pub fn is_repetition(&self, n_times: usize) -> bool {
self.get_num_repetitions() as usize >= n_times
}
#[inline]
pub fn is_threefold_repetition(&self) -> bool {
self.is_repetition(3)
}
#[inline]
pub(crate) fn is_other_draw(&self) -> bool {
self.is_fifty_moves() || self.is_threefold_repetition() || self.is_insufficient_material()
}
#[inline]
pub fn is_draw(&self) -> bool {
self.is_other_draw() || self.is_stalemate()
}
#[inline]
pub fn is_game_over(&self) -> bool {
self.is_other_draw() || self.status() != BoardStatus::Ongoing
}
pub fn pop(&mut self) -> Result<ValidOrNullMove> {
let (position, valid_or_null_move) = self.stack.pop().ok_or(TimecatError::EmptyStack)?;
self.repetition_table.remove(self.get_hash());
self.position = position;
Ok(valid_or_null_move)
}
#[inline]
pub fn get_stack(&self) -> &[(ChessPosition, ValidOrNullMove)] {
&self.stack
}
#[inline]
pub fn get_all_stack_moves(&self) -> impl Iterator<Item = ValidOrNullMove> {
self.stack.iter().map(|(_, m)| *m)
}
#[inline]
pub fn get_last_stack_move(&self) -> Option<ValidOrNullMove> {
self.stack.last().map(|(_, m)| *m)
}
#[inline]
pub fn stack_contains_null_move(&self) -> bool {
self.stack.iter().any(|(_, m)| m.is_null())
}
#[inline]
pub fn get_ply(&self) -> usize {
self.stack.len()
}
#[inline]
pub fn has_empty_stack(&self) -> bool {
self.stack.is_empty()
}
pub fn push_san(&mut self, san: &str) -> Result<ValidOrNullMove> {
let valid_or_null_move = self.parse_san(san)?;
unsafe { self.push_unchecked(valid_or_null_move) };
Ok(valid_or_null_move)
}
#[inline]
pub fn push_san_moves(&mut self, san_moves: &str) -> Result<Vec<ValidOrNullMove>> {
let parsed_str = remove_double_spaces_and_trim(san_moves);
if parsed_str.is_empty() {
const { Ok(Vec::new()) }
} else {
parsed_str
.split(' ')
.map(|san| self.push_san(san))
.collect()
}
}
pub fn push_uci(&mut self, uci: &str) -> Result<ValidOrNullMove> {
let valid_or_null_move = self.parse_uci(uci)?;
self.push(valid_or_null_move)?;
Ok(valid_or_null_move)
}
#[inline]
pub fn push_str(&mut self, s: &str) -> Result<ValidOrNullMove> {
self.push_uci(s)
}
#[inline]
pub fn push_uci_moves(&mut self, uci_moves: &str) -> Result<Vec<ValidOrNullMove>> {
let parsed_str = remove_double_spaces_and_trim(uci_moves);
if parsed_str.is_empty() {
const { Ok(Vec::new()) }
} else {
parsed_str
.split(' ')
.map(|san| self.push_uci(san))
.collect()
}
}
pub fn push_move(&mut self, move_text: &str) -> Result<ValidOrNullMove> {
let valid_or_null_move = self.parse_move(move_text)?;
self.push(valid_or_null_move)?;
Ok(valid_or_null_move)
}
#[inline]
pub fn push_moves(&mut self, uci_moves: &str) -> Result<Vec<ValidOrNullMove>> {
let parsed_str = remove_double_spaces_and_trim(uci_moves);
if parsed_str.is_empty() {
const { Ok(Vec::new()) }
} else {
parsed_str
.split(' ')
.map(|san| self.push_move(san))
.collect()
}
}
pub fn algebraic_and_push(
&mut self,
valid_or_null_move: ValidOrNullMove,
long: bool,
) -> Result<Cow<'static, str>> {
if valid_or_null_move.is_null() {
self.push(valid_or_null_move)?;
return Ok(Cow::Borrowed("--"));
}
let san = valid_or_null_move.algebraic_without_suffix(self.get_position(), long)?;
self.push(valid_or_null_move)?;
let is_checkmate = self.is_checkmate();
if is_checkmate {
Ok(san + "#")
} else if self.is_check() {
Ok(san + "+")
} else {
Ok(san)
}
}
#[inline]
pub fn san_and_push(
&mut self,
valid_or_null_move: ValidOrNullMove,
) -> Result<Cow<'static, str>> {
self.algebraic_and_push(valid_or_null_move, false)
}
#[inline]
pub fn lan_and_push(
&mut self,
valid_or_null_move: ValidOrNullMove,
) -> Result<Cow<'static, str>> {
self.algebraic_and_push(valid_or_null_move, true)
}
pub fn variation_san(
board: &mut Self,
variation: impl Iterator<Item = ValidOrNullMove>,
) -> Result<String> {
let mut variation_san = String::new();
for valid_or_null_move in variation {
if board.turn() == White {
let san_str = board.san_and_push(valid_or_null_move);
write_unchecked!(
&mut variation_san,
"{}. {}",
board.get_fullmove_number(),
san_str.unwrap()
);
} else if variation_san.is_empty() {
let san_str = board.san_and_push(valid_or_null_move);
write_unchecked!(
&mut variation_san,
"{}...{}",
board.get_fullmove_number(),
san_str.unwrap()
);
} else {
variation_san += &board.san_and_push(valid_or_null_move)?;
}
variation_san.push(' ');
}
variation_san.pop(); Ok(variation_san)
}
#[inline]
pub fn get_starting_position(&self) -> &ChessPosition {
self.stack
.first()
.map_or_else(|| self.get_position(), |(position, _)| position)
}
#[inline]
pub fn get_starting_board_fen(&self) -> String {
self.stack
.first()
.map_or_else(|| self.get_fen(), |(position, _)| position.get_fen())
}
pub fn get_pgn(&self) -> Result<String> {
let mut pgn = String::new();
let starting_position = self.get_starting_position();
let starting_fen = self.get_starting_position().get_fen();
if starting_fen != STARTING_POSITION_FEN {
writeln_unchecked!(pgn, "[FEN \"{}\"]", starting_fen);
}
writeln_unchecked!(pgn, "[Result \"{}\"]", self.result());
write_unchecked!(
&mut pgn,
"\n{}",
Self::variation_san(
&mut starting_position.into(),
self.stack.iter().map(|(_, optional_m)| *optional_m),
)?
);
Ok(pgn)
}
#[inline]
#[cfg(feature = "extras")]
pub fn evaluate(&mut self) -> Score {
self.evaluator.evaluate(&self.position)
}
#[inline]
#[cfg(feature = "extras")]
pub fn evaluate_flipped(&mut self) -> Score {
let score = self.evaluate();
self.score_flipped(score)
}
}
impl BoardMethodOverload<Move> for Board {
unsafe fn push_unchecked(&mut self, move_: Move) {
let position_copy = self.position.clone();
self.position.make_move(move_);
self.repetition_table.insert(self.get_hash());
self.stack.push((position_copy, move_.into()));
}
fn push(&mut self, move_: Move) -> Result<()> {
if !self.is_legal(&move_) {
return Err(TimecatError::IllegalMove {
valid_or_null_move: move_.into(),
board_fen: self.get_fen().into(),
});
}
unsafe { self.push_unchecked(move_) };
Ok(())
}
#[inline]
fn gives_repetition(&self, move_: Move) -> bool {
self.repetition_table
.get_repetition(self.position.make_move_new(move_).get_hash())
!= 0
}
#[inline]
fn gives_threefold_repetition(&self, move_: Move) -> bool {
self.repetition_table
.get_repetition(self.position.make_move_new(move_).get_hash())
== 2
}
fn gives_claimable_threefold_repetition(&self, move_: Move) -> bool {
let new_board = self.position.make_move_new(move_);
new_board.generate_legal_moves().into_iter().any(|m| {
let hash = new_board.make_move_new(m).get_hash();
self.repetition_table.get_repetition(hash) == 2
})
}
}
impl BoardMethodOverload<ValidOrNullMove> for Board {
unsafe fn push_unchecked(&mut self, valid_or_null_move: ValidOrNullMove) {
let position_copy = self.position.clone();
self.position.make_move(valid_or_null_move);
self.repetition_table.insert(self.get_hash());
self.stack.push((position_copy, valid_or_null_move));
}
fn push(&mut self, valid_or_null_move: ValidOrNullMove) -> Result<()> {
if let Some(move_) = *valid_or_null_move {
self.push(move_)
} else {
if self.is_check() {
return Err(TimecatError::NullMoveInCheck {
fen: self.get_fen().into(),
});
}
unsafe { self.push_unchecked(valid_or_null_move) };
Ok(())
}
}
#[inline]
fn gives_repetition(&self, valid_or_null_move: ValidOrNullMove) -> bool {
self.repetition_table
.get_repetition(self.position.make_move_new(valid_or_null_move).get_hash())
!= 0
}
#[inline]
fn gives_threefold_repetition(&self, valid_or_null_move: ValidOrNullMove) -> bool {
self.repetition_table
.get_repetition(self.position.make_move_new(valid_or_null_move).get_hash())
== 2
}
fn gives_claimable_threefold_repetition(&self, valid_or_null_move: ValidOrNullMove) -> bool {
let new_board = self.position.make_move_new(valid_or_null_move);
new_board.generate_legal_moves().into_iter().any(|m| {
let hash = new_board.make_move_new(m).get_hash();
self.repetition_table.get_repetition(hash) == 2
})
}
}
impl fmt::Display for Board {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.to_board_string(false, true))
}
}
impl Default for Board {
fn default() -> Self {
Self::from_fen(STARTING_POSITION_FEN).unwrap()
}
}
impl FromStr for Board {
type Err = TimecatError;
fn from_str(fen: &str) -> Result<Self> {
Self::from_fen(fen)
}
}
impl From<ChessPosition> for Board {
fn from(position: ChessPosition) -> Self {
let mut board = Self {
#[cfg(feature = "extras")]
evaluator: Evaluator::new(&position),
position,
stack: Vec::new(),
repetition_table: RepetitionTable::new(),
};
board.repetition_table.insert(board.get_hash());
board
}
}
impl From<&ChessPosition> for Board {
fn from(position: &ChessPosition) -> Self {
position.to_owned().into()
}
}
impl Deref for Board {
type Target = ChessPosition;
fn deref(&self) -> &Self::Target {
&self.position
}
}
#[cfg(feature = "pyo3")]
impl<'source> FromPyObject<'source> for Board {
fn extract_bound(ob: &Bound<'source, PyAny>) -> PyResult<Self> {
if let Ok(position) = ob.extract::<ChessPosition>() {
let mut board = Self::from(position);
if let (Ok(moves_py_object), Ok(states_py_object)) =
(ob.getattr("move_stack"), ob.getattr("_stack"))
{
let states = states_py_object
.extract::<Vec<ChessPosition>>()
.unwrap_or_default();
let moves = moves_py_object
.extract::<Vec<ValidOrNullMove>>()
.unwrap_or_default();
board.stack = states.into_iter().zip(moves).collect_vec();
if !board.stack.windows(2).all(|window| {
let (prev_board, move_) = get_item_unchecked!(window, 0);
let (new_board, _) = get_item_unchecked!(window, 1);
prev_board.make_move_new(*move_) == *new_board
}) {
board.stack.clear()
}
board.update_repetition_table();
}
return Ok(board);
}
Err(Pyo3Error::Pyo3TypeConversionError {
from: ob.to_string().into(),
to: std::any::type_name::<Self>().into(),
}
.into())
}
}