UNPKG

chessground12

Version:
254 lines (227 loc) 9.96 kB
import { dragNewPiece } from './drag'; import { setDropMode, cancelDropMode } from './drop'; import type { HeadlessState, State } from './state'; import { predrop } from './predrop'; import { Color, Elements, FEN, MouchEvent, Orig, Piece, PieceNode, Pocket, PocketPosition, PocketRoles, Pockets, Role, eventsClicking, eventsDragging, } from './types'; import { createEl, letterOf, opposite, pieceClasses, roleOf } from './util'; /** * Logically maybe belongs to fen.ts, but put here to avoid merge conflicts from upsteam * Analogous to fen.ts->read(), but for pocket part of FEN * TODO: See todo in fen.ts->read() as well. Not sure if pocket parsing belongs there unless return * type is extended to contain pocket state. * */ export function readPockets(fen: FEN, pocketRoles: PocketRoles): Pockets { const placement = fen.split(' ')[0]; const bracketPos = placement.indexOf('['); const placementPockets = bracketPos !== -1 ? placement.slice(bracketPos) : ''; const pockets: Pockets = {}; const rWhite = pocketRoles('white'); const rBlack = pocketRoles('black'); if (rWhite) { pockets.white = {}; for (const r of rWhite) { pockets.white[roleOf(r)] = lc(placementPockets, r, 'upper'); } } if (rBlack) { pockets.black = {}; for (const r of rBlack) pockets.black[roleOf(r)] = lc(placementPockets, r, 'lower'); } return pockets; } function lc(str: string, letter: string, letterCase?: 'upper' | 'lower'): number { if (letterCase === 'upper') letter = letter.toUpperCase(); else if (letterCase === 'lower') letter = letter.toLowerCase(); let letterCount = 0; for (let position = 0; position < str.length; position++) if (str.charAt(position) === letter) letterCount += 1; return letterCount; } function renderPiece(el: HTMLElement, state: HeadlessState) { const role = el.getAttribute('data-role') as Role; const color = el.getAttribute('data-color') as Color; el.setAttribute('data-nb', '' + (state.pockets![color]![role] ?? 0)); const dropMode = state.dropmode; const dropPiece = state.dropmode.piece; const selectedSquare = dropMode.active && dropPiece?.role === role && dropPiece.color === color; const preDropRole = state.predroppable.current?.role; const activeColor = color === state.movable.color; if (activeColor && preDropRole === role) { el.classList.add('premove'); } else { el.classList.remove('premove'); } if (selectedSquare) { el.classList.add('selected-square'); } else { el.classList.remove('selected-square'); } } export function renderPocketsInitial( state: HeadlessState, elements: Elements, pocketTop?: HTMLElement, pocketBottom?: HTMLElement, ): void { function pocketView(pocketEl: HTMLElement, position: PocketPosition) { if (!state.pockets) { return; } const color = position === 'top' ? opposite(state.orientation) : state.orientation; const pocket = state.pockets[color]; if (!pocket) return; const roles = Object.keys(pocket); // contains the list of possible pieces/roles (i.e. for crazyhouse p-piece, n-piece, b-piece, r-piece, q-piece) in the order they will be displayed in the pocket const pl = String(roles!.length); const files = String(state.dimensions.width); const ranks = String(state.dimensions.height); // const pocketEl = createEl('div','pocket ' + position + ' usable'); pocketEl.setAttribute('style', `--pocketLength: ${pl}; --files: ${files}; --ranks: ${ranks}`); pocketEl.classList.add('pocket', position, 'usable'); roles.forEach((role: string) => { const pieceName = pieceClasses( { role: role, color: color, promoted: false } as Piece, state.orientation, ); const p = createEl('piece', pieceName); // todo: next 2 attributes already exist as classes, but need inverse function for util.ts->pieceClasses() p.setAttribute('data-color', color); p.setAttribute('data-role', role); renderPiece(p, state); // TODO: i wonder if events.ts->bindBoard() or something similar is a better place similarly to main board? // todo: in spectators mode movable.color is never set (except in goPly to undefined). Simultaneously // state.ts->default is "both" and here as well. Effect is that dragging and clicking is disabled, which is // great, but feels more like an accidental side effect than intention (effectively 'both' means 'none'). // Maybe state.movable.color should be set to undef in roundCtrl ALWAYS when in spectotor mode instead of // left unset (and with its default). Then below we can handle 'both' properly for sake of clarity eventsDragging.forEach(name => p.addEventListener(name, (e: MouchEvent) => { if (state.movable.free || state.movable.color === color) drag(state, e); }), ); eventsClicking.forEach(name => p.addEventListener(name, (e: MouchEvent) => { // movable.free is synonymous with editor mode, and right now click-drop not supported for pocket pieces if (/*state.movable.free ||*/ state.movable.color === color) click(state, e); }), ); pocketEl.appendChild(p); }); } // if (pocketTop) { pocketTop.innerHTML = ''; elements.pocketTop = pocketTop; pocketView(elements.pocketTop, 'top'); } if (pocketBottom) { pocketBottom.innerHTML = ''; elements.pocketBottom = pocketBottom; pocketView(elements.pocketBottom, 'bottom'); } } export function click(state: HeadlessState, e: MouchEvent): void { if (e.button !== undefined && e.button !== 0) return; // only touch or left click const el = e.target as HTMLElement, role = el.getAttribute('data-role') as Role, color = el.getAttribute('data-color') as Color, number = el.getAttribute('data-nb'); if (number === '0') return; const dropMode = state.dropmode; const dropPiece = state.dropmode.piece; const canceledDropMode = el.getAttribute('canceledDropMode'); el.setAttribute('canceledDropMode', ''); if ((!dropMode.active || dropPiece?.role !== role) && canceledDropMode !== 'true') { setDropMode(state as State, { color, role }); } else { cancelDropMode(state); } e.stopPropagation(); e.preventDefault(); } export function drag(state: HeadlessState, e: MouchEvent): void { if (e.button !== undefined && e.button !== 0) return; // only touch or left click const el = e.target as HTMLElement, role = el.getAttribute('data-role') as Role, color = el.getAttribute('data-color') as Color, n = Number(el.getAttribute('data-nb')); el.setAttribute('canceledDropMode', ''); // We want to know if later in this method cancelDropMode was called, // so right after mouse button is up and dragging is over if a click event is triggered // (which annoyingly does happen if mouse is still over same pocket element) // then we know not to call setDropMode selecting the piece we have just unselected. // Alternatively we might not cancelDropMode on drag of same piece but then after drag is over // the selected piece remains selected which is not how board pieces behave and more importantly is counter intuitive if (n === 0) return; state.events!.pocketSelect!({ role, color }); // always cancel drop mode if it is active if (state.dropmode.active) { cancelDropMode(state); if (state.dropmode.piece?.role === role) { // we mark it with this only if we are cancelling the same piece we "drag" el.setAttribute('canceledDropMode', 'true'); } } if (state.movable.dests) { const dropDests = new Map([[role, state.movable.dests.get((letterOf(role, true) + '@') as Orig)!]]); state.dropmode.dropDests = dropDests; } e.stopPropagation(); e.preventDefault(); dragNewPiece(state as State, { color, role }, e); } /** * updates each piece element attributes based on state * */ export function renderPockets(state: State): void { function renderPocket(pocketEl?: HTMLElement) { let el: PieceNode | undefined = pocketEl?.firstChild as PieceNode | undefined; while (el) { renderPiece(el, state); el = el.nextSibling as PieceNode; } } renderPocket(state.dom.elements.pocketBottom); renderPocket(state.dom.elements.pocketTop); } function pocket2str(pocket: Pocket) { const letters: string[] = []; for (const role in pocket) { letters.push(letterOf(role as Role, true).repeat(pocket[role as Role] || 0)); } return letters.join(''); } export function pockets2str(pockets: Pockets): string { return '[' + pocket2str(pockets['white']!) + pocket2str(pockets['black']!).toLowerCase() + ']'; } /** * todo: Ideally this whole method should disappear. It is legacy solution from when pocket was outside CG for the case * when dragging started while another premove/predrop was set. After that premove/drop executes and turn is again * opp's, we are again in predrop state and need to set those again * Maybe predroppable should be initialized in board.ts->setSelected() and implemented similarly as premove dests * Could happen together with further refactoring to make pocket more of a first class citizen and enable other * stuff like highlighting last move etc. maybe. * Even if not made part of the setSelected infrastructure, i am pretty sure this is not needed if we track and * check better what is dragged/clicked and with proper combination of if-s in render.ts and clean-up-to-undef logic * */ export function setPredropDests(state: HeadlessState): void { const piece = state.draggable.current?.piece; if (piece && piece.color !== state.turnColor) { //it is opponents turn, but we are dragging a pocket piece at the same time const dropDests = predrop(state.pieces, piece, state.geometry, state.variant); state.predroppable.dropDests = dropDests; } }