In coordinates.rs: - Add some [must_use] decorators to some getters - Rewrite some unit tests to remove the .expect() and use ? instead
477 lines
13 KiB
Rust
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");
|
|
}
|
|
}
|