Merge pull request #20453 from Tebro/feat/local_eval_glyphs

Add live move annotations using local eval
This commit is contained in:
Thibault Duplessis
2026-05-20 11:00:50 +02:00
committed by GitHub
5 changed files with 64 additions and 6 deletions
+7 -1
View File
@@ -176,7 +176,13 @@ export function compute(ctrl: AnalyseCtrl): DrawShape[] {
}
});
}
if (ctrl.showMoveAnnotationsOnBoard()) shapes = shapes.concat(annotationShapes(ctrl.node));
if (ctrl.showMoveAnnotationsOnBoard()) {
const liveGlyph = ctrl.liveAnnotate.get(ctrl.path);
shapes = shapes.concat(
// Override server analysis glyphs as local eval also overrides the eval score
annotationShapes(liveGlyph ? { ...ctrl.node, glyphs: [liveGlyph] } : ctrl.node),
);
}
if (ctrl.showVariationArrows()) hiliteVariations(ctrl, shapes);
if (ctrl.isCevalAllowed()) {
+5
View File
@@ -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 LiveAnnotate from './liveAnnotate';
import MotifCtrl from './motif/motifCtrl';
import Navigate from './navigate';
import { nextGlyphSymbol, add3or5FoldGlyphs } from './nodeFinder';
@@ -75,6 +76,7 @@ export default class AnalyseCtrl implements CevalHandler {
chessground: ChessgroundApi;
ceval: CevalCtrl;
evalCache: EvalCache;
liveAnnotate: LiveAnnotate;
navigate: Navigate;
idbTree: IdbTree = new IdbTree(this);
actionMenu: Toggle = toggle(false);
@@ -177,6 +179,7 @@ export default class AnalyseCtrl implements CevalHandler {
});
this.instanciateEvalCache();
this.liveAnnotate = new LiveAnnotate();
if (opts.inlinePgn) this.data = this.changePgn(opts.inlinePgn, false) || this.data;
@@ -738,6 +741,8 @@ export default class AnalyseCtrl implements CevalHandler {
if (node.ceval?.cloud && this.ceval.isDeeper()) node.ceval = ev;
}
if (!isThreat) this.liveAnnotate.onNewCeval(path, node, this.tree);
if (path === this.path) {
this.setAutoShapes();
if (!isThreat) {
+41
View File
@@ -0,0 +1,41 @@
import { povChances } from 'lib/ceval/winningChances';
import type { TreeWrapper } from 'lib/tree';
import type { Glyph, TreeNode, TreePath } 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 default class LiveAnnotate {
private readonly glyphs = new Map<TreePath, Glyph>();
readonly get = this.glyphs.get.bind(this.glyphs);
readonly onNewCeval = (path: TreePath, node: TreeNode, tree: TreeWrapper): void => {
const parent = tree.parentNode(path);
this.update(path, node, parent);
node.children.forEach(child => this.update(path + child.id, child, node));
};
private readonly 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;
};
private readonly update = (path: TreePath, node: TreeNode, parent: TreeNode): void => {
if (!path.length) return;
const glyph = parent.ceval && node.ceval && this.liveGlyph(parent.ceval, node.ceval, node.ply);
if (glyph) this.glyphs.set(path, glyph);
else this.glyphs.delete(path);
};
}
+6 -2
View File
@@ -164,11 +164,14 @@ export class InlineView {
'pending-deletion': path.startsWith(ctrl.pendingDeletionPath() || ' '),
'pending-copy': !!ctrl.pendingCopyPath()?.startsWith(path),
};
if (ctrl.showMoveGlyphs())
node.glyphs
const liveGlyph = ctrl.liveAnnotate.get(path);
const glyphs = liveGlyph ? [liveGlyph] : 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 +180,7 @@ export class InlineView {
isMainline && !this.inline,
ctrl.showMoveGlyphs(),
ctrl.allowedEval(node) || false,
glyphs,
),
]);
}
+5 -3
View File
@@ -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;