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.html
pub 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 reduction
const 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 pruning
const 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 deepening
var 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 again
if (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 again
if (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 check
const in_check = gs.inCheck();
if (in_check) depth += 1;
// increase search depth if king exposed to check
const 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 state
var bck: GameState = undefined;
gs.backup(&bck);
// NULL move pruning
{
if (depth >= 3 and !in_check and self.ply != 0) {
// backup board state
var 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 line
if (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 line
if (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 LMR
if (moves_searched == 0) {
scr = -try negamax(gs, -beta, -alpha, depth - 1);
} else {
// Conditions to consider LMR
if (moves_searched >= FULL_DEPTH_MOVES and
gs.ply >= REDUCTION_LIMIT and
!mp.move.capture and mp.move.prom == .none and
!gs.inCheck())
{
// reduced-depth, narrow score search for lame moves
scr = -try negamax(gs, -alpha - 1, -alpha, depth -| 3);
var scr: isize = undefined;
// Normal negamax without PVS or LMR
if (moves_searched == 0) {
scr = -try self.negamax(-beta, -alpha, depth - 1);
// hack to ensure that we continue with PVS code below
scr = alpha + 1;
}
// Conditions to consider LMR
if (moves_searched >= FULL_DEPTH_MOVES and
self.ply >= REDUCTION_LIMIT and
!mp.move.capture and mp.move.prom == .none and
!self.state.inCheck())
{
// reduced-depth, narrow score search for lame moves
scr = -try self.negamax(-alpha - 1, -alpha, depth -| 3);
} else {
// hack to ensure that we continue with PVS code below
scr = alpha + 1;
}
// Continue PVS code
if (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 code
if (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 cutoff
if (scr >= beta) {
// store killer moves
if (!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 cutoff
if (scr >= beta) {
// store killer moves
if (!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 high
return beta;
{
// write PV move
pv_table[gs.ply][gs.ply] = mp.move;
// copy move from deeper self.ply into the current self.ply's line
var 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 line
var 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_length
pv_length[self.ply] = pv_length[self.ply + 1];
// Detect check- and stalemate
if (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 stalemate
if (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 low
return 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 cutoff
if (evaluation >= beta) {
// node (move) fails high
return 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 cutoff
if (scr >= beta) {
// node (move) fails high
return beta;
}
// loop over the pv_line
var count: usize = 0;
while (count < pv_length[0]) : (count += 1) {
// print PV move
const 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 low
return 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_line
var count: usize = 0;
while (count < pv_length[0]) : (count += 1) {
// print PV move
const 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);
}
};