Add promotion selection and square APIs

This commit is contained in:
WayfinderAK 2026-05-20 13:59:57 -08:00
parent 4131c93f99
commit 145bc948dd
No known key found for this signature in database
7 changed files with 1989 additions and 421 deletions

View File

@ -12,8 +12,8 @@ pub const SquareCoord = struct {
};
pub const MoveRequest = struct {
from: SquareCoord,
to: SquareCoord,
from: bitboard.Square,
to: bitboard.Square,
};
pub const ClickResult = union(enum) {
@ -110,7 +110,7 @@ pub const SelectionState = struct {
return .cleared;
};
const clicked_piece = state.getSquare(square.file, square.rank);
const clicked_piece = state.getSquare(coordToSquare(square));
const clicked_color = piece.colorOf(clicked_piece);
if (self.selected) |from| {
@ -126,8 +126,8 @@ pub const SelectionState = struct {
self.selected = null;
return .{ .move_requested = .{
.from = from,
.to = square,
.from = coordToSquare(from),
.to = coordToSquare(square),
} };
}
@ -152,11 +152,8 @@ fn hasDraggedFarEnough(start: [2]f32, current: [2]f32) bool {
return (dx * dx + dy * dy) >= threshold * threshold;
}
pub fn hoveredSquareForState(state: InteractionState, mode: InputMode, cursor_square: ?SquareCoord) ?SquareCoord {
if (state.selection.selected != null or state.selected_palette_piece != null or state.current_drag != null or mode == .edit) {
return cursor_square;
}
return null;
pub fn hoveredSquareForState(_: InteractionState, _: InputMode, cursor_square: ?SquareCoord) ?SquareCoord {
return cursor_square;
}
pub fn handleDragUpdate(state: *InteractionState, cursor_ndc: ?[2]f32) DragUpdateResult {
@ -231,7 +228,7 @@ pub fn handleMousePress(
const square = cursor_square orelse return .none;
const ndc = cursor_ndc orelse return .none;
const encoded = board_state.getSquare(square.file, square.rank);
const encoded = board_state.getSquare(coordToSquare(square));
const previous_selection = input.selection.selected;
if (piece.colorOf(encoded) == board_state.turn) {
@ -273,7 +270,7 @@ pub fn handleMouseRelease(input: *InteractionState, cursor_square: ?SquareCoord)
}
input.selection.selected = null;
return .{ .drag_move_requested = .{ .from = drag.source, .to = target } };
return .{ .drag_move_requested = .{ .from = coordToSquare(drag.source), .to = coordToSquare(target) } };
}
input.selection.selected = drag.previous_selection;
@ -331,6 +328,29 @@ pub fn screenToBoardSquare(
};
}
pub fn screenToPromotionPiece(
mouse_x: f64,
mouse_y: f64,
window_width: u32,
window_height: u32,
board_rect: geometry.BoardRect,
square: bitboard.Square,
) ?piece.PieceType {
const ndc = screenToNdc(mouse_x, mouse_y, window_width, window_height) orelse return null;
const popup_rect = piece_render.promotionPopupRectForSquare(board_rect, square);
const right = popup_rect.left + popup_rect.width;
const top = popup_rect.bottom + popup_rect.height;
if (ndc[0] < popup_rect.left or ndc[0] >= right) return null;
if (ndc[1] < popup_rect.bottom or ndc[1] >= top) return null;
const local_x = (ndc[0] - popup_rect.left) / popup_rect.width;
const index_float = local_x * @as(f32, @floatFromInt(piece_render.promotion_piece_types.len));
const index: usize = @intFromFloat(@floor(index_float));
if (index >= piece_render.promotion_piece_types.len) return null;
return piece_render.promotion_piece_types[index];
}
pub fn screenToModeButton(
mouse_x: f64,
mouse_y: f64,
@ -438,7 +458,7 @@ test "screenToPalettePiece maps palette cells to encoded pieces" {
test "SelectionState selects side-to-move piece and requests move on second click" {
var state = chess_board.BoardState.empty();
state.turn = .white;
state.setSquare(4, 1, piece.encode(.white, .pawn));
state.setSquare(12, piece.encode(.white, .pawn));
var selection = SelectionState{};
@ -449,7 +469,7 @@ test "SelectionState selects side-to-move piece and requests move on second clic
try std.testing.expectEqual(SquareCoord{ .file = 4, .rank = 1 }, selection.selected.?);
try std.testing.expectEqual(
ClickResult{ .move_requested = .{ .from = .{ .file = 4, .rank = 1 }, .to = .{ .file = 4, .rank = 3 } } },
ClickResult{ .move_requested = .{ .from = 12, .to = 28 } },
selection.handleClick(state, .{ .file = 4, .rank = 3 }),
);
try std.testing.expectEqual(null, selection.selected);
@ -458,7 +478,7 @@ test "SelectionState selects side-to-move piece and requests move on second clic
test "SelectionState ignores pieces that are not side to move" {
var state = chess_board.BoardState.empty();
state.turn = .white;
state.setSquare(4, 6, piece.encode(.black, .pawn));
state.setSquare(52, piece.encode(.black, .pawn));
var selection = SelectionState{};
@ -469,7 +489,7 @@ test "SelectionState ignores pieces that are not side to move" {
test "SelectionState deselects when selected square is clicked again" {
var state = chess_board.BoardState.empty();
state.turn = .white;
state.setSquare(4, 1, piece.encode(.white, .pawn));
state.setSquare(12, piece.encode(.white, .pawn));
var selection = SelectionState{};
@ -483,8 +503,8 @@ test "SelectionState deselects when selected square is clicked again" {
test "SelectionState switches selection when same-color piece is clicked" {
var state = chess_board.BoardState.empty();
state.turn = .white;
state.setSquare(4, 1, piece.encode(.white, .pawn));
state.setSquare(6, 0, piece.encode(.white, .knight));
state.setSquare(12, piece.encode(.white, .pawn));
state.setSquare(6, piece.encode(.white, .knight));
var selection = SelectionState{};

File diff suppressed because it is too large Load Diff

View File

@ -57,7 +57,7 @@ pub fn parseBoardPlacement(state: *board.BoardState, placement: []const u8) !voi
=> {
if (file >= 8) return error.InvalidRankWidth;
const p = try piece.fromFENChar(c);
state.setSquare(@intCast(file), rank, p);
state.setSquare((@as(u6, rank) * 8) + file, p);
file += 1;
},
else => return error.InvalidFenCharacter,
@ -128,42 +128,6 @@ pub fn parseCastleRights(state: *board.BoardState, castle_string: []const u8) !v
state.castle_rights = rights;
}
pub fn isEnPassantCapturable(state: *board.BoardState, ep_square: u6) bool {
const file: u3 = @intCast(ep_square % 8);
const rank: u3 = @intCast((ep_square / 8) + 1);
var pawn_rank: u3 = 0;
if (state.turn == piece.Color.white) {
if (rank != 6) return false;
pawn_rank = 4;
} else {
if (rank != 3) return false;
pawn_rank = 3;
}
var p: u4 = 0;
if (file == 0) {
p = state.getSquare(file + 1, pawn_rank);
if (piece.typeOf(p) == piece.PieceType.pawn and piece.colorOf(p) == state.turn) {
return true;
}
} else if (file == 7) {
p = state.getSquare(file - 1, pawn_rank);
if (piece.typeOf(p) == piece.PieceType.pawn and piece.colorOf(p) == state.turn) {
return true;
}
} else {
p = state.getSquare(file - 1, pawn_rank);
if (piece.typeOf(p) == piece.PieceType.pawn and piece.colorOf(p) == state.turn) {
return true;
}
p = state.getSquare(file + 1, pawn_rank);
if (piece.typeOf(p) == piece.PieceType.pawn and piece.colorOf(p) == state.turn) {
return true;
}
}
return false;
}
pub fn parseEnPassant(state: *board.BoardState, ep_string: []const u8) !void {
if (std.mem.eql(u8, ep_string, "-")) {
state.en_passant = 0;
@ -173,11 +137,7 @@ pub fn parseEnPassant(state: *board.BoardState, ep_string: []const u8) !void {
const ep_square = try board.parseSquareFromAlgebraic(ep_string);
if (ep_string[1] != '3' and ep_string[1] != '6') return error.InvalidEnPassantSquare;
if (isEnPassantCapturable(state, ep_square)) {
state.en_passant = (1 << 6) | @as(u7, ep_square);
} else {
state.en_passant = 0;
}
state.en_passant = (1 << 6) | @as(u7, ep_square);
}
fn pieceToFenChar(encoded: u4) !u8 {
@ -202,7 +162,8 @@ fn appendBoardPlacement(allocator: std.mem.Allocator, out: *std.ArrayList(u8), s
var file: u4 = 0;
while (file < 8) : (file += 1) {
const square = state.getSquare(@intCast(file), @intCast(rank));
const square_index: u6 = @intCast((@as(u6, @intCast(rank)) * 8) + @as(u6, @intCast(file)));
const square = state.getSquare(square_index);
if (square == 0) {
empty_count += 1;
continue;
@ -279,6 +240,36 @@ pub fn formatFen(allocator: std.mem.Allocator, state: board.BoardState) ![]u8 {
return try out.toOwnedSlice(allocator);
}
fn expectStartingBoardPieces(state: board.BoardState) !void {
try std.testing.expectEqual(piece.encode(.black, .rook), state.getSquare(56));
try std.testing.expectEqual(piece.encode(.black, .knight), state.getSquare(57));
try std.testing.expectEqual(piece.encode(.black, .bishop), state.getSquare(58));
try std.testing.expectEqual(piece.encode(.black, .queen), state.getSquare(59));
try std.testing.expectEqual(piece.encode(.black, .king), state.getSquare(60));
try std.testing.expectEqual(piece.encode(.black, .bishop), state.getSquare(61));
try std.testing.expectEqual(piece.encode(.black, .knight), state.getSquare(62));
try std.testing.expectEqual(piece.encode(.black, .rook), state.getSquare(63));
for (8..16) |square| {
try std.testing.expectEqual(piece.encode(.white, .pawn), state.getSquare(@intCast(square)));
}
for (16..48) |square| {
try std.testing.expectEqual(@as(u4, 0), state.getSquare(@intCast(square)));
}
for (48..56) |square| {
try std.testing.expectEqual(piece.encode(.black, .pawn), state.getSquare(@intCast(square)));
}
try std.testing.expectEqual(piece.encode(.white, .rook), state.getSquare(0));
try std.testing.expectEqual(piece.encode(.white, .knight), state.getSquare(1));
try std.testing.expectEqual(piece.encode(.white, .bishop), state.getSquare(2));
try std.testing.expectEqual(piece.encode(.white, .queen), state.getSquare(3));
try std.testing.expectEqual(piece.encode(.white, .king), state.getSquare(4));
try std.testing.expectEqual(piece.encode(.white, .bishop), state.getSquare(5));
try std.testing.expectEqual(piece.encode(.white, .knight), state.getSquare(6));
try std.testing.expectEqual(piece.encode(.white, .rook), state.getSquare(7));
}
test "parseBoardPlacement parses starting position" {
var state = board.BoardState.empty();
try parseBoardPlacement(
@ -286,14 +277,7 @@ test "parseBoardPlacement parses starting position" {
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR",
);
try std.testing.expectEqual(@as(u32, 0xCABEDBAC), state.board[0]);
try std.testing.expectEqual(@as(u32, 0x99999999), state.board[1]);
try std.testing.expectEqual(@as(u32, 0x00000000), state.board[2]);
try std.testing.expectEqual(@as(u32, 0x00000000), state.board[3]);
try std.testing.expectEqual(@as(u32, 0x00000000), state.board[4]);
try std.testing.expectEqual(@as(u32, 0x00000000), state.board[5]);
try std.testing.expectEqual(@as(u32, 0x11111111), state.board[6]);
try std.testing.expectEqual(@as(u32, 0x42365324), state.board[7]);
try expectStartingBoardPieces(state);
}
test "parseBoardPlacement rejects invalid rank counts" {
@ -426,22 +410,22 @@ test "parseEnPassant parses no en passant target" {
try std.testing.expectEqual(@as(u7, 0), state.en_passant);
}
test "parseEnPassant stores zero when target is not capturable" {
test "parseEnPassant stores syntactically valid target even when not capturable" {
var state = board.BoardState.empty();
state.turn = .black;
try parseEnPassant(&state, "e3");
try std.testing.expectEqual(@as(u7, 0), state.en_passant);
try std.testing.expectEqual(@as(u7, 84), state.en_passant);
state.turn = .white;
try parseEnPassant(&state, "e6");
try std.testing.expectEqual(@as(u7, 0), state.en_passant);
try std.testing.expectEqual(@as(u7, 108), state.en_passant);
}
test "parseEnPassant stores rank 6 target capturable by white pawn" {
var state = board.BoardState.empty();
state.turn = .white;
state.setSquare(3, 4, piece.encode(.white, .pawn)); // d5 can capture e6
state.setSquare((@as(u6, @intCast(4)) * 8) + @as(u6, @intCast(3)), piece.encode(.white, .pawn)); // d5 can capture e6
try parseEnPassant(&state, "e6");
try std.testing.expectEqual(@as(u7, 108), state.en_passant);
@ -450,7 +434,7 @@ test "parseEnPassant stores rank 6 target capturable by white pawn" {
test "parseEnPassant stores rank 3 target capturable by black pawn" {
var state = board.BoardState.empty();
state.turn = .black;
state.setSquare(5, 3, piece.encode(.black, .pawn)); // f4 can capture e3
state.setSquare((@as(u6, @intCast(3)) * 8) + @as(u6, @intCast(5)), piece.encode(.black, .pawn)); // f4 can capture e3
try parseEnPassant(&state, "e3");
try std.testing.expectEqual(@as(u7, 84), state.en_passant);
@ -459,26 +443,26 @@ test "parseEnPassant stores rank 3 target capturable by black pawn" {
test "parseEnPassant handles capturable edge-file targets" {
var white_state = board.BoardState.empty();
white_state.turn = .white;
white_state.setSquare(1, 4, piece.encode(.white, .pawn)); // b5 can capture a6
white_state.setSquare((@as(u6, @intCast(4)) * 8) + @as(u6, @intCast(1)), piece.encode(.white, .pawn)); // b5 can capture a6
try parseEnPassant(&white_state, "a6");
try std.testing.expectEqual(@as(u7, 104), white_state.en_passant);
var black_state = board.BoardState.empty();
black_state.turn = .black;
black_state.setSquare(6, 3, piece.encode(.black, .pawn)); // g4 can capture h3
black_state.setSquare((@as(u6, @intCast(3)) * 8) + @as(u6, @intCast(6)), piece.encode(.black, .pawn)); // g4 can capture h3
try parseEnPassant(&black_state, "h3");
try std.testing.expectEqual(@as(u7, 87), black_state.en_passant);
}
test "parseEnPassant ignores adjacent pawn of wrong color" {
test "parseEnPassant stores target even if adjacent pawn is wrong color" {
var state = board.BoardState.empty();
state.turn = .white;
state.setSquare(3, 4, piece.encode(.black, .pawn));
state.setSquare((@as(u6, @intCast(4)) * 8) + @as(u6, @intCast(3)), piece.encode(.black, .pawn));
try parseEnPassant(&state, "e6");
try std.testing.expectEqual(@as(u7, 0), state.en_passant);
try std.testing.expectEqual(@as(u7, 108), state.en_passant);
}
test "parseEnPassant rejects invalid target squares" {
@ -502,14 +486,7 @@ test "parseEnPassant rejects target squares outside ranks 3 and 6" {
test "parseFen parses starting position" {
const state = try parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1");
try std.testing.expectEqual(@as(u32, 0xCABEDBAC), state.board[0]);
try std.testing.expectEqual(@as(u32, 0x99999999), state.board[1]);
try std.testing.expectEqual(@as(u32, 0x00000000), state.board[2]);
try std.testing.expectEqual(@as(u32, 0x00000000), state.board[3]);
try std.testing.expectEqual(@as(u32, 0x00000000), state.board[4]);
try std.testing.expectEqual(@as(u32, 0x00000000), state.board[5]);
try std.testing.expectEqual(@as(u32, 0x11111111), state.board[6]);
try std.testing.expectEqual(@as(u32, 0x42365324), state.board[7]);
try expectStartingBoardPieces(state);
try std.testing.expectEqual(piece.Color.white, state.turn);
try std.testing.expectEqual(@as(u4, 0b1111), state.castle_rights);
try std.testing.expectEqual(@as(u7, 0), state.en_passant);
@ -523,20 +500,20 @@ test "parseFen parses capturable en passant position" {
try std.testing.expectEqual(piece.Color.white, state.turn);
try std.testing.expectEqual(@as(u4, 0), state.castle_rights);
try std.testing.expectEqual(@as(u7, 108), state.en_passant);
try std.testing.expectEqual(piece.encode(.white, .pawn), state.getSquare(3, 4));
try std.testing.expectEqual(piece.encode(.black, .pawn), state.getSquare(4, 4));
try std.testing.expectEqual(piece.encode(.white, .pawn), state.getSquare(35));
try std.testing.expectEqual(piece.encode(.black, .pawn), state.getSquare(36));
}
test "parseFen stores zero for non-capturable en passant target" {
test "parseFen preserves non-capturable en passant target" {
const state = try parseFen("r1bqkbnr/pppp1ppp/2n5/4p3/3P4/5N2/PPP2PPP/RNBQKB1R w KQkq e3 4 5");
try std.testing.expectEqual(piece.Color.white, state.turn);
try std.testing.expectEqual(@as(u4, 0b1111), state.castle_rights);
try std.testing.expectEqual(@as(u7, 0), state.en_passant);
try std.testing.expectEqual(@as(u7, 84), state.en_passant);
try std.testing.expectEqual(@as(u8, 4), state.halfmove);
try std.testing.expectEqual(@as(u32, 5), state.fullmove);
try std.testing.expectEqual(piece.encode(.white, .pawn), state.getSquare(3, 3));
try std.testing.expectEqual(piece.encode(.black, .pawn), state.getSquare(4, 4));
try std.testing.expectEqual(piece.encode(.white, .pawn), state.getSquare(27));
try std.testing.expectEqual(piece.encode(.black, .pawn), state.getSquare(36));
}
test "parseFen rejects invalid field counts" {
@ -571,9 +548,9 @@ test "formatFen formats capturable en passant target" {
try std.testing.expectEqualStrings(expected, actual);
}
test "formatFen formats non-capturable en passant as dash" {
test "formatFen preserves non-capturable en passant target" {
const input = "r1bqkbnr/pppp1ppp/2n5/4p3/3P4/5N2/PPP2PPP/RNBQKB1R w KQkq e3 4 5";
const expected = "r1bqkbnr/pppp1ppp/2n5/4p3/3P4/5N2/PPP2PPP/RNBQKB1R w KQkq - 4 5";
const expected = "r1bqkbnr/pppp1ppp/2n5/4p3/3P4/5N2/PPP2PPP/RNBQKB1R w KQkq e3 4 5";
const state = try parseFen(input);
const actual = try formatFen(std.testing.allocator, state);
@ -582,16 +559,67 @@ test "formatFen formats non-capturable en passant as dash" {
try std.testing.expectEqualStrings(expected, actual);
}
test "formatFen includes promoted white pieces" {
const cases = [_]struct {
promotion_type: piece.PieceType,
expected: []const u8,
}{
.{ .promotion_type = .queen, .expected = "4Q3/8/8/8/8/8/8/8 b - - 0 1" },
.{ .promotion_type = .rook, .expected = "4R3/8/8/8/8/8/8/8 b - - 0 1" },
.{ .promotion_type = .bishop, .expected = "4B3/8/8/8/8/8/8/8 b - - 0 1" },
.{ .promotion_type = .knight, .expected = "4N3/8/8/8/8/8/8/8 b - - 0 1" },
};
for (cases) |case| {
var state = board.BoardState.empty();
state.fullmove = 1;
state.setSquare(52, piece.encode(.white, .pawn));
try state.move(52, 60, case.promotion_type);
const actual = try formatFen(std.testing.allocator, state);
defer std.testing.allocator.free(actual);
try std.testing.expectEqualStrings(case.expected, actual);
}
}
test "formatFen includes promoted black pieces" {
const cases = [_]struct {
promotion_type: piece.PieceType,
expected: []const u8,
}{
.{ .promotion_type = .queen, .expected = "8/8/8/8/8/8/8/3q4 w - - 0 8" },
.{ .promotion_type = .rook, .expected = "8/8/8/8/8/8/8/3r4 w - - 0 8" },
.{ .promotion_type = .bishop, .expected = "8/8/8/8/8/8/8/3b4 w - - 0 8" },
.{ .promotion_type = .knight, .expected = "8/8/8/8/8/8/8/3n4 w - - 0 8" },
};
for (cases) |case| {
var state = board.BoardState.empty();
state.turn = .black;
state.fullmove = 7;
state.setSquare(11, piece.encode(.black, .pawn));
try state.move(11, 3, case.promotion_type);
const actual = try formatFen(std.testing.allocator, state);
defer std.testing.allocator.free(actual);
try std.testing.expectEqualStrings(case.expected, actual);
}
}
test "formatFen formats black turn no castling and larger counters" {
var state = board.BoardState.empty();
state.turn = .black;
state.castle_rights = 0;
state.halfmove = 42;
state.fullmove = 300;
state.setSquare(4, 0, piece.encode(.white, .king));
state.setSquare(4, 7, piece.encode(.black, .king));
state.setSquare(0, 0, piece.encode(.white, .rook));
state.setSquare(7, 7, piece.encode(.black, .rook));
state.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(4)), piece.encode(.white, .king));
state.setSquare((@as(u6, @intCast(7)) * 8) + @as(u6, @intCast(4)), piece.encode(.black, .king));
state.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(0)), piece.encode(.white, .rook));
state.setSquare((@as(u6, @intCast(7)) * 8) + @as(u6, @intCast(7)), piece.encode(.black, .rook));
const actual = try formatFen(std.testing.allocator, state);
defer std.testing.allocator.free(actual);

View File

@ -234,7 +234,7 @@ pub fn appendPiecesFromBoard(
) !void {
for (0..8) |rank| {
for (0..8) |file| {
const p = state.getSquare(@intCast(file), @intCast(rank));
const p = state.getSquare(@intCast((rank * 8) + file));
if (piece.typeOf(p) != piece.PieceType.none) {
try appendPieceQuad(vertices, board_rect, allocator, @floatFromInt(file), @floatFromInt(rank));
}

View File

@ -64,8 +64,73 @@ const PendingDragState = board_input.PendingDragState;
const DragState = board_input.DragState;
const AppMode = board_input.InputMode;
const PromotionPopupState = struct {
color: chess_piece.Color,
from: bitboard.Square,
to: bitboard.Square,
};
const PendingPromotion = struct {
move: board_input.MoveRequest,
color: chess_piece.Color,
fn popup(self: PendingPromotion) PromotionPopupState {
return .{ .color = self.color, .from = self.move.from, .to = self.move.to };
}
};
var framebuffer_resized = false;
const GameStatus = struct {
checked_king: ?board_input.SquareCoord = null,
winning_king: ?board_input.SquareCoord = null,
losing_king: ?board_input.SquareCoord = null,
checkmate: bool = false,
stalemate: bool = false,
};
fn oppositeColor(color: chess_piece.Color) chess_piece.Color {
return switch (color) {
.white => .black,
.black => .white,
};
}
fn kingSquare(state: chess_board.BoardState, color: chess_piece.Color) ?board_input.SquareCoord {
const king_bb = state.bitboards[chess_piece.encode(color, .king)];
if (king_bb == 0) return null;
return board_input.squareToCoord(@intCast(@ctz(king_bb)));
}
fn promotionColorForMove(state: chess_board.BoardState, move: board_input.MoveRequest) ?chess_piece.Color {
const moving_piece = state.getSquare(move.from);
if (chess_piece.typeOf(moving_piece) != .pawn) return null;
const color = chess_piece.colorOf(moving_piece) orelse return null;
const target_rank = move.to / 8;
return switch (color) {
.white => if (target_rank == 7) color else null,
.black => if (target_rank == 0) color else null,
};
}
fn gameStatusForTurn(state: chess_board.BoardState) GameStatus {
var state_copy = state;
const side_to_move = state_copy.turn;
const in_check = state_copy.isInCheck(side_to_move);
const checkmate = state_copy.isCheckmate(side_to_move);
const stalemate = state_copy.isStalemate(side_to_move);
const winner = oppositeColor(side_to_move);
return .{
.checked_king = if (in_check) kingSquare(state, side_to_move) else null,
.winning_king = if (checkmate) kingSquare(state, winner) else null,
.losing_king = if (checkmate) kingSquare(state, side_to_move) else null,
.checkmate = checkmate,
.stalemate = stalemate,
};
}
fn framebufferSizeCallback(_: *glfw.Window, _: c_int, _: c_int) callconv(.c) void {
framebuffer_resized = true;
}
@ -84,23 +149,23 @@ fn destroyPieceVertexBuffers(
fn pieceAsset(group: piece_render.PieceGroup) []const u8 {
return switch (group) {
.white_pawn => assets.white_pawn_rgba,
.white_knight => assets.white_knight_rgba,
.white_bishop => assets.white_bishop_rgba,
.white_rook => assets.white_rook_rgba,
.white_queen => assets.white_queen_rgba,
.white_king => assets.white_king_rgba,
.black_pawn => assets.black_pawn_rgba,
.black_knight => assets.black_knight_rgba,
.black_bishop => assets.black_bishop_rgba,
.black_rook => assets.black_rook_rgba,
.black_queen => assets.black_queen_rgba,
.black_king => assets.black_king_rgba,
.white_pawn, .dragged_white_pawn => assets.white_pawn_rgba,
.white_knight, .dragged_white_knight => assets.white_knight_rgba,
.white_bishop, .dragged_white_bishop => assets.white_bishop_rgba,
.white_rook, .dragged_white_rook => assets.white_rook_rgba,
.white_queen, .dragged_white_queen => assets.white_queen_rgba,
.white_king, .dragged_white_king => assets.white_king_rgba,
.black_pawn, .dragged_black_pawn => assets.black_pawn_rgba,
.black_knight, .dragged_black_knight => assets.black_knight_rgba,
.black_bishop, .dragged_black_bishop => assets.black_bishop_rgba,
.black_rook, .dragged_black_rook => assets.black_rook_rgba,
.black_queen, .dragged_black_queen => assets.black_queen_rgba,
.black_king, .dragged_black_king => assets.black_king_rgba,
};
}
fn pieceGroupFromIndex(index: usize) piece_render.PieceGroup {
return @enumFromInt(@as(u4, @intCast(index)));
return @enumFromInt(@as(u5, @intCast(index)));
}
fn initPieceTextures(
@ -210,23 +275,48 @@ fn appendBoardAndTextVertices(
state: chess_board.BoardState,
selected: ?board_input.SquareCoord,
hovered: ?board_input.SquareCoord,
valid_moves: []const bitboard.Square,
valid_moves: bitboard.Bitboard,
selected_palette_piece: ?u4,
mode: AppMode,
promotion_popup: ?PromotionPopupState,
) !void {
try text_render.appendModeMenu(vertices, allocator, mode == .edit);
try geometry.appendChessboard(vertices, board_rect, allocator);
if (mode == .edit) try text_render.appendPalettePieceHighlight(vertices, allocator, board_rect, selected_palette_piece);
try text_render.appendSelectedSquareHighlight(vertices, allocator, board_rect, selected);
const status = gameStatusForTurn(state);
try text_render.appendHoveredSquareHighlight(vertices, allocator, board_rect, hovered);
try text_render.appendCheckBorder(vertices, allocator, board_rect, status.checked_king);
try text_render.appendCheckmateMarker(vertices, allocator, board_rect, status.winning_king);
try text_render.appendValidMoveDots(vertices, allocator, board_rect, state, valid_moves);
try text_render.appendBoardCoordinateLabels(vertices, allocator, board_rect);
if (promotion_popup) |popup| try text_render.appendPromotionPopup(vertices, allocator, board_rect, popup.to);
const fen_text = try fen.formatFen(allocator, state);
defer allocator.free(fen_text);
try text_render.appendFenText(vertices, allocator, board_rect, fen_text);
}
fn clearPiecesUnderPromotionPopup(state: *chess_board.BoardState, board_rect: geometry.BoardRect, popup: PromotionPopupState) void {
state.setSquare(popup.from, 0);
const popup_rect = piece_render.promotionPopupRectForSquare(board_rect, popup.to);
const right = popup_rect.left + popup_rect.width;
const top = popup_rect.bottom + popup_rect.height;
for (0..64) |square_index| {
const square: bitboard.Square = @intCast(square_index);
const center = geometry.boardToNdc(
board_rect,
@as(f32, @floatFromInt(square % 8)) + 0.5,
@as(f32, @floatFromInt(square / 8)) + 0.5,
);
if (center[0] >= popup_rect.left and center[0] <= right and center[1] >= popup_rect.bottom and center[1] <= top) {
state.setSquare(square, 0);
}
}
}
fn rebuildBoardAndPieceRenderResources(
vc: context_mod.VulkanContext,
ldc: device_mod.LogicalDeviceContext,
@ -240,10 +330,11 @@ fn rebuildBoardAndPieceRenderResources(
state: chess_board.BoardState,
selected: ?board_input.SquareCoord,
hovered: ?board_input.SquareCoord,
valid_moves: []const bitboard.Square,
valid_moves: bitboard.Bitboard,
selected_palette_piece: ?u4,
drag_state: ?DragState,
mode: AppMode,
promotion_popup: ?PromotionPopupState,
board_vertices: *std.ArrayList(geometry.Vertex),
piece_vertex_groups: *piece_render.PieceVertexGroups,
board_vertex_buffer_context: *buffer_mod.VertexBufferContext,
@ -253,17 +344,28 @@ fn rebuildBoardAndPieceRenderResources(
) !void {
var new_board_vertices: std.ArrayList(geometry.Vertex) = .empty;
errdefer new_board_vertices.deinit(allocator);
try appendBoardAndTextVertices(&new_board_vertices, allocator, board_rect, state, selected, hovered, valid_moves, selected_palette_piece, mode);
try appendBoardAndTextVertices(&new_board_vertices, allocator, board_rect, state, selected, hovered, valid_moves, selected_palette_piece, mode, promotion_popup);
var new_piece_vertex_groups = piece_render.PieceVertexGroups.init();
errdefer new_piece_vertex_groups.deinit(allocator);
const drag_source = if (drag_state) |drag| drag.source else null;
try piece_render.appendPiecesGroupedFromBoardExcept(&new_piece_vertex_groups, board_rect, allocator, state, drag_source);
const status = gameStatusForTurn(state);
var rendered_state = state;
if (promotion_popup) |popup| clearPiecesUnderPromotionPopup(&rendered_state, board_rect, popup);
try piece_render.appendPiecesGroupedFromBoardExceptWithGameOver(
&new_piece_vertex_groups,
board_rect,
allocator,
rendered_state,
drag_source,
status.losing_king,
);
if (drag_state) |drag| {
try piece_render.appendPieceGhostAtSquare(&new_piece_vertex_groups, board_rect, allocator, drag.encoded, drag.source);
try piece_render.appendDraggedPiece(&new_piece_vertex_groups, board_rect, allocator, drag.encoded, drag.cursor_ndc);
}
if (mode == .edit) try piece_render.appendPalettePiecesGrouped(&new_piece_vertex_groups, board_rect, allocator);
if (promotion_popup) |popup| try piece_render.appendPromotionPiecesGrouped(&new_piece_vertex_groups, board_rect, allocator, popup.color, popup.to);
const new_board_vertex_buffer_context = try buffer_mod.initVertexBuffer(vc, ldc, new_board_vertices.items);
errdefer new_board_vertex_buffer_context.destroy(&ldc);
@ -387,8 +489,16 @@ pub fn main(init: std.process.Init) !void {
var current_state = initial_state;
var input_state = board_input.InteractionState{};
try appendBoardAndTextVertices(&board_vertices, std.heap.page_allocator, board, current_state, null, null, &[_]bitboard.Square{}, input_state.selected_palette_piece, current_mode);
try piece_render.appendPiecesGroupedFromBoard(&piece_vertex_groups, board, std.heap.page_allocator, current_state);
try appendBoardAndTextVertices(&board_vertices, std.heap.page_allocator, board, current_state, null, null, 0, input_state.selected_palette_piece, current_mode, null);
const initial_status = gameStatusForTurn(current_state);
try piece_render.appendPiecesGroupedFromBoardExceptWithGameOver(
&piece_vertex_groups,
board,
std.heap.page_allocator,
current_state,
null,
initial_status.losing_king,
);
var board_vertex_buffer_context = try buffer_mod.initVertexBuffer(
vc,
@ -441,8 +551,8 @@ pub fn main(init: std.process.Init) !void {
defer sync_context.destroy(&ldc);
var hovered_square: ?board_input.SquareCoord = null;
var valid_moves: std.ArrayList(bitboard.Square) = .empty;
defer valid_moves.deinit(std.heap.page_allocator);
var valid_moves: bitboard.Bitboard = 0;
var pending_promotion: ?PendingPromotion = null;
var left_was_pressed = false;
var copy_was_pressed = false;
var paste_was_pressed = false;
@ -479,7 +589,8 @@ pub fn main(init: std.process.Init) !void {
input_state.current_drag = null;
input_state.selection.selected = null;
hovered_square = null;
valid_moves.clearRetainingCapacity();
valid_moves = 0;
pending_promotion = null;
try rebuildBoardAndPieceRenderResources(
vc,
ldc,
@ -493,10 +604,11 @@ pub fn main(init: std.process.Init) !void {
current_state,
null,
null,
valid_moves.items,
valid_moves,
input_state.selected_palette_piece,
null,
current_mode,
null,
&board_vertices,
&piece_vertex_groups,
&board_vertex_buffer_context,
@ -549,10 +661,11 @@ pub fn main(init: std.process.Init) !void {
current_state,
input_state.selection.selected,
hovered_square,
valid_moves.items,
valid_moves,
input_state.selected_palette_piece,
input_state.current_drag,
current_mode,
if (pending_promotion) |promotion| promotion.popup() else null,
&board_vertices,
&piece_vertex_groups,
&board_vertex_buffer_context,
@ -567,9 +680,8 @@ pub fn main(init: std.process.Init) !void {
switch (board_input.handleDragUpdate(&input_state, cursor_ndc)) {
.none => {},
.drag_started => |held_square| {
valid_moves.clearRetainingCapacity();
// Populate valid_moves for the held piece here.
try current_state.getValidMoves(held_square, &valid_moves, std.heap.page_allocator);
valid_moves = 0;
valid_moves = current_state.getLegalMoves(held_square);
try rebuildBoardAndPieceRenderResources(
vc,
ldc,
@ -583,10 +695,11 @@ pub fn main(init: std.process.Init) !void {
current_state,
null,
hovered_square,
valid_moves.items,
valid_moves,
input_state.selected_palette_piece,
input_state.current_drag,
current_mode,
if (pending_promotion) |promotion| promotion.popup() else null,
&board_vertices,
&piece_vertex_groups,
&board_vertex_buffer_context,
@ -609,10 +722,11 @@ pub fn main(init: std.process.Init) !void {
current_state,
null,
hovered_square,
valid_moves.items,
valid_moves,
input_state.selected_palette_piece,
input_state.current_drag,
current_mode,
if (pending_promotion) |promotion| promotion.popup() else null,
&board_vertices,
&piece_vertex_groups,
&board_vertex_buffer_context,
@ -645,7 +759,55 @@ pub fn main(init: std.process.Init) !void {
else
null;
switch (board_input.handleMousePress(
var consumed_mouse_press = false;
if (pending_promotion) |promotion| {
consumed_mouse_press = true;
if (framebuffer_size[0] > 0 and framebuffer_size[1] > 0) {
if (board_input.screenToPromotionPiece(
cursor_pos[0],
cursor_pos[1],
@intCast(framebuffer_size[0]),
@intCast(framebuffer_size[1]),
board,
promotion.move.to,
)) |promotion_type| {
try current_state.move(promotion.move.from, promotion.move.to, promotion_type);
}
}
pending_promotion = null;
input_state.selection.selected = null;
input_state.pending_drag = null;
input_state.current_drag = null;
hovered_square = null;
valid_moves = 0;
try rebuildBoardAndPieceRenderResources(
vc,
ldc,
render_pass_context,
framebuffer_context,
board_pipeline_context,
piece_pipeline_context,
descriptor_contexts,
swapchain_context,
board,
current_state,
null,
null,
valid_moves,
input_state.selected_palette_piece,
null,
current_mode,
null,
&board_vertices,
&piece_vertex_groups,
&board_vertex_buffer_context,
&piece_vertex_buffers,
&command_context,
std.heap.page_allocator,
);
}
if (!consumed_mouse_press) switch (board_input.handleMousePress(
&input_state,
current_state,
current_mode,
@ -658,7 +820,7 @@ pub fn main(init: std.process.Init) !void {
.mode_changed => |new_mode| {
current_mode = new_mode;
hovered_square = null;
valid_moves.clearRetainingCapacity();
valid_moves = 0;
board = geometry.boardRectForExtentWithPalette(
@intCast(framebuffer_size[0]),
@intCast(framebuffer_size[1]),
@ -677,10 +839,11 @@ pub fn main(init: std.process.Init) !void {
current_state,
null,
null,
valid_moves.items,
valid_moves,
input_state.selected_palette_piece,
null,
current_mode,
if (pending_promotion) |promotion| promotion.popup() else null,
&board_vertices,
&piece_vertex_groups,
&board_vertex_buffer_context,
@ -696,7 +859,7 @@ pub fn main(init: std.process.Init) !void {
input_state.pending_drag = null;
input_state.current_drag = null;
hovered_square = null;
valid_moves.clearRetainingCapacity();
valid_moves = 0;
try rebuildBoardAndPieceRenderResources(
vc,
ldc,
@ -710,10 +873,11 @@ pub fn main(init: std.process.Init) !void {
current_state,
null,
null,
valid_moves.items,
valid_moves,
input_state.selected_palette_piece,
null,
current_mode,
if (pending_promotion) |promotion| promotion.popup() else null,
&board_vertices,
&piece_vertex_groups,
&board_vertex_buffer_context,
@ -724,7 +888,7 @@ pub fn main(init: std.process.Init) !void {
},
.palette_changed => {
hovered_square = null;
valid_moves.clearRetainingCapacity();
valid_moves = 0;
try rebuildBoardAndPieceRenderResources(
vc,
ldc,
@ -738,10 +902,11 @@ pub fn main(init: std.process.Init) !void {
current_state,
null,
null,
valid_moves.items,
valid_moves,
input_state.selected_palette_piece,
null,
current_mode,
if (pending_promotion) |promotion| promotion.popup() else null,
&board_vertices,
&piece_vertex_groups,
&board_vertex_buffer_context,
@ -751,7 +916,7 @@ pub fn main(init: std.process.Init) !void {
);
},
.edit_place => |place| {
current_state.setSquare(place.square.file, place.square.rank, place.encoded);
current_state.setSquare((@as(u6, place.square.rank) * 8) + place.square.file, place.encoded);
try rebuildBoardAndPieceRenderResources(
vc,
ldc,
@ -765,10 +930,11 @@ pub fn main(init: std.process.Init) !void {
current_state,
null,
hovered_square,
valid_moves.items,
valid_moves,
input_state.selected_palette_piece,
null,
current_mode,
if (pending_promotion) |promotion| promotion.popup() else null,
&board_vertices,
&piece_vertex_groups,
&board_vertex_buffer_context,
@ -778,7 +944,7 @@ pub fn main(init: std.process.Init) !void {
);
},
.edit_clear => |square| {
current_state.setSquare(square.file, square.rank, 0);
current_state.setSquare((@as(u6, square.rank) * 8) + square.file, 0);
try rebuildBoardAndPieceRenderResources(
vc,
ldc,
@ -792,10 +958,11 @@ pub fn main(init: std.process.Init) !void {
current_state,
null,
hovered_square,
valid_moves.items,
valid_moves,
input_state.selected_palette_piece,
null,
current_mode,
if (pending_promotion) |promotion| promotion.popup() else null,
&board_vertices,
&piece_vertex_groups,
&board_vertex_buffer_context,
@ -807,7 +974,7 @@ pub fn main(init: std.process.Init) !void {
.play_cleared => {
log.debug("selection cleared", .{});
hovered_square = null;
valid_moves.clearRetainingCapacity();
valid_moves = 0;
try rebuildBoardAndPieceRenderResources(
vc,
ldc,
@ -821,10 +988,11 @@ pub fn main(init: std.process.Init) !void {
current_state,
null,
null,
valid_moves.items,
valid_moves,
input_state.selected_palette_piece,
null,
current_mode,
if (pending_promotion) |promotion| promotion.popup() else null,
&board_vertices,
&piece_vertex_groups,
&board_vertex_buffer_context,
@ -837,9 +1005,8 @@ pub fn main(init: std.process.Init) !void {
const selected_coord = board_input.squareToCoord(selected_square);
log.debug("selected square file={} rank={}", .{ selected_coord.file, selected_coord.rank });
hovered_square = selected_coord;
valid_moves.clearRetainingCapacity();
// Populate valid_moves for the selected piece here.
try current_state.getValidMoves(selected_square, &valid_moves, std.heap.page_allocator);
valid_moves = 0;
valid_moves = current_state.getLegalMoves(selected_square);
try rebuildBoardAndPieceRenderResources(
vc,
ldc,
@ -853,10 +1020,11 @@ pub fn main(init: std.process.Init) !void {
current_state,
selected_coord,
hovered_square,
valid_moves.items,
valid_moves,
input_state.selected_palette_piece,
null,
current_mode,
if (pending_promotion) |promotion| promotion.popup() else null,
&board_vertices,
&piece_vertex_groups,
&board_vertex_buffer_context,
@ -867,13 +1035,21 @@ pub fn main(init: std.process.Init) !void {
},
.play_move_requested => |move| {
log.debug(
"move requested from file={} rank={} to file={} rank={}",
.{ move.from.file, move.from.rank, move.to.file, move.to.rank },
"move requested from square={} to square={}",
.{ move.from, move.to },
);
// Validate move against valid_moves here before mutating board.
try current_state.move(move.from.file, move.from.rank, move.to.file, move.to.rank);
if ((valid_moves & bitboard.bit(move.to)) != 0) {
if (promotionColorForMove(current_state, move)) |promotion_color| {
pending_promotion = .{ .move = move, .color = promotion_color };
} else {
try current_state.move(move.from, move.to, null);
}
} else {
log.debug("invalid click move requested; clearing selection", .{});
}
input_state.selection.selected = null;
hovered_square = null;
valid_moves.clearRetainingCapacity();
valid_moves = 0;
try rebuildBoardAndPieceRenderResources(
vc,
ldc,
@ -887,10 +1063,11 @@ pub fn main(init: std.process.Init) !void {
current_state,
null,
null,
valid_moves.items,
valid_moves,
input_state.selected_palette_piece,
null,
current_mode,
if (pending_promotion) |promotion| promotion.popup() else null,
&board_vertices,
&piece_vertex_groups,
&board_vertex_buffer_context,
@ -899,7 +1076,7 @@ pub fn main(init: std.process.Init) !void {
std.heap.page_allocator,
);
},
}
};
}
if (!left_is_pressed and left_was_pressed) {
@ -908,10 +1085,9 @@ pub fn main(init: std.process.Init) !void {
.drag_cancelled => |selected_after_release| {
const selected_coord_after_release = if (selected_after_release) |selected_square| board_input.squareToCoord(selected_square) else null;
hovered_square = selected_coord_after_release;
valid_moves.clearRetainingCapacity();
valid_moves = 0;
if (selected_after_release) |selected_square| {
// Repopulate valid_moves for the restored selection here if desired.
try current_state.getValidMoves(selected_square, &valid_moves, std.heap.page_allocator);
valid_moves = current_state.getLegalMoves(selected_square);
}
try rebuildBoardAndPieceRenderResources(
vc,
@ -926,10 +1102,11 @@ pub fn main(init: std.process.Init) !void {
current_state,
selected_coord_after_release,
hovered_square,
valid_moves.items,
valid_moves,
input_state.selected_palette_piece,
null,
current_mode,
if (pending_promotion) |promotion| promotion.popup() else null,
&board_vertices,
&piece_vertex_groups,
&board_vertex_buffer_context,
@ -941,9 +1118,8 @@ pub fn main(init: std.process.Init) !void {
.drag_selected => |selected_square| {
const selected_coord = board_input.squareToCoord(selected_square);
hovered_square = selected_coord;
valid_moves.clearRetainingCapacity();
// Populate valid_moves for the selected piece here.
try current_state.getValidMoves(selected_square, &valid_moves, std.heap.page_allocator);
valid_moves = 0;
valid_moves = current_state.getLegalMoves(selected_square);
try rebuildBoardAndPieceRenderResources(
vc,
ldc,
@ -957,10 +1133,11 @@ pub fn main(init: std.process.Init) !void {
current_state,
selected_coord,
hovered_square,
valid_moves.items,
valid_moves,
input_state.selected_palette_piece,
null,
current_mode,
if (pending_promotion) |promotion| promotion.popup() else null,
&board_vertices,
&piece_vertex_groups,
&board_vertex_buffer_context,
@ -971,7 +1148,7 @@ pub fn main(init: std.process.Init) !void {
},
.drag_cleared => {
hovered_square = null;
valid_moves.clearRetainingCapacity();
valid_moves = 0;
try rebuildBoardAndPieceRenderResources(
vc,
ldc,
@ -985,10 +1162,11 @@ pub fn main(init: std.process.Init) !void {
current_state,
null,
null,
valid_moves.items,
valid_moves,
input_state.selected_palette_piece,
null,
current_mode,
if (pending_promotion) |promotion| promotion.popup() else null,
&board_vertices,
&piece_vertex_groups,
&board_vertex_buffer_context,
@ -998,10 +1176,18 @@ pub fn main(init: std.process.Init) !void {
);
},
.drag_move_requested => |move| {
// Validate drag move against valid_moves here before mutating board.
try current_state.move(move.from.file, move.from.rank, move.to.file, move.to.rank);
if ((valid_moves & bitboard.bit(move.to)) != 0) {
if (promotionColorForMove(current_state, move)) |promotion_color| {
pending_promotion = .{ .move = move, .color = promotion_color };
} else {
try current_state.move(move.from, move.to, null);
}
} else {
log.debug("invalid drag move requested; clearing selection", .{});
}
input_state.selection.selected = null;
hovered_square = null;
valid_moves.clearRetainingCapacity();
valid_moves = 0;
try rebuildBoardAndPieceRenderResources(
vc,
ldc,
@ -1015,10 +1201,11 @@ pub fn main(init: std.process.Init) !void {
current_state,
null,
null,
valid_moves.items,
valid_moves,
input_state.selected_palette_piece,
null,
current_mode,
if (pending_promotion) |promotion| promotion.popup() else null,
&board_vertices,
&piece_vertex_groups,
&board_vertex_buffer_context,
@ -1052,10 +1239,11 @@ pub fn main(init: std.process.Init) !void {
current_state,
input_state.selection.selected,
hovered_square,
valid_moves.items,
valid_moves,
input_state.selected_palette_piece,
input_state.current_drag,
current_mode,
if (pending_promotion) |promotion| promotion.popup() else null,
&board_vertices,
&piece_vertex_groups,
piece_textures,
@ -1085,10 +1273,11 @@ fn recreateSwapchain(
state: chess_board.BoardState,
selected: ?board_input.SquareCoord,
hovered: ?board_input.SquareCoord,
valid_moves: []const bitboard.Square,
valid_moves: bitboard.Bitboard,
selected_palette_piece: ?u4,
drag_state: ?DragState,
mode: AppMode,
promotion_popup: ?PromotionPopupState,
board_vertices: *std.ArrayList(geometry.Vertex),
piece_vertex_groups: *piece_render.PieceVertexGroups,
piece_textures: [piece_render.PieceGroupCount]?texture_mod.TextureContext,
@ -1137,14 +1326,17 @@ fn recreateSwapchain(
new_swapchain_context.extent.height,
mode == .edit,
);
try appendBoardAndTextVertices(board_vertices, allocator, new_board, state, selected, hovered, valid_moves, selected_palette_piece, mode);
try appendBoardAndTextVertices(board_vertices, allocator, new_board, state, selected, hovered, valid_moves, selected_palette_piece, mode, promotion_popup);
const drag_source = if (drag_state) |drag| drag.source else null;
try piece_render.appendPiecesGroupedFromBoardExcept(piece_vertex_groups, new_board, allocator, state, drag_source);
var rendered_state = state;
if (promotion_popup) |popup| clearPiecesUnderPromotionPopup(&rendered_state, new_board, popup);
try piece_render.appendPiecesGroupedFromBoardExcept(piece_vertex_groups, new_board, allocator, rendered_state, drag_source);
if (drag_state) |drag| {
try piece_render.appendPieceGhostAtSquare(piece_vertex_groups, new_board, allocator, drag.encoded, drag.source);
try piece_render.appendDraggedPiece(piece_vertex_groups, new_board, allocator, drag.encoded, drag.cursor_ndc);
}
if (mode == .edit) try piece_render.appendPalettePiecesGrouped(piece_vertex_groups, new_board, allocator);
if (promotion_popup) |popup| try piece_render.appendPromotionPiecesGrouped(piece_vertex_groups, new_board, allocator, popup.color, popup.to);
const new_board_vertex_buffer_context = try buffer_mod.initVertexBuffer(vc, ldc, board_vertices.items);
errdefer new_board_vertex_buffer_context.destroy(&ldc);

View File

@ -2,10 +2,11 @@ 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(u4) {
pub const PieceGroup = enum(u5) {
white_pawn,
white_knight,
white_bishop,
@ -18,15 +19,29 @@ pub const PieceGroup = enum(u4) {
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 = 12;
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) },
@ -52,6 +67,40 @@ pub fn paletteRectForBoard(board_rect: geometry.BoardRect) geometry.BoardRect {
};
}
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),
@ -77,6 +126,14 @@ pub const PieceVertexGroups = struct {
};
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;
@ -84,26 +141,54 @@ pub fn pieceGroupFromEncoded(encoded: u4) ?PieceGroup {
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,
.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 => .black_pawn,
.knight => .black_knight,
.bishop => .black_bishop,
.rook => .black_rook,
.queen => .black_queen,
.king => .black_king,
.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,
@ -146,6 +231,17 @@ pub fn appendPiecesGroupedFromBoardExcept(
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| {
@ -153,9 +249,22 @@ pub fn appendPiecesGroupedFromBoardExcept(
if (except.file == file and except.rank == rank) continue;
}
const encoded = state.getSquare(@intCast(file), @intCast(rank));
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,
@ -163,6 +272,7 @@ pub fn appendPiecesGroupedFromBoardExcept(
@as(f32, @floatFromInt(file)),
@as(f32, @floatFromInt(rank)),
);
}
}
}
@ -192,7 +302,7 @@ pub fn appendDraggedPiece(
encoded: u4,
center_ndc: [2]f32,
) !void {
const piece_group = pieceGroupFromEncoded(encoded) orelse return;
const piece_group = draggedPieceGroupFromEncoded(encoded) orelse return;
try geometry.appendPieceQuadCenteredAtNdc(
groups.group(piece_group),
allocator,
@ -222,6 +332,27 @@ pub fn appendPalettePiecesGrouped(
}
}
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;
@ -273,11 +404,48 @@ test "appendPalettePiecesGrouped adds one piece to each piece group" {
try appendPalettePiecesGrouped(&groups, board_rect, std.testing.allocator);
for (groups.groups) |vertex_group| {
try std.testing.expectEqual(@as(usize, 6), vertex_group.items.len);
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);

View File

@ -61,6 +61,7 @@ fn glyphForChar(ch: u8) ?Glyph {
'T' => .{ 0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100 },
'Y' => .{ 0b10001, 0b10001, 0b01010, 0b00100, 0b00100, 0b00100, 0b00100 },
'#' => .{ 0b01010, 0b01010, 0b11111, 0b01010, 0b11111, 0b01010, 0b01010 },
'-' => .{ 0b00000, 0b00000, 0b00000, 0b11111, 0b00000, 0b00000, 0b00000 },
'/' => .{ 0b00001, 0b00010, 0b00010, 0b00100, 0b01000, 0b01000, 0b10000 },
else => null,
@ -124,6 +125,9 @@ pub fn appendText(
}
}
const SelectedHighlightColor = [4]f32{ 0.12, 0.55, 0.12, 0.48 };
const HoverHighlightColor = [4]f32{ 0.45, 0.90, 0.45, 0.38 };
fn appendSquareOverlay(
vertices: *std.ArrayList(geometry.Vertex),
allocator: std.mem.Allocator,
@ -152,7 +156,7 @@ pub fn appendSelectedSquareHighlight(
selected: ?board_input.SquareCoord,
) !void {
const square = selected orelse return;
try appendSquareOverlay(vertices, allocator, board_rect, square, .{ 0.45, 0.90, 0.45, 0.38 });
try appendSquareOverlay(vertices, allocator, board_rect, square, SelectedHighlightColor);
}
pub fn appendPalettePieceHighlight(
@ -185,7 +189,55 @@ pub fn appendHoveredSquareHighlight(
hovered: ?board_input.SquareCoord,
) !void {
const square = hovered orelse return;
try appendSquareOverlay(vertices, allocator, board_rect, square, .{ 0.68, 1.0, 0.68, 0.28 });
try appendSquareOverlay(vertices, allocator, board_rect, square, HoverHighlightColor);
}
pub fn appendCheckBorder(
vertices: *std.ArrayList(geometry.Vertex),
allocator: std.mem.Allocator,
board_rect: geometry.BoardRect,
checked: ?board_input.SquareCoord,
) !void {
const square = checked orelse return;
const file: f32 = @floatFromInt(square.file);
const rank: f32 = @floatFromInt(square.rank);
const thickness: f32 = 0.08;
const color = [4]f32{ 1.0, 0.05, 0.05, 0.85 };
try geometry.appendQuad(vertices, allocator, geometry.boardToNdc(board_rect, file, rank), geometry.boardToNdc(board_rect, file + 1.0, rank), geometry.boardToNdc(board_rect, file + 1.0, rank + thickness), geometry.boardToNdc(board_rect, file, rank + thickness), color);
try geometry.appendQuad(vertices, allocator, geometry.boardToNdc(board_rect, file, rank + 1.0 - thickness), geometry.boardToNdc(board_rect, file + 1.0, rank + 1.0 - thickness), geometry.boardToNdc(board_rect, file + 1.0, rank + 1.0), geometry.boardToNdc(board_rect, file, rank + 1.0), color);
try geometry.appendQuad(vertices, allocator, geometry.boardToNdc(board_rect, file, rank), geometry.boardToNdc(board_rect, file + thickness, rank), geometry.boardToNdc(board_rect, file + thickness, rank + 1.0), geometry.boardToNdc(board_rect, file, rank + 1.0), color);
try geometry.appendQuad(vertices, allocator, geometry.boardToNdc(board_rect, file + 1.0 - thickness, rank), geometry.boardToNdc(board_rect, file + 1.0, rank), geometry.boardToNdc(board_rect, file + 1.0, rank + 1.0), geometry.boardToNdc(board_rect, file + 1.0 - thickness, rank + 1.0), color);
}
pub fn appendCheckmateMarker(
vertices: *std.ArrayList(geometry.Vertex),
allocator: std.mem.Allocator,
board_rect: geometry.BoardRect,
winning_king: ?board_input.SquareCoord,
) !void {
const square = winning_king orelse return;
const square_w = board_rect.width / 8.0;
const square_h = board_rect.height / 8.0;
const pixel_size = @min(square_w, square_h) * 0.045;
const glyph_w = pixel_size * 5.0;
const glyph_h = pixel_size * 7.0;
const padding_x = square_w * 0.10;
const padding_y = square_h * 0.10;
const top_right = geometry.boardToNdc(
board_rect,
@as(f32, @floatFromInt(square.file)) + 1.0,
@as(f32, @floatFromInt(square.rank)) + 1.0,
);
try appendText(
vertices,
allocator,
"#",
top_right[0] - padding_x - glyph_w,
top_right[1] - padding_y - glyph_h,
.{ .pixel_size = pixel_size, .color = .{ 1.0, 0.92, 0.18, 1.0 } },
);
}
pub fn appendValidMoveDots(
@ -193,19 +245,22 @@ pub fn appendValidMoveDots(
allocator: std.mem.Allocator,
board_rect: geometry.BoardRect,
state: chess_board.BoardState,
valid_moves: []const bitboard.Square,
valid_moves: bitboard.Bitboard,
) !void {
const square_w = board_rect.width / 8.0;
const square_h = board_rect.height / 8.0;
const radius_x = square_w * 0.11;
const radius_y = square_h * 0.11;
const color = [4]f32{ 0.55, 0.55, 0.55, 0.55 };
const color = SelectedHighlightColor;
const segments = 48;
for (valid_moves) |move_square| {
var moves = valid_moves;
while (moves != 0) {
const move_square: bitboard.Square = @intCast(@ctz(moves));
moves &= moves - 1;
const file: u3 = @intCast(move_square % 8);
const rank: u3 = @intCast(move_square / 8);
if (state.getSquare(file, rank) != 0) {
if (state.getSquare(@intCast((@as(u6, rank) * 8) + @as(u6, file))) != 0) {
const x0 = @as(f32, @floatFromInt(file));
const x1 = x0 + 1.0;
const y0 = @as(f32, @floatFromInt(rank));
@ -238,6 +293,50 @@ pub fn appendValidMoveDots(
}
}
fn appendRectBorder(
vertices: *std.ArrayList(geometry.Vertex),
allocator: std.mem.Allocator,
rect: geometry.BoardRect,
thickness: f32,
color: [4]f32,
) !void {
const x0 = rect.left;
const x1 = rect.left + rect.width;
const y0 = rect.bottom;
const y1 = rect.bottom + rect.height;
try geometry.appendQuad(vertices, allocator, .{ x0, y0 }, .{ x1, y0 }, .{ x1, y0 + thickness }, .{ x0, y0 + thickness }, color);
try geometry.appendQuad(vertices, allocator, .{ x0, y1 - thickness }, .{ x1, y1 - thickness }, .{ x1, y1 }, .{ x0, y1 }, color);
try geometry.appendQuad(vertices, allocator, .{ x0, y0 }, .{ x0 + thickness, y0 }, .{ x0 + thickness, y1 }, .{ x0, y1 }, color);
try geometry.appendQuad(vertices, allocator, .{ x1 - thickness, y0 }, .{ x1, y0 }, .{ x1, y1 }, .{ x1 - thickness, y1 }, color);
}
pub fn appendPromotionPopup(
vertices: *std.ArrayList(geometry.Vertex),
allocator: std.mem.Allocator,
board_rect: geometry.BoardRect,
square: bitboard.Square,
) !void {
const popup_rect = piece_render.promotionPopupRectForSquare(board_rect, square);
try geometry.appendQuad(
vertices,
allocator,
.{ popup_rect.left, popup_rect.bottom },
.{ popup_rect.left + popup_rect.width, popup_rect.bottom },
.{ popup_rect.left + popup_rect.width, popup_rect.bottom + popup_rect.height },
.{ popup_rect.left, popup_rect.bottom + popup_rect.height },
.{ 0.04, 0.04, 0.04, 0.98 },
);
for (piece_render.promotion_piece_types, 0..) |_, i| {
const cell = piece_render.promotionChoiceRectForIndex(board_rect, square, i);
const border_color = geometry.White;
const thickness = board_rect.width / 220.0;
try appendRectBorder(vertices, allocator, cell, thickness, border_color);
}
}
pub fn appendModeMenu(
vertices: *std.ArrayList(geometry.Vertex),
allocator: std.mem.Allocator,
@ -411,19 +510,75 @@ test "appendHoveredSquareHighlight appends one quad when hovered" {
try std.testing.expectEqual(@as(usize, 6), vertices.items.len);
}
test "appendCheckBorder appends four border quads when checked" {
var vertices: std.ArrayList(geometry.Vertex) = .empty;
defer vertices.deinit(std.testing.allocator);
try appendCheckBorder(
&vertices,
std.testing.allocator,
geometry.boardRectForExtent(800, 600),
.{ .file = 4, .rank = 0 },
);
try std.testing.expectEqual(@as(usize, 4 * 6), vertices.items.len);
}
test "appendCheckBorder appends nothing without checked square" {
var vertices: std.ArrayList(geometry.Vertex) = .empty;
defer vertices.deinit(std.testing.allocator);
try appendCheckBorder(
&vertices,
std.testing.allocator,
geometry.boardRectForExtent(800, 600),
null,
);
try std.testing.expectEqual(@as(usize, 0), vertices.items.len);
}
test "appendCheckmateMarker appends hash glyph when winner is present" {
var vertices: std.ArrayList(geometry.Vertex) = .empty;
defer vertices.deinit(std.testing.allocator);
try appendCheckmateMarker(
&vertices,
std.testing.allocator,
geometry.boardRectForExtent(800, 600),
.{ .file = 5, .rank = 5 },
);
try std.testing.expect(vertices.items.len > 0);
}
test "appendCheckmateMarker appends nothing without winner" {
var vertices: std.ArrayList(geometry.Vertex) = .empty;
defer vertices.deinit(std.testing.allocator);
try appendCheckmateMarker(
&vertices,
std.testing.allocator,
geometry.boardRectForExtent(800, 600),
null,
);
try std.testing.expectEqual(@as(usize, 0), vertices.items.len);
}
test "appendValidMoveDots appends one circular triangle fan per move" {
var vertices: std.ArrayList(geometry.Vertex) = .empty;
defer vertices.deinit(std.testing.allocator);
const state = chess_board.BoardState.empty();
const moves = [_]bitboard.Square{ 20, 28 };
const moves = bitboard.bit(20) | bitboard.bit(28);
try appendValidMoveDots(
&vertices,
std.testing.allocator,
geometry.boardRectForExtent(800, 600),
state,
&moves,
moves,
);
try std.testing.expectEqual(@as(usize, 2 * 48 * 3), vertices.items.len);