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(); } }