478 lines
17 KiB
Zig
478 lines
17 KiB
Zig
const std = @import("std");
|
|
|
|
const board = @import("chess/board.zig");
|
|
const fen = @import("chess/fen.zig");
|
|
const bitboard = @import("chess/bitboard.zig");
|
|
const geometry = @import("geometry.zig");
|
|
const piece = @import("chess/piece.zig");
|
|
|
|
pub const PieceGroup = enum(u5) {
|
|
white_pawn,
|
|
white_knight,
|
|
white_bishop,
|
|
white_rook,
|
|
white_queen,
|
|
white_king,
|
|
black_pawn,
|
|
black_knight,
|
|
black_bishop,
|
|
black_rook,
|
|
black_queen,
|
|
black_king,
|
|
dragged_white_pawn,
|
|
dragged_white_knight,
|
|
dragged_white_bishop,
|
|
dragged_white_rook,
|
|
dragged_white_queen,
|
|
dragged_white_king,
|
|
dragged_black_pawn,
|
|
dragged_black_knight,
|
|
dragged_black_bishop,
|
|
dragged_black_rook,
|
|
dragged_black_queen,
|
|
dragged_black_king,
|
|
};
|
|
|
|
pub const PieceGroupCount = 24;
|
|
|
|
pub const PaletteEntry = struct {
|
|
group: PieceGroup,
|
|
encoded: u4,
|
|
};
|
|
|
|
pub const promotion_piece_types = [_]piece.PieceType{ .queen, .rook, .bishop, .knight };
|
|
|
|
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 fn promotionPopupRectForSquare(board_rect: geometry.BoardRect, square: bitboard.Square) geometry.BoardRect {
|
|
const cell_width = (board_rect.width / 8.0) * 0.65;
|
|
const cell_height = (board_rect.height / 8.0) * 0.65;
|
|
const width = cell_width * @as(f32, @floatFromInt(promotion_piece_types.len));
|
|
const height = cell_height;
|
|
const center = geometry.boardToNdc(
|
|
board_rect,
|
|
@as(f32, @floatFromInt(square % 8)) + 0.5,
|
|
@as(f32, @floatFromInt(square / 8)) + 0.5,
|
|
);
|
|
const board_right = board_rect.left + board_rect.width;
|
|
const board_top = board_rect.bottom + board_rect.height;
|
|
const unclamped_left = center[0] - (width / 2.0);
|
|
const unclamped_bottom = center[1] - (height / 2.0);
|
|
|
|
return .{
|
|
.left = @max(board_rect.left, @min(unclamped_left, board_right - width)),
|
|
.bottom = @max(board_rect.bottom, @min(unclamped_bottom, board_top - height)),
|
|
.width = width,
|
|
.height = height,
|
|
};
|
|
}
|
|
|
|
pub fn promotionChoiceRectForIndex(board_rect: geometry.BoardRect, square: bitboard.Square, index: usize) geometry.BoardRect {
|
|
const popup_rect = promotionPopupRectForSquare(board_rect, square);
|
|
const cell_width = popup_rect.width / @as(f32, @floatFromInt(promotion_piece_types.len));
|
|
return .{
|
|
.left = popup_rect.left + (@as(f32, @floatFromInt(index)) * cell_width),
|
|
.bottom = popup_rect.bottom,
|
|
.width = cell_width,
|
|
.height = popup_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 {
|
|
return pieceGroupFromEncodedWithOffset(encoded, false);
|
|
}
|
|
|
|
fn draggedPieceGroupFromEncoded(encoded: u4) ?PieceGroup {
|
|
return pieceGroupFromEncodedWithOffset(encoded, true);
|
|
}
|
|
|
|
fn pieceGroupFromEncodedWithOffset(encoded: u4, dragged: bool) ?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 => if (dragged) .dragged_white_pawn else .white_pawn,
|
|
.knight => if (dragged) .dragged_white_knight else .white_knight,
|
|
.bishop => if (dragged) .dragged_white_bishop else .white_bishop,
|
|
.rook => if (dragged) .dragged_white_rook else .white_rook,
|
|
.queen => if (dragged) .dragged_white_queen else .white_queen,
|
|
.king => if (dragged) .dragged_white_king else .white_king,
|
|
.none => null,
|
|
},
|
|
.black => switch (piece_type) {
|
|
.pawn => if (dragged) .dragged_black_pawn else .black_pawn,
|
|
.knight => if (dragged) .dragged_black_knight else .black_knight,
|
|
.bishop => if (dragged) .dragged_black_bishop else .black_bishop,
|
|
.rook => if (dragged) .dragged_black_rook else .black_rook,
|
|
.queen => if (dragged) .dragged_black_queen else .black_queen,
|
|
.king => if (dragged) .dragged_black_king else .black_king,
|
|
.none => null,
|
|
},
|
|
};
|
|
}
|
|
|
|
fn appendSidewaysPieceQuad(
|
|
vertices: *std.ArrayList(geometry.Vertex),
|
|
board_rect: geometry.BoardRect,
|
|
allocator: std.mem.Allocator,
|
|
file: f32,
|
|
rank: f32,
|
|
) !void {
|
|
const inset: f32 = 0.08;
|
|
const x0 = file + inset;
|
|
const x1 = file + 1.0 - inset;
|
|
const y0 = rank + inset;
|
|
const y1 = rank + 1.0 - inset;
|
|
|
|
try geometry.appendTexturedQuad(
|
|
vertices,
|
|
allocator,
|
|
geometry.boardToNdc(board_rect, x1, y0),
|
|
geometry.boardToNdc(board_rect, x1, y1),
|
|
geometry.boardToNdc(board_rect, x0, y1),
|
|
geometry.boardToNdc(board_rect, x0, y0),
|
|
geometry.White,
|
|
.{ 0.0, 1.0 },
|
|
.{ 1.0, 1.0 },
|
|
.{ 1.0, 0.0 },
|
|
.{ 0.0, 0.0 },
|
|
);
|
|
}
|
|
|
|
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 {
|
|
try appendPiecesGroupedFromBoardExceptWithGameOver(groups, board_rect, allocator, state, except_square, null);
|
|
}
|
|
|
|
pub fn appendPiecesGroupedFromBoardExceptWithGameOver(
|
|
groups: *PieceVertexGroups,
|
|
board_rect: geometry.BoardRect,
|
|
allocator: std.mem.Allocator,
|
|
state: board.BoardState,
|
|
except_square: ?@import("board_input.zig").SquareCoord,
|
|
losing_king_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((rank * 8) + file));
|
|
const piece_group = pieceGroupFromEncoded(encoded) orelse continue;
|
|
|
|
if (losing_king_square) |losing| {
|
|
if (losing.file == file and losing.rank == rank) {
|
|
try appendSidewaysPieceQuad(
|
|
groups.group(piece_group),
|
|
board_rect,
|
|
allocator,
|
|
@as(f32, @floatFromInt(file)),
|
|
@as(f32, @floatFromInt(rank)),
|
|
);
|
|
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 = draggedPieceGroupFromEncoded(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 appendPromotionPiecesGrouped(
|
|
groups: *PieceVertexGroups,
|
|
board_rect: geometry.BoardRect,
|
|
allocator: std.mem.Allocator,
|
|
color: piece.Color,
|
|
square: bitboard.Square,
|
|
) !void {
|
|
for (promotion_piece_types, 0..) |piece_type, i| {
|
|
const encoded = piece.encode(color, piece_type);
|
|
const piece_group = pieceGroupFromEncoded(encoded) orelse continue;
|
|
const cell_rect = promotionChoiceRectForIndex(board_rect, square, i);
|
|
const piece_rect = geometry.BoardRect{
|
|
.left = cell_rect.left + (cell_rect.width * 0.25),
|
|
.bottom = cell_rect.bottom + (cell_rect.height * 0.25),
|
|
.width = cell_rect.width * 0.5,
|
|
.height = cell_rect.height * 0.5,
|
|
};
|
|
try appendPieceQuadInRect(groups.group(piece_group), allocator, piece_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, 0..) |vertex_group, i| {
|
|
const group: PieceGroup = @enumFromInt(@as(u5, @intCast(i)));
|
|
const is_dragged_group = @intFromEnum(group) >= @intFromEnum(PieceGroup.dragged_white_pawn);
|
|
const expected: usize = if (is_dragged_group) 0 else 6;
|
|
try std.testing.expectEqual(expected, vertex_group.items.len);
|
|
}
|
|
}
|
|
|
|
test "appendDraggedPiece uses dragged overlay group" {
|
|
const board_rect = geometry.boardRectForExtent(800, 600);
|
|
|
|
var groups = PieceVertexGroups.init();
|
|
defer groups.deinit(std.testing.allocator);
|
|
|
|
try appendDraggedPiece(&groups, board_rect, std.testing.allocator, piece.encode(.white, .queen), .{ 0.0, 0.0 });
|
|
|
|
try expectGroupVertexCount(&groups, .white_queen, 0);
|
|
try expectGroupVertexCount(&groups, .dragged_white_queen, 6);
|
|
}
|
|
|
|
test "appendPiecesGroupedFromBoardExceptWithGameOver renders sideways losing king" {
|
|
var state = board.BoardState.empty();
|
|
state.setSquare(63, piece.encode(.black, .king));
|
|
state.setSquare(45, piece.encode(.white, .king));
|
|
const board_rect = geometry.boardRectForExtent(800, 600);
|
|
|
|
var groups = PieceVertexGroups.init();
|
|
defer groups.deinit(std.testing.allocator);
|
|
|
|
try appendPiecesGroupedFromBoardExceptWithGameOver(
|
|
&groups,
|
|
board_rect,
|
|
std.testing.allocator,
|
|
state,
|
|
null,
|
|
.{ .file = 7, .rank = 7 },
|
|
);
|
|
|
|
try expectGroupVertexCount(&groups, .white_king, 6);
|
|
try expectGroupVertexCount(&groups, .black_king, 6);
|
|
}
|
|
|
|
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);
|
|
}
|