mirror of
https://github.com/lichess-org/lila.git
synced 2026-05-26 13:51:00 +00:00
Add live move annotations using local eval
- Adds live annotations derived from local engine eval - Adds a UI toggle to enable/disable live annotations - Renders live glyphs on the board and in the move list - Rebuilds and caches glyphs when toggling or navigating
This commit is contained in:
committed by
Thibault Duplessis
parent
e658a5e678
commit
4b07c3cdcd
@@ -176,7 +176,14 @@ export function compute(ctrl: AnalyseCtrl): DrawShape[] {
|
||||
}
|
||||
});
|
||||
}
|
||||
if (ctrl.showMoveAnnotationsOnBoard()) shapes = shapes.concat(annotationShapes(ctrl.node));
|
||||
if (ctrl.showMoveAnnotationsOnBoard()) {
|
||||
const liveGlyphs = ctrl.liveGlyphsOf(ctrl.path);
|
||||
const glyphs = liveGlyphs ?? ctrl.node.glyphs;
|
||||
shapes = shapes.concat(
|
||||
// Override server analysis glyphs as local eval also overrides the eval score
|
||||
annotationShapes(liveGlyphs ? { ...ctrl.node, glyphs } : ctrl.node),
|
||||
);
|
||||
}
|
||||
if (ctrl.showVariationArrows()) hiliteVariations(ctrl, shapes);
|
||||
|
||||
if (ctrl.isCevalAllowed()) {
|
||||
|
||||
+41
-2
@@ -39,7 +39,7 @@ import { pubsub } from 'lib/pubsub';
|
||||
import { storedBooleanProp, storedBooleanPropWithEffect } from 'lib/storage';
|
||||
import { makeTree, treePath, treeOps, type TreeWrapper } from 'lib/tree';
|
||||
import { completeNode } from 'lib/tree/node';
|
||||
import type { ClientEval, LocalEval, ServerEval, TreeNode, TreePath } from 'lib/tree/types';
|
||||
import type { ClientEval, Glyph, LocalEval, ServerEval, TreeNode, TreePath } from 'lib/tree/types';
|
||||
import { confirm } from 'lib/view';
|
||||
|
||||
import { Autoplay, type AutoplayDelay } from './autoplay';
|
||||
@@ -52,6 +52,7 @@ import { ForkCtrl } from './fork';
|
||||
import { IdbTree } from './idbTree';
|
||||
import type { AnalyseOpts, AnalyseData, ServerEvalData, JustCaptured, NvuiPlugin } from './interfaces';
|
||||
import * as keyboard from './keyboard';
|
||||
import { isLocalEval, liveNodeGlyphs } from './liveAnnotate';
|
||||
import MotifCtrl from './motif/motifCtrl';
|
||||
import Navigate from './navigate';
|
||||
import { nextGlyphSymbol, add3or5FoldGlyphs } from './nodeFinder';
|
||||
@@ -122,6 +123,8 @@ export default class AnalyseCtrl implements CevalHandler {
|
||||
);
|
||||
showFishnetAnalysis = storedBooleanProp('analyse.show-computer', true);
|
||||
possiblyShowMoveAnnotationsOnBoard = storedBooleanProp('analyse.show-move-annotation', true);
|
||||
liveAnnotationsProp = storedBooleanProp('analyse.live-annotations', false);
|
||||
liveGlyphs = new Map<TreePath, Glyph[]>();
|
||||
keyboardHelp: boolean = location.hash === '#keyboard';
|
||||
threatMode: Prop<boolean> = prop(false);
|
||||
disclosureMode = storedBooleanProp('analyse.disclosure.enabled', false);
|
||||
@@ -738,6 +741,11 @@ export default class AnalyseCtrl implements CevalHandler {
|
||||
if (node.ceval?.cloud && this.ceval.isDeeper()) node.ceval = ev;
|
||||
}
|
||||
|
||||
if (this.liveAnnotationsProp() && !isThreat && isLocalEval(ev)) {
|
||||
this.annotateLivePath(path);
|
||||
this.annotateLiveChildren(path, node);
|
||||
}
|
||||
|
||||
if (path === this.path) {
|
||||
this.setAutoShapes();
|
||||
if (!isThreat) {
|
||||
@@ -752,6 +760,28 @@ export default class AnalyseCtrl implements CevalHandler {
|
||||
});
|
||||
};
|
||||
|
||||
private annotateLivePath(path: TreePath): void {
|
||||
if (path.length < 2) return;
|
||||
const glyphs = liveNodeGlyphs(this.tree.nodeAtPath(path), this.tree.parentNode(path));
|
||||
if (glyphs) this.liveGlyphs.set(path, glyphs);
|
||||
else this.liveGlyphs.delete(path);
|
||||
}
|
||||
|
||||
private annotateLiveChildren(path: TreePath, node: TreeNode): void {
|
||||
node.children.forEach(child => this.annotateLivePath(path + child.id));
|
||||
}
|
||||
|
||||
private rebuildLiveGlyphs(node = this.tree.root, path: TreePath = treePath.root): void {
|
||||
node.children.forEach(child => {
|
||||
const childPath = path + child.id;
|
||||
this.annotateLivePath(childPath);
|
||||
this.rebuildLiveGlyphs(child, childPath);
|
||||
});
|
||||
}
|
||||
|
||||
liveGlyphsOf = (path: TreePath): Glyph[] | undefined =>
|
||||
this.liveAnnotationsProp() ? this.liveGlyphs.get(path) : undefined;
|
||||
|
||||
private initCeval(): void {
|
||||
const opts: CevalOpts = {
|
||||
variant: this.data.game.variant,
|
||||
@@ -825,7 +855,8 @@ export default class AnalyseCtrl implements CevalHandler {
|
||||
return this.showFishnetAnalysis() || (this.cevalEnabled() && this.isCevalAllowed());
|
||||
}
|
||||
|
||||
showMoveGlyphs = (): boolean => (this.study && !this.study.relay) || this.showFishnetAnalysis();
|
||||
showMoveGlyphs = (): boolean =>
|
||||
(this.study && !this.study.relay) || this.showFishnetAnalysis() || this.liveAnnotationsProp();
|
||||
|
||||
showMoveAnnotationsOnBoard = (): boolean =>
|
||||
this.possiblyShowMoveAnnotationsOnBoard() && this.showMoveGlyphs();
|
||||
@@ -900,6 +931,14 @@ export default class AnalyseCtrl implements CevalHandler {
|
||||
pubsub.emit('analysis.comp.toggle', this.showFishnetAnalysis());
|
||||
};
|
||||
|
||||
toggleLiveAnnotations = () => {
|
||||
this.liveAnnotationsProp(!this.liveAnnotationsProp());
|
||||
this.liveGlyphs.clear();
|
||||
if (this.liveAnnotationsProp()) this.rebuildLiveGlyphs();
|
||||
this.resetAutoShapes();
|
||||
this.redraw();
|
||||
};
|
||||
|
||||
toggleActionMenu = () => {
|
||||
if (!this.actionMenu() && this.explorer.enabled()) this.explorer.toggle();
|
||||
this.actionMenu.toggle();
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { povChances } from 'lib/ceval/winningChances';
|
||||
import type { ClientEval, Glyph, LocalEval, TreeNode } from 'lib/tree/types';
|
||||
|
||||
const glyphs = {
|
||||
inaccuracy: { id: 6, symbol: '?!', name: 'Inaccuracy' } as Glyph,
|
||||
mistake: { id: 2, symbol: '?', name: 'Mistake' } as Glyph,
|
||||
blunder: { id: 4, symbol: '??', name: 'Blunder' } as Glyph,
|
||||
};
|
||||
|
||||
export const isLocalEval = (ev?: ClientEval): ev is LocalEval => !!ev && !ev.cloud;
|
||||
|
||||
export function liveGlyph(parentEval: EvalScore, currentEval: EvalScore, ply: Ply): Glyph | undefined {
|
||||
const color: Color = ply % 2 === 1 ? 'white' : 'black';
|
||||
const loss = povChances(color, parentEval) - povChances(color, currentEval);
|
||||
if (loss > 0.3) return glyphs.blunder;
|
||||
if (loss > 0.2) return glyphs.mistake;
|
||||
if (loss > 0.1) return glyphs.inaccuracy;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function liveNodeGlyphs(node: TreeNode, parentNode: TreeNode): Glyph[] | undefined {
|
||||
if (!isLocalEval(parentNode.ceval) || !isLocalEval(node.ceval)) return;
|
||||
const glyph = liveGlyph(parentNode.ceval, node.ceval, node.ply);
|
||||
return glyph ? [glyph] : [];
|
||||
}
|
||||
@@ -164,11 +164,13 @@ export class InlineView {
|
||||
'pending-deletion': path.startsWith(ctrl.pendingDeletionPath() || ' '),
|
||||
'pending-copy': !!ctrl.pendingCopyPath()?.startsWith(path),
|
||||
};
|
||||
if (ctrl.showMoveGlyphs())
|
||||
node.glyphs
|
||||
const glyphs = ctrl.liveGlyphsOf(path) ?? node.glyphs;
|
||||
if (ctrl.showMoveGlyphs()) {
|
||||
glyphs
|
||||
?.map(g => this.glyphs[g.id - 1])
|
||||
.filter(Boolean)
|
||||
.forEach(cls => (classes[cls] = true));
|
||||
}
|
||||
return hl('move', { attrs: { p: path }, class: classes }, [
|
||||
parentDisclose && this.disclosureBtn(parentNode, parentPath),
|
||||
withIndex && renderIndex(node.ply, true),
|
||||
@@ -177,6 +179,7 @@ export class InlineView {
|
||||
isMainline && !this.inline,
|
||||
ctrl.showMoveGlyphs(),
|
||||
ctrl.allowedEval(node) || false,
|
||||
glyphs,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -184,6 +184,14 @@ export function view(ctrl: AnalyseCtrl): VNode {
|
||||
prop: ctrl.showManeuverMoveArrowsProp,
|
||||
redraw: ctrl.redraw,
|
||||
}),
|
||||
cmnToggleWrap({
|
||||
id: 'live-annotations',
|
||||
name: 'Live move annotations',
|
||||
title: 'Show inaccuracy/mistake/blunder from local engine eval',
|
||||
checked: ctrl.liveAnnotationsProp(),
|
||||
change: ctrl.toggleLiveAnnotations,
|
||||
redraw: ctrl.redraw,
|
||||
}),
|
||||
displayColumns() > 1 &&
|
||||
cmnToggleWrapProp({
|
||||
id: 'gauge',
|
||||
|
||||
@@ -12,7 +12,7 @@ import * as licon from 'lib/licon';
|
||||
import * as Prefs from 'lib/prefs';
|
||||
import { storage } from 'lib/storage';
|
||||
import { path as treePath } from 'lib/tree/tree';
|
||||
import type { ClientEval, ServerEval, TreeNode, TreePath } from 'lib/tree/types';
|
||||
import type { ClientEval, Glyph, ServerEval, TreeNode, TreePath } from 'lib/tree/types';
|
||||
import {
|
||||
type VNode,
|
||||
type LooseVNodes,
|
||||
@@ -276,6 +276,7 @@ export function renderMoveNodes(
|
||||
withEval: boolean,
|
||||
withGlyphs: boolean,
|
||||
ev?: ClientEval | ServerEval | false,
|
||||
glyphs?: Glyph[],
|
||||
): VNode[] {
|
||||
ev ??= node.ceval ?? node.eval; // ev = false will override withEval
|
||||
const evalText = !ev
|
||||
@@ -286,8 +287,9 @@ export function renderMoveNodes(
|
||||
? `#${ev.mate}`
|
||||
: '';
|
||||
const nodes = [h('san', fixCrazySan(node.san!))];
|
||||
if (withGlyphs && node.glyphs)
|
||||
node.glyphs.forEach(g => nodes.push(h('glyph', { attrs: { title: g.name } }, g.symbol)));
|
||||
const relevantGlyphs = glyphs ?? node.glyphs;
|
||||
if (withGlyphs && relevantGlyphs)
|
||||
relevantGlyphs.forEach(g => nodes.push(h('glyph', { attrs: { title: g.name } }, g.symbol)));
|
||||
if (withEval && node.shapes?.length) nodes.push(h('shapes'));
|
||||
if (withEval && evalText) nodes.push(h('eval', evalText.replace('-', '−')));
|
||||
return nodes;
|
||||
|
||||
Reference in New Issue
Block a user