// Eryn Wells use std::{fmt, str::FromStr}; pub enum Direction { North, NorthWest, West, SouthWest, South, SouthEast, East, NorthEast, } #[derive(Debug)] pub struct ParseFileError; #[derive(Debug)] pub struct ParseSquareError; #[derive(Debug)] pub struct SquareOutOfBoundsError; 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), *]; #[inline] pub(crate) fn from_index(index: usize) -> Self { assert!( index < Self::NUM, "Index {} out of bounds for {}.", index, stringify!($name) ); Self::try_index(index).unwrap() } pub fn try_index(index: usize) -> Option { $( #[allow(non_upper_case_globals)] const $variant: usize = $name::$variant as usize; )* #[allow(non_upper_case_globals)] match index { $($variant => Some($name::$variant),)* _ => None, } } } } } #[rustfmt::skip] coordinate_enum!(Rank, One, Two, Three, Four, Five, Six, Seven, Eight ); #[rustfmt::skip] coordinate_enum!(File, A, B, C, D, E, F, G, H ); #[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 Into for File { fn into(self) -> char { ('a' as u8 + self as u8) as char } } impl TryFrom for File { type Error = ParseFileError; fn try_from(value: char) -> Result { let lowercase_value = value.to_ascii_lowercase(); for file in File::ALL.iter() { if lowercase_value == (*file).into() { return Ok(*file); } } Err(ParseFileError) } } impl fmt::Display for File { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", Into::::into(*self).to_uppercase()) } } impl Into for Rank { fn into(self) -> char { ('1' as u8 + self as u8) as char } } impl TryFrom for Rank { type Error = ParseFileError; fn try_from(value: char) -> Result { let lowercase_value = value.to_ascii_lowercase(); for rank in Self::ALL.iter().cloned() { if lowercase_value == rank.into() { return Ok(rank); } } Err(ParseFileError) } } impl fmt::Display for Rank { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", Into::::into(*self)) } } impl Square { #[inline] pub fn from_file_rank(file: File, rank: Rank) -> Square { Self::from_index((rank as usize) << 3 | file as usize) } #[inline] pub fn file(self) -> File { File::from_index(self as usize & 0b000111) } #[inline] pub fn rank(self) -> Rank { Rank::from_index(self as usize >> 3) } } impl Square { pub(crate) const KING_STARTING_SQUARES: [Square; 2] = [Square::E1, Square::E8]; pub(crate) const KING_CASTLE_TARGET_SQUARES: [[Square; 2]; 2] = [[Square::C1, Square::G1], [Square::C8, Square::G8]]; pub fn from_algebraic_str(s: &str) -> Result { s.parse() } pub fn neighbor(self, direction: Direction) -> Option { match direction { Direction::North => Square::try_index(self as usize + 8), Direction::NorthWest => { if self.rank() != Rank::Eight { Square::try_index(self as usize + 7) } else { None } } Direction::West => { if self.file() != File::A { Square::try_index(self as usize - 1) } else { None } } Direction::SouthWest => { if self.rank() != Rank::One { Square::try_index(self as usize - 9) } else { None } } Direction::South => { if self.rank() != Rank::One { Square::try_index(self as usize - 8) } else { None } } Direction::SouthEast => { if self.rank() != Rank::One { Square::try_index(self as usize - 7) } else { None } } Direction::East => { if self.file() != File::H { Square::try_index(self as usize + 1) } else { None } } Direction::NorthEast => Square::try_index(self as usize + 9), } } } 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)) } } macro_rules! try_from_integer { ($int_type:ident) => { impl TryFrom<$int_type> for Square { type Error = SquareOutOfBoundsError; fn try_from(value: $int_type) -> Result { Square::try_index(value as usize).ok_or(SquareOutOfBoundsError) } } }; } try_from_integer!(u8); try_from_integer!(u16); try_from_integer!(u32); try_from_integer!(u64); impl fmt::Display for Square { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "{}{}", ('a' as u8 + self.file() as u8) as char, self.rank() as usize + 1 ) } } #[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::A); assert_eq!(sq.rank(), Rank::Four); let sq = Square::from_algebraic_str("B8").expect("Failed to parse 'B8' square"); assert_eq!(sq.file(), File::B); assert_eq!(sq.rank(), Rank::Eight); let sq = Square::from_algebraic_str("e4").expect("Failed to parse 'B8' square"); assert_eq!(sq.file(), File::E); assert_eq!(sq.rank(), Rank::Four); } #[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_index(4).expect("Unable to get Square from index"); assert_eq!(sq.file(), File::E); assert_eq!(sq.rank(), Rank::One); let sq = Square::try_index(28).expect("Unable to get Square from index"); assert_eq!(sq.file(), File::E); assert_eq!(sq.rank(), Rank::Four); } #[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"); } }