use core::{error, fmt, str::FromStr};
use crate::{CastlingMode, CastlingSide, Move, Position, Rank, Role, Square, util::AppendAscii};
#[derive(Clone, Debug)]
pub struct ParseUciMoveError;
impl fmt::Display for ParseUciMoveError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("invalid uci")
}
}
impl error::Error for ParseUciMoveError {}
#[derive(Clone, Debug)]
pub struct IllegalUciMoveError;
impl fmt::Display for IllegalUciMoveError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("illegal uci")
}
}
impl error::Error for IllegalUciMoveError {}
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
pub enum UciMove {
Normal {
from: Square,
to: Square,
promotion: Option<Role>,
},
Put { role: Role, to: Square },
Null,
}
impl FromStr for UciMove {
type Err = ParseUciMoveError;
fn from_str(uci: &str) -> Result<UciMove, ParseUciMoveError> {
UciMove::from_ascii(uci.as_bytes())
}
}
impl fmt::Display for UciMove {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.append_to(f)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for UciMove {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut s = arrayvec::ArrayString::<5>::new();
let _ = self.append_to(&mut s);
serializer.serialize_str(&s)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for UciMove {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct UciMoveVisitor;
impl serde::de::Visitor<'_> for UciMoveVisitor {
type Value = UciMove;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("UCI move string")
}
fn visit_str<E>(self, value: &str) -> Result<UciMove, E>
where
E: serde::de::Error,
{
value.parse().map_err(serde::de::Error::custom)
}
}
deserializer.deserialize_str(UciMoveVisitor)
}
}
impl UciMove {
pub const fn is_normal(self) -> bool {
matches!(self, UciMove::Normal { .. })
}
pub const fn is_put(self) -> bool {
matches!(self, UciMove::Put { .. })
}
pub const fn is_null(self) -> bool {
matches!(self, UciMove::Null)
}
pub const fn from(self) -> Option<Square> {
match self {
UciMove::Normal { from, .. } => Some(from),
UciMove::Put { .. } | UciMove::Null => None,
}
}
pub const fn to(self) -> Option<Square> {
match self {
UciMove::Normal { to, .. } | UciMove::Put { to, .. } => Some(to),
UciMove::Null => None,
}
}
pub const fn promotion(self) -> Option<Role> {
match self {
UciMove::Normal { promotion, .. } => promotion,
UciMove::Put { .. } | UciMove::Null => None,
}
}
pub const fn is_promotion(self) -> bool {
matches!(
self,
UciMove::Normal {
promotion: Some(_),
..
}
)
}
pub const fn from_ascii(uci: &[u8]) -> Result<UciMove, ParseUciMoveError> {
Ok(if uci.len() == 4 {
if uci[0] == b'0' && uci[1] == b'0' && uci[2] == b'0' && uci[3] == b'0' {
return Ok(UciMove::Null);
} else if uci[1] == b'@' {
let Some(role) = Role::from_char(uci[0] as char) else {
return Err(ParseUciMoveError);
};
let Ok(to) = Square::from_ascii(&[uci[2], uci[3]]) else {
return Err(ParseUciMoveError);
};
UciMove::Put { role, to }
} else {
let Ok(from) = Square::from_ascii(&[uci[0], uci[1]]) else {
return Err(ParseUciMoveError);
};
let Ok(to) = Square::from_ascii(&[uci[2], uci[3]]) else {
return Err(ParseUciMoveError);
};
UciMove::Normal {
from,
to,
promotion: None,
}
}
} else if uci.len() == 5 {
let Ok(from) = Square::from_ascii(&[uci[0], uci[1]]) else {
return Err(ParseUciMoveError);
};
let Ok(to) = Square::from_ascii(&[uci[2], uci[3]]) else {
return Err(ParseUciMoveError);
};
let Some(promotion) = Role::from_char(uci[4] as char) else {
return Err(ParseUciMoveError);
};
UciMove::Normal {
from,
to,
promotion: Some(promotion),
}
} else {
return Err(ParseUciMoveError);
})
}
pub fn from_standard(m: Move) -> UciMove {
match m {
Move::Castle { king, rook } => {
let side = CastlingSide::from_king_side(king < rook);
UciMove::Normal {
from: king,
to: Square::from_coords(side.king_to_file(), king.rank()),
promotion: None,
}
}
_ => UciMove::from_chess960(m),
}
}
pub const fn from_chess960(m: Move) -> UciMove {
match m {
Move::Normal {
from,
to,
promotion,
..
} => UciMove::Normal {
from,
to,
promotion,
},
Move::EnPassant { from, to, .. } => UciMove::Normal {
from,
to,
promotion: None,
},
Move::Castle { king, rook } => UciMove::Normal {
from: king,
to: rook,
promotion: None,
}, Move::Put { role, to } => UciMove::Put { role, to },
}
}
pub fn from_move(m: Move, mode: CastlingMode) -> UciMove {
match mode {
CastlingMode::Standard => UciMove::from_standard(m),
CastlingMode::Chess960 => UciMove::from_chess960(m),
}
}
pub fn to_move<P: Position>(self, pos: &P) -> Result<Move, IllegalUciMoveError> {
let candidate = match self {
UciMove::Normal {
from,
to,
promotion,
} => {
let role = pos.board().role_at(from).ok_or(IllegalUciMoveError)?;
if promotion.is_some() && role != Role::Pawn {
return Err(IllegalUciMoveError);
}
if role == Role::King && (pos.castles().castling_rights() & pos.us()).contains(to) {
Move::Castle {
king: from,
rook: to,
}
} else if role == Role::King
&& from == pos.turn().fold_wb(Square::E1, Square::E8)
&& to.rank() == pos.turn().fold_wb(Rank::First, Rank::Eighth)
&& from.distance(to) == 2
{
if from.file() < to.file() {
Move::Castle {
king: from,
rook: pos.turn().fold_wb(Square::H1, Square::H8),
}
} else {
Move::Castle {
king: from,
rook: pos.turn().fold_wb(Square::A1, Square::A8),
}
}
} else if role == Role::Pawn
&& from.file() != to.file()
&& !pos.board().occupied().contains(to)
{
Move::EnPassant { from, to }
} else {
Move::Normal {
role,
from,
capture: pos.board().role_at(to),
to,
promotion,
}
}
}
UciMove::Put { role, to } => Move::Put { role, to },
UciMove::Null => return Err(IllegalUciMoveError),
};
if pos.is_legal(candidate) {
Ok(candidate)
} else {
Err(IllegalUciMoveError)
}
}
#[must_use]
pub const fn to_mirrored(self) -> UciMove {
match self {
UciMove::Normal {
from,
to,
promotion,
} => UciMove::Normal {
from: from.flip_vertical(),
to: to.flip_vertical(),
promotion,
},
UciMove::Put { role, to } => UciMove::Put {
role,
to: to.flip_vertical(),
},
UciMove::Null => UciMove::Null,
}
}
fn append_to<W: AppendAscii>(self, f: &mut W) -> Result<(), W::Error> {
match self {
UciMove::Normal {
from,
to,
promotion,
} => {
from.append_to(f)?;
to.append_to(f)?;
if let Some(promotion) = promotion {
f.append_ascii(promotion.char())?;
}
}
UciMove::Put { role, to } => {
f.append_ascii(role.upper_char())?;
f.append_ascii('@')?;
to.append_to(f)?;
}
UciMove::Null => {
f.append_ascii('0')?;
f.append_ascii('0')?;
f.append_ascii('0')?;
f.append_ascii('0')?;
}
}
Ok(())
}
#[cfg(feature = "alloc")]
pub fn append_to_string(self, s: &mut alloc::string::String) {
let _ = self.append_to(s);
}
#[cfg(feature = "alloc")]
pub fn append_ascii_to(self, buf: &mut alloc::vec::Vec<u8>) {
let _ = self.append_to(buf);
}
}
#[cfg(feature = "bincode")]
impl bincode::Encode for UciMove {
fn encode<E: bincode::enc::Encoder>(
&self,
encoder: &mut E,
) -> Result<(), bincode::error::EncodeError> {
crate::packed::PackedUciMove::pack(*self).encode(encoder)
}
}
#[cfg(feature = "bincode")]
impl<Config> bincode::Decode<Config> for UciMove {
fn decode<D: bincode::de::Decoder>(
decoder: &mut D,
) -> Result<Self, bincode::error::DecodeError> {
let packed = crate::packed::PackedUciMove::decode(decoder)?;
Ok(packed.unpack())
}
}
#[cfg(feature = "bincode")]
bincode::impl_borrow_decode!(UciMove);
impl Move {
pub fn to_uci(self, mode: CastlingMode) -> UciMove {
UciMove::from_move(self, mode)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Chess, fen::Fen};
#[cfg(feature = "alloc")]
#[test]
#[cfg_attr(miri, ignore)]
fn test_from_to_str() {
use alloc::string::ToString as _;
for from in Square::ALL {
for to in Square::ALL {
let uci = UciMove::Normal {
from,
to,
promotion: None,
};
assert_eq!(uci.to_string().parse::<UciMove>().expect("roundtrip"), uci);
}
}
for from in Square::ALL {
for to in Square::ALL {
for role in Role::ALL {
let uci = UciMove::Normal {
from,
to,
promotion: Some(role),
};
assert_eq!(uci.to_string().parse::<UciMove>().expect("roundtrip"), uci);
}
}
}
assert_eq!(
UciMove::Null
.to_string()
.parse::<UciMove>()
.expect("roundtrip"),
UciMove::Null
);
}
#[test]
fn test_uci_to_en_passant() {
let mut pos = Chess::default();
let e4 = "e2e4"
.parse::<UciMove>()
.expect("e4")
.to_move(&pos)
.expect("legal");
pos.play_unchecked(e4);
let nc6 = "b8c6"
.parse::<UciMove>()
.expect("Nc6")
.to_move(&pos)
.expect("legal");
pos.play_unchecked(nc6);
let e5 = "e4e5"
.parse::<UciMove>()
.expect("e5")
.to_move(&pos)
.expect("legal");
pos.play_unchecked(e5);
let d5 = "d7d5"
.parse::<UciMove>()
.expect("d5")
.to_move(&pos)
.expect("legal");
pos.play_unchecked(d5);
let exd5 = "e5d6"
.parse::<UciMove>()
.expect("exd6")
.to_move(&pos)
.expect("legal en passant");
assert!(exd5.is_en_passant());
}
#[cfg(feature = "variant")]
#[test]
fn test_uci_to_crazyhouse() {
use crate::position::variant::Crazyhouse;
let mut pos = Crazyhouse::default();
let e4 = "e2e4"
.parse::<UciMove>()
.expect("e4")
.to_move(&pos)
.expect("legal");
pos.play_unchecked(e4);
let d5 = "d7d5"
.parse::<UciMove>()
.expect("d5")
.to_move(&pos)
.expect("legal");
pos.play_unchecked(d5);
let exd5 = "e4d5"
.parse::<UciMove>()
.expect("exd5")
.to_move(&pos)
.expect("legal");
pos.play_unchecked(exd5);
let qxd5 = "d8d5"
.parse::<UciMove>()
.expect("Qxd5")
.to_move(&pos)
.expect("legal");
pos.play_unchecked(qxd5);
let p_at_d7 = "P@d7"
.parse::<UciMove>()
.expect("P@d7+")
.to_move(&pos)
.expect("legal");
pos.play_unchecked(p_at_d7);
assert!(pos.is_check());
}
#[test]
fn test_king_captures_ummoved_rook() {
let pos: Chess = "8/8/8/B2p3Q/2qPp1P1/b7/2P2PkP/4K2R b K - 0 1"
.parse::<Fen>()
.expect("valid fen")
.into_position(CastlingMode::Standard)
.expect("valid position");
let uci = "g2h1".parse::<UciMove>().expect("valid uci");
let m = uci.to_move(&pos).expect("legal uci");
assert_eq!(
m,
Move::Normal {
role: Role::King,
from: Square::G2,
capture: Some(Role::Rook),
to: Square::H1,
promotion: None,
}
);
}
#[cfg(feature = "alloc")]
#[test]
fn test_uci_to_castles() {
use alloc::string::ToString as _;
let mut pos: Chess = "nbqrknbr/pppppppp/8/8/8/8/PPPPPPPP/NBQRKNBR w KQkq - 0 1"
.parse::<Fen>()
.expect("valid fen")
.into_position(CastlingMode::Chess960)
.expect("valid position");
for uci in &["f2f4", "d7d6", "f1g3", "c8g4", "g1f2", "e8d8", "e1g1"] {
let m = uci
.parse::<UciMove>()
.expect("valid uci")
.to_move(&pos)
.expect("legal");
pos.play_unchecked(m);
}
assert_eq!(
Fen::from_position(&pos, crate::EnPassantMode::Legal).to_string(),
"nbkr1nbr/ppp1pppp/3p4/8/5Pq1/6N1/PPPPPBPP/NBQR1RK1 b - - 5 4"
);
}
}