chessground
Version:
lichess.org chess ui
372 lines (335 loc) • 11.9 kB
text/typescript
import { HeadlessState } from './state.js';
import { pos2key, key2pos, opposite, distanceSq, allPos, computeSquareCenter } from './util.js';
import { premove, queen, knight } from './premove.js';
import * as cg from './types.js';
export function callUserFunction<T extends (...args: any[]) => void>(
f: T | undefined,
...args: Parameters<T>
): void {
if (f) setTimeout(() => f(...args), 1);
}
export function toggleOrientation(state: HeadlessState): void {
state.orientation = opposite(state.orientation);
state.animation.current = state.draggable.current = state.selected = undefined;
}
export function reset(state: HeadlessState): void {
state.lastMove = undefined;
unselect(state);
unsetPremove(state);
unsetPredrop(state);
}
export function setPieces(state: HeadlessState, pieces: cg.PiecesDiff): void {
for (const [key, piece] of pieces) {
if (piece) state.pieces.set(key, piece);
else state.pieces.delete(key);
}
}
export function setCheck(state: HeadlessState, color: cg.Color | boolean): void {
state.check = undefined;
if (color === true) color = state.turnColor;
if (color)
for (const [k, p] of state.pieces) {
if (p.role === 'king' && p.color === color) {
state.check = k;
}
}
}
function setPremove(state: HeadlessState, orig: cg.Key, dest: cg.Key, meta: cg.SetPremoveMetadata): void {
unsetPredrop(state);
state.premovable.current = [orig, dest];
callUserFunction(state.premovable.events.set, orig, dest, meta);
}
export function unsetPremove(state: HeadlessState): void {
if (state.premovable.current) {
state.premovable.current = undefined;
callUserFunction(state.premovable.events.unset);
}
}
function setPredrop(state: HeadlessState, role: cg.Role, key: cg.Key): void {
unsetPremove(state);
state.predroppable.current = { role, key };
callUserFunction(state.predroppable.events.set, role, key);
}
export function unsetPredrop(state: HeadlessState): void {
const pd = state.predroppable;
if (pd.current) {
pd.current = undefined;
callUserFunction(pd.events.unset);
}
}
function tryAutoCastle(state: HeadlessState, orig: cg.Key, dest: cg.Key): boolean {
if (!state.autoCastle) return false;
const king = state.pieces.get(orig);
if (!king || king.role !== 'king') return false;
const origPos = key2pos(orig);
const destPos = key2pos(dest);
if ((origPos[1] !== 0 && origPos[1] !== 7) || origPos[1] !== destPos[1]) return false;
if (origPos[0] === 4 && !state.pieces.has(dest)) {
if (destPos[0] === 6) dest = pos2key([7, destPos[1]]);
else if (destPos[0] === 2) dest = pos2key([0, destPos[1]]);
}
const rook = state.pieces.get(dest);
if (!rook || rook.color !== king.color || rook.role !== 'rook') return false;
state.pieces.delete(orig);
state.pieces.delete(dest);
if (origPos[0] < destPos[0]) {
state.pieces.set(pos2key([6, destPos[1]]), king);
state.pieces.set(pos2key([5, destPos[1]]), rook);
} else {
state.pieces.set(pos2key([2, destPos[1]]), king);
state.pieces.set(pos2key([3, destPos[1]]), rook);
}
return true;
}
export function baseMove(state: HeadlessState, orig: cg.Key, dest: cg.Key): cg.Piece | boolean {
const origPiece = state.pieces.get(orig),
destPiece = state.pieces.get(dest);
if (orig === dest || !origPiece) return false;
const captured = destPiece && destPiece.color !== origPiece.color ? destPiece : undefined;
if (dest === state.selected) unselect(state);
callUserFunction(state.events.move, orig, dest, captured);
if (!tryAutoCastle(state, orig, dest)) {
state.pieces.set(dest, origPiece);
state.pieces.delete(orig);
}
state.lastMove = [orig, dest];
state.check = undefined;
callUserFunction(state.events.change);
return captured || true;
}
export function baseNewPiece(state: HeadlessState, piece: cg.Piece, key: cg.Key, force?: boolean): boolean {
if (state.pieces.has(key)) {
if (force) state.pieces.delete(key);
else return false;
}
callUserFunction(state.events.dropNewPiece, piece, key);
state.pieces.set(key, piece);
state.lastMove = [key];
state.check = undefined;
callUserFunction(state.events.change);
state.movable.dests = undefined;
state.turnColor = opposite(state.turnColor);
return true;
}
function baseUserMove(state: HeadlessState, orig: cg.Key, dest: cg.Key): cg.Piece | boolean {
const result = baseMove(state, orig, dest);
if (result) {
state.movable.dests = undefined;
state.turnColor = opposite(state.turnColor);
state.animation.current = undefined;
}
return result;
}
export function userMove(state: HeadlessState, orig: cg.Key, dest: cg.Key): boolean {
if (canMove(state, orig, dest)) {
const result = baseUserMove(state, orig, dest);
if (result) {
const holdTime = state.hold.stop();
unselect(state);
const metadata: cg.MoveMetadata = {
premove: false,
ctrlKey: state.stats.ctrlKey,
holdTime,
};
if (result !== true) metadata.captured = result;
callUserFunction(state.movable.events.after, orig, dest, metadata);
return true;
}
} else if (canPremove(state, orig, dest)) {
setPremove(state, orig, dest, {
ctrlKey: state.stats.ctrlKey,
});
unselect(state);
return true;
}
unselect(state);
return false;
}
export function dropNewPiece(state: HeadlessState, orig: cg.Key, dest: cg.Key, force?: boolean): void {
const piece = state.pieces.get(orig);
if (piece && (canDrop(state, orig, dest) || force)) {
state.pieces.delete(orig);
baseNewPiece(state, piece, dest, force);
callUserFunction(state.movable.events.afterNewPiece, piece.role, dest, {
premove: false,
predrop: false,
});
} else if (piece && canPredrop(state, orig, dest)) {
setPredrop(state, piece.role, dest);
} else {
unsetPremove(state);
unsetPredrop(state);
}
state.pieces.delete(orig);
unselect(state);
}
export function selectSquare(state: HeadlessState, key: cg.Key, force?: boolean): void {
callUserFunction(state.events.select, key);
if (state.selected) {
if (state.selected === key && !state.draggable.enabled) {
unselect(state);
state.hold.cancel();
return;
} else if ((state.selectable.enabled || force) && state.selected !== key) {
if (userMove(state, state.selected, key)) {
state.stats.dragged = false;
return;
}
}
}
if (
(state.selectable.enabled || state.draggable.enabled) &&
(isMovable(state, key) || isPremovable(state, key))
) {
setSelected(state, key);
state.hold.start();
}
}
export function setSelected(state: HeadlessState, key: cg.Key): void {
state.selected = key;
if (isPremovable(state, key)) {
// calculate chess premoves if custom premoves are not passed
if (!state.premovable.customDests) {
state.premovable.dests = premove(state.pieces, key, state.premovable.castle);
}
} else state.premovable.dests = undefined;
}
export function unselect(state: HeadlessState): void {
state.selected = undefined;
state.premovable.dests = undefined;
state.hold.cancel();
}
function isMovable(state: HeadlessState, orig: cg.Key): boolean {
const piece = state.pieces.get(orig);
return (
!!piece &&
(state.movable.color === 'both' ||
(state.movable.color === piece.color && state.turnColor === piece.color))
);
}
export const canMove = (state: HeadlessState, orig: cg.Key, dest: cg.Key): boolean =>
orig !== dest &&
isMovable(state, orig) &&
(state.movable.free || !!state.movable.dests?.get(orig)?.includes(dest));
function canDrop(state: HeadlessState, orig: cg.Key, dest: cg.Key): boolean {
const piece = state.pieces.get(orig);
return (
!!piece &&
(orig === dest || !state.pieces.has(dest)) &&
(state.movable.color === 'both' ||
(state.movable.color === piece.color && state.turnColor === piece.color))
);
}
function isPremovable(state: HeadlessState, orig: cg.Key): boolean {
const piece = state.pieces.get(orig);
return (
!!piece &&
state.premovable.enabled &&
state.movable.color === piece.color &&
state.turnColor !== piece.color
);
}
function canPremove(state: HeadlessState, orig: cg.Key, dest: cg.Key): boolean {
const validPremoves: cg.Key[] =
state.premovable.customDests?.get(orig) ?? premove(state.pieces, orig, state.premovable.castle);
return orig !== dest && isPremovable(state, orig) && validPremoves.includes(dest);
}
function canPredrop(state: HeadlessState, orig: cg.Key, dest: cg.Key): boolean {
const piece = state.pieces.get(orig);
const destPiece = state.pieces.get(dest);
return (
!!piece &&
(!destPiece || destPiece.color !== state.movable.color) &&
state.predroppable.enabled &&
(piece.role !== 'pawn' || (dest[1] !== '1' && dest[1] !== '8')) &&
state.movable.color === piece.color &&
state.turnColor !== piece.color
);
}
export function isDraggable(state: HeadlessState, orig: cg.Key): boolean {
const piece = state.pieces.get(orig);
return (
!!piece &&
state.draggable.enabled &&
(state.movable.color === 'both' ||
(state.movable.color === piece.color && (state.turnColor === piece.color || state.premovable.enabled)))
);
}
export function playPremove(state: HeadlessState): boolean {
const move = state.premovable.current;
if (!move) return false;
const orig = move[0],
dest = move[1];
let success = false;
if (canMove(state, orig, dest)) {
const result = baseUserMove(state, orig, dest);
if (result) {
const metadata: cg.MoveMetadata = { premove: true };
if (result !== true) metadata.captured = result;
callUserFunction(state.movable.events.after, orig, dest, metadata);
success = true;
}
}
unsetPremove(state);
return success;
}
export function playPredrop(state: HeadlessState, validate: (drop: cg.Drop) => boolean): boolean {
const drop = state.predroppable.current;
let success = false;
if (!drop) return false;
if (validate(drop)) {
const piece = {
role: drop.role,
color: state.movable.color,
} as cg.Piece;
if (baseNewPiece(state, piece, drop.key)) {
callUserFunction(state.movable.events.afterNewPiece, drop.role, drop.key, {
premove: false,
predrop: true,
});
success = true;
}
}
unsetPredrop(state);
return success;
}
export function cancelMove(state: HeadlessState): void {
unsetPremove(state);
unsetPredrop(state);
unselect(state);
}
export function stop(state: HeadlessState): void {
state.movable.color = state.movable.dests = state.animation.current = undefined;
cancelMove(state);
}
export function getKeyAtDomPos(
pos: cg.NumberPair,
asWhite: boolean,
bounds: DOMRectReadOnly,
): cg.Key | undefined {
let file = Math.floor((8 * (pos[0] - bounds.left)) / bounds.width);
if (!asWhite) file = 7 - file;
let rank = 7 - Math.floor((8 * (pos[1] - bounds.top)) / bounds.height);
if (!asWhite) rank = 7 - rank;
return file >= 0 && file < 8 && rank >= 0 && rank < 8 ? pos2key([file, rank]) : undefined;
}
export function getSnappedKeyAtDomPos(
orig: cg.Key,
pos: cg.NumberPair,
asWhite: boolean,
bounds: DOMRectReadOnly,
): cg.Key | undefined {
const origPos = key2pos(orig);
const validSnapPos = allPos.filter(
pos2 =>
queen(origPos[0], origPos[1], pos2[0], pos2[1]) || knight(origPos[0], origPos[1], pos2[0], pos2[1]),
);
const validSnapCenters = validSnapPos.map(pos2 => computeSquareCenter(pos2key(pos2), asWhite, bounds));
const validSnapDistances = validSnapCenters.map(pos2 => distanceSq(pos, pos2));
const [, closestSnapIndex] = validSnapDistances.reduce(
(a, b, index) => (a[0] < b ? a : [b, index]),
[validSnapDistances[0], 0],
);
return pos2key(validSnapPos[closestSnapIndex]);
}
export const whitePov = (s: HeadlessState): boolean => s.orientation === 'white';