feat: FEN printing

This commit is contained in:
dogeystamp 2024-09-29 12:32:11 -04:00
parent 77838fd417
commit 8db3a236c0
2 changed files with 190 additions and 40 deletions

View File

@ -1,5 +1,4 @@
use crate::{BoardState, Square, Color, ColPiece}; use crate::{BoardState, CastleRights, ColPiece, Color, Square, BOARD_HEIGHT, BOARD_WIDTH};
use crate::{BOARD_WIDTH, BOARD_HEIGHT};
pub trait FromFen { pub trait FromFen {
type Error; type Error;
@ -8,11 +7,15 @@ pub trait FromFen {
Self: std::marker::Sized; Self: std::marker::Sized;
} }
pub trait ToFen {
fn to_fen(self) -> String;
}
/// FEN parsing error, with index of issue if applicable. /// FEN parsing error, with index of issue if applicable.
#[derive(Debug)] #[derive(Debug)]
pub enum FenError { pub enum FenError {
/// Invalid character. /// Invalid character.
BadChar(usize), BadChar(usize, char),
/// FEN is too short, and missing information. /// FEN is too short, and missing information.
MissingFields, MissingFields,
/// Too many pieces on a single row. /// Too many pieces on a single row.
@ -35,7 +38,7 @@ impl FromFen for BoardState {
//! Parse FEN string into position. //! Parse FEN string into position.
/// Parser state machine. /// Parser state machine.
#[derive(Clone, Copy)] #[derive(Debug, Clone, Copy)]
enum FenState { enum FenState {
/// Parses space characters between arguments, and jumps to next state. /// Parses space characters between arguments, and jumps to next state.
Space, Space,
@ -62,8 +65,8 @@ impl FromFen for BoardState {
/// Create parse error at a given index /// Create parse error at a given index
macro_rules! bad_char { macro_rules! bad_char {
($idx:ident) => { ($idx:ident, $char:ident) => {
Err(FenError::BadChar($idx)) Err(FenError::BadChar($idx, $char))
}; };
} }
@ -82,7 +85,7 @@ impl FromFen for BoardState {
' ' => { ' ' => {
parser_state = next_state; parser_state = next_state;
} }
_ => return bad_char!(i), _ => return bad_char!(i, c),
}; };
} }
@ -101,8 +104,8 @@ impl FromFen for BoardState {
row += 1; row += 1;
parser_state = FenState::Piece(row, col) parser_state = FenState::Piece(row, col)
} }
pc_char @ ('b'..='r' | 'B'..='R') => { pc_char @ ('a'..='z' | 'A'..='Z') => {
let pc = ColPiece::try_from(pc_char).or(bad_char!(i))?; let pc = ColPiece::try_from(pc_char).or(bad_char!(i, c))?;
pos.set_piece( pos.set_piece(
Square::from_row_col(real_row, col) Square::from_row_col(real_row, col)
@ -123,7 +126,7 @@ impl FromFen for BoardState {
}; };
parser_state = FenState::Piece(row, col); parser_state = FenState::Piece(row, col);
} else { } else {
return bad_char!(i); return bad_char!(i, c);
} }
} }
' ' => { ' ' => {
@ -134,26 +137,26 @@ impl FromFen for BoardState {
} }
parser_state = FenState::Side parser_state = FenState::Side
} }
_ => return bad_char!(i), _ => return bad_char!(i, c),
}; };
} }
FenState::Side => { FenState::Side => {
match c { match c {
'w' => pos.turn = Color::White, 'w' => pos.turn = Color::White,
'b' => pos.turn = Color::Black, 'b' => pos.turn = Color::Black,
_ => return bad_char!(i), _ => return bad_char!(i, c),
} }
parse_space_and_goto!(FenState::Castle); parse_space_and_goto!(FenState::Castle);
} }
FenState::Castle => { FenState::Castle => {
macro_rules! wc { macro_rules! wc {
() => { () => {
pos.castle[Color::White as usize] pos.castle.0[Color::White as usize]
}; };
} }
macro_rules! bc { macro_rules! bc {
() => { () => {
pos.castle[Color::Black as usize] pos.castle.0[Color::Black as usize]
}; };
} }
match c { match c {
@ -165,7 +168,7 @@ impl FromFen for BoardState {
'-' => { '-' => {
parse_space_and_goto!(FenState::EnPassantRank); parse_space_and_goto!(FenState::EnPassantRank);
} }
_ => return bad_char!(i), _ => return bad_char!(i, c),
} }
} }
FenState::EnPassantRank => { FenState::EnPassantRank => {
@ -177,7 +180,7 @@ impl FromFen for BoardState {
pos.ep_square = Some(Square(c as usize - 'a' as usize)); pos.ep_square = Some(Square(c as usize - 'a' as usize));
parser_state = FenState::EnPassantFile; parser_state = FenState::EnPassantFile;
} }
_ => return bad_char!(i), _ => return bad_char!(i, c),
}; };
} }
FenState::EnPassantFile => { FenState::EnPassantFile => {
@ -187,7 +190,7 @@ impl FromFen for BoardState {
+ (digit as usize - 1) * 8, + (digit as usize - 1) * 8,
)); ));
} else { } else {
return bad_char!(i); return bad_char!(i, c);
} }
parse_space_and_goto!(FenState::HalfMove); parse_space_and_goto!(FenState::HalfMove);
} }
@ -201,7 +204,7 @@ impl FromFen for BoardState {
} else if c == ' ' { } else if c == ' ' {
parser_state = FenState::FullMove; parser_state = FenState::FullMove;
} else { } else {
return bad_char!(i); return bad_char!(i, c);
} }
} }
FenState::FullMove => { FenState::FullMove => {
@ -212,7 +215,7 @@ impl FromFen for BoardState {
pos.full_moves *= 10; pos.full_moves *= 10;
pos.full_moves += digit as usize; pos.full_moves += digit as usize;
} else { } 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::<Vec<String>>()
.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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::CastlePlayer;
use crate::N_SQUARES; use crate::N_SQUARES;
use crate::CastlingRights;
#[test] #[test]
fn test_fen_pieces() { fn test_fen_pieces() {
@ -250,10 +297,10 @@ mod tests {
assert_eq!(board.turn, Color::Black); assert_eq!(board.turn, Color::Black);
} }
macro_rules! make_board{ macro_rules! make_board {
($fen_fmt: expr) => { ($fen_fmt: expr) => {
BoardState::from_fen(format!($fen_fmt)).unwrap() BoardState::from_fen(format!($fen_fmt)).unwrap()
} };
} }
#[test] #[test]
@ -283,56 +330,57 @@ mod tests {
( (
"-", "-",
[ [
CastlingRights { k: false, q: false }, CastlePlayer { k: false, q: false },
CastlingRights { k: false, q: false }, CastlePlayer { k: false, q: false },
], ],
), ),
( (
"k", "k",
[ [
CastlingRights { k: false, q: false }, CastlePlayer { k: false, q: false },
CastlingRights { k: true, q: false }, CastlePlayer { k: true, q: false },
], ],
), ),
( (
"kq", "kq",
[ [
CastlingRights { k: false, q: false }, CastlePlayer { k: false, q: false },
CastlingRights { k: true, q: true }, CastlePlayer { k: true, q: true },
], ],
), ),
// This is the wrong order, but parsers should be lenient
( (
"qk", "qk",
[ [
CastlingRights { k: false, q: false }, CastlePlayer { k: false, q: false },
CastlingRights { k: true, q: true }, CastlePlayer { k: true, q: true },
], ],
), ),
( (
"KQkq", "KQkq",
[ [
CastlingRights { k: true, q: true }, CastlePlayer { k: true, q: true },
CastlingRights { k: true, q: true }, CastlePlayer { k: true, q: true },
], ],
), ),
( (
"KQ", "KQ",
[ [
CastlingRights { k: true, q: true }, CastlePlayer { k: true, q: true },
CastlingRights { k: false, q: false }, CastlePlayer { k: false, q: false },
], ],
), ),
( (
"QK", "QK",
[ [
CastlingRights { k: true, q: true }, CastlePlayer { k: true, q: true },
CastlingRights { k: false, q: false }, CastlePlayer { k: false, q: false },
], ],
), ),
]; ];
for (castle_str, castle) in test_cases { for (castle_str, castle) in test_cases {
let board = make_board!("8/8/8/8/8/8/8/8 w {castle_str} - 0 0"); 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); 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")
}
}
} }

View File

@ -2,7 +2,6 @@
pub mod fen; pub mod fen;
pub mod movegen; pub mod movegen;
use std::rc::Rc;
const BOARD_WIDTH: usize = 8; const BOARD_WIDTH: usize = 8;
const BOARD_HEIGHT: usize = 8; const BOARD_HEIGHT: usize = 8;
@ -26,6 +25,15 @@ impl Color {
} }
} }
impl From<Color> for char {
fn from(value: Color) -> Self {
match value {
Color::White => 'w',
Color::Black => 'b',
}
}
}
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
enum Piece { enum Piece {
Rook, Rook,
@ -102,6 +110,7 @@ impl ColPiece {
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Square(usize); struct Square(usize);
#[derive(Debug)]
enum IndexError { enum IndexError {
OutOfBounds, OutOfBounds,
} }
@ -128,6 +137,35 @@ impl Square {
let ret = BOARD_WIDTH * r + c; let ret = BOARD_WIDTH * r + c;
ret.try_into() 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<char> for Piece { impl TryFrom<char> for Piece {
@ -216,13 +254,38 @@ impl Player {
/// Castling rights for one player /// Castling rights for one player
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] #[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
pub struct CastlingRights { pub struct CastlePlayer {
/// Kingside /// Kingside
k: bool, k: bool,
/// Queenside /// Queenside
q: bool, 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. /// Immutable game state, unique to a position.
/// ///
/// Default is empty. /// Default is empty.
@ -240,7 +303,7 @@ pub struct BoardState {
ep_square: Option<Square>, ep_square: Option<Square>,
/// Castling rights /// Castling rights
castle: [CastlingRights; N_COLORS], castle: CastleRights,
/// Plies since last irreversible (capture, pawn) move /// Plies since last irreversible (capture, pawn) move
half_moves: usize, half_moves: usize,