pounce 2.0.2

A mediocre (but trying its best) uci chess engine
Documentation
use std::fmt::{self, Debug, Display, Formatter};
use std::str::FromStr;

use thiserror::Error;

use crate::chess::board::ParseSquareError;
use crate::chess::piece::ParseRoleError;
use crate::chess::{Position, Role, Square};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MoveType {
    Normal, // Also includes captures
    EnPassant,
    DoublePawnPush,
    Castle,
    Promotion,
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub struct Move(u16);

impl Move {
    #[inline]
    pub fn new(from: Square, to: Square, promotion: Option<Role>) -> Move {
        let from = from as u16;
        let to = to as u16;
        let promotion = promotion
            .map(|role| role as u16)
            .unwrap_or(Role::NUM as u16);
        Move(from | (to << 6) | (promotion << 12))
    }

    #[inline]
    pub fn from(self) -> Square {
        Square::new_unchecked((self.0 & 0x3f) as u8)
    }

    #[inline]
    pub fn to(self) -> Square {
        Square::new_unchecked(((self.0 >> 6) & 0x3f) as u8)
    }

    #[inline]
    pub fn promotion(self) -> Option<Role> {
        unsafe { std::mem::transmute((self.0 >> 12) as u8) }
    }

    // This only works for valid moves
    #[inline]
    pub fn move_type(self, role: Role, ep_square: Option<Square>) -> MoveType {
        if self.promotion().is_some() {
            MoveType::Promotion
        } else if role == Role::Pawn
            && self.from().file() != self.to().file()
            && ep_square.is_some_and(|sq| sq == self.to())
        {
            MoveType::EnPassant
        } else if role == Role::Pawn && self.from().rank().distance(self.to().rank()) == 2 {
            MoveType::DoublePawnPush
        } else if role == Role::King && self.from().file().distance(self.to().file()) == 2 {
            if self.from().rank() == self.to().rank() {
                MoveType::Castle
            } else {
                MoveType::Normal
            }
        } else {
            MoveType::Normal
        }
    }

    #[inline]
    pub fn is_capture(self, pos: &Position) -> bool {
        pos.occupancy.contains(self.to())
            || self.move_type(pos.role_at(self.from()).unwrap(), pos.ep_square)
                == MoveType::EnPassant
    }

    pub fn is_quiet(self, pos: &Position) -> bool {
        !self.is_capture(pos)
            && self.move_type(pos.role_at(self.from()).unwrap(), pos.ep_square)
                != MoveType::Promotion
    }

    pub fn is_promotion(self) -> bool {
        self.promotion().is_some()
    }

    #[inline]
    pub fn captured_role(self, pos: &Position) -> Option<Role> {
        pos.role_at(self.to()).or_else(|| {
            if self.is_capture(pos) {
                Some(Role::Pawn)
            } else {
                None
            }
        })
    }

    pub const NULL: Move = Move(u16::MAX);
    pub const NONE: Move = Move(0);
}

impl Default for Move {
    fn default() -> Self {
        Move::NONE
    }
}

impl Display for Move {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        write!(f, "{}{}", self.from(), self.to())?;
        if let Some(promotion) = self.promotion() {
            let promotion = promotion.to_string().to_ascii_lowercase();
            write!(f, "{promotion}")?;
        }
        Ok(())
    }
}

impl Debug for Move {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        write!(f, "{self}")
    }
}

impl From<Move> for u16 {
    fn from(mv: Move) -> u16 {
        mv.0
    }
}

#[derive(Debug, Error)]
pub enum ParseMoveError {
    #[error("expected 4 or 5 characters, found {0}")]
    InvalidLength(usize),
    #[error("invalid square")]
    InvalidSquare(#[from] ParseSquareError),
    #[error("invalid role")]
    InvalidRole(#[from] ParseRoleError),
}

impl FromStr for Move {
    type Err = ParseMoveError;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.len() {
            4 => {
                let from = Square::from_str(&s[0..2])?;
                let to = Square::from_str(&s[2..4])?;
                Ok(Move::new(from, to, None))
            }
            5 => {
                let from = Square::from_str(&s[0..2])?;
                let to = Square::from_str(&s[2..4])?;
                let promotion = Role::from_str(&s[4..5])?;
                Ok(Move::new(from, to, Some(promotion)))
            }
            _ => Err(ParseMoveError::InvalidLength(s.len())),
        }
    }
}