// Eryn Wells use crate::Color; use std::{fmt, str::FromStr}; 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 { 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 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 fn as_index(&self) -> &$repr { &self.0 } #[must_use] $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; 8] = [ 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; 8] = [ 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]; pub fn is_pawn_starting_rank(&self, color: Color) -> bool { self == &Self::PAWN_STARTING_RANKS[color as usize] } pub fn is_pawn_double_push_target_rank(&self, color: Color) -> bool { self == &Self::PAWN_DOUBLE_PUSH_TARGET_RANKS[color as usize] } } #[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 ]); impl Square { /// # Safety /// /// This function does not do any bounds checking on the input. pub unsafe fn from_index(x: u8) -> Square { debug_assert!((x as usize) < Self::NUM); Self::try_from(x).unwrap_unchecked() } #[inline] 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(rank_int << 3 | file_int) } } pub fn from_algebraic_str(s: &str) -> Result { s.parse() } #[inline] pub fn file(self) -> File { unsafe { File::new_unchecked((self as u8) & 0b000_00111) } } #[inline] pub fn rank(self) -> Rank { unsafe { Rank::new_unchecked((self as u8) >> 3) } } pub fn file_rank(&self) -> (File, Rank) { (self.file(), self.rank()) } pub fn neighbor(self, direction: Direction) -> Option { let index: u8 = self as u8; let dir: i8 = direction.to_offset(); match direction { Direction::North | Direction::NorthEast => { Square::try_from(index.wrapping_add_signed(dir)).ok() } Direction::NorthWest => { if self.rank() == Rank::EIGHT { None } else { Square::try_from(index.wrapping_add_signed(dir)).ok() } } Direction::West => { if self.file() == File::A { None } else { Square::try_from(index.wrapping_add_signed(dir)).ok() } } Direction::SouthEast | Direction::South | Direction::SouthWest => { if self.rank() == Rank::ONE { None } else { Square::try_from(index.wrapping_add_signed(dir)).ok() } } Direction::East => { if self.file() == File::H { None } else { Square::try_from(index.wrapping_add_signed(dir)).ok() } } } } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct ParseSquareError; 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)?; let rank: Rank = chars .next() .and_then(|c| c.try_into().ok()) .ok_or(ParseSquareError)?; if chars.next().is_some() { return Err(ParseSquareError); } Ok(Square::from_file_rank(file, rank)) } } 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 Into for Rank { fn into(self) -> char { let value: u8 = self.into(); (value + b'1') as char } } 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(0)); assert_eq!(sq.rank(), Rank(3)); let sq = Square::from_algebraic_str("B8")?; assert_eq!(sq.file(), File(1)); assert_eq!(sq.rank(), Rank(7)); let sq = Square::from_algebraic_str("e4")?; assert_eq!(sq.file(), File(4)); assert_eq!(sq.rank(), Rank(3)); Ok(()) } #[test] fn bad_algebraic_input() -> Result<(), ParseSquareError> { Square::from_algebraic_str("a0")?; Square::from_algebraic_str("j3")?; Square::from_algebraic_str("a11")?; Square::from_algebraic_str("b-1")?; Square::from_algebraic_str("a 1")?; Square::from_algebraic_str("")?; Ok(()) } #[test] fn from_index() -> Result<(), ()> { let sq = Square::try_from(4u32)?; assert_eq!(sq.file(), File(4)); assert_eq!(sq.rank(), Rank(0)); let sq = Square::try_from(28u32)?; assert_eq!(sq.file(), File(4)); assert_eq!(sq.rank(), Rank(3)); 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"); } }