From 8db3a236c0245f6718729ac4e3f50f48230ba836 Mon Sep 17 00:00:00 2001 From: dogeystamp Date: Sun, 29 Sep 2024 12:32:11 -0400 Subject: [PATCH] feat: FEN printing --- src/fen.rs | 161 +++++++++++++++++++++++++++++++++++++++++------------ src/lib.rs | 69 ++++++++++++++++++++++- 2 files changed, 190 insertions(+), 40 deletions(-) diff --git a/src/fen.rs b/src/fen.rs index 436aff8..0198771 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -1,5 +1,4 @@ -use crate::{BoardState, Square, Color, ColPiece}; -use crate::{BOARD_WIDTH, BOARD_HEIGHT}; +use crate::{BoardState, CastleRights, ColPiece, Color, Square, BOARD_HEIGHT, BOARD_WIDTH}; pub trait FromFen { type Error; @@ -8,11 +7,15 @@ pub trait FromFen { Self: std::marker::Sized; } +pub trait ToFen { + fn to_fen(self) -> String; +} + /// FEN parsing error, with index of issue if applicable. #[derive(Debug)] pub enum FenError { /// Invalid character. - BadChar(usize), + BadChar(usize, char), /// FEN is too short, and missing information. MissingFields, /// Too many pieces on a single row. @@ -35,7 +38,7 @@ impl FromFen for BoardState { //! Parse FEN string into position. /// Parser state machine. - #[derive(Clone, Copy)] + #[derive(Debug, Clone, Copy)] enum FenState { /// Parses space characters between arguments, and jumps to next state. Space, @@ -62,8 +65,8 @@ impl FromFen for BoardState { /// Create parse error at a given index macro_rules! bad_char { - ($idx:ident) => { - Err(FenError::BadChar($idx)) + ($idx:ident, $char:ident) => { + Err(FenError::BadChar($idx, $char)) }; } @@ -82,7 +85,7 @@ impl FromFen for BoardState { ' ' => { parser_state = next_state; } - _ => return bad_char!(i), + _ => return bad_char!(i, c), }; } @@ -101,8 +104,8 @@ impl FromFen for BoardState { row += 1; parser_state = FenState::Piece(row, col) } - pc_char @ ('b'..='r' | 'B'..='R') => { - let pc = ColPiece::try_from(pc_char).or(bad_char!(i))?; + pc_char @ ('a'..='z' | 'A'..='Z') => { + let pc = ColPiece::try_from(pc_char).or(bad_char!(i, c))?; pos.set_piece( Square::from_row_col(real_row, col) @@ -123,7 +126,7 @@ impl FromFen for BoardState { }; parser_state = FenState::Piece(row, col); } else { - return bad_char!(i); + return bad_char!(i, c); } } ' ' => { @@ -134,26 +137,26 @@ impl FromFen for BoardState { } parser_state = FenState::Side } - _ => return bad_char!(i), + _ => return bad_char!(i, c), }; } FenState::Side => { match c { 'w' => pos.turn = Color::White, 'b' => pos.turn = Color::Black, - _ => return bad_char!(i), + _ => return bad_char!(i, c), } parse_space_and_goto!(FenState::Castle); } FenState::Castle => { macro_rules! wc { () => { - pos.castle[Color::White as usize] + pos.castle.0[Color::White as usize] }; } macro_rules! bc { () => { - pos.castle[Color::Black as usize] + pos.castle.0[Color::Black as usize] }; } match c { @@ -165,7 +168,7 @@ impl FromFen for BoardState { '-' => { parse_space_and_goto!(FenState::EnPassantRank); } - _ => return bad_char!(i), + _ => return bad_char!(i, c), } } FenState::EnPassantRank => { @@ -177,7 +180,7 @@ impl FromFen for BoardState { pos.ep_square = Some(Square(c as usize - 'a' as usize)); parser_state = FenState::EnPassantFile; } - _ => return bad_char!(i), + _ => return bad_char!(i, c), }; } FenState::EnPassantFile => { @@ -187,7 +190,7 @@ impl FromFen for BoardState { + (digit as usize - 1) * 8, )); } else { - return bad_char!(i); + return bad_char!(i, c); } parse_space_and_goto!(FenState::HalfMove); } @@ -201,7 +204,7 @@ impl FromFen for BoardState { } else if c == ' ' { parser_state = FenState::FullMove; } else { - return bad_char!(i); + return bad_char!(i, c); } } FenState::FullMove => { @@ -212,7 +215,7 @@ impl FromFen for BoardState { pos.full_moves *= 10; pos.full_moves += digit as usize; } else { - return bad_char!(i); + return bad_char!(i, c); } } } @@ -228,11 +231,55 @@ impl FromFen for BoardState { } } +impl ToFen for BoardState { + fn to_fen(self) -> String { + let pieces_str = (0..BOARD_HEIGHT) + .rev() + .map(|row| { + let mut row_str = String::with_capacity(8); + let mut empty_counter = 0; + macro_rules! compact_empty { + () => { + if empty_counter > 0 { + row_str.push_str(&empty_counter.to_string()); + } + }; + } + + for col in 0..BOARD_WIDTH { + let idx = Square::from_row_col(row, col).unwrap(); + if let Some(pc) = self.get_piece(idx) { + compact_empty!(); + empty_counter = 0; + row_str.push(pc.into()) + } else { + empty_counter += 1; + } + } + compact_empty!(); + row_str + }) + .collect::>() + .join("/"); + + let turn = char::from(self.turn); + let castle = self.castle.to_string(); + let ep_square = match self.ep_square { + Some(sqr) => sqr.to_algebraic(), + None => "-".to_string(), + }; + let half_move = self.half_moves.to_string(); + let full_move = self.full_moves.to_string(); + + format!("{pieces_str} {turn} {castle} {ep_square} {half_move} {full_move}") + } +} + #[cfg(test)] mod tests { use super::*; + use crate::CastlePlayer; use crate::N_SQUARES; - use crate::CastlingRights; #[test] fn test_fen_pieces() { @@ -250,10 +297,10 @@ mod tests { assert_eq!(board.turn, Color::Black); } - macro_rules! make_board{ + macro_rules! make_board { ($fen_fmt: expr) => { BoardState::from_fen(format!($fen_fmt)).unwrap() - } + }; } #[test] @@ -283,56 +330,57 @@ mod tests { ( "-", [ - CastlingRights { k: false, q: false }, - CastlingRights { k: false, q: false }, + CastlePlayer { k: false, q: false }, + CastlePlayer { k: false, q: false }, ], ), ( "k", [ - CastlingRights { k: false, q: false }, - CastlingRights { k: true, q: false }, + CastlePlayer { k: false, q: false }, + CastlePlayer { k: true, q: false }, ], ), ( "kq", [ - CastlingRights { k: false, q: false }, - CastlingRights { k: true, q: true }, + CastlePlayer { k: false, q: false }, + CastlePlayer { k: true, q: true }, ], ), + // This is the wrong order, but parsers should be lenient ( "qk", [ - CastlingRights { k: false, q: false }, - CastlingRights { k: true, q: true }, + CastlePlayer { k: false, q: false }, + CastlePlayer { k: true, q: true }, ], ), ( "KQkq", [ - CastlingRights { k: true, q: true }, - CastlingRights { k: true, q: true }, + CastlePlayer { k: true, q: true }, + CastlePlayer { k: true, q: true }, ], ), ( "KQ", [ - CastlingRights { k: true, q: true }, - CastlingRights { k: false, q: false }, + CastlePlayer { k: true, q: true }, + CastlePlayer { k: false, q: false }, ], ), ( "QK", [ - CastlingRights { k: true, q: true }, - CastlingRights { k: false, q: false }, + CastlePlayer { k: true, q: true }, + CastlePlayer { k: false, q: false }, ], ), ]; for (castle_str, castle) in test_cases { let board = make_board!("8/8/8/8/8/8/8/8 w {castle_str} - 0 0"); - assert_eq!(board.castle, castle); + assert_eq!(board.castle, CastleRights(castle)); } } @@ -353,4 +401,43 @@ mod tests { assert_eq!(board.full_moves, i); } } + + #[test] + fn test_fen_printing() { + //! Test that FENs printed are equivalent to the original. + + // FENs sourced from https://gist.github.com/peterellisjones/8c46c28141c162d1d8a0f0badbc9cff9 + let test_cases = [ + "8/8/8/2k5/2pP4/8/B7/4K3 b - d3 0 3", + "r6r/1b2k1bq/8/8/7B/8/8/R3K2R b KQ - 3 2", + "r1bqkbnr/pppppppp/n7/8/8/P7/1PPPPPPP/RNBQKBNR w KQkq - 2 2", + "r3k2r/p1pp1pb1/bn2Qnp1/2qPN3/1p2P3/2N5/PPPBBPPP/R3K2R b KQkq - 3 2", + "2kr3r/p1ppqpb1/bn2Qnp1/3PN3/1p2P3/2N5/PPPBBPPP/R3K2R b KQ - 3 2", + "rnb2k1r/pp1Pbppp/2p5/q7/2B5/8/PPPQNnPP/RNB1K2R w KQ - 3 9", + "2r5/3pk3/8/2P5/8/2K5/8/8 w - - 5 4", + "rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8", + "r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10", + "3k4/3p4/8/K1P4r/8/8/8/8 b - - 0 1", + "8/8/4k3/8/2p5/8/B2P2K1/8 w - - 0 1", + "8/8/1k6/2b5/2pP4/8/5K2/8 b - d3 0 1", + "5k2/8/8/8/8/8/8/4K2R w K - 0 1", + "3k4/8/8/8/8/8/8/R3K3 w Q - 0 1", + "r3k2r/1b4bq/8/8/8/8/7B/R3K2R w KQkq - 0 1", + "r3k2r/8/3Q4/8/8/5q2/8/R3K2R b KQkq - 0 1", + "2K2r2/4P3/8/8/8/8/8/3k4 w - - 0 1", + "8/8/1P2K3/8/2n5/1q6/8/5k2 b - - 0 1", + "4k3/1P6/8/8/8/8/K7/8 w - - 0 1", + "8/P1k5/K7/8/8/8/8/8 w - - 0 1", + "K1k5/8/P7/8/8/8/8/8 w - - 0 1", + "8/k1P5/8/1K6/8/8/8/8 w - - 0 1", + "8/8/2k5/5q2/5n2/8/5K2/8 b - - 0 1", + ]; + + for (i, fen1) in test_cases.iter().enumerate() { + println!("fen1: {fen1:?}"); + let fen2 = BoardState::from_fen(fen1.to_string()).unwrap().to_fen(); + + assert_eq!(fen1.to_string(), fen2, "FEN not equivalent") + } + } } diff --git a/src/lib.rs b/src/lib.rs index 2fc50aa..7457a07 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,6 @@ pub mod fen; pub mod movegen; -use std::rc::Rc; const BOARD_WIDTH: usize = 8; const BOARD_HEIGHT: usize = 8; @@ -26,6 +25,15 @@ impl Color { } } +impl From for char { + fn from(value: Color) -> Self { + match value { + Color::White => 'w', + Color::Black => 'b', + } + } +} + #[derive(Debug, Copy, Clone)] enum Piece { Rook, @@ -102,6 +110,7 @@ impl ColPiece { #[derive(Debug, Clone, Copy, PartialEq, Eq)] struct Square(usize); +#[derive(Debug)] enum IndexError { OutOfBounds, } @@ -128,6 +137,35 @@ impl Square { let ret = BOARD_WIDTH * r + c; ret.try_into() } + fn to_row_col(self) -> (usize, usize) { + //! Get row, column from index + let div = self.0 / BOARD_WIDTH; + let rem = self.0 % BOARD_WIDTH; + assert!(div <= 7); + assert!(rem <= 7); + (div, rem) + } + + /// Convert square to typical human-readable form (e.g. `e4`). + fn to_algebraic(self) -> String { + let letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']; + let (row, col) = self.to_row_col(); + let rank = (row + 1).to_string(); + let file = letters[col]; + format!("{file}{rank}") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_to_algebraic() { + for (sqr, idx) in [("a1", 0), ("a8", 56), ("h1", 7), ("h8", 63)] { + assert_eq!(Square::try_from(idx).unwrap().to_algebraic(), sqr) + } + } } impl TryFrom for Piece { @@ -216,13 +254,38 @@ impl Player { /// Castling rights for one player #[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] -pub struct CastlingRights { +pub struct CastlePlayer { /// Kingside k: bool, /// Queenside q: bool, } +/// Castling rights for both players +#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] +pub struct CastleRights([CastlePlayer; N_COLORS]); + +impl ToString for CastleRights { + /// Convert to FEN castling rights format. + fn to_string(&self) -> String { + let mut ret = String::with_capacity(4); + for (val, ch) in [ + (self.0[Color::White as usize].k, 'K'), + (self.0[Color::White as usize].q, 'Q'), + (self.0[Color::Black as usize].k, 'k'), + (self.0[Color::Black as usize].q, 'q'), + ] { + if val { + ret.push(ch) + } + } + if ret.is_empty() { + ret.push('-') + } + ret + } +} + /// Immutable game state, unique to a position. /// /// Default is empty. @@ -240,7 +303,7 @@ pub struct BoardState { ep_square: Option, /// Castling rights - castle: [CastlingRights; N_COLORS], + castle: CastleRights, /// Plies since last irreversible (capture, pawn) move half_moves: usize,