Creating FEN Strings
In the previous section, we implemented the functionality required to read a FEN string and convert it into an internal board position. However, a chess engine also needs to perform the reverse operation. Positions often need to be exported for debugging, or testing.
Creating a FEN string is generally simpler than parsing one because the board already contains all required information in a structured format. The task is therefore reduced to extracting the six FEN fields from the current position and assembling them into a correctly formatted string.
The implementation below follows the same structure as the parser. Each helper function generates one of the six FEN fields, and the main function combines them into the final result.
fen_create()
The main entry point for FEN generation. It gathers all six FEN fields and combines them into a single space-separated string.
#![allow(unused)]
fn main() {
impl Board {
pub fn fen_create(&self) -> String {
let pieces = pieces(self);
let color = color(self);
let castling = castling(self);
let en_passant = en_passant(self);
let hmc = half_move_clock(self);
let fmn = full_move_number(self);
[pieces, color, castling, en_passant, hmc, fmn].join(" ")
}
}
}
pieces()
Generates the first FEN field containing the piece placement. The function scans the board rank by rank, counts consecutive empty squares, and emits the appropriate piece characters according to the FEN specification.
#![allow(unused)]
fn main() {
fn pieces(board: &Board) -> String {
let mut part1 = String::with_capacity(64);
for rank in (0u8..8).rev() {
let mut empty = 0;
for file in 0u8..8 {
let square = Square::from(rank * 8 + file);
let piece = board.piece_list[square];
if piece == Piece::None {
empty += 1;
continue;
}
if empty != 0 {
part1.push_str(&empty.to_string());
empty = 0;
}
let white = (board.white_pieces() & BB_SQUARES[square]) != Bitboard::new(0);
let piece_char = if white {
piece.fen_char_white()
} else {
piece.fen_char_black()
};
piece_char.inspect(|c| part1.push(*c));
}
if empty != 0 {
part1.push_str(&empty.to_string());
}
if rank != 0 {
part1.push('/');
}
}
part1
}
}
color()
Creates the second FEN field indicating which side is to move.
#![allow(unused)]
fn main() {
fn color(board: &Board) -> String {
match board.game_state.active_color {
Side::White => "w".to_string(),
Side::Black => "b".to_string(),
}
}
}
castling()
Generates the castling-rights field by examining the current castling permissions stored in the game state.
#![allow(unused)]
fn main() {
fn castling(board: &Board) -> String {
let permissions = board.game_state.castling;
let mut result = String::new();
let rules = [
(Permissions::WK, 'K'),
(Permissions::WQ, 'Q'),
(Permissions::BK, 'k'),
(Permissions::BQ, 'q'),
];
for (flag, character) in rules {
if permissions & flag != Bitboard::empty() {
result.push(character);
}
}
if result.is_empty() {
result = String::from("-");
}
result
}
}
en_passant()
Creates the en passant field. If an en passant target square exists, it is converted to algebraic notation; otherwise a dash is emitted.
#![allow(unused)]
fn main() {
fn en_passant(board: &Board) -> String {
match board.game_state.en_passant {
Some(sq_nr) => Square::from(sq_nr).name().to_string(),
None => "-".to_string(),
}
}
}
half_move_clock()
Generates the half-move clock field used for the fifty-move rule.
#![allow(unused)]
fn main() {
fn half_move_clock(board: &Board) -> String {
format!("{}", board.game_state.half_move_clock)
}
}
full_move_number()
Generates the full-move number field.
#![allow(unused)]
fn main() {
fn full_move_number(board: &Board) -> String {
format!("{}", board.game_state.fullmove_number)
}
}
The FEN creation code mirrors the parser discussed in the previous section. Instead of reading and validating external data, it extracts information from the board and converts it into a standardized textual representation. Together, the parser and generator provide complete support for importing and exporting positions, making the engine compatible with GUIs, test suites, and external tools.
With position serialization now complete, we can move on to another fundamental board operation: updating the position itself by moving pieces from one square to another.