eschac 0.3.0

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

impl core::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 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, 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 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 {}

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()?)?;
                let target = 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(())?;
                SanInner::Normal {
                    role,
                    file,
                    rank,
                    capture,
                    target,
                    promotion,
                }
            }
        };

        Some(Self { inner, suffix })
    }
}