2024-04-29 02:00:26 +02:00
|
|
|
//! 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 cases 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::board::*;
|
|
|
|
|
use crate::position::*;
|
|
|
|
|
|
|
|
|
|
/// **The standard algebraic notation of a move.**
|
|
|
|
|
///
|
|
|
|
|
///
|
|
|
|
|
/// 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(crate) inner: SanInner,
|
|
|
|
|
pub(crate) suffix: Option<SanSuffix>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
|
|
|
pub(crate) enum SanInner {
|
|
|
|
|
Castle(CastlingSide),
|
|
|
|
|
Normal {
|
|
|
|
|
role: Role,
|
|
|
|
|
file: Option<File>,
|
|
|
|
|
rank: Option<Rank>,
|
|
|
|
|
capture: bool,
|
|
|
|
|
target: Square,
|
|
|
|
|
promotion: Option<Role>,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
|
|
|
pub(crate) enum SanSuffix {
|
|
|
|
|
Check,
|
|
|
|
|
Checkmate,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl std::fmt::Display for San {
|
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
|
|
|
match self.inner {
|
|
|
|
|
SanInner::Castle(CastlingSide::Short) => write!(f, "O-O")?,
|
|
|
|
|
SanInner::Castle(CastlingSide::Long) => write!(f, "O-O-O")?,
|
|
|
|
|
SanInner::Normal {
|
|
|
|
|
role,
|
|
|
|
|
file,
|
|
|
|
|
rank,
|
|
|
|
|
capture,
|
|
|
|
|
target,
|
|
|
|
|
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 capture {
|
|
|
|
|
write!(f, "x")?;
|
|
|
|
|
}
|
|
|
|
|
write!(f, "{}", target)?;
|
|
|
|
|
if let Some(promotion) = promotion {
|
|
|
|
|
write!(f, "={}", promotion.to_char_uppercase())?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
match self.suffix {
|
|
|
|
|
Some(SanSuffix::Check) => write!(f, "+")?,
|
|
|
|
|
Some(SanSuffix::Checkmate) => write!(f, "#")?,
|
|
|
|
|
None => (),
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl std::fmt::Debug for San {
|
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
|
|
|
f.debug_tuple("San").field(&self.to_string()).finish()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl std::str::FromStr for San {
|
|
|
|
|
type Err = ParseSanError;
|
|
|
|
|
#[inline]
|
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
|
|
|
San::from_ascii(s.as_bytes()).ok_or(ParseSanError)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A syntax error when parsing [`San`] notation.
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct ParseSanError;
|
|
|
|
|
impl std::fmt::Display for ParseSanError {
|
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
f.write_str("invalid SAN syntax")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
impl std::error::Error for ParseSanError {}
|
|
|
|
|
|
|
|
|
|
/// An error while converting [`San`] notation to a playable [`Move`].
|
|
|
|
|
#[derive(Debug, PartialEq, Eq)]
|
|
|
|
|
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 std::fmt::Display for InvalidSan {
|
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
let details = match self {
|
|
|
|
|
Self::Illegal => "illegal move",
|
|
|
|
|
Self::Ambiguous => "ambiguous move",
|
|
|
|
|
};
|
|
|
|
|
write!(f, "invalid SAN ({details})")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
impl std::error::Error for InvalidSan {}
|
|
|
|
|
|
|
|
|
|
impl San {
|
|
|
|
|
/// 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> {
|
|
|
|
|
position.move_from_san(self)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let inner = match cur {
|
|
|
|
|
b'O' => SanInner::Castle({
|
|
|
|
|
let b'-' = r.next()? else { return None };
|
|
|
|
|
let b'O' = r.next()? else { return None };
|
|
|
|
|
match r.next() {
|
|
|
|
|
None => CastlingSide::Short,
|
|
|
|
|
Some(b'-') => {
|
|
|
|
|
let b'O' = r.next()? else { return None };
|
|
|
|
|
r.next().is_none().then_some(())?;
|
|
|
|
|
CastlingSide::Long
|
|
|
|
|
}
|
|
|
|
|
Some(_) => return None,
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
_ => {
|
|
|
|
|
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()?)?;
|
2025-10-19 14:28:36 +02:00
|
|
|
let target = Square::from_coords(target_file, target_rank);
|
2024-04-29 02:00:26 +02:00
|
|
|
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(())?;
|
|
|
|
|
SanInner::Normal {
|
|
|
|
|
role,
|
|
|
|
|
file,
|
|
|
|
|
rank,
|
|
|
|
|
capture,
|
|
|
|
|
target,
|
|
|
|
|
promotion,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Some(Self { inner, suffix })
|
|
|
|
|
}
|
|
|
|
|
}
|