406 lines
12 KiB
TypeScript
406 lines
12 KiB
TypeScript
import * as winningChances from './winningChances';
|
|
import { defined } from 'common';
|
|
import { Eval, CevalCtrl, ParentCtrl, NodeEvals } from './types';
|
|
import { h } from 'snabbdom';
|
|
import { Position } from 'chessops/chess';
|
|
import { lichessVariantRules } from 'chessops/compat';
|
|
import { makeSanAndPlay } from 'chessops/san';
|
|
import { opposite, parseUci } from 'chessops/util';
|
|
import { parseFen, makeBoardFen } from 'chessops/fen';
|
|
import { renderEval } from './util';
|
|
import { setupPosition } from 'chessops/variant';
|
|
import { VNode } from 'snabbdom/vnode';
|
|
|
|
let gaugeLast = 0;
|
|
const gaugeTicks: VNode[] = [...Array(8).keys()].map(i =>
|
|
h(i === 3 ? 'tick.zero' : 'tick', { attrs: { style: `height: ${(i + 1) * 12.5}%` } })
|
|
);
|
|
|
|
function localEvalInfo(ctrl: ParentCtrl, evs: NodeEvals): Array<VNode | string> {
|
|
const ceval = ctrl.getCeval(),
|
|
trans = ctrl.trans;
|
|
if (!evs.client) {
|
|
const mb = ceval.downloadProgress() / 1024 / 1024;
|
|
return [
|
|
evs.server && ctrl.nextNodeBest()
|
|
? trans.noarg('usingServerAnalysis')
|
|
: trans.noarg('loadingEngine') + (mb >= 1 ? ` (${mb.toFixed(1)} MiB)` : ''),
|
|
];
|
|
}
|
|
|
|
const t: Array<VNode | string> = evs.client.cloud
|
|
? [
|
|
trans('depthX', evs.client.depth || 0),
|
|
h('span.cloud', { attrs: { title: trans.noarg('cloudAnalysis') } }, 'Cloud'),
|
|
]
|
|
: [trans('depthX', (evs.client.depth || 0) + '/' + evs.client.maxDepth)];
|
|
if (ceval.canGoDeeper())
|
|
t.push(
|
|
h('a.deeper', {
|
|
attrs: {
|
|
title: trans.noarg('goDeeper'),
|
|
'data-icon': 'O',
|
|
},
|
|
hook: {
|
|
insert: vnode =>
|
|
(vnode.elm as HTMLElement).addEventListener('click', () => {
|
|
ceval.goDeeper();
|
|
ceval.redraw();
|
|
}),
|
|
},
|
|
})
|
|
);
|
|
else if (!evs.client.cloud && evs.client.knps) t.push(', ' + Math.round(evs.client.knps) + ' knodes/s');
|
|
return t;
|
|
}
|
|
|
|
function threatInfo(ctrl: ParentCtrl, threat?: Tree.ClientEval | false): string {
|
|
if (!threat) return ctrl.trans.noarg('loadingEngine');
|
|
let t = ctrl.trans('depthX', (threat.depth || 0) + '/' + threat.maxDepth);
|
|
if (threat.knps) t += ', ' + Math.round(threat.knps) + ' knodes/s';
|
|
return t;
|
|
}
|
|
|
|
function threatButton(ctrl: ParentCtrl): VNode | null {
|
|
if (ctrl.disableThreatMode && ctrl.disableThreatMode()) return null;
|
|
return h('a.show-threat', {
|
|
class: {
|
|
active: ctrl.threatMode(),
|
|
hidden: !!ctrl.getNode().check,
|
|
},
|
|
attrs: {
|
|
'data-icon': '7',
|
|
title: ctrl.trans.noarg('showThreat') + ' (x)',
|
|
},
|
|
hook: {
|
|
insert: vnode => (vnode.elm as HTMLElement).addEventListener('click', ctrl.toggleThreatMode),
|
|
},
|
|
});
|
|
}
|
|
|
|
function engineName(ctrl: CevalCtrl): VNode[] {
|
|
const version = ctrl.engineName();
|
|
return [
|
|
h(
|
|
'span',
|
|
{ attrs: { title: version || '' } },
|
|
ctrl.technology == 'nnue' ? 'Stockfish 13+' : ctrl.technology == 'hce' ? 'Stockfish 11+' : 'Stockfish 10+'
|
|
),
|
|
ctrl.technology == 'nnue'
|
|
? h(
|
|
'span.technology.good',
|
|
{
|
|
attrs: { title: 'Multi-threaded WebAssembly with SIMD (efficiently updatable neural network, strongest)' },
|
|
},
|
|
'NNUE'
|
|
)
|
|
: ctrl.technology == 'hce'
|
|
? h(
|
|
'span.technology.good',
|
|
{ attrs: { title: 'Multi-threaded WebAssembly (classical hand crafted evaluation)' } },
|
|
'HCE'
|
|
)
|
|
: ctrl.technology == 'wasm'
|
|
? h('span.technology', { attrs: { title: 'Single-threaded WebAssembly fallback (slow)' } }, 'WASM')
|
|
: h('span.technology', { attrs: { title: 'Single-threaded JavaScript fallback (very slow)' } }, 'ASMJS'),
|
|
];
|
|
}
|
|
|
|
const serverNodes = 4e6;
|
|
|
|
export function getBestEval(evs: NodeEvals): Eval | undefined {
|
|
const serverEv = evs.server,
|
|
localEv = evs.client;
|
|
|
|
if (!serverEv) return localEv;
|
|
if (!localEv) return serverEv;
|
|
|
|
// Prefer localEv if it exeeds fishnet node limit or finds a better mate.
|
|
if (
|
|
localEv.nodes > serverNodes ||
|
|
(typeof localEv.mate !== 'undefined' &&
|
|
(typeof serverEv.mate === 'undefined' || Math.abs(localEv.mate) < Math.abs(serverEv.mate)))
|
|
)
|
|
return localEv;
|
|
|
|
return serverEv;
|
|
}
|
|
|
|
export function renderGauge(ctrl: ParentCtrl): VNode | undefined {
|
|
if (ctrl.ongoing || !ctrl.showEvalGauge()) return;
|
|
const bestEv = getBestEval(ctrl.currentEvals());
|
|
let ev;
|
|
if (bestEv) {
|
|
ev = winningChances.povChances('white', bestEv);
|
|
gaugeLast = ev;
|
|
} else ev = gaugeLast;
|
|
return h(
|
|
'div.eval-gauge',
|
|
{
|
|
class: {
|
|
empty: ev === null,
|
|
reverse: ctrl.getOrientation() === 'black',
|
|
},
|
|
},
|
|
[h('div.black', { attrs: { style: `height: ${100 - (ev + 1) * 50}%` } }), ...gaugeTicks]
|
|
);
|
|
}
|
|
|
|
export function renderCeval(ctrl: ParentCtrl): VNode | undefined {
|
|
const instance = ctrl.getCeval(),
|
|
trans = ctrl.trans;
|
|
if (!instance.allowed() || !instance.possible || !ctrl.showComputer()) return;
|
|
const enabled = instance.enabled(),
|
|
evs = ctrl.currentEvals(),
|
|
threatMode = ctrl.threatMode(),
|
|
threat = threatMode && ctrl.getNode().threat,
|
|
bestEv = threat || getBestEval(evs);
|
|
let pearl: VNode | string, percent: number;
|
|
if (bestEv && typeof bestEv.cp !== 'undefined') {
|
|
pearl = renderEval(bestEv.cp);
|
|
percent = evs.client
|
|
? Math.min(100, Math.round((100 * evs.client.depth) / (evs.client.maxDepth || instance.effectiveMaxDepth())))
|
|
: 0;
|
|
} else if (bestEv && defined(bestEv.mate)) {
|
|
pearl = '#' + bestEv.mate;
|
|
percent = 100;
|
|
} else if (ctrl.outcome()) {
|
|
pearl = '-';
|
|
percent = 0;
|
|
} else {
|
|
pearl = enabled ? h('i.ddloader') : h('i');
|
|
percent = 0;
|
|
}
|
|
if (threatMode) {
|
|
if (threat) percent = Math.min(100, Math.round((100 * threat.depth) / threat.maxDepth));
|
|
else percent = 0;
|
|
}
|
|
|
|
const progressBar: VNode | null = enabled
|
|
? h(
|
|
'div.bar',
|
|
h('span', {
|
|
class: { threat: threatMode },
|
|
attrs: { style: `width: ${percent}%` },
|
|
hook: {
|
|
postpatch: (old, vnode) => {
|
|
if (old.data!.percent > percent || !!old.data!.threatMode != threatMode) {
|
|
const el = vnode.elm as HTMLElement;
|
|
const p = el.parentNode as HTMLElement;
|
|
p.removeChild(el);
|
|
p.appendChild(el);
|
|
}
|
|
vnode.data!.percent = percent;
|
|
vnode.data!.threatMode = threatMode;
|
|
},
|
|
},
|
|
})
|
|
)
|
|
: null;
|
|
|
|
const body: Array<VNode | null> = enabled
|
|
? [
|
|
h('pearl', [pearl]),
|
|
h('div.engine', [
|
|
...(threatMode ? [trans.noarg('showThreat')] : engineName(instance)),
|
|
h(
|
|
'span.info',
|
|
ctrl.outcome()
|
|
? [trans.noarg('gameOver')]
|
|
: threatMode
|
|
? [threatInfo(ctrl, threat)]
|
|
: localEvalInfo(ctrl, evs)
|
|
),
|
|
]),
|
|
]
|
|
: [
|
|
pearl ? h('pearl', [pearl]) : null,
|
|
h('help', [...engineName(instance), h('br'), trans.noarg('inLocalBrowser')]),
|
|
];
|
|
|
|
const switchButton: VNode | null =
|
|
ctrl.mandatoryCeval && ctrl.mandatoryCeval()
|
|
? null
|
|
: h(
|
|
'div.switch',
|
|
{
|
|
attrs: { title: trans.noarg('toggleLocalEvaluation') + ' (l)' },
|
|
},
|
|
[
|
|
h('input#analyse-toggle-ceval.cmn-toggle.cmn-toggle--subtle', {
|
|
attrs: {
|
|
type: 'checkbox',
|
|
checked: enabled,
|
|
},
|
|
hook: {
|
|
insert: vnode => (vnode.elm as HTMLElement).addEventListener('change', ctrl.toggleCeval),
|
|
},
|
|
}),
|
|
h('label', { attrs: { for: 'analyse-toggle-ceval' } }),
|
|
]
|
|
);
|
|
|
|
return h(
|
|
'div.ceval' + (enabled ? '.enabled' : ''),
|
|
{
|
|
class: {
|
|
computing: percent < 100 && instance.isComputing(),
|
|
},
|
|
},
|
|
[progressBar, ...body, threatButton(ctrl), switchButton]
|
|
);
|
|
}
|
|
|
|
function getElFen(el: HTMLElement): string {
|
|
return el.getAttribute('data-fen')!;
|
|
}
|
|
|
|
function getElUci(e: MouseEvent): string | undefined {
|
|
return (
|
|
$(e.target as HTMLElement)
|
|
.closest('div.pv')
|
|
.attr('data-uci') || undefined
|
|
);
|
|
}
|
|
|
|
function checkHover(el: HTMLElement, instance: CevalCtrl): void {
|
|
lichess.requestIdleCallback(
|
|
() => instance.setHovering(getElFen(el), $(el).find('div.pv:hover').attr('data-uci') || undefined),
|
|
500
|
|
);
|
|
}
|
|
|
|
export function renderPvs(ctrl: ParentCtrl): VNode | undefined {
|
|
const instance = ctrl.getCeval();
|
|
if (!instance.allowed() || !instance.possible || !instance.enabled()) return;
|
|
const multiPv = parseInt(instance.multiPv()),
|
|
node = ctrl.getNode(),
|
|
setup = parseFen(node.fen).unwrap();
|
|
let pvs: Tree.PvData[],
|
|
threat = false;
|
|
if (ctrl.threatMode() && node.threat) {
|
|
pvs = node.threat.pvs;
|
|
threat = true;
|
|
} else if (node.ceval) pvs = node.ceval.pvs;
|
|
else pvs = [];
|
|
if (threat) {
|
|
setup.turn = opposite(setup.turn);
|
|
if (setup.turn == 'white') setup.fullmoves += 1;
|
|
}
|
|
const pos = setupPosition(lichessVariantRules(instance.variant.key), setup);
|
|
return h(
|
|
'div.pv_box',
|
|
{
|
|
attrs: { 'data-fen': node.fen },
|
|
hook: {
|
|
insert: vnode => {
|
|
const el = vnode.elm as HTMLElement;
|
|
el.addEventListener('mouseover', (e: MouseEvent) => {
|
|
instance.setHovering(getElFen(el), getElUci(e));
|
|
const pvBoard = (e.target as HTMLElement).getAttribute('data-board');
|
|
if (pvBoard) {
|
|
const [fen, uci] = pvBoard.split('|');
|
|
instance.setPvBoard({ fen, uci });
|
|
}
|
|
});
|
|
el.addEventListener('mouseout', () => instance.setHovering(getElFen(el)));
|
|
el.addEventListener('mousedown', (e: MouseEvent) => {
|
|
const uci = getElUci(e);
|
|
if (uci) ctrl.playUci(uci);
|
|
});
|
|
el.addEventListener('mouseleave', () => instance.setPvBoard(null));
|
|
checkHover(el, instance);
|
|
},
|
|
postpatch: (_, vnode) => checkHover(vnode.elm as HTMLElement, instance),
|
|
},
|
|
},
|
|
[...Array(multiPv).keys()]
|
|
.map(function (i) {
|
|
if (!pvs[i]) return h('div.pv');
|
|
return h(
|
|
'div.pv',
|
|
threat
|
|
? {}
|
|
: {
|
|
attrs: { 'data-uci': pvs[i].moves[0] },
|
|
},
|
|
[
|
|
multiPv > 1 ? h('strong', defined(pvs[i].mate) ? '#' + pvs[i].mate : renderEval(pvs[i].cp!)) : null,
|
|
...pos.unwrap(
|
|
pos => renderPv(pos.clone(), pvs[i].moves.slice(0, 12)),
|
|
_ => ['--']
|
|
),
|
|
]
|
|
);
|
|
})
|
|
.concat([renderPvBoard(ctrl) as VNode])
|
|
);
|
|
}
|
|
|
|
function renderPv(pos: Position, pv: Uci[]): VNode[] {
|
|
const vnodes: VNode[] = [];
|
|
let key = makeBoardFen(pos.board);
|
|
for (let i = 0; i < pv.length; i++) {
|
|
let text;
|
|
if (pos.turn === 'white') {
|
|
text = `${pos.fullmoves}.`;
|
|
} else if (i === 0) {
|
|
text = `${pos.fullmoves}...`;
|
|
}
|
|
if (text) {
|
|
vnodes.push(h('span', { key: text }, text));
|
|
}
|
|
const uci = pv[i];
|
|
const san = makeSanAndPlay(pos, parseUci(uci)!);
|
|
const fen = makeBoardFen(pos.board); // Chessground uses only board fen
|
|
if (san === '--') {
|
|
break;
|
|
}
|
|
key += '|' + uci;
|
|
vnodes.push(
|
|
h(
|
|
'span.pv-san',
|
|
{
|
|
key,
|
|
attrs: {
|
|
'data-board': `${fen}|${uci}`,
|
|
},
|
|
},
|
|
san
|
|
)
|
|
);
|
|
}
|
|
return vnodes;
|
|
}
|
|
|
|
function renderPvBoard(ctrl: ParentCtrl): VNode | undefined {
|
|
const instance = ctrl.getCeval();
|
|
const pvBoard = instance.pvBoard();
|
|
if (!pvBoard) {
|
|
return;
|
|
}
|
|
const { fen, uci } = pvBoard;
|
|
const lastMove = uci[1] === '@' ? [uci.slice(2)] : [uci.slice(0, 2), uci.slice(2, 4)];
|
|
const orientation = ctrl.getOrientation();
|
|
const cgConfig = {
|
|
fen,
|
|
lastMove,
|
|
orientation,
|
|
coordinates: false,
|
|
viewOnly: true,
|
|
resizable: false,
|
|
drawable: {
|
|
enabled: false,
|
|
visible: false,
|
|
},
|
|
};
|
|
const cgVNode = h('div.cg-wrap.is2d', {
|
|
hook: {
|
|
insert: (vnode: any) => (vnode.elm._cg = window.Chessground(vnode.elm, cgConfig)),
|
|
update: (vnode: any) => vnode.elm._cg.set(cgConfig),
|
|
destroy: (vnode: any) => vnode.elm._cg.destroy(),
|
|
},
|
|
});
|
|
return h('div.pv-board', h('div.pv-board-square', cgVNode));
|
|
}
|