eschac
This commit is contained in:
commit
faccfbc1c5
16 changed files with 5154 additions and 0 deletions
219
src/san.rs
Normal file
219
src/san.rs
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
//! 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()?)?;
|
||||
let target = Square::new(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 })
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue