1
0
Fork 0
eschac/src/san.rs

220 lines
7.1 KiB
Rust
Raw Normal View History

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 })
}
}