[board] Copy fen.rs here from the position crate

This commit is contained in:
Eryn Wells 2024-07-13 08:08:26 -07:00
parent 3a2ead2668
commit d9c2cfb90c

330
board/src/fen.rs Normal file
View file

@ -0,0 +1,330 @@
// Eryn Wells <eryn@erynwells.me>
use crate::{Board, Builder, Castle, EnPassant};
use chessfriend_core::{
coordinates::ParseSquareError, piece, Color, File, Piece, PlacedPiece, Rank, Square,
};
use std::fmt::Write;
#[macro_export]
macro_rules! fen {
($fen_string:literal) => {
Board::from_fen_str($fen_string)
};
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ToFenStrError {
FmtError(std::fmt::Error),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum FromFenStrError {
MissingField(Field),
MissingPlacement,
InvalidValue,
ParseIntError(std::num::ParseIntError),
ParseSquareError(ParseSquareError),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Field {
Placements,
PlayerToMove,
CastlingRights,
EnPassantSquare,
HalfMoveClock,
FullMoveCounter,
}
pub trait ToFenStr {
type Error;
/// Create a FEN string from `Self`.
///
/// # Errors
///
///
fn to_fen_str(&self) -> Result<String, Self::Error>;
}
pub trait FromFenStr: Sized {
type Error;
/// Create a `Self` from a FEN string.
///
/// # Errors
///
///
fn from_fen_str(string: &str) -> Result<Self, Self::Error>;
}
impl ToFenStr for Board {
type Error = ToFenStrError;
fn to_fen_str(&self) -> Result<String, Self::Error> {
let mut fen_string = String::new();
let mut empty_squares: u8 = 0;
for rank in Rank::ALL.into_iter().rev() {
for file in File::ALL {
let square = Square::from_file_rank(file, rank);
match self.piece_on_square(square) {
Some(piece) => {
if empty_squares > 0 {
write!(fen_string, "{empty_squares}")
.map_err(ToFenStrError::FmtError)?;
empty_squares = 0;
}
write!(fen_string, "{}", piece.to_fen_str()?)
.map_err(ToFenStrError::FmtError)?;
}
None => empty_squares += 1,
}
}
if empty_squares > 0 {
write!(fen_string, "{empty_squares}").map_err(ToFenStrError::FmtError)?;
empty_squares = 0;
}
if rank != Rank::ONE {
write!(fen_string, "/").map_err(ToFenStrError::FmtError)?;
}
}
write!(fen_string, " {}", self.player_to_move().to_fen_str()?)
.map_err(ToFenStrError::FmtError)?;
let castling = [
(Color::White, Castle::KingSide),
(Color::White, Castle::QueenSide),
(Color::Black, Castle::KingSide),
(Color::Black, Castle::QueenSide),
]
.map(|(color, castle)| {
let can_castle = self.player_has_right_to_castle(color, castle);
if !can_castle {
return "";
}
match (color, castle) {
(Color::White, Castle::KingSide) => "K",
(Color::White, Castle::QueenSide) => "Q",
(Color::Black, Castle::KingSide) => "k",
(Color::Black, Castle::QueenSide) => "q",
}
})
.concat();
write!(
fen_string,
" {}",
if castling.is_empty() { "-" } else { &castling }
)
.map_err(ToFenStrError::FmtError)?;
write!(
fen_string,
" {}",
self.en_passant()
.map_or("-".to_string(), |ep| ep.target_square().to_string())
)
.map_err(ToFenStrError::FmtError)?;
write!(fen_string, " {}", self.ply_counter()).map_err(ToFenStrError::FmtError)?;
write!(fen_string, " {}", self.move_number()).map_err(ToFenStrError::FmtError)?;
Ok(fen_string)
}
}
impl ToFenStr for Color {
type Error = ToFenStrError;
fn to_fen_str(&self) -> Result<String, Self::Error> {
match self {
Color::White => Ok("w".to_string()),
Color::Black => Ok("b".to_string()),
}
}
}
impl ToFenStr for Piece {
type Error = ToFenStrError;
fn to_fen_str(&self) -> Result<String, Self::Error> {
let ascii: char = self.to_ascii();
Ok(String::from(match self.color() {
Color::White => ascii.to_ascii_uppercase(),
Color::Black => ascii.to_ascii_lowercase(),
}))
}
}
impl ToFenStr for PlacedPiece {
type Error = ToFenStrError;
fn to_fen_str(&self) -> Result<String, Self::Error> {
self.piece().to_fen_str()
}
}
impl FromFenStr for Board {
type Error = FromFenStrError;
fn from_fen_str(string: &str) -> Result<Self, Self::Error> {
let mut builder = Builder::default();
let mut fields = string.split(' ');
let placements = fields
.next()
.ok_or(FromFenStrError::MissingField(Field::Placements))?;
let ranks = placements.split('/');
for (rank, pieces) in Rank::ALL.iter().rev().zip(ranks) {
let mut files = File::ALL.iter();
for ch in pieces.chars() {
if let Some(skip) = ch.to_digit(10) {
// TODO: Use advance_by() when it's available.
for _ in 0..skip {
files.next();
}
continue;
}
let file = files.next().ok_or(FromFenStrError::MissingPlacement)?;
let piece = Piece::from_fen_str(&ch.to_string())?;
builder.place_piece(PlacedPiece::new(
piece,
Square::from_file_rank(*file, *rank),
));
}
debug_assert_eq!(files.next(), None);
}
let player_to_move = Color::from_fen_str(
fields
.next()
.ok_or(FromFenStrError::MissingField(Field::PlayerToMove))?,
)?;
builder.to_move(player_to_move);
let castling_rights = fields
.next()
.ok_or(FromFenStrError::MissingField(Field::CastlingRights))?;
if castling_rights == "-" {
builder.no_castling_rights();
} else {
for ch in castling_rights.chars() {
match ch {
'K' => builder.player_can_castle(Color::White, Castle::KingSide),
'Q' => builder.player_can_castle(Color::White, Castle::QueenSide),
'k' => builder.player_can_castle(Color::Black, Castle::KingSide),
'q' => builder.player_can_castle(Color::Black, Castle::QueenSide),
_ => return Err(FromFenStrError::InvalidValue),
};
}
}
let en_passant_square = fields
.next()
.ok_or(FromFenStrError::MissingField(Field::EnPassantSquare))?;
if en_passant_square != "-" {
let square = Square::from_algebraic_str(en_passant_square)
.map_err(FromFenStrError::ParseSquareError)?;
builder.en_passant(Some(EnPassant::from_target_square(square).unwrap()));
}
let half_move_clock = fields
.next()
.ok_or(FromFenStrError::MissingField(Field::HalfMoveClock))?;
let half_move_clock: u16 = half_move_clock
.parse()
.map_err(FromFenStrError::ParseIntError)?;
builder.ply_counter(half_move_clock);
let full_move_counter = fields
.next()
.ok_or(FromFenStrError::MissingField(Field::FullMoveCounter))?;
let full_move_counter: u16 = full_move_counter
.parse()
.map_err(FromFenStrError::ParseIntError)?;
builder.move_number(full_move_counter);
debug_assert_eq!(fields.next(), None);
Ok(builder.build())
}
}
impl FromFenStr for Color {
type Error = FromFenStrError;
fn from_fen_str(string: &str) -> Result<Self, Self::Error> {
if string.len() != 1 {
return Err(FromFenStrError::InvalidValue);
}
match string.chars().take(1).next().unwrap() {
'w' => Ok(Color::White),
'b' => Ok(Color::Black),
_ => Err(FromFenStrError::InvalidValue),
}
}
}
impl FromFenStr for Piece {
type Error = FromFenStrError;
fn from_fen_str(string: &str) -> Result<Self, Self::Error> {
if string.len() != 1 {
return Err(FromFenStrError::InvalidValue);
}
match string.chars().take(1).next().unwrap() {
'P' => Ok(piece!(White Pawn)),
'N' => Ok(piece!(White Knight)),
'B' => Ok(piece!(White Bishop)),
'R' => Ok(piece!(White Rook)),
'Q' => Ok(piece!(White Queen)),
'K' => Ok(piece!(White King)),
'p' => Ok(piece!(Black Pawn)),
'n' => Ok(piece!(Black Knight)),
'b' => Ok(piece!(Black Bishop)),
'r' => Ok(piece!(Black Rook)),
'q' => Ok(piece!(Black Queen)),
'k' => Ok(piece!(Black King)),
_ => Err(FromFenStrError::InvalidValue),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_board;
#[test]
fn starting_position() {
let pos = test_board!(starting);
assert_eq!(
pos.to_fen_str(),
Ok(String::from(
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
))
);
}
#[test]
fn from_starting_fen() {
let board = fen!("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1").unwrap();
let expected = Board::starting();
assert_eq!(board, expected, "{board:#?}\n{expected:#?}");
}
}