CU3XNAGXZHXACBEDPM3THFT6AWAJ4HC5PXZM3Z72WWVMSHPOOXFQC EVNZGIR45TJBV5XC5K3NKWIW2IP75QW4R3LMNTSP7BWTRHNFDRDQC 75BIO4XKTLVWVHPXEJ6V45DEP7CFWFFRFF4NAOPQN2MESJG6HUWQC Z4PX3GURAMMW55R5KRP5WCO2CXFEI6LBULGBQRYTABBONCOZ5NHQC ZO7Y4ICU3LPK6SBWPJLBSAEXJMWGEFONSB53KXPSUOPPG7JZVHKAC 5HQBXRO5NILZUAMHJ724XKHASMJMOASA33JCWR2BCI7U5QZN2FUAC X3FYJUNL5ORLRC7TW3C5OMNZOX4JEWL73RXOQZLRKLAUBVNARIMAC R5F5KMWWZ4VS67CSIWRNRM4LNPPXVXSQZLHEZMT6XZODV4AK2MBQC XRCSCQWQKVYASIMAJO7JVUJXHXE44FZROCJPBW2BR7EE4RPEIBKAC FOORIA7SEZCLKDBNMV6KEDTQOJJVAH57BQFBRSQMET6FERJPHCVQC A46B5KNQFPTZEIL2JZKD2CQELGU3COE6FGTV2ABS5U7DVTVDGEBQC ZMNSRH5SD3LEUSRTVRZODEIOZIAOPXJQFW4DNMLAKOWVEAQUTFLAC 2TRWSYAHRLVT2V6FGRNHKYQZKFDCMKMJOI2G3HYMOVLSJMGSBFDQC FRUDIRWXGFOZERET3DNUNAZ5HSA3G32JZX6WMIXNGZOACTTCRIQAC HNCUAGWTMX3UHH2KFTUM3M6Q4CC33M6PVE5DWZWFG5UM2Y7J2ZKAC NCFUC2F34HIV6BZYYMMHIGQOD6G6BUJZYY4RINPCAKGCSVCC46DAC WAJ2J7G4QZE4LLGGZNIQ3ZIBCR3CSFOH3V2NIK2JQPALFHARRNMAC 46RTAHPCLRHCLSYGKZIFIFRIWPJ54TJJ2WM5M5FZNNHUSZZZZAYQC 6S6HHKOXBQ5ARL7S7PKMPDIAH3VTPOAPWWSMGY37RP2R2U662FBAC 3H3DSWLBWCE7C43W6LHICCPJ5KQDLASLQPIKJOK2KAE6OFF2L6WAC GLTWBA5NKQIHAL23SWKCFOZQH62W6B3LX4RC5GFQUEQUIYN4OKSAC WZNDXPQM23LLYDZ3ZCTB4GOVRJLL7PMSEPSYBVFX6AOTJOZ5YYZQC ARKJ6V5BZRW2R5WJHAFDCO5LEZDS75BFPSXWNKTQK4BYT4DCQGHQC BL3ZR4OWJM54HFXUNMUZKB5YQYVBT7ETFIXCOXWL6S5SZFM6IFDQC RNEXG5IFDKMHSUR6RMNTI3Y32ORLVMZ6UJYKHLV2XBMT2QONBTVQC G4HJL4QLASCZBWYGCEXYYRBYL7UVX6ENELHRRBFB5UAXXGVVGZGQC V34YWVR66ERHUV6SOYQWJQLYMPLWEIJDXCR44B3SLPN2CRYIR45QC TB4YBE4CMWCLSKJ43QF6IU5HVYUUO33BLXVG7XDRLJS3IFIBQLYAC WSRRWTTVBE2K6NKPS54CXMY3QYK5YTDRGEH3YJ53VQ2XW5UM5O2QC THYIOCFC72V4ZMTLMGRGRIBARXT2LPSQCL2XNOIBI22QRVYUMIYQC const search = @import("search.zig");
// https://www.wbec-ridderkerk.nl/html/UCIProtocol.htmlpub fn loop(allocator: std.mem.Allocator) !void {var gs = try GameState.init(allocator, null);var search = Search.init(&gs);var buf: [512]u8 = undefined;const stdin = std.io.getStdIn().reader();const stdout = std.io.getStdOut();try hello();
pub fn parseGo(gs: *GameState, in: Str) !void {
while (true) {const input = try stdin.readUntilDelimiterOrEof(&buf, '\n') orelse break;if (std.mem.eql(u8, input, "quit")) {std.log.debug("received UCI quit, quitting...", .{});break;}if (std.mem.eql(u8, input, "debug on")) {log_level = std.log.Level.debug;std.log.debug("log level set to debug", .{});} else if (std.mem.eql(u8, input, "debug off")) {log_level = std.log.default_level;}if (std.mem.eql(u8, input, "stop")) {std.log.debug("received UCI stop command", .{});@atomicStore(bool, &search.stop, true, std.builtin.AtomicOrder.Unordered);std.log.debug("searching stopped", .{});}if (std.mem.eql(u8, input, "isready")) _ = try stdout.write("readyok\n");if (input.len >= 8) {if (std.mem.eql(u8, input[0..8], "position")) {try parsePosition(&gs, input);gs.show();}}if (input.len >= 3) {if (std.mem.eql(u8, input[0..3], "uci")) {if (std.mem.eql(u8, input, "ucinewgame")) {try parsePosition(&gs, "position startpos");std.log.debug("new uci game initialized", .{});} else {try hello();}}}if (input.len >= 2) {if (std.mem.eql(u8, input[0..2], "go")) {const srch = try std.Thread.spawn(std.Thread.SpawnConfig{},parseGo,.{ &search, input },);srch.detach();}}}}fn hello() !void {const stdout = std.io.getStdOut();_ = try stdout.write("id name pistike\n");_ = try stdout.write("id author voroskoi\n");_ = try stdout.write("uciok\n");}pub fn parseGo(search: *Search, in: Str) !void {
try search.bestMove(&gs, 8);} else try handleUCI(arena.allocator());
try search.bestMove(8);} else try uci.loop(arena.allocator());
fn handleUCI(allocator: std.mem.Allocator) !void {var gs = try Board.GameState.init(allocator, null);var buf: [512]u8 = undefined;const stdin = std.io.getStdIn().reader();const stdout = std.io.getStdOut();while (true) {const input = try stdin.readUntilDelimiterOrEof(&buf, '\n') orelse break;if (std.mem.eql(u8, input, "quit")) break;if (std.mem.eql(u8, input, "isready")) _ = try stdout.write("readyok\n");
if (input.len >= 8) {if (std.mem.eql(u8, input[0..8], "position")) {try uci.parsePosition(&gs, input);gs.show();}}if (input.len >= 3) {if (std.mem.eql(u8, input[0..3], "uci")) {if (std.mem.eql(u8, input, "ucinewgame"))try uci.parsePosition(&gs, "position startpos")else_ = try stdout.write("id name pistike\nuciok\n");}}if (input.len >= 2) {if (std.mem.eql(u8, input[0..2], "go")) {try uci.parseGo(&gs, input);}}}}
// Late move reductionconst FULL_DEPTH_MOVES = 4;const REDUCTION_LIMIT = 3;pub fn bestMove(gs: *GameState, depth: usize) !void {const ALPHA = -25_000;const BETA = 25_000;
// NULL move pruningconst NULL_REDUCTION = 2;
// clear helper data for search{score.killer_moves = [_][MAX_PLY]BitMove{[_]BitMove{@bitCast(BitMove, @as(BitMoveType, 0))} ** MAX_PLY,} ** 2;score.history_moves = [_][64]usize{[_]usize{0} ** 64} ** 12;pv_length = [_]usize{0} ** MAX_PLY;pv_table = [_][MAX_PLY]BitMove{[_]BitMove{@bitCast(BitMove, @as(BitMoveType, 0))} ** MAX_PLY,} ** MAX_PLY;nodes = 0;
pub fn init(gs: *GameState) @This() {return Search{ .state = gs };
// iterative deepeningvar current_depth: usize = 1;while (current_depth <= depth) : (current_depth += 1) {pv_follow = true;
pub fn bestMove(self: *@This(), depth: usize) !void {self.stop = false;const ALPHA = -25_000;const BETA = 25_000;
// ASPIRATION window: we fell out, so reset values and try againif (scr <= alpha or scr >= beta) {alpha = ALPHA;beta = BETA;current_depth -= 1;continue;
// clear helper data for search{score.killer_moves = [_][MAX_PLY]BitMove{[_]BitMove{@bitCast(BitMove, @as(BitMoveType, 0))} ** MAX_PLY,} ** 2;score.history_moves = [_][64]usize{[_]usize{0} ** 64} ** 12;pv_length = [_]usize{0} ** MAX_PLY;pv_table = [_][MAX_PLY]BitMove{[_]BitMove{@bitCast(BitMove, @as(BitMoveType, 0))} ** MAX_PLY,} ** MAX_PLY;nodes = 0;
if (current_depth == depth) {try print_info(gs, scr, current_depth, true);} else {try print_info(gs, scr, current_depth, false);
const scr = try self.negamax(alpha, beta, current_depth);// ASPIRATION window: we fell out, so reset values and try againif (scr <= alpha or scr >= beta) {alpha = ALPHA;beta = BETA;current_depth -= 1;continue;}alpha = scr - 50;beta = scr + 50;try self.print_info(scr, current_depth);
fn negamax(gs: *GameState, alpha_orig: isize, beta: isize, depth_orig: usize) !isize {pv_length[gs.ply] = gs.ply;
fn negamax(self: *@This(), alpha_orig: isize, beta: isize, depth_orig: usize) !isize {if (self.stopSearch()) return 0;
// increase search depth if king exposed to checkconst in_check = gs.inCheck();if (in_check) depth += 1;
// increase search depth if king exposed to checkconst in_check = self.state.inCheck();if (in_check) depth += 1;
// NULL move pruning{if (depth >= 3 and !in_check and gs.ply != 0) {// backup board statevar bck: GameState = undefined;gs.backup(&bck);
// NULL move pruning{if (depth >= 3 and !in_check and self.ply != 0) {// backup board statevar bck: GameState = undefined;self.state.backup(&bck);
var ml = try MoveList.init(0);try gs.generateMoves(&ml);score.moves(gs, &ml);
var ml = try MoveList.init(0);try self.state.generateMoves(&ml);score.moves(self, &ml);
// Honor following the PV lineif (pv_follow) {pv_follow = false;for (ml.slice()) |*mp| {if (@bitCast(BitMoveType, pv_table[0][gs.ply]) == @bitCast(BitMoveType, mp.move)) {pv_follow = true;mp.score = score.PV_SCORE;break;
// Honor following the PV lineif (pv_follow) {pv_follow = false;for (ml.slice()) |*mp| {if (@bitCast(BitMoveType, pv_table[0][self.ply]) == @bitCast(BitMoveType, mp.move)) {pv_follow = true;mp.score = score.PV_SCORE;break;}
var moves_searched: usize = 0;for (ml.slice()) |mp| {var bck: GameState = undefined;gs.backup(&bck);
var moves_searched: usize = 0;for (ml.slice()) |mp| {var bck: GameState = undefined;self.state.backup(&bck);
gs.ply += 1;if (!gs.makeMove(mp.move, .all)) {gs.ply -= 1;gs.restore(&bck);continue;}
self.ply += 1;if (!self.state.makeMove(mp.move, .all)) {self.ply -= 1;self.state.restore(&bck);continue;}
var scr: isize = undefined;// Normal negamax without PVS or LMRif (moves_searched == 0) {scr = -try negamax(gs, -beta, -alpha, depth - 1);} else {// Conditions to consider LMRif (moves_searched >= FULL_DEPTH_MOVES andgs.ply >= REDUCTION_LIMIT and!mp.move.capture and mp.move.prom == .none and!gs.inCheck()){// reduced-depth, narrow score search for lame movesscr = -try negamax(gs, -alpha - 1, -alpha, depth -| 3);
var scr: isize = undefined;// Normal negamax without PVS or LMRif (moves_searched == 0) {scr = -try self.negamax(-beta, -alpha, depth - 1);
// hack to ensure that we continue with PVS code belowscr = alpha + 1;}
// Conditions to consider LMRif (moves_searched >= FULL_DEPTH_MOVES andself.ply >= REDUCTION_LIMIT and!mp.move.capture and mp.move.prom == .none and!self.state.inCheck()){// reduced-depth, narrow score search for lame movesscr = -try self.negamax(-alpha - 1, -alpha, depth -| 3);} else {// hack to ensure that we continue with PVS code belowscr = alpha + 1;}
// Continue PVS codeif (scr > alpha) {// Once you've found a move with a score that is between alpha and beta,// the rest of the moves are searched with the goal of proving that they are all bad.// It's possible to do this a bit faster than a search that worries that one// of the remaining moves might be good.scr = -try negamax(gs, -alpha - 1, -alpha, depth - 1);
// Continue PVS codeif (scr > alpha) {// Once you've found a move with a score that is between alpha and beta,// the rest of the moves are searched with the goal of proving that they are all bad.// It's possible to do this a bit faster than a search that worries that one// of the remaining moves might be good.scr = -try self.negamax(-alpha - 1, -alpha, depth - 1);
// If the algorithm finds out that it was wrong, and that one of the// subsequent moves was better than the first PV move, it has to search again,// in the normal alpha-beta manner. This happens sometimes, and it's a waste of time,// but generally not often enough to counteract the savings gained from doing the// "bad move proof" search referred to earlier.if (scr > alpha and scr < beta) {scr = -try negamax(gs, -beta, -alpha, depth - 1);
// If the algorithm finds out that it was wrong, and that one of the// subsequent moves was better than the first PV move, it has to search again,// in the normal alpha-beta manner. This happens sometimes, and it's a waste of time,// but generally not often enough to counteract the savings gained from doing the// "bad move proof" search referred to earlier.if (scr > alpha and scr < beta) {scr = -try self.negamax(-beta, -alpha, depth - 1);}
gs.ply -= 1;gs.restore(&bck);moves_searched += 1;
// fail-hard beta cutoffif (scr >= beta) {// store killer movesif (!mp.move.capture) {score.killer_moves[1][self.ply] = score.killer_moves[0][self.ply];score.killer_moves[0][self.ply] = mp.move;}
// fail-hard beta cutoffif (scr >= beta) {// store killer movesif (!mp.move.capture) {score.killer_moves[1][gs.ply] = score.killer_moves[0][gs.ply];score.killer_moves[0][gs.ply] = mp.move;
// node (move) fails highreturn beta;
{// write PV movepv_table[gs.ply][gs.ply] = mp.move;
// copy move from deeper self.ply into the current self.ply's linevar next: usize = self.ply + 1;while (next < pv_length[self.ply + 1]) : (next += 1) {pv_table[self.ply][next] = pv_table[self.ply + 1][next];}
// copy move from deeper ply into the current ply's linevar next: usize = gs.ply + 1;while (next < pv_length[gs.ply + 1]) : (next += 1) {pv_table[gs.ply][next] = pv_table[gs.ply + 1][next];
// adjust pv_lengthpv_length[self.ply] = pv_length[self.ply + 1];
// Detect check- and stalemateif (legal_moves == 0) {if (in_check) {// Adding ply helps the engine finding fastest mates.return eval.CHECKMATE_SCORE + @intCast(isize, gs.ply);} else {return eval.STALEMATE_SCORE;
// Detect check- and stalemateif (legal_moves == 0) {if (in_check) {// Adding self.ply helps the engine finding fastest mates.return eval.CHECKMATE_SCORE + @intCast(isize, self.ply);} else {return eval.STALEMATE_SCORE;}
}// node (move) fails lowreturn alpha;}// quiescenceSearch helps the engine not thow away its pieces because of// a temporary seemingly good move. (Horizon effect)fn quiescenceSearch(gs: *GameState, alpha_orig: isize, beta: isize) !isize {var alpha = alpha_orig;nodes += 1;const evaluation = EVAL_FUNCTION(gs);// fail-hard beta cutoffif (evaluation >= beta) {// node (move) fails highreturn beta;}
var ml = try MoveList.init(0);try gs.generateMoves(&ml);score.moves(gs, &ml);std.sort.sort(MovePrio, ml.slice(), {}, comptime MovePrio.moreThan);
// quiescenceSearch helps the engine not thow away its pieces because of// a temporary seemingly good move. (Horizon effect)fn quiescenceSearch(self: *@This(), alpha_orig: isize, beta: isize) !isize {if (self.stopSearch()) return 0;
gs.ply += 1;if (!gs.makeMove(mp.move, .captures)) {gs.ply -= 1;gs.restore(&bck);continue;}const scr = -try quiescenceSearch(gs, -beta, -alpha);
nodes += 1;
fn print_info(gs: *const GameState, scr: isize, depth: usize, best: bool) !void {var ret = std.ArrayList(u8).init(gs.allocator);defer ret.deinit();
self.ply += 1;if (!self.state.makeMove(mp.move, .captures)) {self.ply -= 1;self.state.restore(&bck);continue;}const scr = -try self.quiescenceSearch(-beta, -alpha);self.ply -= 1;self.state.restore(&bck);// fail-hard beta cutoffif (scr >= beta) {// node (move) fails highreturn beta;}
// loop over the pv_linevar count: usize = 0;while (count < pv_length[0]) : (count += 1) {// print PV moveconst m = pv_table[0][count];const move = try std.fmt.allocPrint(gs.allocator,"{s}{s} ",.{ @tagName(m.source), @tagName(m.target) },);try ret.appendSlice(move);
// node (move) fails lowreturn alpha;
if (best) {const bestmove = try std.fmt.allocPrint(gs.allocator, "bestmove {s}{s}{s}\n", .{@tagName(pv_table[0][0].source),@tagName(pv_table[0][0].target),if (pv_table[0][0].prom != .none) @tagName(pv_table[0][0].prom) else "",});try ret.appendSlice(bestmove);}
fn print_info(self: @This(), scr: isize, depth: usize) !void {var ret = std.ArrayList(u8).init(self.state.allocator);defer ret.deinit();
_ = try std.io.getStdOut().write(ret.items);}
const info = try std.fmt.allocPrint(self.state.allocator,"info score cp {d} depth {d} nodes {d} pv ",.{ scr, depth, nodes },);try ret.appendSlice(info);
test "bestMove" {var gs = try GameState.init(std.testing.allocator, null);gs.show();std.debug.print("{s}", .{bestMove(&gs, 4)});}
// loop over the pv_linevar count: usize = 0;while (count < pv_length[0]) : (count += 1) {// print PV moveconst m = pv_table[0][count];const move = try std.fmt.allocPrint(self.state.allocator,"{s}{s} ",.{ @tagName(m.source), @tagName(m.target) },);try ret.appendSlice(move);}try ret.append('\n');_ = try std.io.getStdOut().write(ret.items);}};