Add promotion selection and square APIs
This commit is contained in:
+109
-81
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user