const std = @import("std");
const Str = []const u8;
const SIZE = @import("Board.zig").SIZE;
const BitMove = @import("Board.zig").BitMove;
const GameState = @import("Board.zig").GameState;
const MoveList = @import("Board.zig").MoveList;
const Search = @import("Search.zig").Search;
const SearchOptions = @import("Search.zig").SearchOptions;
var log_level = std.log.default_level;
const MIN_HASH_SIZE = 4;
const MAX_HASH_SIZE = 256;
pub const EngineOptions = struct {
hash_size: usize = 128,
};
// https://backscattering.de/chess/uci/
pub fn loop(allocator: std.mem.Allocator) !void {
var gs = try GameState.init(allocator, null);
defer gs.deinit();
var search: ?Search = null;
defer {
if (search != null) {
search.?.deinit();
}
}
var buf: [4 * 1024]u8 = undefined;
const stdin = std.io.getStdIn().reader();
const stdout = std.io.getStdOut();
std.log.info("UCI engine started...", .{});
var engine_options = EngineOptions{};
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;
} else 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;
} else if (std.mem.eql(u8, input, "stop")) {
std.log.debug("received UCI stop command", .{});
if (search != null) {
@atomicStore(bool, &search.?.stop, true, std.builtin.AtomicOrder.Unordered);
std.log.debug("searching stopped", .{});
}
} else if (std.mem.eql(u8, input, "isready")) {
if (search == null) {
search = Search.init(&gs, engine_options) catch {
std.log.err("Unable to initialize Search interface, quitting...", .{});
std.os.exit(1);
};
}
_ = try stdout.write("readyok\n");
} else {
if (input.len >= 9) {
if (std.mem.eql(u8, input[0..9], "setoption")) {
// setoption name Hash value <val>
if (std.mem.containsAtLeast(u8, input, 1, "Hash")) {
const start = std.mem.lastIndexOfLinear(u8, input, " ") orelse continue;
engine_options.hash_size =
std.fmt.parseUnsigned(usize, input[start + 1 ..], 10) catch {
std.log.debug("unable to parse Hash size: `{s}`", .{input[start..]});
continue;
};
if (engine_options.hash_size < MIN_HASH_SIZE) {
engine_options.hash_size = MIN_HASH_SIZE;
} else if (engine_options.hash_size > MAX_HASH_SIZE) {
engine_options.hash_size = MAX_HASH_SIZE;
}
std.log.debug("Hash size set to {d}", .{engine_options.hash_size});
}
}
}
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")) {
std.log.debug("initializing new uci game", .{});
if (search != null) {
std.log.debug("clearing transposition table", .{});
search.?.tt.clear();
}
std.log.debug("empty moves history", .{});
gs.history.clearRetainingCapacity();
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")) {
if (search == null) {
std.log.err("search is not initialized, call isready first!", .{});
continue;
}
search.?.stop = false;
search.?.timer = try std.time.Timer.start();
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 std.fmt.format(
stdout.writer(),
"option name Hash type spin default 64 min {d} max {d}\n",
.{ MIN_HASH_SIZE, MAX_HASH_SIZE },
);
_ = try stdout.write("uciok\n");
}
pub fn parseGo(search: *Search, in: Str) !void {
var options: SearchOptions = .{};
// INFINITE
if (std.mem.containsAtLeast(u8, in, 1, "infinite")) {
options.depth = std.math.maxInt(usize);
}
inline for ([_]Str{ "depth", "movetime", "nodes", "movestogo", "wtime", "btime", "winc", "binc" }) |keyword| {
const start = std.mem.indexOf(u8, in, keyword);
if (start != null) {
const pos = keyword.len + 1 + start.?;
const end = std.mem.indexOfPos(u8, in, pos, " ") orelse in.len;
const value = try std.fmt.parseUnsigned(usize, in[pos..end], 10);
@field(options, keyword) = value;
}
}
std.log.debug("{any}", .{options});
try search.bestMove(options);
}
pub fn parsePosition(gs: *GameState, in: Str) !void {
var words = std.mem.tokenize(u8, in, " ");
if (words.next()) |position| {
if (position[0] != 'p') {
return error.InvalidPosition;
}
} else return error.InvalidPosition;
if (words.next()) |start| {
switch (start[0]) {
's' => {
// startpos
try gs.parseFEN("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1");
},
'f' => {
// fen
const end = std.mem.indexOf(u8, in, "moves");
if (end) |e| {
try gs.parseFEN(in[13 .. e - 1]);
for ([_]u3{ 0, 1, 2, 3, 4, 5 }) |_| {
_ = words.next();
}
} else {
try gs.parseFEN(in[13..]);
return;
}
},
else => return error.InvalidPosition,
}
} else return error.InvalidPosition;
if (words.next()) |moves| {
if (moves[0] != 'm') return error.InvalidPosition;
while (words.next()) |move| {
const m = try parseMove(gs.*, move);
_ = gs.makeMove(m, .all);
try gs.history.put(gs.hash, {});
}
}
}
fn parseMove(gs: GameState, in: Str) !BitMove {
var move: BitMove = .{ .source = undefined, .target = undefined, .piece = undefined };
move.source = @as(Square, @enumFromInt((in[0] - 'a' + (8 - (in[1] - '0')) * SIZE)));
move.target = @as(Square, @enumFromInt((in[2] - 'a' + (8 - (in[3] - '0')) * SIZE)));
var ml = try MoveList.init(0);
try gs.generateMoves(&ml);
for (ml.slice()) |mp| {
if (move.source == mp.move.source and move.target == mp.move.target) {
move = mp.move;
// if there is a promotion, check if it is the right one
if (move.prom != .none) {
switch (move.prom) {
.N, .n => if (in[4] != 'n') continue,
.B, .b => if (in[4] != 'b') continue,
.R, .r => if (in[4] != 'r') continue,
.Q, .q => if (in[4] != 'q') continue,
else => unreachable,
}
}
return move;
}
}
std.log.debug("Illegal move found: \n\texpected: {any}\n\tgot: {s}\n", .{ move, in });
return error.IllegalMove;
}
const Square = @import("Board.zig").Square;
const Chess = @import("Chess.zig");
test "UCI - parseMove" {
var gs = try GameState.init(
std.testing.allocator,
"rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8",
);
defer gs.deinit();
{
// gs.show();
const got = try parseMove(gs, "d7c8b");
try std.testing.expectEqual(Square.d7, got.source);
try std.testing.expectEqual(Square.c8, got.target);
try std.testing.expectEqual(Chess.PE.B, got.prom);
try std.testing.expectEqual(true, got.capture);
try std.testing.expectEqual(Chess.PE.P, got.piece);
// _ = gs.makeMove(got, .all);
// gs.show();
}
{
// gs.show();
const got = parseMove(gs, "d7d8");
try std.testing.expectError(error.IllegalMove, got);
}
}
test "UCI - parsePosition" {
var gs = try GameState.init(std.testing.allocator, null);
defer gs.deinit();
{
try parsePosition(&gs, "position startpos");
// gs.show();
}
{
try parsePosition(&gs, "position startpos moves e2e4 e7e5");
// gs.show();
}
{
try parsePosition(&gs, "position fen 8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - -");
// gs.show();
}
{
try parsePosition(&gs, "position fen 8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1 moves g2g4 c7c5");
// gs.show();
}
}