UNPKG

chess-tools

Version:

A collection of tools / libraries for programming chess applications.

479 lines (457 loc) 14.9 kB
"use strict"; const debug = require("debug")("UCI"); const AbstractEngineManager = require("./abstract-engine-manager.js"); //UCI Protocol //http://wbec-ridderkerk.nl/html/UCIProtocol.html const EventEmitter = require('events'); const stream = require("stream"); class UCIEngineManager extends AbstractEngineManager { constructor(engine, options) { super(engine,options); let ponder_timeout = 30000; if (options && options.ponder_timeout) { ponder_timeout = options.ponder_timeout; } this.options = { ponder_timeout : ponder_timeout //default 30 seconds }; if (options.registration) { this.options.registration = { name : options.registration.name, code : options.registration.code, later : false, }; } else { this.options.registration = { later : true }; } this.engine= engine; this.info = []; this.state = { initialized : false, is_calculating : false, is_ready : false, debug_enabled : false, is_evaluating : false, }; this._clear_stats(); this.positions = []; this.current_position = {}; this.engine_options = { }; this.engine_info = { options : {}, registration : null, copyprotection : null, id : { name : '', author : '' } }; this.on("initialized", this.onInitialized.bind(this)); this.on("readyok", this.onReadyOK.bind(this)); this._sendMessage(" "); this._sendMessage("uci"); this._sendMessage("debug on"); } _clear_stats() { this.handling_best_move = false; this.current_stats = { depth : 0, time : 0, node : 0, lines : [], currmove : "", currmovenumber : 0, hashfull : 0, nps : 0, tbhits : 0, cpuload : 0, string : "", refutation : "", currline : "" }; } onInitialized() { this.state.initialized = true; } onReadyOK() { this.state.is_ready = true; this.emit("ready"); } setOption(option, value) { if ( this.engine_info.options[option] ) { this.engine_info.options[option].value = value; this._sendMessage(this.engine_info.options[option].generateMesssage()); } else { throw new Error("invalid option" + option); } } async ponderPosition(fen, options) { return new Promise( (resolve, reject) =>{ let ponder_options = make_ponder_options_string(options, this.options.ponder_timeout); this._clear_stats(); let messages = [ "ucinewgame" ] if (this.engine_info.options["Clear Hash"]) { messages.push(this.engine_info.options["Clear Hash"].generateMesssage()); } if (this.engine_info.options.UCI_ShowCurrLine) { this.engine_info.options.UCI_ShowCurrLine.value = false; messages.push(this.engine_info.options.UCI_ShowCurrLine.generateMesssage()) } if (this.engine_info.options.UCI_AnalyseMode) { this.engine_info.options.UCI_AnalyseMode.value = true; messages.push(this.engine_info.options.UCI_AnalyseMode.generateMesssage()) } if (options.lines && this.engine_info.options.MultiPV) { if (options.lines <= this.engine_info.options.MultiPV.max ) { this.engine_info.options.MultiPV.value = options.lines; messages.push(this.engine_info.options.MultiPV.generateMesssage()) } else { throw new Error("Number of lines requested exceeds engine max", this.engine_info.options.MultiPV.max); } } messages.push("position fen " + fen); messages.push("go " + ponder_options); this.state.is_calculating = true; for (let m of messages) { this._sendMessage(m); } this.current_position.fen = fen; this.current_position.resolve = resolve; this.current_position.reject = reject; this.ponderaction = setTimeout(()=> { this.stop(); }, this.options.ponder_timeout+5000); }); } stop() { if (this.ponderaction) { clearTimeout(this.ponderaction); this.ponderaction = null; } this._sendMessage("stop"); } quit() { this._sendMessage("quit"); this.engine.quit(); } clearInfo() { this.info = []; } handleMessage(message) { debug("RECV-" + this.name + ":" + message); if (typeof message !== 'string') { this.emit("unknown_message", message); return; } if (message.startsWith("info")) { this._handleInfo(message); } else if (message.startsWith("bestmove") && !this.handling_best_move) { this.handling_best_move = true; //this.state.is_calculating = false; this.info.push({ raw: message }); if (this.ponderaction) { clearTimeout(this.ponderaction); } let match = message.match(/bestmove (\w+)/); this.is_calculating = false; this.current_position.best_move = match[1]; let move_lines = this.getLinesForMove(this.current_position.best_move); this.send_best_move(); } else if (message.startsWith("readyok")) { this.emit("isready"); return; } else { this._handleInitMessages(message); } } send_best_move() { return this.current_position.resolve(this.current_position.best_move); } _handleInfo(message) { let info = { raw: message, data : parse_info_message(message) } this.info.push(info); this.is_calculating = true; if (info.data.multipv) { let line = info.data.multipv -1; this.current_stats.lines[line] = { pv: info.data.pv, score: info.data.score }; this.emit("line", (line, this.current_stats.lines[line])) } else if (info.data.pv && this.current_stats.lines.length < 2) { this.current_stats.lines[0] = { pv: info.data.pv, score: info.data.score }; this.emit("line", (0, this.current_stats.lines[0])) } for (let key of Object.keys(info.data)) { if (key === 'multpv' || key === 'pv' || key === 'score') { continue; } this.current_stats[key] = info.data[key]; } //console.log("info-" + this.name, info); this.emit("info", info); } _handleInitMessages(message) { if (message.startsWith("uciok")) { this.state.initialized = true; this.emit('initialized'); } if (message.startsWith("id")) { if (message.startsWith("id author ")) { this.engine_info.author = message.match(/id author (.+)/)[1]; } if (message.startsWith("id name ")) { this.engine_info.author = message.match(/id name (.+)/)[1]; } } if (message.startsWith("option")) { try { let option = UCIEngineOption.fromMessage(message); this.engine_info.options[option.name] = option; } catch (e) { this.emit('error', e); } } if (message.startsWith("copyprotection")) { this.engine_info.copyprotection = message.match(/copyprotection (\w+)/)[1]; } if (message.startsWith("registration")) { this.engine_info.registration = message.match(/registration (\w+)/)[1]; if (this.engine_info.registration === 'error') { this._sendMessage() } } } getLinesForMove(move) { let l = []; for (let line of this.current_stats.lines) { if (line && line.pv) { //console.log("line.pv", line.pv); let match = line.pv.startsWith(move); if (match) { l.push(line); } } } return l; } } module.exports = UCIEngineManager; function extract_option(message, param, options) { if (param === 'name') { let regex = new RegExp(param + '\\s' + '(.+)\\stype'); let match = message.match(regex); if (match) { options[param] = match[1]; } } if (param === 'var') { let regex = new RegExp(param + '\\s' + '(\\w+(\\sHash)?)', 'g'); let match = message.match(regex); if (match) { match.shift(); let results = new Set(); for (let m of match) { regex = new RegExp(param + '\\s' + '(\\w+(\\sHash)?)'); let submatch = m.match(regex); if (submatch) { results.add(submatch[1]); } } options[param] = results; } } else { let regex = new RegExp(param + '\\s' + '(\\w+(\\sHash)?)'); let match = message.match(regex); if (match) { options[param] = match[1]; } } } const UCI_TYPES = new Set(); UCI_TYPES.add("check"); UCI_TYPES.add("spin"); UCI_TYPES.add("combo"); UCI_TYPES.add("button"); UCI_TYPES.add("string"); let UCI_INFO = new Set(); UCI_INFO.add("depth"); UCI_INFO.add("seldepth"); UCI_INFO.add("time"); UCI_INFO.add("nodes"); UCI_INFO.add("pv"); UCI_INFO.add("multipv"); UCI_INFO.add("score"); UCI_INFO.add("currmove"); UCI_INFO.add("currmovenumber"); UCI_INFO.add("hashfull"); UCI_INFO.add("nps"); UCI_INFO.add("tbhits"); UCI_INFO.add("cpuload"); UCI_INFO.add("string"); UCI_INFO.add("refutation"); UCI_INFO.add("currline"); function make_ponder_options_string(options, max_movetime) { let options_options = []; if (options.searchmoves) { if (typeof options.searchmoves === 'object' && Array.isArray(options.searchmoves) ) { options_options.push("searchmoves " + options.searchmoves.join(" ")); } } if (options.wtime) { options_options.push("wtime " + options.wtime) } if (options.btime) { options_options.push("btime " + options.btime); } if (options.winc) { options_options.push("winc " + options.winc); } if (options.binc) { options_options.push("binc " + options.binc); } if (options.movestogo) { options_options.push("movestogo " + options.movestogo); } if (options.depth) { options_options.push("depth " + options.depth); } if (options.nodes) { options_options.push("nodes " + options.nodes); } if (options.mate) { options_options.push("mate " + options.mate); } if (options.movetime < max_movetime ) { options_options.push("movetime " + options.movetime); } else { options_options.push("movetime " + max_movetime); } return options_options.join(" "); } class UCIEngineOption { static fromMessage(message) { let option = new UCIEngineOption(); extract_option(message, 'name', option); extract_option(message, 'type', option); extract_option(message, 'var', option); extract_option(message, 'min', option); extract_option(message, 'max', option); extract_option(message, 'default', option); return option; } constructor() { this._data = {}; } set name(name) { this._data.name = name; } get name() { return this._data.name; } set type(type) { if (UCI_TYPES.has(type)) { this._data.type = type; } else { throw new Error("Invalid type", type); } } get type() { return this._data.type; } set default(value) { this._data.default = value; } get default() { return this._data_default; } set var(value) { this._data.var = value; } get var() { return this._data.var; } set min(value) { this._data.min = parseInt(value); } get min() { return this._data.min; } set max(value) { this._data.max = parseInt(value); } get max() { return this._data.max; } set value(value) { if (this.type === 'check') { if (value) { this._value = true; } else { this._value = false; } } else if (this.type === 'spin') { if (this._data.min < value && this._data.max >= value) { this._value = value; } else { throw new Error("Invalid Spin value " + value + " must be > " + this._data.min + " " + this._data.max ); } } else if (this.type === 'combo') { if (this.var && this.var.has(value)) { this._value = value; } else { throw new Error("Invalid combo value " + value + " allowed only " + this.var + "]") } } else if (this.type === 'button') { } else if (this.type === 'string') { this._value = value; } } get value() { return this._value; } generateMesssage() { let message = "setoption name " + this.name; if (this.type !== 'button') { message += " value " + this.value; } return message; } } function parse_info_message(message) { let words = message.split(" "); let current_type; let info = {}; let values = []; for (let word of words) { if (word === 'info') { continue; } if (UCI_INFO.has(word)) { if (current_type) { info[current_type] = values.join(" "); } values= []; current_type = word; } else { values.push(word) } } if (current_type) { if (current_type) { info[current_type] = values.join(" "); } } return info; }