lila/ui/analyse/src/ctrl.ts

924 lines
28 KiB
TypeScript
Raw Normal View History

2017-06-24 06:20:20 -06:00
import * as cg from 'chessground/types';
import * as chessUtil from 'chess';
import * as game from 'game';
2017-06-21 05:03:22 -06:00
import * as keyboard from './keyboard';
import * as speech from './speech';
2017-06-21 05:03:22 -06:00
import * as util from './util';
2020-09-06 06:38:32 -06:00
import * as xhr from 'common/xhr';
import debounce from 'common/debounce';
import GamebookPlayCtrl from './study/gamebook/gamebookPlayCtrl';
2017-06-30 04:17:09 -06:00
import makeStudy from './study/studyCtrl';
import throttle from 'common/throttle';
import { AnalyseOpts, AnalyseData, ServerEvalData, Key, JustCaptured, NvuiPlugin, Redraw } from './interfaces';
import { Api as ChessgroundApi } from 'chessground/api';
import { Autoplay, AutoplayDelay } from './autoplay';
import { build as makeTree, path as treePath, ops as treeOps, TreeWrapper } from 'tree';
import { compute as computeAutoShapes } from './autoShape';
import { Config as ChessgroundConfig } from 'chessground/config';
2021-04-07 10:29:53 -06:00
import { ActionMenuCtrl } from './actionMenu';
import { ctrl as cevalCtrl, isEvalBetter, sanIrreversible, CevalCtrl, EvalMeta } from 'ceval';
import { ctrl as treeViewCtrl, TreeView } from './treeView/treeView';
import { defined, prop, Prop } from 'common';
import { DrawShape } from 'chessground/draw';
import { ForecastCtrl } from './forecast/interfaces';
import { lichessRules } from 'chessops/compat';
import { make as makeEvalCache, EvalCache } from './evalCache';
import { make as makeForecast } from './forecast/forecastCtrl';
import { make as makeFork, ForkCtrl } from './fork';
import { make as makePractice, PracticeCtrl } from './practice/practiceCtrl';
import { make as makeRetro, RetroCtrl } from './retrospect/retroCtrl';
import { make as makeSocket, Socket } from './socket';
2017-06-24 05:10:09 -06:00
import { nextGlyphSymbol } from './nodeFinder';
import { opposite, parseUci, makeSquare, roleToChar } from 'chessops/util';
import { Outcome, isNormal } from 'chessops/types';
import { parseFen } from 'chessops/fen';
import { Position, PositionError } from 'chessops/chess';
import { Result } from '@badrap/result';
import { setupPosition } from 'chessops/variant';
import { storedProp, StoredBooleanProp } from 'common/storage';
2021-04-20 12:37:16 -06:00
import { AnaMove, StudyCtrl } from './study/interfaces';
import { StudyPracticeCtrl } from './study/practice/interfaces';
import { valid as crazyValid } from './crazy/crazyCtrl';
import { PromotionCtrl } from 'chess/promotion';
2021-10-06 05:06:23 -06:00
import wikiTheory, { WikiTheory } from './wiki';
import ExplorerCtrl from './explorer/explorerCtrl';
2017-06-21 05:39:50 -06:00
export default class AnalyseCtrl {
data: AnalyseData;
element: HTMLElement;
2017-07-02 05:59:36 -06:00
tree: TreeWrapper;
socket: Socket;
2017-06-22 08:40:51 -06:00
chessground: ChessgroundApi;
2017-06-23 15:30:21 -06:00
trans: Trans;
ceval: CevalCtrl;
2017-06-23 15:30:21 -06:00
evalCache: EvalCache;
// current tree state, cursor, and denormalized node lists
path: Tree.Path;
node: Tree.Node;
nodeList: Tree.Node[];
mainline: Tree.Node[];
// sub controllers
actionMenu: ActionMenuCtrl;
autoplay: Autoplay;
explorer: ExplorerCtrl;
forecast?: ForecastCtrl;
retro?: RetroCtrl;
fork: ForkCtrl;
practice?: PracticeCtrl;
study?: StudyCtrl;
2017-08-15 18:06:59 -06:00
studyPractice?: StudyPracticeCtrl;
promotion: PromotionCtrl;
2021-10-06 04:06:06 -06:00
wiki?: WikiTheory;
// state flags
2017-06-24 06:20:20 -06:00
justPlayed?: string; // pos
justDropped?: string; // role
2017-06-27 04:27:48 -06:00
justCaptured?: JustCaptured;
autoScrollRequested = false;
redirecting = false;
onMainline = true;
synthetic: boolean; // false if coming from a real game
ongoing: boolean; // true if real game is ongoing
// display flags
flipped = false;
embed: boolean;
showComments = true; // whether to display comments in the move tree
2019-01-14 20:31:01 -07:00
showAutoShapes: StoredBooleanProp = storedProp('show-auto-shapes', true);
2017-06-24 05:10:09 -06:00
showGauge: StoredBooleanProp = storedProp('show-gauge', true);
showComputer: StoredBooleanProp = storedProp('show-computer', true);
2021-02-26 04:31:38 -07:00
showMoveAnnotation: StoredBooleanProp = storedProp('show-move-annotation', true);
keyboardHelp: boolean = location.hash === '#keyboard';
threatMode: Prop<boolean> = prop(false);
treeView: TreeView;
cgVersion = {
js: 1, // increment to recreate chessground
2021-02-06 06:26:05 -07:00
dom: 1,
};
// underboard inputs
2020-06-18 13:43:23 -06:00
fenInput?: string;
pgnInput?: string;
// other paths
2017-06-22 08:40:51 -06:00
initialPath: Tree.Path;
2017-06-25 04:19:44 -06:00
contextMenuPath?: Tree.Path;
2017-06-22 08:40:51 -06:00
gamePath?: Tree.Path;
// misc
cgConfig: any; // latest chessground config (useful for revert)
2017-06-24 05:10:09 -06:00
music?: any;
2019-01-25 01:06:31 -07:00
nvui?: NvuiPlugin;
pvUciQueue: Uci[] = [];
2019-08-13 01:47:27 -06:00
constructor(readonly opts: AnalyseOpts, readonly redraw: Redraw) {
2017-06-30 05:04:22 -06:00
this.data = opts.data;
this.element = opts.element;
this.embed = opts.embed;
this.trans = opts.trans;
this.treeView = treeViewCtrl(opts.embed ? 'inline' : 'column');
this.promotion = new PromotionCtrl(this.withCg, () => this.withCg(g => g.set(this.cgConfig)), this.redraw);
2017-06-23 15:30:21 -06:00
2017-06-30 05:04:22 -06:00
if (this.data.forecast) this.forecast = makeForecast(this.data.forecast, this.data, redraw);
2021-10-06 05:06:23 -06:00
if (this.opts.wiki) this.wiki = wikiTheory();
2017-06-23 15:30:21 -06:00
if (lichess.AnalyseNVUI) this.nvui = lichess.AnalyseNVUI(redraw) as NvuiPlugin;
2019-01-25 03:32:42 -07:00
2017-06-23 15:30:21 -06:00
this.instanciateEvalCache();
2017-06-30 05:04:22 -06:00
this.initialize(this.data, false);
this.instanciateCeval();
2017-06-22 08:40:51 -06:00
this.initialPath = treePath.root;
{
2018-03-08 07:14:28 -07:00
const loc = window.location,
hashPly = loc.hash === '#last' ? this.tree.lastPly() : parseInt(loc.hash.substr(1));
if (hashPly) {
// remove location hash - https://stackoverflow.com/questions/1397329/how-to-remove-the-hash-from-window-location-with-javascript-without-page-refresh/5298684#5298684
window.history.replaceState(null, '', loc.pathname + loc.search);
const mainline = treeOps.mainlineNodeList(this.tree.root);
this.initialPath = treeOps.takePathWhile(mainline, n => n.ply <= hashPly);
}
}
2017-06-22 08:40:51 -06:00
2017-06-24 05:10:09 -06:00
this.setPath(this.initialPath);
2017-06-30 05:04:22 -06:00
this.showGround();
this.onToggleComputer();
this.startCeval();
this.explorer.setNode();
2021-02-06 06:26:05 -07:00
this.study = opts.study
? makeStudy(opts.study, this, (opts.tagTypes || '').split(','), opts.practice, opts.relay)
: undefined;
this.studyPractice = this.study ? this.study.practice : undefined;
2017-06-30 05:04:22 -06:00
if (location.hash === '#practice' || (this.study && this.study.data.chapter.practice)) this.togglePractice();
else if (location.hash === '#menu') lichess.requestIdleCallback(this.actionMenu.toggle, 500);
2017-06-30 05:04:22 -06:00
keyboard.bind(this);
2021-04-18 13:19:55 -06:00
lichess.pubsub.on('jump', (ply: string) => {
2017-06-30 11:27:04 -06:00
this.jumpToMain(parseInt(ply));
this.redraw();
});
2017-06-30 05:04:22 -06:00
lichess.pubsub.on('sound_set', (set: string) => {
2017-06-30 11:27:04 -06:00
if (!this.music && set === 'music')
lichess.loadScript('javascripts/music/replay.js').then(() => {
2017-06-30 11:27:04 -06:00
this.music = window.lichessReplayMusic();
});
2019-01-14 20:31:01 -07:00
if (this.music && set !== 'music') this.music = null;
2017-06-30 11:27:04 -06:00
});
2018-01-14 21:47:46 -07:00
lichess.pubsub.on('analysis.change.trigger', this.onChange);
lichess.pubsub.on('analysis.chart.click', index => {
2018-01-16 19:22:19 -07:00
this.jumpToIndex(index);
2021-02-06 06:26:05 -07:00
this.redraw();
2018-01-16 19:22:19 -07:00
});
2020-09-26 09:57:38 -06:00
speech.setup();
}
2017-06-21 05:39:50 -06:00
2017-06-24 05:10:09 -06:00
initialize(data: AnalyseData, merge: boolean): void {
2016-02-24 22:41:27 -07:00
this.data = data;
2019-04-08 21:53:14 -06:00
this.synthetic = data.game.id === 'synthetic';
this.ongoing = !this.synthetic && game.playable(data);
2017-07-01 09:33:27 -06:00
const prevTree = merge && this.tree.root;
2021-04-05 01:10:35 -06:00
this.tree = makeTree(util.treeReconstruct(this.data.treeParts));
if (prevTree) this.tree.merge(prevTree);
this.actionMenu = new ActionMenuCtrl();
2017-06-21 05:03:22 -06:00
this.autoplay = new Autoplay(this);
if (this.socket) this.socket.clearCache();
2017-06-24 05:10:09 -06:00
else this.socket = makeSocket(this.opts.socketSend, this);
if (this.explorer) this.explorer.destroy();
this.explorer = new ExplorerCtrl(this, this.opts.explorer, this.explorer ? this.explorer.allowed() : !this.embed);
2021-02-06 06:26:05 -07:00
this.gamePath =
this.synthetic || this.ongoing ? undefined : treePath.fromNodeList(treeOps.mainlineNodeList(this.tree.root));
this.fork = makeFork(this);
2021-05-18 02:12:10 -06:00
lichess.sound.preloadBoardSounds();
2016-03-06 04:32:14 -07:00
}
enableWiki = (v: boolean) => {
this.wiki = v ? wikiTheory() : undefined;
if (this.wiki) this.wiki(this.nodeList);
};
2019-01-26 04:21:13 -07:00
private setPath = (path: Tree.Path): void => {
this.path = path;
this.nodeList = this.tree.getNodeList(path);
2017-06-24 06:20:20 -06:00
this.node = treeOps.last(this.nodeList) as Tree.Node;
this.mainline = treeOps.mainlineNodeList(this.tree.root);
2021-02-06 06:26:05 -07:00
this.onMainline = this.tree.pathIsMainline(path);
2020-06-18 13:43:23 -06:00
this.fenInput = undefined;
this.pgnInput = undefined;
2021-10-06 05:06:23 -06:00
if (this.wiki) this.wiki(this.nodeList);
2021-02-06 06:26:05 -07:00
};
2016-03-05 18:23:49 -07:00
2017-06-30 05:04:22 -06:00
flip = () => {
2017-06-22 08:40:51 -06:00
this.flipped = !this.flipped;
2014-12-23 09:58:15 -07:00
this.chessground.set({
2021-02-06 06:26:05 -07:00
orientation: this.bottomColor(),
2014-12-23 09:58:15 -07:00
});
if (this.retro && this.data.game.variant.key !== 'racingKings') {
this.retro = makeRetro(this, this.bottomColor());
}
2017-01-18 09:49:46 -07:00
if (this.practice) this.restartPractice();
this.explorer.onFlip();
2017-06-24 06:20:20 -06:00
this.redraw();
2021-02-06 06:26:05 -07:00
};
2016-10-21 05:50:40 -06:00
2017-06-22 08:40:51 -06:00
topColor(): Color {
2016-10-21 05:50:40 -06:00
return opposite(this.bottomColor());
2017-06-22 08:40:51 -06:00
}
bottomColor(): Color {
if (this.data.game.variant.key === 'racingKings') return this.flipped ? 'black' : 'white';
2017-06-22 08:40:51 -06:00
return this.flipped ? opposite(this.data.orientation) : this.data.orientation;
}
2014-12-23 09:58:15 -07:00
2017-10-10 12:10:29 -06:00
bottomIsWhite = () => this.bottomColor() === 'white';
2021-02-06 06:26:05 -07:00
getOrientation(): Color {
// required by ui/ceval
2017-06-22 08:40:51 -06:00
return this.bottomColor();
}
2021-02-06 06:26:05 -07:00
getNode(): Tree.Node {
// required by ui/ceval
return this.node;
}
2017-06-22 08:40:51 -06:00
turnColor(): Color {
2017-08-15 09:13:46 -06:00
return util.plyColor(this.node.ply);
2017-06-22 08:40:51 -06:00
}
2017-01-22 03:52:18 -07:00
2017-06-22 08:40:51 -06:00
togglePlay(delay: AutoplayDelay): void {
this.autoplay.toggle(delay);
this.actionMenu.open = false;
}
2015-01-29 08:05:10 -07:00
2017-12-30 06:18:24 -07:00
private uciToLastMove(uci?: Uci): Key[] | undefined {
2017-06-22 08:40:51 -06:00
if (!uci) return;
2017-06-24 06:20:20 -06:00
if (uci[1] === '@') return [uci.substr(2, 2), uci.substr(2, 2)] as Key[];
return [uci.substr(0, 2), uci.substr(2, 2)] as Key[];
2021-02-06 06:26:05 -07:00
}
2016-01-18 23:32:00 -07:00
2017-06-23 09:22:21 -06:00
private showGround(): void {
2017-06-24 06:20:20 -06:00
this.onChange();
if (!defined(this.node.dests)) this.getDests();
this.withCg(cg => {
cg.set(this.makeCgOpts());
this.setAutoShapes();
if (this.node.shapes) cg.setShapes(this.node.shapes as DrawShape[]);
});
2017-06-23 09:22:21 -06:00
}
getDests: () => void = throttle(800, () => {
2021-02-06 06:26:05 -07:00
if (!this.embed && !defined(this.node.dests))
this.socket.sendAnaDests({
variant: this.data.game.variant.key,
fen: this.node.fen,
path: this.path,
});
2017-06-23 09:22:21 -06:00
});
makeCgOpts(): ChessgroundConfig {
2017-08-16 09:44:22 -06:00
const node = this.node,
2019-01-14 20:31:01 -07:00
color = this.turnColor(),
dests = chessUtil.readDests(this.node.dests),
drops = chessUtil.readDrops(this.node.drops),
gamebookPlay = this.gamebookPlay(),
movableColor = gamebookPlay
? gamebookPlay.movableColor()
2021-02-06 06:26:05 -07:00
: this.practice
? this.bottomColor()
: !this.embed && ((dests && dests.size > 0) || drops === null || drops.length)
? color
: undefined,
2019-01-14 20:31:01 -07:00
config: ChessgroundConfig = {
fen: node.fen,
turnColor: color,
2021-02-06 06:26:05 -07:00
movable: this.embed
? {
color: undefined,
dests: new Map(),
}
: {
color: movableColor,
dests: (movableColor === color && dests) || new Map(),
},
2019-01-14 20:31:01 -07:00
check: !!node.check,
2021-02-06 06:26:05 -07:00
lastMove: this.uciToLastMove(node.uci),
2019-01-14 20:31:01 -07:00
};
2016-03-05 18:23:49 -07:00
if (!dests && !node.check) {
2015-08-25 07:56:55 -06:00
// premove while dests are loading from server
2015-09-02 18:07:49 -06:00
// can't use when in check because it highlights the wrong king
2015-08-25 07:56:55 -06:00
config.turnColor = opposite(color);
2017-06-24 06:20:20 -06:00
config.movable!.color = color;
2015-08-25 07:56:55 -06:00
}
2017-01-19 06:28:21 -07:00
config.premovable = {
2021-02-06 06:26:05 -07:00
enabled: config.movable!.color && config.turnColor !== config.movable!.color,
2017-01-19 06:28:21 -07:00
};
2017-06-23 09:22:21 -06:00
this.cgConfig = config;
return config;
2017-06-23 09:22:21 -06:00
}
2020-09-26 09:57:38 -06:00
private throttleSound = (name: string) => throttle(100, () => lichess.sound.play(name));
private sound = {
move: this.throttleSound('move'),
capture: this.throttleSound('capture'),
2021-02-06 06:26:05 -07:00
check: this.throttleSound('check'),
2020-09-26 09:57:38 -06:00
};
private onChange: () => void = throttle(300, () => {
lichess.pubsub.emit('analysis.change', this.node.fen, this.path, this.onMainline ? this.node.ply : false);
2017-06-30 05:04:22 -06:00
});
2015-10-10 11:38:04 -06:00
2020-09-05 00:27:09 -06:00
private updateHref: () => void = debounce(() => {
2017-06-30 05:04:22 -06:00
if (!this.opts.study) window.history.replaceState(null, '', '#' + this.node.ply);
}, 750);
2017-06-23 09:22:21 -06:00
autoScroll(): void {
this.autoScrollRequested = true;
}
2016-02-26 17:55:43 -07:00
2021-02-06 06:26:05 -07:00
playedLastMoveMyself = () => !!this.justPlayed && !!this.node.uci && this.node.uci.startsWith(this.justPlayed);
2017-06-23 09:22:21 -06:00
jump(path: Tree.Path): void {
const pathChanged = path !== this.path,
2020-09-06 06:38:32 -06:00
isForwardStep = pathChanged && path.length == this.path.length + 2;
2016-04-19 23:33:44 -06:00
this.setPath(path);
if (pathChanged) {
2021-09-10 01:55:30 -06:00
if (this.study) this.study.setPath(path, this.node);
if (isForwardStep) {
2021-02-06 06:26:05 -07:00
if (!this.node.uci) this.sound.move();
// initial position
2021-09-10 01:55:30 -06:00
else if (!this.playedLastMoveMyself()) {
2019-02-28 03:27:57 -07:00
if (this.node.san!.includes('x')) this.sound.capture();
else this.sound.move();
}
2021-04-07 12:47:56 -06:00
if (/\+|#/.test(this.node.san!)) this.sound.check();
}
this.threatMode(false);
this.ceval.stop();
this.startCeval();
speech.node(this.node);
}
2017-08-16 09:44:22 -06:00
this.justPlayed = this.justDropped = this.justCaptured = undefined;
2016-03-05 18:23:49 -07:00
this.explorer.setNode();
2017-06-24 06:20:20 -06:00
this.updateHref();
2016-02-26 17:55:43 -07:00
this.autoScroll();
this.promotion.cancel();
2017-01-19 08:14:46 -07:00
if (pathChanged) {
if (this.retro) this.retro.onJump();
if (this.practice) this.practice.onJump();
2017-01-22 03:52:18 -07:00
if (this.study) this.study.onJump();
2017-01-19 08:14:46 -07:00
}
2017-06-23 09:22:21 -06:00
if (this.music) this.music.jump(this.node);
lichess.pubsub.emit('ply', this.node.ply);
this.showGround();
2017-06-23 09:22:21 -06:00
}
2014-10-27 10:21:52 -06:00
2017-07-01 09:55:43 -06:00
userJump = (path: Tree.Path): void => {
2015-01-29 10:35:09 -07:00
this.autoplay.stop();
2021-11-10 23:35:49 -07:00
if (!this.gamebookPlay()) this.withCg(cg => cg.selectSquare(null));
2017-01-17 15:17:39 -07:00
if (this.practice) {
2017-06-23 09:22:21 -06:00
const prev = this.path;
this.practice.preUserJump(prev, path);
2017-01-17 15:17:39 -07:00
this.jump(path);
2017-06-23 09:22:21 -06:00
this.practice.postUserJump(prev, this.path);
2019-01-26 04:21:13 -07:00
} else this.jump(path);
2021-02-06 06:26:05 -07:00
};
2015-01-29 10:35:09 -07:00
2017-06-23 09:22:21 -06:00
private canJumpTo(path: Tree.Path): boolean {
return !this.study || this.study.canJumpTo(path);
2017-06-23 09:22:21 -06:00
}
2016-07-30 05:40:58 -06:00
2017-06-23 09:22:21 -06:00
userJumpIfCan(path: Tree.Path): void {
2017-06-24 06:20:20 -06:00
if (this.canJumpTo(path)) this.userJump(path);
2017-06-23 09:22:21 -06:00
}
2016-07-30 05:40:58 -06:00
2017-06-23 09:22:21 -06:00
mainlinePathToPly(ply: Ply): Tree.Path {
return treeOps.takePathWhile(this.mainline, n => n.ply <= ply);
}
2016-04-15 05:39:26 -06:00
2017-06-24 05:10:09 -06:00
jumpToMain = (ply: Ply): void => {
2016-04-27 21:42:53 -06:00
this.userJump(this.mainlinePathToPly(ply));
2021-02-06 06:26:05 -07:00
};
2018-01-16 19:22:19 -07:00
jumpToIndex = (index: number): void => {
this.jumpToMain(index + 1 + this.tree.root.ply);
2021-02-06 06:26:05 -07:00
};
2017-06-23 09:22:21 -06:00
jumpToGlyphSymbol(color: Color, symbol: string): void {
2017-06-24 05:10:09 -06:00
const node = nextGlyphSymbol(color, symbol, this.mainline, this.node.ply);
2016-12-17 06:10:32 -07:00
if (node) this.jumpToMain(node.ply);
2017-06-24 06:20:20 -06:00
this.redraw();
2017-06-23 09:22:21 -06:00
}
2015-09-24 18:53:24 -06:00
2017-06-23 15:30:21 -06:00
reloadData(data: AnalyseData, merge: boolean): void {
2017-06-24 06:20:20 -06:00
this.initialize(data, merge);
2017-06-23 09:22:21 -06:00
this.redirecting = false;
this.setPath(treePath.root);
2017-06-23 15:30:21 -06:00
this.instanciateCeval();
this.instanciateEvalCache();
this.cgVersion.js++;
2017-06-23 09:22:21 -06:00
}
2017-06-23 09:22:21 -06:00
changePgn(pgn: string): void {
this.redirecting = true;
2021-02-06 06:26:05 -07:00
xhr
.json('/analysis/pgn', {
method: 'post',
body: xhr.form({ pgn }),
})
.then(
(data: AnalyseData) => {
this.reloadData(data, false);
this.userJump(this.mainlinePathToPly(this.tree.lastPly()));
this.redraw();
},
error => {
console.log(error);
this.redirecting = false;
this.redraw();
}
);
2017-06-23 09:22:21 -06:00
}
2016-02-24 20:58:45 -07:00
2017-06-24 06:20:20 -06:00
changeFen(fen: Fen): void {
2017-06-23 09:22:21 -06:00
this.redirecting = true;
2021-02-06 06:26:05 -07:00
window.location.href =
'/analysis/' +
this.data.game.variant.key +
'/' +
encodeURIComponent(fen).replace(/%20/g, '_').replace(/%2F/g, '/');
2016-02-24 21:11:01 -07:00
}
2017-06-30 05:04:22 -06:00
userNewPiece = (piece: cg.Piece, pos: Key): void => {
2017-06-23 09:22:21 -06:00
if (crazyValid(this.chessground, this.node.drops, piece, pos)) {
2020-06-18 16:02:30 -06:00
this.justPlayed = roleToChar(piece.role).toUpperCase() + '@' + pos;
2017-06-23 09:22:21 -06:00
this.justDropped = piece.role;
2017-06-27 04:27:48 -06:00
this.justCaptured = undefined;
2017-06-24 06:20:20 -06:00
this.sound.move();
2017-06-23 09:22:21 -06:00
const drop = {
2016-01-18 23:32:00 -07:00
role: piece.role,
2017-07-02 09:38:04 -06:00
pos,
2016-01-18 23:32:00 -07:00
variant: this.data.game.variant.key,
2017-06-23 09:22:21 -06:00
fen: this.node.fen,
2021-02-06 06:26:05 -07:00
path: this.path,
2016-01-18 23:32:00 -07:00
};
this.socket.sendAnaDrop(drop);
2017-06-24 06:20:20 -06:00
this.preparePremoving();
this.redraw();
2017-06-23 09:22:21 -06:00
} else this.jump(this.path);
2021-02-06 06:26:05 -07:00
};
2016-01-18 23:32:00 -07:00
2017-06-30 05:04:22 -06:00
userMove = (orig: Key, dest: Key, capture?: JustCaptured): void => {
2017-06-23 09:22:21 -06:00
this.justPlayed = orig;
2017-06-24 06:20:20 -06:00
this.justDropped = undefined;
const piece = this.chessground.state.pieces.get(dest);
const isCapture = capture || (piece && piece.role == 'pawn' && orig[0] != dest[0]);
this.sound[isCapture ? 'capture' : 'move']();
if (!this.promotion.start(orig, dest, (orig, dest, prom) => this.sendMove(orig, dest, capture, prom))) {
this.sendMove(orig, dest, capture);
}
2021-02-06 06:26:05 -07:00
};
2017-06-27 04:27:48 -06:00
sendMove = (orig: Key, dest: Key, capture?: JustCaptured, prom?: cg.Role): void => {
2021-04-20 12:37:16 -06:00
const move: AnaMove = {
2017-07-02 09:38:04 -06:00
orig,
dest,
2015-05-06 11:40:49 -06:00
variant: this.data.game.variant.key,
2017-06-23 09:22:21 -06:00
fen: this.node.fen,
2021-02-06 06:26:05 -07:00
path: this.path,
2015-05-04 15:39:09 -06:00
};
2017-06-23 09:22:21 -06:00
if (capture) this.justCaptured = capture;
2015-05-04 15:39:09 -06:00
if (prom) move.promotion = prom;
if (this.practice) this.practice.onUserMove();
2015-05-06 11:40:49 -06:00
this.socket.sendAnaMove(move);
2017-06-24 06:20:20 -06:00
this.preparePremoving();
this.redraw();
2021-02-06 06:26:05 -07:00
};
2016-01-18 23:32:00 -07:00
2017-06-23 15:30:21 -06:00
private preparePremoving(): void {
this.chessground.set({
2017-06-24 06:20:20 -06:00
turnColor: this.chessground.state.movable.color as cg.Color,
movable: {
2021-02-06 06:26:05 -07:00
color: opposite(this.chessground.state.movable.color as cg.Color),
},
premovable: {
2021-02-06 06:26:05 -07:00
enabled: true,
},
});
2017-06-23 15:30:21 -06:00
}
2015-05-06 11:40:49 -06:00
2018-04-07 16:38:06 -06:00
onPremoveSet = () => {
if (this.study) this.study.onPremoveSet();
2021-02-06 06:26:05 -07:00
};
2018-04-07 16:38:06 -06:00
2017-07-02 05:59:36 -06:00
addNode(node: Tree.Node, path: Tree.Path) {
2017-06-23 15:30:21 -06:00
const newPath = this.tree.addNode(node, path);
if (!newPath) {
2018-03-16 13:12:21 -06:00
console.log("Can't addNode", node, path);
2017-06-24 06:20:20 -06:00
return this.redraw();
}
this.jump(newPath);
2017-06-24 06:20:20 -06:00
this.redraw();
const queuedUci = this.pvUciQueue.shift();
if (queuedUci) this.playUci(queuedUci, this.pvUciQueue);
else this.chessground.playPremove();
2017-06-23 15:30:21 -06:00
}
2015-05-06 11:40:49 -06:00
2020-03-22 15:27:01 -06:00
addDests(dests: string, path: Tree.Path): void {
this.tree.addDests(dests, path);
2017-06-23 09:22:21 -06:00
if (path === this.path) {
2017-06-24 06:20:20 -06:00
this.showGround();
2020-06-18 15:39:13 -06:00
if (this.outcome()) this.ceval.stop();
2015-09-27 03:29:09 -06:00
}
this.withCg(cg => cg.playPremove());
2017-06-23 15:30:21 -06:00
}
2017-06-23 15:30:21 -06:00
deleteNode(path: Tree.Path): void {
const node = this.tree.nodeAtPath(path);
2016-04-19 02:57:16 -06:00
if (!node) return;
2017-06-23 15:30:21 -06:00
const count = treeOps.countChildrenAndComments(node);
2021-02-06 06:26:05 -07:00
if (
(count.nodes >= 10 || count.comments > 0) &&
!confirm(
'Delete ' +
util.plural('move', count.nodes) +
(count.comments ? ' and ' + util.plural('comment', count.comments) : '') +
'?'
)
)
return;
2016-04-19 02:57:16 -06:00
this.tree.deleteNodeAt(path);
2017-06-23 09:22:21 -06:00
if (treePath.contains(this.path, path)) this.userJump(treePath.init(path));
else this.jump(this.path);
2017-06-23 15:30:21 -06:00
if (this.study) this.study.deleteNode(path);
}
2016-01-03 23:31:16 -07:00
2017-06-24 06:20:20 -06:00
promote(path: Tree.Path, toMainline: boolean): void {
this.tree.promoteAt(path, toMainline);
2016-12-15 17:30:13 -07:00
this.jump(path);
if (this.study) this.study.promote(path, toMainline);
2017-06-23 15:30:21 -06:00
}
forceVariation(path: Tree.Path, force: boolean): void {
this.tree.forceVariationAt(path, force);
this.jump(path);
if (this.study) this.study.forceVariation(path, force);
}
2017-06-23 15:30:21 -06:00
reset(): void {
2017-06-24 06:20:20 -06:00
this.showGround();
this.redraw();
2017-06-23 15:30:21 -06:00
}
2015-05-04 15:39:09 -06:00
2017-06-23 15:30:21 -06:00
encodeNodeFen(): Fen {
2017-06-23 09:22:21 -06:00
return this.node.fen.replace(/\s/g, '_');
2017-06-23 15:30:21 -06:00
}
2015-09-04 07:53:25 -06:00
2017-06-23 15:30:21 -06:00
currentEvals() {
2017-01-28 14:17:42 -07:00
return {
2017-06-23 15:30:21 -06:00
server: this.node.eval,
2021-02-06 06:26:05 -07:00
client: this.node.ceval,
2017-01-28 14:17:42 -07:00
};
2017-06-23 15:30:21 -06:00
}
2015-09-15 12:32:57 -06:00
2017-06-23 15:30:21 -06:00
nextNodeBest() {
return treeOps.withMainlineChild(this.node, (n: Tree.Node) => n.eval?.best);
2017-06-23 15:30:21 -06:00
}
2017-06-27 04:27:48 -06:00
setAutoShapes = (): void => {
this.withCg(cg => cg.setAutoShapes(computeAutoShapes(this)));
2021-02-06 06:26:05 -07:00
};
2020-08-12 03:19:35 -06:00
private onNewCeval = (ev: Tree.ClientEval, path: Tree.Path, isThreat?: boolean): void => {
2017-06-27 04:27:48 -06:00
this.tree.updateAt(path, (node: Tree.Node) => {
if (node.fen !== ev.fen && !isThreat) return;
if (isThreat) {
2021-09-04 12:28:15 -06:00
const threat = ev as Tree.LocalEval;
if (!node.threat || isEvalBetter(threat, node.threat) || node.threat.maxDepth < threat.maxDepth)
node.threat = threat;
} else if (!node.ceval || isEvalBetter(ev, node.ceval)) node.ceval = ev;
else if (!ev.cloud) {
if (node.ceval.cloud && this.ceval.isDeeper()) node.ceval = ev;
2021-09-04 12:28:15 -06:00
else if (ev.maxDepth > node.ceval.maxDepth!) node.ceval.maxDepth = ev.maxDepth;
}
2017-02-03 05:38:22 -07:00
2017-06-23 09:22:21 -06:00
if (path === this.path) {
2017-02-02 10:02:41 -07:00
this.setAutoShapes();
if (!isThreat) {
2017-02-02 10:02:41 -07:00
if (this.retro) this.retro.onCeval();
if (this.practice) this.practice.onCeval();
if (this.studyPractice) this.studyPractice.onCeval();
this.evalCache.onCeval();
2017-06-21 05:03:22 -06:00
if (ev.cloud && ev.depth >= this.ceval.effectiveMaxDepth()) this.ceval.stop();
2017-02-02 10:02:41 -07:00
}
2017-06-26 05:02:52 -06:00
this.redraw();
2017-02-02 10:02:41 -07:00
}
2017-06-27 04:27:48 -06:00
});
2021-02-06 06:26:05 -07:00
};
2017-02-02 10:02:41 -07:00
private instanciateCeval(): void {
2016-12-24 05:01:06 -07:00
if (this.ceval) this.ceval.destroy();
this.ceval = cevalCtrl({
2016-10-14 03:32:35 -06:00
variant: this.data.game.variant,
initialFen: this.data.game.initialFen,
2021-02-06 06:26:05 -07:00
possible: !this.embed && (this.synthetic || !game.playable(this.data)),
emit: (ev: Tree.ClientEval, work: EvalMeta) => {
2017-06-23 15:30:21 -06:00
this.onNewCeval(ev, work.path, work.threatMode);
2017-06-27 04:27:48 -06:00
},
2016-10-14 03:32:35 -06:00
setAutoShapes: this.setAutoShapes,
2021-02-06 06:26:05 -07:00
redraw: this.redraw,
...(this.opts.study && this.opts.practice
? {
storageKeyPrefix: 'practice',
multiPvDefault: 1,
}
: {}),
});
2017-06-23 15:30:21 -06:00
}
2015-09-22 04:49:31 -06:00
2017-06-23 15:30:21 -06:00
getCeval() {
return this.ceval;
2017-06-23 15:30:21 -06:00
}
2020-06-18 15:39:13 -06:00
outcome(node?: Tree.Node): Outcome | undefined {
2021-02-06 06:26:05 -07:00
return this.position(node || this.node).unwrap(
pos => pos.outcome(),
_ => undefined
);
2017-06-23 15:30:21 -06:00
}
2016-08-16 08:54:38 -06:00
2020-01-02 08:25:05 -07:00
position(node: Tree.Node): Result<Position, PositionError> {
const setup = parseFen(node.fen).unwrap();
return setupPosition(lichessRules(this.data.game.variant.key), setup);
2020-01-02 08:25:05 -07:00
}
2017-06-23 15:30:21 -06:00
canUseCeval(): boolean {
2020-06-18 15:39:13 -06:00
return !this.node.threefold && !this.outcome();
2017-06-23 15:30:21 -06:00
}
2015-09-22 15:31:34 -06:00
startCeval = throttle(800, () => {
if (this.ceval.enabled()) {
2017-06-26 05:02:52 -06:00
if (this.canUseCeval()) {
this.ceval.start(this.path, this.nodeList, this.threatMode());
2017-06-23 09:22:21 -06:00
this.evalCache.fetch(this.path, parseInt(this.ceval.multiPv()));
} else this.ceval.stop();
}
2017-06-27 04:27:48 -06:00
});
2015-09-22 06:50:50 -06:00
2017-06-30 06:07:26 -06:00
toggleCeval = () => {
if (!this.showComputer()) return;
2015-09-22 06:50:50 -06:00
this.ceval.toggle();
this.setAutoShapes();
this.startCeval();
2017-01-18 05:18:12 -07:00
if (!this.ceval.enabled()) {
this.threatMode(false);
2017-01-18 08:45:54 -07:00
if (this.practice) this.togglePractice();
2017-01-18 05:18:12 -07:00
}
2017-06-26 05:02:52 -06:00
this.redraw();
2021-02-06 06:26:05 -07:00
};
2016-09-26 13:45:46 -06:00
2017-06-30 06:07:26 -06:00
toggleThreatMode = () => {
2017-06-23 09:22:21 -06:00
if (this.node.check) return;
if (!this.ceval.enabled()) this.ceval.toggle();
if (!this.ceval.enabled()) return;
this.threatMode(!this.threatMode());
if (this.threatMode() && this.practice) this.togglePractice();
2016-09-26 13:45:46 -06:00
this.setAutoShapes();
this.startCeval();
2017-06-26 05:02:52 -06:00
this.redraw();
2021-02-06 06:26:05 -07:00
};
2015-09-21 11:43:16 -06:00
2017-06-30 06:07:26 -06:00
disableThreatMode = (): boolean => {
2017-01-19 03:14:24 -07:00
return !!this.practice;
2021-02-06 06:26:05 -07:00
};
2017-01-19 03:14:24 -07:00
2017-06-30 06:07:26 -06:00
mandatoryCeval = (): boolean => {
return !!this.studyPractice;
2021-02-06 06:26:05 -07:00
};
2017-06-23 15:30:21 -06:00
private cevalReset(): void {
2016-10-04 04:45:16 -06:00
this.ceval.stop();
2016-10-04 05:05:41 -06:00
if (!this.ceval.enabled()) this.ceval.toggle();
this.startCeval();
2017-06-26 05:02:52 -06:00
this.redraw();
2017-06-23 15:30:21 -06:00
}
2016-10-04 04:45:16 -06:00
2017-06-30 06:07:26 -06:00
cevalSetMultiPv = (v: number): void => {
2016-10-04 05:05:41 -06:00
this.ceval.multiPv(v);
this.tree.removeCeval();
this.evalCache.clear();
2017-06-26 05:02:52 -06:00
this.cevalReset();
2021-02-06 06:26:05 -07:00
};
2016-10-04 05:05:41 -06:00
2017-06-30 06:07:26 -06:00
cevalSetThreads = (v: number): void => {
if (!this.ceval.threads) return;
2016-10-04 05:05:41 -06:00
this.ceval.threads(v);
2017-06-26 05:02:52 -06:00
this.cevalReset();
2021-02-06 06:26:05 -07:00
};
2016-10-04 05:05:41 -06:00
2017-06-30 06:07:26 -06:00
cevalSetHashSize = (v: number): void => {
if (!this.ceval.hashSize) return;
2016-10-04 05:12:26 -06:00
this.ceval.hashSize(v);
2017-06-26 05:02:52 -06:00
this.cevalReset();
2021-02-06 06:26:05 -07:00
};
2016-10-04 05:12:26 -06:00
2017-06-30 06:07:26 -06:00
cevalSetInfinite = (v: boolean): void => {
this.ceval.infinite(v);
2017-06-26 05:02:52 -06:00
this.cevalReset();
2021-02-06 06:26:05 -07:00
};
2017-06-23 15:30:21 -06:00
showEvalGauge(): boolean {
2020-06-18 15:39:13 -06:00
return this.hasAnyComputerAnalysis() && this.showGauge() && !this.outcome() && this.showComputer();
2017-06-23 15:30:21 -06:00
}
2015-09-24 19:14:46 -06:00
2017-06-23 15:30:21 -06:00
hasAnyComputerAnalysis(): boolean {
2017-06-26 05:02:52 -06:00
return this.data.analysis ? true : this.ceval.enabled();
2017-06-23 15:30:21 -06:00
}
2017-06-30 11:27:04 -06:00
hasFullComputerAnalysis = (): boolean => {
return Object.keys(this.mainline[0].eval || {}).length > 0;
2021-02-06 06:26:05 -07:00
};
2017-06-23 15:30:21 -06:00
private resetAutoShapes() {
if (this.showAutoShapes() || this.showMoveAnnotation()) this.setAutoShapes();
else this.chessground && this.chessground.setAutoShapes([]);
2017-06-23 15:30:21 -06:00
}
2017-08-31 08:47:36 -06:00
toggleAutoShapes = (v: boolean): void => {
2017-06-23 09:22:21 -06:00
this.showAutoShapes(v);
2017-06-26 05:02:52 -06:00
this.resetAutoShapes();
2021-02-06 06:26:05 -07:00
};
2017-10-24 06:33:55 -06:00
toggleGauge = () => {
2017-06-23 09:22:21 -06:00
this.showGauge(!this.showGauge());
2021-02-06 06:26:05 -07:00
};
2015-09-24 11:04:16 -06:00
toggleMoveAnnotation = (v: boolean): void => {
this.showMoveAnnotation(v);
this.resetAutoShapes();
};
2017-06-23 15:30:21 -06:00
private onToggleComputer() {
2017-06-23 09:22:21 -06:00
if (!this.showComputer()) {
2016-10-19 13:50:32 -06:00
this.tree.removeComputerVariations();
if (this.ceval.enabled()) this.toggleCeval();
this.chessground && this.chessground.setAutoShapes([]);
2017-06-26 05:02:52 -06:00
} else this.resetAutoShapes();
2017-06-23 15:30:21 -06:00
}
2017-06-30 06:07:26 -06:00
toggleComputer = () => {
if (this.ceval.enabled()) this.toggleCeval();
2017-06-30 06:07:26 -06:00
const value = !this.showComputer();
2017-06-23 09:22:21 -06:00
this.showComputer(value);
2017-01-18 08:45:54 -07:00
if (!value && this.practice) this.togglePractice();
2017-06-26 05:02:52 -06:00
this.onToggleComputer();
lichess.pubsub.emit('analysis.comp.toggle', value);
2021-02-06 06:26:05 -07:00
};
2018-01-21 14:34:27 -07:00
mergeAnalysisData(data: ServerEvalData): void {
if (this.study && this.study.data.chapter.id !== data.ch) return;
this.tree.merge(data.tree);
2017-06-23 09:22:21 -06:00
if (!this.showComputer()) this.tree.removeComputerVariations();
this.data.analysis = data.analysis;
if (data.analysis)
data.analysis.partial = !!treeOps.findInMainline(data.tree, n => !n.eval && !!n.children.length && n.ply <= 200);
2018-01-21 14:34:27 -07:00
if (data.division) this.data.game.division = data.division;
if (this.retro) this.retro.onMergeAnalysisData();
2018-01-16 16:33:30 -07:00
if (this.study) this.study.serverEval.onMergeAnalysisData();
lichess.pubsub.emit('analysis.server.progress', this.data);
2017-06-26 05:02:52 -06:00
this.redraw();
2017-06-23 15:30:21 -06:00
}
playUci(uci: Uci, uciQueue?: Uci[]): void {
this.pvUciQueue = uciQueue ?? [];
2020-06-18 14:30:23 -06:00
const move = parseUci(uci)!;
const to = makeSquare(move.to);
if (isNormal(move)) {
const piece = this.chessground.state.pieces.get(makeSquare(move.from));
const capture = this.chessground.state.pieces.get(to);
2021-02-06 06:26:05 -07:00
this.sendMove(
makeSquare(move.from),
to,
capture && piece && capture.color !== piece.color ? capture : undefined,
move.promotion
);
} else
this.chessground.newPiece(
{
color: this.chessground.state.movable.color as Color,
role: move.role,
},
to
);
2017-06-23 15:30:21 -06:00
}
playUciList(uciList: Uci[]): void {
this.pvUciQueue = uciList;
const firstUci = this.pvUciQueue.shift();
if (firstUci) this.playUci(firstUci, this.pvUciQueue);
}
2017-06-23 15:30:21 -06:00
explorerMove(uci: Uci) {
2016-10-06 14:07:08 -06:00
this.playUci(uci);
2016-02-06 20:39:39 -07:00
this.explorer.loading(true);
2017-06-23 15:30:21 -06:00
}
2016-02-06 00:19:40 -07:00
2017-06-23 15:30:21 -06:00
playBestMove() {
const uci = this.node.ceval?.pvs[0].moves[0] || this.nextNodeBest();
2016-10-06 14:07:08 -06:00
if (uci) this.playUci(uci);
2017-06-23 15:30:21 -06:00
}
2014-10-27 10:21:52 -06:00
canEvalGet(): boolean {
if (this.node.ply >= 15 && !this.opts.study) return false;
// cloud eval does not support threefold repetition
const fens = new Set();
for (let i = this.nodeList.length - 1; i >= 0; i--) {
const node = this.nodeList[i];
const fen = node.fen.split(' ').slice(0, 4).join(' ');
if (fens.has(fen)) return false;
if (node.san && sanIrreversible(this.data.game.variant.key, node.san)) return true;
fens.add(fen);
}
return true;
}
2017-02-01 05:33:08 -07:00
2017-06-23 15:30:21 -06:00
instanciateEvalCache() {
this.evalCache = makeEvalCache({
2017-04-20 13:52:30 -06:00
variant: this.data.game.variant.key,
canGet: () => this.canEvalGet(),
2021-02-06 06:26:05 -07:00
canPut: () =>
2021-04-20 12:37:16 -06:00
!!(
this.data.evalPut &&
this.canEvalGet() &&
// if not in study, only put decent opening moves
(this.opts.study || (!this.node.ceval!.mate && Math.abs(this.node.ceval!.cp!) < 99))
),
2017-06-27 04:27:48 -06:00
getNode: () => this.node,
send: this.opts.socketSend,
2021-02-06 06:26:05 -07:00
receive: this.onNewCeval,
});
2017-06-23 15:30:21 -06:00
}
2016-12-16 08:20:37 -07:00
2017-06-30 07:16:23 -06:00
toggleRetro = (): void => {
2017-06-27 04:27:48 -06:00
if (this.retro) this.retro = undefined;
2016-12-19 16:59:09 -07:00
else {
this.retro = makeRetro(this, this.bottomColor());
2017-01-17 09:53:27 -07:00
if (this.practice) this.togglePractice();
if (this.explorer.enabled()) this.toggleExplorer();
2016-12-19 16:59:09 -07:00
}
2016-12-18 09:29:42 -07:00
this.setAutoShapes();
2021-02-06 06:26:05 -07:00
};
2016-05-27 12:53:59 -06:00
2017-07-05 01:39:50 -06:00
toggleExplorer = (): void => {
2017-01-18 05:23:18 -07:00
if (this.practice) this.togglePractice();
if (this.explorer.enabled() || this.explorer.allowed()) this.explorer.toggle();
2021-02-06 06:26:05 -07:00
};
2017-01-17 09:53:27 -07:00
2017-06-30 11:27:04 -06:00
togglePractice = () => {
if (this.practice || !this.ceval.possible) {
this.practice = undefined;
this.showGround();
} else {
2017-01-17 15:17:39 -07:00
if (this.retro) this.toggleRetro();
2017-01-17 09:53:27 -07:00
if (this.explorer.enabled()) this.toggleExplorer();
2017-06-27 04:27:48 -06:00
this.practice = makePractice(this, () => {
// push to 20 to store AI moves in the cloud
// lower to 18 after task completion (or failure)
return this.studyPractice && this.studyPractice.success() === null ? 20 : 18;
2017-06-27 04:27:48 -06:00
});
this.setAutoShapes();
2017-01-17 09:53:27 -07:00
}
2017-06-24 05:10:09 -06:00
};
2017-01-17 09:53:27 -07:00
2017-06-24 05:10:09 -06:00
restartPractice() {
2017-06-27 04:27:48 -06:00
this.practice = undefined;
2017-01-18 09:49:46 -07:00
this.togglePractice();
2017-06-24 05:10:09 -06:00
}
2017-08-18 14:32:08 -06:00
gamebookPlay = (): GamebookPlayCtrl | undefined => {
return this.study && this.study.gamebookPlay();
2021-02-06 06:26:05 -07:00
};
2017-08-18 08:29:55 -06:00
2017-08-22 08:22:51 -06:00
isGamebook = (): boolean => !!(this.study && this.study.data.chapter.gamebook);
withCg = <A>(f: (cg: ChessgroundApi) => A): A | undefined => {
2021-02-06 06:26:05 -07:00
if (this.chessground && this.cgVersion.js === this.cgVersion.dom) return f(this.chessground);
2020-07-01 21:48:43 -06:00
return undefined;
};
2021-02-06 06:26:05 -07:00
}