[core, moves, position] Implement parsing long algebraic moves

UCI uses a move format it calls "long algebraic". They look like either "e2e4"
for a regular move, or "h7h8q" for a promotion. Implement parsing these move
strings as a two step process. First define an AlgebraicMoveComponents struct
in the moves crate that implements FromStr. This struct reads out an origin
square, a target square, and an optional promotion shape from a string. Then,
implement a pair of methods on Position that take the move components struct
and return a fully encoded Move struct with them.

This process is required because the algebraic string is not enough by itself to
know what kind of move was made. The current position is required to understand
that.

Implement Shape::is_promotable().

Add a NULL move to the Move struct. I'm not sure what this is used for yet, but
the UCI spec specifically calls out a string that encodes a null move, so I added
it. It may end up being unused!

Do a little bit of cleanup in the core crate as well. Use deeper imports (import
std::fmt instead of requring the fully qualified type path) and remove some
unnecessary From implementations.

This commit is also the first instance (I think) of defining an errors module
in lib.rs for the core crate that holds the various error types the crate exports.
This commit is contained in:
Eryn Wells 2025-06-16 08:57:48 -07:00
parent fb182e7ac0
commit 3951af76cb
8 changed files with 232 additions and 24 deletions

134
moves/src/algebraic.rs Normal file
View file

@ -0,0 +1,134 @@
// Eryn Wells <eryn@erynwells.me>
use chessfriend_core::{
errors::{ParseShapeError, ParseSquareError},
Shape, Square,
};
use std::str::FromStr;
use thiserror::Error;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum AlgebraicMoveComponents {
/// An empty move
Null,
Regular {
origin: Square,
target: Square,
promotion: Option<Shape>,
},
}
#[derive(Clone, Debug, Error)]
pub enum ParseAlgebraicMoveComponentsError {
#[error("string not long enough to contain valid move")]
InsufficientLength,
#[error("{0}")]
ParseSquareError(#[from] ParseSquareError),
#[error("{0}")]
ParseShapeError(#[from] ParseShapeError),
#[error("{0} is not a promotable shape")]
InvalidPromotionShape(Shape),
}
impl FromStr for AlgebraicMoveComponents {
type Err = ParseAlgebraicMoveComponentsError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
if s == "0000" {
return Ok(Self::Null);
}
if s.len() < 4 {
return Err(ParseAlgebraicMoveComponentsError::InsufficientLength);
}
let s = s.to_lowercase();
let (origin_string, s) = s.split_at(2);
let s = s.trim_start();
let (target_string, s) = s.split_at(2);
let promotion = if s.len() >= 1 {
let s = s.trim_start();
let (promotion_string, _s) = s.split_at(1);
Some(promotion_string)
} else {
None
};
let origin: Square = origin_string.parse()?;
let target: Square = target_string.parse()?;
let promotion = if let Some(promotion) = promotion {
let promotion: Shape = promotion.parse()?;
if promotion.is_promotable() {
Some(promotion)
} else {
return Err(ParseAlgebraicMoveComponentsError::InvalidPromotionShape(
promotion,
));
}
} else {
None
};
Ok(AlgebraicMoveComponents::Regular {
origin,
target,
promotion,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_null_move() -> Result<(), ParseAlgebraicMoveComponentsError> {
let components: AlgebraicMoveComponents = "0000".parse()?;
assert_eq!(components, AlgebraicMoveComponents::Null);
Ok(())
}
#[test]
fn parse_origin_target_move() -> Result<(), ParseAlgebraicMoveComponentsError> {
let components: AlgebraicMoveComponents = "e2e4".parse()?;
assert_eq!(
components,
AlgebraicMoveComponents::Regular {
origin: Square::E2,
target: Square::E4,
promotion: None
}
);
Ok(())
}
#[test]
fn parse_origin_target_promotion_move() -> Result<(), ParseAlgebraicMoveComponentsError> {
let components: AlgebraicMoveComponents = "h7h8q".parse()?;
assert_eq!(
components,
AlgebraicMoveComponents::Regular {
origin: Square::H7,
target: Square::H8,
promotion: Some(Shape::Queen),
}
);
Ok(())
}
}

View file

@ -13,7 +13,7 @@ pub use pawn::PawnMoveGenerator;
pub use slider::{BishopMoveGenerator, QueenMoveGenerator, RookMoveGenerator};
use crate::Move;
use chessfriend_core::Square;
use chessfriend_core::{Shape, Square};
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct GeneratedMove {
@ -31,6 +31,11 @@ impl GeneratedMove {
self.ply.target_square()
}
#[must_use]
pub fn promotion_shape(&self) -> Option<Shape> {
self.ply.promotion_shape()
}
#[must_use]
pub fn ply(&self) -> Move {
self.ply

View file

@ -1,5 +1,6 @@
// Eryn Wells <eryn@erynwells.me>
pub mod algebraic;
pub mod generators;
pub mod testing;

View file

@ -78,6 +78,11 @@ fn target_bits(square: Square) -> u16 {
}
impl Move {
#[must_use]
pub const fn null() -> Self {
Move(0)
}
#[must_use]
pub fn quiet(origin: Square, target: Square) -> Self {
Move(origin_bits(origin) | target_bits(target))