// Eryn Wells mod wings; pub use wings::Wing; use crate::Color; use std::{fmt, str::FromStr}; use thiserror::Error; macro_rules! try_from_integer { ($type:ident, $int_type:ident) => { impl TryFrom<$int_type> for $type { type Error = (); fn try_from(value: $int_type) -> Result { #[allow(clippy::cast_possible_truncation)] Self::try_from(value as u8) } } }; } macro_rules! coordinate_enum { ($name: ident, [ $($variant:ident),* ]) => { #[repr(u8)] #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum $name { $($variant), * } impl $name { pub const NUM: usize = [$(Self::$variant), *].len(); pub const ALL: [Self; Self::NUM] = [$(Self::$variant), *]; } impl TryFrom for $name { type Error = (); fn try_from(value: u8) -> Result { let value_usize = value as usize; if value_usize < Self::NUM { Ok($name::ALL[value_usize]) } else { Err(()) } } } try_from_integer!($name, u16); try_from_integer!($name, u32); try_from_integer!($name, u64); } } macro_rules! range_bound_struct { ($vis:vis $type:ident, $repr:ty, $max:expr) => { #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] $vis struct $type($repr); #[allow(dead_code)] impl $type { $vis const NUM: usize = $max; $vis const FIRST: $type = $type(0); $vis const LAST: $type = $type($max - 1); } impl $type { #[must_use] $vis fn new(x: $repr) -> Option { if x < $max { Some(Self(x)) } else { None } } /// Create a new `Self` /// /// # Safety /// /// This function does not perform any bounds checking. It should only be called when /// the input is already known to be within bounds, i.e. when `x >= Self::FIRST && x < Self::LAST`. #[must_use] $vis unsafe fn new_unchecked(x: $repr) -> Self { debug_assert!((Self::FIRST.0..=Self::LAST.0).contains(&x)); Self(x) } #[must_use] $vis const fn as_index(&self) -> usize { self.0 as usize } $vis fn iter(&self) -> impl Iterator { (Self::FIRST.0..=Self::LAST.0).map(Self) } } impl From<$type> for $repr { fn from(x: $type) -> Self { x.0 } } impl TryFrom<$repr> for $type { type Error = (); fn try_from(value: $repr) -> Result { Self::new(value).ok_or(()) } } } } coordinate_enum!( Direction, [ North, NorthEast, East, SouthEast, South, SouthWest, West, NorthWest ] ); impl Direction { #[must_use] pub fn to_offset(&self) -> i8 { const OFFSETS: [i8; 8] = [8, 9, 1, -7, -8, -9, -1, 7]; OFFSETS[*self as usize] } #[must_use] pub fn opposite(&self) -> Direction { const OPPOSITES: [Direction; 8] = [ Direction::South, Direction::SouthEast, Direction::East, Direction::NorthEast, Direction::North, Direction::NorthWest, Direction::West, Direction::SouthWest, ]; OPPOSITES[*self as usize] } } range_bound_struct!(pub File, u8, 8); impl File { pub const A: File = File(0); pub const B: File = File(1); pub const C: File = File(2); pub const D: File = File(3); pub const E: File = File(4); pub const F: File = File(5); pub const G: File = File(6); pub const H: File = File(7); pub const ALL: [File; File::NUM] = [ File::A, File::B, File::C, File::D, File::E, File::F, File::G, File::H, ]; } range_bound_struct!(pub Rank, u8, 8); #[allow(dead_code)] impl Rank { pub const ONE: Rank = Rank(0); pub const TWO: Rank = Rank(1); pub const THREE: Rank = Rank(2); pub const FOUR: Rank = Rank(3); pub const FIVE: Rank = Rank(4); pub const SIX: Rank = Rank(5); pub const SEVEN: Rank = Rank(6); pub const EIGHT: Rank = Rank(7); pub const ALL: [Rank; Self::NUM] = [ Rank::ONE, Rank::TWO, Rank::THREE, Rank::FOUR, Rank::FIVE, Rank::SIX, Rank::SEVEN, Rank::EIGHT, ]; /// Ranks on which pawns start, by color. /// /// ``` /// use chessfriend_core::{Color, Rank}; /// assert_eq!(Rank::PAWN_STARTING_RANKS[Color::White as usize], Rank::TWO); /// assert_eq!(Rank::PAWN_STARTING_RANKS[Color::Black as usize], Rank::SEVEN); /// ``` pub const PAWN_STARTING_RANKS: [Rank; 2] = [Rank::TWO, Rank::SEVEN]; pub const PAWN_DOUBLE_PUSH_TARGET_RANKS: [Rank; 2] = [Rank::FOUR, Rank::FIVE]; #[must_use] pub fn is_pawn_starting_rank(&self, color: Color) -> bool { self == &Self::PAWN_STARTING_RANKS[color as usize] } #[must_use] pub fn is_pawn_double_push_target_rank(&self, color: Color) -> bool { self == &Self::PAWN_DOUBLE_PUSH_TARGET_RANKS[color as usize] } /// Ranks where promotions happen. #[must_use] pub fn is_promotable_rank(&self) -> bool { matches!(*self, Rank::ONE | Rank::EIGHT) } } #[rustfmt::skip] coordinate_enum!(Square, [ A1, B1, C1, D1, E1, F1, G1, H1, A2, B2, C2, D2, E2, F2, G2, H2, A3, B3, C3, D3, E3, F3, G3, H3, A4, B4, C4, D4, E4, F4, G4, H4, A5, B5, C5, D5, E5, F5, G5, H5, A6, B6, C6, D6, E6, F6, G6, H6, A7, B7, C7, D7, E7, F7, G7, H7, A8, B8, C8, D8, E8, F8, G8, H8 ]); /// Generate an enum that maps its values to variants of [Square]. macro_rules! to_square_enum { ($vis:vis $name:ident { $($variant:ident)* }) => { #[repr(u8)] #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] $vis enum $name { $($variant = Square::$variant as u8,)* } impl From<$name> for Square { fn from(value: $name) -> Self { unsafe { Square::from_index_unchecked(value as u8) } } } }; } to_square_enum!( pub EnPassantTargetSquare { A3 B3 C3 D3 E3 F3 G3 H3 A6 B6 C6 D6 E6 F6 G6 H6 } ); impl Square { /// # Safety /// /// This function does not do any bounds checking on the input. In debug /// builds, this function will assert that the argument is in bounds. #[must_use] pub unsafe fn from_index_unchecked(x: u8) -> Square { debug_assert!((x as usize) < Self::NUM); Self::ALL[x as usize] } #[inline] #[must_use] pub fn from_file_rank(file: File, rank: Rank) -> Square { let file_int: u8 = file.into(); let rank_int: u8 = rank.into(); unsafe { Self::from_index_unchecked(rank_int << 3 | file_int) } } pub fn from_algebraic_str(s: &str) -> Result { s.parse() } #[must_use] #[inline] pub fn file(self) -> File { unsafe { File::new_unchecked((self as u8) & 0b000_00111) } } #[must_use] #[inline] pub fn rank(self) -> Rank { unsafe { Rank::new_unchecked((self as u8) >> 3) } } #[must_use] pub fn file_rank(&self) -> (File, Rank) { (self.file(), self.rank()) } #[must_use] pub fn neighbor(self, direction: Direction) -> Option { match direction { Direction::North => { if self.rank() == Rank::EIGHT { return None; } } Direction::NorthEast => { let (file, rank) = self.file_rank(); if rank == Rank::EIGHT || file == File::H { return None; } } Direction::East => { if self.file() == File::H { return None; } } Direction::SouthEast => { let (file, rank) = self.file_rank(); if rank == Rank::ONE || file == File::H { return None; } } Direction::South => { if self.rank() == Rank::ONE { return None; } } Direction::SouthWest => { let (file, rank) = self.file_rank(); if rank == Rank::ONE || file == File::A { return None; } } Direction::West => { if self.file() == File::A { return None; } } Direction::NorthWest => { let (file, rank) = self.file_rank(); if rank == Rank::EIGHT || file == File::A { return None; } } } let index: u8 = self as u8; let direction = direction.to_offset(); Square::try_from(index.wrapping_add_signed(direction)).ok() } } #[derive(Clone, Debug, Error, Eq, PartialEq)] pub enum ParseSquareError { #[error("{0}")] RankError(#[from] ParseRankError), #[error("{0}")] FileError(#[from] ParseFileError), } impl TryFrom<&str> for Square { type Error = ParseSquareError; fn try_from(value: &str) -> Result { let mut chars = value.chars(); let file: File = chars .next() .and_then(|c| c.try_into().ok()) .ok_or(ParseSquareError::FileError(ParseFileError))?; let rank: Rank = chars .next() .and_then(|c| c.try_into().ok()) .ok_or(ParseSquareError::RankError(ParseRankError))?; Ok(Square::from_file_rank(file, rank)) } } impl FromStr for Square { type Err = ParseSquareError; fn from_str(s: &str) -> Result { let mut chars = s.chars(); let file: File = chars .next() .and_then(|c| c.try_into().ok()) .ok_or(ParseSquareError::FileError(ParseFileError))?; let rank: Rank = chars .next() .and_then(|c| c.try_into().ok()) .ok_or(ParseSquareError::RankError(ParseRankError))?; Ok(Square::from_file_rank(file, rank)) } } #[derive(Clone, Debug, Error, Eq, PartialEq)] #[error("invalid rank")] pub struct ParseRankError; impl FromStr for Rank { type Err = ParseRankError; fn from_str(s: &str) -> Result { let ch = s .chars() .nth(0) .ok_or(ParseRankError) .map(|ch| ch.to_ascii_lowercase())?; let offset = 'a' as usize - (ch as usize); if offset >= Rank::ALL.len() { return Err(ParseRankError); } Ok(Rank::ALL[offset]) } } #[derive(Clone, Debug, Error, Eq, PartialEq)] #[error("invalid file")] pub struct ParseFileError; impl FromStr for File { type Err = ParseFileError; fn from_str(s: &str) -> Result { let ch = s .chars() .nth(0) .ok_or(ParseFileError) .map(|ch| ch.to_ascii_lowercase())?; let offset = '1' as usize - (ch as usize); if offset >= File::ALL.len() { return Err(ParseFileError); } Ok(File::ALL[offset]) } } impl fmt::Display for File { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", Into::::into(*self)) } } impl fmt::Display for Rank { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", Into::::into(*self)) } } impl fmt::Display for Square { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.file().fmt(f)?; self.rank().fmt(f)?; Ok(()) } } impl From for char { fn from(value: File) -> Self { let u8value: u8 = value.into(); (u8value + b'a') as char } } impl From for char { fn from(value: Rank) -> Self { Self::from(value.0 + b'1') } } impl TryFrom for File { type Error = (); fn try_from(value: char) -> Result { File::try_from(value.to_ascii_lowercase() as u8 - b'a') } } impl TryFrom for Rank { type Error = (); fn try_from(value: char) -> Result { let result = (value as u8).checked_sub(b'1').ok_or(())?; Self::try_from(result) } } #[cfg(test)] mod tests { use super::*; #[test] fn direction_offsets() { assert_eq!(Direction::North.to_offset(), 8); assert_eq!(Direction::NorthEast.to_offset(), 9); assert_eq!(Direction::East.to_offset(), 1); assert_eq!(Direction::SouthEast.to_offset(), -7); assert_eq!(Direction::South.to_offset(), -8); assert_eq!(Direction::SouthWest.to_offset(), -9); assert_eq!(Direction::West.to_offset(), -1); assert_eq!(Direction::NorthWest.to_offset(), 7); } #[test] fn good_algebraic_input() -> Result<(), ParseSquareError> { let sq = Square::from_algebraic_str("a4")?; assert_eq!(sq.file(), File::A); assert_eq!(sq.rank(), Rank::FOUR); let sq = Square::from_algebraic_str("B8")?; assert_eq!(sq.file(), File::B); assert_eq!(sq.rank(), Rank::EIGHT); let sq = Square::from_algebraic_str("e4")?; assert_eq!(sq.file(), File::E); assert_eq!(sq.rank(), Rank::FOUR); Ok(()) } #[test] fn bad_algebraic_input() { assert!("a0".parse::().is_err()); assert!("j3".parse::().is_err()); assert!("a9".parse::().is_err()); assert!("b-1".parse::().is_err()); assert!("a 1".parse::().is_err()); assert!("".parse::().is_err()); } #[test] fn from_index() -> Result<(), ()> { let sq = Square::try_from(4u32)?; assert_eq!(sq.file(), File::E); assert_eq!(sq.rank(), Rank::ONE); let sq = Square::try_from(28u32)?; assert_eq!(sq.file(), File::E); assert_eq!(sq.rank(), Rank::FOUR); Ok(()) } #[test] fn to_index() { assert_eq!(Square::A1 as usize, 0); assert_eq!(Square::H8 as usize, 63); } #[test] fn valid_neighbors() { let sq = Square::E4; assert_eq!(sq.neighbor(Direction::North), Some(Square::E5)); assert_eq!(sq.neighbor(Direction::NorthEast), Some(Square::F5)); assert_eq!(sq.neighbor(Direction::East), Some(Square::F4)); assert_eq!(sq.neighbor(Direction::SouthEast), Some(Square::F3)); assert_eq!(sq.neighbor(Direction::South), Some(Square::E3)); assert_eq!(sq.neighbor(Direction::SouthWest), Some(Square::D3)); assert_eq!(sq.neighbor(Direction::West), Some(Square::D4)); assert_eq!(sq.neighbor(Direction::NorthWest), Some(Square::D5)); } #[test] fn invalid_neighbors() { let sq = Square::A1; assert!(sq.neighbor(Direction::West).is_none()); assert!(sq.neighbor(Direction::SouthWest).is_none()); assert!(sq.neighbor(Direction::South).is_none()); let sq = Square::H1; assert!(sq.neighbor(Direction::East).is_none()); assert!(sq.neighbor(Direction::SouthEast).is_none()); assert!(sq.neighbor(Direction::South).is_none()); let sq = Square::A8; assert!(sq.neighbor(Direction::North).is_none()); assert!(sq.neighbor(Direction::NorthWest).is_none()); assert!(sq.neighbor(Direction::West).is_none()); let sq = Square::H8; assert!(sq.neighbor(Direction::North).is_none()); assert!(sq.neighbor(Direction::NorthEast).is_none()); assert!(sq.neighbor(Direction::East).is_none()); } #[test] fn display() { assert_eq!(format!("{}", Square::C5), "c5"); assert_eq!(format!("{}", Square::A1), "a1"); assert_eq!(format!("{}", Square::H8), "h8"); assert_eq!(format!("{}", Rank::FIVE), "5"); assert_eq!(format!("{}", File::H), "h"); } #[test] fn into_char() { assert_eq!(Into::::into(Rank::ONE), '1'); assert_eq!(Into::::into(File::B), 'b'); } }