eschac 0.6.2

computing chess moves
Documentation
//! Standard algebraic notation.
//!
//! SAN notation is the FIDE standard for writing moves.
//!
//! In its simplest form, it consists of the type of the moving piece followed by the target
//! square. This may be ambiguous, in which case the origin file and/or rank is also specified.
//! The notation is shortened for pawns, and extra information may be added, to specify a capture
//! or a check.
//!
//! Examples: *`e4`*, *`Qxd8#`*, *`O-O`*, *`h7h8=Q`*

use crate::bitboard::*;
use crate::board::*;
use crate::position::*;

use core::ops::ControlFlow;

/// **The standard algebraic notation of a move.**
///
/// See [`SanData`] which [`San`] dereferences to.
///
/// When converting [`San`] notation to a playable [`Move`], the optional capture flag (*x*) and
/// suffix (*+* or *#*) are ignored (as they are redundant). Thus, conversion will not fail if they
/// are incorrectly set. Similarly, conversion will not fail when the move is unnecessarily
/// disambiguated. For example, *Ke1xd1* and *Kd1* will always be equivalent, even if there is no
/// piece on *d1*.
///
/// SAN notation can be obtained from a legal move using the [`Move::to_san`] method.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct San {
	pub data: SanData,
	pub capture: bool,
	pub suffix: Option<SanSuffix>,
}

impl core::ops::Deref for San {
	type Target = SanData;
	fn deref(&self) -> &SanData {
		&self.data
	}
}

impl San {
	/// Tries to read SAN notation from ascii text.
	pub fn from_ascii(s: &[u8]) -> Option<Self> {
		let mut r = s.iter().copied().rev();
		let mut cur = r.next()?;

		let suffix = match cur {
			b'+' => {
				cur = r.next()?;
				Some(SanSuffix::Check)
			}
			b'#' => {
				cur = r.next()?;
				Some(SanSuffix::Checkmate)
			}
			_ => None,
		};

		if cur == b'O' {
			let b'-' = r.next()? else { return None };
			let b'O' = r.next()? else { return None };
			let side = match r.next() {
				None => Castle::Short,
				Some(b'-') => {
					let b'O' = r.next()? else { return None };
					r.next().is_none().then_some(())?;
					Castle::Long
				}
				Some(_) => return None,
			};
			return Some(Self {
				data: SanData::Castle { side },
				capture: false,
				suffix,
			});
		}

		let promotion = Role::from_ascii(cur);
		if promotion.is_some() {
			(r.next()? == b'=').then_some(())?;
			cur = r.next()?;
		}
		let target_rank = Rank::from_ascii(cur)?;
		let target_file = File::from_ascii(r.next()?)?;
		let to = Square::from_coords(target_file, target_rank);
		let mut cur = r.next();
		let capture = cur == Some(b'x');
		if capture {
			cur = r.next();
		}
		let rank = cur.and_then(Rank::from_ascii);
		if rank.is_some() {
			cur = r.next();
		}
		let file = cur.and_then(File::from_ascii);
		if file.is_some() {
			cur = r.next();
		}
		let role = match cur {
			Some(a) => {
				cur = r.next();
				Role::from_ascii(a)?
			}
			None => Role::Pawn,
		};
		cur.is_none().then_some(())?;
		(role != Role::Pawn || file.is_some() || !capture).then_some(())?;
		(role == Role::Pawn || promotion.is_none()).then_some(())?;
		Some(Self {
			data: SanData::Normal {
				role,
				from: (file, rank),
				to,
				promotion,
			},
			capture,
			suffix,
		})
	}
}

impl core::fmt::Debug for San {
	fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
		write!(f, "{self}")
	}
}

impl core::fmt::Display for San {
	fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
		match self.data {
			SanData::Normal {
				role,
				from: (file, rank),
				to,
				promotion,
			} => {
				if role != Role::Pawn {
					write!(f, "{}", role.to_char_uppercase())?;
				}
				if let Some(file) = file {
					write!(f, "{}", file.to_char())?;
				}
				if let Some(rank) = rank {
					write!(f, "{}", rank.to_char())?;
				}
				if self.capture {
					write!(f, "x")?;
				}
				write!(f, "{to}")?;
				if let Some(promotion) = promotion {
					write!(f, "={}", promotion.to_char_uppercase())?;
				}
			}
			SanData::Castle { side } => match side {
				Castle::Short => write!(f, "O-O")?,
				Castle::Long => write!(f, "O-O-O")?,
			},
		}
		match self.suffix {
			Some(SanSuffix::Check) => write!(f, "+")?,
			Some(SanSuffix::Checkmate) => write!(f, "#")?,
			None => (),
		}
		Ok(())
	}
}

impl core::str::FromStr for San {
	type Err = ParseSanError;
	#[inline]
	fn from_str(s: &str) -> Result<Self, Self::Err> {
		Self::from_ascii(s.as_bytes()).ok_or(ParseSanError)
	}
}

/// The algebraic data of a SAN move.
///
/// This is the relevant move information in algebraic notation (without the suffix or the capture
/// flag).
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum SanData {
	Normal {
		role: Role,
		from: (Option<File>, Option<Rank>),
		to: Square,
		promotion: Option<Role>,
	},
	Castle {
		side: Castle,
	},
}

impl SanData {
	/// Tries to convert SAN notation to a playable move.
	///
	/// This function ignores the suffix and the capture flag. It also accepts unnecessarily
	/// desambiguated moves.
	#[inline]
	pub fn to_move<'l>(self, position: &'l Position) -> Result<Move<'l>, InvalidSan> {
		let (role, from, to, promotion) = match self {
			Self::Normal {
				role,
				from: (file, rank),
				to,
				promotion,
			} => (
				role,
				file.map(File::bitboard).unwrap_or(!Bitboard::new())
					& rank.map(Rank::bitboard).unwrap_or(!Bitboard::new()),
				to,
				promotion,
			),
			Self::Castle { side } => {
				let from = match position.turn() {
					Color::White => sq("e1"),
					Color::Black => sq("e8"),
				};
				let to = match (position.turn(), side) {
					(Color::White, Castle::Short) => sq("g1"),
					(Color::White, Castle::Long) => sq("c1"),
					(Color::Black, Castle::Short) => sq("g8"),
					(Color::Black, Castle::Long) => sq("c8"),
				};
				(Role::King, from.bitboard(), to, None)
			}
		};
		let mut res = None;
		match position.try_for_each_candidate(role, from, to, promotion, |m: Move<'l>| {
			if res.replace(m).is_some() {
				ControlFlow::Break(())
			} else {
				ControlFlow::Continue(())
			}
		}) {
			ControlFlow::Continue(()) => res.ok_or(InvalidSan::Illegal),
			ControlFlow::Break(()) => Err(InvalidSan::Ambiguous),
		}
	}
}

impl core::fmt::Debug for SanData {
	fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
		write!(f, "{self}")
	}
}

impl core::fmt::Display for SanData {
	fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
		write!(
			f,
			"{}",
			San {
				data: *self,
				capture: false,
				suffix: None
			}
		)
	}
}

impl core::str::FromStr for SanData {
	type Err = ParseSanError;
	#[inline]
	fn from_str(s: &str) -> Result<Self, Self::Err> {
		Ok(San::from_str(s)?.data)
	}
}

#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum SanSuffix {
	Check,
	Checkmate,
}

/// A syntax error when parsing [`San`] notation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ParseSanError;
impl core::fmt::Display for ParseSanError {
	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
		f.write_str("invalid SAN syntax")
	}
}
impl core::error::Error for ParseSanError {}

/// An error while converting [`San`] notation to a playable [`Move`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum InvalidSan {
	/// There is no move on the position that matches the SAN notation.
	Illegal,
	/// There is more than one move on the position that matches the SAN notation.
	Ambiguous,
}
impl core::fmt::Display for InvalidSan {
	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
		let details = match self {
			Self::Illegal => "illegal move",
			Self::Ambiguous => "ambiguous move",
		};
		write!(f, "invalid SAN ({details})")
	}
}
impl core::error::Error for InvalidSan {}