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:

kiwiPete by Peter McKenzie

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:

kiwiPete by Peter McKenzie but bad because I made it

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 :)