836 lines
28 KiB
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);
|
|
}
|