diff --git a/ui/analyse/src/plugins/analyse.nvui.ts b/ui/analyse/src/plugins/analyse.nvui.ts index 879f83a61048..5b0c05ca0f48 100644 --- a/ui/analyse/src/plugins/analyse.nvui.ts +++ b/ui/analyse/src/plugins/analyse.nvui.ts @@ -25,6 +25,7 @@ import { castlingFlavours, inputToLegalUci, lastCapturedCommandHandler, + type DropMove, } from 'nvui/chess'; import { renderSetting } from 'nvui/setting'; import { Notify } from 'nvui/notify'; @@ -317,10 +318,8 @@ function onSubmit( if (isShortCommand(input)) input = '/' + input; if (input[0] === '/') onCommand(ctrl, notify, input.slice(1), style()); else { - const uci = inputToLegalUci(input, ctrl.node.fen, ctrl.chessground); - if (uci) - ctrl.sendMove(uci.slice(0, 2) as Key, uci.slice(2, 4) as Key, undefined, charToRole(uci.slice(4))); - else notify('Invalid command'); + const uciOrDrop = inputToLegalUci(input, ctrl.node.fen, ctrl.chessground); + uciOrDrop ? sendMove(uciOrDrop, ctrl) : notify('Invalid command'); } $input.val(''); }; @@ -329,6 +328,17 @@ function onSubmit( const isShortCommand = (input: string) => ['p', 's', 'next', 'prev', 'eval', 'best'].includes(input.split(' ')[0].toLowerCase()); +function sendMove(uciOrDrop: string | DropMove, ctrl: AnalyseController) { + if (typeof uciOrDrop === 'string') + ctrl.sendMove( + uciOrDrop.slice(0, 2) as Key, + uciOrDrop.slice(2, 4) as Key, + undefined, + charToRole(uciOrDrop.slice(4)), + ); + else if (ctrl.crazyValid(uciOrDrop.role, uciOrDrop.key)) ctrl.sendNewPiece(uciOrDrop.role, uciOrDrop.key); +} + function onCommand(ctrl: AnalyseController, notify: (txt: string) => void, c: string, style: MoveStyle) { const lowered = c.toLowerCase(); if (lowered === 'next') doAndRedraw(ctrl, next); @@ -354,7 +364,7 @@ function renderAcpl(ctrl: AnalyseController, style: MoveStyle): MaybeVNodes | un const analysisNodes = ctrl.mainline.filter(n => n.glyphs?.find(g => analysisGlyphs.includes(g.symbol))); const res: Array = []; ['white', 'black'].forEach((color: Color) => { - res.push(h('h3', `${color} player: ${anal[color].acpl} ACPL`)); + res.push(h('h3', `${color} player: ${anal[color].acpl} ${i18n.site.averageCentipawnLoss}`)); res.push( h( 'select', diff --git a/ui/nvui/src/chess.ts b/ui/nvui/src/chess.ts index 9020411ed9be..fa59973e5d5c 100644 --- a/ui/nvui/src/chess.ts +++ b/ui/nvui/src/chess.ts @@ -43,8 +43,6 @@ const anna: { [file in Files]: string } = { h: 'hector', }; -export const supportedVariant = (key: VariantKey): boolean => key !== 'crazyhouse'; - export function boardSetting(): Setting { return makeSetting({ choices: [ @@ -169,6 +167,15 @@ export const renderPieces = (pieces: Pieces, style: MoveStyle): VNode => ), ); +type CrazyPocket = { [role in Role]?: number }; +export const renderPockets = (pockets: [CrazyPocket, CrazyPocket]): VNode[] => + COLORS.map((color, i) => h('h2', `${color} pocket: ${pocketsStr(pockets[i])}`)); + +export const pocketsStr = (pocket: CrazyPocket): string => + Object.entries(pocket) + .map(([role, count]) => `${role}: ${count}`) + .join(', '); + const keysWithPiece = (pieces: Pieces, role?: Role, color?: Color): Key[] => Array.from(pieces).reduce( (keys, [key, p]) => (p.color === color && p.role === role ? keys.concat(key) : keys), @@ -476,8 +483,10 @@ export function possibleMovesHandler( const promotionRegex = /^([a-h]x?)?[a-h](1|8)=[kqnbr]$/; const uciPromotionRegex = /^([a-h][1-8])([a-h](1|8))[kqnbr]$/; +const dropRegex = /^([qrnb])@([a-h][1-8])|p?@([a-h][2-7])$/; +export type DropMove = { role: Role; key: Key }; -export function inputToLegalUci(input: string, fen: string, chessground: CgApi): string | undefined { +export function inputToLegalUci(input: string, fen: string, chessground: CgApi): Uci | DropMove | undefined { const dests = chessground.state.movable.dests; if (!dests) return; const legalUcis = destsToUcis(dests), @@ -485,6 +494,8 @@ export function inputToLegalUci(input: string, fen: string, chessground: CgApi): let uci = sanToUci(input, legalSans) || input, promotion = ''; + const drop = input.match(dropRegex); + if (drop) return { role: charToRole(input[0]) || 'pawn', key: input.split('@')[1] as Key }; if (input.match(promotionRegex)) { uci = sanToUci(input.slice(0, -2), legalSans) || input; promotion = input.slice(-1).toLowerCase(); @@ -494,8 +505,7 @@ export function inputToLegalUci(input: string, fen: string, chessground: CgApi): } else if ('18'.includes(uci[3]) && chessground.state.pieces.get(uci.slice(0, 2) as Key)?.role === 'pawn') promotion = 'q'; - if (legalUcis.includes(uci.toLowerCase())) return uci + promotion; - else return; + return legalUcis.includes(uci.toLowerCase()) ? `${uci}${promotion}` : undefined; } export function renderMainline(nodes: Tree.Node[], currentPath: Tree.Path, style: MoveStyle): VNodeChildren { diff --git a/ui/puzzle/src/plugins/puzzle.nvui.ts b/ui/puzzle/src/plugins/puzzle.nvui.ts index 092a0ff9c295..3582f55b641d 100644 --- a/ui/puzzle/src/plugins/puzzle.nvui.ts +++ b/ui/puzzle/src/plugins/puzzle.nvui.ts @@ -222,7 +222,7 @@ function onSubmit( if (input[0] === '/') onCommand(ctrl, notify, input.slice(1), style()); else { const uci = inputToLegalUci(input, ctrl.node.fen, ground); - if (uci) { + if (uci && typeof uci === 'string') { ctrl.playUci(uci); const fback = ctrl.lastFeedback; if (fback === 'fail') notify(i18n.puzzle.notTheMove); diff --git a/ui/round/src/plugins/round.nvui.ts b/ui/round/src/plugins/round.nvui.ts index de6e451fb40e..880c3541a463 100644 --- a/ui/round/src/plugins/round.nvui.ts +++ b/ui/round/src/plugins/round.nvui.ts @@ -26,8 +26,9 @@ import { positionJumpHandler, pieceJumpingHandler, castlingFlavours, - supportedVariant, inputToLegalUci, + renderPockets, + type DropMove, } from 'nvui/chess'; import { renderSetting } from 'nvui/setting'; import { Notify } from 'nvui/notify'; @@ -69,6 +70,7 @@ export function initModule(): NvuiPlugin { nvui = ctrl.nvui!, step = plyStep(d, ctrl.ply), style = moveStyle.get(), + pockets = ctrl.data.crazyhouse?.pockets, clocks = [anyClock(ctrl, 'bottom'), anyClock(ctrl, 'top')]; if (!ctrl.chessground) { ctrl.setChessground( @@ -79,7 +81,6 @@ export function initModule(): NvuiPlugin { coordinates: false, }), ); - if (variantNope) setTimeout(() => notify.set(variantNope), 3000); } return h('div.nvui', { hook: onInsert(_ => setTimeout(() => notify.set(gameText(ctrl)), 2000)) }, [ h('h1', gameText(ctrl)), @@ -93,6 +94,7 @@ export function initModule(): NvuiPlugin { h('p.moves', { attrs: { role: 'log', 'aria-live': 'off' } }, renderMoves(d.steps.slice(1), style)), h('h2', 'Pieces'), h('div.pieces', renderPieces(ctrl.chessground.state.pieces, style)), + pockets && h('div.pockets', renderPockets(pockets)), h('h2', 'Game status'), h('div.status', { attrs: { role: 'status', 'aria-live': 'assertive', 'aria-atomic': 'true' } }, [ ctrl.data.game.status.name === 'started' ? i18n.site.playingRightNow : renderResult(ctrl), @@ -129,8 +131,6 @@ export function initModule(): NvuiPlugin { type: 'text', autocomplete: 'off', autofocus: true, - disabled: !!variantNope, - title: variantNope, }, }), ]), @@ -250,15 +250,17 @@ function createSubmitHandler( } else notify('Invalid move'); } - let input = submitStoredPremove ? nvui.premoveInput : castlingFlavours(($input.val() as string).trim()); + let input = submitStoredPremove + ? nvui.premoveInput + : castlingFlavours(($input.val() as string).trim().toLowerCase()); if (!input) return; // commands may be submitted with or without a leading / const command = isShortCommand(input) || isShortCommand(input.slice(1)); if (command) onCommand(ctrl, notify, command, style(), input); else { - const uci = inputToLegalUci(input, plyStep(ctrl.data, ctrl.ply).fen, ctrl.chessground); - if (uci) ctrl.socket.send('move', { u: uci }, { ackable: true }); + const uciOrDrop = inputToLegalUci(input, plyStep(ctrl.data, ctrl.ply).fen, ctrl.chessground); + if (uciOrDrop) sendMove(uciOrDrop, ctrl, !!nvui.premoveInput); else if (ctrl.data.player.color !== ctrl.data.game.player) { // if it is not the user's turn, store this input as a premove nvui.premoveInput = input; @@ -288,6 +290,12 @@ type ShortCommand = (typeof shortCommands)[number]; const isShortCommand = (input: string): ShortCommand | undefined => shortCommands.find(c => c === input.split(' ')[0].toLowerCase()); +function sendMove(uciOrDrop: string | DropMove, ctrl: RoundController, premove: boolean): void { + if (typeof uciOrDrop === 'string') ctrl.socket.send('move', { u: uciOrDrop }, { ackable: true }); + else if (ctrl.crazyValid(uciOrDrop.role, uciOrDrop.key)) + ctrl.sendNewPiece(uciOrDrop.role, uciOrDrop.key, premove); +} + function onCommand( ctrl: RoundController, notify: (txt: string) => void,