use super::*;
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(PartialEq, Eq, Clone, Copy, Debug, Hash)]
pub struct Move {
source: Square,
dest: Square,
promotion: Option<PieceType>,
}
impl Move {
#[inline]
pub const unsafe fn new_unchecked(
source: Square,
dest: Square,
promotion: Option<PieceType>,
) -> Self {
Self {
source,
dest,
promotion,
}
}
#[inline]
pub const fn new(source: Square, dest: Square, promotion: Option<PieceType>) -> Result<Self> {
let move_ = unsafe { Self::new_unchecked(source, dest, promotion) };
if source.to_int() == dest.to_int() {
return Err(TimecatError::SameSourceAndDestination { move_ });
}
if let Some(Pawn) | Some(King) = promotion {
return Err(TimecatError::InvalidPromotion { move_ });
}
Ok(move_)
}
#[inline]
pub const fn get_source(&self) -> Square {
self.source
}
#[inline]
pub const fn get_dest(&self) -> Square {
self.dest
}
#[inline]
pub const fn get_promotion(&self) -> Option<PieceType> {
self.promotion
}
#[inline]
pub fn from_uci(uci: &str) -> Result<Self> {
uci.parse()
}
pub fn from_san(position: &ChessPosition, san: &str) -> Result<Self> {
let san = san.trim().replace('0', "O");
for move_ in position.generate_legal_moves() {
if move_.san(position).unwrap() == san {
return Ok(move_);
}
}
Err(TimecatError::InvalidSanMoveString { s: san.into() })
}
pub fn from_lan(position: &ChessPosition, lan: &str) -> Result<Self> {
let lan = lan.trim().replace('0', "O");
for move_ in position.generate_legal_moves() {
if move_.lan(position).unwrap() == lan {
return Ok(move_);
}
}
Err(TimecatError::InvalidLanMoveString { s: lan.into() })
}
pub fn algebraic_without_suffix(
self,
position: &ChessPosition,
long: bool,
) -> Result<Cow<'static, str>> {
let source = self.get_source();
let dest = self.get_dest();
if position.is_castling(self) {
return if dest.get_file() < source.get_file() {
Ok(Cow::Borrowed("O-O-O"))
} else {
Ok(Cow::Borrowed("O-O"))
};
}
let piece = position.get_piece_type_at(source).ok_or_else(|| {
TimecatError::InvalidSanOrLanMove {
valid_or_null_move: self.into(),
fen: position.get_fen().into(),
}
})?;
let capture = position.is_capture(self);
let mut san = if piece == Pawn {
String::new()
} else {
piece.to_colored_piece_str(White).into()
};
if long {
write_unchecked!(san, "{}", source);
} else if piece != Pawn {
let mut others = BitBoard::EMPTY;
let from_mask =
position.get_piece_mask(piece) & position.self_occupied() & !source.to_bitboard();
let to_mask = dest.to_bitboard();
for candidate in position.generate_masked_legal_moves(from_mask, to_mask) {
others |= candidate.get_source().to_bitboard();
}
if !others.is_empty() {
let (mut row, mut column) = (false, false);
if !(others & source.get_rank_bb()).is_empty() {
column = true;
}
if !(others & source.get_file_bb()).is_empty() {
row = true;
} else {
column = true;
}
if column {
san.push(
"abcdefgh"
.chars()
.nth(source.get_file().to_index())
.unwrap(),
);
}
if row {
write_unchecked!(san, "{}", source.get_rank().to_index() + 1);
}
}
} else if capture {
san.push(
"abcdefgh"
.chars()
.nth(source.get_file().to_index())
.unwrap(),
);
}
if capture {
san.push('x');
} else if long {
san.push('-');
}
write_unchecked!(san, "{}", dest);
if let Some(promotion) = self.get_promotion() {
write_unchecked!(san, "={}", promotion.to_colored_piece_str(White));
}
Ok(san.into())
}
pub fn algebraic_and_new_position(
self,
position: &ChessPosition,
long: bool,
) -> Result<(Cow<'static, str>, ChessPosition)> {
let san = self.algebraic_without_suffix(position, long)?;
let new_position = position.make_move_new(self);
let is_checkmate = new_position.is_checkmate();
let san = if is_checkmate {
san + "#"
} else if new_position.is_check() {
san + "+"
} else {
san
};
Ok((san, new_position))
}
#[cfg(feature = "pyo3")]
fn from_py_move(ob: &Bound<'_, PyAny>) -> PyResult<Self> {
let source = ob.getattr("from_square")?.extract()?;
let dest = ob.getattr("to_square")?.extract()?;
let promotion = ob.getattr("promotion")?.extract()?;
Ok(Self::new(source, dest, promotion)?)
}
}
macro_rules! generate_move_error {
($s: ident) => {
TimecatError::InvalidUciMoveString {
s: $s.to_string().into(),
}
};
}
impl FromStr for Move {
type Err = TimecatError;
fn from_str(mut s: &str) -> Result<Self> {
s = s.trim();
if s.len() > 5 {
return Err(generate_move_error!(s));
}
let source = s
.get(0..2)
.ok_or_else(|| generate_move_error!(s))?
.parse()
.map_err(|_| generate_move_error!(s))?;
let dest = s
.get(2..4)
.ok_or_else(|| generate_move_error!(s))?
.parse()
.map_err(|_| generate_move_error!(s))?;
let promotion = (s.len() > 4)
.then(|| {
s.get(4..)
.ok_or_else(|| generate_move_error!(s))?
.parse()
.map_err(|_| generate_move_error!(s))
})
.transpose()?;
Self::new(source, dest, promotion)
}
}
impl fmt::Display for Move {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.promotion {
Some(piece) => write!(f, "{}{}{}", self.source, self.dest, piece),
None => write!(f, "{}{}", self.source, self.dest),
}
}
}
#[cfg(feature = "pyo3")]
impl<'source> FromPyObject<'source> for Move {
fn extract_bound(ob: &Bound<'source, PyAny>) -> PyResult<Self> {
if let Ok(move_text) = ob.extract::<&str>()
&& let Ok(move_) = move_text.parse()
{
return Ok(move_);
}
if let Ok(move_) = Self::from_py_move(ob) {
return Ok(move_);
}
Err(Pyo3Error::Pyo3TypeConversionError {
from: ob.to_string().into(),
to: std::any::type_name::<Self>().into(),
}
.into())
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(PartialEq, Eq, Clone, Copy, Debug, Hash)]
#[repr(transparent)]
pub struct ValidOrNullMove(Option<Move>);
impl ValidOrNullMove {
#[expect(non_upper_case_globals)]
pub const NullMove: Self = Self(None);
#[inline]
pub const unsafe fn new_unchecked(
source: Square,
dest: Square,
promotion: Option<PieceType>,
) -> Self {
Self(Some(Move::new_unchecked(source, dest, promotion)))
}
#[inline]
pub fn new(source: Square, dest: Square, promotion: Option<PieceType>) -> Result<Self> {
Ok(Self(Some(Move::new(source, dest, promotion)?)))
}
#[inline]
pub const fn into_inner(&self) -> Option<&Move> {
self.0.as_ref()
}
#[inline]
pub const fn into_inner_mut(&mut self) -> Option<&mut Move> {
self.0.as_mut()
}
#[inline]
pub const fn is_null(&self) -> bool {
self.into_inner().is_none()
}
#[inline]
pub fn get_source(&self) -> Option<Square> {
self.into_inner().map(|move_| move_.source)
}
#[inline]
pub fn get_dest(&self) -> Option<Square> {
self.into_inner().map(|move_| move_.dest)
}
#[inline]
pub fn get_promotion(&self) -> Option<PieceType> {
self.into_inner()?.promotion
}
pub fn from_san(position: &ChessPosition, san: &str) -> Result<Self> {
let san = san.trim();
if san == "--" || san == "0000" {
return Ok(Self::NullMove);
}
Ok(Move::from_san(position, san)?.into())
}
pub fn from_lan(position: &ChessPosition, lan: &str) -> Result<Self> {
let lan = lan.trim();
if lan == "--" || lan == "0000" {
return Ok(Self::NullMove);
}
Ok(Move::from_lan(position, lan)?.into())
}
#[inline]
pub fn algebraic_without_suffix(
self,
position: &ChessPosition,
long: bool,
) -> Result<Cow<'static, str>> {
self.map_or(Ok(Cow::Borrowed("--")), |move_| {
move_.algebraic_without_suffix(position, long)
})
}
#[inline]
pub fn algebraic_and_new_position(
self,
position: &ChessPosition,
long: bool,
) -> Result<(Cow<'static, str>, ChessPosition)> {
self.map_or_else(
|| Ok((Cow::Borrowed("--"), position.null_move()?)),
|move_| move_.algebraic_and_new_position(position, long),
)
}
}
impl Default for ValidOrNullMove {
fn default() -> Self {
Self::NullMove
}
}
impl fmt::Display for ValidOrNullMove {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if let Some(move_) = self.into_inner() {
write!(f, "{}", move_)
} else {
write!(f, "--")
}
}
}
impl FromStr for ValidOrNullMove {
type Err = TimecatError;
#[inline]
fn from_str(s: &str) -> Result<Self> {
Ok(match s.trim() {
"--" | "0000" => Self::NullMove,
s_trimmed => Move::from_str(s_trimmed)?.into(),
})
}
}
impl From<Move> for ValidOrNullMove {
#[inline]
fn from(value: Move) -> Self {
Some(value).into()
}
}
impl From<&Move> for ValidOrNullMove {
#[inline]
fn from(value: &Move) -> Self {
Some(value).into()
}
}
impl From<Option<Move>> for ValidOrNullMove {
#[inline]
fn from(value: Option<Move>) -> Self {
Self(value)
}
}
impl From<Option<&Move>> for ValidOrNullMove {
#[inline]
fn from(value: Option<&Move>) -> Self {
value.copied().into()
}
}
impl Deref for ValidOrNullMove {
type Target = Option<Move>;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for ValidOrNullMove {
#[inline]
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[cfg(feature = "pyo3")]
impl<'source> FromPyObject<'source> for ValidOrNullMove {
fn extract_bound(ob: &Bound<'source, PyAny>) -> PyResult<Self> {
if let Ok(move_) = ob.extract::<Move>() {
return Ok(move_.into());
}
if let Ok(move_text) = ob.extract::<&str>()
&& let Ok(valid_or_null_move) = move_text.parse()
{
return Ok(valid_or_null_move);
}
Err(Pyo3Error::Pyo3TypeConversionError {
from: ob.to_string().into(),
to: std::any::type_name::<Self>().into(),
}
.into())
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct WeightedMove {
pub move_: Move,
pub weight: MoveWeight,
}
impl PartialOrd for WeightedMove {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for WeightedMove {
#[inline]
fn cmp(&self, other: &Self) -> Ordering {
self.weight.cmp(&other.weight)
}
}
impl WeightedMove {
#[inline]
pub fn new(move_: Move, weight: MoveWeight) -> Self {
Self { move_, weight }
}
}
impl fmt::Display for WeightedMove {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.move_, self.weight)
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub enum CastleMoveType {
KingSide,
QueenSide,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug, Hash)]
pub enum MoveType {
Capture {
is_en_passant: bool,
},
Castle(CastleMoveType),
DoublePawnPush,
Promotion(PieceType),
#[default]
Other,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MoveWithInfo {
valid_or_null_move: ValidOrNullMove,
type_: MoveType,
is_check: bool,
}