feat: FEN printing
This commit is contained in:
parent
77838fd417
commit
8db3a236c0
161
src/fen.rs
161
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::<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)]
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
69
src/lib.rs
69
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<Color> 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<char> 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<Square>,
|
||||
|
||||
/// Castling rights
|
||||
castle: [CastlingRights; N_COLORS],
|
||||
castle: CastleRights,
|
||||
|
||||
/// Plies since last irreversible (capture, pawn) move
|
||||
half_moves: usize,
|
||||
|
Loading…
Reference in New Issue
Block a user