629 lines
22 KiB
Zig
629 lines
22 KiB
Zig
const std = @import("std");
|
|
|
|
const piece = @import("piece.zig");
|
|
const board = @import("board.zig");
|
|
|
|
pub fn parseFen(fen: []const u8) !board.BoardState {
|
|
var state = board.BoardState.empty();
|
|
var it = std.mem.splitScalar(u8, fen, ' ');
|
|
var field_index: usize = 0;
|
|
|
|
while (it.next()) |field| : (field_index += 1) {
|
|
if (field_index >= 6) return error.InvalidFenFieldCount;
|
|
|
|
switch (field_index) {
|
|
0 => try parseBoardPlacement(&state, field),
|
|
1 => try parseTurn(&state, field),
|
|
2 => try parseCastleRights(&state, field),
|
|
3 => try parseEnPassant(&state, field),
|
|
4 => try parseHalfmove(&state, field),
|
|
5 => try parseFullmove(&state, field),
|
|
else => unreachable,
|
|
}
|
|
}
|
|
if (field_index != 6) return error.InvalidFenFieldCount;
|
|
|
|
return state;
|
|
}
|
|
|
|
pub fn parseBoardPlacement(state: *board.BoardState, placement: []const u8) !void {
|
|
var it = std.mem.splitScalar(u8, placement, '/');
|
|
|
|
var rank_count: u4 = 0;
|
|
while (it.next()) |part| {
|
|
if (rank_count >= 8) return error.InvalidRankCount;
|
|
|
|
const rank: u3 = @intCast(7 - rank_count);
|
|
var file: u4 = 0;
|
|
|
|
for (part) |c| {
|
|
switch (c) {
|
|
'1'...'9' => {
|
|
file += @intCast(c - '0');
|
|
if (file > 8) return error.InvalidRankWidth;
|
|
},
|
|
'P',
|
|
'N',
|
|
'B',
|
|
'R',
|
|
'Q',
|
|
'K',
|
|
'p',
|
|
'n',
|
|
'b',
|
|
'r',
|
|
'q',
|
|
'k',
|
|
=> {
|
|
if (file >= 8) return error.InvalidRankWidth;
|
|
const p = try piece.fromFENChar(c);
|
|
state.setSquare((@as(u6, rank) * 8) + file, p);
|
|
file += 1;
|
|
},
|
|
else => return error.InvalidFenCharacter,
|
|
}
|
|
}
|
|
|
|
if (file != 8) return error.InvalidRankWidth;
|
|
rank_count += 1;
|
|
}
|
|
|
|
if (rank_count != 8) return error.InvalidRankCount;
|
|
}
|
|
|
|
pub fn parseTurn(state: *board.BoardState, turn_string: []const u8) !void {
|
|
if (turn_string.len != 1) return error.InvalidTurnChar;
|
|
const turn_char = turn_string[0];
|
|
if (turn_char == 'w') {
|
|
state.turn = piece.Color.white;
|
|
} else if (turn_char == 'b') {
|
|
state.turn = piece.Color.black;
|
|
} else {
|
|
return error.InvalidTurnChar;
|
|
}
|
|
}
|
|
|
|
pub fn parseHalfmove(state: *board.BoardState, halfmove_string: []const u8) !void {
|
|
state.halfmove = try std.fmt.parseInt(u8, halfmove_string, 10);
|
|
}
|
|
|
|
pub fn parseFullmove(state: *board.BoardState, fullmove_string: []const u8) !void {
|
|
const fullmove = try std.fmt.parseInt(u32, fullmove_string, 10);
|
|
if (fullmove == 0) return error.InvalidFullmoveNumber;
|
|
state.fullmove = fullmove;
|
|
}
|
|
|
|
pub fn parseCastleRights(state: *board.BoardState, castle_string: []const u8) !void {
|
|
if (std.mem.eql(u8, castle_string, "")) return error.InvalidCastlingRights;
|
|
if (std.mem.eql(u8, castle_string, "-")) {
|
|
state.castle_rights = 0;
|
|
return;
|
|
}
|
|
var rights: u4 = 0;
|
|
var last_order: u3 = 0;
|
|
for (castle_string) |c| {
|
|
const bit: u4 = switch (c) {
|
|
'K' => 0b1000,
|
|
'Q' => 0b0100,
|
|
'k' => 0b0010,
|
|
'q' => 0b0001,
|
|
else => return error.InvalidCastlingRights,
|
|
};
|
|
const order: u3 = switch (c) {
|
|
'K' => 1,
|
|
'Q' => 2,
|
|
'k' => 3,
|
|
'q' => 4,
|
|
else => unreachable,
|
|
};
|
|
if (order <= last_order) {
|
|
return error.InvalidCastlingRights;
|
|
}
|
|
if ((rights & bit) != 0) {
|
|
return error.InvalidCastlingRights;
|
|
}
|
|
rights |= bit;
|
|
last_order = order;
|
|
}
|
|
state.castle_rights = rights;
|
|
}
|
|
|
|
pub fn parseEnPassant(state: *board.BoardState, ep_string: []const u8) !void {
|
|
if (std.mem.eql(u8, ep_string, "-")) {
|
|
state.en_passant = 0;
|
|
return;
|
|
}
|
|
if (ep_string.len != 2) return error.InvalidSquare;
|
|
const ep_square = try board.parseSquareFromAlgebraic(ep_string);
|
|
if (ep_string[1] != '3' and ep_string[1] != '6') return error.InvalidEnPassantSquare;
|
|
|
|
state.en_passant = (1 << 6) | @as(u7, ep_square);
|
|
}
|
|
|
|
fn pieceToFenChar(encoded: u4) !u8 {
|
|
const color = piece.colorOf(encoded) orelse return error.InvalidPiece;
|
|
|
|
return switch (piece.typeOf(encoded)) {
|
|
.pawn => if (color == .white) 'P' else 'p',
|
|
.knight => if (color == .white) 'N' else 'n',
|
|
.bishop => if (color == .white) 'B' else 'b',
|
|
.rook => if (color == .white) 'R' else 'r',
|
|
.queen => if (color == .white) 'Q' else 'q',
|
|
.king => if (color == .white) 'K' else 'k',
|
|
.none => error.InvalidPiece,
|
|
};
|
|
}
|
|
|
|
fn appendBoardPlacement(allocator: std.mem.Allocator, out: *std.ArrayList(u8), state: board.BoardState) !void {
|
|
var rank: u4 = 8;
|
|
while (rank > 0) {
|
|
rank -= 1;
|
|
var empty_count: u4 = 0;
|
|
|
|
var file: u4 = 0;
|
|
while (file < 8) : (file += 1) {
|
|
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;
|
|
}
|
|
|
|
if (empty_count > 0) {
|
|
try out.append(allocator, '0' + @as(u8, @intCast(empty_count)));
|
|
empty_count = 0;
|
|
}
|
|
|
|
try out.append(allocator, try pieceToFenChar(square));
|
|
}
|
|
|
|
if (empty_count > 0) {
|
|
try out.append(allocator, '0' + @as(u8, @intCast(empty_count)));
|
|
}
|
|
|
|
if (rank > 0) try out.append(allocator, '/');
|
|
}
|
|
}
|
|
|
|
fn appendCastleRights(allocator: std.mem.Allocator, out: *std.ArrayList(u8), rights: u4) !void {
|
|
if (rights == 0) {
|
|
try out.append(allocator, '-');
|
|
return;
|
|
}
|
|
|
|
if ((rights & 0b1000) != 0) try out.append(allocator, 'K');
|
|
if ((rights & 0b0100) != 0) try out.append(allocator, 'Q');
|
|
if ((rights & 0b0010) != 0) try out.append(allocator, 'k');
|
|
if ((rights & 0b0001) != 0) try out.append(allocator, 'q');
|
|
}
|
|
|
|
fn appendEnPassant(allocator: std.mem.Allocator, out: *std.ArrayList(u8), en_passant: u7) !void {
|
|
const valid = (en_passant & (@as(u7, 1) << 6)) != 0;
|
|
if (!valid) {
|
|
try out.append(allocator, '-');
|
|
return;
|
|
}
|
|
|
|
const square = en_passant & 0b111111;
|
|
const file: u8 = @intCast(square % 8);
|
|
const rank: u8 = @intCast(square / 8);
|
|
|
|
try out.append(allocator, 'a' + file);
|
|
try out.append(allocator, '1' + rank);
|
|
}
|
|
|
|
pub fn formatFen(allocator: std.mem.Allocator, state: board.BoardState) ![]u8 {
|
|
var out: std.ArrayList(u8) = .empty;
|
|
errdefer out.deinit(allocator);
|
|
|
|
try appendBoardPlacement(allocator, &out, state);
|
|
try out.append(allocator, ' ');
|
|
|
|
try out.append(allocator, if (state.turn == .white) 'w' else 'b');
|
|
try out.append(allocator, ' ');
|
|
|
|
try appendCastleRights(allocator, &out, state.castle_rights);
|
|
try out.append(allocator, ' ');
|
|
|
|
try appendEnPassant(allocator, &out, state.en_passant);
|
|
try out.append(allocator, ' ');
|
|
|
|
var halfmove_buf: [3]u8 = undefined;
|
|
const halfmove = try std.fmt.bufPrint(&halfmove_buf, "{}", .{state.halfmove});
|
|
try out.appendSlice(allocator, halfmove);
|
|
try out.append(allocator, ' ');
|
|
|
|
var fullmove_buf: [10]u8 = undefined;
|
|
const fullmove = try std.fmt.bufPrint(&fullmove_buf, "{}", .{state.fullmove});
|
|
try out.appendSlice(allocator, fullmove);
|
|
|
|
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(
|
|
&state,
|
|
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR",
|
|
);
|
|
|
|
try expectStartingBoardPieces(state);
|
|
}
|
|
|
|
test "parseBoardPlacement rejects invalid rank counts" {
|
|
var state = board.BoardState.empty();
|
|
|
|
try std.testing.expectError(
|
|
error.InvalidRankCount,
|
|
parseBoardPlacement(&state, "8/8/8/8/8/8/8"),
|
|
);
|
|
|
|
try std.testing.expectError(
|
|
error.InvalidRankCount,
|
|
parseBoardPlacement(&state, "8/8/8/8/8/8/8/8/8"),
|
|
);
|
|
}
|
|
|
|
test "parseBoardPlacement rejects ranks that are not exactly eight squares" {
|
|
var state = board.BoardState.empty();
|
|
|
|
try std.testing.expectError(
|
|
error.InvalidRankWidth,
|
|
parseBoardPlacement(&state, "7/8/8/8/8/8/8/8"),
|
|
);
|
|
|
|
try std.testing.expectError(
|
|
error.InvalidRankWidth,
|
|
parseBoardPlacement(&state, "9/8/8/8/8/8/8/8"),
|
|
);
|
|
|
|
try std.testing.expectError(
|
|
error.InvalidRankWidth,
|
|
parseBoardPlacement(&state, "8P/8/8/8/8/8/8/8"),
|
|
);
|
|
}
|
|
|
|
test "parseTurn accepts white and black active colors" {
|
|
var state = board.BoardState.empty();
|
|
|
|
try parseTurn(&state, "b");
|
|
try std.testing.expectEqual(piece.Color.black, state.turn);
|
|
|
|
try parseTurn(&state, "w");
|
|
try std.testing.expectEqual(piece.Color.white, state.turn);
|
|
}
|
|
|
|
test "parseTurn rejects invalid active color" {
|
|
var state = board.BoardState.empty();
|
|
|
|
try std.testing.expectError(error.InvalidTurnChar, parseTurn(&state, "x"));
|
|
try std.testing.expectError(error.InvalidTurnChar, parseTurn(&state, "white"));
|
|
}
|
|
|
|
test "parseHalfmove parses decimal halfmove clock" {
|
|
var state = board.BoardState.empty();
|
|
|
|
try parseHalfmove(&state, "42");
|
|
try std.testing.expectEqual(@as(u8, 42), state.halfmove);
|
|
}
|
|
|
|
test "parseHalfmove rejects invalid or overflowing values" {
|
|
var state = board.BoardState.empty();
|
|
|
|
try std.testing.expectError(error.InvalidCharacter, parseHalfmove(&state, "abc"));
|
|
try std.testing.expectError(error.Overflow, parseHalfmove(&state, "256"));
|
|
}
|
|
|
|
test "parseFullmove parses decimal fullmove number" {
|
|
var state = board.BoardState.empty();
|
|
|
|
try parseFullmove(&state, "1");
|
|
try std.testing.expectEqual(@as(u32, 1), state.fullmove);
|
|
|
|
try parseFullmove(&state, "300");
|
|
try std.testing.expectEqual(@as(u32, 300), state.fullmove);
|
|
}
|
|
|
|
test "parseFullmove rejects zero and invalid values" {
|
|
var state = board.BoardState.empty();
|
|
|
|
try std.testing.expectError(error.InvalidFullmoveNumber, parseFullmove(&state, "0"));
|
|
try std.testing.expectError(error.InvalidCharacter, parseFullmove(&state, "abc"));
|
|
}
|
|
|
|
test "parseCastleRights parses no castling rights" {
|
|
var state = board.BoardState.empty();
|
|
|
|
try parseCastleRights(&state, "-");
|
|
try std.testing.expectEqual(@as(u4, 0b0000), state.castle_rights);
|
|
}
|
|
|
|
test "parseCastleRights parses individual and combined rights" {
|
|
var state = board.BoardState.empty();
|
|
|
|
try parseCastleRights(&state, "K");
|
|
try std.testing.expectEqual(@as(u4, 0b1000), state.castle_rights);
|
|
|
|
try parseCastleRights(&state, "Q");
|
|
try std.testing.expectEqual(@as(u4, 0b0100), state.castle_rights);
|
|
|
|
try parseCastleRights(&state, "k");
|
|
try std.testing.expectEqual(@as(u4, 0b0010), state.castle_rights);
|
|
|
|
try parseCastleRights(&state, "q");
|
|
try std.testing.expectEqual(@as(u4, 0b0001), state.castle_rights);
|
|
|
|
try parseCastleRights(&state, "KQkq");
|
|
try std.testing.expectEqual(@as(u4, 0b1111), state.castle_rights);
|
|
|
|
try parseCastleRights(&state, "Kq");
|
|
try std.testing.expectEqual(@as(u4, 0b1001), state.castle_rights);
|
|
|
|
try parseCastleRights(&state, "kq");
|
|
try std.testing.expectEqual(@as(u4, 0b0011), state.castle_rights);
|
|
}
|
|
|
|
test "parseCastleRights rejects invalid castling rights" {
|
|
var state = board.BoardState.empty();
|
|
|
|
try std.testing.expectError(error.InvalidCastlingRights, parseCastleRights(&state, ""));
|
|
try std.testing.expectError(error.InvalidCastlingRights, parseCastleRights(&state, "K-"));
|
|
try std.testing.expectError(error.InvalidCastlingRights, parseCastleRights(&state, "A"));
|
|
try std.testing.expectError(error.InvalidCastlingRights, parseCastleRights(&state, "KK"));
|
|
try std.testing.expectError(error.InvalidCastlingRights, parseCastleRights(&state, "qK"));
|
|
}
|
|
|
|
test "parseEnPassant parses no en passant target" {
|
|
var state = board.BoardState.empty();
|
|
|
|
try parseEnPassant(&state, "-");
|
|
try std.testing.expectEqual(@as(u7, 0), state.en_passant);
|
|
}
|
|
|
|
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, 84), state.en_passant);
|
|
|
|
state.turn = .white;
|
|
try parseEnPassant(&state, "e6");
|
|
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((@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);
|
|
}
|
|
|
|
test "parseEnPassant stores rank 3 target capturable by black pawn" {
|
|
var state = board.BoardState.empty();
|
|
state.turn = .black;
|
|
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);
|
|
}
|
|
|
|
test "parseEnPassant handles capturable edge-file targets" {
|
|
var white_state = board.BoardState.empty();
|
|
white_state.turn = .white;
|
|
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((@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 stores target even if adjacent pawn is wrong color" {
|
|
var state = board.BoardState.empty();
|
|
state.turn = .white;
|
|
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, 108), state.en_passant);
|
|
}
|
|
|
|
test "parseEnPassant rejects invalid target squares" {
|
|
var state = board.BoardState.empty();
|
|
|
|
try std.testing.expectError(error.InvalidSquare, parseEnPassant(&state, ""));
|
|
try std.testing.expectError(error.InvalidSquare, parseEnPassant(&state, "e"));
|
|
try std.testing.expectError(error.InvalidSquare, parseEnPassant(&state, "i3"));
|
|
try std.testing.expectError(error.InvalidSquare, parseEnPassant(&state, "e9"));
|
|
}
|
|
|
|
test "parseEnPassant rejects target squares outside ranks 3 and 6" {
|
|
var state = board.BoardState.empty();
|
|
|
|
try std.testing.expectError(error.InvalidEnPassantSquare, parseEnPassant(&state, "e2"));
|
|
try std.testing.expectError(error.InvalidEnPassantSquare, parseEnPassant(&state, "e4"));
|
|
try std.testing.expectError(error.InvalidEnPassantSquare, parseEnPassant(&state, "e5"));
|
|
try std.testing.expectError(error.InvalidEnPassantSquare, parseEnPassant(&state, "e7"));
|
|
}
|
|
|
|
test "parseFen parses starting position" {
|
|
const state = try parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1");
|
|
|
|
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);
|
|
try std.testing.expectEqual(@as(u8, 0), state.halfmove);
|
|
try std.testing.expectEqual(@as(u32, 1), state.fullmove);
|
|
}
|
|
|
|
test "parseFen parses capturable en passant position" {
|
|
const state = try parseFen("8/8/8/3Pp3/8/8/8/8 w - e6 0 1");
|
|
|
|
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(35));
|
|
try std.testing.expectEqual(piece.encode(.black, .pawn), state.getSquare(36));
|
|
}
|
|
|
|
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, 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(27));
|
|
try std.testing.expectEqual(piece.encode(.black, .pawn), state.getSquare(36));
|
|
}
|
|
|
|
test "parseFen rejects invalid field counts" {
|
|
try std.testing.expectError(
|
|
error.InvalidFenFieldCount,
|
|
parseFen("8/8/8/8/8/8/8/8 w - - 0"),
|
|
);
|
|
|
|
try std.testing.expectError(
|
|
error.InvalidFenFieldCount,
|
|
parseFen("8/8/8/8/8/8/8/8 w - - 0 1 extra"),
|
|
);
|
|
}
|
|
|
|
test "formatFen formats starting position" {
|
|
const expected = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
|
|
const state = try parseFen(expected);
|
|
|
|
const actual = try formatFen(std.testing.allocator, state);
|
|
defer std.testing.allocator.free(actual);
|
|
|
|
try std.testing.expectEqualStrings(expected, actual);
|
|
}
|
|
|
|
test "formatFen formats capturable en passant target" {
|
|
const expected = "8/8/8/3Pp3/8/8/8/8 w - e6 0 1";
|
|
const state = try parseFen(expected);
|
|
|
|
const actual = try formatFen(std.testing.allocator, state);
|
|
defer std.testing.allocator.free(actual);
|
|
|
|
try std.testing.expectEqualStrings(expected, actual);
|
|
}
|
|
|
|
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 e3 4 5";
|
|
const state = try parseFen(input);
|
|
|
|
const actual = try formatFen(std.testing.allocator, state);
|
|
defer std.testing.allocator.free(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));
|
|
|
|
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));
|
|
|
|
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((@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);
|
|
|
|
try std.testing.expectEqualStrings("4k2r/8/8/8/8/8/8/R3K3 b - - 42 300", actual);
|
|
}
|