UNPKG

shogiground

Version:
198 lines (185 loc) 9.82 kB
import type { HeadlessState } from './state.js'; import type { DrawShape, SquareHighlight } from './draw.js'; import type * as sg from './types.js'; import { setChecks, setPreDests } from './board.js'; import { inferDimensions, sfenToBoard, sfenToHands } from './sfen.js'; export interface Config { sfen?: { board?: sg.BoardSfen; // pieces on the board in Forsyth notation hands?: sg.HandsSfen; // pieces in hand in Forsyth notation }; orientation?: sg.Color; // board orientation. sente | gote turnColor?: sg.Color; // turn to play. sente | gote activeColor?: sg.Color | 'both'; // color that can move or drop. sente | gote | both | undefined checks?: sg.Key[] | sg.Color | boolean; // squares currently in check ["5a"], color in check (see highlight.checkRoles) or boolean for current turn color lastDests?: sg.Key[]; // squares part of the last move or drop ["3c", "4c"] lastPiece?: sg.Piece; // piece part of the last drop selected?: sg.Key; // square currently selected "1a" selectedPiece?: sg.Piece; // piece in hand currently selected hovered?: sg.Key; // square currently being hovered viewOnly?: boolean; // don't bind events: the user will never be able to move pieces around squareRatio?: sg.NumberPair; // ratio of a single square [width, height] disableContextMenu?: boolean; // because who needs a context menu on a board, only without viewOnly blockTouchScroll?: boolean; // block scrolling via touch dragging on the board, e.g. for coordinate training scaleDownPieces?: boolean; // helpful for pngs - https://ctidd.com/2015/svg-background-scaling coordinates?: { enabled?: boolean; // include coords attributes files?: sg.Notation; ranks?: sg.Notation; }; highlight?: { lastDests?: boolean; // add last-dest class to squares and pieces check?: boolean; // add check class to squares checkRoles?: sg.RoleString[]; // roles to be highlighted when check is boolean is passed from config hovered?: boolean; // add hover class to hovered squares }; animation?: { enabled?: boolean; hands?: boolean; duration?: number }; hands?: { inlined?: boolean; // attaches sg-hands directly to sg-wrap, ignores HTMLElements passed to Shogiground roles?: sg.RoleString[]; // roles to render in sg-hand }; movable?: { free?: boolean; // all moves are valid - board editor dests?: sg.MoveDests; // valid moves. {"2a" ["3a" "4a"] "1b" ["3a" "3c"]} showDests?: boolean; // whether to add the dest class on squares events?: { after?: (orig: sg.Key, dest: sg.Key, prom: boolean, metadata: sg.MoveMetadata) => void; // called after the move has been played }; }; droppable?: { free?: boolean; // all drops are valid - board editor dests?: sg.DropDests; // valid drops. {"sente pawn" ["3a" "4a"] "sente lance" ["3a" "3c"]} showDests?: boolean; // whether to add the dest class on squares spare?: boolean; // whether to remove dropped piece from hand after drop - board editor events?: { after?: (piece: sg.Piece, key: sg.Key, prom: boolean, metadata: sg.MoveMetadata) => void; // called after the drop has been played }; }; premovable?: { enabled?: boolean; // allow premoves for color that can not move showDests?: boolean; // whether to add the pre-dest class on squares dests?: sg.Key[]; // premove destinations for the current selection generate?: (key: sg.Key, pieces: sg.Pieces) => sg.Key[]; // function to generate destinations that user can premove to events?: { set?: (orig: sg.Key, dest: sg.Key, prom: boolean) => 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 showDests?: boolean; // whether to add the pre-dest class on squares for drops dests?: sg.Key[]; // premove destinations for the drop selection generate?: (piece: sg.Piece, pieces: sg.Pieces) => sg.Key[]; // function to generate destinations that user can predrop on events?: { set?: (piece: sg.Piece, key: sg.Key, prom: boolean) => 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 shogiground set distance to zero when user drags pieces showGhost?: boolean; // show ghost of piece being dragged showTouchSquareOverlay?: boolean; // show square overlay on the square that is currently being hovered, touch only deleteOnDropOff?: boolean; // delete a piece when it is dropped off the board addToHandOnDropOff?: boolean; // add a piece to hand when it is dropped on it, requires deleteOnDropOff }; selectable?: { enabled?: boolean; // disable to enforce dragging over click-click move forceSpares?: boolean; // allow dropping spare pieces even with selectable disabled deleteOnTouch?: boolean; // selecting a piece on the board or in hand will remove it - board editor addSparesToHand?: boolean; // add selected spare piece to hand - board editor }; events?: { change?: () => void; // called after the situation changes on the board move?: (orig: sg.Key, dest: sg.Key, prom: boolean, capturedPiece?: sg.Piece) => void; drop?: (piece: sg.Piece, key: sg.Key, prom: boolean) => void; select?: (key: sg.Key) => void; // called when a square is selected unselect?: (key: sg.Key) => void; // called when a selected square is directly unselected - dropped back or clicked on the original square pieceSelect?: (piece: sg.Piece) => void; // called when a piece in hand is selected pieceUnselect?: (piece: sg.Piece) => void; // called when a selected piece is directly unselected - dropped back or clicked on the same piece insert?: (boardElements?: sg.BoardElements, handElements?: sg.HandElements) => void; // when the board/hands DOM has been (re)inserted }; drawable?: { enabled?: boolean; // can draw visible?: boolean; // can view forced?: boolean; // can only draw eraseOnClick?: boolean; shapes?: DrawShape[]; autoShapes?: DrawShape[]; squares?: SquareHighlight[]; onChange?: (shapes: DrawShape[]) => void; // called after drawable shapes change }; forsyth?: { toForsyth?: (role: sg.RoleString) => string | undefined; fromForsyth?: (str: string) => sg.RoleString | undefined; }; promotion?: { promotesTo?: (role: sg.RoleString) => sg.RoleString | undefined; unpromotesTo?: (role: sg.RoleString) => sg.RoleString | undefined; movePromotionDialog?: (orig: sg.Key, dest: sg.Key) => boolean; // activate promotion dialog forceMovePromotion?: (orig: sg.Key, dest: sg.Key) => boolean; // auto promote after move dropPromotionDialog?: (piece: sg.Piece, key: sg.Key) => boolean; // activate promotion dialog forceDropPromotion?: (piece: sg.Piece, key: sg.Key) => boolean; // auto promote after drop events?: { initiated?: () => void; // called when promotion dialog is started after?: (piece: sg.Piece) => void; // called after user selects a piece cancel?: () => void; // called after user cancels the selection }; }; } 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, just override. if (config.movable?.dests) state.movable.dests = undefined; if (config.droppable?.dests) state.droppable.dests = undefined; if (config.drawable?.shapes) state.drawable.shapes = []; if (config.drawable?.autoShapes) state.drawable.autoShapes = []; if (config.drawable?.squares) state.drawable.squares = []; if (config.hands?.roles) state.hands.roles = []; deepMerge(state, config); // if a sfen was provided, replace the pieces, except the currently dragged one if (config.sfen?.board) { state.dimensions = inferDimensions(config.sfen.board); state.pieces = sfenToBoard(config.sfen.board, state.dimensions, state.forsyth.fromForsyth); state.drawable.shapes = config.drawable?.shapes || []; } if (config.sfen?.hands) { state.hands.handMap = sfenToHands(config.sfen.hands, state.forsyth.fromForsyth); } // apply config values that could be undefined yet meaningful if ('checks' in config) setChecks(state, config.checks || false); if ('lastPiece' in config && !config.lastPiece) state.lastPiece = undefined; // in case of 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. if ('lastDests' in config && !config.lastDests) state.lastDests = undefined; else if (config.lastDests) state.lastDests = config.lastDests; // fix move/premove dests setPreDests(state); applyAnimation(state, config); } 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; }