[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:
parent
fb182e7ac0
commit
3951af76cb
8 changed files with 232 additions and 24 deletions
|
@ -12,3 +12,8 @@ pub use colors::Color;
|
|||
pub use coordinates::{Direction, File, Rank, Square, Wing};
|
||||
pub use pieces::Piece;
|
||||
pub use shapes::{Shape, Slider};
|
||||
|
||||
pub mod errors {
|
||||
pub use crate::coordinates::ParseSquareError;
|
||||
pub use crate::shapes::ParseShapeError;
|
||||
}
|
||||
|
|
|
@ -99,9 +99,8 @@ mod tests {
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn shape_try_from() {
|
||||
assert_eq!(Shape::try_from('p'), Ok(Shape::Pawn));
|
||||
assert_eq!(Shape::try_from("p"), Ok(Shape::Pawn));
|
||||
fn parse_shape() {
|
||||
assert_eq!("p".parse(), Ok(Shape::Pawn));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use std::{array, slice};
|
||||
use std::{array, fmt, slice, str::FromStr};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
|
@ -27,6 +27,8 @@ impl Shape {
|
|||
Shape::King,
|
||||
];
|
||||
|
||||
const PROMOTABLE_SHAPES: [Shape; 4] = [Shape::Queen, Shape::Rook, Shape::Bishop, Shape::Knight];
|
||||
|
||||
pub fn iter() -> slice::Iter<'static, Self> {
|
||||
Shape::ALL.iter()
|
||||
}
|
||||
|
@ -38,10 +40,7 @@ impl Shape {
|
|||
|
||||
/// An iterator over the shapes that a pawn can promote to
|
||||
pub fn promotable() -> slice::Iter<'static, Shape> {
|
||||
const PROMOTABLE_SHAPES: [Shape; 4] =
|
||||
[Shape::Queen, Shape::Rook, Shape::Bishop, Shape::Knight];
|
||||
|
||||
PROMOTABLE_SHAPES.iter()
|
||||
Self::PROMOTABLE_SHAPES.iter()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
|
@ -67,6 +66,11 @@ impl Shape {
|
|||
Shape::King => "king",
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_promotable(&self) -> bool {
|
||||
Self::PROMOTABLE_SHAPES.contains(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
|
@ -121,32 +125,24 @@ impl TryFrom<char> for Shape {
|
|||
|
||||
#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
|
||||
#[error("no matching piece shape for string")]
|
||||
pub struct ShapeFromStrError;
|
||||
pub struct ParseShapeError;
|
||||
|
||||
impl TryFrom<&str> for Shape {
|
||||
type Error = ShapeFromStrError;
|
||||
impl FromStr for Shape {
|
||||
type Err = ParseShapeError;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
match value.to_lowercase().as_str() {
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"p" | "pawn" => Ok(Shape::Pawn),
|
||||
"n" | "knight" => Ok(Shape::Knight),
|
||||
"b" | "bishop" => Ok(Shape::Bishop),
|
||||
"r" | "rook" => Ok(Shape::Rook),
|
||||
"q" | "queen" => Ok(Shape::Queen),
|
||||
"k" | "king" => Ok(Shape::King),
|
||||
_ => Err(ShapeFromStrError),
|
||||
_ => Err(ParseShapeError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for Shape {
|
||||
type Err = ShapeFromStrError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::try_from(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Shape> for char {
|
||||
fn from(shape: &Shape) -> char {
|
||||
char::from(*shape)
|
||||
|
@ -159,7 +155,7 @@ impl From<Shape> for char {
|
|||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Shape {
|
||||
impl fmt::Display for Shape {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let self_char: char = self.into();
|
||||
write!(f, "{self_char}")
|
||||
|
|
134
moves/src/algebraic.rs
Normal file
134
moves/src/algebraic.rs
Normal 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(())
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
pub mod algebraic;
|
||||
pub mod generators;
|
||||
pub mod testing;
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -11,6 +11,7 @@ use chessfriend_board::{
|
|||
};
|
||||
use chessfriend_core::{Color, Piece, Shape, Square};
|
||||
use chessfriend_moves::{
|
||||
algebraic::AlgebraicMoveComponents,
|
||||
generators::{
|
||||
AllPiecesMoveGenerator, BishopMoveGenerator, KingMoveGenerator, KnightMoveGenerator,
|
||||
PawnMoveGenerator, QueenMoveGenerator, RookMoveGenerator,
|
||||
|
@ -222,6 +223,68 @@ impl Position {
|
|||
|
||||
unmake_result
|
||||
}
|
||||
|
||||
/// Build a move given its origin, target, and possible promotion. Perform
|
||||
/// some minimal validation. If a move cannot be
|
||||
#[must_use]
|
||||
pub fn move_from_algebraic_components(
|
||||
&self,
|
||||
components: AlgebraicMoveComponents,
|
||||
) -> Option<Move> {
|
||||
match components {
|
||||
AlgebraicMoveComponents::Null => Some(Move::null()),
|
||||
AlgebraicMoveComponents::Regular {
|
||||
origin,
|
||||
target,
|
||||
promotion,
|
||||
} => self.move_from_origin_target(origin, target, promotion),
|
||||
}
|
||||
}
|
||||
|
||||
fn move_from_origin_target(
|
||||
&self,
|
||||
origin: Square,
|
||||
target: Square,
|
||||
promotion: Option<Shape>,
|
||||
) -> Option<Move> {
|
||||
let piece = self.get_piece(origin)?;
|
||||
|
||||
let color = piece.color;
|
||||
|
||||
// Pawn and King are the two most interesting shapes here, because of en
|
||||
// passant, castling and so on. So, let the move generators do their
|
||||
// thing and find the move that fits the parameters. For the rest of the
|
||||
// pieces, do something a little more streamlined.
|
||||
|
||||
match piece.shape {
|
||||
Shape::Pawn => PawnMoveGenerator::new(&self.board, None)
|
||||
.find(|ply| {
|
||||
ply.origin() == origin
|
||||
&& ply.target() == target
|
||||
&& ply.promotion_shape() == promotion
|
||||
})
|
||||
.map(std::convert::Into::into),
|
||||
Shape::King => KingMoveGenerator::new(&self.board, None)
|
||||
.find(|ply| ply.origin() == origin && ply.target() == target)
|
||||
.map(std::convert::Into::into),
|
||||
_ => {
|
||||
if color != self.board.active_color() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let target_bitboard: BitBoard = target.into();
|
||||
if !(self.movement(origin) & target_bitboard).is_populated() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.get_piece(target).is_some() {
|
||||
return Some(Move::capture(origin, target));
|
||||
}
|
||||
|
||||
Some(Move::quiet(origin, target))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Position {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue