Merge pull request #7772 from TTWNO/nvui-table-board

NVUI Round Additions
tos-wording
Thibault Duplessis 2021-01-14 11:43:08 +01:00 committed by GitHub
commit 51d8f389cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 531 additions and 24 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -7,18 +7,31 @@ import { makeConfig as makeCgConfig } from '../ground';
import { Chessground } from 'chessground';
import { Redraw, AnalyseData, MaybeVNodes } from '../interfaces';
import { Player } from 'game';
import { renderSan, renderPieces, renderBoard, styleSetting } from 'nvui/chess';
import { renderSan, renderPieces, renderBoard, styleSetting, pieceSetting, prefixSetting, boardSetting, positionSetting } from 'nvui/chess';
import { boardCommandsHandler, selectionHandler, arrowKeyHandler, positionJumpHandler, pieceJumpingHandler } from 'nvui/chess';
import { renderSetting } from 'nvui/setting';
import { Notify } from 'nvui/notify';
import { Style } from 'nvui/chess';
import { commands } from 'nvui/command';
import * as moveView from '../moveView';
import { bind } from '../util';
import throttle from 'common/throttle';
export const throttled = (sound: string) => throttle(100, () => lichess.sound.play(sound));
const selectSound = throttled('select');
const wrapSound = throttled('wrapAround');
const borderSound = throttled('outOfBound');
const errorSound = throttled('error');
lichess.AnalyseNVUI = function(redraw: Redraw) {
const notify = new Notify(redraw),
moveStyle = styleSetting(),
pieceStyle = pieceSetting(),
prefixStyle = prefixSetting(),
positionStyle = positionSetting(),
boardStyle = boardSetting(),
analysisInProgress = prop(false);
lichess.pubsub.on('analysis.server.progress', (data: AnalyseData) => {
@ -89,7 +102,19 @@ lichess.AnalyseNVUI = function(redraw: Redraw) {
h('h2', 'Computer analysis'),
...(renderAcpl(ctrl, style) || [requestAnalysisButton(ctrl, analysisInProgress, notify.set)]),
h('h2', 'Board'),
h('pre.board', renderBoard(ctrl.chessground.state.pieces, ctrl.data.player.color)),
h('div.board',
{hook: {
insert: (el) => {
const $board = $(el.elm as HTMLElement);
$board.on('keypress', boardCommandsHandler());
const $buttons = $board.find('button');
$buttons.on('click', selectionHandler(ctrl.data.opponent.color, selectSound));
$buttons.on('keydown', arrowKeyHandler(ctrl.data.player.color, borderSound));
$buttons.on('keypress', positionJumpHandler());
$buttons.on('keypress', pieceJumpingHandler(wrapSound, errorSound));
}
}},
renderBoard(ctrl.chessground.state.pieces, ctrl.data.player.color, pieceStyle.get(), prefixStyle.get(), positionStyle.get(), boardStyle.get())),
h('div.content', {
hook: {
insert: vnode => {
@ -102,10 +127,40 @@ lichess.AnalyseNVUI = function(redraw: Redraw) {
'Move notation',
renderSetting(moveStyle, ctrl.redraw)
]),
h('h3', 'Board Settings'),
h('label', [
'Piece style',
renderSetting(pieceStyle, ctrl.redraw)
]),
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)
]),
h('h2', 'Keyboard shortcuts'),
h('p', [
'Use arrow keys to navigate in the game.'
]),
h('h2', 'Board Mode commands'),
h('p', [
'Use these commands when focused on the board itself.', h('br'),
'o: announce current position.', h('br'),
'c: announce last move\'s captured piece.', h('br'),
'l: display last move.', h('br'),
't: display clocks.', h('br'),
'arrow keys: move left, right, up or down.', h('br'),
'kqrbnp/KQRBNP: move forward/backward to a piece.', h('br'),
'1-8: move to rank 1-8.', h('br'),
'Shift+1-8: move to file a-h.', h('br'),
'', h('br')
]),
h('h2', 'Commands'),
h('p', [
'Type these commands in the command input.', h('br'),

View File

@ -1,23 +1,57 @@
import { h } from 'snabbdom'
import { VNode } from 'snabbdom/vnode'
import { Pieces } from 'chessground/types';
import { Rank, File } from 'chessground/types';
import { invRanks, allKeys } from 'chessground/util';
import { Setting, makeSetting } from './setting';
import { files } from 'chessground/types';
import { parseFen } from 'chessops/fen';
import { Chess } from 'chessops/chess';
import { chessgroundDests } from 'chessops/compat';
import { SquareName } from 'chessops/types';
export type Style = 'uci' | 'san' | 'literate' | 'nato' | 'anna';
export type PieceStyle = 'letter' | 'white uppercase letter' | 'name' | 'white uppercase name';
export type PrefixStyle = 'letter' | 'name' | 'none';
export type PositionStyle = 'before' | 'after' | 'none';
export type BoardStyle = 'plain' | 'table'
const nato: { [letter: string]: string } = { a: 'alpha', b: 'bravo', c: 'charlie', d: 'delta', e: 'echo', f: 'foxtrot', g: 'golf', h: 'hotel' };
const anna: { [letter: string]: string } = { a: 'anna', b: 'bella', c: 'cesar', d: 'david', e: 'eva', f: 'felix', g: 'gustav', h: 'hector' };
const roles: { [letter: string]: string } = { P: 'pawn', R: 'rook', N: 'knight', B: 'bishop', Q: 'queen', K: 'king' };
const letters = { pawn: 'p', rook: 'r', knight: 'n', bishop: 'b', queen: 'q', king: 'k' };
const letterPiece: { [letter: string]: string} = { p: 'p', r: 'r', n: 'n', b: 'b', q: 'q', k: 'k',
P: 'p', R: 'r', N: 'n', B: 'b', Q: 'q', K: 'k'};
const whiteUpperLetterPiece: { [letter: string]: string} = { p: 'p', r: 'r', n: 'n', b: 'b', q: 'q', k: 'k',
P: 'P', R: 'R', N: 'N', B: 'B', Q: 'Q', K: 'K'};
const namePiece: { [letter: string]: string} = { p: 'pawn', r: 'rook', n: 'knight', b: 'bishop', q: 'queen', k: 'king',
P: 'pawn', R: 'rook', N: 'knight', B: 'bishop', Q: 'queen', K: 'king'};
const whiteUpperNamePiece: { [letter: string]: string} = { p: 'pawn', r: 'rook', n: 'knight', b: 'bishop', q: 'queen', k: 'king',
P: 'Pawn', R: 'Rook', N: 'Knight', B: 'Bishop', Q: 'Queen', K: 'King'};
const skipToFile: { [letter: string]: string} = {'!': 'a', '@': 'b', '#': 'c', '$': 'd', '%': 'e', '^': 'f', '&': 'g', '*': 'h'};
export function symbolToFile(char: string) {
return skipToFile[char] ?? "";
}
export function supportedVariant(key: string) {
return [
'standard', 'chess960', 'kingOfTheHill', 'threeCheck', 'fromPosition'
].includes(key);
}
export function boardSetting(): Setting<BoardStyle> {
return makeSetting<BoardStyle>({
choices: [
['plain', 'plain: layout with no semantic rows or columns'],
['table', 'table: layout using a table with rank and file columns and row headers']
],
default: 'plain',
storage: lichess.storage.make('nvui.boardLayout')
});
}
export function styleSetting(): Setting<Style> {
return makeSetting<Style>({
choices: [
@ -25,13 +59,94 @@ export function styleSetting(): Setting<Style> {
['uci', 'UCI: g1f3'],
['literate', 'Literate: knight takes f 3'],
['anna', 'Anna: knight takes felix 3'],
['nato', 'Nato: knight takes foxtrot 3']
['nato', 'Nato: knight takes foxtrot 3'],
],
default: 'anna', // all the rage in OTB blind chess tournaments
storage: lichess.storage.make('nvui.moveNotation')
});
}
export function pieceSetting(): Setting<PieceStyle> {
return makeSetting<PieceStyle>({
choices: [
['letter', 'Letter: p, p'],
['white uppercase letter', 'White uppecase letter: P, p'],
['name', 'Name: pawn, pawn'],
['white uppercase name', 'White uppercase name: Pawn, pawn']
],
default: 'letter',
storage: lichess.storage.make('nvui.pieceStyle')
});
}
export function prefixSetting(): Setting<PrefixStyle> {
return makeSetting<PrefixStyle>({
choices: [
['letter', 'Letter: w/b'],
['name', 'Name: white/black'],
['none', 'None']
],
default: 'letter',
storage: lichess.storage.make('nvui.prefixStyle')
});
}
export function positionSetting(): Setting<PositionStyle> {
return makeSetting<PositionStyle>({
choices: [
['before', 'before: c2: wp'],
['after', 'after: wp: c2'],
['none', 'none']
],
default: 'before',
storage: lichess.storage.make('nvui.positionStyle')
});
}
const renderPieceStyle = (piece: string, pieceStyle: PieceStyle) => {
switch(pieceStyle) {
case 'letter':
return letterPiece[piece];
case 'white uppercase letter':
return whiteUpperLetterPiece[piece];
case 'name':
return namePiece[piece];
case 'white uppercase name':
return whiteUpperNamePiece[piece];
}
}
const renderPrefixStyle = (color: Color, prefixStyle: PrefixStyle) => {
switch(prefixStyle) {
case 'letter':
return color.charAt(0);
case 'name':
return color + " ";
case 'none':
return '';
}
}
export function lastCaptured(movesGenerator: () => string[], pieceStyle: PieceStyle, prefixStyle: PrefixStyle): string {
const moves = movesGenerator();
const oldFen = moves[moves.length-2];
const newFen = moves[moves.length-1];
if (!oldFen || !newFen) {
return 'none';
}
const oldSplitFen = oldFen.split(' ')[0];
const newSplitFen = newFen.split(' ')[0];
for (var p of 'kKqQrRbBnNpP') {
const diff = (oldSplitFen.split(p).length - 1) - (newSplitFen.split(p).length -1);
const pcolor = p.toUpperCase() === p ? 'white' : 'black';
if (diff === 1) {
const prefix = renderPrefixStyle(pcolor, prefixStyle);
const piece = renderPieceStyle(p, pieceStyle);
return prefix + piece;
}
}
return 'none';
}
export function renderSan(san: San, uci: Uci | undefined, style: Style) {
if (!san) return '';
let move: string;
@ -95,26 +210,64 @@ export function renderPiecesOn(pieces: Pieces, rankOrFile: string, style: Style)
return res.length ? res.join(', ') : 'blank';
}
export function renderBoard(pieces: Pieces, pov: Color): string {
const board = [[' ', ...files, ' ']];
for (let rank of invRanks) {
let line = [];
for (let file of files) {
let key = file + rank as Key;
const piece = pieces.get(key);
if (piece) {
const letter = letters[piece.role];
line.push(piece.color === 'white' ? letter.toUpperCase() : letter);
} else line.push((key.charCodeAt(0) + key.charCodeAt(1)) % 2 ? '-' : '+');
export function renderBoard(pieces: Pieces, pov: Color, pieceStyle: PieceStyle, prefixStyle: PrefixStyle, positionStyle: PositionStyle, boardStyle: BoardStyle): VNode {
const doRankHeader = (rank: Rank): VNode => {
return h('th', {attrs: {scope: 'row'}}, rank);
}
const doFileHeaders = (): VNode => {
let ths = files.map(file => h('th', {attrs: {scope: 'col'}}, file));
if (pov === 'black') ths.reverse();
return h('tr', [
h('td'),
...ths,
h('td')
]);
}
const renderPositionStyle = (rank: Rank, file: File, orig: string) => {
switch(positionStyle) {
case 'before':
return file.toUpperCase() + rank + ' ' + orig;
case 'after':
return orig + ' ' + file.toUpperCase() + rank;
case 'none':
return orig;
}
board.push(['' + rank, ...line, '' + rank]);
}
board.push([' ', ...files, ' ']);
if (pov === 'black') {
board.reverse();
board.forEach(r => r.reverse());
const doPieceButton = (rank: Rank, file: File, letter: string, color: Color | 'none', text: string): VNode => {
return h('button', {
attrs: { rank: rank, file: file, piece: letter.toLowerCase(), color: color}
}, text);
}
return board.map(line => line.join(' ')).join('\n');
const doPiece = (rank: Rank, file: File): VNode => {
const key = file + rank as Key;
const piece = pieces.get(key);
const pieceWrapper = boardStyle === 'table' ? 'td' : 'span';
if (piece) {
const role = letters[piece.role];
const pieceText = renderPieceStyle(piece.color === 'white' ? role.toUpperCase() : role, pieceStyle);
const prefix = renderPrefixStyle(piece.color, prefixStyle);
const text = renderPositionStyle(rank, file, prefix + pieceText);
return h(pieceWrapper, doPieceButton(rank, file, role, piece.color, text));
} else {
const letter = (key.charCodeAt(0) + key.charCodeAt(1)) % 2 ? '-' : '+';
const text = renderPositionStyle(rank, file, letter);
return h(pieceWrapper, doPieceButton(rank, file, letter, 'none', text));
}
}
const doRank = (pov: Color, rank: Rank): VNode => {
let rankElements = [];
if (boardStyle === 'table') rankElements.push(doRankHeader(rank));
rankElements.push(...files.map(file => doPiece(rank, file)));
if (boardStyle === 'table') rankElements.push(doRankHeader(rank));
if (pov === 'black') rankElements.reverse();
return h((boardStyle === 'table' ? 'tr' : 'div'), rankElements);
}
let ranks: VNode[] = [];
if (boardStyle === 'table') ranks.push(doFileHeaders());
ranks.push(...invRanks.map(rank => doRank(pov, rank)));
if (boardStyle === 'table') ranks.push(doFileHeaders());
if (pov === 'black') ranks.reverse();
return h((boardStyle === 'table' ? 'table.board-wrapper' : 'div.board-wrapper'), ranks);
}
export function renderFile(f: string, style: Style): string {
@ -132,3 +285,217 @@ export function castlingFlavours(input: string): string {
}
return input;
}
/* Listen to interactions on the chessboard */
export function positionJumpHandler() {
return (ev: KeyboardEvent) => {
const $btn = $(ev.target as HTMLElement);
const $file = $btn.attr('file') ?? "";
const $rank = $btn.attr('rank') ?? "";
let $newRank = "";
let $newFile = "";
if (ev.key.match(/^[1-8]$/)) {
$newRank = ev.key;
$newFile = $file;
} else if (ev.key.match(/^[!@#$%^&*]$/)) {
$newRank = $rank;
$newFile = symbolToFile(ev.key);
// if not a valid key for jumping
} else {
return true;
}
const newBtn = document.querySelector('.board-wrapper button[rank="' + $newRank + '"][file="' + $newFile + '"]') as HTMLElement;
if (newBtn) {
newBtn.focus();
return false;
}
return true;
}
}
export function pieceJumpingHandler(wrapSound: () => void, errorSound: () => void) {
return (ev: KeyboardEvent) => {
if (!ev.key.match(/^[kqrbnp]$/i)) return true;
const $currBtn = $(ev.target as HTMLElement);
// TODO: decouple from promotion attribute setting in selectionHandler
if ($currBtn.attr('promotion') === 'true') {
const $moveBox = $('input.move');
const $boardLive = $('.boardstatus');
const $promotionPiece = ev.key.toLowerCase();
const $form = $moveBox.parent().parent();
if (!$promotionPiece.match(/^[qnrb]$/)) {
$boardLive.text('Invalid promotion piece. q for queen, n for knight, r for rook, b for bisho');
return false;
}
$moveBox.val($moveBox.val() + $promotionPiece);
$currBtn.removeAttr('promotion');
const $sendForm = new Event('submit', {
cancelable: true,
bubbles: true
});
$form.trigger($sendForm);
return false;
}
const $myBtnAttrs = '.board-wrapper [rank="' + $currBtn.attr('rank') + '"][file="' + $currBtn.attr('file') + '"]';
const $allPieces = $('.board-wrapper [piece="' + ev.key.toLowerCase() + '"], ' + $myBtnAttrs);
const $myPieceIndex = $allPieces.index($myBtnAttrs);
const $next = ev.key.toLowerCase() === ev.key;
const $prevNextPieces = $next ? $allPieces.slice($myPieceIndex+1) : $allPieces.slice(0, $myPieceIndex);
const $piece = $next ? $prevNextPieces.get(0) : $prevNextPieces.get($prevNextPieces.length-1);
if ($piece) {
$piece.focus();
// if detected any matching piece; one is the pice being clicked on,
} else if ($allPieces.length >= 2) {
const $wrapPiece = $next ? $allPieces.get(0): $allPieces.get($allPieces.length-1);
$wrapPiece?.focus();
wrapSound();
} else {
errorSound();
}
return false;
};
}
export function arrowKeyHandler(pov: Color, borderSound: () => void) {
return (ev: KeyboardEvent) => {
const $currBtn = $(ev.target as HTMLElement);
const $isWhite = pov === 'white';
let $file = $currBtn.attr('file') ?? " ";
let $rank = Number($currBtn.attr('rank'));
if (ev.key === 'ArrowUp') {
$rank = $isWhite ? $rank += 1 : $rank -= 1;
} else if (ev.key === 'ArrowDown') {
$rank = $isWhite ? $rank -= 1 : $rank += 1;
} else if (ev.key === 'ArrowLeft') {
$file = String.fromCharCode($isWhite ? $file.charCodeAt(0) - 1 : $file.charCodeAt(0) + 1);
} else if (ev.key === 'ArrowRight') {
$file = String.fromCharCode($isWhite ? $file.charCodeAt(0) + 1 : $file.charCodeAt(0) - 1);
} else {
return true;
}
const $newSq = document.querySelector('.board-wrapper [file="' + $file + '"][rank="' + $rank + '"]') as HTMLElement;
if ($newSq) {
$newSq.focus();
} else {
borderSound();
}
ev.preventDefault();
return false;
};
}
export function selectionHandler(opponentColor: Color, selectSound: () => void) {
return (ev: MouseEvent) => {
// this depends on the current document structure. This may not be advisable in case the structure wil change.
const $evBtn = $(ev.target as HTMLElement);
const $rank = $evBtn.attr('rank');
const $pos = ($evBtn.attr('file') ?? "") + $rank;
const $boardLive = $('.boardstatus');
const $promotionRank = opponentColor === 'black' ? '8' : '1';
const $moveBox = $(document.querySelector('input.move') as HTMLInputElement);
if (!$moveBox) return false;
// if no move in box yet
if ($moveBox.val() === '') {
// if user selects anothers' piece first
if ($evBtn.attr('color') === opponentColor) return false;
// as long as the user is selecting a piece and not a blank tile
if ($evBtn.text().match(/^[^\-+]+/g)) {
$moveBox.val($pos);
selectSound();
}
} else {
// if user selects their own piece second
if ($evBtn.attr('color') === (opponentColor === 'black' ? 'white' : 'black')) return false;
const $first = $moveBox.val();
const $firstPiece = $('.board-wrapper [file="' + $first[0] + '"][rank="' + $first[1] + '"]');
$moveBox.val($moveBox.val() + $pos);
// this is coupled to pieceJumpingHandler() noticing that the attribute is set and acting differently. TODO: make cleaner
// if pawn promotion
if ($rank === $promotionRank && $firstPiece.attr('piece')?.toLowerCase() === 'p') {
$evBtn.attr('promotion', 'true');
$boardLive.text('Promote to? q for queen, n for knight, r for rook, b for bishop');
return false;
}
// this section depends on the form being the granparent of the input.move box.
const $form = $moveBox.parent().parent();
const $event = new Event('submit', {
cancelable: true,
bubbles: true
})
$form.trigger($event);
}
return false;
};
}
export function boardCommandsHandler() {
return (ev: KeyboardEvent) => {
const $currBtn = $(ev.target as HTMLElement);
const $boardLive = $('.boardstatus');
const $position = ($currBtn.attr('file') ?? "") + ($currBtn.attr('rank') ?? "")
if (ev.key === 'o') {
$boardLive.text()
$boardLive.text($position);
return false;
} else if (ev.key === 'l') {
const $lastMove = $('p.lastMove').text();
$boardLive.text();
$boardLive.text($lastMove);
return false;
} else if (ev.key === 't') {
$boardLive.text();
$boardLive.text($('.nvui .botc').text() + ', ' + $('.nvui .topc').text());
return false;
}
return true;
};
}
export function lastCapturedCommandHandler(steps: () => string[], pieceStyle: PieceStyle, prefixStyle: PrefixStyle) {
return (ev: KeyboardEvent) => {
const $boardLive = $('.boardstatus');
if (ev.key === 'c') {
$boardLive.text();
$boardLive.text(lastCaptured(steps, pieceStyle, prefixStyle));
return false;
}
return true;
}
}
export function possibleMovesHandler(color: Color, fen: () => string, pieces: () => Pieces) {
return (ev: KeyboardEvent) => {
if (ev.key !== 'm' && ev.key !== 'M') return true;
const $boardLive = $('.boardstatus');
const $pieces = pieces();
const myTurnFen = color === 'white' ? 'w' : 'b';
const opponentTurnFen = color === 'white' ? 'b' : 'w';
const $btn = $(ev.target as HTMLElement);
const $pos = ($btn.attr('file') ?? "")
+ $btn.attr('rank') as SquareName;
// possible ineffecient to reparse fen; but seems to work when it is AND when it is not the users' turn.
const possibleMoves = chessgroundDests(
Chess.fromSetup(
parseFen(fen().replace(' ' + opponentTurnFen + ' ', ' ' + myTurnFen + ' ')).unwrap()).unwrap())
.get($pos)
?.map(i => {
const p = $pieces.get(i);
return p ? i + ' captures ' + p.role : i;
})
.filter(i => ev.key === 'm' || i.includes('captures'));
if (!possibleMoves) {
$boardLive.text("None");
// if filters out non-capturing moves
} else if (possibleMoves.length === 0) {
$boardLive.text("No captures")
} else {
$boardLive.text(possibleMoves.join(', '));
}
return false;
};
}

View File

@ -21,6 +21,21 @@
font-family: monospace;
font-size: 1.5em;
margin: 0;
line-height: 32px;
border-collapse: collapse;
text-align: center;
td, th {
width: 32px;
border: 1px solid black;
.black {
background-color: black;
color: white;
}
.white {
background-color: white;
color: black;
}
}
}
.pieces h3 {

View File

@ -12,16 +12,27 @@ import { plyStep } from '../round';
import { onInsert } from '../util';
import { Step, Dests, Position, Redraw } from '../interfaces';
import * as game from 'game';
import { renderSan, renderPieces, renderBoard, styleSetting } from 'nvui/chess';
import { renderSan, renderPieces, renderBoard, styleSetting, pieceSetting, prefixSetting, positionSetting, boardSetting } from 'nvui/chess';
import { renderSetting } from 'nvui/setting';
import { boardCommandsHandler, possibleMovesHandler, lastCapturedCommandHandler, selectionHandler, arrowKeyHandler, positionJumpHandler, pieceJumpingHandler } from 'nvui/chess';
import { Notify } from 'nvui/notify';
import { castlingFlavours, supportedVariant, Style } from 'nvui/chess';
import { commands } from 'nvui/command';
import { throttled } from '../sound';
const selectSound = throttled('select');
const wrapSound = throttled('wrapAround');
const borderSound = throttled('outOfBound');
const errorSound = throttled('error');
lichess.RoundNVUI = function(redraw: Redraw) {
const notify = new Notify(redraw),
moveStyle = styleSetting();
moveStyle = styleSetting(),
prefixStyle = prefixSetting(),
pieceStyle = pieceSetting(),
positionStyle = positionSetting(),
boardStyle = boardSetting();
lichess.pubsub.on('socket.in.message', line => {
if (line.u === 'lichess') notify.set(line.t);
@ -111,12 +122,49 @@ lichess.RoundNVUI = function(redraw: Redraw) {
game.playable(ctrl.data) ? renderTablePlay(ctrl) : renderTableEnd(ctrl)
)),
h('h2', 'Board'),
h('pre.board', renderBoard(ctrl.chessground.state.pieces, ctrl.data.player.color)),
h('div.board', {
hook: onInsert(el => {
const $board = $(el as HTMLElement);
$board.on('keypress', boardCommandsHandler());
$board.on('keypress', () => console.log(ctrl));
// NOTE: This is the only line different from analysis board listener setup
$board.on('keypress', lastCapturedCommandHandler(() => ctrl.data.steps.map(step => step.fen), pieceStyle.get(), prefixStyle.get()));
const $buttons = $board.find('button');
$buttons.on('click', selectionHandler(ctrl.data.opponent.color, selectSound));
$buttons.on('keydown', arrowKeyHandler(ctrl.data.player.color, borderSound));
$buttons.on('keypress', possibleMovesHandler(ctrl.data.player.color, ctrl.chessground.getFen, () => ctrl.chessground.state.pieces));
$buttons.on('keypress', positionJumpHandler());
$buttons.on('keypress', pieceJumpingHandler(wrapSound, errorSound));
})}, renderBoard(ctrl.chessground.state.pieces, ctrl.data.player.color, pieceStyle.get(), prefixStyle.get(), positionStyle.get(), boardStyle.get())),
h('div.boardstatus', {
attrs: {
'aria-live': 'polite',
'aria-atomic': true
}
}, ''),
// h('p', takes(ctrl.data.steps.map(data => data.fen))),
h('h2', 'Settings'),
h('label', [
'Move notation',
renderSetting(moveStyle, ctrl.redraw)
]),
h('h3', 'Board Settings'),
h('label', [
'Piece style',
renderSetting(pieceStyle, ctrl.redraw)
]),
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)
]),
h('h2', 'Commands'),
h('p', [
'Type these commands in the move input.', h('br'),
@ -130,6 +178,20 @@ lichess.RoundNVUI = function(redraw: Redraw) {
'draw: Offer or accept draw.', h('br'),
'takeback: Offer or accept take back.', h('br')
]),
h('h2', 'Board Mode commands'),
h('p', [
'Use these commands when focused on the board itself.', h('br'),
'o: announce current position.', h('br'),
'c: announce last move\'s captured piece.', h('br'),
'l: announce last move.', h('br'),
't: announce clocks.', h('br'),
'm: announce possible moves for the selected piece.', h('br'),
'shift+m: announce possible moves for the selected pieces which capture..', h('br'),
'arrow keys: move left, right, up or down.', h('br'),
'kqrbnp/KQRBNP: move forward/backward to a piece.', h('br'),
'1-8: move to rank 1-8.', h('br'),
'Shift+1-8: move to file a-h.', h('br'),
]),
h('h2', 'Promotion'),
h('p', [
'Standard PGN notation selects the piece to promote to. Example: a8=n promotes to a knight.',
@ -142,6 +204,7 @@ lichess.RoundNVUI = function(redraw: Redraw) {
}
const promotionRegex = /^([a-h]x?)?[a-h](1|8)=\w$/;
const uciPromotionRegex = /^([a-h][1-8])([a-h](1|8))[qrbn]$/;
function onSubmit(ctrl: RoundController, notify: (txt: string) => void, style: () => Style, $input: Cash) {
return () => {
@ -158,7 +221,14 @@ function onSubmit(ctrl: RoundController, notify: (txt: string) => void, style: (
if (input.match(promotionRegex)) {
uci = sanToUci(input.slice(0, -2), legalSans) || input;
promotion = input.slice(-1).toLowerCase();
} else if (input.match(uciPromotionRegex)) {
uci = input.slice(0, -1);
promotion = input.slice(-1).toLowerCase();
}
console.log(uci);
console.log(uci.slice(0, -1));
console.log(promotion);
console.log(legalSans);
if (legalUcis.includes(uci.toLowerCase())) ctrl.socket.send("move", {
u: uci + promotion

View File

@ -1,6 +1,6 @@
import throttle from 'common/throttle';
const throttled = (sound: string) => throttle(100, () => lichess.sound.play(sound));
export const throttled = (sound: string) => throttle(100, () => lichess.sound.play(sound));
export const move = throttled('move');
export const capture = throttled('capture');