implemented full FEN parser

todo: fix bugs
This commit is contained in:
dogeystamp 2024-09-24 22:01:49 -04:00
parent c3439bdd46
commit cccf41e7b0
2 changed files with 150 additions and 34 deletions

View File

@ -1,11 +1,8 @@
use chess_inator::Position; use chess_inator::Position;
fn main() { fn main() {
let fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR "; let fen = "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1";
let board = Position::from_fen(fen.into()).unwrap(); let board = Position::from_fen(fen.into()).unwrap();
println!("{}", board); println!("{}", board);
println!("{:#?}", board);
let fen = "rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R ";
let board = Position::from_fen(fen.into()).unwrap();
print!("{}", board);
} }

View File

@ -4,9 +4,9 @@ const BOARD_WIDTH: usize = 8;
const BOARD_HEIGHT: usize = 8; const BOARD_HEIGHT: usize = 8;
const N_SQUARES: usize = BOARD_WIDTH * BOARD_HEIGHT; const N_SQUARES: usize = BOARD_WIDTH * BOARD_HEIGHT;
#[derive(Debug, Copy, Clone, Default)]
#[derive(Debug, Copy, Clone)]
enum Color { enum Color {
#[default]
White, White,
Black, Black,
} }
@ -36,10 +36,17 @@ impl TryFrom<char> for ColPiece {
type Error = PieceErr; type Error = PieceErr;
fn try_from(value: char) -> Result<Self, Self::Error> { fn try_from(value: char) -> Result<Self, Self::Error> {
let col = if value.is_ascii_uppercase() { Color::White } else { Color::Black }; let col = if value.is_ascii_uppercase() {
Color::White
} else {
Color::Black
};
let mut lower = value; let mut lower = value;
lower.make_ascii_lowercase(); lower.make_ascii_lowercase();
Ok(ColPiece {pc: Piece::try_from(lower)?, col}) Ok(ColPiece {
pc: Piece::try_from(lower)?,
col,
})
} }
} }
@ -210,10 +217,27 @@ pub enum FenError {
TooManyRows(usize), TooManyRows(usize),
/// Too little rows. /// Too little rows.
NotEnoughRows(usize), NotEnoughRows(usize),
/// Parser refuses to keep parsing move counter because it is too big.
TooManyMoves,
/// Error in the parser. /// Error in the parser.
InternalError(usize), InternalError(usize),
} }
/// Castling rights for one player
#[derive(Debug)]
pub struct CastlingRights {
/// Kingside
k: bool,
/// Queenside
q: bool,
}
impl Default for CastlingRights {
fn default() -> Self {
CastlingRights { k: true, q: true }
}
}
/// Game state. /// Game state.
/// ///
/// Default is empty. /// Default is empty.
@ -224,6 +248,25 @@ pub struct Position {
/// Mailbox (array) board. Location -> piece. /// Mailbox (array) board. Location -> piece.
mail: Mailbox, mail: Mailbox,
/// En-passant square.
///
/// (If a pawn moves twice, this is one square in front of the start position.)
ep_square: Option<Index>,
/// Castling rights (white)
white_castle: CastlingRights,
/// Castling rights (black)
black_castle: CastlingRights,
/// Plies since last irreversible (capture, pawn) move
half_moves: usize,
/// Full move counter (incremented after each black turn)
full_moves: usize,
/// Whose turn it is
turn: Color,
} }
impl Position { impl Position {
@ -257,9 +300,10 @@ impl Position {
//! Parse FEN string into position. //! Parse FEN string into position.
/// Parser state machine. /// Parser state machine.
/// #[derive(Clone, Copy)]
/// Space characters are considered part of the preceding state.
enum FenState { enum FenState {
/// Parses space characters between arguments, and jumps to next state.
Space,
/// Accepts pieces in a row, or a slash, and stores row and column (0-indexed) /// Accepts pieces in a row, or a slash, and stores row and column (0-indexed)
Piece(usize, usize), Piece(usize, usize),
/// Player whose turn it is /// Player whose turn it is
@ -267,23 +311,49 @@ impl Position {
/// Castling ability /// Castling ability
Castle, Castle,
/// En passant square, letter part /// En passant square, letter part
EnPassantRow, EnPassantFile,
/// En passant square, digit part /// En passant square, digit part
EnPassantCol, EnPassantRank,
/// Half-move counter for 50-move draw rule. Resets after capture/pawn move. /// Half-move counter for 50-move draw rule
HalfMove, HalfMove,
/// Full-move counter, incremented after each Black move /// Full-move counter
FullMove, FullMove,
/// Final state.
Stop,
} }
/// Maximum amount of moves in the counter to parse before giving up
const MAX_MOVES: usize = 999_999;
let mut pos = Position::default(); let mut pos = Position::default();
let mut parser_state = FenState::Piece(0, 0); let mut parser_state = FenState::Piece(0, 0);
let mut next_state = FenState::Space;
/// Create parse error at a given index
macro_rules! bad_char {
($idx:ident) => {
Err(FenError::BadChar($idx))
};
}
/// Parse a space character, then jump to the given state
macro_rules! parse_space_and_goto {
($next:expr) => {
parser_state = FenState::Space;
next_state = $next;
};
}
for (i, c) in fen.chars().enumerate() { for (i, c) in fen.chars().enumerate() {
match parser_state { match parser_state {
FenState::Space => {
match c {
' ' => {
parser_state = next_state;
}
_ => return bad_char!(i),
};
}
FenState::Piece(mut row, mut col) => { FenState::Piece(mut row, mut col) => {
// FEN stores rows differently from our bitboard // FEN stores rows differently from our bitboard
let real_row = BOARD_HEIGHT - 1 - row; let real_row = BOARD_HEIGHT - 1 - row;
@ -300,7 +370,7 @@ impl Position {
parser_state = FenState::Piece(row, col) parser_state = FenState::Piece(row, col)
} }
pc_char @ ('b'..='r' | 'B'..='R') => { pc_char @ ('b'..='r' | 'B'..='R') => {
let pc = ColPiece::try_from(pc_char).or(Err(FenError::BadChar(i)))?; let pc = ColPiece::try_from(pc_char).or(bad_char!(i))?;
pos.set_piece( pos.set_piece(
Index::from_row_col(real_row, col) Index::from_row_col(real_row, col)
@ -321,7 +391,7 @@ impl Position {
}; };
parser_state = FenState::Piece(row, col); parser_state = FenState::Piece(row, col);
} else { } else {
return Err(FenError::BadChar(i)); return bad_char!(i);
} }
} }
' ' => { ' ' => {
@ -330,34 +400,83 @@ impl Position {
} else if col < BOARD_WIDTH { } else if col < BOARD_WIDTH {
return Err(FenError::NotEnoughPieces(i)); return Err(FenError::NotEnoughPieces(i));
} }
parser_state = FenState::Stop parser_state = FenState::Side
} }
_ => return Err(FenError::BadChar(i)), _ => return bad_char!(i),
}; };
} }
FenState::Side => { FenState::Side => {
todo!() match c {
'w' => pos.turn = Color::White,
'b' => pos.turn = Color::Black,
_ => return bad_char!(i),
}
parse_space_and_goto!(FenState::Castle);
} }
FenState::Castle => { FenState::Castle => match c {
todo!() 'Q' => pos.white_castle.q = true,
'q' => pos.black_castle.q = true,
'K' => pos.white_castle.k = true,
'k' => pos.black_castle.k = true,
' ' => parser_state = FenState::EnPassantRank,
'-' => {
parse_space_and_goto!(FenState::EnPassantRank);
}
_ => return bad_char!(i),
},
FenState::EnPassantRank => {
match c {
'-' => {
parse_space_and_goto!(FenState::HalfMove);
}
'a'..='h' => {
// TODO: fix this
pos.ep_square = Some(Index((c as usize - 'a' as usize) * 8));
parser_state = FenState::EnPassantFile;
}
_ => return bad_char!(i),
};
} }
FenState::EnPassantRow => { FenState::EnPassantFile => {
todo!() if let Some(digit) = c.to_digit(10) {
} pos.ep_square = Some(Index(
FenState::EnPassantCol => { usize::from(pos.ep_square.unwrap_or(Index(0))) + digit as usize,
todo!() ));
} else {
return bad_char!(i);
}
parse_space_and_goto!(FenState::HalfMove);
} }
FenState::HalfMove => { FenState::HalfMove => {
todo!() if let Some(digit) = c.to_digit(10) {
if pos.half_moves > MAX_MOVES {
return Err(FenError::TooManyMoves);
}
pos.half_moves *= 10;
pos.half_moves += digit as usize;
} else if c == ' ' {
parser_state = FenState::FullMove;
} else {
return bad_char!(i);
}
} }
FenState::FullMove => { FenState::FullMove => {
todo!() if let Some(digit) = c.to_digit(10) {
if pos.half_moves > MAX_MOVES {
return Err(FenError::TooManyMoves);
}
pos.full_moves *= 10;
pos.full_moves += digit as usize;
} else {
return bad_char!(i);
}
} }
FenState::Stop => return Err(FenError::ExtraChar(i)),
} }
} }
if matches!(parser_state, FenState::Stop) { // parser is always ready to receive another full move digit,
// so there is no real "stop" state
if matches!(parser_state, FenState::FullMove) {
Ok(pos) Ok(pos)
} else { } else {
Err(FenError::MissingFields) Err(FenError::MissingFields)
@ -375,7 +494,7 @@ impl core::fmt::Display for Position {
str.push(ColPiece::opt_to_char(pc)); str.push(ColPiece::opt_to_char(pc));
} }
str += "\n"; str += "\n";
}; }
write!(f, "{}", str) write!(f, "{}", str)
} }
} }