Add chess GUI and move generation

This commit is contained in:
2026-05-19 16:05:53 -08:00
parent b36f5b0b84
commit 4131c93f99
69 changed files with 80468 additions and 218 deletions
+309
View File
@@ -0,0 +1,309 @@
const std = @import("std");
const board = @import("chess/board.zig");
const fen = @import("chess/fen.zig");
const geometry = @import("geometry.zig");
const piece = @import("chess/piece.zig");
pub const PieceGroup = enum(u4) {
white_pawn,
white_knight,
white_bishop,
white_rook,
white_queen,
white_king,
black_pawn,
black_knight,
black_bishop,
black_rook,
black_queen,
black_king,
};
pub const PieceGroupCount = 12;
pub const PaletteEntry = struct {
group: PieceGroup,
encoded: u4,
};
pub const palette_entries = [_]PaletteEntry{
.{ .group = .white_king, .encoded = piece.encode(.white, .king) },
.{ .group = .white_queen, .encoded = piece.encode(.white, .queen) },
.{ .group = .white_rook, .encoded = piece.encode(.white, .rook) },
.{ .group = .white_bishop, .encoded = piece.encode(.white, .bishop) },
.{ .group = .white_knight, .encoded = piece.encode(.white, .knight) },
.{ .group = .white_pawn, .encoded = piece.encode(.white, .pawn) },
.{ .group = .black_king, .encoded = piece.encode(.black, .king) },
.{ .group = .black_queen, .encoded = piece.encode(.black, .queen) },
.{ .group = .black_rook, .encoded = piece.encode(.black, .rook) },
.{ .group = .black_bishop, .encoded = piece.encode(.black, .bishop) },
.{ .group = .black_knight, .encoded = piece.encode(.black, .knight) },
.{ .group = .black_pawn, .encoded = piece.encode(.black, .pawn) },
};
pub fn paletteRectForBoard(board_rect: geometry.BoardRect) geometry.BoardRect {
const gap = board_rect.width / 48.0;
return .{
.left = board_rect.left + board_rect.width + gap,
.bottom = board_rect.bottom,
.width = board_rect.width / @as(f32, @floatFromInt(palette_entries.len)),
.height = board_rect.height,
};
}
pub const PieceVertexGroups = struct {
groups: [PieceGroupCount]std.ArrayList(geometry.Vertex),
pub fn init() PieceVertexGroups {
return .{
.groups = [_]std.ArrayList(geometry.Vertex){.empty} ** PieceGroupCount,
};
}
pub fn deinit(self: *PieceVertexGroups, allocator: std.mem.Allocator) void {
for (&self.groups) |*vertex_group| {
vertex_group.deinit(allocator);
}
}
pub fn group(self: *PieceVertexGroups, piece_group: PieceGroup) *std.ArrayList(geometry.Vertex) {
return &self.groups[@intFromEnum(piece_group)];
}
pub fn constGroup(self: *const PieceVertexGroups, piece_group: PieceGroup) *const std.ArrayList(geometry.Vertex) {
return &self.groups[@intFromEnum(piece_group)];
}
};
pub fn pieceGroupFromEncoded(encoded: u4) ?PieceGroup {
if (encoded == 0) return null;
const color = piece.colorOf(encoded) orelse return null;
const piece_type = piece.typeOf(encoded);
return switch (color) {
.white => switch (piece_type) {
.pawn => .white_pawn,
.knight => .white_knight,
.bishop => .white_bishop,
.rook => .white_rook,
.queen => .white_queen,
.king => .white_king,
.none => null,
},
.black => switch (piece_type) {
.pawn => .black_pawn,
.knight => .black_knight,
.bishop => .black_bishop,
.rook => .black_rook,
.queen => .black_queen,
.king => .black_king,
.none => null,
},
};
}
fn appendPieceQuadInRect(
vertices: *std.ArrayList(geometry.Vertex),
allocator: std.mem.Allocator,
rect: geometry.BoardRect,
) !void {
const inset_x = rect.width * 0.08;
const inset_y = rect.height * 0.08;
const x0 = rect.left + inset_x;
const x1 = rect.left + rect.width - inset_x;
const y0 = rect.bottom + inset_y;
const y1 = rect.bottom + rect.height - inset_y;
try geometry.appendTexturedQuad(
vertices,
allocator,
.{ x0, y0 },
.{ x1, y0 },
.{ x1, y1 },
.{ x0, y1 },
geometry.White,
.{ 0.0, 1.0 },
.{ 1.0, 1.0 },
.{ 1.0, 0.0 },
.{ 0.0, 0.0 },
);
}
pub fn appendPiecesGroupedFromBoard(
groups: *PieceVertexGroups,
board_rect: geometry.BoardRect,
allocator: std.mem.Allocator,
state: board.BoardState,
) !void {
try appendPiecesGroupedFromBoardExcept(groups, board_rect, allocator, state, null);
}
pub fn appendPiecesGroupedFromBoardExcept(
groups: *PieceVertexGroups,
board_rect: geometry.BoardRect,
allocator: std.mem.Allocator,
state: board.BoardState,
except_square: ?@import("board_input.zig").SquareCoord,
) !void {
for (0..8) |rank| {
for (0..8) |file| {
if (except_square) |except| {
if (except.file == file and except.rank == rank) continue;
}
const encoded = state.getSquare(@intCast(file), @intCast(rank));
const piece_group = pieceGroupFromEncoded(encoded) orelse continue;
try geometry.appendPieceQuad(
groups.group(piece_group),
board_rect,
allocator,
@as(f32, @floatFromInt(file)),
@as(f32, @floatFromInt(rank)),
);
}
}
}
pub fn appendPieceGhostAtSquare(
groups: *PieceVertexGroups,
board_rect: geometry.BoardRect,
allocator: std.mem.Allocator,
encoded: u4,
square: @import("board_input.zig").SquareCoord,
) !void {
const piece_group = pieceGroupFromEncoded(encoded) orelse return;
try geometry.appendPieceQuadWithColor(
groups.group(piece_group),
board_rect,
allocator,
@floatFromInt(square.file),
@floatFromInt(square.rank),
.{ 1.0, 1.0, 1.0, 0.35 },
);
}
pub fn appendDraggedPiece(
groups: *PieceVertexGroups,
board_rect: geometry.BoardRect,
allocator: std.mem.Allocator,
encoded: u4,
center_ndc: [2]f32,
) !void {
const piece_group = pieceGroupFromEncoded(encoded) orelse return;
try geometry.appendPieceQuadCenteredAtNdc(
groups.group(piece_group),
allocator,
center_ndc,
.{ board_rect.width / 8.0 * 0.84, board_rect.height / 8.0 * 0.84 },
geometry.White,
);
}
pub fn appendPalettePiecesGrouped(
groups: *PieceVertexGroups,
board_rect: geometry.BoardRect,
allocator: std.mem.Allocator,
) !void {
const palette_rect = paletteRectForBoard(board_rect);
const cell_height = palette_rect.height / @as(f32, @floatFromInt(palette_entries.len));
for (palette_entries, 0..) |entry, i| {
const y = palette_rect.bottom + (@as(f32, @floatFromInt(palette_entries.len - 1 - i)) * cell_height);
const cell_rect = geometry.BoardRect{
.left = palette_rect.left,
.bottom = y,
.width = palette_rect.width,
.height = cell_height,
};
try appendPieceQuadInRect(groups.group(entry.group), allocator, cell_rect);
}
}
pub fn paletteIndexForEncoded(encoded: u4) ?usize {
for (palette_entries, 0..) |entry, i| {
if (entry.encoded == encoded) return i;
}
return null;
}
fn expectGroupVertexCount(groups: *const PieceVertexGroups, piece_group: PieceGroup, expected: usize) !void {
try std.testing.expectEqual(expected, groups.constGroup(piece_group).items.len);
}
test "pieceGroupFromEncoded maps encoded pieces to render groups" {
try std.testing.expectEqual(PieceGroup.white_pawn, pieceGroupFromEncoded(piece.encode(.white, .pawn)).?);
try std.testing.expectEqual(PieceGroup.white_knight, pieceGroupFromEncoded(piece.encode(.white, .knight)).?);
try std.testing.expectEqual(PieceGroup.white_bishop, pieceGroupFromEncoded(piece.encode(.white, .bishop)).?);
try std.testing.expectEqual(PieceGroup.white_rook, pieceGroupFromEncoded(piece.encode(.white, .rook)).?);
try std.testing.expectEqual(PieceGroup.white_queen, pieceGroupFromEncoded(piece.encode(.white, .queen)).?);
try std.testing.expectEqual(PieceGroup.white_king, pieceGroupFromEncoded(piece.encode(.white, .king)).?);
try std.testing.expectEqual(PieceGroup.black_pawn, pieceGroupFromEncoded(piece.encode(.black, .pawn)).?);
try std.testing.expectEqual(PieceGroup.black_knight, pieceGroupFromEncoded(piece.encode(.black, .knight)).?);
try std.testing.expectEqual(PieceGroup.black_bishop, pieceGroupFromEncoded(piece.encode(.black, .bishop)).?);
try std.testing.expectEqual(PieceGroup.black_rook, pieceGroupFromEncoded(piece.encode(.black, .rook)).?);
try std.testing.expectEqual(PieceGroup.black_queen, pieceGroupFromEncoded(piece.encode(.black, .queen)).?);
try std.testing.expectEqual(PieceGroup.black_king, pieceGroupFromEncoded(piece.encode(.black, .king)).?);
try std.testing.expectEqual(null, pieceGroupFromEncoded(0));
}
test "appendPiecesGroupedFromBoard leaves all groups empty for empty board" {
const state = board.BoardState.empty();
const board_rect = geometry.boardRectForExtent(800, 600);
var groups = PieceVertexGroups.init();
defer groups.deinit(std.testing.allocator);
try appendPiecesGroupedFromBoard(&groups, board_rect, std.testing.allocator, state);
for (groups.groups) |vertex_group| {
try std.testing.expectEqual(@as(usize, 0), vertex_group.items.len);
}
}
test "appendPalettePiecesGrouped adds one piece to each piece group" {
const board_rect = geometry.boardRectForExtent(800, 600);
var groups = PieceVertexGroups.init();
defer groups.deinit(std.testing.allocator);
try appendPalettePiecesGrouped(&groups, board_rect, std.testing.allocator);
for (groups.groups) |vertex_group| {
try std.testing.expectEqual(@as(usize, 6), vertex_group.items.len);
}
}
test "appendPiecesGroupedFromBoard groups starting position vertices by piece" {
const state = try fen.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1");
const board_rect = geometry.boardRectForExtent(800, 600);
var groups = PieceVertexGroups.init();
defer groups.deinit(std.testing.allocator);
try appendPiecesGroupedFromBoard(&groups, board_rect, std.testing.allocator, state);
try expectGroupVertexCount(&groups, .white_pawn, 8 * 6);
try expectGroupVertexCount(&groups, .white_knight, 2 * 6);
try expectGroupVertexCount(&groups, .white_bishop, 2 * 6);
try expectGroupVertexCount(&groups, .white_rook, 2 * 6);
try expectGroupVertexCount(&groups, .white_queen, 1 * 6);
try expectGroupVertexCount(&groups, .white_king, 1 * 6);
try expectGroupVertexCount(&groups, .black_pawn, 8 * 6);
try expectGroupVertexCount(&groups, .black_knight, 2 * 6);
try expectGroupVertexCount(&groups, .black_bishop, 2 * 6);
try expectGroupVertexCount(&groups, .black_rook, 2 * 6);
try expectGroupVertexCount(&groups, .black_queen, 1 * 6);
try expectGroupVertexCount(&groups, .black_king, 1 * 6);
var total_vertices: usize = 0;
for (groups.groups) |vertex_group| {
total_vertices += vertex_group.items.len;
}
try std.testing.expectEqual(@as(usize, 32 * 6), total_vertices);
}