stockfish
Version:
The Stockfish chess engine in Web Assembly (WASM)
292 lines (255 loc) • 13.9 kB
HTML
<head>
<meta charset="utf-8" />
<title>Stockfish NNUE JS</title>
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1, maximum-scale=1, user-scalable=no" />
</head>
<!-- CSS -->
<style>
*, *::before, *::after {
box-sizing: border-box;
margin: 0; padding: 0; border: 0;
font: inherit;
}
html, body, #root {
height: 100%;
}
#root {
font-family: 'mono';
font-size: 13px;
}
main {
height: 100%;
display: flex;
flex-direction: column;
padding: 10px;
}
#input {
display: flex;
margin-bottom: 10px;
}
#input #command {
width: 80%;
padding: 2px 0 2px 6px;
border: 1px solid #ddd;
border-right: 0;
}
#input #send {
display: inline-block;
padding: 3px 6px 3px 6px;
background: #888;
color: #fff;
font-weight: bold;
margin-right: 6px;
cursor: pointer;
}
#input #examples {
width: 120px;
background: #eee;
}
#misc {
margin-bottom: 10px;
}
#output {
flex: 1 1 auto;
width: 100%;
border: 1px solid #ddd;
padding: 10px;
overflow: scroll;
box-shadow: inset 1px 1px 2px #eee;
font-size: 11px;
}
</style>
<body>
<div id="root"></div>
<!-- Javascript -->
<!-- <script src="./stockfish.js"></script> -->
<script src="./mithril.2.0.4.min.js"></script>
<script>
const $ = (...args) => document.querySelector(...args);
const formatMB = (n) => {
return (n ? (n / 1e6).toPrecision(3) : '?') + 'MB';
};
const isSupported = () => {
if (typeof WebAssembly !== 'object') return false;
//const source = Uint8Array.from([0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 123, 3, 2, 1, 0, 7, 8, 1, 4, 116, 101, 115, 116, 0, 0, 10, 15, 1, 13, 0, 65, 0, 253, 17, 65, 0, 253, 17, 253, 186, 1, 11]);
//if (typeof WebAssembly.validate !== 'function' || !WebAssembly.validate(source)) return false;
if (typeof Atomics !== 'object') return false;
if (typeof SharedArrayBuffer !== 'function') return false;
return true;
};
const RequestProgress = ({ attrs: { url, onFinishDownload } }) => {
let state = 'INIT'; // 'LOADING', 'DONE', 'FAILED'
let loaded = 0;
let total = 0;
const oninit = () => {
state = 'LOADING';
m.request({
url: url,
method: 'GET',
responseType: 'arraybuffer',
headers: { Accept: '*/*' },
config: (xhr) => {
xhr.onprogress = (e) => {
// TODO:
// When gzip compressed, the value of "loaded/total" gets messed up.
// On Chrome, "loaded" is the value after decompression, but on the other hand,
// On Firefox, "loaded" is the value before decompression.
loaded = e.loaded;
total = e.total || Number(e.target.getResponseHeader("x-decompressed-content-length"));
m.redraw();
};
}
}).then(
(response) => { state = 'DONE'; onFinishDownload(response); },
(e) => { console.error(e); state = 'FAILED'; onFinishDownload(null); }
)
};
const view = () => {
const fraction = (total == -1) ? `?MB/?MB` : `${formatMB(loaded)}/${formatMB(total)}`;
return m("span", [
`${fraction} [${state}] `,
m("span", {
style: 'cursor: pointer;',
onclick: () => window.alert('On some browsers, download size might look contradicted due to file compression.'),
}, '[?]')
]);
};
return { oninit, view };
};
const App = () => {
let stockfish = null;
let stockfish_state = 'INIT'; // 'READY', 'FAILED'
let output = '';
let tail_mode = true;
const examples = [
'uci',
'go',
'go depth 20',
'go depth 25',
'stop',
'd',
'eval',
'position startpos',
'position fen r1k4r/ppp1bq1p/2n1N3/6B1/3p2Q1/8/PPP2PPP/R5K1 w - - 0 1',
'setoption name Use NNUE value true',
'setoption name Use NNUE value false',
'setoption name Threads value 1',
'setoption name Threads value 4',
'bench 16 1 20 current depth NNUE',
'bench 16 1 20 current depth classical',
'bench_eval',
'setoption name EvalFile value old-nn/nn-76a8a7ffb820.nnue',
'setoption name EvalFile value http://127.0.0.1:8080/old-nn/nn-76a8a7ffb820.nnue',
'position fen 8/7p/6p1/5p2/Q4P2/2p3P1/3r3P/2K1k3 w - - 2 44 moves a4a7',
];
const sendCommand = () => {
const command = $('#command').value;
if (command.length > 0) { stockfish.postMessage(command); }
};
const scrollOutput = () => {
if (!tail_mode) { return; }
$('#output').scrollTo({ top: $('#output').scrollHeight, behavior: 'smooth' });
};
const loadStockfish = function ()
{
var mod = {
locateFile: function (path)
{
if (path.indexOf(".wasm") > -1) {
return "stockfish.wasm";
} else {
return "stockfish.js#stockfish.wasm,worker";
//console.error("TODO: CONCAT worker");
//return "stockfish.worker.js";
//const code = "(" + '"use strict";var Module={};if(typeof process==="object"&&typeof process.versions==="object"&&typeof process.versions.node==="string"){var nodeWorkerThreads=require("worker_threads");var parentPort=nodeWorkerThreads.parentPort;parentPort.on("message",function(data){onmessage({data:data})});var nodeFS=require("fs");Object.assign(global,{self:global,require:require,Module:Module,location:{href:__filename},Worker:nodeWorkerThreads.Worker,importScripts:function(f){(0,eval)(nodeFS.readFileSync(f,"utf8"))},postMessage:function(msg){parentPort.postMessage(msg)},performance:global.performance||{now:function(){return Date.now()}}})}function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=function(info,receiveInstance){var instance=new WebAssembly.Instance(Module["wasmModule"],info);receiveInstance(instance);Module["wasmModule"]=null;return instance.exports};function moduleLoaded(){}self.onmessage=function(e){try{if(e.data.cmd==="load"){Module["wasmModule"]=e.data.wasmModule;Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;if(typeof e.data.urlOrBlob==="string"){importScripts(e.data.urlOrBlob)}else{var objectUrl=URL.createObjectURL(e.data.urlOrBlob);importScripts(objectUrl);URL.revokeObjectURL(objectUrl)}Stockfish(Module).then(function(instance){Module=instance;moduleLoaded()})}else if(e.data.cmd==="objectTransfer"){Module["PThread"].receiveObjectTransfer(e.data)}else if(e.data.cmd==="run"){Module["__performance_now_clock_drift"]=performance.now()-e.data.time;Module["__emscripten_thread_init"](e.data.threadInfoStruct,/*isMainBrowserThread=*/0,/*isMainRuntimeThread=*/0);var max=e.data.stackBase;var top=e.data.stackBase+e.data.stackSize;Module["establishStackSpace"](top,max);Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInit();try{var result=Module["invokeEntryPoint"](e.data.start_routine,e.data.arg);if(Module["keepRuntimeAlive"]()){Module["PThread"].setExitStatus(result)}else{Module["PThread"].threadExit(result)}}catch(ex){if(ex==="Canceled!"){Module["PThread"].threadCancel()}else if(ex!="unwind"){if(ex instanceof Module["ExitStatus"]){if(Module["keepRuntimeAlive"]()){}else{Module["PThread"].threadExit(ex.status)}}else{Module["PThread"].threadExit(-2);throw ex}}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["PThread"].threadCancel()}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="processThreadQueue"){if(Module["_pthread_self"]()){Module["_emscripten_current_thread_process_queued_calls"]()}}else{err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){err("worker.js onmessage() captured an uncaught exception: "+ex);if(ex&&ex.stack)err(ex.stack);throw ex}};' + ")();";
//const blob = new Blob([code], {type: 'application/javascript'})
//const code = '"use strict";var Module={};if(typeof process==="object"&&typeof process.versions==="object"&&typeof process.versions.node==="string"){var nodeWorkerThreads=require("worker_threads");var parentPort=nodeWorkerThreads.parentPort;parentPort.on("message",function(data){onmessage({data:data})});var nodeFS=require("fs");Object.assign(global,{self:global,require:require,Module:Module,location:{href:__filename},Worker:nodeWorkerThreads.Worker,importScripts:function(f){(0,eval)(nodeFS.readFileSync(f,"utf8"))},postMessage:function(msg){parentPort.postMessage(msg)},performance:global.performance||{now:function(){return Date.now()}}})}function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=function(info,receiveInstance){var instance=new WebAssembly.Instance(Module["wasmModule"],info);receiveInstance(instance);Module["wasmModule"]=null;return instance.exports};function moduleLoaded(){}self.onmessage=function(e){try{if(e.data.cmd==="load"){Module["wasmModule"]=e.data.wasmModule;Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;if(typeof e.data.urlOrBlob==="string"){importScripts(e.data.urlOrBlob)}else{var objectUrl=URL.createObjectURL(e.data.urlOrBlob);importScripts(objectUrl);URL.revokeObjectURL(objectUrl)}Stockfish(Module).then(function(instance){Module=instance;moduleLoaded()})}else if(e.data.cmd==="objectTransfer"){Module["PThread"].receiveObjectTransfer(e.data)}else if(e.data.cmd==="run"){Module["__performance_now_clock_drift"]=performance.now()-e.data.time;Module["__emscripten_thread_init"](e.data.threadInfoStruct,/*isMainBrowserThread=*/0,/*isMainRuntimeThread=*/0);var max=e.data.stackBase;var top=e.data.stackBase+e.data.stackSize;Module["establishStackSpace"](top,max);Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInit();try{var result=Module["invokeEntryPoint"](e.data.start_routine,e.data.arg);if(Module["keepRuntimeAlive"]()){Module["PThread"].setExitStatus(result)}else{Module["PThread"].threadExit(result)}}catch(ex){if(ex==="Canceled!"){Module["PThread"].threadCancel()}else if(ex!="unwind"){if(ex instanceof Module["ExitStatus"]){if(Module["keepRuntimeAlive"]()){}else{Module["PThread"].threadExit(ex.status)}}else{Module["PThread"].threadExit(-2);throw ex}}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["PThread"].threadCancel()}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="processThreadQueue"){if(Module["_pthread_self"]()){Module["_emscripten_current_thread_process_queued_calls"]()}}else{err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){err("worker.js onmessage() captured an uncaught exception: "+ex);if(ex&&ex.stack)err(ex.stack);throw ex}};';
//const worker = new Worker(URL.createObjectURL(blob))
//return 'data:application/javascript,' + encodeURIComponent(code);
}
}
};
Stockfish(mod).then(_stockfish => {
stockfish = _stockfish;
stockfish_state = 'READY';
stockfish.addMessageListener((line) => {
output += line + '\n';
m.redraw();
});
})
};
function loadStockfishWorker()
{
stockfish = new Worker("stockfish.js#stockfish.wasm");
stockfish.onmessage = function (line)
{
output += line.data + '\n';
m.redraw();
};
}
/*
const onFinishDownload = (data) => {
if (!data) { stockfish_state = 'FAILED'; m.redraw(); return; }
loadStockfish({ wasmBinary: data })
.then(_stockfish => {
stockfish = _stockfish;
stockfish_state = 'READY';
stockfish.addMessageListener((line) => {
output += line + '\n';
m.redraw();
});
})
.catch(e => { stockfish_state = 'FAILED'; throw e; })
.finally(() => m.redraw());
};
*/
const oninit = () => {
stockfish_state = 'READY';
};
const view = () => {
const is_ready = stockfish_state == 'READY';
//
return m("main", [
m("div#input", [
m("input#command", {
placeholder: 'Input UCI Command Here',
disabled: !is_ready,
onkeyup: (e) => {
if (e.keyCode != 13) { e.redraw = false; return; }
sendCommand();
}
}),
m("span#send", { disabled: !is_ready, onclick: sendCommand }, 'SEND'),
m("select#examples", {
disabled: !is_ready,
onchange: (e) => { $("#command").value = e.target.value; window.setTimeout(sendCommand, 10); }
}, [
m("option", { value: '' }, "-- EXAMPLE --"),
...examples.map((ex, index) => m("option", { value: ex }, ex)),
])
]),
m("div#misc", [
//m("div", ['- download: ', m(RequestProgress, { url: './stockfish.wasm', onFinishDownload })]),
m("div", `- stockfish_state: ${stockfish_state}`),
m("div", [
m("span", { style: 'margin-right: 5px;' }, '- tail_mode:'),
m("span", { style: 'cursor: pointer;', onclick: () => { tail_mode = !tail_mode; scrollOutput(); } }, [
tail_mode ? '[x]' : '[ ]'
]),
]),
]),
m("div#output", { onupdate: scrollOutput }, m("pre", output))
]);
};
//loadStockfish();
loadStockfishWorker();
return { oninit, view };
};
if (!isSupported()) {
window.alert("Your browser is not supported. For more information, please take a look at https://github.com/hi-ogawa/Stockfish/wiki.");
} else {
m.mount($("#root"), App);
}
</script>
</body>