diff --git a/app/views/analyse/replay.scala b/app/views/analyse/replay.scala index 9d65bfaa71589..742f5900e8f12 100644 --- a/app/views/analyse/replay.scala +++ b/app/views/analyse/replay.scala @@ -115,6 +115,7 @@ object replay: .cssTag("analyse.round") .cssTag((pov.game.variant == Crazyhouse).option("analyse.zh")) .cssTag(ctx.blind.option("round.nvui")) + .cssTag(ctx.pref.hasKeyboardMove.option("keyboardMove")) .js(analyseNvuiTag) .js( bits.analyseModule( diff --git a/app/views/study.scala b/app/views/study.scala index 25d4bc155e996..9264e17f219b2 100644 --- a/app/views/study.scala +++ b/app/views/study.scala @@ -56,6 +56,7 @@ def show( )(using ctx: Context) = Page(s.name.value) .cssTag("analyse.study") + .cssTag(ctx.pref.hasKeyboardMove.option("keyboardMove")) .js(analyseNvuiTag) .js( PageModule( diff --git a/modules/analyse/src/main/ui/AnalyseUi.scala b/modules/analyse/src/main/ui/AnalyseUi.scala index 4179683ff9e60..f30e379296298 100644 --- a/modules/analyse/src/main/ui/AnalyseUi.scala +++ b/modules/analyse/src/main/ui/AnalyseUi.scala @@ -20,6 +20,7 @@ final class AnalyseUi(helpers: Helpers)(externalEngineEndpoint: String): .cssTag((pov.game.variant == Crazyhouse).option("analyse.zh")) .cssTag(withForecast.option("analyse.forecast")) .cssTag(ctx.blind.option("round.nvui")) + .cssTag(ctx.pref.hasKeyboardMove.option("keyboardMove")) .csp(csp.compose(_.withExternalAnalysisApis)) .graph( title = "Chess analysis board", diff --git a/modules/round/src/main/JsonView.scala b/modules/round/src/main/JsonView.scala index 406c62dec1bd4..cc20700fad8ab 100644 --- a/modules/round/src/main/JsonView.scala +++ b/modules/round/src/main/JsonView.scala @@ -208,7 +208,8 @@ final class JsonView( "coords" -> pref.coords, "resizeHandle" -> pref.resizeHandle, "replay" -> pref.replay, - "clockTenths" -> pref.clockTenths + "clockTenths" -> pref.clockTenths, + "keyboardMove" -> pref.hasKeyboardMove ) .add("is3d" -> pref.is3d) .add("clockBar" -> pref.clockBar) @@ -288,7 +289,8 @@ final class JsonView( "animationDuration" -> animationMillis(pov, pref), "coords" -> pref.coords, "moveEvent" -> pref.moveEvent, - "showCaptured" -> pref.captured + "showCaptured" -> pref.captured, + "keyboardMove" -> pref.hasKeyboardMove ) .add("rookCastle" -> (pref.rookCastle == Pref.RookCastle.YES)) .add("is3d" -> pref.is3d) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f25a027bf6f3..197cc0471babd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -103,6 +103,9 @@ importers: game: specifier: workspace:* version: link:../game + keyboardMove: + specifier: workspace:* + version: link:../keyboardMove nvui: specifier: workspace:* version: link:../nvui diff --git a/ui/analyse/css/_layout.scss b/ui/analyse/css/_layout.scss index e709ff7ddaa67..4d380989bc0d4 100644 --- a/ui/analyse/css/_layout.scss +++ b/ui/analyse/css/_layout.scss @@ -63,6 +63,11 @@ body { display: none; } + .keyboard-move { + grid-area: kb-move; + margin: $analyse-block-gap; + } + ---chat-height: fit-content(0); &--wiki { ---chat-height: 0; @@ -72,8 +77,9 @@ body { margin: $analyse-block-gap 0 0 0; } - grid-template-rows: auto auto minmax(20em, 30vh); + grid-template-rows: auto auto auto minmax(20em, 30vh); grid-template-areas: + 'kb-move' 'board' 'controls' 'tools' @@ -88,6 +94,7 @@ body { grid-template-rows: fit-content(0); grid-template-areas: 'board gauge tools' + 'kb-move . controls' 'under . controls' 'under . round-training' 'under . side' @@ -109,6 +116,10 @@ body { .eval-gauge { display: block; } + + .keyboard-move { + margin: calc($analyse-block-gap / 2) 0 0 0; + } } @include mq-is-col2-squeeze { @@ -117,12 +128,13 @@ body { @include mq-at-least-col3 { grid-template-columns: $col3-uniboard-side $analyse-block-gap var(---col3-uniboard-width) $analyse-block-gap $col3-uniboard-table; - grid-template-rows: auto $chat-height 2.5em 1fr; + grid-template-rows: auto $chat-height auto 2.5em 1fr; grid-template-areas: - 'side . board gauge tools' - 'chat . board gauge tools' - 'uchat . under . controls' - 'uchat . under . round-training'; + 'side . board gauge tools' + 'chat . board gauge tools' + 'uchat . kb-move . controls' + 'uchat . under . controls' + 'uchat . under . round-training'; &__side { margin-top: 0; diff --git a/ui/analyse/css/_zh.scss b/ui/analyse/css/_zh.scss index 378e2e5613a63..b3ef7c565f8e8 100644 --- a/ui/analyse/css/_zh.scss +++ b/ui/analyse/css/_zh.scss @@ -23,6 +23,7 @@ $pocket-height: 60px; 'pocket-top' 'board' 'pocket-bot' + 'kb-move' 'controls' 'tools' 'side' @@ -37,6 +38,7 @@ $pocket-height: 60px; 'board gauge pocket-top' 'board gauge tools' 'board gauge pocket-bot' + 'kb-move . controls' 'under . controls' 'under . round-training' 'under . side' @@ -47,12 +49,13 @@ $pocket-height: 60px; @include mq-at-least-col3 { grid-template-rows: $pocket-height auto auto $pocket-height; grid-template-areas: - 'side . board gauge pocket-top' - 'side . board gauge tools' - 'chat . board gauge tools' - 'chat . board gauge pocket-bot' - 'uchat . under . controls' - 'uchat . under . round-training'; + 'side . board gauge pocket-top' + 'side . board gauge tools' + 'chat . board gauge tools' + 'chat . board gauge pocket-bot' + 'uchat . kb-move . controls' + 'uchat . under . controls' + 'uchat . under . round-training'; } } diff --git a/ui/analyse/css/study/_layout.scss b/ui/analyse/css/study/_layout.scss index 7b3c7a5a52172..bd71eb48c336c 100644 --- a/ui/analyse/css/study/_layout.scss +++ b/ui/analyse/css/study/_layout.scss @@ -1,3 +1,8 @@ .analyse__underboard { margin-top: calc($analyse-block-gap / 2); } + +.analyse .keyboard-move { + margin-top: calc($analyse-block-gap * 2); + margin-bottom: calc($analyse-block-gap * -1); +} diff --git a/ui/analyse/package.json b/ui/analyse/package.json index a2a7641af0f08..951c2729f61ca 100644 --- a/ui/analyse/package.json +++ b/ui/analyse/package.json @@ -26,6 +26,7 @@ "common": "workspace:*", "debounce-promise": "^3.1.2", "game": "workspace:*", + "keyboardMove": "workspace:*", "nvui": "workspace:*", "prop-types": "^15.8.1", "shepherd.js": "^11.2.0", diff --git a/ui/analyse/src/ctrl.ts b/ui/analyse/src/ctrl.ts index c9006e6570324..ef760074db6a0 100644 --- a/ui/analyse/src/ctrl.ts +++ b/ui/analyse/src/ctrl.ts @@ -51,6 +51,8 @@ import { uciToMove } from 'chessground/util'; import Persistence from './persistence'; import pgnImport from './pgnImport'; import ForecastCtrl from './forecast/forecastCtrl'; +import { ArrowKey, KeyboardMove, ctrl as makeKeyboardMove } from 'keyboardMove'; +import * as control from './control'; export default class AnalyseCtrl { data: AnalyseData; @@ -123,6 +125,7 @@ export default class AnalyseCtrl { cgConfig: any; // latest chessground config (useful for revert) nvui?: NvuiPlugin; pvUciQueue: Uci[] = []; + keyboardMove?: KeyboardMove; constructor( readonly opts: AnalyseOpts, @@ -359,6 +362,24 @@ export default class AnalyseCtrl { return config; } + setChessground = (cg: CgApi) => { + this.chessground = cg; + + if (this.data.pref.keyboardMove) { + this.keyboardMove ??= makeKeyboardMove({ + ...this, + data: { ...this.data, player: { color: 'both' } }, + flipNow: this.flip, + }); + this.keyboardMove.update({ fen: this.node.fen, canMove: true, cg }); + requestAnimationFrame(() => this.redraw()); + } + + this.setAutoShapes(); + if (this.node.shapes) this.chessground.setShapes(this.node.shapes as DrawShape[]); + this.cgVersion.dom = this.cgVersion.js; + }; + private onChange: () => void = throttle(300, () => { site.pubsub.emit('analysis.change', this.node.fen, this.path); }); @@ -398,6 +419,7 @@ export default class AnalyseCtrl { } site.pubsub.emit('ply', this.node.ply, this.tree.lastMainlineNode(this.path).ply === this.node.ply); this.showGround(); + this.pluginUpdate(this.node.fen); } userJump = (path: Tree.Path): void => { @@ -418,12 +440,12 @@ export default class AnalyseCtrl { if (this.canJumpTo(path)) this.userJump(path); } - mainlinePathToPly(ply: Ply): Tree.Path { + mainlinePlyToPath(ply: Ply): Tree.Path { return treeOps.takePathWhile(this.mainline, n => n.ply <= ply); } jumpToMain = (ply: Ply): void => { - this.userJump(this.mainlinePathToPly(ply)); + this.userJump(this.mainlinePlyToPath(ply)); }; jumpToIndex = (index: number): void => { @@ -455,7 +477,7 @@ export default class AnalyseCtrl { } as AnalyseData; if (andReload) { this.reloadData(data, false); - this.userJump(this.mainlinePathToPly(this.tree.lastPly())); + this.userJump(this.mainlinePlyToPath(this.tree.lastPly())); this.redraw(); } return data; @@ -474,6 +496,21 @@ export default class AnalyseCtrl { encodeURIComponent(fen).replace(/%20/g, '_').replace(/%2F/g, '/'); } + crazyValid = (role: cg.Role, key: cg.Key): boolean => { + const color = this.chessground.state.movable.color; + return ( + (color === 'white' || color === 'black') && + crazyValid(this.chessground, this.node.drops, { color, role }, key) + ); + }; + + getCrazyhousePockets = () => this.node.crazy?.pockets; + + sendNewPiece = (role: cg.Role, key: cg.Key): void => { + const color = this.chessground.state.movable.color; + if (color === 'white' || color === 'black') this.userNewPiece({ color, role }, key); + }; + userNewPiece = (piece: cg.Piece, pos: Key): void => { if (crazyValid(this.chessground, this.node.drops, piece, pos)) { this.justPlayed = roleToChar(piece.role).toUpperCase() + '@' + pos; @@ -555,6 +592,7 @@ export default class AnalyseCtrl { this.tree.addDests(dests, path); if (path === this.path) { this.showGround(); + this.pluginUpdate(this.node.fen); if (this.outcome()) this.ceval.stop(); } this.withCg(cg => cg.playPremove()); @@ -947,4 +985,29 @@ export default class AnalyseCtrl { withCg = (f: (cg: ChessgroundApi) => A): A | undefined => this.chessground && this.cgVersion.js === this.cgVersion.dom ? f(this.chessground) : undefined; + + handleArrowKey = (arrowKey: ArrowKey) => { + if (arrowKey === 'ArrowUp') { + if (this.fork.prev()) this.setAutoShapes(); + else control.first(this); + } else if (arrowKey === 'ArrowDown') { + if (this.fork.next()) this.setAutoShapes(); + else control.last(this); + } else if (arrowKey === 'ArrowLeft') control.prev(this); + else if (arrowKey === 'ArrowRight') control.next(this); + this.redraw(); + }; + + pluginMove = (orig: cg.Key, dest: cg.Key, prom: cg.Role | undefined) => { + const capture = this.chessground.state.pieces.get(dest); + this.sendMove(orig, dest, capture, prom); + }; + + pluginUpdate = (fen: string) => { + // if controller and chessground board state differ, ignore this update. once the chessground + // state is updated to match, pluginUpdate will be called again. + if (!fen.startsWith(this.chessground?.getFen())) return; + + this.keyboardMove?.update({ fen, canMove: true }); + }; } diff --git a/ui/analyse/src/ground.ts b/ui/analyse/src/ground.ts index 35272d58cd02d..775555db52c34 100644 --- a/ui/analyse/src/ground.ts +++ b/ui/analyse/src/ground.ts @@ -2,7 +2,6 @@ import { h, VNode } from 'snabbdom'; import { Api as CgApi } from 'chessground/api'; import { Config as CgConfig } from 'chessground/config'; import * as cg from 'chessground/types'; -import { DrawShape } from 'chessground/draw'; import resizeHandle from 'common/resize'; import AnalyseCtrl from './ctrl'; import * as Prefs from 'common/prefs'; @@ -10,12 +9,7 @@ import * as Prefs from 'common/prefs'; export const render = (ctrl: AnalyseCtrl): VNode => h('div.cg-wrap.cgv' + ctrl.cgVersion.js, { hook: { - insert: vnode => { - ctrl.chessground = site.makeChessground(vnode.elm as HTMLElement, makeConfig(ctrl)); - ctrl.setAutoShapes(); - if (ctrl.node.shapes) ctrl.chessground.setShapes(ctrl.node.shapes as DrawShape[]); - ctrl.cgVersion.dom = ctrl.cgVersion.js; - }, + insert: vnode => ctrl.setChessground(site.makeChessground(vnode.elm as HTMLElement, makeConfig(ctrl))), destroy: _ => ctrl.chessground.destroy(), }, }); diff --git a/ui/analyse/src/interfaces.ts b/ui/analyse/src/interfaces.ts index 06bd8fe34439c..065fbf2854332 100644 --- a/ui/analyse/src/interfaces.ts +++ b/ui/analyse/src/interfaces.ts @@ -65,6 +65,7 @@ export interface AnalysePref { highlight?: boolean; showCaptured?: boolean; animationDuration?: number; + keyboardMove: boolean; moveEvent: Prefs.MoveEvent; } diff --git a/ui/analyse/src/retrospect/retroCtrl.ts b/ui/analyse/src/retrospect/retroCtrl.ts index c52c2e438c5fc..c0ae5d3b2b67d 100644 --- a/ui/analyse/src/retrospect/retroCtrl.ts +++ b/ui/analyse/src/retrospect/retroCtrl.ts @@ -78,7 +78,7 @@ export function make(root: AnalyseCtrl, color: Color): RetroCtrl { } const fault = { node, - path: root.mainlinePathToPly(node.ply), + path: root.mainlinePlyToPath(node.ply), }; const prevPath = treePath.init(fault.path); const prev = { diff --git a/ui/analyse/src/view/main.ts b/ui/analyse/src/view/main.ts index 05e3af29df555..b52b70ffe83ed 100644 --- a/ui/analyse/src/view/main.ts +++ b/ui/analyse/src/view/main.ts @@ -9,6 +9,7 @@ import crazyView from '../crazy/crazyView'; import AnalyseCtrl from '../ctrl'; import forecastView from '../forecast/forecastView'; import { view as keyboardView } from '../keyboard'; +import { render as renderKeyboardMove } from 'keyboardMove'; import type * as studyDeps from '../study/studyDeps'; import { relayView } from '../study/relay/relayView'; import { @@ -42,6 +43,7 @@ function analyseView(ctrl: AnalyseCtrl, deps?: typeof studyDeps): VNode { !menuIsOpen && crazyView(ctrl, ctrl.bottomColor(), 'bottom'), !gamebookPlayView && renderControls(ctrl), renderUnderboard(ctx), + ctrl.keyboardMove && renderKeyboardMove(ctrl.keyboardMove), trainingView(ctrl), ctrl.studyPractice ? deps?.studyPracticeView.side(study!) diff --git a/ui/analyse/tsconfig.json b/ui/analyse/tsconfig.json index 001ae80d34790..8def919153a86 100644 --- a/ui/analyse/tsconfig.json +++ b/ui/analyse/tsconfig.json @@ -11,6 +11,7 @@ { "path": "../chess/tsconfig.json" }, { "path": "../common/tsconfig.json" }, { "path": "../game/tsconfig.json" }, + { "path": "../keyboardMove/tsconfig.json" }, { "path": "../nvui/tsconfig.json" }, { "path": "../tree/tsconfig.json" } ], diff --git a/ui/ceval/src/view/main.ts b/ui/ceval/src/view/main.ts index 77228cc00a347..addf901d518c2 100644 --- a/ui/ceval/src/view/main.ts +++ b/ui/ceval/src/view/main.ts @@ -1,7 +1,7 @@ import * as winningChances from '../winningChances'; import * as licon from 'common/licon'; import { stepwiseScroll } from 'common/scroll'; -import { bind, LooseVNodes, looseH as h } from 'common/snabbdom'; +import { onInsert, bind, LooseVNodes, looseH as h } from 'common/snabbdom'; import { defined, notNull } from 'common'; import { ParentCtrl, NodeEvals, CevalState } from '../types'; import { VNode } from 'snabbdom'; @@ -236,7 +236,10 @@ export function renderCeval(ctrl: ParentCtrl): LooseVNodes { h('div.switch', { attrs: { title: trans.noarg('toggleLocalEvaluation') + ' (L)' } }, [ h('input#analyse-toggle-ceval.cmn-toggle.cmn-toggle--subtle', { attrs: { type: 'checkbox', checked: enabled, disabled: !ceval.analysable }, - hook: bind('change', ctrl.toggleCeval), + hook: onInsert((el: HTMLInputElement) => { + el.addEventListener('keydown', e => (e.key === 'Enter' || e.key === ' ') && ctrl.toggleCeval()); + el.addEventListener('change', () => ctrl.toggleCeval()); + }), }), h('label', { attrs: { for: 'analyse-toggle-ceval' } }), ]); diff --git a/ui/chess/src/moveRootCtrl.ts b/ui/chess/src/moveRootCtrl.ts index 0e346801d280e..d01ef94d3084a 100644 --- a/ui/chess/src/moveRootCtrl.ts +++ b/ui/chess/src/moveRootCtrl.ts @@ -1,7 +1,7 @@ import * as cg from 'chessground/types'; export interface MoveRootCtrl { - auxMove: (orig: cg.Key, dest: cg.Key, prom: cg.Role | undefined) => void; + pluginMove: (orig: cg.Key, dest: cg.Key, prom: cg.Role | undefined) => void; redraw: () => void; flipNow: () => void; offerDraw?: (v: boolean, immediately?: boolean) => void; diff --git a/ui/keyboardMove/src/ctrl.ts b/ui/keyboardMove/src/ctrl.ts index 0e7a0e46f7f16..71837733b78dc 100644 --- a/ui/keyboardMove/src/ctrl.ts +++ b/ui/keyboardMove/src/ctrl.ts @@ -9,6 +9,10 @@ import KeyboardChecker from './keyboardChecker'; export type KeyboardMoveHandler = (fen: cg.FEN, dests?: cg.Dests, yourMove?: boolean) => void; +export const arrowKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'] as const; +export type ArrowKey = (typeof arrowKeys)[number]; +export const isArrowKey = (v: string): v is ArrowKey => arrowKeys.includes(v as ArrowKey); + export interface KeyboardMove { drop(key: cg.Key, piece: string): void; promote(orig: cg.Key, dest: cg.Key, piece: string): void; @@ -20,7 +24,7 @@ export interface KeyboardMove { hasSelected(): cg.Key | undefined; confirmMove(): void; usedSan: boolean; - jump(delta: number): void; + arrowNavigate(arrowKey: ArrowKey): void; justSelected(): boolean; draw(): void; next(): void; @@ -47,18 +51,18 @@ interface CrazyPocket { } export interface RootData { - crazyhouse?: { pockets: [CrazyPocket, CrazyPocket] }; game: { variant: { key: VariantKey } }; - player: { color: Color }; + player: { color: Color | 'both' }; opponent?: { color: Color; user?: { username: string } }; } export interface KeyboardMoveRootCtrl extends MoveRootCtrl { sendNewPiece?: (role: cg.Role, key: cg.Key, isPredrop: boolean) => void; userJumpPlyDelta?: (plyDelta: Ply) => void; - sendMove?: (orig: cg.Key, dest: cg.Key, prom: cg.Role | undefined, meta: cg.MoveMetadata) => void; + handleArrowKey?: (arrowKey: ArrowKey) => void; submitMove?: (v: boolean) => void; crazyValid?: (role: cg.Role, key: cg.Key) => boolean; + getCrazyhousePockets?: () => [CrazyPocket, CrazyPocket] | undefined; data: RootData; } @@ -80,14 +84,16 @@ export function ctrl(root: KeyboardMoveRootCtrl): KeyboardMove { return { drop(key, piece) { const role = sanToRole[piece]; - const crazyData = root.data.crazyhouse; - const color = root.data.player.color; + const crazyhousePockets = root.getCrazyhousePockets?.(); + const color = root.data.player.color === 'both' ? cg.state.movable.color : root.data.player.color; + // Unable to determine what color we are + if (!color || color === 'both') return; // Crazyhouse not set up properly if (!root.crazyValid || !root.sendNewPiece) return; // Square occupied - if (!role || !crazyData || cg.state.pieces.has(key)) return; + if (!role || !crazyhousePockets || cg.state.pieces.has(key)) return; // Piece not in Pocket - if (!crazyData.pockets[color === 'white' ? 0 : 1][role]) return; + if (!crazyhousePockets[color === 'white' ? 0 : 1][role]) return; if (!root.crazyValid(role, key)) return; cg.cancelMove(); cg.newPiece({ role, color }, key); @@ -99,7 +105,7 @@ export function ctrl(root: KeyboardMoveRootCtrl): KeyboardMove { if (!role || role == 'pawn' || (role == 'king' && variant !== 'antichess')) return; cg.cancelMove(); promote(cg, dest, role); - root.auxMove(orig, dest, role); + root.pluginMove(orig, dest, role); }, update(up: MoveUpdate) { if (up.cg) cg = up.cg; @@ -122,9 +128,19 @@ export function ctrl(root: KeyboardMoveRootCtrl): KeyboardMove { hasSelected: () => cg.state.selected, confirmMove: () => (root.submitMove ? root.submitMove(true) : null), usedSan, - jump(plyDelta: number) { - root.userJumpPlyDelta && root.userJumpPlyDelta(plyDelta); - root.redraw(); + arrowNavigate(arrowKey: ArrowKey) { + if (root.handleArrowKey) { + root.handleArrowKey?.(arrowKey); + return; + } + + const arrowKeyToPlyDelta = { + ArrowUp: -999, + ArrowDown: 999, + ArrowLeft: -1, + ArrowRight: 1, + }; + root.userJumpPlyDelta?.(arrowKeyToPlyDelta[arrowKey]); }, justSelected: () => performance.now() - lastSelect < 500, draw: () => (root.offerDraw ? root.offerDraw(true, true) : null), diff --git a/ui/keyboardMove/src/keyboardMove.test.ts b/ui/keyboardMove/src/keyboardMove.test.ts index fe81a08d41b28..6545f5d701905 100644 --- a/ui/keyboardMove/src/keyboardMove.test.ts +++ b/ui/keyboardMove/src/keyboardMove.test.ts @@ -25,7 +25,7 @@ const defaultCtrl = { vote: unexpectedErrorThrower('vote'), drop: unexpectedErrorThrower('drop'), hasSelected: () => undefined, - jump: () => null, + arrowNavigate: unexpectedErrorThrower('arrowNavigate'), justSelected: () => true, promote: unexpectedErrorThrower('promote'), registerHandler: () => null, diff --git a/ui/keyboardMove/src/keyboardMove.ts b/ui/keyboardMove/src/keyboardMove.ts index bc448777ca56a..0f000c7e45203 100644 --- a/ui/keyboardMove/src/keyboardMove.ts +++ b/ui/keyboardMove/src/keyboardMove.ts @@ -1,6 +1,6 @@ import { Dests, files } from 'chessground/types'; import { sanWriter, SanToUci, destsToUcis } from 'chess'; -import { KeyboardMoveHandler, KeyboardMove } from './ctrl'; +import { KeyboardMoveHandler, KeyboardMove, isArrowKey } from './ctrl'; const keyRegex = /^[a-h][1-8]$/; const fileRegex = /^[a-h]$/; @@ -11,6 +11,8 @@ const promotionRegex = /^([a-h]x?)?[a-h](1|8)=?[nbrqkNBRQK]$/; // accept partial ICCF because submit runs on every keypress const iccfRegex = /^[1-8][1-8]?[1-5]?$/; +const isKey = (v: string): v is Key => !!v.match(keyRegex); + interface SubmitOpts { isTrusted: boolean; force?: boolean; @@ -32,8 +34,6 @@ export function initModule(opts: Opts) { opts.input.classList.add('ready'); let legalSans: SanToUci | null = null; - const isKey = (v: string): v is Key => !!v.match(keyRegex); - const submit: Submit = (v: string, submitOpts: SubmitOpts) => { if (!submitOpts.isTrusted) return; // consider 0's as O's for castling @@ -97,8 +97,8 @@ export function initModule(opts: Opts) { clear(); } } else if (v.length > 0 && 'who'.startsWith(v.toLowerCase())) { - if ('who' === v.toLowerCase() && opts.ctrl.opponent) { - site.sound.say(opts.ctrl.opponent, false, true); + if ('who' === v.toLowerCase()) { + if (opts.ctrl.opponent) site.sound.say(opts.ctrl.opponent, false, true); clear(); } } else if (v.length > 0 && 'draw'.startsWith(v.toLowerCase())) { @@ -189,11 +189,8 @@ function makeBindings(opts: Opts, submit: Submit, clear: () => void) { opts.input.addEventListener('blur', () => opts.ctrl.isFocused(false)); // prevent default on arrow keys: they only replay moves opts.input.addEventListener('keydown', (e: KeyboardEvent) => { - if (e.which > 36 && e.which < 41) { - if (e.which == 37) opts.ctrl.jump(-1); - else if (e.which == 38) opts.ctrl.jump(-999); - else if (e.which == 39) opts.ctrl.jump(1); - else opts.ctrl.jump(999); + if (isArrowKey(e.key)) { + opts.ctrl.arrowNavigate(e.key); e.preventDefault(); } }); diff --git a/ui/puzzle/src/ctrl.ts b/ui/puzzle/src/ctrl.ts index 5af9a431f34ad..57aaacd378c84 100644 --- a/ui/puzzle/src/ctrl.ts +++ b/ui/puzzle/src/ctrl.ts @@ -137,8 +137,7 @@ export default class PuzzleCtrl implements ParentCtrl { game: { variant: { key: 'standard' } }, player: { color: this.pov }, }, - sendMove: this.playUserMove, - auxMove: this.auxMove, + pluginMove: this.pluginMove, redraw: this.redraw, flipNow: this.flip, userJumpPlyDelta: this.userJumpPlyDelta, @@ -252,7 +251,7 @@ export default class PuzzleCtrl implements ParentCtrl { showGround = (g: CgApi): void => g.set(this.makeCgOpts()); - auxMove = (orig: Key, dest: Key, role?: Role) => { + pluginMove = (orig: Key, dest: Key, role?: Role) => { if (role) this.playUserMove(orig, dest, role); else this.withGround(g => { @@ -262,7 +261,7 @@ export default class PuzzleCtrl implements ParentCtrl { }); }; - auxUpdate = (fen: string): void => { + pluginUpdate = (fen: string): void => { this.voiceMove?.update({ fen, canMove: true }); this.keyboardMove?.update({ fen, canMove: true }); }; @@ -273,7 +272,7 @@ export default class PuzzleCtrl implements ParentCtrl { !this.promotion.start(orig, dest, { submit: this.playUserMove, show: this.voiceMove?.promotionHook() }) ) this.playUserMove(orig, dest); - this.auxUpdate(this.node.fen); + this.pluginUpdate(this.node.fen); }; playUci = (uci: Uci): void => this.sendMove(parseUci(uci)!); @@ -540,7 +539,7 @@ export default class PuzzleCtrl implements ParentCtrl { this.promotion.cancel(); this.justPlayed = undefined; this.autoScrollRequested = true; - this.auxUpdate(this.node.fen); + this.pluginUpdate(this.node.fen); site.pubsub.emit('ply', this.node.ply); }; diff --git a/ui/round/src/ctrl.ts b/ui/round/src/ctrl.ts index f1fc588c6593f..c62ef8e3d1567 100644 --- a/ui/round/src/ctrl.ts +++ b/ui/round/src/ctrl.ts @@ -278,7 +278,7 @@ export default class RoundController implements MoveRootCtrl { this.chessground.set(config); if (s.san && isForwardStep) site.sound.move(s); this.autoScroll(); - this.auxUpdate(s.fen); + this.pluginUpdate(s.fen); site.pubsub.emit('ply', ply); return true; }; @@ -335,7 +335,7 @@ export default class RoundController implements MoveRootCtrl { this.redraw(); }; - auxMove = (orig: cg.Key, dest: cg.Key, role?: cg.Role) => { + pluginMove = (orig: cg.Key, dest: cg.Key, role?: cg.Role) => { if (!role) { this.chessground.move(orig, dest); // TODO look into possibility of making cg.Api.move function update player turn itself. @@ -347,7 +347,7 @@ export default class RoundController implements MoveRootCtrl { this.sendMove(orig, dest, role, { premove: false }); }; - auxUpdate = (fen: string) => { + pluginUpdate = (fen: string) => { this.voiceMove?.update({ fen, canMove: this.canMove() }); this.keyboardMove?.update({ fen, canMove: this.canMove() }); }; @@ -501,7 +501,7 @@ export default class RoundController implements MoveRootCtrl { } this.autoScroll(); this.onChange(); - this.auxUpdate(step.fen); + this.pluginUpdate(step.fen); site.sound.move({ ...o, filter: 'music' }); site.sound.saySan(step.san); return true; // prevents default socket pubsub @@ -509,6 +509,8 @@ export default class RoundController implements MoveRootCtrl { crazyValid = (role: cg.Role, key: cg.Key) => crazyValid(this.data, role, key); + getCrazyhousePockets = () => this.data.crazyhouse?.pockets; + private playPredrop = () => { return this.chessground.playPredrop(drop => { return crazyValid(this.data, drop.role, drop.key); @@ -537,7 +539,7 @@ export default class RoundController implements MoveRootCtrl { this.autoScroll(); this.onChange(); this.setLoading(false); - this.auxUpdate(d.steps[d.steps.length - 1].fen); + this.pluginUpdate(d.steps[d.steps.length - 1].fen); }; endWithData = (o: ApiEnd): void => { diff --git a/ui/voice/src/move/voice.move.ts b/ui/voice/src/move/voice.move.ts index 7d374d42ff7cf..7575ac2e756bc 100644 --- a/ui/voice/src/move/voice.move.ts +++ b/ui/voice/src/move/voice.move.ts @@ -366,7 +366,7 @@ export function initModule(opts: { root: MoveRootCtrl; ui: VoiceCtrl; initial: M const role = cs.promo(uci); cg.cancelMove(); if (role) promote(cg, dest(uci), role); - root.auxMove(src(uci), dest(uci), role); + root.pluginMove(src(uci), dest(uci), role); return true; }