lila/ui/round/src/plugins/keyboardMove.ts

171 lines
5.8 KiB
TypeScript

import { Dests } from '../interfaces';
import { sanWriter, SanToUci } from './sanWriter';
import { KeyboardMove } from '../keyboardMove';
const keyRegex = /^[a-h][1-8]$/;
const fileRegex = /^[a-h]$/;
const crazyhouseRegex = /^\w?@[a-h][1-8]$/;
const ambiguousPromotionCaptureRegex = /^([a-h]x?)?[a-h](1|8)$/;
const promotionRegex = /^([a-h]x?)?[a-h](1|8)=?[nbrqNBRQ]$/;
interface Opts {
input: HTMLInputElement;
ctrl: KeyboardMove;
}
interface SubmitOpts {
isTrusted: boolean;
force?: boolean;
yourMove?: boolean;
}
type Submit = (v: string, submitOpts: SubmitOpts) => void;
lichess.keyboardMove = function (opts: Opts) {
if (opts.input.classList.contains('ready')) return;
opts.input.classList.add('ready');
let legalSans: SanToUci | null = null;
const isKey = (v: string): v is Key => !!v.match(keyRegex);
const submit: Submit = function (v: string, submitOpts: SubmitOpts) {
if (!submitOpts.isTrusted) return;
// consider 0's as O's for castling
v = v.replace(/0/g, 'O');
const foundUci = v.length >= 2 && legalSans && sanToUci(v, legalSans);
if (v == 'resign') {
opts.ctrl.resign(true, true);
clear();
} else if (legalSans && foundUci) {
// ambiguous castle
if (v.toLowerCase() === 'o-o' && legalSans['O-O-O'] && !submitOpts.force) return;
// ambiguous UCI
if (isKey(v) && opts.ctrl.hasSelected()) opts.ctrl.select(v);
// ambiguous promotion (also check legalSans[v] here because bc8 could mean Bc8)
if (v.match(ambiguousPromotionCaptureRegex) && legalSans[v] && !submitOpts.force) return;
else opts.ctrl.san(foundUci.slice(0, 2) as Key, foundUci.slice(2) as Key);
clear();
} else if (legalSans && isKey(v)) {
opts.ctrl.select(v);
clear();
} else if (legalSans && v.match(fileRegex)) {
// do nothing
} else if (legalSans && v.match(promotionRegex)) {
const foundUci = sanToUci(v.replace('=', '').slice(0, -1), legalSans);
if (!foundUci) return;
opts.ctrl.promote(foundUci.slice(0, 2) as Key, foundUci.slice(2) as Key, v.slice(-1).toUpperCase());
clear();
} else if (v.match(crazyhouseRegex)) {
if (v.length === 3) v = 'P' + v;
opts.ctrl.drop(v.slice(2) as Key, v[0].toUpperCase());
clear();
} else if (v.length > 0 && 'clock'.startsWith(v.toLowerCase())) {
if ('clock' === v.toLowerCase()) {
readClocks(opts.ctrl.clock());
clear();
}
} else if (submitOpts.yourMove && v.length > 1) {
setTimeout(() => lichess.sound.play('error'), 500);
opts.input.value = '';
} else {
const wrong = v.length && legalSans && !sanCandidates(v, legalSans).length;
if (wrong && !opts.input.classList.contains('wrong')) lichess.sound.play('error');
opts.input.classList.toggle('wrong', !!wrong);
}
};
const clear = () => {
opts.input.value = '';
opts.input.classList.remove('wrong');
};
makeBindings(opts, submit, clear);
return (fen: string, dests: Dests | undefined, yourMove: boolean) => {
legalSans = dests && dests.size > 0 ? sanWriter(fen, destsToUcis(dests)) : null;
submit(opts.input.value, {
isTrusted: true,
yourMove: yourMove,
});
};
};
function makeBindings(opts: any, submit: Submit, clear: () => void) {
window.Mousetrap.bind('enter', () => opts.input.focus());
/* keypress doesn't cut it here;
* at the time it fires, the last typed char
* is not available yet. Reported by:
* https://lichess.org/forum/lichess-feedback/keyboard-input-changed-today-maybe-a-bug
*/
opts.input.addEventListener('keyup', (e: KeyboardEvent) => {
if (!e.isTrusted) return;
const v = (e.target as HTMLInputElement).value;
if (v.includes('/')) {
focusChat();
clear();
} else if (v === '' && e.which == 13) opts.ctrl.confirmMove();
else
submit(v, {
force: e.which === 13,
isTrusted: e.isTrusted,
});
});
opts.input.addEventListener('focus', () => opts.ctrl.setFocus(true));
opts.input.addEventListener('blur', () => opts.ctrl.setFocus(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);
e.preventDefault();
}
});
}
function sanToUci(san: string, legalSans: SanToUci): Uci | undefined {
if (san in legalSans) return legalSans[san];
const lowered = san.toLowerCase();
for (const i in legalSans) if (i.toLowerCase() === lowered) return legalSans[i];
return;
}
function sanCandidates(san: string, legalSans: SanToUci): San[] {
// replace '=' in promotion moves (#7326)
const lowered = san.replace('=', '').toLowerCase();
return Object.keys(legalSans).filter(function (s) {
return s.toLowerCase().startsWith(lowered);
});
}
function destsToUcis(dests: Dests): Uci[] {
const ucis: string[] = [];
for (const [orig, d] of dests) {
d.forEach(function (dest) {
ucis.push(orig + dest);
});
}
return ucis;
}
function focusChat() {
const chatInput = document.querySelector('.mchat .mchat__say') as HTMLInputElement;
if (chatInput) chatInput.focus();
}
function readClocks(clockCtrl: any | undefined) {
if (!clockCtrl) return;
const msgs = ['white', 'black'].map(color => {
const time = clockCtrl.millisOf(color);
const date = new Date(time);
const msg =
(time >= 3600000 ? simplePlural(Math.floor(time / 3600000), 'hour') : '') +
' ' +
simplePlural(date.getUTCMinutes(), 'minute') +
' ' +
simplePlural(date.getUTCSeconds(), 'second');
return `${color}: ${msg}`;
});
lichess.sound.say(msgs.join('. '));
}
function simplePlural(nb: number, word: string) {
return `${nb} ${word}${nb != 1 ? 's' : ''}`;
}