Add promotion selection and square APIs

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