Reading FEN Strings
Forsyth-Edwards Notation (FEN) is a standardized format for describing a chess position using a single text string. A FEN string contains all information required to reconstruct a position on the board, including piece placement, the side to move, castling rights, en passant status, the half-move clock, and the full-move number.
FEN strings are used extensively throughout the chess world. Graphical user interfaces use them to load positions, test suites use them to provide problems to engines, and tournament managers use them to resume games or start from custom positions. Because of this, every chess engine needs a reliable way to parse a FEN string and convert it into an internal board representation.
The implementation presented below divides the parsing process into several small functions. Each function is responsible for validating and processing one of the six FEN fields. Together, these functions read a FEN string, verify that it is valid, and use its contents to set up a complete chess position.
fen_setup()
The main FEN-loading function. It parses a FEN string on a temporary board and only replaces the current board if parsing succeeds completely. This protects the engine from board corruption when invalid FEN strings are supplied.
pub fn fen_setup(&mut self, fen_string: Option<&str>) -> FenResult {
let fen_parts = split_fen_string(fen_string)?;
let mut temp_board = self.clone();
temp_board.reset();
pieces(&mut temp_board, &fen_parts[0])?;
color(&mut temp_board, &fen_parts[1])?;
castling(&mut temp_board, &fen_parts[2])?;
en_passant(&mut temp_board, &fen_parts[3])?;
half_move_clock(&mut temp_board, &fen_parts[4])?;
full_move_number(&mut temp_board, &fen_parts[5])?;
temp_board.init();
*self = temp_board;
Ok(())
}
fen_setup_fast()
A performance-oriented version of the FEN parser. It operates directly on the provided board and avoids the temporary copy used by `fen_setup(). This makes it faster, but invalid input may leave the board in an inconsistent state.
pub fn fen_setup_fast(board: &mut Board, fen_string: Option<&str>) -> FenResult {
let fen_parts = split_fen_string(fen_string)?;
board.reset();
pieces(board, &fen_parts[0])?;
color(board, &fen_parts[1])?;
castling(board, &fen_parts[2])?;
en_passant(board, &fen_parts[3])?;
half_move_clock(board, &fen_parts[4])?;
full_move_number(board, &fen_parts[5])?;
board.init();
Ok(())
}
split_fen_string()
Splits the incoming FEN string into its six individual components and performs basic validation on the number of fields. It also supports abbreviated four-field FEN strings by automatically appending default move counters.
fn split_fen_string(fen_string: Option<&str>) -> SplitResult {
const SHORT_FEN_LENGTH: usize = 4;
let mut fen_string: Vec<String> = match fen_string {
Some(fen) => fen,
None => FEN_START_POSITION,
}
.replace(EM_DASH, DASH.encode_utf8(&mut [0; 4]))
.split(SPACE)
.map(String::from)
.collect();
if fen_string.len() == SHORT_FEN_LENGTH {
fen_string.append(&mut vec![String::from("0"), String::from("1")]);
}
if fen_string.len() != FEN_NR_OF_PARTS {
return Err(FenError::IncorrectLength);
}
Ok(fen_string)
}
pieces()
Parses the first FEN field containing piece placement information and populates the appropriate piece bitboards.
fn pieces(board: &mut Board, part: &str) -> FenResult {
let mut rank = Rank::R8 as u8;
let mut file = File::A as u8;
for c in part.chars() {
let square = ((rank * 8) + file) as usize;
match c {
'k' => board.bb_pieces[Side::Black][Piece::King] |= BB_SQUARES[square],
'q' => board.bb_pieces[Side::Black][Piece::Queen] |= BB_SQUARES[square],
'r' => board.bb_pieces[Side::Black][Piece::Rook] |= BB_SQUARES[square],
'b' => board.bb_pieces[Side::Black][Piece::Bishop] |= BB_SQUARES[square],
'n' => board.bb_pieces[Side::Black][Piece::Knight] |= BB_SQUARES[square],
'p' => board.bb_pieces[Side::Black][Piece::Pawn] |= BB_SQUARES[square],
'K' => board.bb_pieces[Side::White][Piece::King] |= BB_SQUARES[square],
'Q' => board.bb_pieces[Side::White][Piece::Queen] |= BB_SQUARES[square],
'R' => board.bb_pieces[Side::White][Piece::Rook] |= BB_SQUARES[square],
'B' => board.bb_pieces[Side::White][Piece::Bishop] |= BB_SQUARES[square],
'N' => board.bb_pieces[Side::White][Piece::Knight] |= BB_SQUARES[square],
'P' => board.bb_pieces[Side::White][Piece::Pawn] |= BB_SQUARES[square],
'1'..='8' => {
if let Some(x) = c.to_digit(10) {
file += x as u8;
}
}
SPLITTER => {
if file != 8 {
return Err(FenError::Part1);
}
rank -= 1;
file = 0;
}
_ => return Err(FenError::Part1),
}
if LIST_OF_PIECES.contains(c) {
file += 1;
}
}
Ok(())
}
color()
Parses the side-to-move field and sets the active color to either White or Black.
fn color(board: &mut Board, part: &str) -> FenResult {
if part.len() == 1
&& let Some(c) = part.chars().next()
&& WHITE_OR_BLACK.contains(c)
{
match c {
'w' => board.game_state.active_color = Side::White,
'b' => board.game_state.active_color = Side::Black,
_ => (),
}
return Ok(());
}
Err(FenError::Part2)
}
castling()
Parses the castling-rights field and records which castling permissions are available in the current position.
fn castling(board: &mut Board, part: &str) -> FenResult {
if (1..=4).contains(&part.len()) {
for c in part.chars() {
match c {
'K' => board.game_state.castling |= Permissions::WK,
'Q' => board.game_state.castling |= Permissions::WQ,
'k' => board.game_state.castling |= Permissions::BK,
'q' => board.game_state.castling |= Permissions::BQ,
'-' => (),
_ => return Err(FenError::Part3),
}
}
return Ok(());
}
Err(FenError::Part3)
}
en_passant()
Parses the en passant field and stores the target square when an en passant capture is available.
fn en_passant(board: &mut Board, part: &str) -> FenResult {
if part.len() == 1
&& let Some(x) = part.chars().next()
&& x == DASH
{
return Ok(());
}
if part.len() == 2 {
let number = parse::algebraic_square_to_number(part);
match number {
Some(n)
if EP_SQUARES_WHITE.contains(&Square::from(n))
|| EP_SQUARES_BLACK.contains(&Square::from(n)) =>
{
board.game_state.en_passant = Some(Square::from(n));
return Ok(());
}
_ => return Err(FenError::Part4),
};
}
Err(FenError::Part4)
}
half_move_clock()
Parses the half-move clock used for the fifty-move rule.
fn half_move_clock(board: &mut Board, part: &str) -> FenResult {
if (1..=3).contains(&part.len())
&& let Ok(x) = part.parse::<u8>()
&& x <= MAX_MOVE_RULE
{
board.game_state.half_move_clock = x;
return Ok(());
}
Err(FenError::Part5)
}
full_move_number()
Parses the full-move number and stores it in the game state.
fn full_move_number(board: &mut Board, part: &str) -> FenResult {
if !part.is_empty()
&& part.len() <= 4
&& let Ok(x) = part.parse::<u16>()
&& x <= (MAX_GAME_MOVES as u16)
{
board.game_state.fullmove_number = x;
return Ok(());
}
Err(FenError::Part6)
}
The FEN parser is one of the most important components of the board implementation. It provides a reliable way to construct arbitrary chess positions from a standardized text representation while ensuring that all aspects of the game state are initialized correctly. By dividing the parsing process into a series of focused functions, the code remains easy to understand, test, and maintain.
At this point, the board can successfully consume a FEN string and convert it into an internal position. However, the reverse operation is equally important. Chess engines frequently need to export positions for debugging, logging, and testing.
In the next section, we will examine the function that performs the opposite task: generating a valid FEN string from the current board position.