eschac 0.6.1

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::*;

/// **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,
		};

		Some(match 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,
				};
				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(())?;
				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, "San(\"{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;
		let mut ambiguous = false;
		position.for_each_candidate(role, from, to, promotion, |m: Move<'l>| {
			ambiguous = res.replace(m).is_some();
		});
		if let Some(m) = res {
			if !ambiguous {
				Ok(m)
			} else {
				Err(InvalidSan::Ambiguous)
			}
		} else {
			Err(InvalidSan::Illegal)
		}
	}
}

impl core::fmt::Debug for SanData {
	fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
		write!(f, "SanData(\"{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 {}