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 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> {
if source.to_int() == dest.to_int() {
return Err(TimecatError::InvalidMoveStructGeneration);
}
Ok(Self::new_unchecked(source, dest, promotion))
}
#[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> {
Self::from_str(uci)
}
pub fn from_san(position: &BoardPosition, 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.to_string() })
}
pub fn from_lan(position: &BoardPosition, lan: &str) -> Result<Self> {
let lan = lan.trim().replace('0', "O");
let lan = lan.replace('0', "O");
for valid_or_null_move in position.generate_legal_moves() {
if valid_or_null_move.lan(position).unwrap() == lan {
return Ok(valid_or_null_move);
}
}
Err(TimecatError::InvalidLanMoveString { s: lan.to_string() })
}
pub fn algebraic_without_suffix(self, position: &BoardPosition, long: bool) -> Result<String> {
let source = self.get_source();
let dest = self.get_dest();
if position.is_castling(self) {
return if dest.get_file() < source.get_file() {
Ok("O-O-O".to_string())
} else {
Ok("O-O".to_string())
};
}
let piece =
position
.get_piece_type_at(source)
.ok_or(TimecatError::InvalidSanOrLanMove {
valid_or_null_move: self.into(),
fen: position.get_fen(),
})?;
let capture = position.is_capture(self);
let mut san = if piece == Pawn {
String::new()
} else {
piece.to_colored_piece_string(White)
};
if long {
san += &source.to_string();
} 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 {
san += &(source.get_rank().to_index() + 1).to_string();
}
}
} else if capture {
san.push(
"abcdefgh"
.chars()
.nth(source.get_file().to_index())
.unwrap(),
);
}
if capture {
san += "x";
} else if long {
san += "-";
}
san += &dest.to_string();
if let Some(promotion) = self.get_promotion() {
san += &format!("={}", promotion.to_colored_piece_string(White))
}
Ok(san)
}
pub fn algebraic_and_new_position(
self,
position: &BoardPosition,
long: bool,
) -> Result<(String, BoardPosition)> {
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)?)
}
}
impl FromStr for Move {
type Err = TimecatError;
fn from_str(mut s: &str) -> Result<Self> {
let error = TimecatError::InvalidUciMoveString { s: s.to_string() };
s = s.trim();
if s.len() > 6 {
return Err(error.clone());
}
let source = Square::from_str(s.get(0..2).ok_or(error.clone())?)?;
let dest = Square::from_str(s.get(2..4).ok_or(error.clone())?)?;
let mut promotion = None;
if s.len() == 5 {
promotion = Some(match s.chars().last().ok_or(error.clone())? {
'q' => Queen,
'r' => Rook,
'n' => Knight,
'b' => Bishop,
_ => return Err(error.clone()),
});
}
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>() {
if let Ok(move_) = Self::from_str(move_text) {
return Ok(move_);
}
}
if let Ok(move_) = Self::from_py_move(ob) {
return Ok(move_);
}
Err(Pyo3Error::Pyo3TypeConversionError {
from: ob.to_string(),
to: std::any::type_name::<Self>().to_string(),
}
.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 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: &BoardPosition, san: &str) -> Result<Self> {
let san = san.trim();
if san == "--" || san == "0000" {
return Ok(ValidOrNullMove::NullMove);
}
Ok(Move::from_san(position, san)?.into())
}
pub fn from_lan(position: &BoardPosition, lan: &str) -> Result<Self> {
let lan = lan.trim();
if lan == "--" || lan == "0000" {
return Ok(ValidOrNullMove::NullMove);
}
Ok(Move::from_lan(position, lan)?.into())
}
#[inline]
pub fn algebraic_without_suffix(self, position: &BoardPosition, long: bool) -> Result<String> {
self.map(|move_| move_.algebraic_without_suffix(position, long))
.unwrap_or(Ok("--".to_string()))
}
#[inline]
pub fn algebraic_and_new_position(
self,
position: &BoardPosition,
long: bool,
) -> Result<(String, BoardPosition)> {
self.map(|move_| move_.algebraic_and_new_position(position, long))
.unwrap_or(Ok(("--".to_string(), position.null_move()?)))
}
}
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;
fn from_str(s: &str) -> Result<Self> {
if s == "--" || s == "0000" {
return Ok(Self::NullMove);
}
Ok(Move::from_str(s)?.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>() {
if let Ok(valid_or_null_move) = Self::from_str(move_text) {
return Ok(valid_or_null_move);
}
}
Err(Pyo3Error::Pyo3TypeConversionError {
from: ob.to_string(),
to: std::any::type_name::<Self>().to_string(),
}
.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,
}