Devlog 0x4 - Parsing FEN strings and attempting to debug my implementation
Devlog 0x4. Today, I implemented a parseFEN function into Kirin. FEN, which stands for Forsyth-Edwards Notation, is the standard notation to describe positions of a chess game. Also, some debugging that I have yet to solve.
What is FEN?
Forsyth-Edwards Notation, or FEN, is the standard notation for representing positions of a chess game. Chess.com has an excellent article about FEN if you want to read more. In short, FEN strings look something like this:
r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -
This is my favorite debugging position, known as "kiwipete" by Peter McKenzie. The FEN string probably doesn't mean much to you right now, but they are quite simple. Let's see what this position would look like graphically:
Let's look at the first rank: r3k2r. In FEN notation, we start from the square a8 and work down the board. Lowercase pieces are black and uppercase pieces are white. This means that r3k2r means there is a black rook on a8, three empty squares, black's king on e8(a8 - 3 squares), two more empty squares, and another black rook on a h8. You continue this pattern down the board, until you represent the entire position.
Because game state and bitboards are now functional, this is simply a case of parsing a defined input structure. Here is how I went about parsing the FEN strings and modifying the game state:
pub fn parseFEN(fen: []const u8) void {
// Reset board state
@memset(bb.bitboards[0..], 0);
@memset(bb.occupancies[0..], 0);
bb.sideToMove = 0;
bb.enpassant = @intFromEnum(bb.boardSquares.noSquare);
bb.castle = 0;
var fenIndex: usize = 0;
var fileIndex: usize = 0;
// Parse piece positions
for (0..8) |rank| {
fileIndex = 0;
while (fileIndex < 8) {
const square: u6 = @intCast(rank * 8 + fileIndex);
// Handle pieces
if ((fen[fenIndex] >= 'a' and fen[fenIndex] <= 'z') or
(fen[fenIndex] >= 'A' and fen[fenIndex] <= 'Z'))
{
const piece: u8 = bb.charPieces[fen[fenIndex]];
utils.setBit(&bb.bitboards[piece], square);
fenIndex += 1;
fileIndex += 1;
}
// Handle empty squares
else if (fen[fenIndex] >= '0' and fen[fenIndex] <= '9') {
const offset = fen[fenIndex] - '0';
fileIndex += offset; // -1 because the loop will increment file
fenIndex += 1;
}
// Handle rank separator
else if (fen[fenIndex] == '/') {
fenIndex += 1;
break; // Move to next rank
}
}
}
// Skip to side to move
fenIndex += 1;
// Parse side to move
bb.sideToMove = if (fen[fenIndex] == 'w') @intFromEnum(bb.side.white) else @intFromEnum(bb.side.black);
// Skip to castling rights
fenIndex += 2;
// Parse castling rights
while (fen[fenIndex] != ' ') : (fenIndex += 1) {
switch (fen[fenIndex]) {
'K' => bb.castle |= @intFromEnum(bb.castlingRights.wk),
'Q' => bb.castle |= @intFromEnum(bb.castlingRights.wq),
'k' => bb.castle |= @intFromEnum(bb.castlingRights.bk),
'q' => bb.castle |= @intFromEnum(bb.castlingRights.bq),
'-' => {},
else => {},
}
}
// Skip to en passant square
fenIndex += 1;
// Parse en passant square
if (fen[fenIndex] != '-') {
const file_val: i32 = fen[fenIndex] - 'a';
const rank_val: i32 = 8 - (@as(i32, fen[fenIndex + 1] -% '0'));
if (file_val >= 0 and file_val < 8 and
rank_val >= 0 and rank_val < 8)
{
const file: i8 = @intCast(file_val);
const rank: i8 = @intCast(rank_val);
bb.enpassant = @intCast(rank * 8 + file);
}
}
for (@intFromEnum(bb.pieceEncoding.P)..@intFromEnum(bb.pieceEncoding.K) + 1) |piece| {
bb.occupancies[@intFromEnum(bb.side.white)] |= bb.bitboards[piece];
}
for (@intFromEnum(bb.pieceEncoding.p)..@intFromEnum(bb.pieceEncoding.k) + 1) |piece| {
bb.occupancies[@intFromEnum(bb.side.black)] |= bb.bitboards[piece];
}
bb.occupancies[@intFromEnum(bb.side.both)] = bb.occupancies[@intFromEnum(bb.side.white)] | bb.occupancies[@intFromEnum(bb.side.black)];
}
It's a bit lengthy, but quite didactic and easy to follow. Essentially, we parse each rank at a time, then increment the file and repeat the process. Unfortunately, here is where we get to the nasty bug that I wasn't able to fix this morning.
We saw the correct KiwiPete position earlier, but here is how my parseFEN function is currently working:
Unfortunately, I think I have an off by one error that is causing every other rank to be skipped when occupying the bitboards with the values they are supposed to contain. I'm going to continue working on debugging today and I will have an update tomorrow on my discoveries.
Thanks for reading, and I'll see you tomorrow :)