zig-chess/tools/magic_numbers.zig

836 lines
28 KiB
Zig

const std = @import("std");
const Bitboard = u64;
const Square = u6;
const rank_1_mask: u64 = 0x0000_0000_0000_00FF;
const file_a_mask: u64 = 0x0101_0101_0101_0101;
const SlidingPiece = enum {
bishop,
rook,
fn parse(text: []const u8) ?SlidingPiece {
if (std.mem.eql(u8, text, "bishop")) return .bishop;
if (std.mem.eql(u8, text, "rook")) return .rook;
return null;
}
};
const RookAttackTableSize = 4096;
const BishopAttackTableSize = 512;
const MagicInfo = struct {
mask: Bitboard,
magic: Bitboard,
shift: u7,
};
const Config = struct {
piece: SlidingPiece = .rook,
square: ?Square = null,
seed: u64 = 0x9e37_79b9_7f4a_7c15,
attempts: u64 = 1_000_000,
output_path: ?[]const u8 = null,
};
pub fn main(init: std.process.Init) !void {
var args = try std.process.Args.Iterator.initAllocator(init.minimal.args, std.heap.page_allocator);
defer args.deinit();
_ = args.next(); // executable path
var config = Config{};
while (args.next()) |arg| {
if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
printUsage();
return;
} else if (std.mem.eql(u8, arg, "--piece")) {
const value = args.next() orelse return error.MissingPieceValue;
config.piece = SlidingPiece.parse(value) orelse return error.InvalidPiece;
} else if (std.mem.eql(u8, arg, "--square")) {
const value = args.next() orelse return error.MissingSquareValue;
const parsed = try std.fmt.parseUnsigned(u8, value, 10);
if (parsed >= 64) return error.InvalidSquare;
config.square = @intCast(parsed);
} else if (std.mem.eql(u8, arg, "--seed")) {
const value = args.next() orelse return error.MissingSeedValue;
config.seed = try std.fmt.parseUnsigned(u64, value, 0);
} else if (std.mem.eql(u8, arg, "--attempts")) {
const value = args.next() orelse return error.MissingAttemptsValue;
config.attempts = try std.fmt.parseUnsigned(u64, value, 10);
} else if (std.mem.eql(u8, arg, "--output")) {
config.output_path = args.next() orelse return error.MissingOutputValue;
} else {
std.log.err("unknown argument: {s}", .{arg});
printUsage();
return error.UnknownArgument;
}
}
std.debug.print("magic number utility scaffold\n", .{});
std.debug.print("piece: {s}\n", .{@tagName(config.piece)});
if (config.square) |square| {
std.debug.print("square: {}\n", .{square});
} else {
std.debug.print("square: all\n", .{});
}
std.debug.print("seed: 0x{x}\n", .{config.seed});
std.debug.print("attempts: {}\n\n", .{config.attempts});
std.debug.print(
"TODO: add occupancy-mask generation, attack-table generation, and random magic search here.\n" ++
"This tool intentionally does not import src/chess/ so it can evolve independently from chess implementation code.\n",
.{},
);
printBitboard(getQueenAttackMask(0));
var gpa = std.heap.DebugAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
if (config.output_path) |output_path| {
const rook_magic_info = try allocator.create([64]MagicInfo);
defer allocator.destroy(rook_magic_info);
const bishop_magic_info = try allocator.create([64]MagicInfo);
defer allocator.destroy(bishop_magic_info);
const rook_attacks = try allocator.create([64][RookAttackTableSize]Bitboard);
defer allocator.destroy(rook_attacks);
const bishop_attacks = try allocator.create([64][BishopAttackTableSize]Bitboard);
defer allocator.destroy(bishop_attacks);
rook_magic_info.* = .{@as(MagicInfo, .{ .mask = 0, .magic = 0, .shift = 0 })} ** 64;
bishop_magic_info.* = .{@as(MagicInfo, .{ .mask = 0, .magic = 0, .shift = 0 })} ** 64;
rook_attacks.* = .{.{0} ** RookAttackTableSize} ** 64;
bishop_attacks.* = .{.{0} ** BishopAttackTableSize} ** 64;
var prng = std.Random.DefaultPrng.init(config.seed);
const random = prng.random();
for (0..64) |square_index| {
const square: Square = @intCast(square_index);
try generateRookMagicNumber(square, random, rook_magic_info, rook_attacks, allocator, config.attempts);
try generateBishopMagicNumber(square, random, bishop_magic_info, bishop_attacks, allocator, config.attempts);
std.debug.print("generated magic tables for square {}\n", .{square_index});
}
try writeGeneratedMagicTables(
output_path,
rook_magic_info,
bishop_magic_info,
rook_attacks,
bishop_attacks,
allocator,
init.io,
);
std.debug.print("wrote generated magic tables to {s}\n", .{output_path});
}
}
fn printUsage() void {
std.debug.print(
\\Usage:
\\ zig build run-magic-numbers -- [options]
\\
\\Options:
\\ --piece rook|bishop Sliding piece to search for. Default: rook
\\ --square 0..63 Single square to search. Default: all squares
\\ --seed N Random seed. Supports decimal or 0x-prefixed hex
\\ --attempts N Candidate magic numbers to try per square
\\ --output PATH Write generated Zig tables to PATH
\\ -h, --help Show this help
\\
,
.{},
);
}
pub fn bit(square: Square) Bitboard {
return @as(Bitboard, 1) << square;
}
pub fn getRookAttackMask(square: Square) Bitboard {
const rank = square / 8;
const file = square % 8;
const rank_mask = rank_1_mask << (rank * 8);
const file_mask = file_a_mask << file;
var rook_mask = (rank_mask | file_mask) & ~bit(square);
if (rank_mask != rank_1_mask) {
rook_mask = rook_mask & ~rank_1_mask;
}
if (rank_mask != (rank_1_mask << 56)) {
rook_mask = rook_mask & ~(rank_1_mask << 56);
}
if (file_mask != file_a_mask) {
rook_mask = rook_mask & ~file_a_mask;
}
if (file_mask != (file_a_mask << 7)) {
rook_mask = rook_mask & ~(file_a_mask << 7);
}
return rook_mask;
}
pub fn generateRookMagicNumber(square: Square, random: std.Random, rook_info: *[64]MagicInfo, rook_attacks: *[64][RookAttackTableSize]Bitboard, allocator: std.mem.Allocator, max_attempts: u64) !void {
const mask = getRookAttackMask(square);
const set_bits = try getMaskSetBits(mask, allocator);
defer allocator.free(set_bits);
const relevant_bits = set_bits.len;
const table_size = @as(usize, 1) << @intCast(relevant_bits);
const shift: u7 = @intCast(64 - relevant_bits);
const occupancies = try getAllPotentialOccupiedSquares(set_bits, allocator);
defer allocator.free(occupancies);
const expected_attacks = try allocator.alloc(Bitboard, table_size);
defer allocator.free(expected_attacks);
for (occupancies, 0..) |occupancy, i| {
expected_attacks[i] = getRookAttacks(square, occupancy);
}
const temp_table = try allocator.alloc(Bitboard, table_size);
defer allocator.free(temp_table);
const used = try allocator.alloc(bool, table_size);
defer allocator.free(used);
var found_magic: Bitboard = 0;
var attempts: u64 = 0;
while (attempts < max_attempts) : (attempts += 1) {
const magic = magicRandom(random);
if (testMagicCandidate(magic, shift, occupancies, expected_attacks, temp_table, used)) {
found_magic = magic;
break;
}
}
if (found_magic == 0) return error.MagicNotFound;
rook_info[square] = .{
.magic = found_magic,
.shift = shift,
.mask = mask,
};
@memset(&rook_attacks[square], 0);
for (temp_table, 0..) |attack, i| {
rook_attacks[square][i] = attack;
}
}
pub fn generateBishopMagicNumber(square: Square, random: std.Random, bishop_info: *[64]MagicInfo, bishop_attacks: *[64][BishopAttackTableSize]Bitboard, allocator: std.mem.Allocator, max_attempts: u64) !void {
const mask = getBishopAttackMask(square);
const set_bits = try getMaskSetBits(mask, allocator);
defer allocator.free(set_bits);
const relevant_bits = set_bits.len;
const table_size = @as(usize, 1) << @intCast(relevant_bits);
const shift: u7 = @intCast(64 - relevant_bits);
const occupancies = try getAllPotentialOccupiedSquares(set_bits, allocator);
defer allocator.free(occupancies);
const expected_attacks = try allocator.alloc(Bitboard, table_size);
defer allocator.free(expected_attacks);
for (occupancies, 0..) |occupancy, i| {
expected_attacks[i] = getBishopAttacks(square, occupancy);
}
const temp_table = try allocator.alloc(Bitboard, table_size);
defer allocator.free(temp_table);
const used = try allocator.alloc(bool, table_size);
defer allocator.free(used);
var found_magic: Bitboard = 0;
var attempts: u64 = 0;
while (attempts < max_attempts) : (attempts += 1) {
const magic = magicRandom(random);
if (testMagicCandidate(magic, shift, occupancies, expected_attacks, temp_table, used)) {
found_magic = magic;
break;
}
}
if (found_magic == 0) return error.MagicNotFound;
bishop_info[square] = .{
.mask = mask,
.magic = found_magic,
.shift = shift,
};
@memset(&bishop_attacks[square], 0);
for (temp_table, 0..) |attack, i| {
bishop_attacks[square][i] = attack;
}
}
fn testMagicCandidate(
magic: Bitboard,
shift: u7,
occupancies: []const Bitboard,
expected_attacks: []const Bitboard,
temp_table: []Bitboard,
used: []bool,
) bool {
@memset(temp_table, 0);
@memset(used, false);
for (occupancies, 0..) |occupancy, i| {
const shift_amount: u6 = @intCast(shift);
const index: usize = @intCast((occupancy *% magic) >> shift_amount);
const expected = expected_attacks[i];
if (!used[index]) {
used[index] = true;
temp_table[index] = expected;
} else if (temp_table[index] != expected) {
return false;
}
}
return true;
}
fn magicRandom(random: std.Random) u64 {
return random.int(u64) & random.int(u64) & random.int(u64);
}
fn writeGeneratedMagicTables(
output_path: []const u8,
rook_info: *const [64]MagicInfo,
bishop_info: *const [64]MagicInfo,
rook_attacks: *const [64][RookAttackTableSize]Bitboard,
bishop_attacks: *const [64][BishopAttackTableSize]Bitboard,
allocator: std.mem.Allocator,
io: std.Io,
) !void {
var allocating_writer = std.Io.Writer.Allocating.init(allocator);
defer allocating_writer.deinit();
try writeMagicTables(&allocating_writer.writer, rook_info, bishop_info, rook_attacks, bishop_attacks);
var contents = allocating_writer.toArrayList();
defer contents.deinit(allocator);
try std.Io.Dir.cwd().writeFile(io, .{
.sub_path = output_path,
.data = contents.items,
});
}
fn writeMagicTables(
writer: *std.Io.Writer,
rook_info: *const [64]MagicInfo,
bishop_info: *const [64]MagicInfo,
rook_attacks: *const [64][RookAttackTableSize]Bitboard,
bishop_attacks: *const [64][BishopAttackTableSize]Bitboard,
) std.Io.Writer.Error!void {
try writer.print(
\\// Generated by tools/magic_numbers.zig. Do not edit by hand.
\\const bitboard = @import("bitboard.zig");
\\
\\pub const Bitboard = bitboard.Bitboard;
\\
\\pub const MagicInfo = struct {{
\\ mask: Bitboard,
\\ magic: Bitboard,
\\ shift: u7,
\\}};
\\
\\pub const RookAttackTableSize = {};
\\pub const BishopAttackTableSize = {};
\\
,
.{ RookAttackTableSize, BishopAttackTableSize },
);
try writeMagicInfoArray(writer, "rook_magic_info", rook_info);
try writeMagicInfoArray(writer, "bishop_magic_info", bishop_info);
try writeAttackTable(writer, "rook_attacks", RookAttackTableSize, rook_attacks);
try writeAttackTable(writer, "bishop_attacks", BishopAttackTableSize, bishop_attacks);
}
fn writeMagicInfoArray(writer: *std.Io.Writer, name: []const u8, infos: *const [64]MagicInfo) std.Io.Writer.Error!void {
try writer.print("pub const {s}: [64]MagicInfo = .{{\n", .{name});
for (infos) |info| {
try writer.print(" .{{ .mask = 0x{x}, .magic = 0x{x}, .shift = {} }},\n", .{ info.mask, info.magic, info.shift });
}
try writer.print("}};\n\n", .{});
}
fn writeAttackTable(
writer: *std.Io.Writer,
name: []const u8,
comptime table_size: usize,
attacks: *const [64][table_size]Bitboard,
) std.Io.Writer.Error!void {
try writer.print("pub const {s}: [64][{}]Bitboard = .{{\n", .{ name, table_size });
for (attacks) |square_attacks| {
try writer.print(" .{{\n", .{});
for (square_attacks, 0..) |attack, i| {
if (i % 4 == 0) try writer.print(" ", .{});
try writer.print("0x{x}, ", .{attack});
if (i % 4 == 3) try writer.print("\n", .{});
}
if (table_size % 4 != 0) try writer.print("\n", .{});
try writer.print(" }},\n", .{});
}
try writer.print("}};\n\n", .{});
}
pub fn getRookAttacks(square: Square, blockers: Bitboard) Bitboard {
const rank: i32 = @intCast(square / 8);
const file: i32 = @intCast(square % 8);
var attacks: Bitboard = 0;
var r = rank + 1;
var f = file;
while (r <= 7) : (r += 1) {
const target: Square = @intCast(r * 8 + f);
attacks |= @as(Bitboard, 1) << target;
if ((blockers & (@as(Bitboard, 1) << target)) != 0) break;
}
r = rank - 1;
f = file;
while (r >= 0) : (r -= 1) {
const target: Square = @intCast(r * 8 + f);
attacks |= @as(Bitboard, 1) << target;
if ((blockers & (@as(Bitboard, 1) << target)) != 0) break;
}
r = rank;
f = file + 1;
while (f <= 7) : (f += 1) {
const target: Square = @intCast(r * 8 + f);
attacks |= @as(Bitboard, 1) << target;
if ((blockers & (@as(Bitboard, 1) << target)) != 0) break;
}
r = rank;
f = file - 1;
while (f >= 0) : (f -= 1) {
const target: Square = @intCast(r * 8 + f);
attacks |= @as(Bitboard, 1) << target;
if ((blockers & (@as(Bitboard, 1) << target)) != 0) break;
}
return attacks;
}
pub fn getBishopAttacks(square: Square, blockers: Bitboard) Bitboard {
const rank: i32 = @intCast(square / 8);
const file: i32 = @intCast(square % 8);
var attacks: Bitboard = 0;
var r = rank + 1;
var f = file + 1;
while (r <= 7 and f <= 7) {
const target: Square = @intCast(r * 8 + f);
attacks |= @as(Bitboard, 1) << target;
if ((blockers & (@as(Bitboard, 1) << target)) != 0) break;
r += 1;
f += 1;
}
r = rank - 1;
f = file + 1;
while (r >= 0 and f <= 7) {
const target: Square = @intCast(r * 8 + f);
attacks |= @as(Bitboard, 1) << target;
if ((blockers & (@as(Bitboard, 1) << target)) != 0) break;
r -= 1;
f += 1;
}
r = rank + 1;
f = file - 1;
while (r <= 7 and f >= 0) {
const target: Square = @intCast(r * 8 + f);
attacks |= @as(Bitboard, 1) << target;
if ((blockers & (@as(Bitboard, 1) << target)) != 0) break;
r += 1;
f -= 1;
}
r = rank - 1;
f = file - 1;
while (r >= 0 and f >= 0) {
const target: Square = @intCast(r * 8 + f);
attacks |= @as(Bitboard, 1) << target;
if ((blockers & (@as(Bitboard, 1) << target)) != 0) break;
r -= 1;
f -= 1;
}
return attacks;
}
pub fn getQueenAttacks(square: Square, blockers: Bitboard) Bitboard {
return getRookAttacks(square, blockers) | getBishopAttacks(square, blockers);
}
pub fn getBishopAttackMask(square: Square) Bitboard {
const rank = square / 8;
const file = square % 8;
var bishop_mask: Bitboard = 0;
const rank_i: i32 = @intCast(rank);
const file_i: i32 = @intCast(file);
var r: i32 = rank_i + 1;
var f: i32 = file_i + 1;
while (r < 7 and f < 7) {
bishop_mask |= @as(Bitboard, 1) << @intCast(r * 8 + f);
r += 1;
f += 1;
}
r = rank_i + 1;
f = file_i - 1;
while (r < 7 and f > 0) {
bishop_mask |= @as(Bitboard, 1) << @intCast(r * 8 + f);
r += 1;
f -= 1;
}
r = rank_i - 1;
f = file_i + 1;
while (r > 0 and f < 7) {
bishop_mask |= @as(Bitboard, 1) << @intCast(r * 8 + f);
r -= 1;
f += 1;
}
r = rank_i - 1;
f = file_i - 1;
while (r > 0 and f > 0) {
bishop_mask |= @as(Bitboard, 1) << @intCast(r * 8 + f);
r -= 1;
f -= 1;
}
return bishop_mask;
}
fn getMaskSetBits(bitboard: Bitboard, allocator: std.mem.Allocator) ![]u6 {
const count: usize = @intCast(@popCount(bitboard));
const squares = try allocator.alloc(u6, count);
var bb = bitboard;
var i: usize = 0;
while (bb != 0) {
const square: u6 = @intCast(@ctz(bb));
squares[i] = square;
i += 1;
bb &= bb - 1;
}
return squares;
}
fn getAllPotentialOccupiedSquares(squares: []u6, allocator: std.mem.Allocator) ![]Bitboard {
const count = @as(usize, 1) << @intCast(squares.len);
const bitboards: []Bitboard = try allocator.alloc(Bitboard, count);
var combo: usize = 0;
while (combo < count) : (combo += 1) {
var occupancy: Bitboard = 0;
for (squares, 0..) |square, i| {
const combo_bit = @as(usize, 1) << @intCast(i);
if ((combo & combo_bit) != 0) {
occupancy |= @as(u64, 1) << square;
}
}
bitboards[combo] = occupancy;
}
return bitboards;
}
pub fn getQueenAttackMask(square: Square) Bitboard {
return getRookAttackMask(square) | getBishopAttackMask(square);
}
fn printBitboard(bb: u64) void {
var rank: i32 = 7;
while (rank >= 0) : (rank -= 1) {
var file: u6 = 0;
while (file < 8) : (file += 1) {
const square: u6 = @intCast((rank * 8) + file);
const mask = @as(u64, 1) << square;
if ((bb & mask) != 0) {
std.debug.print("1 ", .{});
} else {
std.debug.print(". ", .{});
}
}
std.debug.print("\n", .{});
}
std.debug.print("\n", .{});
}
test "bit returns a bitboard with one square set" {
try std.testing.expectEqual(@as(Bitboard, 1), bit(0));
try std.testing.expectEqual(@as(Bitboard, 1) << 63, bit(63));
}
test "rook attack mask for A1 excludes self and board edges" {
try std.testing.expectEqual(@as(Bitboard, 0x0001_0101_0101_017E), getRookAttackMask(0));
}
test "rook attack mask for D4 excludes self and board edges" {
try std.testing.expectEqual(@as(Bitboard, 0x0008_0808_7608_0800), getRookAttackMask(27));
}
test "rook attack mask for H8 excludes self and board edges" {
try std.testing.expectEqual(@as(Bitboard, 0x7E80_8080_8080_8000), getRookAttackMask(63));
}
test "rook attack mask for A4 excludes self and board edges" {
try std.testing.expectEqual(@as(Bitboard, 0x0001_0101_7E01_0100), getRookAttackMask(24));
}
test "rook attack mask for D1 excludes self and board edges" {
try std.testing.expectEqual(@as(Bitboard, 0x0008_0808_0808_0876), getRookAttackMask(3));
}
test "bishop attack mask for A1 excludes self and board edges" {
try std.testing.expectEqual(@as(Bitboard, 0x0040_2010_0804_0200), getBishopAttackMask(0));
}
test "bishop attack mask for D4 excludes self and board edges" {
try std.testing.expectEqual(@as(Bitboard, 0x0040_2214_0014_2200), getBishopAttackMask(27));
}
test "bishop attack mask for H8 excludes self and board edges" {
try std.testing.expectEqual(@as(Bitboard, 0x0040_2010_0804_0200), getBishopAttackMask(63));
}
test "bishop attack mask for A4 excludes self and board edges" {
try std.testing.expectEqual(@as(Bitboard, 0x0008_0402_0002_0400), getBishopAttackMask(24));
}
test "bishop attack mask for D1 excludes self and board edges" {
try std.testing.expectEqual(@as(Bitboard, 0x0000_0000_4022_1400), getBishopAttackMask(3));
}
test "queen attack mask for A1 combines rook and bishop masks" {
try std.testing.expectEqual(@as(Bitboard, 0x0041_2111_0905_037E), getQueenAttackMask(0));
}
test "queen attack mask for D4 combines rook and bishop masks" {
try std.testing.expectEqual(@as(Bitboard, 0x0048_2A1C_761C_2A00), getQueenAttackMask(27));
}
test "queen attack mask for H8 combines rook and bishop masks" {
try std.testing.expectEqual(@as(Bitboard, 0x7EC0_A090_8884_8200), getQueenAttackMask(63));
}
test "queen attack mask for A4 combines rook and bishop masks" {
try std.testing.expectEqual(@as(Bitboard, 0x0009_0503_7E03_0500), getQueenAttackMask(24));
}
test "queen attack mask for D1 combines rook and bishop masks" {
try std.testing.expectEqual(@as(Bitboard, 0x0008_0808_482A_1C76), getQueenAttackMask(3));
}
test "rook attacks from D4 on empty board include board edges" {
try std.testing.expectEqual(@as(Bitboard, 0x0808_0808_F708_0808), getRookAttacks(27, 0));
}
test "rook attacks from D4 stop at nearest blockers and include blocker squares" {
const blockers = bit(35) | bit(25) | bit(30) | bit(11) | bit(45) | bit(9);
try std.testing.expectEqual(@as(Bitboard, 0x0000_0008_7608_0800), getRookAttacks(27, blockers));
}
test "rook attacks from A1 on empty board include rank and file edges" {
try std.testing.expectEqual(@as(Bitboard, 0x0101_0101_0101_01FE), getRookAttacks(0, 0));
}
test "rook attacks from A1 stop at adjacent blockers" {
const blockers = bit(8) | bit(1) | bit(9);
try std.testing.expectEqual(@as(Bitboard, 0x0000_0000_0000_0102), getRookAttacks(0, blockers));
}
test "bishop attacks from D4 on empty board include diagonal edges" {
try std.testing.expectEqual(@as(Bitboard, 0x8041_2214_0014_2241), getBishopAttacks(27, 0));
}
test "bishop attacks from D4 stop at nearest blockers and include blocker squares" {
const blockers = bit(35) | bit(25) | bit(30) | bit(11) | bit(45) | bit(9);
try std.testing.expectEqual(@as(Bitboard, 0x0001_2214_0014_2240), getBishopAttacks(27, blockers));
}
test "bishop attacks from A1 on empty board include diagonal edge" {
try std.testing.expectEqual(@as(Bitboard, 0x8040_2010_0804_0200), getBishopAttacks(0, 0));
}
test "bishop attacks from A1 stop at adjacent diagonal blocker" {
const blockers = bit(8) | bit(1) | bit(9);
try std.testing.expectEqual(@as(Bitboard, 0x0000_0000_0000_0200), getBishopAttacks(0, blockers));
}
test "queen attacks from D4 combine rook and bishop attacks on empty board" {
try std.testing.expectEqual(@as(Bitboard, 0x8849_2A1C_F71C_2A49), getQueenAttacks(27, 0));
}
test "queen attacks from D4 combine rook and bishop attacks with blockers" {
const blockers = bit(35) | bit(25) | bit(30) | bit(11) | bit(45) | bit(9);
try std.testing.expectEqual(@as(Bitboard, 0x0001_221C_761C_2A40), getQueenAttacks(27, blockers));
}
test "queen attacks from A1 on empty board combine rook and bishop edges" {
try std.testing.expectEqual(@as(Bitboard, 0x8141_2111_0905_03FE), getQueenAttacks(0, 0));
}
test "queen attacks from A1 stop at adjacent blockers" {
const blockers = bit(8) | bit(1) | bit(9);
try std.testing.expectEqual(@as(Bitboard, 0x0000_0000_0000_0302), getQueenAttacks(0, blockers));
}
test "getMaskSetBits returns an empty slice for an empty bitboard" {
const squares = try getMaskSetBits(0, std.testing.allocator);
defer std.testing.allocator.free(squares);
try std.testing.expectEqual(@as(usize, 0), squares.len);
}
test "getMaskSetBits returns set square indexes from least to greatest" {
const mask = bit(0) | bit(7) | bit(27) | bit(63);
const squares = try getMaskSetBits(mask, std.testing.allocator);
defer std.testing.allocator.free(squares);
try std.testing.expectEqualSlices(u6, &.{ 0, 7, 27, 63 }, squares);
}
test "getMaskSetBits returns every set bit from a rook mask" {
const squares = try getMaskSetBits(getRookAttackMask(0), std.testing.allocator);
defer std.testing.allocator.free(squares);
try std.testing.expectEqualSlices(u6, &.{ 1, 2, 3, 4, 5, 6, 8, 16, 24, 32, 40, 48 }, squares);
}
test "getAllPotentialOccupiedSquares returns only the empty occupancy for no squares" {
var input = [_]u6{};
const occupancies = try getAllPotentialOccupiedSquares(&input, std.testing.allocator);
defer std.testing.allocator.free(occupancies);
try std.testing.expectEqual(@as(usize, 1), occupancies.len);
try std.testing.expectEqual(@as(Bitboard, 0), occupancies[0]);
}
test "getAllPotentialOccupiedSquares returns every subset for three squares" {
var input = [_]u6{ 1, 3, 8 };
const occupancies = try getAllPotentialOccupiedSquares(&input, std.testing.allocator);
defer std.testing.allocator.free(occupancies);
try std.testing.expectEqual(@as(usize, 8), occupancies.len);
try std.testing.expectEqualSlices(Bitboard, &.{
0,
bit(1),
bit(3),
bit(1) | bit(3),
bit(8),
bit(1) | bit(8),
bit(3) | bit(8),
bit(1) | bit(3) | bit(8),
}, occupancies);
}
test "getAllPotentialOccupiedSquares count is two to the number of squares" {
var input = [_]u6{ 1, 2, 3, 4, 5 };
const occupancies = try getAllPotentialOccupiedSquares(&input, std.testing.allocator);
defer std.testing.allocator.free(occupancies);
try std.testing.expectEqual(@as(usize, 32), occupancies.len);
}
test "testMagicCandidate accepts a magic that gives unique indexes" {
const occupancies = [_]Bitboard{ 0, bit(0), bit(1), bit(0) | bit(1) };
const expected_attacks = [_]Bitboard{ 0x11, 0x22, 0x44, 0x88 };
var temp_table = [_]Bitboard{0} ** 4;
var used = [_]bool{false} ** 4;
const magic = @as(Bitboard, 1) << 62;
try std.testing.expect(testMagicCandidate(
magic,
62,
&occupancies,
&expected_attacks,
&temp_table,
&used,
));
try std.testing.expectEqualSlices(Bitboard, &expected_attacks, &temp_table);
try std.testing.expectEqualSlices(bool, &[_]bool{ true, true, true, true }, &used);
}
test "testMagicCandidate rejects harmful collisions" {
const occupancies = [_]Bitboard{ 0, bit(0) };
const expected_attacks = [_]Bitboard{ 0x11, 0x22 };
var temp_table = [_]Bitboard{0} ** 2;
var used = [_]bool{false} ** 2;
try std.testing.expect(!testMagicCandidate(
0,
63,
&occupancies,
&expected_attacks,
&temp_table,
&used,
));
}
test "testMagicCandidate allows constructive collisions with identical attacks" {
const occupancies = [_]Bitboard{ 0, bit(0), bit(1), bit(0) | bit(1) };
const expected_attacks = [_]Bitboard{ 0x55, 0x55, 0x55, 0x55 };
var temp_table = [_]Bitboard{0} ** 1;
var used = [_]bool{false} ** 1;
try std.testing.expect(testMagicCandidate(
0,
63,
&occupancies,
&expected_attacks,
&temp_table,
&used,
));
try std.testing.expect(used[0]);
try std.testing.expectEqual(@as(Bitboard, 0x55), temp_table[0]);
}
test "writeMagicInfoArray emits Zig constants" {
var infos = [_]MagicInfo{.{ .mask = 0, .magic = 0, .shift = 0 }} ** 64;
infos[0] = .{ .mask = 0x12, .magic = 0x34, .shift = 56 };
var allocating_writer = std.Io.Writer.Allocating.init(std.testing.allocator);
defer allocating_writer.deinit();
try writeMagicInfoArray(&allocating_writer.writer, "test_infos", &infos);
const output = allocating_writer.writer.buffer[0..allocating_writer.writer.end];
try std.testing.expect(std.mem.indexOf(u8, output, "pub const test_infos: [64]MagicInfo") != null);
try std.testing.expect(std.mem.indexOf(u8, output, ".{ .mask = 0x12, .magic = 0x34, .shift = 56 },") != null);
}
test "writeAttackTable emits nested Zig attack arrays" {
var attacks = [_][2]Bitboard{.{ 0, 0 }} ** 64;
attacks[0] = .{ 0x11, 0x22 };
var allocating_writer = std.Io.Writer.Allocating.init(std.testing.allocator);
defer allocating_writer.deinit();
try writeAttackTable(&allocating_writer.writer, "test_attacks", 2, &attacks);
const output = allocating_writer.writer.buffer[0..allocating_writer.writer.end];
try std.testing.expect(std.mem.indexOf(u8, output, "pub const test_attacks: [64][2]Bitboard") != null);
try std.testing.expect(std.mem.indexOf(u8, output, "0x11, 0x22,") != null);
}