chessfriend/board/src/square.rs
Eryn Wells 153e21b693 [board] Implement finding a neighbor of a square
Use the Direction enum to calculate the neighbor of a square with bounds checking.
Remove the unused rank_index() and file_index methods.
Add Square::from_rank_file and Square::from_index_unsafe to support tests.
Unify the Square*OutOfBounds error types
2023-12-23 09:30:45 -07:00

246 lines
7.2 KiB
Rust

// Eryn Wells <eryn@erynwells.me>
use crate::neighbor::Direction;
use std::str::FromStr;
#[derive(Debug)]
pub struct ParseSquareError;
#[derive(Debug)]
pub struct SquareOutOfBoundsError;
#[derive(Debug, Eq, PartialEq)]
pub struct Square {
pub rank: u8,
pub file: u8,
pub index: u8,
}
impl Square {
pub fn from_index(index: u8) -> Result<Square, SquareOutOfBoundsError> {
if index >= 64 {
return Err(SquareOutOfBoundsError);
}
Ok(Square::from_index_unsafe(index))
}
fn from_index_unsafe(index: u8) -> Square {
Square {
rank: index / 8,
file: index % 8,
index: index,
}
}
pub fn from_rank_file(rank: u8, file: u8) -> Result<Square, SquareOutOfBoundsError> {
if rank >= 8 || file >= 8 {
return Err(SquareOutOfBoundsError);
}
Ok(Square {
rank,
file,
index: rank * 8 + file,
})
}
pub fn from_algebraic_string(s: &str) -> Result<Square, ParseSquareError> {
s.parse()
}
pub fn neighbor(&self, direction: Direction) -> Option<Square> {
match direction {
Direction::North => Square::from_index(self.index + 8),
Direction::NorthWest => {
if self.rank < 7 {
Square::from_index(self.index + 7)
} else {
Err(SquareOutOfBoundsError)
}
}
Direction::West => {
if self.file > 0 {
Square::from_index(self.index - 1)
} else {
Err(SquareOutOfBoundsError)
}
}
Direction::SouthWest => {
if self.rank > 0 {
Square::from_index(self.index - 9)
} else {
Err(SquareOutOfBoundsError)
}
}
Direction::South => {
if self.rank > 0 {
Square::from_index(self.index - 8)
} else {
Err(SquareOutOfBoundsError)
}
}
Direction::SouthEast => {
if self.rank > 0 {
Square::from_index(self.index - 7)
} else {
Err(SquareOutOfBoundsError)
}
}
Direction::East => {
if self.file < 7 {
Square::from_index(self.index + 1)
} else {
Err(SquareOutOfBoundsError)
}
}
Direction::NorthEast => Square::from_index(self.index + 9),
}
.ok()
}
}
impl FromStr for Square {
type Err = ParseSquareError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if !s.is_ascii() || s.len() != 2 {
return Err(ParseSquareError);
}
let mut chars = s.chars();
let file_char = chars.next().unwrap().to_ascii_lowercase();
if !file_char.is_ascii_lowercase() {
return Err(ParseSquareError);
}
let file = (file_char as u8) - ('a' as u8);
if file >= 8 {
return Err(ParseSquareError);
}
let converted_rank_digit = chars
.next()
.unwrap()
.to_digit(10)
.and_then(|x| if x >= 1 && x <= 8 { Some(x) } else { None })
.ok_or(ParseSquareError)?;
let rank = u8::try_from(converted_rank_digit).map_err(|_| ParseSquareError)? - 1;
Ok(Square {
rank,
file,
index: rank * 8 + file,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn good_algebraic_input() {
let sq1 = Square::from_algebraic_string("a4").expect("Failed to parse 'a4' square");
assert_eq!(sq1.file, 0);
assert_eq!(sq1.rank, 3);
let sq2 = Square::from_algebraic_string("B8").expect("Failed to parse 'B8' square");
assert_eq!(sq2.file, 1);
assert_eq!(sq2.rank, 7);
let sq3 = Square::from_algebraic_string("e4").expect("Failed to parse 'B8' square");
assert_eq!(sq3.rank, 3, "Expected rank of e4 to be 3");
assert_eq!(sq3.file, 4, "Expected file of e4 to be 4");
}
#[test]
fn bad_algebraic_input() {
Square::from_algebraic_string("a0").expect_err("Got valid Square for 'a0'");
Square::from_algebraic_string("j3").expect_err("Got valid Square for 'j3'");
Square::from_algebraic_string("a11").expect_err("Got valid Square for 'a11'");
Square::from_algebraic_string("b-1").expect_err("Got valid Square for 'b-1'");
Square::from_algebraic_string("a 1").expect_err("Got valid Square for 'a 1'");
Square::from_algebraic_string("").expect_err("Got valid Square for ''");
}
#[test]
fn from_index() {
let sq1 = Square::from_index(4).expect("Unable to get Square from index");
assert_eq!(sq1.rank, 0);
assert_eq!(sq1.file, 4);
let sq1 = Square::from_index(28).expect("Unable to get Square from index");
assert_eq!(sq1.rank, 3);
assert_eq!(sq1.file, 4);
}
#[test]
fn valid_neighbors() {
let sq = Square::from_index_unsafe(28);
assert_eq!(
sq.neighbor(Direction::North),
Some(Square::from_index_unsafe(36))
);
assert_eq!(
sq.neighbor(Direction::NorthEast),
Some(Square::from_index_unsafe(37))
);
assert_eq!(
sq.neighbor(Direction::East),
Some(Square::from_index_unsafe(29))
);
assert_eq!(
sq.neighbor(Direction::SouthEast),
Some(Square::from_index_unsafe(21))
);
assert_eq!(
sq.neighbor(Direction::South),
Some(Square::from_index_unsafe(20))
);
assert_eq!(
sq.neighbor(Direction::SouthWest),
Some(Square::from_index_unsafe(19))
);
assert_eq!(
sq.neighbor(Direction::West),
Some(Square::from_index_unsafe(27))
);
assert_eq!(
sq.neighbor(Direction::NorthWest),
Some(Square::from_index_unsafe(35))
);
}
#[test]
fn invalid_neighbors() {
let square0 = Square::from_index_unsafe(0);
assert!(square0.neighbor(Direction::West).is_none());
assert!(square0.neighbor(Direction::SouthWest).is_none());
assert!(square0.neighbor(Direction::South).is_none());
let square7 = Square::from_index_unsafe(7);
assert!(square7.neighbor(Direction::East).is_none());
assert!(square7.neighbor(Direction::SouthEast).is_none());
assert!(square7.neighbor(Direction::South).is_none());
let square56 = Square::from_index_unsafe(56);
assert!(square56.neighbor(Direction::North).is_none());
assert!(square56.neighbor(Direction::NorthWest).is_none());
assert!(square56.neighbor(Direction::West).is_none());
let square63 = Square::from_index_unsafe(63);
assert!(square63.neighbor(Direction::North).is_none());
assert!(square63.neighbor(Direction::NorthEast).is_none());
assert!(square63.neighbor(Direction::East).is_none());
}
}