chessground
Version:
lichess.org chess ui
166 lines (154 loc) • 7.26 kB
text/typescript
import { HeadlessState } from './state.js';
import { setCheck, setSelected } from './board.js';
import { read as fenRead } from './fen.js';
import { DrawShape, DrawBrushes } from './draw.js';
import * as cg from './types.js';
export interface Config {
fen?: cg.FEN; // chess position in Forsyth notation
orientation?: cg.Color; // board orientation. white | black
turnColor?: cg.Color; // turn to play. white | black
check?: cg.Color | boolean; // true for current color, false to unset
lastMove?: cg.Key[]; // squares part of the last move ["c3", "c4"]
selected?: cg.Key; // square currently selected "a1"
coordinates?: boolean; // include coords attributes
coordinatesOnSquares?: boolean; // include coords attributes on every square
autoCastle?: boolean; // immediately complete the castle by moving the rook after king move
viewOnly?: boolean; // don't bind events: the user will never be able to move pieces around
disableContextMenu?: boolean; // because who needs a context menu on a chessboard
addPieceZIndex?: boolean; // adds z-index values to pieces (for 3D)
addDimensionsCssVarsTo?: HTMLElement; // add ---cg-width and ---cg-height CSS vars containing the board's dimensions to this element
blockTouchScroll?: boolean; // block scrolling via touch dragging on the board, e.g. for coordinate training
// pieceKey: boolean; // add a data-key attribute to piece elements
trustAllEvents?: boolean; // disable checking for human only input (e.isTrusted)
highlight?: {
lastMove?: boolean; // add last-move class to squares
check?: boolean; // add check class to squares
custom?: cg.SquareClasses; // add custom classes to custom squares
};
animation?: {
enabled?: boolean;
duration?: number;
};
movable?: {
free?: boolean; // all moves are valid - board editor
color?: cg.Color | 'both'; // color that can move. white | black | both | undefined
dests?: cg.Dests; // valid moves. {"a2" ["a3" "a4"] "b1" ["a3" "c3"]}
showDests?: boolean; // whether to add the move-dest class on squares
events?: {
after?: (orig: cg.Key, dest: cg.Key, metadata: cg.MoveMetadata) => void; // called after the move has been played
afterNewPiece?: (role: cg.Role, key: cg.Key, metadata: cg.MoveMetadata) => void; // called after a new piece is dropped on the board
};
rookCastle?: boolean; // castle by moving the king to the rook
};
premovable?: {
enabled?: boolean; // allow premoves for color that can not move
showDests?: boolean; // whether to add the premove-dest class on squares
castle?: boolean; // whether to allow king castle premoves
dests?: cg.Key[]; // premove destinations for the current selection
customDests?: cg.Dests; // use custom valid premoves. {"a2" ["a3" "a4"] "b1" ["a3" "c3"]}
events?: {
set?: (orig: cg.Key, dest: cg.Key, metadata?: cg.SetPremoveMetadata) => void; // called after the premove has been set
unset?: () => void; // called after the premove has been unset
};
};
predroppable?: {
enabled?: boolean; // allow predrops for color that can not move
events?: {
set?: (role: cg.Role, key: cg.Key) => void; // called after the predrop has been set
unset?: () => void; // called after the predrop has been unset
};
};
draggable?: {
enabled?: boolean; // allow moves & premoves to use drag'n drop
distance?: number; // minimum distance to initiate a drag; in pixels
autoDistance?: boolean; // lets chessground set distance to zero when user drags pieces
showGhost?: boolean; // show ghost of piece being dragged
deleteOnDropOff?: boolean; // delete a piece when it is dropped off the board
};
selectable?: {
// disable to enforce dragging over click-click move
enabled?: boolean;
};
events?: {
change?: () => void; // called after the situation changes on the board
// called after a piece has been moved.
// capturedPiece is undefined or like {color: 'white'; 'role': 'queen'}
move?: (orig: cg.Key, dest: cg.Key, capturedPiece?: cg.Piece) => void;
dropNewPiece?: (piece: cg.Piece, key: cg.Key) => void;
select?: (key: cg.Key) => void; // called when a square is selected
insert?: (elements: cg.Elements) => void; // when the board DOM has been (re)inserted
};
drawable?: {
enabled?: boolean; // can draw
visible?: boolean; // can view
defaultSnapToValidMove?: boolean;
// false to keep the drawing if a movable piece is clicked.
// Clicking an empty square or immovable piece will clear the drawing regardless.
eraseOnClick?: boolean;
shapes?: DrawShape[];
autoShapes?: DrawShape[];
brushes?: DrawBrushes;
onChange?: (shapes: DrawShape[]) => void; // called after drawable shapes change
};
}
export function applyAnimation(state: HeadlessState, config: Config): void {
if (config.animation) {
deepMerge(state.animation, config.animation);
// no need for such short animations
if ((state.animation.duration || 0) < 70) state.animation.enabled = false;
}
}
export function configure(state: HeadlessState, config: Config): void {
// don't merge destinations and autoShapes. Just override.
if (config.movable?.dests) state.movable.dests = undefined;
if (config.drawable?.autoShapes) state.drawable.autoShapes = [];
deepMerge(state, config);
// if a fen was provided, replace the pieces
if (config.fen) {
state.pieces = fenRead(config.fen);
state.drawable.shapes = config.drawable?.shapes || [];
}
// apply config values that could be undefined yet meaningful
if ('check' in config) setCheck(state, config.check || false);
if ('lastMove' in config && !config.lastMove) state.lastMove = undefined;
// in case of ZH drop last move, there's a single square.
// if the previous last move had two squares,
// the merge algorithm will incorrectly keep the second square.
else if (config.lastMove) state.lastMove = config.lastMove;
// fix move/premove dests
if (state.selected) setSelected(state, state.selected);
applyAnimation(state, config);
if (!state.movable.rookCastle && state.movable.dests) {
const rank = state.movable.color === 'white' ? '1' : '8',
kingStartPos = ('e' + rank) as cg.Key,
dests = state.movable.dests.get(kingStartPos),
king = state.pieces.get(kingStartPos);
if (!dests || !king || king.role !== 'king') return;
state.movable.dests.set(
kingStartPos,
dests.filter(
d =>
!(d === 'a' + rank && dests.includes(('c' + rank) as cg.Key)) &&
!(d === 'h' + rank && dests.includes(('g' + rank) as cg.Key)),
),
);
}
}
function deepMerge(base: any, extend: any): void {
for (const key in extend) {
if (Object.prototype.hasOwnProperty.call(extend, key)) {
if (
Object.prototype.hasOwnProperty.call(base, key) &&
isPlainObject(base[key]) &&
isPlainObject(extend[key])
)
deepMerge(base[key], extend[key]);
else base[key] = extend[key];
}
}
}
function isPlainObject(o: unknown): boolean {
if (typeof o !== 'object' || o === null) return false;
const proto = Object.getPrototypeOf(o);
return proto === Object.prototype || proto === null;
}