diff --git a/.gitmodules b/.gitmodules index efca0b4ec52e..427e2f0709b3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -44,3 +44,7 @@ path = public/vendor/Sunsetter8 url = https://github.com/niklasf/Sunsetter8 branch = js +[submodule "public/vendor/stockfish.pexe"] + path = public/vendor/stockfish.pexe + url = https://github.com/niklasf/stockfish.pexe + branch = ddugovic diff --git a/public/vendor/stockfish.pexe b/public/vendor/stockfish.pexe new file mode 160000 index 000000000000..6e07387f0ade --- /dev/null +++ b/public/vendor/stockfish.pexe @@ -0,0 +1 @@ +Subproject commit 6e07387f0adef9d521dd2eb4b1400031d4591e14 diff --git a/ui/analyse/src/ceval/cevalCtrl.js b/ui/analyse/src/ceval/cevalCtrl.js index 7cf2b224eaae..5b14ada9f91b 100644 --- a/ui/analyse/src/ceval/cevalCtrl.js +++ b/ui/analyse/src/ceval/cevalCtrl.js @@ -2,8 +2,8 @@ var m = require('mithril'); var makePool = require('./cevalPool'); var dict = require('./cevalDict'); var util = require('../util'); -var stockfishWorker = require('./stockfishWorker'); -var sunsetterWorker = require('./sunsetterWorker'); +var stockfishProtocol = require('./stockfishProtocol'); +var sunsetterProtocol = require('./sunsetterProtocol'); module.exports = function(possible, variant, emit) { @@ -16,13 +16,22 @@ module.exports = function(possible, variant, emit) { var allowed = m.prop(true); var enabled = m.prop(possible() && allowed() && lichess.storage.get(storageKey) === '1'); var started = false; - var engine = variant.key !== 'crazyhouse' ? stockfishWorker : sunsetterWorker; - var pool = makePool({ - minDepth: minDepth, - maxDepth: maxDepth, - variant: variant - }, engine, nbWorkers); + var pool; + if (variant.key !== 'crazyhouse') { + pool = makePool(stockfishProtocol, { + asmjs: '/assets/vendor/stockfish.js/stockfish.js', + pnacl: '/assets/vendor/stockfish.pexe/nacl/stockfish.nmf' + }, { + minDepth: minDepth, + maxDepth: maxDepth, + variant: variant, + }); + } else { + pool = makePool(sunsetterProtocol, { + asmjs: '/assets/vendor/Sunsetter8/sunsetter.js' + }); + } // adjusts maxDepth based on nodes per second var npsRecorder = (function() { diff --git a/ui/analyse/src/ceval/cevalPool.js b/ui/analyse/src/ceval/cevalPool.js index fbaf486b1878..eaa8bc677d2c 100644 --- a/ui/analyse/src/ceval/cevalPool.js +++ b/ui/analyse/src/ceval/cevalPool.js @@ -1,7 +1,65 @@ var m = require('mithril'); -module.exports = function(opts, makeWorker, nb) { +function makeHelper(makeWorker, terminateWorker, poolOpts, makeProtocol, protocolOpts) { + var worker, protocol, api; + var boot = function () { + worker = makeWorker(poolOpts); + protocol = makeProtocol(api, protocolOpts); + worker.addEventListener('message', function(e) { + protocol.received(e.data); + }, true); + }; + + var stop = function() { + var stopped = m.deferred(false); + setTimeout(function () { + stopped.reject(); + }, 1000); + return protocol.stop(stopped); + }; + + api = { + send: function(text) { + worker.postMessage(text); + }, + start: function (work) { + stop().then(function () { + protocol.start(work); + }, function () { + terminateWorker(worker); + boot(); + }); + }, + stop: stop + }; + + boot(); + + return api; +} + +function makeWebWorker(makeProtocol, poolOpts, protocolOpts) { + return makeHelper(function () { + return new Worker(poolOpts.asmjs); + }, function (worker) { + worker.terminate(); + }, poolOpts, makeProtocol, protocolOpts); +} + +function makePNaClModule(makeProtocol, poolOpts, protocolOpts) { + return makeHelper(function () { + var module = document.createElement('embed'); + module.setAttribute('src', poolOpts.pnacl); + module.setAttribute('type', 'application/x-pnacl'); + module.setAttribute('width', '0'); + module.setAttribute('height', '0'); + document.body.appendChild(module); + return module; + }, function () {}, poolOpts, makeProtocol, protocolOpts); +} + +module.exports = function(makeProtocol, poolOpts, protocolOpts) { var workers = []; var token = -1; @@ -12,9 +70,13 @@ module.exports = function(opts, makeWorker, nb) { }; var initWorkers = function() { - if (!workers.length) - for (var i = 1; i <= nb; i++) - workers.push(makeWorker(opts, 'W' + i)); + if (workers.length) return; + + if (poolOpts.pnacl && navigator.mimeTypes['application/x-pnacl']) + workers.push(makePNaClModule(makeProtocol, poolOpts, protocolOpts)); + else + for (var i = 1; i <= 4; i++) + workers.push(makeWebWorker(makeProtocol, poolOpts, protocolOpts)); } var stopAll = function() { diff --git a/ui/analyse/src/ceval/stockfishProtocol.js b/ui/analyse/src/ceval/stockfishProtocol.js new file mode 100644 index 000000000000..4888fc8b97bf --- /dev/null +++ b/ui/analyse/src/ceval/stockfishProtocol.js @@ -0,0 +1,75 @@ +var m = require('mithril'); + +var legacyVariantMap = { + fromPosition: 'Chess960', + chess960: 'Chess960', + atomic: 'Atomic', + horde: 'Horde', + crazyhouse: 'House', + kingOfTheHill: 'KingOfTheHill', + racingKings: 'Race', + threeCheck: '3Check', + antichess: 'Anti' +}; + +module.exports = function(worker, opts) { + + var work = null; + var stopped = m.deferred(); + + var legacyVariant = legacyVariantMap[opts.variant.key]; + if (legacyVariant) worker.send('setoption name UCI_' + legacyVariant + ' value true'); + else worker.send('uci'); + + // TODO: Modern variant selector + + var processOutput = function(text) { + if (text.indexOf('bestmove ') === 0) { + stopped.resolve(true); + return; + } + if (!work) return; + if (/currmovenumber|lowerbound|upperbound/.test(text)) return; + var matches = text.match(/depth (\d+) .*score (cp|mate) ([-\d]+) .*nps (\d+) .*pv (.+)/); + if (!matches) return; + var depth = parseInt(matches[1]); + if (depth < opts.minDepth) return; + var cp, mate; + if (matches[2] === 'cp') cp = parseFloat(matches[3]); + else mate = parseFloat(matches[3]); + if (work.ply % 2 === 1) { + if (matches[2] === 'cp') cp = -cp; + else mate = -mate; + } + var best = matches[5].split(' ')[0]; + work.emit({ + work: work, + eval: { + depth: depth, + cp: cp, + mate: mate, + best: best, + nps: parseInt(matches[4]) + }, + name: name + }); + }; + + return { + start: function(w) { + work = w; + worker.send(['position', 'fen', work.initialFen, 'moves'].concat(work.moves).join(' ')); + worker.send('go depth ' + work.maxDepth); + }, + stop: function(s) { + if (!work) s.resolve(true); + else { + work = null; + stopped = s; + worker.send('stop'); + } + return s.promise; + }, + received: processOutput + }; +}; diff --git a/ui/analyse/src/ceval/stockfishWorker.js b/ui/analyse/src/ceval/stockfishWorker.js deleted file mode 100644 index 988e95c894f9..000000000000 --- a/ui/analyse/src/ceval/stockfishWorker.js +++ /dev/null @@ -1,86 +0,0 @@ -var m = require('mithril'); - -var variantMap = { - fromPosition: 'Chess960', - chess960: 'Chess960', - atomic: 'Atomic', - horde: 'Horde', - crazyhouse: 'House', - kingOfTheHill: 'KingOfTheHill', - racingKings: 'Race', - threeCheck: '3Check', - antichess: 'Anti' -}; - -module.exports = function(opts, name) { - - var instance = null; - var busy = false; - var stopping = false; - - var send = function(text) { - instance.postMessage(text); - }; - - var processOutput = function(text, work) { - if (text.indexOf('bestmove ') === 0) { - busy = false; - stopping = false; - return; - } - if (stopping) return; - if (/currmovenumber|lowerbound|upperbound/.test(text)) return; - var matches = text.match(/depth (\d+) .*score (cp|mate) ([-\d]+) .*nps (\d+) .*pv (.+)/); - if (!matches) return; - var depth = parseInt(matches[1]); - if (depth < opts.minDepth) return; - var cp, mate; - if (matches[2] === 'cp') cp = parseFloat(matches[3]); - else mate = parseFloat(matches[3]); - if (work.ply % 2 === 1) { - if (matches[2] === 'cp') cp = -cp; - else mate = -mate; - } - var best = matches[5].split(' ')[0]; - work.emit({ - work: work, - eval: { - depth: depth, - cp: cp, - mate: mate, - best: best, - nps: parseInt(matches[4]) - }, - name: name - }); - }; - - var reboot = function() { - if (instance) instance.terminate(); - instance = new Worker('/assets/vendor/stockfish.js/stockfish.js'); - busy = false; - stopping = false; - var uciVariant = variantMap[opts.variant.key]; - if (uciVariant) send('setoption name UCI_' + uciVariant + ' value true'); - else send('uci'); // send something to warm up - }; - - reboot(); - - return { - start: function(work) { - if (busy) reboot(); - busy = true; - send(['position', 'fen', work.initialFen, 'moves'].concat(work.moves).join(' ')); - send('go depth ' + work.maxDepth); - instance.onmessage = function(msg) { - processOutput(msg.data, work); - }; - }, - stop: function() { - if (!busy) return; - stopping = true; - send('stop'); - } - }; -}; diff --git a/ui/analyse/src/ceval/sunsetterWorker.js b/ui/analyse/src/ceval/sunsetterProtocol.js similarity index 60% rename from ui/analyse/src/ceval/sunsetterWorker.js rename to ui/analyse/src/ceval/sunsetterProtocol.js index a92bae0b52cc..2eeb0ad38749 100644 --- a/ui/analyse/src/ceval/sunsetterWorker.js +++ b/ui/analyse/src/ceval/sunsetterProtocol.js @@ -1,10 +1,11 @@ var m = require('mithril'); -module.exports = function(opts, name) { +module.exports = function(worker, opts) { - var instance = null; - var busy = false; - var stopping = false; + var work = null; + var stopped = m.deferred(); + + worker.send('xboard'); // Sunsetter always plays only moves right away. Count the number of played // moves to show the correct mate in #n. @@ -13,24 +14,19 @@ module.exports = function(opts, name) { var aiMoves = 0; var best; - var send = function(text) { - instance.postMessage(text); - }; - - var processOutput = function(text, work) { + var processOutput = function(text) { if (text === 'tellics stopped') { - busy = false; - stopping = false; aiMoves = 0; best = undefined; + stopped.resolve(true); return; } - if (stopping) return; + if (!work) return; if (text.indexOf('move ') == 0) { aiMoves++; best = text.split(' ')[1]; - if (!stopping) send('analyze'); + if (work) worker.send('analyze'); return; } @@ -42,14 +38,13 @@ module.exports = function(opts, name) { cp = parseInt(matches[2], 10); if (!aiMoves) { best = matches[5]; - if (depth < opts.minDepth) return; } } else { matches = text.match(/Found move:\s+([a-h1-8=@PNBRQK]+)\s+([-+]?\d+)\s.*/); if (matches) { cp = parseInt(matches[2], 10); if (!aiMoves) best = matches[1]; - if (!stopping) send('analyze'); + if (work) worker.send('analyze'); } else { return; } @@ -81,40 +76,30 @@ module.exports = function(opts, name) { }); }; - var reboot = function() { - if (instance) instance.terminate(); - instance = new Worker('/assets/vendor/Sunsetter8/sunsetter.js'); - busy = false; - stopping = false; - aiMoves = 0; - send('xboard'); - }; - - reboot(); - return { - start: function(work) { - if (busy) reboot(); - busy = true; - send('reset ' + opts.variant.key); - send('setboard ' + work.initialFen); - send('easy'); - send('force'); + start: function(w) { + work = w; + worker.send('reset crazyhouse'); + worker.send('setboard ' + work.initialFen); + worker.send('easy'); + worker.send('force'); for (var i = 0; i < work.moves.length; i++) { - send(work.moves[i]); + worker.send(work.moves[i]); } - send('go'); - instance.onmessage = function(msg) { - processOutput(msg.data, work); - }; + worker.send('go'); }, - stop: function() { - if (!busy) return; - stopping = true; - aiMoves = 0; - best = undefined; - send('exit'); - send('tellics stopped'); - } + stop: function(s) { + if (!work) s.resolve(true); + else { + stopped = s; + work = null; + aiMoves = 0; + best = undefined; + worker.send('exit'); + worker.send('tellics stopped'); + } + return s.promise; + }, + received: processOutput }; };