496 lines
17 KiB
TypeScript
496 lines
17 KiB
TypeScript
import { view as cevalView } from 'ceval';
|
|
import { read as readFen } from 'chessground/fen';
|
|
import { parseFen } from 'chessops/fen';
|
|
import { defined } from 'common';
|
|
import changeColorHandle from 'common/coordsColor';
|
|
import { bind, bindNonPassive, MaybeVNodes, onInsert } from 'common/snabbdom';
|
|
import { getPlayer, playable } from 'game';
|
|
import * as router from 'game/router';
|
|
import * as materialView from 'game/view/material';
|
|
import statusView from 'game/view/status';
|
|
import { h, VNode } from 'snabbdom';
|
|
import { path as treePath } from 'tree';
|
|
import { render as acplView } from './acpl';
|
|
import { view as actionMenu } from './actionMenu';
|
|
import renderClocks from './clocks';
|
|
import * as control from './control';
|
|
import crazyView from './crazy/crazyView';
|
|
import AnalyseCtrl from './ctrl';
|
|
import explorerView from './explorer/explorerView';
|
|
import forecastView from './forecast/forecastView';
|
|
import { view as forkView } from './fork';
|
|
import * as gridHacks from './gridHacks';
|
|
import * as chessground from './ground';
|
|
import { ConcealOf } from './interfaces';
|
|
import { view as keyboardView } from './keyboard';
|
|
import * as pgnExport from './pgnExport';
|
|
import practiceView from './practice/practiceView';
|
|
import retroView from './retrospect/retroView';
|
|
import serverSideUnderboard from './serverSideUnderboard';
|
|
import * as gbEdit from './study/gamebook/gamebookEdit';
|
|
import * as gbPlay from './study/gamebook/gamebookPlayView';
|
|
import { StudyCtrl } from './study/interfaces';
|
|
import renderPlayerBars from './study/playerBars';
|
|
import * as studyPracticeView from './study/practice/studyPracticeView';
|
|
import relayManager from './study/relay/relayManagerView';
|
|
import relayTour from './study/relay/relayTourView';
|
|
import { findTag } from './study/studyChapters';
|
|
import * as studyView from './study/studyView';
|
|
import { render as renderTreeView } from './treeView/treeView';
|
|
import { bindMobileMousedown, dataIcon, spinner } from './util';
|
|
|
|
function renderResult(ctrl: AnalyseCtrl): VNode[] {
|
|
const render = (result: string, status: MaybeVNodes) => [h('div.result', result), h('div.status', status)];
|
|
if (ctrl.data.game.status.id >= 30) {
|
|
let result;
|
|
switch (ctrl.data.game.winner) {
|
|
case 'white':
|
|
result = '1-0';
|
|
break;
|
|
case 'black':
|
|
result = '0-1';
|
|
break;
|
|
default:
|
|
result = '½-½';
|
|
}
|
|
const winner = getPlayer(ctrl.data, ctrl.data.game.winner!);
|
|
return render(result, [
|
|
statusView(ctrl),
|
|
winner ? ', ' + ctrl.trans(winner.color == 'white' ? 'whiteIsVictorious' : 'blackIsVictorious') : null,
|
|
]);
|
|
} else if (ctrl.study) {
|
|
const result = findTag(ctrl.study.data.chapter.tags, 'result');
|
|
if (!result || result === '*') return [];
|
|
if (result === '1-0') return render(result, [ctrl.trans.noarg('whiteIsVictorious')]);
|
|
if (result === '0-1') return render(result, [ctrl.trans.noarg('blackIsVictorious')]);
|
|
return render('½-½', [ctrl.trans.noarg('draw')]);
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function makeConcealOf(ctrl: AnalyseCtrl): ConcealOf | undefined {
|
|
const conceal =
|
|
ctrl.study && ctrl.study.data.chapter.conceal !== undefined
|
|
? {
|
|
owner: ctrl.study.isChapterOwner(),
|
|
ply: ctrl.study.data.chapter.conceal,
|
|
}
|
|
: null;
|
|
if (conceal)
|
|
return (isMainline: boolean) => (path: Tree.Path, node: Tree.Node) => {
|
|
if (!conceal || (isMainline && conceal.ply >= node.ply)) return null;
|
|
if (treePath.contains(ctrl.path, path)) return null;
|
|
return conceal.owner ? 'conceal' : 'hide';
|
|
};
|
|
return undefined;
|
|
}
|
|
|
|
function renderAnalyse(ctrl: AnalyseCtrl, concealOf?: ConcealOf) {
|
|
return h(
|
|
'div.analyse__moves.areplay',
|
|
[
|
|
ctrl.embed && ctrl.study ? h('div.chapter-name', ctrl.study.currentChapter().name) : null,
|
|
renderTreeView(ctrl, concealOf),
|
|
].concat(renderResult(ctrl))
|
|
);
|
|
}
|
|
|
|
function wheel(ctrl: AnalyseCtrl, e: WheelEvent) {
|
|
if (ctrl.gamebookPlay()) return;
|
|
const target = e.target as HTMLElement;
|
|
if (target.tagName !== 'PIECE' && target.tagName !== 'SQUARE' && target.tagName !== 'CG-BOARD') return;
|
|
e.preventDefault();
|
|
if (e.deltaY > 0) control.next(ctrl);
|
|
else if (e.deltaY < 0) control.prev(ctrl);
|
|
ctrl.redraw();
|
|
return false;
|
|
}
|
|
|
|
function inputs(ctrl: AnalyseCtrl): VNode | undefined {
|
|
if (ctrl.ongoing || !ctrl.data.userAnalysis) return;
|
|
if (ctrl.redirecting) return spinner();
|
|
return h('div.copyables', [
|
|
h('div.pair', [
|
|
h('label.name', 'FEN'),
|
|
h('input.copyable.autoselect.analyse__underboard__fen', {
|
|
attrs: { spellCheck: false },
|
|
hook: {
|
|
insert: vnode => {
|
|
const el = vnode.elm as HTMLInputElement;
|
|
el.value = defined(ctrl.fenInput) ? ctrl.fenInput : ctrl.node.fen;
|
|
el.addEventListener('change', _ => {
|
|
if (el.value !== ctrl.node.fen && el.reportValidity()) ctrl.changeFen(el.value.trim());
|
|
});
|
|
el.addEventListener('input', _ => {
|
|
ctrl.fenInput = el.value;
|
|
el.setCustomValidity(parseFen(el.value.trim()).isOk ? '' : 'Invalid FEN');
|
|
});
|
|
},
|
|
postpatch: (_, vnode) => {
|
|
const el = vnode.elm as HTMLInputElement;
|
|
if (!defined(ctrl.fenInput)) {
|
|
el.value = ctrl.node.fen;
|
|
el.setCustomValidity('');
|
|
} else if (el.value != ctrl.fenInput) el.value = ctrl.fenInput;
|
|
},
|
|
},
|
|
}),
|
|
]),
|
|
h('div.pgn', [
|
|
h('div.pair', [
|
|
h('label.name', 'PGN'),
|
|
h('textarea.copyable.autoselect', {
|
|
attrs: { spellCheck: false },
|
|
hook: {
|
|
...onInsert(el => {
|
|
(el as HTMLTextAreaElement).value = defined(ctrl.pgnInput)
|
|
? ctrl.pgnInput
|
|
: pgnExport.renderFullTxt(ctrl);
|
|
el.addEventListener('input', e => (ctrl.pgnInput = (e.target as HTMLTextAreaElement).value));
|
|
}),
|
|
postpatch: (_, vnode) => {
|
|
(vnode.elm as HTMLTextAreaElement).value = defined(ctrl.pgnInput)
|
|
? ctrl.pgnInput
|
|
: pgnExport.renderFullTxt(ctrl);
|
|
},
|
|
},
|
|
}),
|
|
h(
|
|
'button.button.button-thin.action.text',
|
|
{
|
|
attrs: dataIcon(''),
|
|
hook: bind(
|
|
'click',
|
|
_ => {
|
|
const pgn = $('.copyables .pgn textarea').val() as string;
|
|
if (pgn !== pgnExport.renderFullTxt(ctrl)) ctrl.changePgn(pgn);
|
|
},
|
|
ctrl.redraw
|
|
),
|
|
},
|
|
ctrl.trans.noarg('importPgn')
|
|
),
|
|
]),
|
|
]),
|
|
]);
|
|
}
|
|
|
|
function jumpButton(icon: string, effect: string, enabled: boolean): VNode {
|
|
return h('button.fbt', {
|
|
class: { disabled: !enabled },
|
|
attrs: { 'data-act': effect, 'data-icon': icon },
|
|
});
|
|
}
|
|
|
|
function dataAct(e: Event): string | null {
|
|
const target = e.target as HTMLElement;
|
|
return target.getAttribute('data-act') || (target.parentNode as HTMLElement).getAttribute('data-act');
|
|
}
|
|
|
|
function repeater(ctrl: AnalyseCtrl, action: 'prev' | 'next', e: Event) {
|
|
const repeat = function () {
|
|
control[action](ctrl);
|
|
ctrl.redraw();
|
|
delay = Math.max(100, delay - delay / 15);
|
|
timeout = setTimeout(repeat, delay);
|
|
};
|
|
let delay = 350;
|
|
let timeout = setTimeout(repeat, 500);
|
|
control[action](ctrl);
|
|
const eventName = e.type == 'touchstart' ? 'touchend' : 'mouseup';
|
|
document.addEventListener(eventName, () => clearTimeout(timeout), { once: true });
|
|
}
|
|
|
|
function controls(ctrl: AnalyseCtrl) {
|
|
const canJumpPrev = ctrl.path !== '',
|
|
canJumpNext = !!ctrl.node.children[0],
|
|
menuIsOpen = ctrl.actionMenu.open,
|
|
noarg = ctrl.trans.noarg;
|
|
return h(
|
|
'div.analyse__controls.analyse-controls',
|
|
{
|
|
hook: onInsert(el => {
|
|
bindMobileMousedown(
|
|
el,
|
|
e => {
|
|
const action = dataAct(e);
|
|
if (action === 'prev' || action === 'next') repeater(ctrl, action, e);
|
|
else if (action === 'first') control.first(ctrl);
|
|
else if (action === 'last') control.last(ctrl);
|
|
else if (action === 'explorer') ctrl.toggleExplorer();
|
|
else if (action === 'practice') ctrl.togglePractice();
|
|
else if (action === 'menu') ctrl.actionMenu.toggle();
|
|
},
|
|
ctrl.redraw
|
|
);
|
|
}),
|
|
},
|
|
[
|
|
ctrl.embed
|
|
? null
|
|
: h(
|
|
'div.features',
|
|
ctrl.studyPractice
|
|
? [
|
|
h('a.fbt', {
|
|
attrs: {
|
|
title: noarg('analysis'),
|
|
target: '_blank',
|
|
rel: 'noopener',
|
|
href: ctrl.studyPractice.analysisUrl(),
|
|
'data-icon': '',
|
|
},
|
|
}),
|
|
]
|
|
: [
|
|
h('button.fbt', {
|
|
attrs: {
|
|
title: noarg('openingExplorerAndTablebase'),
|
|
'data-act': 'explorer',
|
|
'data-icon': '',
|
|
},
|
|
class: {
|
|
hidden: menuIsOpen || !ctrl.explorer.allowed() || !!ctrl.retro,
|
|
active: ctrl.explorer.enabled(),
|
|
},
|
|
}),
|
|
ctrl.ceval.possible && ctrl.ceval.allowed() && !ctrl.isGamebook()
|
|
? h('button.fbt', {
|
|
attrs: {
|
|
title: noarg('practiceWithComputer'),
|
|
'data-act': 'practice',
|
|
'data-icon': '',
|
|
},
|
|
class: {
|
|
hidden: menuIsOpen || !!ctrl.retro,
|
|
active: !!ctrl.practice,
|
|
},
|
|
})
|
|
: null,
|
|
]
|
|
),
|
|
h('div.jumps', [
|
|
jumpButton('', 'first', canJumpPrev),
|
|
jumpButton('', 'prev', canJumpPrev),
|
|
jumpButton('', 'next', canJumpNext),
|
|
jumpButton('', 'last', canJumpNext),
|
|
]),
|
|
ctrl.studyPractice
|
|
? h('div.noop')
|
|
: h('button.fbt', {
|
|
class: { active: menuIsOpen },
|
|
attrs: {
|
|
title: noarg('menu'),
|
|
'data-act': 'menu',
|
|
'data-icon': '',
|
|
},
|
|
}),
|
|
]
|
|
);
|
|
}
|
|
|
|
function forceInnerCoords(ctrl: AnalyseCtrl, v: boolean) {
|
|
if (ctrl.data.pref.coords === Prefs.Coords.Outside) {
|
|
$('body').toggleClass('coords-in', v).toggleClass('coords-out', !v);
|
|
changeColorHandle();
|
|
}
|
|
}
|
|
|
|
function addChapterId(study: StudyCtrl | undefined, cssClass: string) {
|
|
return cssClass + (study && study.data.chapter ? '.' + study.data.chapter.id : '');
|
|
}
|
|
|
|
function analysisDisabled(ctrl: AnalyseCtrl): VNode {
|
|
return h('div.comp-off__hint', [
|
|
h('span', ctrl.trans.noarg('computerAnalysisDisabled')),
|
|
h(
|
|
'button',
|
|
{
|
|
hook: bind('click', ctrl.toggleComputer, ctrl.redraw),
|
|
attrs: { type: 'button' },
|
|
},
|
|
ctrl.trans.noarg('enable')
|
|
),
|
|
]);
|
|
}
|
|
|
|
export function renderMaterialDiffs(ctrl: AnalyseCtrl): [VNode, VNode] {
|
|
const cgState = ctrl.chessground?.state,
|
|
pieces = cgState ? cgState.pieces : readFen(ctrl.node.fen);
|
|
|
|
return materialView.renderMaterialDiffs(
|
|
true, // showCaptured
|
|
ctrl.flipped,
|
|
ctrl.data.player,
|
|
ctrl.data.opponent,
|
|
pieces,
|
|
ctrl.nodeList,
|
|
ctrl.node.ply
|
|
);
|
|
}
|
|
|
|
function renderPlayerStrip(cls: string, materialDiff: VNode, clock?: VNode): VNode {
|
|
return h('div.analyse__player-strip.' + cls, [materialDiff, clock]);
|
|
}
|
|
|
|
function renderPlayerStrips(ctrl: AnalyseCtrl): [VNode, VNode] | undefined {
|
|
if (ctrl.embed) return;
|
|
|
|
const clocks = renderClocks(ctrl),
|
|
whitePov = ctrl.bottomIsWhite(),
|
|
materialDiffs = renderMaterialDiffs(ctrl);
|
|
|
|
return [
|
|
renderPlayerStrip('top', materialDiffs[0], clocks?.[whitePov ? 1 : 0]),
|
|
renderPlayerStrip('bottom', materialDiffs[1], clocks?.[whitePov ? 0 : 1]),
|
|
];
|
|
}
|
|
|
|
export default function (ctrl: AnalyseCtrl): VNode {
|
|
if (ctrl.nvui) return ctrl.nvui.render(ctrl);
|
|
const concealOf = makeConcealOf(ctrl),
|
|
study = ctrl.study,
|
|
showCevalPvs = !(ctrl.retro && ctrl.retro.isSolving()) && !ctrl.practice,
|
|
menuIsOpen = ctrl.actionMenu.open,
|
|
gamebookPlay = ctrl.gamebookPlay(),
|
|
gamebookPlayView = gamebookPlay && gbPlay.render(gamebookPlay),
|
|
gamebookEditView = gbEdit.running(ctrl) ? gbEdit.render(ctrl) : undefined,
|
|
playerBars = renderPlayerBars(ctrl),
|
|
playerStrips = !playerBars && renderPlayerStrips(ctrl),
|
|
gaugeOn = ctrl.showEvalGauge(),
|
|
needsInnerCoords = !!gaugeOn || !!playerBars,
|
|
tour = relayTour(ctrl);
|
|
|
|
return h(
|
|
'main.analyse.variant-' + ctrl.data.game.variant.key,
|
|
{
|
|
hook: {
|
|
insert: vn => {
|
|
forceInnerCoords(ctrl, needsInnerCoords);
|
|
if (!!playerBars != $('body').hasClass('header-margin')) {
|
|
requestAnimationFrame(() => {
|
|
$('body').toggleClass('header-margin', !!playerBars);
|
|
ctrl.redraw();
|
|
});
|
|
}
|
|
gridHacks.start(vn.elm as HTMLElement);
|
|
},
|
|
update(_, _2) {
|
|
forceInnerCoords(ctrl, needsInnerCoords);
|
|
},
|
|
postpatch(old, vnode) {
|
|
if (old.data!.gaugeOn !== gaugeOn) document.body.dispatchEvent(new Event('chessground.resize'));
|
|
vnode.data!.gaugeOn = gaugeOn;
|
|
},
|
|
},
|
|
class: {
|
|
'comp-off': !ctrl.showComputer(),
|
|
'gauge-on': gaugeOn,
|
|
'has-players': !!playerBars,
|
|
'has-relay-tour': !!tour,
|
|
'analyse-hunter': ctrl.opts.hunter,
|
|
},
|
|
},
|
|
[
|
|
ctrl.keyboardHelp ? keyboardView(ctrl) : null,
|
|
study ? studyView.overboard(study) : null,
|
|
tour ||
|
|
h(
|
|
addChapterId(study, 'div.analyse__board.main-board'),
|
|
{
|
|
hook:
|
|
'ontouchstart' in window || lichess.storage.get('scrollMoves') == '0'
|
|
? undefined
|
|
: bindNonPassive('wheel', (e: WheelEvent) => wheel(ctrl, e)),
|
|
},
|
|
[
|
|
...(playerStrips || []),
|
|
playerBars ? playerBars[ctrl.bottomIsWhite() ? 1 : 0] : null,
|
|
chessground.render(ctrl),
|
|
playerBars ? playerBars[ctrl.bottomIsWhite() ? 0 : 1] : null,
|
|
ctrl.promotion.view(ctrl.data.game.variant.key === 'antichess'),
|
|
]
|
|
),
|
|
gaugeOn && !tour ? cevalView.renderGauge(ctrl) : null,
|
|
menuIsOpen || tour ? null : crazyView(ctrl, ctrl.topColor(), 'top'),
|
|
gamebookPlayView ||
|
|
(tour
|
|
? null
|
|
: h(addChapterId(study, 'div.analyse__tools'), [
|
|
...(menuIsOpen
|
|
? [actionMenu(ctrl)]
|
|
: [
|
|
ctrl.showComputer() ? cevalView.renderCeval(ctrl) : analysisDisabled(ctrl),
|
|
showCevalPvs ? cevalView.renderPvs(ctrl) : null,
|
|
renderAnalyse(ctrl, concealOf),
|
|
gamebookEditView || forkView(ctrl, concealOf),
|
|
retroView(ctrl) || practiceView(ctrl) || explorerView(ctrl),
|
|
]),
|
|
])),
|
|
menuIsOpen || tour ? null : crazyView(ctrl, ctrl.bottomColor(), 'bottom'),
|
|
gamebookPlayView || tour ? null : controls(ctrl),
|
|
ctrl.embed || tour
|
|
? null
|
|
: h(
|
|
'div.analyse__underboard',
|
|
{
|
|
hook:
|
|
ctrl.synthetic || playable(ctrl.data) ? undefined : onInsert(elm => serverSideUnderboard(elm, ctrl)),
|
|
},
|
|
study ? studyView.underboard(ctrl) : [inputs(ctrl)]
|
|
),
|
|
tour ? null : acplView(ctrl),
|
|
ctrl.embed
|
|
? null
|
|
: ctrl.studyPractice
|
|
? studyPracticeView.side(study!)
|
|
: h(
|
|
'aside.analyse__side',
|
|
{
|
|
hook: onInsert(elm => {
|
|
ctrl.opts.$side && ctrl.opts.$side.length && $(elm).replaceWith(ctrl.opts.$side);
|
|
$(elm).append($('.context-streamers').clone().removeClass('none'));
|
|
}),
|
|
},
|
|
ctrl.studyPractice
|
|
? [studyPracticeView.side(study!)]
|
|
: study
|
|
? [studyView.side(study)]
|
|
: [
|
|
ctrl.forecast ? forecastView(ctrl, ctrl.forecast) : null,
|
|
!ctrl.synthetic && playable(ctrl.data)
|
|
? h(
|
|
'div.back-to-game',
|
|
h(
|
|
'a.button.button-empty.text',
|
|
{
|
|
attrs: {
|
|
href: router.game(ctrl.data, ctrl.data.player.color),
|
|
'data-icon': '',
|
|
},
|
|
},
|
|
ctrl.trans.noarg('backToGame')
|
|
)
|
|
)
|
|
: null,
|
|
]
|
|
),
|
|
study && study.relay && relayManager(study.relay),
|
|
ctrl.opts.chat &&
|
|
h('section.mchat', {
|
|
hook: onInsert(_ => {
|
|
const chatOpts = ctrl.opts.chat;
|
|
chatOpts.instance?.then(c => c.destroy());
|
|
chatOpts.parseMoves = true;
|
|
chatOpts.instance = lichess.makeChat(chatOpts);
|
|
}),
|
|
}),
|
|
ctrl.embed
|
|
? null
|
|
: h('div.chat__members.none', {
|
|
hook: onInsert(lichess.watchers),
|
|
}),
|
|
]
|
|
);
|
|
}
|