lila/ui/analyse/src/evalCache.ts

119 lines
3.5 KiB
TypeScript

import { defined, prop } from 'common';
import throttle from 'common/throttle';
import { CachedEval, EvalGetData, EvalPutData } from './interfaces';
import { AnalyseSocketSend } from './socket';
export interface EvalCacheOpts {
variant: VariantKey;
receive(ev: Tree.ClientEval, path: Tree.Path): void;
send: AnalyseSocketSend;
getNode(): Tree.Node;
canPut(): boolean;
canGet(): boolean;
}
export interface EvalCache {
onCeval(): void;
fetch(path: Tree.Path, multiPv: number): void;
onCloudEval(serverEval: CachedEval): void;
clear(): void;
}
const evalPutMinDepth = 20;
const evalPutMinNodes = 3e6;
const evalPutMaxMoves = 10;
function qualityCheck(ev: Tree.ClientEval): boolean {
// below 500k nodes, the eval might come from an imminent threefold repetition
// and should therefore be ignored
return ev.nodes > 500000 && (ev.depth >= evalPutMinDepth || ev.nodes > evalPutMinNodes);
}
// from client eval to server eval
function toPutData(variant: VariantKey, ev: Tree.ClientEval): EvalPutData {
const data: EvalPutData = {
fen: ev.fen,
knodes: Math.round(ev.nodes / 1000),
depth: ev.depth,
pvs: ev.pvs.map(pv => {
return {
cp: pv.cp,
mate: pv.mate,
moves: pv.moves.slice(0, evalPutMaxMoves).join(' '),
};
}),
};
if (variant !== 'standard') data.variant = variant;
return data;
}
// from server eval to client eval
function toCeval(e: Tree.ServerEval): Tree.ClientEval {
// TODO: this type is not quite right
const res: any = {
fen: e.fen,
nodes: e.knodes * 1000,
depth: e.depth,
pvs: e.pvs.map(from => {
const to: Tree.PvData = {
moves: from.moves.split(' '), // moves come from the server as a single string
};
if (defined(from.cp)) to.cp = from.cp;
else to.mate = from.mate;
return to;
}),
cloud: true,
};
if (defined(res.pvs[0].cp)) res.cp = res.pvs[0].cp;
else res.mate = res.pvs[0].mate;
res.cloud = true;
return res;
}
export function make(opts: EvalCacheOpts): EvalCache {
let fetchedByFen: Dictionary<CachedEval> = {};
const upgradable = prop(false);
lichess.pubsub.on('socket.in.crowd', d => upgradable(d.nb > 2 && d.nb < 99999));
return {
onCeval: throttle(500, () => {
const node = opts.getNode(),
ev = node.ceval;
const fetched = fetchedByFen[node.fen];
if (
ev &&
!ev.cloud &&
node.fen in fetchedByFen &&
(!fetched || fetched.depth < ev.depth) &&
qualityCheck(ev) &&
opts.canPut()
) {
opts.send('evalPut', toPutData(opts.variant, ev));
}
}),
fetch(path: Tree.Path, multiPv: number): void {
const node = opts.getNode();
if ((node.ceval && node.ceval.cloud) || !opts.canGet()) return;
const serverEval = fetchedByFen[node.fen];
if (serverEval) return opts.receive(toCeval(serverEval), path);
else if (node.fen in fetchedByFen) return;
// waiting for response
else fetchedByFen[node.fen] = undefined; // mark as waiting
const obj: EvalGetData = {
fen: node.fen,
path,
};
if (opts.variant !== 'standard') obj.variant = opts.variant;
if (multiPv > 1) obj.mpv = multiPv;
if (upgradable()) obj.up = true;
opts.send('evalGet', obj);
},
onCloudEval(serverEval: CachedEval) {
fetchedByFen[serverEval.fen] = serverEval;
opts.receive(toCeval(serverEval), serverEval.path);
},
clear() {
fetchedByFen = {};
},
};
}