#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct AlgebraicMove(String);
pub trait Move {
fn try_from_algebraic(algebraic: String) -> Result<Self, SANError>
where
Self: Sized;
fn from_algebraic(algebraic: String) -> Self
where
Self: Sized,
{
Self::try_from_algebraic(algebraic).unwrap()
}
fn to_algebraic(self) -> String;
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct SANError {
message: String,
}
impl SANError {
pub fn new(message: String) -> SANError {
SANError { message: message }
}
pub fn message(&self) -> &str {
&self.message
}
}
impl AlgebraicMove {
fn is_effect(symbol: char) -> bool {
symbol == '+' || symbol == '#'
}
fn is_coordinate(coordinate: &str) -> Result<(), SANError> {
let file = coordinate.chars().nth(0).unwrap();
let rank = coordinate.chars().nth(1).unwrap();
AlgebraicMove::is_file(file)?;
AlgebraicMove::is_rank(rank)?;
Ok(())
}
fn is_file(file: char) -> Result<(), SANError> {
if file < 'a' || file > 'h' {
Err(SANError::new(String::from(format!(
"Invalid file: {file}",
file = file
))))
} else {
Ok(())
}
}
fn is_rank(rank: char) -> Result<(), SANError> {
if rank < '1' || rank > '8' {
Err(SANError::new(String::from(format!(
"Invalid rank: {rank}",
rank = rank
))))
} else {
Ok(())
}
}
fn is_rank_or_file(rank: char) -> Result<(), SANError> {
let is_rank = AlgebraicMove::is_rank(rank).is_ok();
let is_file = AlgebraicMove::is_file(rank).is_ok();
if !is_rank && !is_file {
Err(SANError::new(String::from(format!(
"Invalid rank/file: {rank}",
rank = rank
))))
} else {
Ok(())
}
}
fn is_piece(piece: char) -> Result<(), SANError> {
if !"NBRQK".contains(piece) {
Err(SANError::new(String::from(format!(
"Invalid piece: {piece}",
piece = piece
))))
} else {
Ok(())
}
}
fn is_piece_or_file(piece: char) -> Result<(), SANError> {
let is_piece = AlgebraicMove::is_piece(piece).is_ok();
let is_file = AlgebraicMove::is_file(piece).is_ok();
if !is_piece && !is_file {
Err(SANError::new(String::from(format!(
"Invalid piece/file: {piece}",
piece = piece
))))
} else {
Ok(())
}
}
fn is_takes(takes: char) -> Result<(), SANError> {
if takes != 'x' {
Err(SANError::new(String::from("Invalid takes symbol: x")))
} else {
Ok(())
}
}
fn is_specified_piece(piece: char) -> Result<(), SANError> {
AlgebraicMove::is_piece(piece)?;
if piece == 'K' {
Err(SANError::new(String::from("Invalid piece: K")))
} else {
Ok(())
}
}
fn is_promotion(symbol: char) -> Result<(), SANError> {
if symbol != '=' {
Err(SANError::new(String::from(format!(
"Invalid promotion symbol: {symbol}",
symbol = symbol
))))
} else {
Ok(())
}
}
}
impl Move for AlgebraicMove {
fn try_from_algebraic(algebraic: String) -> Result<AlgebraicMove, SANError> {
let mut test_algebraic = algebraic.clone();
if let Some(last_char) = test_algebraic.chars().last() {
if AlgebraicMove::is_effect(last_char) {
test_algebraic.pop();
}
let move_length = test_algebraic.len();
if move_length == 0 || move_length == 1 {
return Err(SANError::new(String::from(format!(
"Move is too short: {algebraic}",
algebraic = algebraic
))));
} else if move_length == 2 {
AlgebraicMove::is_coordinate(&test_algebraic)?;
} else if move_length == 3 {
if &test_algebraic != "O-O" {
AlgebraicMove::is_piece(test_algebraic.remove(0))?;
AlgebraicMove::is_coordinate(&test_algebraic)?;
}
} else if move_length == 4 {
if test_algebraic.chars().nth(1).unwrap() == 'x' {
test_algebraic.remove(1);
AlgebraicMove::is_piece_or_file(test_algebraic.remove(0))?;
AlgebraicMove::is_coordinate(&test_algebraic)?;
} else if test_algebraic.chars().nth(2).unwrap() == '=' {
AlgebraicMove::is_piece(test_algebraic.remove(3))?;
AlgebraicMove::is_promotion(test_algebraic.remove(2))?;
AlgebraicMove::is_coordinate(&test_algebraic)?;
} else {
AlgebraicMove::is_rank_or_file(test_algebraic.remove(1))?;
AlgebraicMove::is_specified_piece(test_algebraic.remove(0))?;
AlgebraicMove::is_coordinate(&test_algebraic)?;
}
} else if move_length == 5 {
if &test_algebraic != "O-O-O" {
AlgebraicMove::is_takes(test_algebraic.remove(2))?;
AlgebraicMove::is_rank_or_file(test_algebraic.remove(1))?;
AlgebraicMove::is_specified_piece(test_algebraic.remove(0))?;
AlgebraicMove::is_coordinate(&test_algebraic)?;
}
} else if move_length == 6 {
AlgebraicMove::is_piece(test_algebraic.remove(5))?;
AlgebraicMove::is_promotion(test_algebraic.remove(4))?;
AlgebraicMove::is_takes(test_algebraic.remove(1))?;
AlgebraicMove::is_file(test_algebraic.remove(0))?;
AlgebraicMove::is_coordinate(&test_algebraic)?;
} else {
return Err(SANError::new(String::from(format!(
"Move is too long: {test_algebraic}",
test_algebraic = test_algebraic
))));
}
} else {
return Err(SANError::new(String::from("Empty string")));
}
Ok(AlgebraicMove(String::from(algebraic)))
}
fn to_algebraic(self) -> String {
self.0
}
}
#[cfg(test)]
mod tests {
use rstest::*;
use super::{AlgebraicMove, Move};
#[rstest(
san_move,
case("e4"),
case("Be3"),
case("Ndf6"),
case("Kxe2"),
case("bxc4"),
case("Raxd8"),
case("e8=Q"),
case("bxc8=R"),
case("O-O"),
case("O-O-O"),
case("c4+"),
case("Qe7#"),
case("O-O-O#"),
case("axb8=N+")
)]
fn algebraic_move_should_accept_valid_san(san_move: &str) {
let _algebraic_move = AlgebraicMove::from_algebraic(String::from(san_move));
}
#[rstest(
invalid_san_move,
expected_message,
case("D4", "Invalid file: D"),
case("Pd4", "Invalid piece: P"),
case("Lxd4", "Invalid piece/file: L"),
case("5", "Move is too short: 5"),
case("#", "Move is too short: #"),
case("Raaaxd8+", "Move is too long: Raaaxd8"),
case("Rd9#", "Invalid rank: 9"),
case("Kab2", "Invalid piece: K"),
case("N9b7", "Invalid rank/file: 9"),
case("d1=Z", "Invalid piece: Z"),
case("b8=a", "Invalid piece: a"),
case("d8/", "Invalid piece: d"),
case("Nxa8=B", "Invalid file: N"),
case("", "Empty string")
)]
fn algebraic_move_should_reject_invalid_san(invalid_san_move: &str, expected_message: &str) {
let bad_algebraic_move = AlgebraicMove::try_from_algebraic(String::from(invalid_san_move));
let san_error = bad_algebraic_move.expect_err("Invalid SAN accepted");
assert_eq!(san_error.message(), expected_message);
}
#[rstest(san_move, case("e4"), case("bxc8#"))]
fn algebraic_move_should_convert_to_valid_san(san_move: &str) {
let algebraic_move = AlgebraicMove::from_algebraic(String::from(san_move));
assert_eq!(&algebraic_move.to_algebraic(), san_move);
}
}