use crate::bitboard::*;
use crate::board::*;
use crate::position::*;
use core::ops::ControlFlow;
#[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 {
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,
};
if 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,
};
return Some(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(())?;
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, "{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)
}
}
#[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 {
#[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;
match position.try_for_each_candidate(role, from, to, promotion, |m: Move<'l>| {
if res.replace(m).is_some() {
ControlFlow::Break(())
} else {
ControlFlow::Continue(())
}
}) {
ControlFlow::Continue(()) => res.ok_or(InvalidSan::Illegal),
ControlFlow::Break(()) => Err(InvalidSan::Ambiguous),
}
}
}
impl core::fmt::Debug for SanData {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "{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,
}
#[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 {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum InvalidSan {
Illegal,
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 {}