const std = @import("std"); const Str = []const u8; const Chess = @import("Chess.zig"); const Attacks = @import("Attacks.zig").Attacks; const Search = @import("Search").Search; const zobrist = @import("zobrist.zig"); /// SIZE is row and column size of the chessboard. pub const SIZE = 8; pub const SquareType = u6; pub const Square = enum(SquareType) { // zig fmt: off a8, b8, c8, d8, e8, f8, g8, h8, a7, b7, c7, d7, e7, f7, g7, h7, a6, b6, c6, d6, e6, f6, g6, h6, a5, b5, c5, d5, e5, f5, g5, h5, a4, b4, c4, d4, e4, f4, g4, h4, a3, b3, c3, d3, e3, f3, g3, h3, a2, b2, c2, d2, e2, f2, g2, h2, a1, b1, c1, d1, e1, f1, g1, h1, // zig fmt: on pub fn rank(self: @This()) u3 { return @as(u3, @intCast(@intFromEnum(self) / SIZE)); } pub fn file(self: @This()) u3 { return @as(u3, @intCast(@intFromEnum(self) % SIZE)); } }; pub const BoardType = u64; pub const BitBoard = packed struct(BoardType) { // zig fmt: off a8:bool=false, b8:bool=false, c8:bool=false, d8:bool=false, e8:bool=false, f8:bool=false, g8:bool=false, h8:bool=false, a7:bool=false, b7:bool=false, c7:bool=false, d7:bool=false, e7:bool=false, f7:bool=false, g7:bool=false, h7:bool=false, a6:bool=false, b6:bool=false, c6:bool=false, d6:bool=false, e6:bool=false, f6:bool=false, g6:bool=false, h6:bool=false, a5:bool=false, b5:bool=false, c5:bool=false, d5:bool=false, e5:bool=false, f5:bool=false, g5:bool=false, h5:bool=false, a4:bool=false, b4:bool=false, c4:bool=false, d4:bool=false, e4:bool=false, f4:bool=false, g4:bool=false, h4:bool=false, a3:bool=false, b3:bool=false, c3:bool=false, d3:bool=false, e3:bool=false, f3:bool=false, g3:bool=false, h3:bool=false, a2:bool=false, b2:bool=false, c2:bool=false, d2:bool=false, e2:bool=false, f2:bool=false, g2:bool=false, h2:bool=false, a1:bool=false, b1:bool=false, c1:bool=false, d1:bool=false, e1:bool=false, f1:bool=false, g1:bool=false, h1:bool=false, // zig fmt: on fn set(self: *@This(), s: Square) void { var ret: BoardType = @as(BoardType, @bitCast(self.*)); ret |= @as(BoardType, 1) << @intFromEnum(s); self.* = @as(BitBoard, @bitCast(ret)); } pub fn setSlice(self: *@This(), squares: []const Square) void { var ret = @as(BoardType, @bitCast(self.*)); for (squares) |square| { ret |= @as(BoardType, 1) << @intFromEnum(square); } self.* = @as(BitBoard, @bitCast(ret)); } pub fn isSet(self: @This(), s: Square) bool { var ret = @as(BoardType, @bitCast(self)); return (ret & @as(BoardType, 1) << @intFromEnum(s) != 0); } fn pop(self: *@This(), s: Square) void { var ret = @as(BoardType, @bitCast(self.*)); ret ^= @as(BoardType, 1) << @intFromEnum(s); self.* = @as(BitBoard, @bitCast(ret)); } fn unSet(self: *@This(), s: Square) void { var ret = @as(BoardType, @bitCast(self.*)); ret &= ~(@as(BoardType, 1) << @intFromEnum(s)); self.* = @as(BitBoard, @bitCast(ret)); } pub fn show(self: @This()) void { std.debug.print("\n", .{}); std.debug.print("{s:>18}\n", .{"0 1 2 3 4 5 6 7"}); std.debug.print("{s:>18}\n", .{"---------------"}); var rank: usize = 0; while (rank < SIZE) : (rank += 1) { var file: usize = 0; while (file < SIZE) : (file += 1) { if (file == 0) std.debug.print("{d}| ", .{rank}); const square = @as(SquareType, @intCast(rank * SIZE + file)); const mask: usize = if (@as(BoardType, @bitCast(self)) & (@as(BoardType, 1) << square) != 0) 1 else 0; std.debug.print("{d} ", .{mask}); } std.debug.print("|{d}\n", .{SIZE - rank}); } std.debug.print("{s:>18}\n", .{"---------------"}); std.debug.print("{s:>18}\n", .{"a b c d e f g h"}); std.debug.print("\nBitBoard: {d}\n", .{@as(BoardType, @bitCast(self))}); } }; const Moves = enum { all, captures, }; pub const MovePrio = struct { move: BitMove, score: isize = undefined, pub fn moreThan(context: void, a: @This(), b: @This()) bool { _ = context; if (a.score > b.score) return true; return false; } }; pub const MoveList = std.BoundedArray(MovePrio, 128); pub const BitMoveType = u24; pub const BitMove = packed struct(BitMoveType) { source: Square, target: Square, piece: Chess.PE, prom: Chess.PE = .none, capture: bool = false, double: bool = false, enpassant: bool = false, castling: bool = false, pub fn show(self: @This()) void { // std.debug.print("source: {any}\n", .{self.source}); // std.debug.print("target: {any}\n", .{self.target}); // std.debug.print("piece: {any}\n", .{self.piece}); // std.debug.print("promote: {any}\n", .{self.prom}); // std.debug.print("capture: {any}\n", .{self.capture}); // std.debug.print("double: {any}\n", .{self.double}); // std.debug.print("enpassant: {any}\n", .{self.enpassant}); // std.debug.print("castling: {any}\n", .{self.castling}); std.debug.print("{s} {s}{s}\t", .{ @tagName(self.piece), @tagName(self.source), @tagName(self.target), }); } }; pub const CastlingType = u4; const Castling = packed struct(CastlingType) { WK: bool = false, WQ: bool = false, BK: bool = false, BQ: bool = false, // castling move in in // right update binary decimal // king & rooks didn't move: 1111 & 1111 = 1111 15 // white king moved: 1111 & 1100 = 1100 12 // white king's rook moved: 1111 & 1110 = 1110 14 // white queen's rook moved: 1111 & 1101 = 1101 13 // // black king moved: 1111 & 0011 = 1011 3 // black king's rook moved: 1111 & 1011 = 1011 11 // black queen's rook moved: 1111 & 0111 = 0111 7 // zig fmt: off const CASTLING_RIGHTS: [64]CastlingType= .{ 7, 15, 15, 15, 3, 15, 15, 11, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 13, 15, 15, 15, 12, 15, 15, 14 }; // zig fmt: on fn update(self: *@This(), square: Square) void { self.* = @as(Castling, @bitCast(@as(CastlingType, @bitCast(self.*)) & CASTLING_RIGHTS[@intFromEnum(square)])); } }; pub const GameState = struct { bitboards: [12]BitBoard = blk: { var bbs: [12]BitBoard = undefined; for (&bbs) |*bb| { bb.* = BitBoard{}; } break :blk bbs; }, // TODO: try with separate both occupancy occupancies: [@typeInfo(Chess.Colors).Enum.fields.len]BitBoard = blk: { var bbs: [2]BitBoard = undefined; for (&bbs) |*bb| { bb.* = BitBoard{}; } break :blk bbs; }, attacks: Attacks, side: Chess.Colors = .white, enpassant: ?Square = null, castling: Castling = .{}, fifty: u7 = 0, hash: u64 = undefined, history: HashHistory = undefined, allocator: std.mem.Allocator, const HashHistoryContext = struct { pub const hash = hashFn; pub const eql = eqlFn; fn hashFn(self: @This(), key: u64) u32 { _ = self; return @as(u32, @truncate(key)); } fn eqlFn(self: @This(), a: u64, b: u64, b_index: usize) bool { _ = self; _ = b_index; return a == b; } }; const HashHistory = std.ArrayHashMap(u64, void, HashHistoryContext, false); fn reset(self: *@This()) void { for (&self.bitboards) |*bb| { bb.* = .{}; } for (&self.occupancies) |*bb| { bb.* = .{}; } self.castling = .{}; } pub fn init(allocator: std.mem.Allocator, FEN: ?Str) !@This() { var gs = GameState{ .allocator = allocator, .attacks = Attacks.init() }; gs.history = HashHistory.init(allocator); zobrist.init(); if (FEN != null) try gs.parseFEN(FEN.?) else try gs.parseFEN("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); return gs; } pub fn deinit(self: *@This()) void { self.history.deinit(); } pub fn backup(self: *const @This(), new_gs: *@This()) void { for (self.bitboards, 0..) |bb, idx| { new_gs.bitboards[idx] = bb; } for (self.occupancies, 0..) |occ, idx| { new_gs.occupancies[idx] = occ; } new_gs.side = self.side; new_gs.enpassant = self.enpassant; new_gs.castling = self.castling; new_gs.hash = self.hash; new_gs.fifty = self.fifty; } pub fn restore(self: *@This(), bck: *const @This()) void { for (bck.bitboards, 0..) |bb, idx| { self.bitboards[idx] = bb; } for (bck.occupancies, 0..) |occ, idx| { self.occupancies[idx] = occ; } self.side = bck.side; self.enpassant = bck.enpassant; self.castling = bck.castling; self.hash = bck.hash; self.fifty = bck.fifty; } pub fn occupBoth(self: @This()) BoardType { return @as(BoardType, @bitCast(self.occupancies[@intFromEnum(Chess.Colors.white)])) | @as(BoardType, @bitCast(self.occupancies[@intFromEnum(Chess.Colors.black)])); } pub fn show(self: @This()) void { std.debug.print("\n", .{}); var rank: usize = 0; while (rank < SIZE) : (rank += 1) { var file: usize = 0; FILE: while (file < SIZE) : (file += 1) { if (file == 0) std.debug.print("{d}| ", .{SIZE - rank}); const square = @as(SquareType, @intCast(rank * SIZE + file)); for (self.bitboards, 0..) |bb, idx| { if (@as(BoardType, @bitCast(bb)) & (@as(BoardType, 1) << square) != 0) { std.debug.print("{c} ", .{@as(Chess.PE, @enumFromInt(idx)).char()}); continue :FILE; } } std.debug.print(". ", .{}); } std.debug.print("\n", .{}); } std.debug.print("{s:>18}\n", .{"---------------"}); std.debug.print("{s:>18}\n", .{"a b c d e f g h"}); std.debug.print("Side to move: {any}\n", .{self.side}); std.debug.print("Enpassant squares: {any}\n", .{self.enpassant}); std.debug.print("Castling: {any}\n", .{self.castling}); std.debug.print("Hash: {any}\n", .{self.hash}); } pub fn parseFEN(self: *@This(), in: []const u8) !void { self.reset(); // std.debug.print("in: {s}\n", .{in}); // parse ranks var rank: SquareType = 0; var file: SquareType = 0; var idx: usize = 0; while (idx < in.len) : ({ idx += 1; if (in[idx - 1] != '/') file += 1; }) { // std.debug.print("{c} {d}\n", .{ in[idx], rank * SIZE + file }); const ch = in[idx]; switch (ch) { '/' => { rank += 1; file = 0; }, '0'...'9' => { file += try std.fmt.parseUnsigned(SquareType, in[idx .. idx + 1], 10) - 1; }, 'P', 'N', 'B', 'R', 'Q', 'K' => { // uppercase const square = @as(Square, @enumFromInt(rank * SIZE + file)); self.occupancies[@intFromEnum(Chess.Colors.white)].set(square); switch (ch) { 'P' => self.bitboards[@intFromEnum(Chess.PE.P)].set(square), 'N' => self.bitboards[@intFromEnum(Chess.PE.N)].set(square), 'B' => self.bitboards[@intFromEnum(Chess.PE.B)].set(square), 'R' => self.bitboards[@intFromEnum(Chess.PE.R)].set(square), 'Q' => self.bitboards[@intFromEnum(Chess.PE.Q)].set(square), 'K' => self.bitboards[@intFromEnum(Chess.PE.K)].set(square), else => unreachable, } }, 'p', 'n', 'b', 'r', 'q', 'k' => { // lowercase const square = @as(Square, @enumFromInt(rank * SIZE + file)); self.occupancies[@intFromEnum(Chess.Colors.black)].set(square); switch (ch) { 'p' => self.bitboards[@intFromEnum(Chess.PE.p)].set(square), 'n' => self.bitboards[@intFromEnum(Chess.PE.n)].set(square), 'b' => self.bitboards[@intFromEnum(Chess.PE.b)].set(square), 'r' => self.bitboards[@intFromEnum(Chess.PE.r)].set(square), 'q' => self.bitboards[@intFromEnum(Chess.PE.q)].set(square), 'k' => self.bitboards[@intFromEnum(Chess.PE.k)].set(square), else => unreachable, } }, ' ' => break, else => unreachable, } } // parse rest // std.debug.print("rest: {s}\n", .{in[idx..]}); var parts = std.mem.tokenize(u8, in[idx..], " "); // side const side = parts.next().?[0]; // std.debug.print("side: {c}\n", .{side}); switch (side) { 'w' => { self.side = .white; }, 'b' => { self.side = .black; }, else => unreachable, } // castling const castling = parts.next().?; // std.debug.print("castling: {s}\n", .{castling}); for (castling) |ch| { switch (ch) { 'K' => self.castling.WK = true, 'Q' => self.castling.WQ = true, 'k' => self.castling.BK = true, 'q' => self.castling.BQ = true, '-' => break, else => unreachable, } } // enpassant const enpassant = parts.next().?; // std.debug.print("enpassant: {s}\n", .{enpassant}); if (enpassant[0] != '-') { const f = enpassant[0] - 'a'; const r = SIZE - @as( SquareType, @intCast(try std.fmt.parseUnsigned(u3, enpassant[1..], 10)), ); self.enpassant = @as(Square, @enumFromInt(r * SIZE + f)); } else self.enpassant = null; const fifty = parts.next().?; self.fifty = std.fmt.parseUnsigned(u7, fifty, 10) catch 0; zobrist.updateHash(self); return; } pub fn isSquareAttacked(self: *const @This(), square: SquareType, color: Chess.Colors) bool { switch (color) { .white => { // attacked by white pawns const pa = self.attacks.pawn_attacks[@intFromEnum(Chess.Colors.black)][square]; if (pa & @as(BoardType, @bitCast(self.bitboards[@intFromEnum(Chess.PE.P)])) != 0) return true; }, .black => { const pa = self.attacks.pawn_attacks[@intFromEnum(Chess.Colors.white)][square]; if (pa & @as(BoardType, @bitCast(self.bitboards[@intFromEnum(Chess.PE.p)])) != 0) return true; }, } // attacked by knight const na = self.attacks.knight_attacks[square]; const nbb = switch (color) { .white => self.bitboards[@intFromEnum(Chess.PE.N)], .black => self.bitboards[@intFromEnum(Chess.PE.n)], }; if (na & @as(BoardType, @bitCast(nbb)) != 0) return true; // attacked by king const ka = self.attacks.king_attacks[square]; const kbb = switch (color) { .white => self.bitboards[@intFromEnum(Chess.PE.K)], .black => self.bitboards[@intFromEnum(Chess.PE.k)], }; if (ka & @as(BoardType, @bitCast(kbb)) != 0) return true; const occup_both = self.occupBoth(); // attacked by bishop const ba = self.attacks.getBishopAttacks(square, occup_both); const bbb = switch (color) { .white => self.bitboards[@intFromEnum(Chess.PE.B)], .black => self.bitboards[@intFromEnum(Chess.PE.b)], }; if (ba.* & @as(BoardType, @bitCast(bbb)) != 0) return true; // attacked by rook const ra = self.attacks.getRookAttacks(square, occup_both); const rbb = switch (color) { .white => self.bitboards[@intFromEnum(Chess.PE.R)], .black => self.bitboards[@intFromEnum(Chess.PE.r)], }; if (ra.* & @as(BoardType, @bitCast(rbb)) != 0) return true; // attacked by queen const qbb = switch (color) { .white => self.bitboards[@intFromEnum(Chess.PE.Q)], .black => self.bitboards[@intFromEnum(Chess.PE.q)], }; if ((ba.* | ra.*) & @as(BoardType, @bitCast(qbb)) != 0) return true; return false; } pub fn generateMoves(self: *const @This(), ml: *MoveList) !void { const pieces = switch (self.side) { .white => &[_]Chess.PE{ .P, .N, .B, .R, .Q, .K }, .black => &[_]Chess.PE{ .p, .n, .b, .r, .q, .k }, }; for (pieces) |piece| { const board = @as(BoardType, @bitCast(self.bitboards[@intFromEnum(piece)])); switch (piece) { .P, .p => try self.pawnMoves(ml, board, piece), .K, .k => { try self.castlingMoves(ml, piece); try self.genMoves(ml, board, piece); }, .none => unreachable, else => try self.genMoves(ml, board, piece), } } } fn pawnMoves(self: *const @This(), ml: *MoveList, board_in: BoardType, piece: Chess.PE) !void { var source_square: SquareType = undefined; var target_square: SquareType = undefined; var attacks: BoardType = undefined; const promote_options = switch (self.side) { .white => [_]Chess.PE{ .Q, .R, .B, .N }, .black => [_]Chess.PE{ .q, .r, .b, .n }, }; var board = board_in; while (board != 0) { source_square = @as(SquareType, @intCast(@ctz(board))); // early exit on underflow/overflow switch (self.side) { .white => { const ts = @subWithOverflow(source_square, 8); if (ts[1] == 1) continue; target_square = ts[0]; }, .black => { const ts = @addWithOverflow(source_square, 8); if (ts[1] == 1) continue; target_square = ts[0]; }, } const promote_condition = switch (self.side) { .white => source_square >= @intFromEnum(Square.a7) and source_square <= @intFromEnum(Square.h7), .black => source_square >= @intFromEnum(Square.a2) and source_square <= @intFromEnum(Square.h2), }; // generate quiet moves if (self.occupBoth() & (@as(BoardType, 1) << target_square) == 0) { const double_step_condition = switch (self.side) { .white => source_square >= @intFromEnum(Square.a2) and source_square <= @intFromEnum(Square.h2) and self.occupBoth() & (@as(BoardType, 1) << (target_square - 8)) == 0, .black => source_square >= @intFromEnum(Square.a7) and source_square <= @intFromEnum(Square.h7) and self.occupBoth() & (@as(BoardType, 1) << (target_square + 8)) == 0, }; // promotion if (promote_condition) { for (promote_options) |prom| { const move = BitMove{ .source = @as(Square, @enumFromInt(source_square)), .target = @as(Square, @enumFromInt(target_square)), .piece = piece, .prom = prom, }; try ml.append(MovePrio{ .move = move }); } } else { { // single pawn move const move = BitMove{ .source = @as(Square, @enumFromInt(source_square)), .target = @as(Square, @enumFromInt(target_square)), .piece = piece, }; try ml.append(.{ .move = move }); } // double pawn move if (double_step_condition) { const double_target = switch (self.side) { .white => target_square - 8, .black => target_square + 8, }; const move = BitMove{ .source = @as(Square, @enumFromInt(source_square)), .target = @as(Square, @enumFromInt(double_target)), .piece = piece, .double = true, }; try ml.append(.{ .move = move }); } } } // generate attacks attacks = self.attacks.pawn_attacks[@intFromEnum(self.side)][source_square] & @as(BoardType, @bitCast(self.occupancies[@intFromEnum(self.side.enemy())])); while (attacks != 0) { target_square = @as(SquareType, @intCast(@ctz(attacks))); if (promote_condition) { for (promote_options) |prom| { const move = BitMove{ .source = @as(Square, @enumFromInt(source_square)), .target = @as(Square, @enumFromInt(target_square)), .piece = piece, .prom = prom, .capture = true, }; try ml.append(.{ .move = move }); } } else { const move = BitMove{ .source = @as(Square, @enumFromInt(source_square)), .target = @as(Square, @enumFromInt(target_square)), .piece = piece, .capture = true, }; try ml.append(.{ .move = move }); } // pop processed attack bit attacks ^= @as(BoardType, 1) << target_square; } // generate enpassant captures if (self.enpassant != null) { const enpassant_attacks = self.attacks.pawn_attacks[@intFromEnum(self.side)][source_square] & @as(BoardType, 1) << @intFromEnum(self.enpassant.?); if (enpassant_attacks != 0) { const target_enpassant: SquareType = @as(u6, @intCast(@ctz(enpassant_attacks))); const move = BitMove{ .source = @as(Square, @enumFromInt(source_square)), .target = @as(Square, @enumFromInt(target_enpassant)), .piece = piece, .capture = true, .enpassant = true, }; try ml.append(.{ .move = move }); } } // pop processed board bit board ^= @as(BoardType, 1) << source_square; } } fn castlingMoves(self: *const @This(), ml: *MoveList, piece: Chess.PE) !void { { // king-side castling const king_side = switch (self.side) { .white => [_]Square{ .f1, .g1 }, .black => [_]Square{ .f8, .g8 }, }; const attacked_king = switch (self.side) { .white => [_]Square{ .e1, .f1 }, .black => [_]Square{ .e8, .f8 }, }; if ((self.side == .white and self.castling.WK) or (self.side == .black and self.castling.BK)) { const OK = blk: { // make sure king side is emptry for (king_side) |square| { if (self.occupBoth() & @as(BoardType, 1) << @intFromEnum(square) != 0) break :blk false; } // check that castling squares are not attacked for (attacked_king) |square| { if (self.isSquareAttacked(@intFromEnum(square), self.side.enemy())) break :blk false; } break :blk true; }; if (OK) { const move = BitMove{ .source = attacked_king[0], .target = king_side[1], .piece = piece, .castling = true, }; try ml.append(.{ .move = move }); } } } { // queen-side castling const queen_side = switch (self.side) { .white => [_]Square{ .d1, .c1, .b1 }, .black => [_]Square{ .d8, .c8, .b8 }, }; const attacked_queen = switch (self.side) { .white => [_]Square{ .e1, .d1 }, .black => [_]Square{ .e8, .d8 }, }; if ((self.side == .white and self.castling.WQ) or (self.side == .black and self.castling.BQ)) { const OK = blk: { // make sure queen side is emptry for (queen_side) |square| { if (self.occupBoth() & @as(BoardType, 1) << @intFromEnum(square) != 0) break :blk false; } // check that castling squares are not attacked for (attacked_queen) |square| { if (self.isSquareAttacked(@intFromEnum(square), self.side.enemy())) break :blk false; } break :blk true; }; if (OK) { const move = BitMove{ .source = attacked_queen[0], .target = queen_side[1], .piece = piece, .castling = true, }; try ml.append(.{ .move = move }); } } } } fn genMoves(self: *const @This(), ml: *MoveList, board_in: BoardType, piece: Chess.PE) !void { var source_square: SquareType = undefined; var target_square: SquareType = undefined; var attacks: BoardType = undefined; var board = board_in; while (board != 0) { source_square = @as(SquareType, @intCast(@ctz(board))); const attack_mask = switch (piece) { .N, .n => self.attacks.knight_attacks[source_square], .K, .k => self.attacks.king_attacks[source_square], .B, .b => self.attacks.getBishopAttacks(source_square, self.occupBoth()).*, .R, .r => self.attacks.getRookAttacks(source_square, self.occupBoth()).*, .Q, .q => self.attacks.getQueenAttacks(source_square, self.occupBoth()), .P, .p, .none => unreachable, }; attacks = attack_mask & ~@as(BoardType, @bitCast(self.occupancies[@intFromEnum(self.side)])); while (attacks != 0) { target_square = @as(SquareType, @intCast(@ctz(attacks))); if (self.occupancies[@intFromEnum(self.side.enemy())].isSet(@as(Square, @enumFromInt(target_square)))) { const move = BitMove{ .source = @as(Square, @enumFromInt(source_square)), .target = @as(Square, @enumFromInt(target_square)), .piece = piece, .capture = true, }; try ml.append(.{ .move = move }); } else { const move = BitMove{ .source = @as(Square, @enumFromInt(source_square)), .target = @as(Square, @enumFromInt(target_square)), .piece = piece, }; try ml.append(.{ .move = move }); } attacks ^= @as(BoardType, 1) << target_square; } board ^= @as(BoardType, 1) << source_square; } } pub fn makeMove(self: *@This(), move: BitMove, flag: Moves) bool { switch (flag) { .captures => { if (move.capture) { return self.makeMove(move, .all); } // the move is not a capture return false; }, .all => { // move piece self.bitboards[@intFromEnum(move.piece)].pop(move.source); self.bitboards[@intFromEnum(move.piece)].set(move.target); // Zobrist: update piece hash self.hash ^= zobrist.piece_hashes[@intFromEnum(move.piece)][@intFromEnum(move.source)]; self.hash ^= zobrist.piece_hashes[@intFromEnum(move.piece)][@intFromEnum(move.target)]; if (move.piece == .P or move.piece == .p) { self.fifty = 0; } else { self.fifty += 1; } // handle occupancy arrays // TODO: perf test against full update used below! // { // // remove source target, one of them is no-op // self.occupancies[@enumToInt(Chess.Colors.white)].unSet(move.source); // self.occupancies[@enumToInt(Chess.Colors.black)].unSet(move.source); // // set target for the proper side // switch (self.side) { // .white => { // self.occupancies[@enumToInt(Chess.Colors.white)].set(move.target); // self.occupancies[@enumToInt(Chess.Colors.black)].unSet(move.target); // }, // .black => { // self.occupancies[@enumToInt(Chess.Colors.black)].set(move.target); // self.occupancies[@enumToInt(Chess.Colors.white)].unSet(move.target); // }, // } // } { // handling captures if (move.capture) { self.fifty = 0; const pieces = switch (self.side) { .white => [_]Chess.PE{ .p, .n, .b, .r, .q, .k }, .black => [_]Chess.PE{ .P, .N, .B, .R, .Q, .K }, }; // loop over enemy bitboards, when target occupancy found pop it for (pieces) |piece| { if (self.bitboards[@intFromEnum(piece)].isSet(move.target)) { self.bitboards[@intFromEnum(piece)].pop(move.target); // Zobrist: update capture hash self.hash ^= zobrist.piece_hashes[@intFromEnum(piece)][@intFromEnum(move.target)]; break; } } } } { // pawn stuff // handling promotions if (move.prom != .none) { // erase pawn from target square (already moved) self.bitboards[@intFromEnum(move.piece)].pop(move.target); // add promoted item to target peace self.bitboards[@intFromEnum(move.prom)].set(move.target); // Zobrist self.hash ^= zobrist.piece_hashes[@intFromEnum(move.piece)][@intFromEnum(move.target)]; self.hash ^= zobrist.piece_hashes[@intFromEnum(move.prom)][@intFromEnum(move.target)]; } // handling enpassant captures if (move.enpassant) { switch (self.side) { .white => { self.bitboards[@intFromEnum(Chess.PE.p)] .pop(@as(Square, @enumFromInt(@intFromEnum(move.target) + 8))); // Zobrist self.hash ^= zobrist.piece_hashes[@intFromEnum(Chess.PE.p)][@intFromEnum(move.target) + 8]; }, .black => { self.bitboards[@intFromEnum(Chess.PE.P)] .pop(@as(Square, @enumFromInt(@intFromEnum(move.target) - 8))); // Zobrist self.hash ^= zobrist.piece_hashes[@intFromEnum(Chess.PE.P)][@intFromEnum(move.target) - 8]; }, } } // Zobrist: remove enpassant hash if (self.enpassant != null) self.hash ^= zobrist.enpassant_hashes[@intFromEnum(self.enpassant.?)]; // reset enpassant self.enpassant = null; // handle double pawn step if (move.double) { const enpassant = switch (self.side) { .white => @as(Square, @enumFromInt(@intFromEnum(move.target) + 8)), .black => @as(Square, @enumFromInt(@intFromEnum(move.target) - 8)), }; self.enpassant = enpassant; // Zobrist: add enpassant hash self.hash ^= zobrist.enpassant_hashes[@intFromEnum(enpassant)]; } } { // handle castling if (move.castling) { // king part is already handled, take care of the rooks switch (move.target) { .g1 => { self.bitboards[@intFromEnum(Chess.PE.R)].pop(Square.h1); self.bitboards[@intFromEnum(Chess.PE.R)].set(Square.f1); // self.occupancies[@enumToInt(Chess.Colors.white)].pop(Square.h1); // self.occupancies[@enumToInt(Chess.Colors.white)].set(Square.f1); // Zobrist self.hash ^= zobrist.piece_hashes[@intFromEnum(Chess.PE.R)][@intFromEnum(Square.h1)]; self.hash ^= zobrist.piece_hashes[@intFromEnum(Chess.PE.R)][@intFromEnum(Square.f1)]; }, .c1 => { self.bitboards[@intFromEnum(Chess.PE.R)].pop(Square.a1); self.bitboards[@intFromEnum(Chess.PE.R)].set(Square.d1); // self.occupancies[@enumToInt(Chess.Colors.white)].pop(Square.a1); // self.occupancies[@enumToInt(Chess.Colors.white)].set(Square.d1); // Zobrist self.hash ^= zobrist.piece_hashes[@intFromEnum(Chess.PE.R)][@intFromEnum(Square.a1)]; self.hash ^= zobrist.piece_hashes[@intFromEnum(Chess.PE.R)][@intFromEnum(Square.d1)]; }, .g8 => { self.bitboards[@intFromEnum(Chess.PE.r)].pop(Square.h8); self.bitboards[@intFromEnum(Chess.PE.r)].set(Square.f8); // self.occupancies[@enumToInt(Chess.Colors.black)].pop(Square.h1); // self.occupancies[@enumToInt(Chess.Colors.black)].set(Square.f1); // Zobrist self.hash ^= zobrist.piece_hashes[@intFromEnum(Chess.PE.r)][@intFromEnum(Square.h8)]; self.hash ^= zobrist.piece_hashes[@intFromEnum(Chess.PE.r)][@intFromEnum(Square.f8)]; }, .c8 => { self.bitboards[@intFromEnum(Chess.PE.r)].pop(Square.a8); self.bitboards[@intFromEnum(Chess.PE.r)].set(Square.d8); // self.occupancies[@enumToInt(Chess.Colors.black)].pop(Square.a8); // self.occupancies[@enumToInt(Chess.Colors.black)].set(Square.d8); // Zobrist self.hash ^= zobrist.piece_hashes[@intFromEnum(Chess.PE.r)][@intFromEnum(Square.a8)]; self.hash ^= zobrist.piece_hashes[@intFromEnum(Chess.PE.r)][@intFromEnum(Square.d8)]; }, else => unreachable, } } // Zobrist - castling rights - remove all self.hash ^= zobrist.castle_hashes[@as(CastlingType, @bitCast(self.castling))]; // update castling rights self.castling.update(move.source); self.castling.update(move.target); // have to handle target too! // Zobrist - castling rights - reset all self.hash ^= zobrist.castle_hashes[@as(CastlingType, @bitCast(self.castling))]; } { // update occupancy boards var white: BoardType = 0; var black: BoardType = 0; for ([_]Chess.PE{ .P, .N, .B, .R, .Q, .K }) |piece| { white |= @as(BoardType, @bitCast(self.bitboards[@intFromEnum(piece)])); } for ([_]Chess.PE{ .p, .n, .b, .r, .q, .k }) |piece| { black |= @as(BoardType, @bitCast(self.bitboards[@intFromEnum(piece)])); } self.occupancies[@intFromEnum(Chess.Colors.white)] = @as(BitBoard, @bitCast(white)); self.occupancies[@intFromEnum(Chess.Colors.black)] = @as(BitBoard, @bitCast(black)); } { // make sure the move is valid (king is not in check) if (self.inCheck()) return false; } // FLIP SIDE self.side = self.side.enemy(); // update Zobrist hash self.hash ^= zobrist.side_hash; return true; }, } } pub fn inCheck(self: *const @This()) bool { const king_square = switch (self.side) { .white => @as(Square, @enumFromInt(@ctz( @as(BoardType, @bitCast(self.bitboards[@intFromEnum(Chess.PE.K)])), ))), .black => @as(Square, @enumFromInt(@ctz( @as(BoardType, @bitCast(self.bitboards[@intFromEnum(Chess.PE.k)])), ))), }; if (self.isSquareAttacked(@intFromEnum(king_square), self.side.enemy())) return true; return false; } }; test "isSquareAttacked" { var game = try GameState.init( std.testing.allocator, "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1", ); defer game.deinit(); var got: BoardType = 0; for (std.enums.values(Square)) |square| { if (game.isSquareAttacked(@intFromEnum(square), .white)) { got |= @as(BoardType, 1) << @intFromEnum(square); } } try std.testing.expectEqual(@as(BoardType, 9149624999898064896), got); } test "generateMoves" { const tricky_position = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq c6 0 1"; var gs = try GameState.init(std.testing.allocator, tricky_position); defer gs.deinit(); var ml = try MoveList.init(0); try gs.generateMoves(&ml); try std.testing.expectEqual(@as(usize, 49), ml.len); } test "backup and restore" { const tricky_position = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq c6 0 1"; var gs = try GameState.init(std.testing.allocator, tricky_position); defer gs.deinit(); var backup: GameState = undefined; gs.backup(&backup); gs.bitboards[@intFromEnum(Chess.PE.P)].b8 = true; gs.enpassant = null; gs.castling = .{}; gs.side = .black; gs.restore(&backup); try std.testing.expectEqual(false, gs.bitboards[@intFromEnum(Chess.PE.P)].h8); try std.testing.expectEqual(Square.c6, gs.enpassant.?); try std.testing.expectEqual(@as(u4, 0b1111), @as(u4, @bitCast(gs.castling))); }