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); }