chessfriend/core/src/coordinates.rs
Eryn Wells a2d0c638d0 [core] Address clippy suggestions; clean up unit tests
In coordinates.rs:

- Add some [must_use] decorators to some getters
- Rewrite some unit tests to remove the .expect() and use ? instead
2024-04-25 08:05:07 -07:00

477 lines
13 KiB
Rust

// Eryn Wells <eryn@erynwells.me>
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, Self::Error> {
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<u8> for $name {
type Error = ();
fn try_from(value: u8) -> Result<Self, Self::Error> {
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<Self> {
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<Item = Self> {
(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, Self::Error> {
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<Square, ParseSquareError> {
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<Square> {
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<Self, Self::Err> {
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::<char>::into(*self))
}
}
impl fmt::Display for Rank {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", Into::<char>::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<File> for char {
fn from(value: File) -> Self {
let u8value: u8 = value.into();
(u8value + b'a') as char
}
}
impl Into<char> for Rank {
fn into(self) -> char {
let value: u8 = self.into();
(value + b'1') as char
}
}
impl TryFrom<char> for File {
type Error = ();
fn try_from(value: char) -> Result<Self, Self::Error> {
File::try_from(value.to_ascii_lowercase() as u8 - b'a')
}
}
impl TryFrom<char> for Rank {
type Error = ();
fn try_from(value: char) -> Result<Self, Self::Error> {
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");
}
}