From 054465d90a4900f149c0ce248294d43f9cdc0a3f Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Sun, 17 Dec 2023 14:47:06 +0100 Subject: [PATCH 1/5] rewrite ui/puzzle/ctrl as a class yet another attempt, hopefuly the last one --- ui/puzzle/src/control.ts | 12 +- ui/puzzle/src/ctrl.ts | 826 ++++++++++++++---------------- ui/puzzle/src/interfaces.ts | 81 +-- ui/puzzle/src/keyboard.ts | 8 +- ui/puzzle/src/main.ts | 6 +- ui/puzzle/src/plugins/nvui.ts | 44 +- ui/puzzle/src/view/after.ts | 14 +- ui/puzzle/src/view/boardMenu.ts | 4 +- ui/puzzle/src/view/chessground.ts | 6 +- ui/puzzle/src/view/feedback.ts | 12 +- ui/puzzle/src/view/main.ts | 17 +- ui/puzzle/src/view/side.ts | 51 +- ui/puzzle/src/view/theme.ts | 10 +- ui/puzzle/src/view/tree.ts | 14 +- 14 files changed, 484 insertions(+), 621 deletions(-) diff --git a/ui/puzzle/src/control.ts b/ui/puzzle/src/control.ts index ce060c90f69ad..ae4278edd1420 100644 --- a/ui/puzzle/src/control.ts +++ b/ui/puzzle/src/control.ts @@ -1,26 +1,26 @@ import { path as treePath } from 'tree'; -import { KeyboardController } from './interfaces'; +import PuzzleCtrl from './ctrl'; -export function canGoForward(ctrl: KeyboardController): boolean { +export function canGoForward(ctrl: PuzzleCtrl): boolean { return ctrl.vm.node.children.length > 0; } -export function next(ctrl: KeyboardController): void { +export function next(ctrl: PuzzleCtrl): void { const child = ctrl.vm.node.children[0]; if (!child) return; ctrl.userJump(ctrl.vm.path + child.id); } -export function prev(ctrl: KeyboardController): void { +export function prev(ctrl: PuzzleCtrl): void { ctrl.userJump(treePath.init(ctrl.vm.path)); } -export function last(ctrl: KeyboardController): void { +export function last(ctrl: PuzzleCtrl): void { const toInit = !treePath.contains(ctrl.vm.path, ctrl.vm.initialPath); ctrl.userJump(toInit ? ctrl.vm.initialPath : treePath.fromNodeList(ctrl.vm.mainline)); } -export function first(ctrl: KeyboardController): void { +export function first(ctrl: PuzzleCtrl): void { const toInit = ctrl.vm.path !== ctrl.vm.initialPath && treePath.contains(ctrl.vm.path, ctrl.vm.initialPath); ctrl.userJump(toInit ? ctrl.vm.initialPath : treePath.root); } diff --git a/ui/puzzle/src/ctrl.ts b/ui/puzzle/src/ctrl.ts index 32cd87f528f18..b10c955297be5 100644 --- a/ui/puzzle/src/ctrl.ts +++ b/ui/puzzle/src/ctrl.ts @@ -6,7 +6,7 @@ import moveTest from './moveTest'; import PuzzleSession from './session'; import PuzzleStreak from './streak'; import throttle from 'common/throttle'; -import { Vm, Controller, PuzzleOpts, PuzzleData, MoveTest, ThemeKey, ReplayEnd } from './interfaces'; +import { Vm, PuzzleOpts, PuzzleData, MoveTest, ThemeKey, ReplayEnd, NvuiPlugin } from './interfaces'; import { Api as CgApi } from 'chessground/api'; import { build as treeBuild, ops as treeOps, path as treePath, TreeWrapper } from 'tree'; import { Chess, normalizeMove } from 'chessops/chess'; @@ -16,155 +16,195 @@ import { CevalCtrl } from 'ceval'; import { makeVoiceMove, VoiceMove, RootCtrl as VoiceRoot } from 'voice'; import { ctrl as makeKeyboardMove, KeyboardMove, RootController as KeyboardRoot } from 'keyboardMove'; import { defer } from 'common/defer'; -import { defined, prop, Prop, propWithEffect, toggle } from 'common'; +import { defined, prop, Prop, propWithEffect, Toggle, toggle } from 'common'; import { makeSanAndPlay } from 'chessops/san'; import { parseFen, makeFen } from 'chessops/fen'; import { parseSquare, parseUci, makeSquare, makeUci, opposite } from 'chessops/util'; import { pgnToTree, mergeSolution } from './moveTree'; import { PromotionCtrl } from 'chess/promotion'; import { Role, Move, Outcome } from 'chessops/types'; -import { storedBooleanProp } from 'common/storage'; +import { StoredProp, storedBooleanProp, storedBooleanPropWithEffect } from 'common/storage'; import { fromNodeList } from 'tree/dist/path'; import { last } from 'tree/dist/ops'; import { uciToMove } from 'chessground/util'; import { Redraw } from 'common/snabbdom'; +import { ParentCtrl } from 'ceval/src/types'; -export default function (opts: PuzzleOpts, redraw: Redraw): Controller { - const vm: Vm = { +export default class PuzzleCtrl implements ParentCtrl { + vm: Vm = { next: defer(), + showAutoShapes: () => true, } as Vm; - let data: PuzzleData, tree: TreeWrapper, ceval: CevalCtrl; - const hasStreak = !!opts.data.streak; - const autoNext = storedBooleanProp(`puzzle.autoNext${hasStreak ? '.streak' : ''}`, hasStreak); - const rated = storedBooleanProp('puzzle.rated', true); - const ground = prop(undefined) as Prop; - const threatMode = prop(false); - const streak = opts.data.streak ? new PuzzleStreak(opts.data) : undefined; - const streakFailStorage = lichess.storage.make('puzzle.streak.fail'); - if (streak) { - opts.data = { - ...opts.data, - ...streak.data.current, - }; - streakFailStorage.listen(_ => failStreak(streak)); - } - const session = new PuzzleSession(opts.data.angle.key, opts.data.user?.id, hasStreak); - const menu = toggle(false, redraw); + data: PuzzleData; + trans: Trans; + tree: TreeWrapper; + ceval: CevalCtrl; + autoNext: StoredProp; + rated: StoredProp; + ground: Prop = prop(undefined) as Prop; + threatMode: Toggle = toggle(false); + streak?: PuzzleStreak; + streakFailStorage = lichess.storage.make('puzzle.streak.fail'); + session: PuzzleSession; + menu: Toggle; + flipped = toggle(false); + keyboardMove?: KeyboardMove; + voiceMove?: VoiceMove; + promotion: PromotionCtrl; + keyboardHelp: Prop; + + constructor( + readonly opts: PuzzleOpts, + readonly redraw: Redraw, + readonly nvui?: NvuiPlugin, + ) { + this.trans = lichess.trans(opts.i18n); + this.rated = storedBooleanPropWithEffect('puzzle.rated', true, this.redraw); + this.autoNext = storedBooleanProp( + `puzzle.autoNext${opts.data.streak ? '.streak' : ''}`, + !!opts.data.streak, + ); + this.streak = opts.data.streak ? new PuzzleStreak(opts.data) : undefined; + if (this.streak) { + opts.data = { ...opts.data, ...this.streak.data.current }; + this.streakFailStorage.listen(_ => this.failStreak(this.streak!)); + } + this.session = new PuzzleSession(opts.data.angle.key, opts.data.user?.id, !!opts.data.streak); + this.menu = toggle(false, redraw); - // required by ceval - vm.showComputer = () => vm.mode === 'view'; - vm.showAutoShapes = () => true; + this.initiate(opts.data); + this.promotion = new PromotionCtrl( + this.withGround, + () => this.withGround(g => g.set(this.vm.cgConfig)), + redraw, + ); + + this.keyboardHelp = propWithEffect(location.hash === '#keyboard', this.redraw); + keyboard(this); + + // If the page loads while being hidden (like when changing settings), + // chessground is not displayed, and the first move is not fully applied. + // Make sure chessground is fully shown when the page goes back to being visible. + document.addEventListener('visibilitychange', () => + lichess.requestIdleCallback(() => this.jump(this.vm.path), 500), + ); + + lichess.pubsub.on('zen', () => { + const zen = $('body').toggleClass('zen').hasClass('zen'); + window.dispatchEvent(new Event('resize')); + if (!$('body').hasClass('zen-auto')) xhr.setZen(zen); + }); + $('body').addClass('playing'); // for zen + $('#zentog').on('click', () => lichess.pubsub.emit('zen')); + } - const loadSound = (file: string, volume?: number) => { + private loadSound = (file: string, volume?: number) => { lichess.sound.load(file, `${lichess.sound.baseUrl}/${file}`); return () => lichess.sound.play(file, volume); }; - const sound = { - good: loadSound('lisp/PuzzleStormGood', 0.7), - end: loadSound('lisp/PuzzleStormEnd', 1), + sound = { + good: this.loadSound('lisp/PuzzleStormGood', 0.7), + end: this.loadSound('lisp/PuzzleStormEnd', 1), }; - let flipped = false; - - function setPath(path: Tree.Path): void { - vm.path = path; - vm.nodeList = tree.getNodeList(path); - vm.node = treeOps.last(vm.nodeList)!; - vm.mainline = treeOps.mainlineNodeList(tree.root); - } - - let keyboardMove: KeyboardMove | undefined; - let voiceMove: VoiceMove | undefined; + setPath = (path: Tree.Path): void => { + this.vm.path = path; + this.vm.nodeList = this.tree.getNodeList(path); + this.vm.node = treeOps.last(this.vm.nodeList)!; + this.vm.mainline = treeOps.mainlineNodeList(this.tree.root); + }; - function setChessground(this: Controller, cg: CgApi): void { - ground(cg); + setChessground = (cg: CgApi): void => { + this.ground(cg); const makeRoot = () => ({ data: { game: { variant: { key: 'standard' } }, - player: { color: vm.pov }, + player: { color: this.vm.pov }, }, chessground: cg, - sendMove: playUserMove, - auxMove: auxMove, + sendMove: this.playUserMove, + auxMove: this.auxMove, redraw: this.redraw, - flipNow: flip, - userJumpPlyDelta, - next: nextPuzzle, - vote, - solve: viewSolution, + flipNow: this.flip, + userJumpPlyDelta: this.userJumpPlyDelta, + next: this.nextPuzzle, + vote: this.vote, + solve: this.viewSolution, }); - if (opts.pref.voiceMove) - this.voiceMove = voiceMove = makeVoiceMove(makeRoot() as VoiceRoot, this.vm.node.fen); - if (opts.pref.keyboardMove) - this.keyboardMove = keyboardMove = makeKeyboardMove(makeRoot() as KeyboardRoot, { + if (this.opts.pref.voiceMove) this.voiceMove = makeVoiceMove(makeRoot() as VoiceRoot, this.vm.node.fen); + if (this.opts.pref.keyboardMove) + this.keyboardMove = makeKeyboardMove(makeRoot() as KeyboardRoot, { fen: this.vm.node.fen, }); requestAnimationFrame(() => this.redraw()); - } + }; + + pref = this.opts.pref; - function withGround(f: (cg: CgApi) => A): A | undefined { - const g = ground(); + withGround = (f: (cg: CgApi) => A): A | undefined => { + const g = this.ground(); return g && f(g); - } + }; - function initiate(fromData: PuzzleData): void { - data = fromData; - tree = treeBuild(pgnToTree(data.game.pgn.split(' '))); - const initialPath = treePath.fromNodeList(treeOps.mainlineNodeList(tree.root)); - vm.mode = 'play'; - vm.next = defer(); - vm.round = undefined; - vm.justPlayed = undefined; - vm.resultSent = false; - vm.lastFeedback = 'init'; - vm.initialPath = initialPath; - vm.initialNode = tree.nodeAtPath(initialPath); - vm.pov = vm.initialNode.ply % 2 == 1 ? 'black' : 'white'; - vm.isDaily = location.href.endsWith('/daily'); - - setPath(lichess.blindMode ? initialPath : treePath.init(initialPath)); + initiate = (fromData: PuzzleData): void => { + this.data = fromData; + this.tree = treeBuild(pgnToTree(this.data.game.pgn.split(' '))); + const initialPath = treePath.fromNodeList(treeOps.mainlineNodeList(this.tree.root)); + this.vm.mode = 'play'; + this.vm.next = defer(); + this.vm.round = undefined; + this.vm.justPlayed = undefined; + this.vm.resultSent = false; + this.vm.lastFeedback = 'init'; + this.vm.initialPath = initialPath; + this.vm.initialNode = this.tree.nodeAtPath(initialPath); + this.vm.pov = this.vm.initialNode.ply % 2 == 1 ? 'black' : 'white'; + this.vm.isDaily = location.href.endsWith('/daily'); + + this.setPath(lichess.blindMode ? initialPath : treePath.init(initialPath)); setTimeout( () => { - jump(initialPath); - redraw(); + this.jump(initialPath); + this.redraw(); }, - opts.pref.animation.duration > 0 ? 500 : 0, + this.opts.pref.animation.duration > 0 ? 500 : 0, ); // just to delay button display - vm.canViewSolution = false; - if (!vm.canViewSolution) { + this.vm.canViewSolution = false; + if (!this.vm.canViewSolution) { setTimeout( () => { - vm.canViewSolution = true; - redraw(); + this.vm.canViewSolution = true; + this.redraw(); }, - rated() ? 4000 : 1000, + this.rated() ? 4000 : 1000, ); } - withGround(g => { + this.withGround(g => { g.selectSquare(null); g.setAutoShapes([]); g.setShapes([]); - showGround(g); + this.showGround(g); }); - instanciateCeval(); - } + this.instanciateCeval(); + }; - function position(): Chess { - const setup = parseFen(vm.node.fen).unwrap(); + position = (): Chess => { + const setup = parseFen(this.vm.node.fen).unwrap(); return Chess.fromSetup(setup).unwrap(); - } + }; - function makeCgOpts(): CgConfig { - const node = vm.node; + makeCgOpts = (): CgConfig => { + const node = this.vm.node; const color: Color = node.ply % 2 === 0 ? 'white' : 'black'; - const dests = chessgroundDests(position()); - const nextNode = vm.node.children[0]; - const canMove = vm.mode === 'view' || (color === vm.pov && (!nextNode || nextNode.puzzle == 'fail')); + const dests = chessgroundDests(this.position()); + const nextNode = this.vm.node.children[0]; + const canMove = + this.vm.mode === 'view' || (color === this.vm.pov && (!nextNode || nextNode.puzzle == 'fail')); const movable = canMove ? { color: dests.size > 0 ? color : undefined, @@ -176,7 +216,7 @@ export default function (opts: PuzzleOpts, redraw: Redraw): Controller { }; const config = { fen: node.fen, - orientation: flipped ? opposite(vm.pov) : vm.pov, + orientation: this.flipped() ? opposite(this.vm.pov) : this.vm.pov, turnColor: color, movable: movable, premovable: { @@ -185,63 +225,56 @@ export default function (opts: PuzzleOpts, redraw: Redraw): Controller { check: !!node.check, lastMove: uciToMove(node.uci), }; - if (node.ply >= vm.initialNode.ply) { - if (vm.mode !== 'view' && color !== vm.pov && !nextNode) { - config.movable.color = vm.pov; + if (node.ply >= this.vm.initialNode.ply) { + if (this.vm.mode !== 'view' && color !== this.vm.pov && !nextNode) { + config.movable.color = this.vm.pov; config.premovable.enabled = true; } } - vm.cgConfig = config; + this.vm.cgConfig = config; return config; - } + }; - function showGround(g: CgApi): void { - g.set(makeCgOpts()); - } + showGround = (g: CgApi): void => g.set(this.makeCgOpts()); - function auxMove(orig: Key, dest: Key, role?: Role) { - if (role) playUserMove(orig, dest, role); + auxMove = (orig: Key, dest: Key, role?: Role) => { + if (role) this.playUserMove(orig, dest, role); else - withGround(g => { + this.withGround(g => { g.move(orig, dest); g.state.movable.dests = undefined; g.state.turnColor = opposite(g.state.turnColor); }); - } + }; - function userMove(orig: Key, dest: Key): void { - vm.justPlayed = orig; - if (!promotion.start(orig, dest, { submit: playUserMove, show: voiceMove?.promotionHook() })) - playUserMove(orig, dest); - voiceMove?.update(vm.node.fen, true); - keyboardMove?.update({ fen: vm.node.fen }); - } + userMove = (orig: Key, dest: Key): void => { + this.vm.justPlayed = orig; + if ( + !this.promotion.start(orig, dest, { submit: this.playUserMove, show: this.voiceMove?.promotionHook() }) + ) + this.playUserMove(orig, dest); + this.voiceMove?.update(this.vm.node.fen, true); + this.keyboardMove?.update({ fen: this.vm.node.fen }); + }; - function playUci(uci: Uci): void { - sendMove(parseUci(uci)!); - } + playUci = (uci: Uci): void => this.sendMove(parseUci(uci)!); - function playUciList(uciList: Uci[]): void { - uciList.forEach(playUci); - } + playUciList = (uciList: Uci[]): void => uciList.forEach(this.playUci); - function playUserMove(orig: Key, dest: Key, promotion?: Role): void { - sendMove({ + playUserMove = (orig: Key, dest: Key, promotion?: Role): void => + this.sendMove({ from: parseSquare(orig)!, to: parseSquare(dest)!, promotion, }); - } - function sendMove(move: Move): void { - sendMoveAt(vm.path, position(), move); - } + sendMove = (move: Move): void => this.sendMoveAt(this.vm.path, this.position(), move); - function sendMoveAt(path: Tree.Path, pos: Chess, move: Move): void { + sendMoveAt = (path: Tree.Path, pos: Chess, move: Move): void => { move = normalizeMove(pos, move); const san = makeSanAndPlay(pos, move); const check = pos.isCheck() ? pos.board.kingOf(pos.turn) : undefined; - addNode( + this.addNode( { ply: 2 * (pos.fullmoves - 1) + (pos.turn == 'white' ? 0 : 1), fen: makeFen(pos.toSetup()), @@ -253,149 +286,149 @@ export default function (opts: PuzzleOpts, redraw: Redraw): Controller { }, path, ); - } + }; - function addNode(node: Tree.Node, path: Tree.Path): void { - const newPath = tree.addNode(node, path)!; - jump(newPath); - withGround(g => g.playPremove()); + addNode = (node: Tree.Node, path: Tree.Path): void => { + const newPath = this.tree.addNode(node, path)!; + this.jump(newPath); + this.withGround(g => g.playPremove()); - const progress = moveTest(vm, data.puzzle); + const progress = moveTest(this.vm, this.data.puzzle); if (progress === 'fail') lichess.sound.say('incorrect'); - if (progress) applyProgress(progress); - reorderChildren(path); - redraw(); - } + if (progress) this.applyProgress(progress); + this.reorderChildren(path); + this.redraw(); + }; - function reorderChildren(path: Tree.Path, recursive?: boolean): void { - const node = tree.nodeAtPath(path); + reorderChildren = (path: Tree.Path, recursive?: boolean): void => { + const node = this.tree.nodeAtPath(path); node.children.sort((c1, _) => { const p = c1.puzzle; if (p == 'fail') return 1; if (p == 'good' || p == 'win') return -1; return 0; }); - if (recursive) node.children.forEach(child => reorderChildren(path + child.id, true)); - } + if (recursive) node.children.forEach(child => this.reorderChildren(path + child.id, true)); + }; - function instantRevertUserMove(): void { - withGround(g => { + private instantRevertUserMove = (): void => { + this.withGround(g => { g.cancelPremove(); g.selectSquare(null); }); - jump(treePath.init(vm.path)); - redraw(); - } + this.jump(treePath.init(this.vm.path)); + this.redraw(); + }; - function revertUserMove(): void { - if (lichess.blindMode) instantRevertUserMove(); - else setTimeout(instantRevertUserMove, 100); - } + revertUserMove = (): void => { + if (lichess.blindMode) this.instantRevertUserMove(); + else setTimeout(this.instantRevertUserMove, 100); + }; - function applyProgress(progress: undefined | 'fail' | 'win' | MoveTest): void { + applyProgress = (progress: undefined | 'fail' | 'win' | MoveTest): void => { if (progress === 'fail') { - vm.lastFeedback = 'fail'; - revertUserMove(); - if (vm.mode === 'play') { - if (streak) { - failStreak(streak); - streakFailStorage.fire(); + this.vm.lastFeedback = 'fail'; + this.revertUserMove(); + if (this.vm.mode === 'play') { + if (this.streak) { + this.failStreak(this.streak); + this.streakFailStorage.fire(); } else { - vm.canViewSolution = true; - vm.mode = 'try'; - sendResult(false); + this.vm.canViewSolution = true; + this.vm.mode = 'try'; + this.sendResult(false); } } } else if (progress == 'win') { - if (streak) sound.good(); - vm.lastFeedback = 'win'; - if (vm.mode != 'view') { - const sent = vm.mode == 'play' ? sendResult(true) : Promise.resolve(); - vm.mode = 'view'; - withGround(showGround); - sent.then(_ => (autoNext() ? nextPuzzle() : startCeval())); + if (this.streak) this.sound.good(); + this.vm.lastFeedback = 'win'; + if (this.vm.mode != 'view') { + const sent = this.vm.mode == 'play' ? this.sendResult(true) : Promise.resolve(); + this.vm.mode = 'view'; + this.withGround(this.showGround); + sent.then(_ => (this.autoNext() ? this.nextPuzzle() : this.startCeval())); } } else if (progress) { - vm.lastFeedback = 'good'; + this.vm.lastFeedback = 'good'; setTimeout( () => { const pos = Chess.fromSetup(parseFen(progress.fen).unwrap()).unwrap(); - sendMoveAt(progress.path, pos, progress.move); + this.sendMoveAt(progress.path, pos, progress.move); }, - opts.pref.animation.duration * (autoNext() ? 1 : 1.5), + this.opts.pref.animation.duration * (this.autoNext() ? 1 : 1.5), ); } - } + }; - function failStreak(streak: PuzzleStreak): void { - vm.mode = 'view'; + failStreak = (streak: PuzzleStreak): void => { + this.vm.mode = 'view'; streak.onComplete(false); - setTimeout(viewSolution, 500); - sound.end(); - } + setTimeout(this.viewSolution, 500); + this.sound.end(); + }; - async function sendResult(win: boolean): Promise { - if (vm.resultSent) return Promise.resolve(); - vm.resultSent = true; - session.complete(data.puzzle.id, win); + sendResult = async (win: boolean): Promise => { + if (this.vm.resultSent) return Promise.resolve(); + this.vm.resultSent = true; + this.session.complete(this.data.puzzle.id, win); const res = await xhr.complete( - data.puzzle.id, - data.angle.key, + this.data.puzzle.id, + this.data.angle.key, win, - rated, - data.replay, - streak, - opts.settings.color, + this.rated, + this.data.replay, + this.streak, + this.opts.settings.color, ); const next = res.next; - if (next?.user && data.user) { - data.user.rating = next.user.rating; - data.user.provisional = next.user.provisional; - vm.round = res.round; - if (res.round?.ratingDiff) session.setRatingDiff(data.puzzle.id, res.round.ratingDiff); + if (next?.user && this.data.user) { + this.data.user.rating = next.user.rating; + this.data.user.provisional = next.user.provisional; + this.vm.round = res.round; + if (res.round?.ratingDiff) this.session.setRatingDiff(this.data.puzzle.id, res.round.ratingDiff); } if (win) lichess.sound.say('Success!'); if (next) { - vm.next.resolve(data.replay && res.replayComplete ? data.replay : next); - if (streak && win) streak.onComplete(true, res.next); + this.vm.next.resolve(this.data.replay && res.replayComplete ? this.data.replay : next); + if (this.streak && win) this.streak.onComplete(true, res.next); } - redraw(); + this.redraw(); if (!next) { - if (!data.replay) { + if (!this.data.replay) { alert('No more puzzles available! Try another theme.'); lichess.redirect('/training/themes'); } } - } + }; - const isPuzzleData = (d: PuzzleData | ReplayEnd): d is PuzzleData => 'puzzle' in d; + private isPuzzleData = (d: PuzzleData | ReplayEnd): d is PuzzleData => 'puzzle' in d; - function nextPuzzle(): void { - if (streak && vm.lastFeedback != 'win') return; - if (vm.mode !== 'view') return; + nextPuzzle = (): void => { + if (this.streak && this.vm.lastFeedback != 'win') return; + if (this.vm.mode !== 'view') return; - ceval.stop(); - vm.next.promise.then(n => { - if (isPuzzleData(n)) { - initiate(n); - redraw(); + this.ceval.stop(); + this.vm.next.promise.then(n => { + if (this.isPuzzleData(n)) { + this.initiate(n); + this.redraw(); } }); - if (data.replay && vm.round === undefined) { - lichess.redirect(`/training/dashboard/${data.replay.days}`); + if (this.data.replay && this.vm.round === undefined) { + lichess.redirect(`/training/dashboard/${this.data.replay.days}`); } - if (!streak && !data.replay) { - const path = router.withLang(`/training/${data.angle.key}`); + if (!this.streak && !this.data.replay) { + const path = router.withLang(`/training/${this.data.angle.key}`); if (location.pathname != path) history.replaceState(null, '', path); } - } + }; - function instanciateCeval(): void { - if (ceval) ceval.destroy(); - ceval = new CevalCtrl({ - redraw, + instanciateCeval = (): void => { + this.ceval?.destroy(); + this.ceval = new CevalCtrl({ + redraw: this.redraw, variant: { short: 'Std', name: 'Standard', @@ -403,285 +436,188 @@ export default function (opts: PuzzleOpts, redraw: Redraw): Controller { }, initialFen: undefined, // always standard starting position possible: true, - emit: function (ev, work) { - tree.updateAt(work.path, function (node) { + emit: (ev, work) => { + this.tree.updateAt(work.path, node => { if (work.threatMode) { const threat = ev as Tree.LocalEval; if (!node.threat || node.threat.depth <= threat.depth) node.threat = threat; } else if (!node.ceval || node.ceval.depth <= ev.depth) node.ceval = ev; - if (work.path === vm.path) { - setAutoShapes(); - redraw(); + if (work.path === this.vm.path) { + this.setAutoShapes(); + this.redraw(); } }); }, - setAutoShapes: setAutoShapes, + setAutoShapes: this.setAutoShapes, }); - } + }; - function setAutoShapes(): void { - withGround(g => { + setAutoShapes = (): void => + this.withGround(g => g.setAutoShapes( computeAutoShapes({ - vm: vm, - ceval: ceval, + vm: this.vm, + ceval: this.ceval, ground: g, - threatMode: threatMode(), - nextNodeBest: nextNodeBest(), + threatMode: this.threatMode(), + nextNodeBest: this.nextNodeBest(), }), - ); - }); - } + ), + ); - function canUseCeval(): boolean { - return vm.mode === 'view' && !outcome(); - } + canUseCeval = (): boolean => this.vm.mode === 'view' && !this.outcome(); - function startCeval(): void { - if (ceval.enabled() && canUseCeval()) doStartCeval(); - } - - const doStartCeval = throttle(800, function () { - ceval.start(vm.path, vm.nodeList, threatMode()); - }); + startCeval = (): void => { + if (this.ceval.enabled() && this.canUseCeval()) this.doStartCeval(); + }; - const nextNodeBest = () => treeOps.withMainlineChild(vm.node, n => n.eval?.best); + private doStartCeval = throttle(800, () => + this.ceval.start(this.vm.path, this.vm.nodeList, this.threatMode()), + ); - const getCeval = () => ceval; + nextNodeBest = () => treeOps.withMainlineChild(this.vm.node, n => n.eval?.best); - function toggleCeval(): void { - ceval.toggle(); - setAutoShapes(); - startCeval(); - if (!ceval.enabled()) threatMode(false); - vm.autoScrollRequested = true; - redraw(); - } + toggleCeval = (): void => { + this.ceval.toggle(); + this.setAutoShapes(); + this.startCeval(); + if (!this.ceval.enabled()) this.threatMode(false); + this.vm.autoScrollRequested = true; + this.redraw(); + }; - function restartCeval(): void { - ceval.stop(); - startCeval(); - redraw(); - } + restartCeval = (): void => { + this.ceval.stop(); + this.startCeval(); + this.redraw(); + }; - function toggleThreatMode(): void { - if (vm.node.check) return; - if (!ceval.enabled()) ceval.toggle(); - if (!ceval.enabled()) return; - threatMode(!threatMode()); - setAutoShapes(); - startCeval(); - redraw(); - } + toggleThreatMode = (): void => { + if (this.vm.node.check) return; + if (!this.ceval.enabled()) this.ceval.toggle(); + if (!this.ceval.enabled()) return; + this.threatMode.toggle(); + this.setAutoShapes(); + this.startCeval(); + this.redraw(); + }; - function outcome(): Outcome | undefined { - return position().outcome(); - } + outcome = (): Outcome | undefined => this.position().outcome(); - function jump(path: Tree.Path): void { - const pathChanged = path !== vm.path, - isForwardStep = pathChanged && path.length === vm.path.length + 2; - setPath(path); - withGround(showGround); + jump = (path: Tree.Path): void => { + const pathChanged = path !== this.vm.path, + isForwardStep = pathChanged && path.length === this.vm.path.length + 2; + this.setPath(path); + this.withGround(this.showGround); if (pathChanged) { if (isForwardStep) { - lichess.sound.saySan(vm.node.san); - lichess.sound.move(vm.node); + lichess.sound.saySan(this.vm.node.san); + lichess.sound.move(this.vm.node); } - threatMode(false); - ceval.stop(); - startCeval(); + this.threatMode(false); + this.ceval.stop(); + this.startCeval(); } - promotion.cancel(); - vm.justPlayed = undefined; - vm.autoScrollRequested = true; - keyboardMove?.update({ fen: vm.node.fen }); - voiceMove?.update(vm.node.fen, true); - lichess.pubsub.emit('ply', vm.node.ply); - } + this.promotion.cancel(); + this.vm.justPlayed = undefined; + this.vm.autoScrollRequested = true; + this.keyboardMove?.update({ fen: this.vm.node.fen }); + this.voiceMove?.update(this.vm.node.fen, true); + lichess.pubsub.emit('ply', this.vm.node.ply); + }; - function userJump(path: Tree.Path): void { - if (tree.nodeAtPath(path)?.puzzle == 'fail' && vm.mode != 'view') return; - withGround(g => g.selectSquare(null)); - jump(path); - } + userJump = (path: Tree.Path): void => { + if (this.tree.nodeAtPath(path)?.puzzle == 'fail' && this.vm.mode != 'view') return; + this.withGround(g => g.selectSquare(null)); + this.jump(path); + }; - function userJumpPlyDelta(plyDelta: Ply) { + userJumpPlyDelta = (plyDelta: Ply) => { // ensure we are jumping to a valid ply - let maxValidPly = vm.mainline.length - 1; - if (last(vm.mainline)?.puzzle == 'fail' && vm.mode != 'view') maxValidPly -= 1; - const newPly = Math.min(Math.max(vm.node.ply + plyDelta, 0), maxValidPly); - userJump(fromNodeList(vm.mainline.slice(0, newPly + 1))); - } + let maxValidPly = this.vm.mainline.length - 1; + if (last(this.vm.mainline)?.puzzle == 'fail' && this.vm.mode != 'view') maxValidPly -= 1; + const newPly = Math.min(Math.max(this.vm.node.ply + plyDelta, 0), maxValidPly); + this.userJump(fromNodeList(this.vm.mainline.slice(0, newPly + 1))); + }; - function viewSolution(): void { - sendResult(false); - vm.mode = 'view'; - mergeSolution(tree, vm.initialPath, data.puzzle.solution, vm.pov); - reorderChildren(vm.initialPath, true); + viewSolution = (): void => { + this.sendResult(false); + this.vm.mode = 'view'; + mergeSolution(this.tree, this.vm.initialPath, this.data.puzzle.solution, this.vm.pov); + this.reorderChildren(this.vm.initialPath, true); // try to play the solution next move - const next = vm.node.children[0]; - if (next && next.puzzle === 'good') userJump(vm.path + next.id); + const next = this.vm.node.children[0]; + if (next && next.puzzle === 'good') this.userJump(this.vm.path + next.id); else { - const firstGoodPath = treeOps.takePathWhile(vm.mainline, node => node.puzzle != 'good'); - if (firstGoodPath) userJump(firstGoodPath + tree.nodeAtPath(firstGoodPath).children[0].id); + const firstGoodPath = treeOps.takePathWhile(this.vm.mainline, node => node.puzzle != 'good'); + if (firstGoodPath) this.userJump(firstGoodPath + this.tree.nodeAtPath(firstGoodPath).children[0].id); } - vm.autoScrollRequested = true; - vm.voteDisabled = true; - redraw(); - startCeval(); + this.vm.autoScrollRequested = true; + this.vm.voteDisabled = true; + this.redraw(); + this.startCeval(); setTimeout(() => { - vm.voteDisabled = false; - redraw(); + this.vm.voteDisabled = false; + this.redraw(); }, 500); - } + }; - const skip = () => { - if (!streak || !streak.data.skip || vm.mode != 'play') return; - streak.skip(); - userJump(treePath.fromNodeList(vm.mainline)); - const moveIndex = treePath.size(vm.path) - treePath.size(vm.initialPath); - const solution = data.puzzle.solution[moveIndex]; - playUci(solution); - playBestMove(); + skip = () => { + if (!this.streak || !this.streak.data.skip || this.vm.mode != 'play') return; + this.streak.skip(); + this.userJump(treePath.fromNodeList(this.vm.mainline)); + const moveIndex = treePath.size(this.vm.path) - treePath.size(this.vm.initialPath); + const solution = this.data.puzzle.solution[moveIndex]; + this.playUci(solution); + this.playBestMove(); }; - const flip = () => { - flipped = !flipped; - withGround(g => g.toggleOrientation()); - redraw(); + flip = () => { + this.flipped.toggle(); + this.withGround(g => g.toggleOrientation()); + this.redraw(); }; - const vote = (v: boolean) => { - if (!vm.voteDisabled) { - xhr.vote(data.puzzle.id, v); - nextPuzzle(); + vote = (v: boolean) => { + if (!this.vm.voteDisabled) { + xhr.vote(this.data.puzzle.id, v); + this.nextPuzzle(); } }; - const voteTheme = (theme: ThemeKey, v: boolean) => { - if (vm.round) { - vm.round.themes = vm.round.themes || {}; - if (v === vm.round.themes[theme]) { - delete vm.round.themes[theme]; - xhr.voteTheme(data.puzzle.id, theme, undefined); + voteTheme = (theme: ThemeKey, v: boolean) => { + if (this.vm.round) { + this.vm.round.themes = this.vm.round.themes || {}; + if (v === this.vm.round.themes[theme]) { + delete this.vm.round.themes[theme]; + xhr.voteTheme(this.data.puzzle.id, theme, undefined); } else { - if (v || data.puzzle.themes.includes(theme)) vm.round.themes[theme] = v; - else delete vm.round.themes[theme]; - xhr.voteTheme(data.puzzle.id, theme, v); + if (v || this.data.puzzle.themes.includes(theme)) this.vm.round.themes[theme] = v; + else delete this.vm.round.themes[theme]; + xhr.voteTheme(this.data.puzzle.id, theme, v); } - redraw(); + this.redraw(); } }; - initiate(opts.data); - - const promotion = new PromotionCtrl(withGround, () => withGround(g => g.set(vm.cgConfig)), redraw); - - function playBestMove(): void { - const uci = nextNodeBest() || (vm.node.ceval && vm.node.ceval.pvs[0].moves[0]); - if (uci) playUci(uci); - } - - const keyboardHelp = propWithEffect(location.hash === '#keyboard', redraw); - keyboard({ - vm, - userJump, - getCeval, - toggleCeval, - toggleThreatMode, - redraw, - playBestMove, - flip, - flipped: () => flipped, - nextPuzzle, - keyboardHelp, - }); - - // If the page loads while being hidden (like when changing settings), - // chessground is not displayed, and the first move is not fully applied. - // Make sure chessground is fully shown when the page goes back to being visible. - document.addEventListener('visibilitychange', () => lichess.requestIdleCallback(() => jump(vm.path), 500)); - - lichess.pubsub.on('zen', () => { - const zen = $('body').toggleClass('zen').hasClass('zen'); - window.dispatchEvent(new Event('resize')); - if (!$('body').hasClass('zen-auto')) { - xhr.setZen(zen); - } - }); - $('body').addClass('playing'); // for zen - $('#zentog').on('click', () => lichess.pubsub.emit('zen')); - - return { - vm, - getData() { - return data; - }, - getTree() { - return tree; - }, - setChessground, - ground, - makeCgOpts, - voiceMove, - keyboardMove, - keyboardHelp, - userJump, - viewSolution, - nextPuzzle, - vote, - voteTheme, - getCeval, - pref: opts.pref, - settings: opts.settings, - trans: lichess.trans(opts.i18n), - autoNext, - autoNexting: () => vm.lastFeedback == 'win' && autoNext(), - rated, - toggleRated: () => { - rated(!rated()); - redraw(); - }, - outcome, - toggleCeval, - toggleThreatMode, - threatMode, - currentEvals() { - return { client: vm.node.ceval }; - }, - nextNodeBest, - userMove, - playUci, - playUciList, - showEvalGauge() { - return vm.showComputer() && ceval.enabled() && !outcome(); - }, - getOrientation() { - return withGround(g => g.state.orientation)!; - }, - getNode() { - return vm.node; - }, - showComputer: vm.showComputer, - promotion, - redraw, - ongoing: false, - playBestMove, - session, - allThemes: opts.themes && { - dynamic: opts.themes.dynamic.split(' '), - static: new Set(opts.themes.static.split(' ')), - }, - streak, - skip, - flip, - flipped: () => flipped, - showRatings: opts.showRatings, - menu, - restartCeval: restartCeval, - clearCeval: restartCeval, + playBestMove = (): void => { + const uci = this.nextNodeBest() || (this.vm.node.ceval && this.vm.node.ceval.pvs[0].moves[0]); + if (uci) this.playUci(uci); + }; + autoNexting = () => this.vm.lastFeedback == 'win' && this.autoNext(); + currentEvals = () => ({ client: this.vm.node.ceval }); + showEvalGauge = () => this.vm.showComputer() && this.ceval.enabled() && !this.outcome(); + getOrientation = () => this.withGround(g => g.state.orientation)!; + allThemes = this.opts.themes && { + dynamic: this.opts.themes.dynamic.split(' '), + static: new Set(this.opts.themes.static.split(' ')), }; + toggleRated = () => this.rated(!this.rated()); + // implement cetal ParentCtrl: + getCeval = () => this.ceval; + ongoing = false; + getNode = () => this.vm.node; + showComputer = () => this.vm.mode === 'view'; } diff --git a/ui/puzzle/src/interfaces.ts b/ui/puzzle/src/interfaces.ts index 325e43912e555..7c2e4334b19be 100644 --- a/ui/puzzle/src/interfaces.ts +++ b/ui/puzzle/src/interfaces.ts @@ -1,99 +1,26 @@ -import PuzzleSession from './session'; -import { Api as CgApi } from 'chessground/api'; -import { CevalCtrl, NodeEvals } from 'ceval'; import { Config as CgConfig } from 'chessground/config'; import { Deferred } from 'common/defer'; -import { Outcome, Move } from 'chessops/types'; -import { Prop, Toggle } from 'common'; -import { StoredProp } from 'common/storage'; -import { TreeWrapper } from 'tree'; +import { Move } from 'chessops/types'; import { VNode } from 'snabbdom'; -import PuzzleStreak from './streak'; -import { PromotionCtrl } from 'chess/promotion'; -import { KeyboardMove } from 'keyboardMove'; -import { VoiceMove } from 'voice'; import * as Prefs from 'common/prefs'; import perfIcons from 'common/perfIcons'; -import { Redraw } from 'common/snabbdom'; +import PuzzleCtrl from './ctrl'; export type PuzzleId = string; -export interface KeyboardController { - vm: Vm; - redraw: Redraw; - userJump(path: Tree.Path): void; - getCeval(): CevalCtrl; - toggleCeval(): void; - toggleThreatMode(): void; - playBestMove(): void; - flip(): void; - flipped(): boolean; - nextPuzzle(): void; - keyboardHelp: Prop; -} - export type ThemeKey = string; export interface AllThemes { dynamic: ThemeKey[]; static: Set; } -export interface Controller extends KeyboardController { - nextNodeBest(): string | undefined; - disableThreatMode?: Prop; - outcome(): Outcome | undefined; - mandatoryCeval?: Prop; - showEvalGauge: Prop; - currentEvals(): NodeEvals; - ongoing: boolean; - playUci(uci: string): void; - playUciList(uciList: string[]): void; - getOrientation(): Color; - threatMode: Prop; - getNode(): Tree.Node; - showComputer(): boolean; - trans: Trans; - getData(): PuzzleData; - getTree(): TreeWrapper; - ground: Prop; - setChessground(cg: CgApi): void; - makeCgOpts(): CgConfig; - viewSolution(): void; - nextPuzzle(): void; - vote(v: boolean): void; - voteTheme(theme: ThemeKey, v: boolean): void; - pref: PuzzlePrefs; - settings: PuzzleSettings; - userMove(orig: Key, dest: Key): void; - promotion: PromotionCtrl; - autoNext: StoredProp; - autoNexting: () => boolean; - rated: StoredProp; - toggleRated: () => void; - session: PuzzleSession; - allThemes?: AllThemes; - showRatings: boolean; - keyboardMove?: KeyboardMove; - voiceMove?: VoiceMove; - - streak?: PuzzleStreak; - skip(): void; - - path?: Tree.Path; - autoScrollRequested?: boolean; - - nvui?: NvuiPlugin; - menu: Toggle; - restartCeval(): void; - clearCeval(): void; -} - export interface NvuiPlugin { - render(ctrl: Controller): VNode; + render(ctrl: PuzzleCtrl): VNode; } export type ReplayEnd = PuzzleReplay; +// #TODO wut export interface Vm { path: Tree.Path; nodeList: Tree.Node[]; diff --git a/ui/puzzle/src/keyboard.ts b/ui/puzzle/src/keyboard.ts index 59ec666fd01da..2405ba0378344 100644 --- a/ui/puzzle/src/keyboard.ts +++ b/ui/puzzle/src/keyboard.ts @@ -1,8 +1,8 @@ import * as control from './control'; -import { Controller, KeyboardController } from './interfaces'; +import PuzzleCtrl from './ctrl'; import { snabDialog } from 'common/dialog'; -export default (ctrl: KeyboardController) => +export default (ctrl: PuzzleCtrl) => lichess.mousetrap .bind(['left', 'k'], () => { control.prev(ctrl); @@ -24,7 +24,7 @@ export default (ctrl: KeyboardController) => .bind('x', ctrl.toggleThreatMode) .bind('space', () => { if (ctrl.vm.mode === 'view') { - if (ctrl.getCeval().enabled()) ctrl.playBestMove(); + if (ctrl.ceval.enabled()) ctrl.playBestMove(); else ctrl.toggleCeval(); } }) @@ -33,7 +33,7 @@ export default (ctrl: KeyboardController) => .bind('f', ctrl.flip) .bind('n', ctrl.nextPuzzle); -export const view = (ctrl: Controller) => +export const view = (ctrl: PuzzleCtrl) => snabDialog({ class: 'help', htmlUrl: '/training/help', diff --git a/ui/puzzle/src/main.ts b/ui/puzzle/src/main.ts index 11691331e8a7b..f439c2d7cdecd 100644 --- a/ui/puzzle/src/main.ts +++ b/ui/puzzle/src/main.ts @@ -1,5 +1,5 @@ import { attributesModule, classModule, init } from 'snabbdom'; -import makeCtrl from './ctrl'; +import PuzzleCtrl from './ctrl'; import menuHover from 'common/menuHover'; import view from './view/main'; import { PuzzleOpts, NvuiPlugin } from './interfaces'; @@ -8,8 +8,8 @@ const patch = init([classModule, attributesModule]); export async function initModule(opts: PuzzleOpts) { const element = document.querySelector('main.puzzle') as HTMLElement; - const ctrl = makeCtrl(opts, redraw); - ctrl.nvui = lichess.blindMode ? await lichess.asset.loadEsm('puzzle.nvui') : undefined; + const nvui = lichess.blindMode ? await lichess.asset.loadEsm('puzzle.nvui') : undefined; + const ctrl = new PuzzleCtrl(opts, redraw, nvui); const blueprint = view(ctrl); element.innerHTML = ''; diff --git a/ui/puzzle/src/plugins/nvui.ts b/ui/puzzle/src/plugins/nvui.ts index 468788d4da605..42daa8a66a5fc 100644 --- a/ui/puzzle/src/plugins/nvui.ts +++ b/ui/puzzle/src/plugins/nvui.ts @@ -1,5 +1,4 @@ import { h, VNode } from 'snabbdom'; -import { Controller } from '../interfaces'; import { puzzleBox, renderDifficultyForm, userBox } from '../view/side'; import theme from '../view/theme'; import { @@ -31,6 +30,7 @@ import * as control from '../control'; import { bind, onInsert } from 'common/snabbdom'; import { Api } from 'chessground/api'; import throttle from 'common/throttle'; +import PuzzleCtrl from '../ctrl'; const throttled = (sound: string) => throttle(100, () => lichess.sound.play(sound)); const selectSound = throttled('select'); @@ -45,7 +45,7 @@ export function initModule() { positionStyle = positionSetting(), boardStyle = boardSetting(); return { - render(ctrl: Controller): VNode { + render(ctrl: PuzzleCtrl): VNode { notify.redraw = ctrl.redraw; const ground = ctrl.ground() || @@ -58,7 +58,7 @@ export function initModule() { ctrl.ground(ground); return h( - `main.puzzle.puzzle--nvui.puzzle-${ctrl.getData().replay ? 'replay' : 'play'}${ + `main.puzzle.puzzle--nvui.puzzle-${ctrl.data.replay ? 'replay' : 'play'}${ ctrl.streak ? '.puzzle--streak' : '' }`, h('div.nvui', [ @@ -140,7 +140,7 @@ export function initModule() { hook: onInsert(el => { const $board = $(el); const $buttons = $board.find('button'); - const steps = () => ctrl.getTree().getNodeList(ctrl.vm.path); + const steps = () => ctrl.tree.getNodeList(ctrl.vm.path); const uciSteps = () => steps().filter(hasUci); const fenSteps = () => steps().map(step => step.fen); const opponentColor = ctrl.vm.pov === 'white' ? 'black' : 'white'; @@ -196,7 +196,7 @@ export function initModule() { h('label', ['Piece prefix style', renderSetting(prefixStyle, ctrl.redraw)]), h('label', ['Show position', renderSetting(positionStyle, ctrl.redraw)]), h('label', ['Board layout', renderSetting(boardStyle, ctrl.redraw)]), - ...(!ctrl.getData().replay && !ctrl.streak + ...(!ctrl.data.replay && !ctrl.streak ? [h('h3', 'Puzzle Settings'), renderDifficultyForm(ctrl)] : []), h('h2', 'Keyboard shortcuts'), @@ -263,7 +263,7 @@ function hasUci(step: Tree.Node): step is StepWithUci { return step.uci !== undefined; } -function lastMove(ctrl: Controller, style: Style): string { +function lastMove(ctrl: PuzzleCtrl, style: Style): string { const node = ctrl.vm.node; if (node.ply === 0) return 'Initial position'; // make sure consecutive moves are different so that they get re-read @@ -271,7 +271,7 @@ function lastMove(ctrl: Controller, style: Style): string { } function onSubmit( - ctrl: Controller, + ctrl: PuzzleCtrl, notify: (txt: string) => void, style: () => Style, $input: Cash, @@ -304,11 +304,11 @@ function onSubmit( }; } -function isYourMove(ctrl: Controller) { +function isYourMove(ctrl: PuzzleCtrl) { return ctrl.vm.node.children.length === 0 || ctrl.vm.node.children[0].puzzle === 'fail'; } -function browseHint(ctrl: Controller): string[] { +function browseHint(ctrl: PuzzleCtrl): string[] { if (ctrl.vm.mode !== 'view' && !isYourMove(ctrl)) return ['You browsed away from the latest position.']; else return []; } @@ -319,7 +319,7 @@ function isShortCommand(input: string): boolean { return shortCommands.includes(input.split(' ')[0].toLowerCase()); } -function onCommand(ctrl: Controller, notify: (txt: string) => void, c: string, style: Style): void { +function onCommand(ctrl: PuzzleCtrl, notify: (txt: string) => void, c: string, style: Style): void { const lowered = c.toLowerCase(); const pieces = ctrl.ground()!.state.pieces; if (lowered === 'l' || lowered === 'last') notify($('.lastMove').text()); @@ -332,7 +332,7 @@ function onCommand(ctrl: Controller, notify: (txt: string) => void, c: string, s ); } -function viewOrAdvanceSolution(ctrl: Controller, notify: (txt: string) => void): void { +function viewOrAdvanceSolution(ctrl: PuzzleCtrl, notify: (txt: string) => void): void { if (ctrl.vm.mode === 'view') { const node = ctrl.vm.node, next = nextNode(node), @@ -359,26 +359,26 @@ function nextNode(node?: Tree.Node): Tree.Node | undefined { else return; } -function renderStreak(ctrl: Controller): VNode[] { +function renderStreak(ctrl: PuzzleCtrl): VNode[] { if (!ctrl.streak) return []; return [h('h2', 'Puzzle streak'), h('p', ctrl.streak.data.index || ctrl.trans.noarg('streakDescription'))]; } -function renderStatus(ctrl: Controller): string { +function renderStatus(ctrl: PuzzleCtrl): string { if (ctrl.vm.mode !== 'view') return 'Solving'; else if (ctrl.streak) return `GAME OVER. Your streak: ${ctrl.streak.data.index}`; else if (ctrl.vm.lastFeedback === 'win') return 'Puzzle solved!'; else return 'Puzzle complete.'; } -function renderReplay(ctrl: Controller): string { - const replay = ctrl.getData().replay; +function renderReplay(ctrl: PuzzleCtrl): string { + const replay = ctrl.data.replay; if (!replay) return ''; const i = replay.i + (ctrl.vm.mode === 'play' ? 0 : 1); - return `Replaying ${ctrl.trans.noarg(ctrl.getData().angle.key)} puzzles: ${i} of ${replay.of}`; + return `Replaying ${ctrl.trans.noarg(ctrl.data.angle.key)} puzzles: ${i} of ${replay.of}`; } -function playActions(ctrl: Controller): VNode { +function playActions(ctrl: PuzzleCtrl): VNode { if (ctrl.streak) return button( ctrl.trans.noarg('skip'), @@ -389,7 +389,7 @@ function playActions(ctrl: Controller): VNode { else return h('div.actions_play', button('View the solution', ctrl.viewSolution)); } -function afterActions(ctrl: Controller): VNode { +function afterActions(ctrl: PuzzleCtrl): VNode { const win = ctrl.vm.lastFeedback === 'win'; return h( 'div.actions_after', @@ -399,13 +399,13 @@ function afterActions(ctrl: Controller): VNode { ); } -const renderVoteTutorial = (ctrl: Controller): VNode[] => - ctrl.session.isNew() && ctrl.getData().user?.provisional +const renderVoteTutorial = (ctrl: PuzzleCtrl): VNode[] => + ctrl.session.isNew() && ctrl.data.user?.provisional ? [h('p', ctrl.trans.noarg('didYouLikeThisPuzzle')), h('p', ctrl.trans.noarg('voteToLoadNextOne'))] : []; -function renderVote(ctrl: Controller): VNode[] { - if (!ctrl.getData().user || ctrl.autoNexting()) return []; +function renderVote(ctrl: PuzzleCtrl): VNode[] { + if (!ctrl.data.user || ctrl.autoNexting()) return []; return [ ...renderVoteTutorial(ctrl), button('Thumbs up', () => ctrl.vote(true), undefined, ctrl.vm.voteDisabled), diff --git a/ui/puzzle/src/view/after.ts b/ui/puzzle/src/view/after.ts index 66efd770142bd..f7b05ea90c965 100644 --- a/ui/puzzle/src/view/after.ts +++ b/ui/puzzle/src/view/after.ts @@ -1,16 +1,16 @@ import * as licon from 'common/licon'; import { MaybeVNodes, bind, dataIcon } from 'common/snabbdom'; -import { Controller } from '../interfaces'; import { h, VNode } from 'snabbdom'; import * as router from 'common/router'; +import PuzzleCtrl from '../ctrl'; -const renderVote = (ctrl: Controller): VNode => +const renderVote = (ctrl: PuzzleCtrl): VNode => h( 'div.puzzle__vote', ctrl.autoNexting() ? [] : [ - ctrl.session.isNew() && ctrl.getData().user?.provisional + ctrl.session.isNew() && ctrl.data.user?.provisional ? h('div.puzzle__vote__help', [ h('p', ctrl.trans.noarg('didYouLikeThisPuzzle')), h('p', ctrl.trans.noarg('voteToLoadNextOne')), @@ -35,7 +35,7 @@ const renderVote = (ctrl: Controller): VNode => ], ); -const renderContinue = (ctrl: Controller) => +const renderContinue = (ctrl: PuzzleCtrl) => h( 'a.continue', { @@ -44,7 +44,7 @@ const renderContinue = (ctrl: Controller) => [h('i', { attrs: dataIcon(licon.PlayTriangle) }), ctrl.trans.noarg('continueTraining')], ); -const renderStreak = (ctrl: Controller): MaybeVNodes => [ +const renderStreak = (ctrl: PuzzleCtrl): MaybeVNodes => [ h('div.complete', [ h('span.game-over', 'GAME OVER'), h('span', ctrl.trans.vdom('yourStreakX', h('strong', ctrl.streak?.data.index))), @@ -58,8 +58,8 @@ const renderStreak = (ctrl: Controller): MaybeVNodes => [ ), ]; -export default function (ctrl: Controller): VNode { - const data = ctrl.getData(); +export default function (ctrl: PuzzleCtrl): VNode { + const data = ctrl.data; const win = ctrl.vm.lastFeedback == 'win'; return h( 'div.puzzle__feedback.after', diff --git a/ui/puzzle/src/view/boardMenu.ts b/ui/puzzle/src/view/boardMenu.ts index ee69dfb0bc53f..467474172d60a 100644 --- a/ui/puzzle/src/view/boardMenu.ts +++ b/ui/puzzle/src/view/boardMenu.ts @@ -1,9 +1,9 @@ import { h } from 'snabbdom'; import { menu as menuDropdown } from 'board/menu'; -import { Controller } from '../interfaces'; import { boolPrefXhrToggle } from 'common/controls'; +import PuzzleCtrl from '../ctrl'; -export default function (ctrl: Controller) { +export default function (ctrl: PuzzleCtrl) { return menuDropdown(ctrl.trans, ctrl.redraw, ctrl.menu, menu => [ h('section', [menu.flip(ctrl.trans.noarg('flipBoard'), ctrl.flipped(), ctrl.flip)]), h('section', [ diff --git a/ui/puzzle/src/view/chessground.ts b/ui/puzzle/src/view/chessground.ts index 3274e1f544e7c..16d4a6923a822 100644 --- a/ui/puzzle/src/view/chessground.ts +++ b/ui/puzzle/src/view/chessground.ts @@ -1,10 +1,10 @@ import resizeHandle from 'common/resize'; import { Config as CgConfig } from 'chessground/config'; -import { Controller } from '../interfaces'; import { h, VNode } from 'snabbdom'; import * as Prefs from 'common/prefs'; +import PuzzleCtrl from '../ctrl'; -export default function (ctrl: Controller): VNode { +export default function (ctrl: PuzzleCtrl): VNode { return h('div.cg-wrap', { hook: { insert: vnode => @@ -14,7 +14,7 @@ export default function (ctrl: Controller): VNode { }); } -export function makeConfig(ctrl: Controller): CgConfig { +export function makeConfig(ctrl: PuzzleCtrl): CgConfig { const opts = ctrl.makeCgOpts(); return { fen: opts.fen, diff --git a/ui/puzzle/src/view/feedback.ts b/ui/puzzle/src/view/feedback.ts index 0c75f5d65c609..7e8a921e41847 100644 --- a/ui/puzzle/src/view/feedback.ts +++ b/ui/puzzle/src/view/feedback.ts @@ -1,9 +1,9 @@ import { bind, MaybeVNode } from 'common/snabbdom'; import { h, VNode } from 'snabbdom'; -import { Controller } from '../interfaces'; import afterView from './after'; +import PuzzleCtrl from '../ctrl'; -const viewSolution = (ctrl: Controller): VNode => +const viewSolution = (ctrl: PuzzleCtrl): VNode => ctrl.streak ? h( 'div.view_solution.skip', @@ -39,7 +39,7 @@ const viewSolution = (ctrl: Controller): VNode => ], ); -const initial = (ctrl: Controller): VNode => +const initial = (ctrl: PuzzleCtrl): VNode => h('div.puzzle__feedback.play', [ h('div.player', [ h('div.no-square', h('piece.king.' + ctrl.vm.pov)), @@ -54,7 +54,7 @@ const initial = (ctrl: Controller): VNode => viewSolution(ctrl), ]); -const good = (ctrl: Controller): VNode => +const good = (ctrl: PuzzleCtrl): VNode => h('div.puzzle__feedback.good', [ h('div.player', [ h('div.icon', '✓'), @@ -66,7 +66,7 @@ const good = (ctrl: Controller): VNode => viewSolution(ctrl), ]); -const fail = (ctrl: Controller): VNode => +const fail = (ctrl: PuzzleCtrl): VNode => h('div.puzzle__feedback.fail', [ h('div.player', [ h('div.icon', '✗'), @@ -78,7 +78,7 @@ const fail = (ctrl: Controller): VNode => viewSolution(ctrl), ]); -export default function (ctrl: Controller): MaybeVNode { +export default function (ctrl: PuzzleCtrl): MaybeVNode { if (ctrl.vm.mode === 'view') return afterView(ctrl); switch (ctrl.vm.lastFeedback) { case 'init': diff --git a/ui/puzzle/src/view/main.ts b/ui/puzzle/src/view/main.ts index 54f4ef4a14418..971ab2ddf3501 100644 --- a/ui/puzzle/src/view/main.ts +++ b/ui/puzzle/src/view/main.ts @@ -6,7 +6,6 @@ import chessground from './chessground'; import feedbackView from './feedback'; import * as licon from 'common/licon'; import { stepwiseScroll } from 'common/scroll'; -import { Controller } from '../interfaces'; import { h, VNode } from 'snabbdom'; import { onInsert, bindNonPassive } from 'common/snabbdom'; import { bindMobileMousedown } from 'common/device'; @@ -16,10 +15,10 @@ import { renderVoiceBar } from 'voice'; import { render as renderKeyboardMove } from 'keyboardMove'; import { toggleButton as boardMenuToggleButton } from 'board/menu'; import boardMenu from './boardMenu'; - import * as Prefs from 'common/prefs'; +import PuzzleCtrl from '../ctrl'; -const renderAnalyse = (ctrl: Controller): VNode => h('div.puzzle__moves.areplay', [treeView(ctrl)]); +const renderAnalyse = (ctrl: PuzzleCtrl): VNode => h('div.puzzle__moves.areplay', [treeView(ctrl)]); function dataAct(e: Event): string | null { const target = e.target as HTMLElement; @@ -36,7 +35,7 @@ function jumpButton(icon: string, effect: string, disabled: boolean, glowing = f }); } -function controls(ctrl: Controller): VNode { +function controls(ctrl: PuzzleCtrl): VNode { const node = ctrl.vm.node; const nextNode = node.children[0]; const notOnLastMove = ctrl.vm.mode == 'play' && nextNode && nextNode.puzzle != 'fail'; @@ -68,7 +67,7 @@ function controls(ctrl: Controller): VNode { let cevalShown = false; -export default function (ctrl: Controller): VNode { +export default function (ctrl: PuzzleCtrl): VNode { if (ctrl.nvui) return ctrl.nvui.render(ctrl); const showCeval = ctrl.vm.showComputer(), gaugeOn = ctrl.showEvalGauge(); @@ -77,7 +76,7 @@ export default function (ctrl: Controller): VNode { cevalShown = showCeval; } return h( - `main.puzzle.puzzle-${ctrl.getData().replay ? 'replay' : 'play'}${ctrl.streak ? '.puzzle--streak' : ''}`, + `main.puzzle.puzzle-${ctrl.data.replay ? 'replay' : 'play'}${ctrl.streak ? '.puzzle--streak' : ''}`, { class: { 'gauge-on': gaugeOn }, hook: { @@ -148,13 +147,13 @@ export default function (ctrl: Controller): VNode { ); } -function session(ctrl: Controller) { +function session(ctrl: PuzzleCtrl) { const rounds = ctrl.session.get().rounds, - current = ctrl.getData().puzzle.id; + current = ctrl.data.puzzle.id; return h('div.puzzle__session', [ ...rounds.map(round => { const rd = - round.ratingDiff && ctrl.showRatings + round.ratingDiff && ctrl.opts.showRatings ? round.ratingDiff > 0 ? '+' + round.ratingDiff : round.ratingDiff diff --git a/ui/puzzle/src/view/side.ts b/ui/puzzle/src/view/side.ts index ef981ab4485d4..cc9c2014c36af 100644 --- a/ui/puzzle/src/view/side.ts +++ b/ui/puzzle/src/view/side.ts @@ -1,4 +1,4 @@ -import { Controller, Puzzle, PuzzleGame, PuzzleDifficulty } from '../interfaces'; +import { Puzzle, PuzzleGame, PuzzleDifficulty } from '../interfaces'; import * as licon from 'common/licon'; import { dataIcon, onInsert, MaybeVNode } from 'common/snabbdom'; import { h, VNode } from 'snabbdom'; @@ -7,27 +7,28 @@ import perfIcons from 'common/perfIcons'; import * as router from 'common/router'; import { userLink } from 'common/userLink'; import PuzzleStreak from '../streak'; +import PuzzleCtrl from '../ctrl'; -export function puzzleBox(ctrl: Controller): VNode { - const data = ctrl.getData(); +export function puzzleBox(ctrl: PuzzleCtrl): VNode { + const data = ctrl.data; return h('div.puzzle__side__metas', [ puzzleInfos(ctrl, data.puzzle), gameInfos(ctrl, data.game, data.puzzle), ]); } -const angleImg = (ctrl: Controller): string => { - const angle = ctrl.getData().angle; +const angleImg = (ctrl: PuzzleCtrl): string => { + const angle = ctrl.data.angle; const name = angle.opening ? 'opening' : angle.key.startsWith('mateIn') ? 'mate' : angle.key; return lichess.asset.url(`images/puzzle-themes/${name}.svg`); }; -const puzzleInfos = (ctrl: Controller, puzzle: Puzzle): VNode => +const puzzleInfos = (ctrl: PuzzleCtrl, puzzle: Puzzle): VNode => h('div.infos.puzzle', [ h('img.infos__angle-img', { attrs: { src: angleImg(ctrl), - alt: ctrl.getData().angle.name, + alt: ctrl.data.angle.name, }, }), h('div', [ @@ -49,7 +50,7 @@ const puzzleInfos = (ctrl: Controller, puzzle: Puzzle): VNode => ), ), ), - ctrl.showRatings + ctrl.opts.showRatings ? h( 'p', ctrl.trans.vdom( @@ -64,7 +65,7 @@ const puzzleInfos = (ctrl: Controller, puzzle: Puzzle): VNode => ]), ]); -function gameInfos(ctrl: Controller, game: PuzzleGame, puzzle: Puzzle): VNode { +function gameInfos(ctrl: PuzzleCtrl, game: PuzzleGame, puzzle: Puzzle): VNode { const gameName = `${game.clock} • ${game.perf.name}`; return h('div.infos', { attrs: dataIcon(perfIcons[game.perf.key]) }, [ h('div', [ @@ -88,7 +89,7 @@ function gameInfos(ctrl: Controller, game: PuzzleGame, puzzle: Puzzle): VNode { game.players.map(p => { const user = { ...p, - rating: ctrl.showRatings ? p.rating : undefined, + rating: ctrl.opts.showRatings ? p.rating : undefined, line: false, }; return h('div.player.color-icon.is.text.' + p.color, userLink(user)); @@ -121,8 +122,8 @@ const renderStreak = (streak: PuzzleStreak, noarg: TransNoArg) => ), ); -export const userBox = (ctrl: Controller): VNode => { - const data = ctrl.getData(), +export const userBox = (ctrl: PuzzleCtrl): VNode => { + const data = ctrl.data, noarg = ctrl.trans.noarg; if (!data.user) return h('div.puzzle__side__user', [ @@ -153,7 +154,7 @@ export const userBox = (ctrl: Controller): VNode => { h( 'div.puzzle__side__user__rating', ctrl.rated() - ? ctrl.showRatings + ? ctrl.opts.showRatings ? h('strong', [ data.user.rating - (diff || 0), ...(diff && diff > 0 ? [' ', h('good.rp', '+' + diff)] : []), @@ -165,7 +166,7 @@ export const userBox = (ctrl: Controller): VNode => { ]); }; -export const streakBox = (ctrl: Controller) => +export const streakBox = (ctrl: PuzzleCtrl) => h('div.puzzle__side__user', renderStreak(ctrl.streak!, ctrl.trans.noarg)); const difficulties: [PuzzleDifficulty, number][] = [ @@ -181,8 +182,8 @@ const colors = [ ['white', 'asWhite'], ]; -export function replay(ctrl: Controller): MaybeVNode { - const replay = ctrl.getData().replay; +export function replay(ctrl: PuzzleCtrl): MaybeVNode { + const replay = ctrl.data.replay; if (!replay) return; const i = replay.i + (ctrl.vm.mode == 'play' ? 0 : 1); return h('div.puzzle__side__replay', [ @@ -193,7 +194,7 @@ export function replay(ctrl: Controller): MaybeVNode { href: `/training/dashboard/${replay.days}`, }, }, - ['« ', `Replaying ${ctrl.trans.noarg(ctrl.getData().angle.key)} puzzles`], + ['« ', `Replaying ${ctrl.trans.noarg(ctrl.data.angle.key)} puzzles`], ), h('div.puzzle__side__replay__bar', { attrs: { @@ -204,10 +205,10 @@ export function replay(ctrl: Controller): MaybeVNode { ]); } -export function config(ctrl: Controller): MaybeVNode { +export function config(ctrl: PuzzleCtrl): MaybeVNode { const autoNextId = 'puzzle-toggle-autonext', noarg = ctrl.trans.noarg, - data = ctrl.getData(); + data = ctrl.data; return h('div.puzzle__side__config', [ h('div.puzzle__side__config__toggle', [ h('div.switch', [ @@ -234,12 +235,12 @@ export function config(ctrl: Controller): MaybeVNode { ]); } -export const renderDifficultyForm = (ctrl: Controller): VNode => +export const renderDifficultyForm = (ctrl: PuzzleCtrl): VNode => h( 'form.puzzle__side__config__difficulty', { attrs: { - action: `/training/difficulty/${ctrl.getData().angle.key}`, + action: `/training/difficulty/${ctrl.data.angle.key}`, method: 'post', }, }, @@ -265,7 +266,7 @@ export const renderDifficultyForm = (ctrl: Controller): VNode => { attrs: { value: key, - selected: key == ctrl.settings.difficulty, + selected: key == ctrl.opts.settings.difficulty, title: !!delta && ctrl.trans.pluralSame( @@ -281,7 +282,7 @@ export const renderDifficultyForm = (ctrl: Controller): VNode => ], ); -export const renderColorForm = (ctrl: Controller): VNode => +export const renderColorForm = (ctrl: PuzzleCtrl): VNode => h( 'div.puzzle__side__config__color', h( @@ -289,10 +290,10 @@ export const renderColorForm = (ctrl: Controller): VNode => colors.map(([key, i18n]) => h('div', [ h( - `a.label.color-${key}${key === (ctrl.settings.color || 'random') ? '.active' : ''}`, + `a.label.color-${key}${key === (ctrl.opts.settings.color || 'random') ? '.active' : ''}`, { attrs: { - href: `/training/${ctrl.getData().angle.key}/${key}`, + href: `/training/${ctrl.data.angle.key}/${key}`, title: ctrl.trans.noarg(i18n), }, }, diff --git a/ui/puzzle/src/view/theme.ts b/ui/puzzle/src/view/theme.ts index 60caaad1f53ab..80687e70e966b 100644 --- a/ui/puzzle/src/view/theme.ts +++ b/ui/puzzle/src/view/theme.ts @@ -1,14 +1,14 @@ import * as licon from 'common/licon'; import * as router from 'common/router'; import { MaybeVNode, bind, dataIcon } from 'common/snabbdom'; -import { Controller } from '../interfaces'; import { h, VNode } from 'snabbdom'; import { renderColorForm } from './side'; +import PuzzleCtrl from '../ctrl'; const studyUrl = 'https://lichess.org/study/viiWlKjv'; -export default function theme(ctrl: Controller): MaybeVNode { - const data = ctrl.getData(), +export default function theme(ctrl: PuzzleCtrl): MaybeVNode { + const data = ctrl.data, angle = data.angle; const showEditor = ctrl.vm.mode == 'view' && !ctrl.autoNexting(); if (data.replay) return showEditor ? h('div.puzzle__side__theme', editor(ctrl)) : null; @@ -63,8 +63,8 @@ export default function theme(ctrl: Controller): MaybeVNode { const invisibleThemes = new Set(['master', 'masterVsMaster', 'superGM']); -const editor = (ctrl: Controller): VNode[] => { - const data = ctrl.getData(), +const editor = (ctrl: PuzzleCtrl): VNode[] => { + const data = ctrl.data, trans = ctrl.trans.noarg, votedThemes = ctrl.vm.round?.themes || {}; const visibleThemes: string[] = data.puzzle.themes diff --git a/ui/puzzle/src/view/tree.ts b/ui/puzzle/src/view/tree.ts index 1f5a5be8023af..fcfa488e62ef0 100644 --- a/ui/puzzle/src/view/tree.ts +++ b/ui/puzzle/src/view/tree.ts @@ -3,11 +3,11 @@ import { defined } from 'common'; import throttle from 'common/throttle'; import { renderEval as normalizeEval } from 'ceval'; import { path as treePath } from 'tree'; -import { Controller } from '../interfaces'; import { MaybeVNode, MaybeVNodes } from 'common/snabbdom'; +import PuzzleCtrl from '../ctrl'; interface Ctx { - ctrl: Controller; + ctrl: PuzzleCtrl; } interface RenderOpts { @@ -21,7 +21,7 @@ interface Glyph { symbol: string; } -const autoScroll = throttle(150, (ctrl: Controller, el) => { +const autoScroll = throttle(150, (ctrl: PuzzleCtrl, el) => { const cont = el.parentNode; const target = el.querySelector('.active'); if (!target) { @@ -213,8 +213,8 @@ function eventPath(e: Event): Tree.Path | null { return target.getAttribute('p') || (target.parentNode as HTMLElement).getAttribute('p'); } -export function render(ctrl: Controller): VNode { - const root = ctrl.getTree().root; +export function render(ctrl: PuzzleCtrl): VNode { + const root = ctrl.tree.root; const ctx = { ctrl: ctrl, showComputer: false, @@ -225,7 +225,7 @@ export function render(ctrl: Controller): VNode { hook: { insert: vnode => { const el = vnode.elm as HTMLElement; - if (ctrl.path !== treePath.root) autoScroll(ctrl, el); + if (ctrl.vm.path !== treePath.root) autoScroll(ctrl, el); el.addEventListener('mousedown', (e: MouseEvent) => { if (defined(e.button) && e.button !== 0) return; // only touch or left click const path = eventPath(e); @@ -237,7 +237,7 @@ export function render(ctrl: Controller): VNode { if (ctrl.vm.autoScrollNow) { autoScroll(ctrl, vnode.elm as HTMLElement); ctrl.vm.autoScrollNow = false; - ctrl.autoScrollRequested = false; + ctrl.vm.autoScrollRequested = false; } else if (ctrl.vm.autoScrollRequested) { if (ctrl.vm.path !== treePath.root) autoScroll(ctrl, vnode.elm as HTMLElement); ctrl.vm.autoScrollRequested = false; From 9e75ca9a78a1a29e7433ef2375e0b112125fc169 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Wed, 20 Dec 2023 11:56:10 +0100 Subject: [PATCH 2/5] fix remaining merge conflicts from ui TS reformat --- ui/puzzle/src/view/after.ts | 2 +- ui/puzzle/src/view/main.ts | 12 ++++++------ ui/puzzle/src/view/side.ts | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ui/puzzle/src/view/after.ts b/ui/puzzle/src/view/after.ts index 633e1e5398326..b447d0cdba1db 100644 --- a/ui/puzzle/src/view/after.ts +++ b/ui/puzzle/src/view/after.ts @@ -10,7 +10,7 @@ const renderVote = (ctrl: PuzzleCtrl): VNode => {}, !ctrl.autoNexting() && [ ctrl.session.isNew() && - ctrl.getData().user?.provisional && + ctrl.data.user?.provisional && h('div.puzzle__vote__help', [ h('p', ctrl.trans.noarg('didYouLikeThisPuzzle')), h('p', ctrl.trans.noarg('voteToLoadNextOne')), diff --git a/ui/puzzle/src/view/main.ts b/ui/puzzle/src/view/main.ts index 6afe6913b56ea..d6cabbd3262c3 100644 --- a/ui/puzzle/src/view/main.ts +++ b/ui/puzzle/src/view/main.ts @@ -15,10 +15,10 @@ import { renderVoiceBar } from 'voice'; import { render as renderKeyboardMove } from 'keyboardMove'; import { toggleButton as boardMenuToggleButton } from 'board/menu'; import boardMenu from './boardMenu'; - import * as Prefs from 'common/prefs'; +import PuzzleCtrl from '../ctrl'; -const renderAnalyse = (ctrl: Controller): VNode => h('div.puzzle__moves.areplay', [treeView(ctrl)]); +const renderAnalyse = (ctrl: PuzzleCtrl): VNode => h('div.puzzle__moves.areplay', [treeView(ctrl)]); function dataAct(e: Event): string | null { const target = e.target as HTMLElement; @@ -29,7 +29,7 @@ function jumpButton(icon: string, effect: string, disabled: boolean, glowing = f return h('button.fbt', { class: { disabled, glowing }, attrs: { 'data-act': effect, 'data-icon': icon } }); } -function controls(ctrl: Controller): VNode { +function controls(ctrl: PuzzleCtrl): VNode { const node = ctrl.vm.node; const nextNode = node.children[0]; const notOnLastMove = ctrl.vm.mode == 'play' && nextNode && nextNode.puzzle != 'fail'; @@ -61,7 +61,7 @@ function controls(ctrl: Controller): VNode { let cevalShown = false; -export default function (ctrl: Controller): VNode { +export default function (ctrl: PuzzleCtrl): VNode { if (ctrl.nvui) return ctrl.nvui.render(ctrl); const showCeval = ctrl.vm.showComputer(), gaugeOn = ctrl.showEvalGauge(); @@ -139,9 +139,9 @@ export default function (ctrl: Controller): VNode { ); } -function session(ctrl: Controller) { +function session(ctrl: PuzzleCtrl) { const rounds = ctrl.session.get().rounds, - current = ctrl.getData().puzzle.id; + current = ctrl.data.puzzle.id; return h('div.puzzle__session', [ ...rounds.map(round => { const rd = diff --git a/ui/puzzle/src/view/side.ts b/ui/puzzle/src/view/side.ts index 92c77993dc105..a52352bd633e4 100644 --- a/ui/puzzle/src/view/side.ts +++ b/ui/puzzle/src/view/side.ts @@ -25,7 +25,7 @@ const angleImg = (ctrl: PuzzleCtrl): string => { const puzzleInfos = (ctrl: PuzzleCtrl, puzzle: Puzzle): VNode => h('div.infos.puzzle', [ - h('img.infos__angle-img', { attrs: { src: angleImg(ctrl), alt: ctrl.getData().angle.name } }), + h('img.infos__angle-img', { attrs: { src: angleImg(ctrl), alt: ctrl.data.angle.name } }), h('div', [ h( 'p', @@ -75,7 +75,7 @@ function gameInfos(ctrl: PuzzleCtrl, game: PuzzleGame, puzzle: Puzzle): VNode { h( 'div.players', game.players.map(p => { - const user = { ...p, rating: ctrl.showRatings ? p.rating : undefined, line: false }; + const user = { ...p, rating: ctrl.opts.showRatings ? p.rating : undefined, line: false }; return h('div.player.color-icon.is.text.' + p.color, userLink(user)); }), ), @@ -161,7 +161,7 @@ export function replay(ctrl: PuzzleCtrl): MaybeVNode { return h('div.puzzle__side__replay', [ h('a', { attrs: { href: `/training/dashboard/${replay.days}` } }, [ '« ', - `Replaying ${ctrl.trans.noarg(ctrl.getData().angle.key)} puzzles`, + `Replaying ${ctrl.trans.noarg(ctrl.data.angle.key)} puzzles`, ]), h('div.puzzle__side__replay__bar', { attrs: { @@ -202,7 +202,7 @@ export function config(ctrl: PuzzleCtrl): MaybeVNode { export const renderDifficultyForm = (ctrl: PuzzleCtrl): VNode => h( 'form.puzzle__side__config__difficulty', - { attrs: { action: `/training/difficulty/${ctrl.getData().angle.key}`, method: 'post' } }, + { attrs: { action: `/training/difficulty/${ctrl.data.angle.key}`, method: 'post' } }, [ h('label', { attrs: { for: 'puzzle-difficulty' } }, ctrl.trans.noarg('difficultyLevel')), h( From d77ae46c1c2930c03fbcfcfaef7eefbde77d383d Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Wed, 20 Dec 2023 12:01:48 +0100 Subject: [PATCH 3/5] make sure snabbdom kid filter function is only instanciated once probaly superfluous? who knows. Certainly a hot path though --- ui/common/src/snabbdom.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ui/common/src/snabbdom.ts b/ui/common/src/snabbdom.ts index ead7974af763e..746a199fac3d0 100644 --- a/ui/common/src/snabbdom.ts +++ b/ui/common/src/snabbdom.ts @@ -42,11 +42,11 @@ export type LooseVNodes = (MaybeVNode | boolean)[]; type LooseVNode = VNodeChildElement | boolean; type VNodeKids = LooseVNode | LooseVNode[]; -function filterKids(children: VNodeKids): VNodeChildElement[] { - return (Array.isArray(children) ? children : [children]).filter( - x => (x && x !== true) || x === '', // '' may be falsy but it's a valid VNode - ) as VNodeChildElement[]; -} +// '' may be falsy but it's a valid VNode +const kidFilter = (x: any): boolean => (x && x !== true) || x === ''; + +const filterKids = (children: VNodeKids): VNodeChildElement[] => + (Array.isArray(children) ? children : [children]).filter(kidFilter) as VNodeChildElement[]; /* obviate need for some ternary expressions in renders. Allows looseH('div', [ kids && h('div', 'kid') ]) From 92dd8521d0b69d2f848b9b9efaee3005be4e1caf Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Wed, 20 Dec 2023 12:39:13 +0100 Subject: [PATCH 4/5] remove ui/puzzle ctrl.vm --- ui/puzzle/src/autoShape.ts | 18 +-- ui/puzzle/src/control.ts | 16 +- ui/puzzle/src/ctrl.ts | 250 +++++++++++++++--------------- ui/puzzle/src/interfaces.ts | 27 ---- ui/puzzle/src/keyboard.ts | 2 +- ui/puzzle/src/moveTest.ts | 31 ++-- ui/puzzle/src/plugins/nvui.ts | 44 +++--- ui/puzzle/src/view/after.ts | 6 +- ui/puzzle/src/view/chessground.ts | 2 +- ui/puzzle/src/view/feedback.ts | 10 +- ui/puzzle/src/view/main.ts | 8 +- ui/puzzle/src/view/side.ts | 16 +- ui/puzzle/src/view/theme.ts | 6 +- ui/puzzle/src/view/tree.ts | 26 ++-- 14 files changed, 220 insertions(+), 242 deletions(-) diff --git a/ui/puzzle/src/autoShape.ts b/ui/puzzle/src/autoShape.ts index 8b018dc2977c8..5e98eca04c4c1 100644 --- a/ui/puzzle/src/autoShape.ts +++ b/ui/puzzle/src/autoShape.ts @@ -1,16 +1,16 @@ import { winningChances, CevalCtrl } from 'ceval'; import { DrawModifiers, DrawShape } from 'chessground/draw'; -import { Vm } from './interfaces'; import { Api as CgApi } from 'chessground/api'; import { opposite, parseUci, makeSquare } from 'chessops/util'; import { NormalMove } from 'chessops/types'; interface Opts { - vm: Vm; + node: Tree.Node; + showComputer(): boolean; ceval: CevalCtrl; ground: CgApi; - nextNodeBest?: Uci; - threatMode: boolean; + nextNodeBest(): Uci | undefined; + threatMode(): boolean; } function makeAutoShapesFromUci( @@ -30,16 +30,16 @@ function makeAutoShapesFromUci( } export default function (opts: Opts): DrawShape[] { - const n = opts.vm.node, + const n = opts.node, hovering = opts.ceval.hovering(), color = n.fen.includes(' w ') ? 'white' : 'black'; let shapes: DrawShape[] = []; if (hovering && hovering.fen === n.fen) shapes = shapes.concat(makeAutoShapesFromUci(color, hovering.uci, 'paleBlue')); - if (opts.vm.showAutoShapes() && opts.vm.showComputer()) { + if (opts.showComputer()) { if (n.eval) shapes = shapes.concat(makeAutoShapesFromUci(color, n.eval.best!, 'paleGreen')); if (!hovering) { - let nextBest: Uci | undefined = opts.nextNodeBest; + let nextBest: Uci | undefined = opts.nextNodeBest(); if (!nextBest && opts.ceval.enabled() && n.ceval) nextBest = n.ceval.pvs[0].moves[0]; if (nextBest) shapes = shapes.concat(makeAutoShapesFromUci(color, nextBest, 'paleBlue')); if ( @@ -47,7 +47,7 @@ export default function (opts: Opts): DrawShape[] { n.ceval && n.ceval.pvs && n.ceval.pvs[1] && - !(opts.threatMode && n.threat && n.threat.pvs[2]) + !(opts.threatMode() && n.threat && n.threat.pvs[2]) ) { n.ceval.pvs.forEach(function (pv) { if (pv.moves[0] === nextBest) return; @@ -62,7 +62,7 @@ export default function (opts: Opts): DrawShape[] { } } } - if (opts.ceval.enabled() && opts.threatMode && n.threat) { + if (opts.ceval.enabled() && opts.threatMode() && n.threat) { if (n.threat.pvs[1]) { shapes = shapes.concat(makeAutoShapesFromUci(opposite(color), n.threat.pvs[0].moves[0], 'paleRed')); n.threat.pvs.slice(1).forEach(function (pv) { diff --git a/ui/puzzle/src/control.ts b/ui/puzzle/src/control.ts index ae4278edd1420..36578220460f2 100644 --- a/ui/puzzle/src/control.ts +++ b/ui/puzzle/src/control.ts @@ -2,25 +2,25 @@ import { path as treePath } from 'tree'; import PuzzleCtrl from './ctrl'; export function canGoForward(ctrl: PuzzleCtrl): boolean { - return ctrl.vm.node.children.length > 0; + return ctrl.node.children.length > 0; } export function next(ctrl: PuzzleCtrl): void { - const child = ctrl.vm.node.children[0]; + const child = ctrl.node.children[0]; if (!child) return; - ctrl.userJump(ctrl.vm.path + child.id); + ctrl.userJump(ctrl.path + child.id); } export function prev(ctrl: PuzzleCtrl): void { - ctrl.userJump(treePath.init(ctrl.vm.path)); + ctrl.userJump(treePath.init(ctrl.path)); } export function last(ctrl: PuzzleCtrl): void { - const toInit = !treePath.contains(ctrl.vm.path, ctrl.vm.initialPath); - ctrl.userJump(toInit ? ctrl.vm.initialPath : treePath.fromNodeList(ctrl.vm.mainline)); + const toInit = !treePath.contains(ctrl.path, ctrl.initialPath); + ctrl.userJump(toInit ? ctrl.initialPath : treePath.fromNodeList(ctrl.mainline)); } export function first(ctrl: PuzzleCtrl): void { - const toInit = ctrl.vm.path !== ctrl.vm.initialPath && treePath.contains(ctrl.vm.path, ctrl.vm.initialPath); - ctrl.userJump(toInit ? ctrl.vm.initialPath : treePath.root); + const toInit = ctrl.path !== ctrl.initialPath && treePath.contains(ctrl.path, ctrl.initialPath); + ctrl.userJump(toInit ? ctrl.initialPath : treePath.root); } diff --git a/ui/puzzle/src/ctrl.ts b/ui/puzzle/src/ctrl.ts index b10c955297be5..ae1ace5f09bb2 100644 --- a/ui/puzzle/src/ctrl.ts +++ b/ui/puzzle/src/ctrl.ts @@ -6,7 +6,7 @@ import moveTest from './moveTest'; import PuzzleSession from './session'; import PuzzleStreak from './streak'; import throttle from 'common/throttle'; -import { Vm, PuzzleOpts, PuzzleData, MoveTest, ThemeKey, ReplayEnd, NvuiPlugin } from './interfaces'; +import { PuzzleOpts, PuzzleData, MoveTest, ThemeKey, ReplayEnd, NvuiPlugin, PuzzleRound } from './interfaces'; import { Api as CgApi } from 'chessground/api'; import { build as treeBuild, ops as treeOps, path as treePath, TreeWrapper } from 'tree'; import { Chess, normalizeMove } from 'chessops/chess'; @@ -15,7 +15,7 @@ import { Config as CgConfig } from 'chessground/config'; import { CevalCtrl } from 'ceval'; import { makeVoiceMove, VoiceMove, RootCtrl as VoiceRoot } from 'voice'; import { ctrl as makeKeyboardMove, KeyboardMove, RootController as KeyboardRoot } from 'keyboardMove'; -import { defer } from 'common/defer'; +import { Deferred, defer } from 'common/defer'; import { defined, prop, Prop, propWithEffect, Toggle, toggle } from 'common'; import { makeSanAndPlay } from 'chessops/san'; import { parseFen, makeFen } from 'chessops/fen'; @@ -31,12 +31,8 @@ import { Redraw } from 'common/snabbdom'; import { ParentCtrl } from 'ceval/src/types'; export default class PuzzleCtrl implements ParentCtrl { - vm: Vm = { - next: defer(), - showAutoShapes: () => true, - } as Vm; - data: PuzzleData; + next: Deferred = defer(); trans: Trans; tree: TreeWrapper; ceval: CevalCtrl; @@ -53,6 +49,24 @@ export default class PuzzleCtrl implements ParentCtrl { voiceMove?: VoiceMove; promotion: PromotionCtrl; keyboardHelp: Prop; + cgConfig?: CgConfig; + path: Tree.Path; + node: Tree.Node; + nodeList: Tree.Node[]; + mainline: Tree.Node[]; + pov: Color; + mode: 'play' | 'view' | 'try'; + round?: PuzzleRound; + justPlayed?: Key; + resultSent: boolean; + lastFeedback: 'init' | 'fail' | 'win' | 'good' | 'retry'; + initialPath: Tree.Path; + initialNode: Tree.Node; + canViewSolution = toggle(false); + autoScrollRequested: boolean; + autoScrollNow: boolean; + voteDisabled?: boolean; + isDaily: boolean; constructor( readonly opts: PuzzleOpts, @@ -76,7 +90,7 @@ export default class PuzzleCtrl implements ParentCtrl { this.initiate(opts.data); this.promotion = new PromotionCtrl( this.withGround, - () => this.withGround(g => g.set(this.vm.cgConfig)), + () => this.withGround(g => g.set(this.cgConfig!)), redraw, ); @@ -87,7 +101,7 @@ export default class PuzzleCtrl implements ParentCtrl { // chessground is not displayed, and the first move is not fully applied. // Make sure chessground is fully shown when the page goes back to being visible. document.addEventListener('visibilitychange', () => - lichess.requestIdleCallback(() => this.jump(this.vm.path), 500), + lichess.requestIdleCallback(() => this.jump(this.path), 500), ); lichess.pubsub.on('zen', () => { @@ -109,10 +123,10 @@ export default class PuzzleCtrl implements ParentCtrl { }; setPath = (path: Tree.Path): void => { - this.vm.path = path; - this.vm.nodeList = this.tree.getNodeList(path); - this.vm.node = treeOps.last(this.vm.nodeList)!; - this.vm.mainline = treeOps.mainlineNodeList(this.tree.root); + this.path = path; + this.nodeList = this.tree.getNodeList(path); + this.node = treeOps.last(this.nodeList)!; + this.mainline = treeOps.mainlineNodeList(this.tree.root); }; setChessground = (cg: CgApi): void => { @@ -120,7 +134,7 @@ export default class PuzzleCtrl implements ParentCtrl { const makeRoot = () => ({ data: { game: { variant: { key: 'standard' } }, - player: { color: this.vm.pov }, + player: { color: this.pov }, }, chessground: cg, sendMove: this.playUserMove, @@ -132,11 +146,9 @@ export default class PuzzleCtrl implements ParentCtrl { vote: this.vote, solve: this.viewSolution, }); - if (this.opts.pref.voiceMove) this.voiceMove = makeVoiceMove(makeRoot() as VoiceRoot, this.vm.node.fen); + if (this.opts.pref.voiceMove) this.voiceMove = makeVoiceMove(makeRoot() as VoiceRoot, this.node.fen); if (this.opts.pref.keyboardMove) - this.keyboardMove = makeKeyboardMove(makeRoot() as KeyboardRoot, { - fen: this.vm.node.fen, - }); + this.keyboardMove = makeKeyboardMove(makeRoot() as KeyboardRoot, { fen: this.node.fen }); requestAnimationFrame(() => this.redraw()); }; @@ -151,16 +163,16 @@ export default class PuzzleCtrl implements ParentCtrl { this.data = fromData; this.tree = treeBuild(pgnToTree(this.data.game.pgn.split(' '))); const initialPath = treePath.fromNodeList(treeOps.mainlineNodeList(this.tree.root)); - this.vm.mode = 'play'; - this.vm.next = defer(); - this.vm.round = undefined; - this.vm.justPlayed = undefined; - this.vm.resultSent = false; - this.vm.lastFeedback = 'init'; - this.vm.initialPath = initialPath; - this.vm.initialNode = this.tree.nodeAtPath(initialPath); - this.vm.pov = this.vm.initialNode.ply % 2 == 1 ? 'black' : 'white'; - this.vm.isDaily = location.href.endsWith('/daily'); + this.mode = 'play'; + this.next = defer(); + this.round = undefined; + this.justPlayed = undefined; + this.resultSent = false; + this.lastFeedback = 'init'; + this.initialPath = initialPath; + this.initialNode = this.tree.nodeAtPath(initialPath); + this.pov = this.initialNode.ply % 2 == 1 ? 'black' : 'white'; + this.isDaily = location.href.endsWith('/daily'); this.setPath(lichess.blindMode ? initialPath : treePath.init(initialPath)); setTimeout( @@ -172,16 +184,13 @@ export default class PuzzleCtrl implements ParentCtrl { ); // just to delay button display - this.vm.canViewSolution = false; - if (!this.vm.canViewSolution) { - setTimeout( - () => { - this.vm.canViewSolution = true; - this.redraw(); - }, - this.rated() ? 4000 : 1000, - ); - } + setTimeout( + () => { + this.canViewSolution(true); + this.redraw(); + }, + this.rated() ? 4000 : 1000, + ); this.withGround(g => { g.selectSquare(null); @@ -194,17 +203,16 @@ export default class PuzzleCtrl implements ParentCtrl { }; position = (): Chess => { - const setup = parseFen(this.vm.node.fen).unwrap(); + const setup = parseFen(this.node.fen).unwrap(); return Chess.fromSetup(setup).unwrap(); }; makeCgOpts = (): CgConfig => { - const node = this.vm.node; + const node = this.node; const color: Color = node.ply % 2 === 0 ? 'white' : 'black'; const dests = chessgroundDests(this.position()); - const nextNode = this.vm.node.children[0]; - const canMove = - this.vm.mode === 'view' || (color === this.vm.pov && (!nextNode || nextNode.puzzle == 'fail')); + const nextNode = this.node.children[0]; + const canMove = this.mode === 'view' || (color === this.pov && (!nextNode || nextNode.puzzle == 'fail')); const movable = canMove ? { color: dests.size > 0 ? color : undefined, @@ -216,7 +224,7 @@ export default class PuzzleCtrl implements ParentCtrl { }; const config = { fen: node.fen, - orientation: this.flipped() ? opposite(this.vm.pov) : this.vm.pov, + orientation: this.flipped() ? opposite(this.pov) : this.pov, turnColor: color, movable: movable, premovable: { @@ -225,13 +233,13 @@ export default class PuzzleCtrl implements ParentCtrl { check: !!node.check, lastMove: uciToMove(node.uci), }; - if (node.ply >= this.vm.initialNode.ply) { - if (this.vm.mode !== 'view' && color !== this.vm.pov && !nextNode) { - config.movable.color = this.vm.pov; + if (node.ply >= this.initialNode.ply) { + if (this.mode !== 'view' && color !== this.pov && !nextNode) { + config.movable.color = this.pov; config.premovable.enabled = true; } } - this.vm.cgConfig = config; + this.cgConfig = config; return config; }; @@ -248,13 +256,13 @@ export default class PuzzleCtrl implements ParentCtrl { }; userMove = (orig: Key, dest: Key): void => { - this.vm.justPlayed = orig; + this.justPlayed = orig; if ( !this.promotion.start(orig, dest, { submit: this.playUserMove, show: this.voiceMove?.promotionHook() }) ) this.playUserMove(orig, dest); - this.voiceMove?.update(this.vm.node.fen, true); - this.keyboardMove?.update({ fen: this.vm.node.fen }); + this.voiceMove?.update(this.node.fen, true); + this.keyboardMove?.update({ fen: this.node.fen }); }; playUci = (uci: Uci): void => this.sendMove(parseUci(uci)!); @@ -268,7 +276,7 @@ export default class PuzzleCtrl implements ParentCtrl { promotion, }); - sendMove = (move: Move): void => this.sendMoveAt(this.vm.path, this.position(), move); + sendMove = (move: Move): void => this.sendMoveAt(this.path, this.position(), move); sendMoveAt = (path: Tree.Path, pos: Chess, move: Move): void => { move = normalizeMove(pos, move); @@ -293,7 +301,7 @@ export default class PuzzleCtrl implements ParentCtrl { this.jump(newPath); this.withGround(g => g.playPremove()); - const progress = moveTest(this.vm, this.data.puzzle); + const progress = moveTest(this); if (progress === 'fail') lichess.sound.say('incorrect'); if (progress) this.applyProgress(progress); this.reorderChildren(path); @@ -316,7 +324,7 @@ export default class PuzzleCtrl implements ParentCtrl { g.cancelPremove(); g.selectSquare(null); }); - this.jump(treePath.init(this.vm.path)); + this.jump(treePath.init(this.path)); this.redraw(); }; @@ -327,29 +335,29 @@ export default class PuzzleCtrl implements ParentCtrl { applyProgress = (progress: undefined | 'fail' | 'win' | MoveTest): void => { if (progress === 'fail') { - this.vm.lastFeedback = 'fail'; + this.lastFeedback = 'fail'; this.revertUserMove(); - if (this.vm.mode === 'play') { + if (this.mode === 'play') { if (this.streak) { this.failStreak(this.streak); this.streakFailStorage.fire(); } else { - this.vm.canViewSolution = true; - this.vm.mode = 'try'; + this.canViewSolution(true); + this.mode = 'try'; this.sendResult(false); } } } else if (progress == 'win') { if (this.streak) this.sound.good(); - this.vm.lastFeedback = 'win'; - if (this.vm.mode != 'view') { - const sent = this.vm.mode == 'play' ? this.sendResult(true) : Promise.resolve(); - this.vm.mode = 'view'; + this.lastFeedback = 'win'; + if (this.mode != 'view') { + const sent = this.mode == 'play' ? this.sendResult(true) : Promise.resolve(); + this.mode = 'view'; this.withGround(this.showGround); sent.then(_ => (this.autoNext() ? this.nextPuzzle() : this.startCeval())); } } else if (progress) { - this.vm.lastFeedback = 'good'; + this.lastFeedback = 'good'; setTimeout( () => { const pos = Chess.fromSetup(parseFen(progress.fen).unwrap()).unwrap(); @@ -361,15 +369,15 @@ export default class PuzzleCtrl implements ParentCtrl { }; failStreak = (streak: PuzzleStreak): void => { - this.vm.mode = 'view'; + this.mode = 'view'; streak.onComplete(false); setTimeout(this.viewSolution, 500); this.sound.end(); }; sendResult = async (win: boolean): Promise => { - if (this.vm.resultSent) return Promise.resolve(); - this.vm.resultSent = true; + if (this.resultSent) return Promise.resolve(); + this.resultSent = true; this.session.complete(this.data.puzzle.id, win); const res = await xhr.complete( this.data.puzzle.id, @@ -384,12 +392,12 @@ export default class PuzzleCtrl implements ParentCtrl { if (next?.user && this.data.user) { this.data.user.rating = next.user.rating; this.data.user.provisional = next.user.provisional; - this.vm.round = res.round; + this.round = res.round; if (res.round?.ratingDiff) this.session.setRatingDiff(this.data.puzzle.id, res.round.ratingDiff); } if (win) lichess.sound.say('Success!'); if (next) { - this.vm.next.resolve(this.data.replay && res.replayComplete ? this.data.replay : next); + this.next.resolve(this.data.replay && res.replayComplete ? this.data.replay : next); if (this.streak && win) this.streak.onComplete(true, res.next); } this.redraw(); @@ -404,18 +412,18 @@ export default class PuzzleCtrl implements ParentCtrl { private isPuzzleData = (d: PuzzleData | ReplayEnd): d is PuzzleData => 'puzzle' in d; nextPuzzle = (): void => { - if (this.streak && this.vm.lastFeedback != 'win') return; - if (this.vm.mode !== 'view') return; + if (this.streak && this.lastFeedback != 'win') return; + if (this.mode !== 'view') return; this.ceval.stop(); - this.vm.next.promise.then(n => { + this.next.promise.then(n => { if (this.isPuzzleData(n)) { this.initiate(n); this.redraw(); } }); - if (this.data.replay && this.vm.round === undefined) { + if (this.data.replay && this.round === undefined) { lichess.redirect(`/training/dashboard/${this.data.replay.days}`); } @@ -442,7 +450,7 @@ export default class PuzzleCtrl implements ParentCtrl { const threat = ev as Tree.LocalEval; if (!node.threat || node.threat.depth <= threat.depth) node.threat = threat; } else if (!node.ceval || node.ceval.depth <= ev.depth) node.ceval = ev; - if (work.path === this.vm.path) { + if (work.path === this.path) { this.setAutoShapes(); this.redraw(); } @@ -456,33 +464,29 @@ export default class PuzzleCtrl implements ParentCtrl { this.withGround(g => g.setAutoShapes( computeAutoShapes({ - vm: this.vm, - ceval: this.ceval, + ...this, ground: g, - threatMode: this.threatMode(), - nextNodeBest: this.nextNodeBest(), + node: this.node, }), ), ); - canUseCeval = (): boolean => this.vm.mode === 'view' && !this.outcome(); + canUseCeval = (): boolean => this.mode === 'view' && !this.outcome(); startCeval = (): void => { if (this.ceval.enabled() && this.canUseCeval()) this.doStartCeval(); }; - private doStartCeval = throttle(800, () => - this.ceval.start(this.vm.path, this.vm.nodeList, this.threatMode()), - ); + private doStartCeval = throttle(800, () => this.ceval.start(this.path, this.nodeList, this.threatMode())); - nextNodeBest = () => treeOps.withMainlineChild(this.vm.node, n => n.eval?.best); + nextNodeBest = () => treeOps.withMainlineChild(this.node, n => n.eval?.best); toggleCeval = (): void => { this.ceval.toggle(); this.setAutoShapes(); this.startCeval(); if (!this.ceval.enabled()) this.threatMode(false); - this.vm.autoScrollRequested = true; + this.autoScrollRequested = true; this.redraw(); }; @@ -493,7 +497,7 @@ export default class PuzzleCtrl implements ParentCtrl { }; toggleThreatMode = (): void => { - if (this.vm.node.check) return; + if (this.node.check) return; if (!this.ceval.enabled()) this.ceval.toggle(); if (!this.ceval.enabled()) return; this.threatMode.toggle(); @@ -505,70 +509,70 @@ export default class PuzzleCtrl implements ParentCtrl { outcome = (): Outcome | undefined => this.position().outcome(); jump = (path: Tree.Path): void => { - const pathChanged = path !== this.vm.path, - isForwardStep = pathChanged && path.length === this.vm.path.length + 2; + const pathChanged = path !== this.path, + isForwardStep = pathChanged && path.length === this.path.length + 2; this.setPath(path); this.withGround(this.showGround); if (pathChanged) { if (isForwardStep) { - lichess.sound.saySan(this.vm.node.san); - lichess.sound.move(this.vm.node); + lichess.sound.saySan(this.node.san); + lichess.sound.move(this.node); } this.threatMode(false); this.ceval.stop(); this.startCeval(); } this.promotion.cancel(); - this.vm.justPlayed = undefined; - this.vm.autoScrollRequested = true; - this.keyboardMove?.update({ fen: this.vm.node.fen }); - this.voiceMove?.update(this.vm.node.fen, true); - lichess.pubsub.emit('ply', this.vm.node.ply); + this.justPlayed = undefined; + this.autoScrollRequested = true; + this.keyboardMove?.update({ fen: this.node.fen }); + this.voiceMove?.update(this.node.fen, true); + lichess.pubsub.emit('ply', this.node.ply); }; userJump = (path: Tree.Path): void => { - if (this.tree.nodeAtPath(path)?.puzzle == 'fail' && this.vm.mode != 'view') return; + if (this.tree.nodeAtPath(path)?.puzzle == 'fail' && this.mode != 'view') return; this.withGround(g => g.selectSquare(null)); this.jump(path); }; userJumpPlyDelta = (plyDelta: Ply) => { // ensure we are jumping to a valid ply - let maxValidPly = this.vm.mainline.length - 1; - if (last(this.vm.mainline)?.puzzle == 'fail' && this.vm.mode != 'view') maxValidPly -= 1; - const newPly = Math.min(Math.max(this.vm.node.ply + plyDelta, 0), maxValidPly); - this.userJump(fromNodeList(this.vm.mainline.slice(0, newPly + 1))); + let maxValidPly = this.mainline.length - 1; + if (last(this.mainline)?.puzzle == 'fail' && this.mode != 'view') maxValidPly -= 1; + const newPly = Math.min(Math.max(this.node.ply + plyDelta, 0), maxValidPly); + this.userJump(fromNodeList(this.mainline.slice(0, newPly + 1))); }; viewSolution = (): void => { this.sendResult(false); - this.vm.mode = 'view'; - mergeSolution(this.tree, this.vm.initialPath, this.data.puzzle.solution, this.vm.pov); - this.reorderChildren(this.vm.initialPath, true); + this.mode = 'view'; + mergeSolution(this.tree, this.initialPath, this.data.puzzle.solution, this.pov); + this.reorderChildren(this.initialPath, true); // try to play the solution next move - const next = this.vm.node.children[0]; - if (next && next.puzzle === 'good') this.userJump(this.vm.path + next.id); + const next = this.node.children[0]; + if (next && next.puzzle === 'good') this.userJump(this.path + next.id); else { - const firstGoodPath = treeOps.takePathWhile(this.vm.mainline, node => node.puzzle != 'good'); + const firstGoodPath = treeOps.takePathWhile(this.mainline, node => node.puzzle != 'good'); if (firstGoodPath) this.userJump(firstGoodPath + this.tree.nodeAtPath(firstGoodPath).children[0].id); } - this.vm.autoScrollRequested = true; - this.vm.voteDisabled = true; + this.autoScrollRequested = true; + this.voteDisabled = true; this.redraw(); this.startCeval(); setTimeout(() => { - this.vm.voteDisabled = false; + this.voteDisabled = false; this.redraw(); }, 500); }; skip = () => { - if (!this.streak || !this.streak.data.skip || this.vm.mode != 'play') return; + if (!this.streak || !this.streak.data.skip || this.mode != 'play') return; this.streak.skip(); - this.userJump(treePath.fromNodeList(this.vm.mainline)); - const moveIndex = treePath.size(this.vm.path) - treePath.size(this.vm.initialPath); + this.userJump(treePath.fromNodeList(this.mainline)); + const moveIndex = treePath.size(this.path) - treePath.size(this.initialPath); const solution = this.data.puzzle.solution[moveIndex]; this.playUci(solution); this.playBestMove(); @@ -581,21 +585,21 @@ export default class PuzzleCtrl implements ParentCtrl { }; vote = (v: boolean) => { - if (!this.vm.voteDisabled) { + if (!this.voteDisabled) { xhr.vote(this.data.puzzle.id, v); this.nextPuzzle(); } }; voteTheme = (theme: ThemeKey, v: boolean) => { - if (this.vm.round) { - this.vm.round.themes = this.vm.round.themes || {}; - if (v === this.vm.round.themes[theme]) { - delete this.vm.round.themes[theme]; + if (this.round) { + this.round.themes = this.round.themes || {}; + if (v === this.round.themes[theme]) { + delete this.round.themes[theme]; xhr.voteTheme(this.data.puzzle.id, theme, undefined); } else { - if (v || this.data.puzzle.themes.includes(theme)) this.vm.round.themes[theme] = v; - else delete this.vm.round.themes[theme]; + if (v || this.data.puzzle.themes.includes(theme)) this.round.themes[theme] = v; + else delete this.round.themes[theme]; xhr.voteTheme(this.data.puzzle.id, theme, v); } this.redraw(); @@ -603,12 +607,12 @@ export default class PuzzleCtrl implements ParentCtrl { }; playBestMove = (): void => { - const uci = this.nextNodeBest() || (this.vm.node.ceval && this.vm.node.ceval.pvs[0].moves[0]); + const uci = this.nextNodeBest() || (this.node.ceval && this.node.ceval.pvs[0].moves[0]); if (uci) this.playUci(uci); }; - autoNexting = () => this.vm.lastFeedback == 'win' && this.autoNext(); - currentEvals = () => ({ client: this.vm.node.ceval }); - showEvalGauge = () => this.vm.showComputer() && this.ceval.enabled() && !this.outcome(); + autoNexting = () => this.lastFeedback == 'win' && this.autoNext(); + currentEvals = () => ({ client: this.node.ceval }); + showEvalGauge = () => this.showComputer() && this.ceval.enabled() && !this.outcome(); getOrientation = () => this.withGround(g => g.state.orientation)!; allThemes = this.opts.themes && { dynamic: this.opts.themes.dynamic.split(' '), @@ -618,6 +622,6 @@ export default class PuzzleCtrl implements ParentCtrl { // implement cetal ParentCtrl: getCeval = () => this.ceval; ongoing = false; - getNode = () => this.vm.node; - showComputer = () => this.vm.mode === 'view'; + getNode = () => this.node; + showComputer = () => this.mode === 'view'; } diff --git a/ui/puzzle/src/interfaces.ts b/ui/puzzle/src/interfaces.ts index 7c2e4334b19be..44d709e5a3656 100644 --- a/ui/puzzle/src/interfaces.ts +++ b/ui/puzzle/src/interfaces.ts @@ -1,5 +1,3 @@ -import { Config as CgConfig } from 'chessground/config'; -import { Deferred } from 'common/defer'; import { Move } from 'chessops/types'; import { VNode } from 'snabbdom'; import * as Prefs from 'common/prefs'; @@ -20,31 +18,6 @@ export interface NvuiPlugin { export type ReplayEnd = PuzzleReplay; -// #TODO wut -export interface Vm { - path: Tree.Path; - nodeList: Tree.Node[]; - node: Tree.Node; - mainline: Tree.Node[]; - pov: Color; - mode: 'play' | 'view' | 'try'; - round?: PuzzleRound; - next: Deferred; - justPlayed?: Key; - resultSent: boolean; - lastFeedback: 'init' | 'fail' | 'win' | 'good' | 'retry'; - initialPath: Tree.Path; - initialNode: Tree.Node; - canViewSolution: boolean; - autoScrollRequested: boolean; - autoScrollNow: boolean; - voteDisabled?: boolean; - cgConfig: CgConfig; - showComputer(): boolean; - showAutoShapes(): boolean; - isDaily: boolean; -} - export type PuzzleDifficulty = 'easiest' | 'easier' | 'normal' | 'harder' | 'hardest'; export interface PuzzleSettings { diff --git a/ui/puzzle/src/keyboard.ts b/ui/puzzle/src/keyboard.ts index 2405ba0378344..6c9b8187e9274 100644 --- a/ui/puzzle/src/keyboard.ts +++ b/ui/puzzle/src/keyboard.ts @@ -23,7 +23,7 @@ export default (ctrl: PuzzleCtrl) => .bind('l', ctrl.toggleCeval) .bind('x', ctrl.toggleThreatMode) .bind('space', () => { - if (ctrl.vm.mode === 'view') { + if (ctrl.mode === 'view') { if (ctrl.ceval.enabled()) ctrl.playBestMove(); else ctrl.toggleCeval(); } diff --git a/ui/puzzle/src/moveTest.ts b/ui/puzzle/src/moveTest.ts index 0dee8f4ad1888..282a509832b7d 100644 --- a/ui/puzzle/src/moveTest.ts +++ b/ui/puzzle/src/moveTest.ts @@ -1,7 +1,8 @@ import { altCastles } from 'chess'; import { parseUci } from 'chessops/util'; import { path as pathOps } from 'tree'; -import { Vm, Puzzle, MoveTest } from './interfaces'; +import { MoveTest } from './interfaces'; +import PuzzleCtrl from './ctrl'; type MoveTestReturn = undefined | 'fail' | 'win' | MoveTest; @@ -11,36 +12,36 @@ function isAltCastle(str: string): str is AltCastle { return str in altCastles; } -export default function moveTest(vm: Vm, puzzle: Puzzle): MoveTestReturn { - if (vm.mode === 'view') return; - if (!pathOps.contains(vm.path, vm.initialPath)) return; +export default function moveTest(ctrl: PuzzleCtrl): MoveTestReturn { + if (ctrl.mode === 'view') return; + if (!pathOps.contains(ctrl.path, ctrl.initialPath)) return; - const playedByColor = vm.node.ply % 2 === 1 ? 'white' : 'black'; - if (playedByColor !== vm.pov) return; + const playedByColor = ctrl.node.ply % 2 === 1 ? 'white' : 'black'; + if (playedByColor !== ctrl.pov) return; - const nodes = vm.nodeList.slice(pathOps.size(vm.initialPath) + 1).map(node => ({ + const nodes = ctrl.nodeList.slice(pathOps.size(ctrl.initialPath) + 1).map(node => ({ uci: node.uci, castle: node.san!.startsWith('O-O'), checkmate: node.san!.endsWith('#'), })); for (const i in nodes) { - if (nodes[i].checkmate) return (vm.node.puzzle = 'win'); + if (nodes[i].checkmate) return (ctrl.node.puzzle = 'win'); const uci = nodes[i].uci!, - solUci = puzzle.solution[i]; + solUci = ctrl.data.puzzle.solution[i]; if (uci != solUci && (!nodes[i].castle || !isAltCastle(uci) || altCastles[uci] != solUci)) - return (vm.node.puzzle = 'fail'); + return (ctrl.node.puzzle = 'fail'); } - const nextUci = puzzle.solution[nodes.length]; - if (!nextUci) return (vm.node.puzzle = 'win'); + const nextUci = ctrl.data.puzzle.solution[nodes.length]; + if (!nextUci) return (ctrl.node.puzzle = 'win'); // from here we have a next move - vm.node.puzzle = 'good'; + ctrl.node.puzzle = 'good'; return { move: parseUci(nextUci)!, - fen: vm.node.fen, - path: vm.path, + fen: ctrl.node.fen, + path: ctrl.path, }; } diff --git a/ui/puzzle/src/plugins/nvui.ts b/ui/puzzle/src/plugins/nvui.ts index 61e399245b7c6..2b74fa80afa62 100644 --- a/ui/puzzle/src/plugins/nvui.ts +++ b/ui/puzzle/src/plugins/nvui.ts @@ -62,7 +62,7 @@ export function initModule() { ctrl.streak ? '.puzzle--streak' : '' }`, h('div.nvui', [ - h('h1', `Puzzle: ${ctrl.vm.pov} to play.`), + h('h1', `Puzzle: ${ctrl.pov} to play.`), h('h2', 'Puzzle info'), puzzleBox(ctrl), theme(ctrl), @@ -71,7 +71,7 @@ export function initModule() { h( 'p.moves', { attrs: { role: 'log', 'aria-live': 'off' } }, - renderMainline(ctrl.vm.mainline, ctrl.vm.path, moveStyle.get()), + renderMainline(ctrl.mainline, ctrl.path, moveStyle.get()), ), h('h2', 'Pieces'), h('div.pieces', renderPieces(ground.state.pieces, moveStyle.get())), @@ -102,7 +102,7 @@ export function initModule() { }, [ h('label', [ - ctrl.vm.mode === 'view' ? 'Command input' : `Find the best move for ${ctrl.vm.pov}.`, + ctrl.mode === 'view' ? 'Command input' : `Find the best move for ${ctrl.pov}.`, h('input.move.mousetrap', { attrs: { name: 'move', type: 'text', autocomplete: 'off', autofocus: true }, }), @@ -111,7 +111,7 @@ export function initModule() { ), notify.render(), h('h2', 'Actions'), - ctrl.vm.mode === 'view' ? afterActions(ctrl) : playActions(ctrl), + ctrl.mode === 'view' ? afterActions(ctrl) : playActions(ctrl), h('h2', 'Board'), h( 'div.board', @@ -119,15 +119,15 @@ export function initModule() { hook: onInsert(el => { const $board = $(el); const $buttons = $board.find('button'); - const steps = () => ctrl.tree.getNodeList(ctrl.vm.path); + const steps = () => ctrl.tree.getNodeList(ctrl.path); const uciSteps = () => steps().filter(hasUci); const fenSteps = () => steps().map(step => step.fen); - const opponentColor = ctrl.vm.pov === 'white' ? 'black' : 'white'; + const opponentColor = ctrl.pov === 'white' ? 'black' : 'white'; $board.on( 'click', selectionHandler(() => opponentColor, selectSound), ); - $board.on('keydown', arrowKeyHandler(ctrl.vm.pov, borderSound)); + $board.on('keydown', arrowKeyHandler(ctrl.pov, borderSound)); $board.on('keypress', boardCommandsHandler()); $buttons.on( 'keypress', @@ -136,7 +136,7 @@ export function initModule() { $buttons.on( 'keypress', possibleMovesHandler( - ctrl.vm.pov, + ctrl.pov, () => ground.state.turnColor, ground.getFen, () => ground.state.pieces, @@ -151,7 +151,7 @@ export function initModule() { }, renderBoard( ground.state.pieces, - ctrl.vm.pov, + ctrl.pov, pieceStyle.get(), prefixStyle.get(), positionStyle.get(), @@ -234,7 +234,7 @@ function hasUci(step: Tree.Node): step is StepWithUci { } function lastMove(ctrl: PuzzleCtrl, style: Style): string { - const node = ctrl.vm.node; + const node = ctrl.node; if (node.ply === 0) return 'Initial position'; // make sure consecutive moves are different so that they get re-read return renderSan(node.san || '', node.uci, style) + (node.ply % 2 === 0 ? '' : ' '); @@ -252,10 +252,10 @@ function onSubmit( if (isShortCommand(input)) input = '/' + input; if (input[0] === '/') onCommand(ctrl, notify, input.slice(1), style()); else { - const uci = inputToLegalUci(input, ctrl.vm.node.fen, ground); + const uci = inputToLegalUci(input, ctrl.node.fen, ground); if (uci) { ctrl.playUci(uci); - switch (ctrl.vm.lastFeedback) { + switch (ctrl.lastFeedback) { case 'fail': notify(ctrl.trans.noarg('notTheMove')); break; @@ -275,11 +275,11 @@ function onSubmit( } function isYourMove(ctrl: PuzzleCtrl) { - return ctrl.vm.node.children.length === 0 || ctrl.vm.node.children[0].puzzle === 'fail'; + return ctrl.node.children.length === 0 || ctrl.node.children[0].puzzle === 'fail'; } function browseHint(ctrl: PuzzleCtrl): string[] { - if (ctrl.vm.mode !== 'view' && !isYourMove(ctrl)) return ['You browsed away from the latest position.']; + if (ctrl.mode !== 'view' && !isYourMove(ctrl)) return ['You browsed away from the latest position.']; else return []; } @@ -303,8 +303,8 @@ function onCommand(ctrl: PuzzleCtrl, notify: (txt: string) => void, c: string, s } function viewOrAdvanceSolution(ctrl: PuzzleCtrl, notify: (txt: string) => void): void { - if (ctrl.vm.mode === 'view') { - const node = ctrl.vm.node, + if (ctrl.mode === 'view') { + const node = ctrl.node, next = nextNode(node), nextNext = nextNode(next); if (isInSolution(next) || (isInSolution(node) && isInSolution(nextNext))) { @@ -335,16 +335,16 @@ function renderStreak(ctrl: PuzzleCtrl): VNode[] { } function renderStatus(ctrl: PuzzleCtrl): string { - if (ctrl.vm.mode !== 'view') return 'Solving'; + if (ctrl.mode !== 'view') return 'Solving'; else if (ctrl.streak) return `GAME OVER. Your streak: ${ctrl.streak.data.index}`; - else if (ctrl.vm.lastFeedback === 'win') return 'Puzzle solved!'; + else if (ctrl.lastFeedback === 'win') return 'Puzzle solved!'; else return 'Puzzle complete.'; } function renderReplay(ctrl: PuzzleCtrl): string { const replay = ctrl.data.replay; if (!replay) return ''; - const i = replay.i + (ctrl.vm.mode === 'play' ? 0 : 1); + const i = replay.i + (ctrl.mode === 'play' ? 0 : 1); return `Replaying ${ctrl.trans.noarg(ctrl.data.angle.key)} puzzles: ${i} of ${replay.of}`; } @@ -360,7 +360,7 @@ function playActions(ctrl: PuzzleCtrl): VNode { } function afterActions(ctrl: PuzzleCtrl): VNode { - const win = ctrl.vm.lastFeedback === 'win'; + const win = ctrl.lastFeedback === 'win'; return h( 'div.actions_after', ctrl.streak && !win @@ -378,8 +378,8 @@ function renderVote(ctrl: PuzzleCtrl): VNode[] { if (!ctrl.data.user || ctrl.autoNexting()) return []; return [ ...renderVoteTutorial(ctrl), - button('Thumbs up', () => ctrl.vote(true), undefined, ctrl.vm.voteDisabled), - button('Thumbs down', () => ctrl.vote(false), undefined, ctrl.vm.voteDisabled), + button('Thumbs up', () => ctrl.vote(true), undefined, ctrl.voteDisabled), + button('Thumbs down', () => ctrl.vote(false), undefined, ctrl.voteDisabled), ]; } diff --git a/ui/puzzle/src/view/after.ts b/ui/puzzle/src/view/after.ts index b447d0cdba1db..df0a1f6aac20d 100644 --- a/ui/puzzle/src/view/after.ts +++ b/ui/puzzle/src/view/after.ts @@ -15,7 +15,7 @@ const renderVote = (ctrl: PuzzleCtrl): VNode => h('p', ctrl.trans.noarg('didYouLikeThisPuzzle')), h('p', ctrl.trans.noarg('voteToLoadNextOne')), ]), - h('div.puzzle__vote__buttons', { class: { enabled: !ctrl.vm.voteDisabled } }, [ + h('div.puzzle__vote__buttons', { class: { enabled: !ctrl.voteDisabled } }, [ h('div.vote.vote-up', { hook: bind('click', () => ctrl.vote(true)) }), h('div.vote.vote-down', { hook: bind('click', () => ctrl.vote(false)) }), ]), @@ -41,7 +41,7 @@ const renderStreak = (ctrl: PuzzleCtrl): MaybeVNodes => [ export default function (ctrl: PuzzleCtrl): VNode { const data = ctrl.data; - const win = ctrl.vm.lastFeedback == 'win'; + const win = ctrl.lastFeedback == 'win'; return h( 'div.puzzle__feedback.after', ctrl.streak && !win @@ -53,7 +53,7 @@ export default function (ctrl: PuzzleCtrl): VNode { h('a', { attrs: { 'data-icon': licon.Bullseye, - href: `/analysis/${ctrl.vm.node.fen.replace(/ /g, '_')}?color=${ctrl.vm.pov}#practice`, + href: `/analysis/${ctrl.node.fen.replace(/ /g, '_')}?color=${ctrl.pov}#practice`, title: ctrl.trans.noarg('playWithTheMachine'), target: '_blank', rel: 'noopener', diff --git a/ui/puzzle/src/view/chessground.ts b/ui/puzzle/src/view/chessground.ts index 13fbe595ce34b..1bba0f5543e3b 100644 --- a/ui/puzzle/src/view/chessground.ts +++ b/ui/puzzle/src/view/chessground.ts @@ -42,7 +42,7 @@ export function makeConfig(ctrl: PuzzleCtrl): CgConfig { events: { move: ctrl.userMove, insert(elements) { - resizeHandle(elements, Prefs.ShowResizeHandle.Always, ctrl.vm.node.ply); + resizeHandle(elements, Prefs.ShowResizeHandle.Always, ctrl.node.ply); }, }, premovable: { diff --git a/ui/puzzle/src/view/feedback.ts b/ui/puzzle/src/view/feedback.ts index a80e8a7dc35a0..ef16b5b368af5 100644 --- a/ui/puzzle/src/view/feedback.ts +++ b/ui/puzzle/src/view/feedback.ts @@ -12,7 +12,7 @@ const viewSolution = (ctrl: PuzzleCtrl): VNode => ctrl.trans.noarg('skip'), ), ]) - : h('div.view_solution', { class: { show: ctrl.vm.canViewSolution } }, [ + : h('div.view_solution', { class: { show: ctrl.canViewSolution() } }, [ h( 'a.button.button-empty', { hook: bind('click', ctrl.viewSolution) }, @@ -23,12 +23,12 @@ const viewSolution = (ctrl: PuzzleCtrl): VNode => const initial = (ctrl: PuzzleCtrl): VNode => h('div.puzzle__feedback.play', [ h('div.player', [ - h('div.no-square', h('piece.king.' + ctrl.vm.pov)), + h('div.no-square', h('piece.king.' + ctrl.pov)), h('div.instruction', [ h('strong', ctrl.trans.noarg('yourTurn')), h( 'em', - ctrl.trans.noarg(ctrl.vm.pov === 'white' ? 'findTheBestMoveForWhite' : 'findTheBestMoveForBlack'), + ctrl.trans.noarg(ctrl.pov === 'white' ? 'findTheBestMoveForWhite' : 'findTheBestMoveForBlack'), ), ]), ]), @@ -60,8 +60,8 @@ const fail = (ctrl: PuzzleCtrl): VNode => ]); export default function (ctrl: PuzzleCtrl): MaybeVNode { - if (ctrl.vm.mode === 'view') return afterView(ctrl); - switch (ctrl.vm.lastFeedback) { + if (ctrl.mode === 'view') return afterView(ctrl); + switch (ctrl.lastFeedback) { case 'init': return initial(ctrl); case 'good': diff --git a/ui/puzzle/src/view/main.ts b/ui/puzzle/src/view/main.ts index d6cabbd3262c3..960e9006e4999 100644 --- a/ui/puzzle/src/view/main.ts +++ b/ui/puzzle/src/view/main.ts @@ -30,9 +30,9 @@ function jumpButton(icon: string, effect: string, disabled: boolean, glowing = f } function controls(ctrl: PuzzleCtrl): VNode { - const node = ctrl.vm.node; + const node = ctrl.node; const nextNode = node.children[0]; - const notOnLastMove = ctrl.vm.mode == 'play' && nextNode && nextNode.puzzle != 'fail'; + const notOnLastMove = ctrl.mode == 'play' && nextNode && nextNode.puzzle != 'fail'; return h('div.puzzle__controls.analyse-controls', [ h( 'div.jumps', @@ -63,10 +63,10 @@ let cevalShown = false; export default function (ctrl: PuzzleCtrl): VNode { if (ctrl.nvui) return ctrl.nvui.render(ctrl); - const showCeval = ctrl.vm.showComputer(), + const showCeval = ctrl.showComputer(), gaugeOn = ctrl.showEvalGauge(); if (cevalShown !== showCeval) { - if (!cevalShown) ctrl.vm.autoScrollNow = true; + if (!cevalShown) ctrl.autoScrollNow = true; cevalShown = showCeval; } return h( diff --git a/ui/puzzle/src/view/side.ts b/ui/puzzle/src/view/side.ts index a52352bd633e4..177cc095724c9 100644 --- a/ui/puzzle/src/view/side.ts +++ b/ui/puzzle/src/view/side.ts @@ -31,7 +31,7 @@ const puzzleInfos = (ctrl: PuzzleCtrl, puzzle: Puzzle): VNode => 'p', ctrl.trans.vdom( 'puzzleId', - ctrl.streak && ctrl.vm.mode === 'play' + ctrl.streak && ctrl.mode === 'play' ? h('span.hidden', ctrl.trans.noarg('hidden')) : h( 'a', @@ -50,7 +50,7 @@ const puzzleInfos = (ctrl: PuzzleCtrl, puzzle: Puzzle): VNode => 'p', ctrl.trans.vdom( 'ratingX', - !ctrl.streak && ctrl.vm.mode === 'play' + !ctrl.streak && ctrl.mode === 'play' ? h('span.hidden', ctrl.trans.noarg('hidden')) : h('strong', puzzle.rating), ), @@ -67,9 +67,9 @@ function gameInfos(ctrl: PuzzleCtrl, game: PuzzleGame, puzzle: Puzzle): VNode { 'p', ctrl.trans.vdom( 'fromGameLink', - ctrl.vm.mode == 'play' + ctrl.mode == 'play' ? h('span', gameName) - : h('a', { attrs: { href: `/${game.id}/${ctrl.vm.pov}#${puzzle.initialPly}` } }, gameName), + : h('a', { attrs: { href: `/${game.id}/${ctrl.pov}#${puzzle.initialPly}` } }, gameName), ), ), h( @@ -106,7 +106,7 @@ export const userBox = (ctrl: PuzzleCtrl): VNode => { h('p', noarg('toGetPersonalizedPuzzles')), h('a.button', { attrs: { href: router.withLang('/signup') } }, noarg('signUp')), ]); - const diff = ctrl.vm.round?.ratingDiff, + const diff = ctrl.round?.ratingDiff, ratedId = 'puzzle-toggle-rated'; return h('div.puzzle__side__user', [ !data.replay && @@ -115,7 +115,7 @@ export const userBox = (ctrl: PuzzleCtrl): VNode => { h('div.puzzle__side__config__toggle', [ h('div.switch', [ h(`input#${ratedId}.cmn-toggle.cmn-toggle--subtle`, { - attrs: { type: 'checkbox', checked: ctrl.rated(), disabled: ctrl.vm.lastFeedback != 'init' }, + attrs: { type: 'checkbox', checked: ctrl.rated(), disabled: ctrl.lastFeedback != 'init' }, hook: { insert: vnode => (vnode.elm as HTMLElement).addEventListener('change', ctrl.toggleRated), }, @@ -157,7 +157,7 @@ const colors = [ export function replay(ctrl: PuzzleCtrl): MaybeVNode { const replay = ctrl.data.replay; if (!replay) return; - const i = replay.i + (ctrl.vm.mode == 'play' ? 0 : 1); + const i = replay.i + (ctrl.mode == 'play' ? 0 : 1); return h('div.puzzle__side__replay', [ h('a', { attrs: { href: `/training/dashboard/${replay.days}` } }, [ '« ', @@ -185,7 +185,7 @@ export function config(ctrl: PuzzleCtrl): MaybeVNode { insert: vnode => (vnode.elm as HTMLElement).addEventListener('change', () => { ctrl.autoNext(!ctrl.autoNext()); - if (ctrl.autoNext() && ctrl.vm.resultSent && !ctrl.streak) { + if (ctrl.autoNext() && ctrl.resultSent && !ctrl.streak) { ctrl.nextPuzzle(); } }), diff --git a/ui/puzzle/src/view/theme.ts b/ui/puzzle/src/view/theme.ts index 35a1fcafabfb9..909c4c9b53a76 100644 --- a/ui/puzzle/src/view/theme.ts +++ b/ui/puzzle/src/view/theme.ts @@ -10,11 +10,11 @@ const studyUrl = 'https://lichess.org/study/viiWlKjv'; export default function theme(ctrl: PuzzleCtrl): MaybeVNode { const data = ctrl.data, angle = data.angle; - const showEditor = ctrl.vm.mode == 'view' && !ctrl.autoNexting(); + const showEditor = ctrl.mode == 'view' && !ctrl.autoNexting(); if (data.replay) return showEditor ? h('div.puzzle__side__theme', editor(ctrl)) : null; const puzzleMenu = (v: VNode): VNode => h('a', { attrs: { href: router.withLang(`/training/${angle.opening ? 'openings' : 'themes'}`) } }, v); - return !ctrl.streak && ctrl.vm.isDaily + return !ctrl.streak && ctrl.isDaily ? h( 'div.puzzle__side__theme.puzzle__side__theme--daily', puzzleMenu(h('h2', ctrl.trans.noarg('dailyPuzzle'))), @@ -46,7 +46,7 @@ const invisibleThemes = new Set(['master', 'masterVsMaster', 'superGM']); const editor = (ctrl: PuzzleCtrl): VNode[] => { const data = ctrl.data, trans = ctrl.trans.noarg, - votedThemes = ctrl.vm.round?.themes || {}; + votedThemes = ctrl.round?.themes || {}; const visibleThemes: string[] = data.puzzle.themes .filter(t => !invisibleThemes.has(t)) .concat(Object.keys(votedThemes).filter(t => votedThemes[t] && !data.puzzle.themes.includes(t))) diff --git a/ui/puzzle/src/view/tree.ts b/ui/puzzle/src/view/tree.ts index f571ffe46d53e..9aaf8def95f54 100644 --- a/ui/puzzle/src/view/tree.ts +++ b/ui/puzzle/src/view/tree.ts @@ -25,14 +25,14 @@ const autoScroll = throttle(150, (ctrl: PuzzleCtrl, el) => { const cont = el.parentNode; const target = el.querySelector('.active'); if (!target) { - cont.scrollTop = ctrl.vm.path === treePath.root ? 0 : 99999; + cont.scrollTop = ctrl.path === treePath.root ? 0 : 99999; return; } cont.scrollTop = target.offsetTop - cont.offsetHeight / 2 + target.offsetHeight; }); function pathContains(ctx: Ctx, path: Tree.Path): boolean { - return treePath.contains(ctx.ctrl.vm.path, path); + return treePath.contains(ctx.ctrl.path, path); } function plyToTurn(ply: number): number { @@ -91,9 +91,9 @@ function renderMoveOf(ctx: Ctx, node: Tree.Node, opts: RenderOpts): VNode { function renderMainlineMoveOf(ctx: Ctx, node: Tree.Node, opts: RenderOpts): VNode { const path = opts.parentPath + node.id; const classes: Classes = { - active: path === ctx.ctrl.vm.path, - current: path === ctx.ctrl.vm.initialPath, - hist: node.ply < ctx.ctrl.vm.initialNode.ply, + active: path === ctx.ctrl.path, + current: path === ctx.ctrl.initialPath, + hist: node.ply < ctx.ctrl.initialNode.ply, }; if (node.puzzle) classes[node.puzzle] = true; return h('move', { attrs: { p: path }, class: classes }, renderMove(ctx, node)); @@ -129,7 +129,7 @@ function renderMove(ctx: Ctx, node: Tree.Node): LooseVNodes { function renderVariationMoveOf(ctx: Ctx, node: Tree.Node, opts: RenderOpts): VNode { const withIndex = opts.withIndex || node.ply % 2 === 1; const path = opts.parentPath + node.id; - const active = path === ctx.ctrl.vm.path; + const active = path === ctx.ctrl.path; const classes: Classes = { active, parent: !active && pathContains(ctx, path) }; if (node.puzzle) classes[node.puzzle] = true; return h('move', { attrs: { p: path }, class: classes }, [ @@ -168,7 +168,7 @@ export function render(ctrl: PuzzleCtrl): VNode { hook: { insert: vnode => { const el = vnode.elm as HTMLElement; - if (ctrl.vm.path !== treePath.root) autoScroll(ctrl, el); + if (ctrl.path !== treePath.root) autoScroll(ctrl, el); el.addEventListener('mousedown', (e: MouseEvent) => { if (defined(e.button) && e.button !== 0) return; // only touch or left click const path = eventPath(e); @@ -177,13 +177,13 @@ export function render(ctrl: PuzzleCtrl): VNode { }); }, postpatch: (_, vnode) => { - if (ctrl.vm.autoScrollNow) { + if (ctrl.autoScrollNow) { autoScroll(ctrl, vnode.elm as HTMLElement); - ctrl.vm.autoScrollNow = false; - ctrl.vm.autoScrollRequested = false; - } else if (ctrl.vm.autoScrollRequested) { - if (ctrl.vm.path !== treePath.root) autoScroll(ctrl, vnode.elm as HTMLElement); - ctrl.vm.autoScrollRequested = false; + ctrl.autoScrollNow = false; + ctrl.autoScrollRequested = false; + } else if (ctrl.autoScrollRequested) { + if (ctrl.path !== treePath.root) autoScroll(ctrl, vnode.elm as HTMLElement); + ctrl.autoScrollRequested = false; } }, }, From 901914bdede9eefdd08a2e69128d9bcc46bbce0c Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Wed, 20 Dec 2023 14:02:39 +0100 Subject: [PATCH 5/5] move puzzle ctrl fields --- ui/puzzle/src/ctrl.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/puzzle/src/ctrl.ts b/ui/puzzle/src/ctrl.ts index ae1ace5f09bb2..0c029d7a5b32a 100644 --- a/ui/puzzle/src/ctrl.ts +++ b/ui/puzzle/src/ctrl.ts @@ -54,14 +54,14 @@ export default class PuzzleCtrl implements ParentCtrl { node: Tree.Node; nodeList: Tree.Node[]; mainline: Tree.Node[]; + initialPath: Tree.Path; + initialNode: Tree.Node; pov: Color; mode: 'play' | 'view' | 'try'; round?: PuzzleRound; justPlayed?: Key; resultSent: boolean; lastFeedback: 'init' | 'fail' | 'win' | 'good' | 'retry'; - initialPath: Tree.Path; - initialNode: Tree.Node; canViewSolution = toggle(false); autoScrollRequested: boolean; autoScrollNow: boolean;