Add promotion selection and square APIs
This commit is contained in:
parent
4131c93f99
commit
145bc948dd
@ -12,8 +12,8 @@ pub const SquareCoord = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub const MoveRequest = struct {
|
pub const MoveRequest = struct {
|
||||||
from: SquareCoord,
|
from: bitboard.Square,
|
||||||
to: SquareCoord,
|
to: bitboard.Square,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const ClickResult = union(enum) {
|
pub const ClickResult = union(enum) {
|
||||||
@ -110,7 +110,7 @@ pub const SelectionState = struct {
|
|||||||
return .cleared;
|
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);
|
const clicked_color = piece.colorOf(clicked_piece);
|
||||||
|
|
||||||
if (self.selected) |from| {
|
if (self.selected) |from| {
|
||||||
@ -126,8 +126,8 @@ pub const SelectionState = struct {
|
|||||||
|
|
||||||
self.selected = null;
|
self.selected = null;
|
||||||
return .{ .move_requested = .{
|
return .{ .move_requested = .{
|
||||||
.from = from,
|
.from = coordToSquare(from),
|
||||||
.to = square,
|
.to = coordToSquare(square),
|
||||||
} };
|
} };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,11 +152,8 @@ fn hasDraggedFarEnough(start: [2]f32, current: [2]f32) bool {
|
|||||||
return (dx * dx + dy * dy) >= threshold * threshold;
|
return (dx * dx + dy * dy) >= threshold * threshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hoveredSquareForState(state: InteractionState, mode: InputMode, cursor_square: ?SquareCoord) ?SquareCoord {
|
pub fn hoveredSquareForState(_: InteractionState, _: 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 cursor_square;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handleDragUpdate(state: *InteractionState, cursor_ndc: ?[2]f32) DragUpdateResult {
|
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 square = cursor_square orelse return .none;
|
||||||
const ndc = cursor_ndc 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;
|
const previous_selection = input.selection.selected;
|
||||||
|
|
||||||
if (piece.colorOf(encoded) == board_state.turn) {
|
if (piece.colorOf(encoded) == board_state.turn) {
|
||||||
@ -273,7 +270,7 @@ pub fn handleMouseRelease(input: *InteractionState, cursor_square: ?SquareCoord)
|
|||||||
}
|
}
|
||||||
|
|
||||||
input.selection.selected = null;
|
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;
|
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(
|
pub fn screenToModeButton(
|
||||||
mouse_x: f64,
|
mouse_x: f64,
|
||||||
mouse_y: 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" {
|
test "SelectionState selects side-to-move piece and requests move on second click" {
|
||||||
var state = chess_board.BoardState.empty();
|
var state = chess_board.BoardState.empty();
|
||||||
state.turn = .white;
|
state.turn = .white;
|
||||||
state.setSquare(4, 1, piece.encode(.white, .pawn));
|
state.setSquare(12, piece.encode(.white, .pawn));
|
||||||
|
|
||||||
var selection = SelectionState{};
|
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(SquareCoord{ .file = 4, .rank = 1 }, selection.selected.?);
|
||||||
|
|
||||||
try std.testing.expectEqual(
|
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 }),
|
selection.handleClick(state, .{ .file = 4, .rank = 3 }),
|
||||||
);
|
);
|
||||||
try std.testing.expectEqual(null, selection.selected);
|
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" {
|
test "SelectionState ignores pieces that are not side to move" {
|
||||||
var state = chess_board.BoardState.empty();
|
var state = chess_board.BoardState.empty();
|
||||||
state.turn = .white;
|
state.turn = .white;
|
||||||
state.setSquare(4, 6, piece.encode(.black, .pawn));
|
state.setSquare(52, piece.encode(.black, .pawn));
|
||||||
|
|
||||||
var selection = SelectionState{};
|
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" {
|
test "SelectionState deselects when selected square is clicked again" {
|
||||||
var state = chess_board.BoardState.empty();
|
var state = chess_board.BoardState.empty();
|
||||||
state.turn = .white;
|
state.turn = .white;
|
||||||
state.setSquare(4, 1, piece.encode(.white, .pawn));
|
state.setSquare(12, piece.encode(.white, .pawn));
|
||||||
|
|
||||||
var selection = SelectionState{};
|
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" {
|
test "SelectionState switches selection when same-color piece is clicked" {
|
||||||
var state = chess_board.BoardState.empty();
|
var state = chess_board.BoardState.empty();
|
||||||
state.turn = .white;
|
state.turn = .white;
|
||||||
state.setSquare(4, 1, piece.encode(.white, .pawn));
|
state.setSquare(12, piece.encode(.white, .pawn));
|
||||||
state.setSquare(6, 0, piece.encode(.white, .knight));
|
state.setSquare(6, piece.encode(.white, .knight));
|
||||||
|
|
||||||
var selection = SelectionState{};
|
var selection = SelectionState{};
|
||||||
|
|
||||||
|
|||||||
1453
src/chess/board.zig
1453
src/chess/board.zig
File diff suppressed because it is too large
Load Diff
@ -57,7 +57,7 @@ pub fn parseBoardPlacement(state: *board.BoardState, placement: []const u8) !voi
|
|||||||
=> {
|
=> {
|
||||||
if (file >= 8) return error.InvalidRankWidth;
|
if (file >= 8) return error.InvalidRankWidth;
|
||||||
const p = try piece.fromFENChar(c);
|
const p = try piece.fromFENChar(c);
|
||||||
state.setSquare(@intCast(file), rank, p);
|
state.setSquare((@as(u6, rank) * 8) + file, p);
|
||||||
file += 1;
|
file += 1;
|
||||||
},
|
},
|
||||||
else => return error.InvalidFenCharacter,
|
else => return error.InvalidFenCharacter,
|
||||||
@ -128,42 +128,6 @@ pub fn parseCastleRights(state: *board.BoardState, castle_string: []const u8) !v
|
|||||||
state.castle_rights = rights;
|
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 {
|
pub fn parseEnPassant(state: *board.BoardState, ep_string: []const u8) !void {
|
||||||
if (std.mem.eql(u8, ep_string, "-")) {
|
if (std.mem.eql(u8, ep_string, "-")) {
|
||||||
state.en_passant = 0;
|
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);
|
const ep_square = try board.parseSquareFromAlgebraic(ep_string);
|
||||||
if (ep_string[1] != '3' and ep_string[1] != '6') return error.InvalidEnPassantSquare;
|
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);
|
||||||
state.en_passant = (1 << 6) | @as(u7, ep_square);
|
|
||||||
} else {
|
|
||||||
state.en_passant = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pieceToFenChar(encoded: u4) !u8 {
|
fn pieceToFenChar(encoded: u4) !u8 {
|
||||||
@ -202,7 +162,8 @@ fn appendBoardPlacement(allocator: std.mem.Allocator, out: *std.ArrayList(u8), s
|
|||||||
|
|
||||||
var file: u4 = 0;
|
var file: u4 = 0;
|
||||||
while (file < 8) : (file += 1) {
|
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) {
|
if (square == 0) {
|
||||||
empty_count += 1;
|
empty_count += 1;
|
||||||
continue;
|
continue;
|
||||||
@ -279,6 +240,36 @@ pub fn formatFen(allocator: std.mem.Allocator, state: board.BoardState) ![]u8 {
|
|||||||
return try out.toOwnedSlice(allocator);
|
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" {
|
test "parseBoardPlacement parses starting position" {
|
||||||
var state = board.BoardState.empty();
|
var state = board.BoardState.empty();
|
||||||
try parseBoardPlacement(
|
try parseBoardPlacement(
|
||||||
@ -286,14 +277,7 @@ test "parseBoardPlacement parses starting position" {
|
|||||||
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR",
|
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR",
|
||||||
);
|
);
|
||||||
|
|
||||||
try std.testing.expectEqual(@as(u32, 0xCABEDBAC), state.board[0]);
|
try expectStartingBoardPieces(state);
|
||||||
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]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test "parseBoardPlacement rejects invalid rank counts" {
|
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);
|
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();
|
var state = board.BoardState.empty();
|
||||||
|
|
||||||
state.turn = .black;
|
state.turn = .black;
|
||||||
try parseEnPassant(&state, "e3");
|
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;
|
state.turn = .white;
|
||||||
try parseEnPassant(&state, "e6");
|
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" {
|
test "parseEnPassant stores rank 6 target capturable by white pawn" {
|
||||||
var state = board.BoardState.empty();
|
var state = board.BoardState.empty();
|
||||||
state.turn = .white;
|
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 parseEnPassant(&state, "e6");
|
||||||
try std.testing.expectEqual(@as(u7, 108), state.en_passant);
|
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" {
|
test "parseEnPassant stores rank 3 target capturable by black pawn" {
|
||||||
var state = board.BoardState.empty();
|
var state = board.BoardState.empty();
|
||||||
state.turn = .black;
|
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 parseEnPassant(&state, "e3");
|
||||||
try std.testing.expectEqual(@as(u7, 84), state.en_passant);
|
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" {
|
test "parseEnPassant handles capturable edge-file targets" {
|
||||||
var white_state = board.BoardState.empty();
|
var white_state = board.BoardState.empty();
|
||||||
white_state.turn = .white;
|
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 parseEnPassant(&white_state, "a6");
|
||||||
try std.testing.expectEqual(@as(u7, 104), white_state.en_passant);
|
try std.testing.expectEqual(@as(u7, 104), white_state.en_passant);
|
||||||
|
|
||||||
var black_state = board.BoardState.empty();
|
var black_state = board.BoardState.empty();
|
||||||
black_state.turn = .black;
|
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 parseEnPassant(&black_state, "h3");
|
||||||
try std.testing.expectEqual(@as(u7, 87), black_state.en_passant);
|
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();
|
var state = board.BoardState.empty();
|
||||||
state.turn = .white;
|
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 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" {
|
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" {
|
test "parseFen parses starting position" {
|
||||||
const state = try parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1");
|
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 expectStartingBoardPieces(state);
|
||||||
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 std.testing.expectEqual(piece.Color.white, state.turn);
|
try std.testing.expectEqual(piece.Color.white, state.turn);
|
||||||
try std.testing.expectEqual(@as(u4, 0b1111), state.castle_rights);
|
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, 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(piece.Color.white, state.turn);
|
||||||
try std.testing.expectEqual(@as(u4, 0), state.castle_rights);
|
try std.testing.expectEqual(@as(u4, 0), state.castle_rights);
|
||||||
try std.testing.expectEqual(@as(u7, 108), state.en_passant);
|
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(.white, .pawn), state.getSquare(35));
|
||||||
try std.testing.expectEqual(piece.encode(.black, .pawn), state.getSquare(4, 4));
|
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");
|
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(piece.Color.white, state.turn);
|
||||||
try std.testing.expectEqual(@as(u4, 0b1111), state.castle_rights);
|
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(u8, 4), state.halfmove);
|
||||||
try std.testing.expectEqual(@as(u32, 5), state.fullmove);
|
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(.white, .pawn), state.getSquare(27));
|
||||||
try std.testing.expectEqual(piece.encode(.black, .pawn), state.getSquare(4, 4));
|
try std.testing.expectEqual(piece.encode(.black, .pawn), state.getSquare(36));
|
||||||
}
|
}
|
||||||
|
|
||||||
test "parseFen rejects invalid field counts" {
|
test "parseFen rejects invalid field counts" {
|
||||||
@ -571,9 +548,9 @@ test "formatFen formats capturable en passant target" {
|
|||||||
try std.testing.expectEqualStrings(expected, actual);
|
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 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 state = try parseFen(input);
|
||||||
|
|
||||||
const actual = try formatFen(std.testing.allocator, state);
|
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);
|
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" {
|
test "formatFen formats black turn no castling and larger counters" {
|
||||||
var state = board.BoardState.empty();
|
var state = board.BoardState.empty();
|
||||||
state.turn = .black;
|
state.turn = .black;
|
||||||
state.castle_rights = 0;
|
state.castle_rights = 0;
|
||||||
state.halfmove = 42;
|
state.halfmove = 42;
|
||||||
state.fullmove = 300;
|
state.fullmove = 300;
|
||||||
state.setSquare(4, 0, piece.encode(.white, .king));
|
state.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(4)), piece.encode(.white, .king));
|
||||||
state.setSquare(4, 7, piece.encode(.black, .king));
|
state.setSquare((@as(u6, @intCast(7)) * 8) + @as(u6, @intCast(4)), piece.encode(.black, .king));
|
||||||
state.setSquare(0, 0, piece.encode(.white, .rook));
|
state.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(0)), piece.encode(.white, .rook));
|
||||||
state.setSquare(7, 7, piece.encode(.black, .rook));
|
state.setSquare((@as(u6, @intCast(7)) * 8) + @as(u6, @intCast(7)), piece.encode(.black, .rook));
|
||||||
|
|
||||||
const actual = try formatFen(std.testing.allocator, state);
|
const actual = try formatFen(std.testing.allocator, state);
|
||||||
defer std.testing.allocator.free(actual);
|
defer std.testing.allocator.free(actual);
|
||||||
|
|||||||
@ -234,7 +234,7 @@ pub fn appendPiecesFromBoard(
|
|||||||
) !void {
|
) !void {
|
||||||
for (0..8) |rank| {
|
for (0..8) |rank| {
|
||||||
for (0..8) |file| {
|
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) {
|
if (piece.typeOf(p) != piece.PieceType.none) {
|
||||||
try appendPieceQuad(vertices, board_rect, allocator, @floatFromInt(file), @floatFromInt(rank));
|
try appendPieceQuad(vertices, board_rect, allocator, @floatFromInt(file), @floatFromInt(rank));
|
||||||
}
|
}
|
||||||
|
|||||||
334
src/main.zig
334
src/main.zig
@ -64,8 +64,73 @@ const PendingDragState = board_input.PendingDragState;
|
|||||||
const DragState = board_input.DragState;
|
const DragState = board_input.DragState;
|
||||||
const AppMode = board_input.InputMode;
|
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;
|
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 {
|
fn framebufferSizeCallback(_: *glfw.Window, _: c_int, _: c_int) callconv(.c) void {
|
||||||
framebuffer_resized = true;
|
framebuffer_resized = true;
|
||||||
}
|
}
|
||||||
@ -84,23 +149,23 @@ fn destroyPieceVertexBuffers(
|
|||||||
|
|
||||||
fn pieceAsset(group: piece_render.PieceGroup) []const u8 {
|
fn pieceAsset(group: piece_render.PieceGroup) []const u8 {
|
||||||
return switch (group) {
|
return switch (group) {
|
||||||
.white_pawn => assets.white_pawn_rgba,
|
.white_pawn, .dragged_white_pawn => assets.white_pawn_rgba,
|
||||||
.white_knight => assets.white_knight_rgba,
|
.white_knight, .dragged_white_knight => assets.white_knight_rgba,
|
||||||
.white_bishop => assets.white_bishop_rgba,
|
.white_bishop, .dragged_white_bishop => assets.white_bishop_rgba,
|
||||||
.white_rook => assets.white_rook_rgba,
|
.white_rook, .dragged_white_rook => assets.white_rook_rgba,
|
||||||
.white_queen => assets.white_queen_rgba,
|
.white_queen, .dragged_white_queen => assets.white_queen_rgba,
|
||||||
.white_king => assets.white_king_rgba,
|
.white_king, .dragged_white_king => assets.white_king_rgba,
|
||||||
.black_pawn => assets.black_pawn_rgba,
|
.black_pawn, .dragged_black_pawn => assets.black_pawn_rgba,
|
||||||
.black_knight => assets.black_knight_rgba,
|
.black_knight, .dragged_black_knight => assets.black_knight_rgba,
|
||||||
.black_bishop => assets.black_bishop_rgba,
|
.black_bishop, .dragged_black_bishop => assets.black_bishop_rgba,
|
||||||
.black_rook => assets.black_rook_rgba,
|
.black_rook, .dragged_black_rook => assets.black_rook_rgba,
|
||||||
.black_queen => assets.black_queen_rgba,
|
.black_queen, .dragged_black_queen => assets.black_queen_rgba,
|
||||||
.black_king => assets.black_king_rgba,
|
.black_king, .dragged_black_king => assets.black_king_rgba,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pieceGroupFromIndex(index: usize) piece_render.PieceGroup {
|
fn pieceGroupFromIndex(index: usize) piece_render.PieceGroup {
|
||||||
return @enumFromInt(@as(u4, @intCast(index)));
|
return @enumFromInt(@as(u5, @intCast(index)));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initPieceTextures(
|
fn initPieceTextures(
|
||||||
@ -210,23 +275,48 @@ fn appendBoardAndTextVertices(
|
|||||||
state: chess_board.BoardState,
|
state: chess_board.BoardState,
|
||||||
selected: ?board_input.SquareCoord,
|
selected: ?board_input.SquareCoord,
|
||||||
hovered: ?board_input.SquareCoord,
|
hovered: ?board_input.SquareCoord,
|
||||||
valid_moves: []const bitboard.Square,
|
valid_moves: bitboard.Bitboard,
|
||||||
selected_palette_piece: ?u4,
|
selected_palette_piece: ?u4,
|
||||||
mode: AppMode,
|
mode: AppMode,
|
||||||
|
promotion_popup: ?PromotionPopupState,
|
||||||
) !void {
|
) !void {
|
||||||
try text_render.appendModeMenu(vertices, allocator, mode == .edit);
|
try text_render.appendModeMenu(vertices, allocator, mode == .edit);
|
||||||
try geometry.appendChessboard(vertices, board_rect, allocator);
|
try geometry.appendChessboard(vertices, board_rect, allocator);
|
||||||
if (mode == .edit) try text_render.appendPalettePieceHighlight(vertices, allocator, board_rect, selected_palette_piece);
|
if (mode == .edit) try text_render.appendPalettePieceHighlight(vertices, allocator, board_rect, selected_palette_piece);
|
||||||
try text_render.appendSelectedSquareHighlight(vertices, allocator, board_rect, selected);
|
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.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.appendValidMoveDots(vertices, allocator, board_rect, state, valid_moves);
|
||||||
try text_render.appendBoardCoordinateLabels(vertices, allocator, board_rect);
|
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);
|
const fen_text = try fen.formatFen(allocator, state);
|
||||||
defer allocator.free(fen_text);
|
defer allocator.free(fen_text);
|
||||||
try text_render.appendFenText(vertices, allocator, board_rect, 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(
|
fn rebuildBoardAndPieceRenderResources(
|
||||||
vc: context_mod.VulkanContext,
|
vc: context_mod.VulkanContext,
|
||||||
ldc: device_mod.LogicalDeviceContext,
|
ldc: device_mod.LogicalDeviceContext,
|
||||||
@ -240,10 +330,11 @@ fn rebuildBoardAndPieceRenderResources(
|
|||||||
state: chess_board.BoardState,
|
state: chess_board.BoardState,
|
||||||
selected: ?board_input.SquareCoord,
|
selected: ?board_input.SquareCoord,
|
||||||
hovered: ?board_input.SquareCoord,
|
hovered: ?board_input.SquareCoord,
|
||||||
valid_moves: []const bitboard.Square,
|
valid_moves: bitboard.Bitboard,
|
||||||
selected_palette_piece: ?u4,
|
selected_palette_piece: ?u4,
|
||||||
drag_state: ?DragState,
|
drag_state: ?DragState,
|
||||||
mode: AppMode,
|
mode: AppMode,
|
||||||
|
promotion_popup: ?PromotionPopupState,
|
||||||
board_vertices: *std.ArrayList(geometry.Vertex),
|
board_vertices: *std.ArrayList(geometry.Vertex),
|
||||||
piece_vertex_groups: *piece_render.PieceVertexGroups,
|
piece_vertex_groups: *piece_render.PieceVertexGroups,
|
||||||
board_vertex_buffer_context: *buffer_mod.VertexBufferContext,
|
board_vertex_buffer_context: *buffer_mod.VertexBufferContext,
|
||||||
@ -253,17 +344,28 @@ fn rebuildBoardAndPieceRenderResources(
|
|||||||
) !void {
|
) !void {
|
||||||
var new_board_vertices: std.ArrayList(geometry.Vertex) = .empty;
|
var new_board_vertices: std.ArrayList(geometry.Vertex) = .empty;
|
||||||
errdefer new_board_vertices.deinit(allocator);
|
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();
|
var new_piece_vertex_groups = piece_render.PieceVertexGroups.init();
|
||||||
errdefer new_piece_vertex_groups.deinit(allocator);
|
errdefer new_piece_vertex_groups.deinit(allocator);
|
||||||
const drag_source = if (drag_state) |drag| drag.source else null;
|
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| {
|
if (drag_state) |drag| {
|
||||||
try piece_render.appendPieceGhostAtSquare(&new_piece_vertex_groups, board_rect, allocator, drag.encoded, drag.source);
|
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);
|
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 (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);
|
const new_board_vertex_buffer_context = try buffer_mod.initVertexBuffer(vc, ldc, new_board_vertices.items);
|
||||||
errdefer new_board_vertex_buffer_context.destroy(&ldc);
|
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 current_state = initial_state;
|
||||||
var input_state = board_input.InteractionState{};
|
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 appendBoardAndTextVertices(&board_vertices, std.heap.page_allocator, board, current_state, null, null, 0, input_state.selected_palette_piece, current_mode, null);
|
||||||
try piece_render.appendPiecesGroupedFromBoard(&piece_vertex_groups, board, std.heap.page_allocator, current_state);
|
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(
|
var board_vertex_buffer_context = try buffer_mod.initVertexBuffer(
|
||||||
vc,
|
vc,
|
||||||
@ -441,8 +551,8 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
defer sync_context.destroy(&ldc);
|
defer sync_context.destroy(&ldc);
|
||||||
|
|
||||||
var hovered_square: ?board_input.SquareCoord = null;
|
var hovered_square: ?board_input.SquareCoord = null;
|
||||||
var valid_moves: std.ArrayList(bitboard.Square) = .empty;
|
var valid_moves: bitboard.Bitboard = 0;
|
||||||
defer valid_moves.deinit(std.heap.page_allocator);
|
var pending_promotion: ?PendingPromotion = null;
|
||||||
var left_was_pressed = false;
|
var left_was_pressed = false;
|
||||||
var copy_was_pressed = false;
|
var copy_was_pressed = false;
|
||||||
var paste_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.current_drag = null;
|
||||||
input_state.selection.selected = null;
|
input_state.selection.selected = null;
|
||||||
hovered_square = null;
|
hovered_square = null;
|
||||||
valid_moves.clearRetainingCapacity();
|
valid_moves = 0;
|
||||||
|
pending_promotion = null;
|
||||||
try rebuildBoardAndPieceRenderResources(
|
try rebuildBoardAndPieceRenderResources(
|
||||||
vc,
|
vc,
|
||||||
ldc,
|
ldc,
|
||||||
@ -493,10 +604,11 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
current_state,
|
current_state,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
valid_moves.items,
|
valid_moves,
|
||||||
input_state.selected_palette_piece,
|
input_state.selected_palette_piece,
|
||||||
null,
|
null,
|
||||||
current_mode,
|
current_mode,
|
||||||
|
null,
|
||||||
&board_vertices,
|
&board_vertices,
|
||||||
&piece_vertex_groups,
|
&piece_vertex_groups,
|
||||||
&board_vertex_buffer_context,
|
&board_vertex_buffer_context,
|
||||||
@ -549,10 +661,11 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
current_state,
|
current_state,
|
||||||
input_state.selection.selected,
|
input_state.selection.selected,
|
||||||
hovered_square,
|
hovered_square,
|
||||||
valid_moves.items,
|
valid_moves,
|
||||||
input_state.selected_palette_piece,
|
input_state.selected_palette_piece,
|
||||||
input_state.current_drag,
|
input_state.current_drag,
|
||||||
current_mode,
|
current_mode,
|
||||||
|
if (pending_promotion) |promotion| promotion.popup() else null,
|
||||||
&board_vertices,
|
&board_vertices,
|
||||||
&piece_vertex_groups,
|
&piece_vertex_groups,
|
||||||
&board_vertex_buffer_context,
|
&board_vertex_buffer_context,
|
||||||
@ -567,9 +680,8 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
switch (board_input.handleDragUpdate(&input_state, cursor_ndc)) {
|
switch (board_input.handleDragUpdate(&input_state, cursor_ndc)) {
|
||||||
.none => {},
|
.none => {},
|
||||||
.drag_started => |held_square| {
|
.drag_started => |held_square| {
|
||||||
valid_moves.clearRetainingCapacity();
|
valid_moves = 0;
|
||||||
// Populate valid_moves for the held piece here.
|
valid_moves = current_state.getLegalMoves(held_square);
|
||||||
try current_state.getValidMoves(held_square, &valid_moves, std.heap.page_allocator);
|
|
||||||
try rebuildBoardAndPieceRenderResources(
|
try rebuildBoardAndPieceRenderResources(
|
||||||
vc,
|
vc,
|
||||||
ldc,
|
ldc,
|
||||||
@ -583,10 +695,11 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
current_state,
|
current_state,
|
||||||
null,
|
null,
|
||||||
hovered_square,
|
hovered_square,
|
||||||
valid_moves.items,
|
valid_moves,
|
||||||
input_state.selected_palette_piece,
|
input_state.selected_palette_piece,
|
||||||
input_state.current_drag,
|
input_state.current_drag,
|
||||||
current_mode,
|
current_mode,
|
||||||
|
if (pending_promotion) |promotion| promotion.popup() else null,
|
||||||
&board_vertices,
|
&board_vertices,
|
||||||
&piece_vertex_groups,
|
&piece_vertex_groups,
|
||||||
&board_vertex_buffer_context,
|
&board_vertex_buffer_context,
|
||||||
@ -609,10 +722,11 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
current_state,
|
current_state,
|
||||||
null,
|
null,
|
||||||
hovered_square,
|
hovered_square,
|
||||||
valid_moves.items,
|
valid_moves,
|
||||||
input_state.selected_palette_piece,
|
input_state.selected_palette_piece,
|
||||||
input_state.current_drag,
|
input_state.current_drag,
|
||||||
current_mode,
|
current_mode,
|
||||||
|
if (pending_promotion) |promotion| promotion.popup() else null,
|
||||||
&board_vertices,
|
&board_vertices,
|
||||||
&piece_vertex_groups,
|
&piece_vertex_groups,
|
||||||
&board_vertex_buffer_context,
|
&board_vertex_buffer_context,
|
||||||
@ -645,7 +759,55 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
else
|
else
|
||||||
null;
|
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,
|
&input_state,
|
||||||
current_state,
|
current_state,
|
||||||
current_mode,
|
current_mode,
|
||||||
@ -658,7 +820,7 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
.mode_changed => |new_mode| {
|
.mode_changed => |new_mode| {
|
||||||
current_mode = new_mode;
|
current_mode = new_mode;
|
||||||
hovered_square = null;
|
hovered_square = null;
|
||||||
valid_moves.clearRetainingCapacity();
|
valid_moves = 0;
|
||||||
board = geometry.boardRectForExtentWithPalette(
|
board = geometry.boardRectForExtentWithPalette(
|
||||||
@intCast(framebuffer_size[0]),
|
@intCast(framebuffer_size[0]),
|
||||||
@intCast(framebuffer_size[1]),
|
@intCast(framebuffer_size[1]),
|
||||||
@ -677,10 +839,11 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
current_state,
|
current_state,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
valid_moves.items,
|
valid_moves,
|
||||||
input_state.selected_palette_piece,
|
input_state.selected_palette_piece,
|
||||||
null,
|
null,
|
||||||
current_mode,
|
current_mode,
|
||||||
|
if (pending_promotion) |promotion| promotion.popup() else null,
|
||||||
&board_vertices,
|
&board_vertices,
|
||||||
&piece_vertex_groups,
|
&piece_vertex_groups,
|
||||||
&board_vertex_buffer_context,
|
&board_vertex_buffer_context,
|
||||||
@ -696,7 +859,7 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
input_state.pending_drag = null;
|
input_state.pending_drag = null;
|
||||||
input_state.current_drag = null;
|
input_state.current_drag = null;
|
||||||
hovered_square = null;
|
hovered_square = null;
|
||||||
valid_moves.clearRetainingCapacity();
|
valid_moves = 0;
|
||||||
try rebuildBoardAndPieceRenderResources(
|
try rebuildBoardAndPieceRenderResources(
|
||||||
vc,
|
vc,
|
||||||
ldc,
|
ldc,
|
||||||
@ -710,10 +873,11 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
current_state,
|
current_state,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
valid_moves.items,
|
valid_moves,
|
||||||
input_state.selected_palette_piece,
|
input_state.selected_palette_piece,
|
||||||
null,
|
null,
|
||||||
current_mode,
|
current_mode,
|
||||||
|
if (pending_promotion) |promotion| promotion.popup() else null,
|
||||||
&board_vertices,
|
&board_vertices,
|
||||||
&piece_vertex_groups,
|
&piece_vertex_groups,
|
||||||
&board_vertex_buffer_context,
|
&board_vertex_buffer_context,
|
||||||
@ -724,7 +888,7 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
},
|
},
|
||||||
.palette_changed => {
|
.palette_changed => {
|
||||||
hovered_square = null;
|
hovered_square = null;
|
||||||
valid_moves.clearRetainingCapacity();
|
valid_moves = 0;
|
||||||
try rebuildBoardAndPieceRenderResources(
|
try rebuildBoardAndPieceRenderResources(
|
||||||
vc,
|
vc,
|
||||||
ldc,
|
ldc,
|
||||||
@ -738,10 +902,11 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
current_state,
|
current_state,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
valid_moves.items,
|
valid_moves,
|
||||||
input_state.selected_palette_piece,
|
input_state.selected_palette_piece,
|
||||||
null,
|
null,
|
||||||
current_mode,
|
current_mode,
|
||||||
|
if (pending_promotion) |promotion| promotion.popup() else null,
|
||||||
&board_vertices,
|
&board_vertices,
|
||||||
&piece_vertex_groups,
|
&piece_vertex_groups,
|
||||||
&board_vertex_buffer_context,
|
&board_vertex_buffer_context,
|
||||||
@ -751,7 +916,7 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
.edit_place => |place| {
|
.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(
|
try rebuildBoardAndPieceRenderResources(
|
||||||
vc,
|
vc,
|
||||||
ldc,
|
ldc,
|
||||||
@ -765,10 +930,11 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
current_state,
|
current_state,
|
||||||
null,
|
null,
|
||||||
hovered_square,
|
hovered_square,
|
||||||
valid_moves.items,
|
valid_moves,
|
||||||
input_state.selected_palette_piece,
|
input_state.selected_palette_piece,
|
||||||
null,
|
null,
|
||||||
current_mode,
|
current_mode,
|
||||||
|
if (pending_promotion) |promotion| promotion.popup() else null,
|
||||||
&board_vertices,
|
&board_vertices,
|
||||||
&piece_vertex_groups,
|
&piece_vertex_groups,
|
||||||
&board_vertex_buffer_context,
|
&board_vertex_buffer_context,
|
||||||
@ -778,7 +944,7 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
.edit_clear => |square| {
|
.edit_clear => |square| {
|
||||||
current_state.setSquare(square.file, square.rank, 0);
|
current_state.setSquare((@as(u6, square.rank) * 8) + square.file, 0);
|
||||||
try rebuildBoardAndPieceRenderResources(
|
try rebuildBoardAndPieceRenderResources(
|
||||||
vc,
|
vc,
|
||||||
ldc,
|
ldc,
|
||||||
@ -792,10 +958,11 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
current_state,
|
current_state,
|
||||||
null,
|
null,
|
||||||
hovered_square,
|
hovered_square,
|
||||||
valid_moves.items,
|
valid_moves,
|
||||||
input_state.selected_palette_piece,
|
input_state.selected_palette_piece,
|
||||||
null,
|
null,
|
||||||
current_mode,
|
current_mode,
|
||||||
|
if (pending_promotion) |promotion| promotion.popup() else null,
|
||||||
&board_vertices,
|
&board_vertices,
|
||||||
&piece_vertex_groups,
|
&piece_vertex_groups,
|
||||||
&board_vertex_buffer_context,
|
&board_vertex_buffer_context,
|
||||||
@ -807,7 +974,7 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
.play_cleared => {
|
.play_cleared => {
|
||||||
log.debug("selection cleared", .{});
|
log.debug("selection cleared", .{});
|
||||||
hovered_square = null;
|
hovered_square = null;
|
||||||
valid_moves.clearRetainingCapacity();
|
valid_moves = 0;
|
||||||
try rebuildBoardAndPieceRenderResources(
|
try rebuildBoardAndPieceRenderResources(
|
||||||
vc,
|
vc,
|
||||||
ldc,
|
ldc,
|
||||||
@ -821,10 +988,11 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
current_state,
|
current_state,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
valid_moves.items,
|
valid_moves,
|
||||||
input_state.selected_palette_piece,
|
input_state.selected_palette_piece,
|
||||||
null,
|
null,
|
||||||
current_mode,
|
current_mode,
|
||||||
|
if (pending_promotion) |promotion| promotion.popup() else null,
|
||||||
&board_vertices,
|
&board_vertices,
|
||||||
&piece_vertex_groups,
|
&piece_vertex_groups,
|
||||||
&board_vertex_buffer_context,
|
&board_vertex_buffer_context,
|
||||||
@ -837,9 +1005,8 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
const selected_coord = board_input.squareToCoord(selected_square);
|
const selected_coord = board_input.squareToCoord(selected_square);
|
||||||
log.debug("selected square file={} rank={}", .{ selected_coord.file, selected_coord.rank });
|
log.debug("selected square file={} rank={}", .{ selected_coord.file, selected_coord.rank });
|
||||||
hovered_square = selected_coord;
|
hovered_square = selected_coord;
|
||||||
valid_moves.clearRetainingCapacity();
|
valid_moves = 0;
|
||||||
// Populate valid_moves for the selected piece here.
|
valid_moves = current_state.getLegalMoves(selected_square);
|
||||||
try current_state.getValidMoves(selected_square, &valid_moves, std.heap.page_allocator);
|
|
||||||
try rebuildBoardAndPieceRenderResources(
|
try rebuildBoardAndPieceRenderResources(
|
||||||
vc,
|
vc,
|
||||||
ldc,
|
ldc,
|
||||||
@ -853,10 +1020,11 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
current_state,
|
current_state,
|
||||||
selected_coord,
|
selected_coord,
|
||||||
hovered_square,
|
hovered_square,
|
||||||
valid_moves.items,
|
valid_moves,
|
||||||
input_state.selected_palette_piece,
|
input_state.selected_palette_piece,
|
||||||
null,
|
null,
|
||||||
current_mode,
|
current_mode,
|
||||||
|
if (pending_promotion) |promotion| promotion.popup() else null,
|
||||||
&board_vertices,
|
&board_vertices,
|
||||||
&piece_vertex_groups,
|
&piece_vertex_groups,
|
||||||
&board_vertex_buffer_context,
|
&board_vertex_buffer_context,
|
||||||
@ -867,13 +1035,21 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
},
|
},
|
||||||
.play_move_requested => |move| {
|
.play_move_requested => |move| {
|
||||||
log.debug(
|
log.debug(
|
||||||
"move requested from file={} rank={} to file={} rank={}",
|
"move requested from square={} to square={}",
|
||||||
.{ move.from.file, move.from.rank, move.to.file, move.to.rank },
|
.{ move.from, move.to },
|
||||||
);
|
);
|
||||||
// Validate move against valid_moves here before mutating board.
|
if ((valid_moves & bitboard.bit(move.to)) != 0) {
|
||||||
try current_state.move(move.from.file, move.from.rank, move.to.file, move.to.rank);
|
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;
|
hovered_square = null;
|
||||||
valid_moves.clearRetainingCapacity();
|
valid_moves = 0;
|
||||||
try rebuildBoardAndPieceRenderResources(
|
try rebuildBoardAndPieceRenderResources(
|
||||||
vc,
|
vc,
|
||||||
ldc,
|
ldc,
|
||||||
@ -887,10 +1063,11 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
current_state,
|
current_state,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
valid_moves.items,
|
valid_moves,
|
||||||
input_state.selected_palette_piece,
|
input_state.selected_palette_piece,
|
||||||
null,
|
null,
|
||||||
current_mode,
|
current_mode,
|
||||||
|
if (pending_promotion) |promotion| promotion.popup() else null,
|
||||||
&board_vertices,
|
&board_vertices,
|
||||||
&piece_vertex_groups,
|
&piece_vertex_groups,
|
||||||
&board_vertex_buffer_context,
|
&board_vertex_buffer_context,
|
||||||
@ -899,7 +1076,7 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
std.heap.page_allocator,
|
std.heap.page_allocator,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!left_is_pressed and left_was_pressed) {
|
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| {
|
.drag_cancelled => |selected_after_release| {
|
||||||
const selected_coord_after_release = if (selected_after_release) |selected_square| board_input.squareToCoord(selected_square) else null;
|
const selected_coord_after_release = if (selected_after_release) |selected_square| board_input.squareToCoord(selected_square) else null;
|
||||||
hovered_square = selected_coord_after_release;
|
hovered_square = selected_coord_after_release;
|
||||||
valid_moves.clearRetainingCapacity();
|
valid_moves = 0;
|
||||||
if (selected_after_release) |selected_square| {
|
if (selected_after_release) |selected_square| {
|
||||||
// Repopulate valid_moves for the restored selection here if desired.
|
valid_moves = current_state.getLegalMoves(selected_square);
|
||||||
try current_state.getValidMoves(selected_square, &valid_moves, std.heap.page_allocator);
|
|
||||||
}
|
}
|
||||||
try rebuildBoardAndPieceRenderResources(
|
try rebuildBoardAndPieceRenderResources(
|
||||||
vc,
|
vc,
|
||||||
@ -926,10 +1102,11 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
current_state,
|
current_state,
|
||||||
selected_coord_after_release,
|
selected_coord_after_release,
|
||||||
hovered_square,
|
hovered_square,
|
||||||
valid_moves.items,
|
valid_moves,
|
||||||
input_state.selected_palette_piece,
|
input_state.selected_palette_piece,
|
||||||
null,
|
null,
|
||||||
current_mode,
|
current_mode,
|
||||||
|
if (pending_promotion) |promotion| promotion.popup() else null,
|
||||||
&board_vertices,
|
&board_vertices,
|
||||||
&piece_vertex_groups,
|
&piece_vertex_groups,
|
||||||
&board_vertex_buffer_context,
|
&board_vertex_buffer_context,
|
||||||
@ -941,9 +1118,8 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
.drag_selected => |selected_square| {
|
.drag_selected => |selected_square| {
|
||||||
const selected_coord = board_input.squareToCoord(selected_square);
|
const selected_coord = board_input.squareToCoord(selected_square);
|
||||||
hovered_square = selected_coord;
|
hovered_square = selected_coord;
|
||||||
valid_moves.clearRetainingCapacity();
|
valid_moves = 0;
|
||||||
// Populate valid_moves for the selected piece here.
|
valid_moves = current_state.getLegalMoves(selected_square);
|
||||||
try current_state.getValidMoves(selected_square, &valid_moves, std.heap.page_allocator);
|
|
||||||
try rebuildBoardAndPieceRenderResources(
|
try rebuildBoardAndPieceRenderResources(
|
||||||
vc,
|
vc,
|
||||||
ldc,
|
ldc,
|
||||||
@ -957,10 +1133,11 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
current_state,
|
current_state,
|
||||||
selected_coord,
|
selected_coord,
|
||||||
hovered_square,
|
hovered_square,
|
||||||
valid_moves.items,
|
valid_moves,
|
||||||
input_state.selected_palette_piece,
|
input_state.selected_palette_piece,
|
||||||
null,
|
null,
|
||||||
current_mode,
|
current_mode,
|
||||||
|
if (pending_promotion) |promotion| promotion.popup() else null,
|
||||||
&board_vertices,
|
&board_vertices,
|
||||||
&piece_vertex_groups,
|
&piece_vertex_groups,
|
||||||
&board_vertex_buffer_context,
|
&board_vertex_buffer_context,
|
||||||
@ -971,7 +1148,7 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
},
|
},
|
||||||
.drag_cleared => {
|
.drag_cleared => {
|
||||||
hovered_square = null;
|
hovered_square = null;
|
||||||
valid_moves.clearRetainingCapacity();
|
valid_moves = 0;
|
||||||
try rebuildBoardAndPieceRenderResources(
|
try rebuildBoardAndPieceRenderResources(
|
||||||
vc,
|
vc,
|
||||||
ldc,
|
ldc,
|
||||||
@ -985,10 +1162,11 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
current_state,
|
current_state,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
valid_moves.items,
|
valid_moves,
|
||||||
input_state.selected_palette_piece,
|
input_state.selected_palette_piece,
|
||||||
null,
|
null,
|
||||||
current_mode,
|
current_mode,
|
||||||
|
if (pending_promotion) |promotion| promotion.popup() else null,
|
||||||
&board_vertices,
|
&board_vertices,
|
||||||
&piece_vertex_groups,
|
&piece_vertex_groups,
|
||||||
&board_vertex_buffer_context,
|
&board_vertex_buffer_context,
|
||||||
@ -998,10 +1176,18 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
.drag_move_requested => |move| {
|
.drag_move_requested => |move| {
|
||||||
// Validate drag move against valid_moves here before mutating board.
|
if ((valid_moves & bitboard.bit(move.to)) != 0) {
|
||||||
try current_state.move(move.from.file, move.from.rank, move.to.file, move.to.rank);
|
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;
|
hovered_square = null;
|
||||||
valid_moves.clearRetainingCapacity();
|
valid_moves = 0;
|
||||||
try rebuildBoardAndPieceRenderResources(
|
try rebuildBoardAndPieceRenderResources(
|
||||||
vc,
|
vc,
|
||||||
ldc,
|
ldc,
|
||||||
@ -1015,10 +1201,11 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
current_state,
|
current_state,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
valid_moves.items,
|
valid_moves,
|
||||||
input_state.selected_palette_piece,
|
input_state.selected_palette_piece,
|
||||||
null,
|
null,
|
||||||
current_mode,
|
current_mode,
|
||||||
|
if (pending_promotion) |promotion| promotion.popup() else null,
|
||||||
&board_vertices,
|
&board_vertices,
|
||||||
&piece_vertex_groups,
|
&piece_vertex_groups,
|
||||||
&board_vertex_buffer_context,
|
&board_vertex_buffer_context,
|
||||||
@ -1052,10 +1239,11 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
current_state,
|
current_state,
|
||||||
input_state.selection.selected,
|
input_state.selection.selected,
|
||||||
hovered_square,
|
hovered_square,
|
||||||
valid_moves.items,
|
valid_moves,
|
||||||
input_state.selected_palette_piece,
|
input_state.selected_palette_piece,
|
||||||
input_state.current_drag,
|
input_state.current_drag,
|
||||||
current_mode,
|
current_mode,
|
||||||
|
if (pending_promotion) |promotion| promotion.popup() else null,
|
||||||
&board_vertices,
|
&board_vertices,
|
||||||
&piece_vertex_groups,
|
&piece_vertex_groups,
|
||||||
piece_textures,
|
piece_textures,
|
||||||
@ -1085,10 +1273,11 @@ fn recreateSwapchain(
|
|||||||
state: chess_board.BoardState,
|
state: chess_board.BoardState,
|
||||||
selected: ?board_input.SquareCoord,
|
selected: ?board_input.SquareCoord,
|
||||||
hovered: ?board_input.SquareCoord,
|
hovered: ?board_input.SquareCoord,
|
||||||
valid_moves: []const bitboard.Square,
|
valid_moves: bitboard.Bitboard,
|
||||||
selected_palette_piece: ?u4,
|
selected_palette_piece: ?u4,
|
||||||
drag_state: ?DragState,
|
drag_state: ?DragState,
|
||||||
mode: AppMode,
|
mode: AppMode,
|
||||||
|
promotion_popup: ?PromotionPopupState,
|
||||||
board_vertices: *std.ArrayList(geometry.Vertex),
|
board_vertices: *std.ArrayList(geometry.Vertex),
|
||||||
piece_vertex_groups: *piece_render.PieceVertexGroups,
|
piece_vertex_groups: *piece_render.PieceVertexGroups,
|
||||||
piece_textures: [piece_render.PieceGroupCount]?texture_mod.TextureContext,
|
piece_textures: [piece_render.PieceGroupCount]?texture_mod.TextureContext,
|
||||||
@ -1137,14 +1326,17 @@ fn recreateSwapchain(
|
|||||||
new_swapchain_context.extent.height,
|
new_swapchain_context.extent.height,
|
||||||
mode == .edit,
|
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;
|
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| {
|
if (drag_state) |drag| {
|
||||||
try piece_render.appendPieceGhostAtSquare(piece_vertex_groups, new_board, allocator, drag.encoded, drag.source);
|
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);
|
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 (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);
|
const new_board_vertex_buffer_context = try buffer_mod.initVertexBuffer(vc, ldc, board_vertices.items);
|
||||||
errdefer new_board_vertex_buffer_context.destroy(&ldc);
|
errdefer new_board_vertex_buffer_context.destroy(&ldc);
|
||||||
|
|||||||
@ -2,10 +2,11 @@ const std = @import("std");
|
|||||||
|
|
||||||
const board = @import("chess/board.zig");
|
const board = @import("chess/board.zig");
|
||||||
const fen = @import("chess/fen.zig");
|
const fen = @import("chess/fen.zig");
|
||||||
|
const bitboard = @import("chess/bitboard.zig");
|
||||||
const geometry = @import("geometry.zig");
|
const geometry = @import("geometry.zig");
|
||||||
const piece = @import("chess/piece.zig");
|
const piece = @import("chess/piece.zig");
|
||||||
|
|
||||||
pub const PieceGroup = enum(u4) {
|
pub const PieceGroup = enum(u5) {
|
||||||
white_pawn,
|
white_pawn,
|
||||||
white_knight,
|
white_knight,
|
||||||
white_bishop,
|
white_bishop,
|
||||||
@ -18,15 +19,29 @@ pub const PieceGroup = enum(u4) {
|
|||||||
black_rook,
|
black_rook,
|
||||||
black_queen,
|
black_queen,
|
||||||
black_king,
|
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 {
|
pub const PaletteEntry = struct {
|
||||||
group: PieceGroup,
|
group: PieceGroup,
|
||||||
encoded: u4,
|
encoded: u4,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const promotion_piece_types = [_]piece.PieceType{ .queen, .rook, .bishop, .knight };
|
||||||
|
|
||||||
pub const palette_entries = [_]PaletteEntry{
|
pub const palette_entries = [_]PaletteEntry{
|
||||||
.{ .group = .white_king, .encoded = piece.encode(.white, .king) },
|
.{ .group = .white_king, .encoded = piece.encode(.white, .king) },
|
||||||
.{ .group = .white_queen, .encoded = piece.encode(.white, .queen) },
|
.{ .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 {
|
pub const PieceVertexGroups = struct {
|
||||||
groups: [PieceGroupCount]std.ArrayList(geometry.Vertex),
|
groups: [PieceGroupCount]std.ArrayList(geometry.Vertex),
|
||||||
|
|
||||||
@ -77,6 +126,14 @@ pub const PieceVertexGroups = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub fn pieceGroupFromEncoded(encoded: u4) ?PieceGroup {
|
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;
|
if (encoded == 0) return null;
|
||||||
|
|
||||||
const color = piece.colorOf(encoded) orelse return null;
|
const color = piece.colorOf(encoded) orelse return null;
|
||||||
@ -84,26 +141,54 @@ pub fn pieceGroupFromEncoded(encoded: u4) ?PieceGroup {
|
|||||||
|
|
||||||
return switch (color) {
|
return switch (color) {
|
||||||
.white => switch (piece_type) {
|
.white => switch (piece_type) {
|
||||||
.pawn => .white_pawn,
|
.pawn => if (dragged) .dragged_white_pawn else .white_pawn,
|
||||||
.knight => .white_knight,
|
.knight => if (dragged) .dragged_white_knight else .white_knight,
|
||||||
.bishop => .white_bishop,
|
.bishop => if (dragged) .dragged_white_bishop else .white_bishop,
|
||||||
.rook => .white_rook,
|
.rook => if (dragged) .dragged_white_rook else .white_rook,
|
||||||
.queen => .white_queen,
|
.queen => if (dragged) .dragged_white_queen else .white_queen,
|
||||||
.king => .white_king,
|
.king => if (dragged) .dragged_white_king else .white_king,
|
||||||
.none => null,
|
.none => null,
|
||||||
},
|
},
|
||||||
.black => switch (piece_type) {
|
.black => switch (piece_type) {
|
||||||
.pawn => .black_pawn,
|
.pawn => if (dragged) .dragged_black_pawn else .black_pawn,
|
||||||
.knight => .black_knight,
|
.knight => if (dragged) .dragged_black_knight else .black_knight,
|
||||||
.bishop => .black_bishop,
|
.bishop => if (dragged) .dragged_black_bishop else .black_bishop,
|
||||||
.rook => .black_rook,
|
.rook => if (dragged) .dragged_black_rook else .black_rook,
|
||||||
.queen => .black_queen,
|
.queen => if (dragged) .dragged_black_queen else .black_queen,
|
||||||
.king => .black_king,
|
.king => if (dragged) .dragged_black_king else .black_king,
|
||||||
.none => null,
|
.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(
|
fn appendPieceQuadInRect(
|
||||||
vertices: *std.ArrayList(geometry.Vertex),
|
vertices: *std.ArrayList(geometry.Vertex),
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
@ -146,6 +231,17 @@ pub fn appendPiecesGroupedFromBoardExcept(
|
|||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
state: board.BoardState,
|
state: board.BoardState,
|
||||||
except_square: ?@import("board_input.zig").SquareCoord,
|
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 {
|
) !void {
|
||||||
for (0..8) |rank| {
|
for (0..8) |rank| {
|
||||||
for (0..8) |file| {
|
for (0..8) |file| {
|
||||||
@ -153,9 +249,22 @@ pub fn appendPiecesGroupedFromBoardExcept(
|
|||||||
if (except.file == file and except.rank == rank) continue;
|
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;
|
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(
|
try geometry.appendPieceQuad(
|
||||||
groups.group(piece_group),
|
groups.group(piece_group),
|
||||||
board_rect,
|
board_rect,
|
||||||
@ -163,6 +272,7 @@ pub fn appendPiecesGroupedFromBoardExcept(
|
|||||||
@as(f32, @floatFromInt(file)),
|
@as(f32, @floatFromInt(file)),
|
||||||
@as(f32, @floatFromInt(rank)),
|
@as(f32, @floatFromInt(rank)),
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -192,7 +302,7 @@ pub fn appendDraggedPiece(
|
|||||||
encoded: u4,
|
encoded: u4,
|
||||||
center_ndc: [2]f32,
|
center_ndc: [2]f32,
|
||||||
) !void {
|
) !void {
|
||||||
const piece_group = pieceGroupFromEncoded(encoded) orelse return;
|
const piece_group = draggedPieceGroupFromEncoded(encoded) orelse return;
|
||||||
try geometry.appendPieceQuadCenteredAtNdc(
|
try geometry.appendPieceQuadCenteredAtNdc(
|
||||||
groups.group(piece_group),
|
groups.group(piece_group),
|
||||||
allocator,
|
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 {
|
pub fn paletteIndexForEncoded(encoded: u4) ?usize {
|
||||||
for (palette_entries, 0..) |entry, i| {
|
for (palette_entries, 0..) |entry, i| {
|
||||||
if (entry.encoded == encoded) return 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);
|
try appendPalettePiecesGrouped(&groups, board_rect, std.testing.allocator);
|
||||||
|
|
||||||
for (groups.groups) |vertex_group| {
|
for (groups.groups, 0..) |vertex_group, i| {
|
||||||
try std.testing.expectEqual(@as(usize, 6), vertex_group.items.len);
|
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" {
|
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 state = try fen.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1");
|
||||||
const board_rect = geometry.boardRectForExtent(800, 600);
|
const board_rect = geometry.boardRectForExtent(800, 600);
|
||||||
|
|||||||
@ -61,6 +61,7 @@ fn glyphForChar(ch: u8) ?Glyph {
|
|||||||
'T' => .{ 0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100 },
|
'T' => .{ 0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100 },
|
||||||
'Y' => .{ 0b10001, 0b10001, 0b01010, 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 },
|
'-' => .{ 0b00000, 0b00000, 0b00000, 0b11111, 0b00000, 0b00000, 0b00000 },
|
||||||
'/' => .{ 0b00001, 0b00010, 0b00010, 0b00100, 0b01000, 0b01000, 0b10000 },
|
'/' => .{ 0b00001, 0b00010, 0b00010, 0b00100, 0b01000, 0b01000, 0b10000 },
|
||||||
else => null,
|
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(
|
fn appendSquareOverlay(
|
||||||
vertices: *std.ArrayList(geometry.Vertex),
|
vertices: *std.ArrayList(geometry.Vertex),
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
@ -152,7 +156,7 @@ pub fn appendSelectedSquareHighlight(
|
|||||||
selected: ?board_input.SquareCoord,
|
selected: ?board_input.SquareCoord,
|
||||||
) !void {
|
) !void {
|
||||||
const square = selected orelse return;
|
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(
|
pub fn appendPalettePieceHighlight(
|
||||||
@ -185,7 +189,55 @@ pub fn appendHoveredSquareHighlight(
|
|||||||
hovered: ?board_input.SquareCoord,
|
hovered: ?board_input.SquareCoord,
|
||||||
) !void {
|
) !void {
|
||||||
const square = hovered orelse return;
|
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(
|
pub fn appendValidMoveDots(
|
||||||
@ -193,19 +245,22 @@ pub fn appendValidMoveDots(
|
|||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
board_rect: geometry.BoardRect,
|
board_rect: geometry.BoardRect,
|
||||||
state: chess_board.BoardState,
|
state: chess_board.BoardState,
|
||||||
valid_moves: []const bitboard.Square,
|
valid_moves: bitboard.Bitboard,
|
||||||
) !void {
|
) !void {
|
||||||
const square_w = board_rect.width / 8.0;
|
const square_w = board_rect.width / 8.0;
|
||||||
const square_h = board_rect.height / 8.0;
|
const square_h = board_rect.height / 8.0;
|
||||||
const radius_x = square_w * 0.11;
|
const radius_x = square_w * 0.11;
|
||||||
const radius_y = square_h * 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;
|
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 file: u3 = @intCast(move_square % 8);
|
||||||
const rank: 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 x0 = @as(f32, @floatFromInt(file));
|
||||||
const x1 = x0 + 1.0;
|
const x1 = x0 + 1.0;
|
||||||
const y0 = @as(f32, @floatFromInt(rank));
|
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(
|
pub fn appendModeMenu(
|
||||||
vertices: *std.ArrayList(geometry.Vertex),
|
vertices: *std.ArrayList(geometry.Vertex),
|
||||||
allocator: std.mem.Allocator,
|
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);
|
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" {
|
test "appendValidMoveDots appends one circular triangle fan per move" {
|
||||||
var vertices: std.ArrayList(geometry.Vertex) = .empty;
|
var vertices: std.ArrayList(geometry.Vertex) = .empty;
|
||||||
defer vertices.deinit(std.testing.allocator);
|
defer vertices.deinit(std.testing.allocator);
|
||||||
|
|
||||||
const state = chess_board.BoardState.empty();
|
const state = chess_board.BoardState.empty();
|
||||||
const moves = [_]bitboard.Square{ 20, 28 };
|
const moves = bitboard.bit(20) | bitboard.bit(28);
|
||||||
|
|
||||||
try appendValidMoveDots(
|
try appendValidMoveDots(
|
||||||
&vertices,
|
&vertices,
|
||||||
std.testing.allocator,
|
std.testing.allocator,
|
||||||
geometry.boardRectForExtent(800, 600),
|
geometry.boardRectForExtent(800, 600),
|
||||||
state,
|
state,
|
||||||
&moves,
|
moves,
|
||||||
);
|
);
|
||||||
|
|
||||||
try std.testing.expectEqual(@as(usize, 2 * 48 * 3), vertices.items.len);
|
try std.testing.expectEqual(@as(usize, 2 * 48 * 3), vertices.items.len);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user