chessfriend/board/src/square.rs
Eryn Wells 0bc7e8d542 [board] Implement some castling checks on Move
Define some constants in Square that refer to the starting positions of the two
kings, and the target castling squares. Then implement the following methods that
use those constants to determine if a move is a castle.

- Move::is_kingside_castle()
- Move::is_queenside_castle()
- Move::is_castle()

These checks only apply to King moves, and if the king is moving from and to
specific squares.
2024-01-10 13:37:18 -08:00

334 lines
9.4 KiB
Rust

// Eryn Wells <eryn@erynwells.me>
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<Self> {
$(
#[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<char> for File {
fn into(self) -> char {
('a' as u8 + self as u8) as char
}
}
impl TryFrom<char> for File {
type Error = ParseFileError;
fn try_from(value: char) -> Result<Self, Self::Error> {
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::<char>::into(*self).to_uppercase())
}
}
impl Into<char> for Rank {
fn into(self) -> char {
('1' as u8 + self as u8) as char
}
}
impl TryFrom<char> for Rank {
type Error = ParseFileError;
fn try_from(value: char) -> Result<Self, Self::Error> {
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::<char>::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<Square, ParseSquareError> {
s.parse()
}
pub fn neighbor(self, direction: Direction) -> Option<Square> {
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<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_none() {
return Err(ParseSquareError);
}
Ok(Square::from_file_rank(file, rank))
}
}
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");
}
}