From 025ceb2694886149668de6cec0d76da4ef958aa5 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 23 Jan 2024 17:18:48 -0800 Subject: [PATCH] [core] Move the contents of board::square to core::cordinates Export Square, Direction, Rank, and File from the core crate. --- Cargo.toml | 1 + core/Cargo.toml | 8 + core/src/coordinates.rs | 417 ++++++++++++++++++++++++++++++++++++++++ core/src/lib.rs | 3 + 4 files changed, 429 insertions(+) create mode 100644 core/Cargo.toml create mode 100644 core/src/coordinates.rs create mode 100644 core/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 655f6ac..d595fa5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,5 +2,6 @@ members = [ "board", + "core", "explorer", ] diff --git a/core/Cargo.toml b/core/Cargo.toml new file mode 100644 index 0000000..900733d --- /dev/null +++ b/core/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "core" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/core/src/coordinates.rs b/core/src/coordinates.rs new file mode 100644 index 0000000..fd0d25e --- /dev/null +++ b/core/src/coordinates.rs @@ -0,0 +1,417 @@ +// 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"); + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs new file mode 100644 index 0000000..2fe191d --- /dev/null +++ b/core/src/lib.rs @@ -0,0 +1,3 @@ +mod coordinates; + +pub use coordinates::{Direction, File, Rank, Square};