Devlog 0x3 - General Improvements and Printing Current Game State

Devlog 0x3. Today, I implemented a printBoard function that iterates through the list of piece bitboards and prints a representation of the current game state, along with general improvements to the popBit() and setBit() functions.

Improved Bit Manipulation Functions

Today, I refined the way bitboards are manipulated in my chess engine. Instead of returning a new bitboard value (and requiring the caller to store or handle it), I now pass a pointer to the existing bitboard and modify it directly in place. This approach improves both readability and performance by minimizing unnecessary copies or assignments.

pub fn setBit(bitboard: *u64, square: u6) void {
    bitboard.* |= (@as(u64, 1) << square);
}

Previously, I returned a u64 after setting the bit, forcing me assign that value back to the bitboard. Now, the bitboard is updated directly.

pub fn popBit(bitboard: *u64, square: u6) void {
    if (getBit(bitboard.*, square) != 0) {
        bitboard.* ^= (@as(u64, 1) << square);
    }
}

Here as well, the bit is cleared (“popped”) right in the provided bitboard’s memory address. No need for an intermediate variable to hold a modified return value. These changes help to make the code more straightforward and far less verbose.

printBoard() function

The printBoard() function iterates through a list of piece bitboards and prints out a readable chessboard layout in the console. This function is almost purely for debugging purposes and won't be used by the engine in any meaningful way.

pub fn printBoard() void {
    std.debug.print("\n", .{});
    for (0..8) |rank| {
        for (0..8) |file| {
            const square: u6 = @intCast(rank * 8 + file);
            if (file == 0) std.debug.print("  {d} ", .{8 - rank});
            var piece: i5 = -1;
            for (0..12) |bitboardPiece| {
                if (getBit(bb.bitboards[bitboardPiece], square) != 0) {
                    piece = @intCast(bitboardPiece);
                    break;
                } else {
                    piece = -1;
                }
            }
            if (piece == -1) {
                std.debug.print(" .", .{});
            } else {
                std.debug.print(" {s}", .{bb.unicodePieces[@intCast(piece)]});
            }
        }
        std.debug.print("\n", .{});
    }
    std.debug.print("\n     a b c d e f g h\n\n", .{});
    const side = if (bb.sideToMove == 0) "white" else "black";
    std.debug.print("Side: {s}", .{side});
}

Essentially, if a set bit is found within a given pieces bitboard, the appropriate unicode piece representation is printed. If no set bit is found at a square, we print a dot, indicating an empty space. This function allows us to easily look at the current state without needing a separate GUI.

Next Steps

  • Move Generation: Now that bitboard manipulation is streamlined, I will further develop my move-generation functions.
  • Performance & Cleanliness: Explore further opportunities for optimization, keeping the codebase clear and efficient.

Thanks for reading, and I'll see you tomorrow :)