[board] Copy fen.rs here from the position crate
This commit is contained in:
		
							parent
							
								
									3a2ead2668
								
							
						
					
					
						commit
						d9c2cfb90c
					
				
					 1 changed files with 330 additions and 0 deletions
				
			
		
							
								
								
									
										330
									
								
								board/src/fen.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										330
									
								
								board/src/fen.rs
									
										
									
									
									
										Normal 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:#?}");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue