// Eryn Wells use std::fmt; use std::str::FromStr; #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[repr(u8)] pub enum Direction { North, NorthWest, West, SouthWest, South, SouthEast, East, NorthEast, } impl Direction { pub fn to_offset(&self) -> i8 { const OFFSETS: [i8; 8] = [8, 7, -1, -9, -8, -7, 1, 9]; OFFSETS[*self as usize] } } 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 { Square::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 { $vis fn new(x: $repr) -> Option { if x < $max { Some(Self(x)) } else { None } } $vis unsafe fn new_unchecked(x: $repr) -> Self { Self(x) } } impl Into<$repr> for $type { fn into(self) -> $repr { self.0 } } impl TryFrom<$repr> for $type { type Error = (); fn try_from(value: $repr) -> Result { Self::new(value).ok_or(()) } } } } 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, ]; } #[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 { pub unsafe fn from_index(x: u8) -> Square { 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) & 0b000111) } } #[inline] pub fn rank(self) -> Rank { unsafe { Rank::new_unchecked((self as u8) >> 3) } } pub fn neighbor(self, direction: Direction) -> Option { let index: u8 = self as u8; let dir: i8 = direction.to_offset(); match direction { Direction::North => Square::try_from(index.wrapping_add_signed(dir)).ok(), Direction::NorthWest => { if self.rank() != Rank::EIGHT { Square::try_from(index.wrapping_add_signed(dir)).ok() } else { None } } Direction::West => { if self.file() != File::A { Square::try_from(index.wrapping_add_signed(dir)).ok() } else { None } } Direction::SouthWest => { if self.rank() != Rank::ONE { Square::try_from(index.wrapping_add_signed(dir)).ok() } else { None } } Direction::South => { if self.rank() != Rank::ONE { Square::try_from(index.wrapping_add_signed(dir)).ok() } else { None } } Direction::SouthEast => { if self.rank() != Rank::ONE { Square::try_from(index.wrapping_add_signed(dir)).ok() } else { None } } Direction::East => { if self.file() != File::H { Square::try_from(index.wrapping_add_signed(dir)).ok() } else { None } } Direction::NorthEast => 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_none() { 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 Into for File { fn into(self) -> char { let value: u8 = self.into(); (value + 'a' as u8) as char } } impl Into for Rank { fn into(self) -> char { let value: u8 = self.into(); (value + '1' as u8) as char } } impl TryFrom for File { type Error = (); fn try_from(value: char) -> Result { File::try_from(value.to_ascii_lowercase() as u8 - 'a' as u8) } } impl TryFrom for Rank { type Error = (); fn try_from(value: char) -> Result { let result = (value as u8).checked_sub('1' as u8).ok_or(())?; Self::try_from(result) } } #[cfg(test)] mod tests { use super::*; #[test] fn good_algebraic_input() { let sq = Square::from_algebraic_str("a4").expect("Failed to parse 'a4' square"); assert_eq!(sq.file(), File(0)); assert_eq!(sq.rank(), Rank(3)); let sq = Square::from_algebraic_str("B8").expect("Failed to parse 'B8' square"); assert_eq!(sq.file(), File(1)); assert_eq!(sq.rank(), Rank(7)); let sq = Square::from_algebraic_str("e4").expect("Failed to parse 'B8' square"); assert_eq!(sq.file(), File(4)); assert_eq!(sq.rank(), Rank(3)); } #[test] fn bad_algebraic_input() { Square::from_algebraic_str("a0").expect_err("Got valid Square for 'a0'"); Square::from_algebraic_str("j3").expect_err("Got valid Square for 'j3'"); Square::from_algebraic_str("a11").expect_err("Got valid Square for 'a11'"); Square::from_algebraic_str("b-1").expect_err("Got valid Square for 'b-1'"); Square::from_algebraic_str("a 1").expect_err("Got valid Square for 'a 1'"); Square::from_algebraic_str("").expect_err("Got valid Square for ''"); } #[test] fn from_index() { let sq = Square::try_from(4u32).expect("Unable to get Square from index"); assert_eq!(sq.file(), File(4)); assert_eq!(sq.rank(), Rank(0)); let sq = Square::try_from(28u32).expect("Unable to get Square from index"); assert_eq!(sq.file(), File(4)); assert_eq!(sq.rank(), Rank(3)); } #[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"); } }