Fix analysis board. Analysis board still not functional, but it does render now.

pull/7772/head
Tait Hoyem 2020-12-27 18:03:57 -07:00
parent 1b9f66352d
commit e9a78ac940
4 changed files with 242 additions and 167 deletions

View File

@ -7,13 +7,21 @@ 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, pieceSetting, prefixSetting } from 'nvui/chess';
import { renderSan, renderPieces, renderBoard, styleSetting, pieceSetting, prefixSetting, boardSetting, positionSetting, analysisBoardListenersSetup } 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) {
@ -21,6 +29,8 @@ lichess.AnalyseNVUI = function(redraw: Redraw) {
moveStyle = styleSetting(),
pieceStyle = pieceSetting(),
prefixStyle = prefixSetting(),
positionStyle = positionSetting(),
boardStyle = boardSetting(),
analysisInProgress = prop(false);
lichess.pubsub.on('analysis.server.progress', (data: AnalyseData) => {
@ -91,7 +101,13 @@ lichess.AnalyseNVUI = function(redraw: Redraw) {
h('h2', 'Computer analysis'),
...(renderAcpl(ctrl, style) || [requestAnalysisButton(ctrl, analysisInProgress, notify.set)]),
h('h2', 'Board'),
h('table.board', renderBoard(ctrl.chessground.state.pieces, ctrl.data.player.color, pieceSetting.get(), prefixStyle.get())),
h('div.board',
{hook: {
insert: (vnode) => {
analysisBoardListenersSetup(ctrl.data.player.color, ctrl.data.opponent.color, selectSound, wrapSound, borderSound, errorSound)(vnode.elm as HTMLElement);
}
}},
renderBoard(ctrl.chessground.state.pieces, ctrl.data.player.color, pieceStyle.get(), prefixStyle.get(), positionStyle.get(), boardStyle.get())),
h('div.content', {
hook: {
insert: vnode => {
@ -104,10 +120,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

@ -37,6 +37,14 @@ import relayIntro from './study/relay/relayIntroView';
import renderPlayerBars from './study/playerBars';
import serverSideUnderboard from './serverSideUnderboard';
import * as gridHacks from './gridHacks';
import throttle from 'common/throttle';
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');
function renderResult(ctrl: AnalyseCtrl): VNode[] {
let result: string | undefined;

View File

@ -122,14 +122,17 @@ const renderPrefixStyle = (color: Color, prefixStyle: PrefixStyle) => {
}
export function lastCaptured(moves: string[], pieceStyle: PieceStyle, prefixStyle: PrefixStyle): string {
const oldFen = moves[moves.length-2].split(' ')[0];
const newFen = moves[moves.length-1].split(' ')[0];
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 = (oldFen.split(p).length - 1) - (newFen.split(p).length -1);
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);
@ -260,7 +263,7 @@ export function renderBoard(pieces: Pieces, pov: Color, pieceStyle: PieceStyle,
ranks.push(...invRanks.map(rank => doRank(pov, rank)));
if (boardStyle === 'table') ranks.push(doFileHeaders());
if (pov === 'black') ranks.reverse();
return h((boardStyle === 'table' ? 'table' : 'div'), ranks);
return h((boardStyle === 'table' ? 'table.board-wrapper' : 'div.board-wrapper'), ranks);
}
export function renderFile(f: string, style: Style): string {
@ -278,3 +281,178 @@ export function castlingFlavours(input: string): string {
}
return input;
}
/* Listen to interactions on the chessboard */
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;
}
}
function pieceJumpingHandler(wrapSound: () => void, errorSound: () => void) {
return (ev: KeyboardEvent) => {
if (!ev.key.match(/^[kqrbnp]$/i)) return true;
const $currBtn = $(ev.target as HTMLElement);
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;
};
}
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;
};
}
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 $pos = ($evBtn.attr('file') ?? "") + $evBtn.attr('rank');
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;
$moveBox.val($moveBox.val() + $pos);
// 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;
};
}
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;
};
}
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 analysisBoardListenersSetup(color: Color, ocolor: Color, selectSound: () => void, wrapSound: () => void, borderSound: () => void, errorSound: () => void): (vnode: HTMLElement) => void {
return (el: HTMLElement) => {
const $board = $(el as HTMLElement);
$board.on('keypress', boardCommandsHandler());
const $buttons = $board.find('button');
$buttons.on('click', selectionHandler(ocolor, selectSound));
$buttons.on('keydown', arrowKeyHandler(color, borderSound));
$buttons.on('keypress', positionJumpHandler());
$buttons.on('keypress', pieceJumpingHandler(wrapSound, errorSound));
};
}
export function roundBoardListenersSetup(color: Color, ocolor: Color, steps: () => string[], pieceStyle: PieceStyle, prefixStyle: PrefixStyle, selectSound: () => void, wrapSound: () => void, borderSound: () => void, errorSound: () => void) {
return (el: HTMLElement) => {
console.log(steps);
console.log(steps());
const $board = $(el);
$board.on('keypress', boardCommandsHandler());
// NOTE: This is the only line different from analysisBoardListenerSetup
$board.on('keypress', lastCapturedCommandHandler(steps, pieceStyle, prefixStyle));
const $buttons = $board.find('button');
$buttons.on('click', selectionHandler(ocolor, selectSound));
$buttons.on('keydown', arrowKeyHandler(color, borderSound));
$buttons.on('keypress', positionJumpHandler());
$buttons.on('keypress', pieceJumpingHandler(wrapSound, errorSound));
};
}

View File

@ -12,10 +12,10 @@ 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, pieceSetting, prefixSetting, positionSetting, boardSetting, lastCaptured, PieceStyle, PrefixStyle} from 'nvui/chess';
import { renderSan, renderPieces, renderBoard, styleSetting, pieceSetting, prefixSetting, positionSetting, boardSetting, lastCaptured, PieceStyle, PrefixStyle } from 'nvui/chess';
import { renderSetting } from 'nvui/setting';
import { Notify } from 'nvui/notify';
import { castlingFlavours, supportedVariant, Style, symbolToFile } from 'nvui/chess';
import { castlingFlavours, supportedVariant, Style, symbolToFile, roundBoardListenersSetup } from 'nvui/chess';
import { commands } from 'nvui/command';
import { TourStandingCtrl } from '../tourStanding';
import { throttled } from '../sound';
@ -123,18 +123,7 @@ lichess.RoundNVUI = function(redraw: Redraw) {
)),
h('h2', 'Board'),
h('div.board', {
hook: onInsert(el => {
const $board = $(el as HTMLElement);
$board.on('keypress', boardCommandsHandler((): string[] => ctrl.data.steps.map(step => step.fen), pieceStyle.get(), prefixStyle.get()));
$board.on('keypress', showctrl(ctrl));
// looking for specific elements tightly couples this file and nvui/chess.ts
// unsure if a bad thing?
const $buttons = $board.find('button');
$buttons.on('click', selectionHandler((): Color => ctrl.data.opponent.color));
$buttons.on('keydown', arrowKeyHandler(ctrl.data.player.color));
$buttons.on('keypress', positionJumpHandler());
$buttons.on('keypress', pieceJumpingHandler());
})
hook: onInsert(roundBoardListenersSetup(ctrl.data.player.color, ctrl.data.opponent.color, () => ctrl.data.steps.map(step => step.fen), pieceStyle.get(), prefixStyle.get(), selectSound, wrapSound, borderSound, errorSound))
}, renderBoard(ctrl.chessground.state.pieces, ctrl.data.player.color, pieceStyle.get(), prefixStyle.get(), positionStyle.get(), boardStyle.get())),
h('div.boardstatus', {
attrs: {
@ -204,152 +193,6 @@ lichess.RoundNVUI = function(redraw: Redraw) {
const promotionRegex = /^([a-h]x?)?[a-h](1|8)=\w$/;
function showctrl(ctrl: RoundController) {
return () => {
console.log(ctrl);
return true;
}
}
function boardCommandsHandler(steps: () => string[], pieceStyle: PieceStyle, prefixStyle: PrefixStyle) {
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 === 'c') {
$boardLive.text();
$boardLive.text(lastCaptured(steps(), pieceStyle, prefixStyle));
return false;
} else if (ev.key === 't') {
$boardLive.text();
$boardLive.text($('.nvui .botc').text() + ', ' + $('.nvui .topc').text());
} else {
return true;
}
};
}
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 button[rank="' + $newRank + '"][file="' + $newFile + '"]') as HTMLElement;
if (newBtn) {
newBtn.focus();
return false;
}
return true;
}
}
function pieceJumpingHandler() {
return (ev: KeyboardEvent) => {
if (!ev.key.match(/^[kqrbnp]$/i)) return true;
const $currBtn = $(ev.target as HTMLElement);
const $myBtnAttrs = '.board [rank="' + $currBtn.attr('rank') + '"][file="' + $currBtn.attr('file') + '"]';
const $allPieces = $('.board [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;
};
}
function arrowKeyHandler(pov: Color) {
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 [file="' + $file + '"][rank="' + $rank + '"]') as HTMLElement;
if ($newSq) {
$newSq.focus();
} else {
borderSound();
}
ev.preventDefault();
return false;
};
}
function selectionHandler(opponentColor: () => Color) {
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 $pos = ($evBtn.attr('file') ?? "") + $evBtn.attr('rank');
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;
$moveBox.val($moveBox.val() + $pos);
// 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;
};
}
function onSubmit(ctrl: RoundController, notify: (txt: string) => void, style: () => Style, $input: Cash) {
return () => {
let input = castlingFlavours(($input.val() as string).trim());