From 1f02e9d700e0bce4c5a206432dc3260b68a8e2c9 Mon Sep 17 00:00:00 2001 From: Matt Tucker Date: Tue, 3 Sep 2024 11:32:47 +0100 Subject: [PATCH 01/22] Add Abalone defaults --- src/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types.ts b/src/types.ts index 31fdc54..2ebf0ca 100644 --- a/src/types.ts +++ b/src/types.ts @@ -57,6 +57,7 @@ export type Variant = | 'nackgammon' | 'breakthroughtroyka' | 'minibreakthroughtroyka' + | 'abalone' | undefined; export type PlayerIndex = (typeof playerIndexs)[number]; export type Letter = (typeof letters)[number]; From 5b563104b871b11f6e2ccbce8ef59e6477803b4a Mon Sep 17 00:00:00 2001 From: Vincent FROCHOT Date: Wed, 13 Nov 2024 20:20:30 +0100 Subject: [PATCH 02/22] chore: allow splitting src files into folders apply same logic that is in stratops generate a types folder for .d.ts files also reorder tsconconfig keys alphabetically --- package.json | 27 ++++++++++++++++----------- tsconfig.json | 23 +++++++++++------------ 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index a933499..5348e55 100644 --- a/package.json +++ b/package.json @@ -3,19 +3,26 @@ "version": "7.11.1-pstrat3.3", "description": "playstrategy.org chess ui, forked from lichess.org", "type": "module", + "module": "dist/chessground.js", "main": "dist/chessground.js", "types": "chessground.d.ts", - "exports": { - ".": "./dist/chessground.js", - "./*": "./dist/*.js" - }, "typesVersions": { "*": { "*": [ - "dist/*" + "dist/types/*" ] } }, + "exports": { + ".": { + "import": "./dist/chessground.js", + "types": "./dist/types/chessground.d.ts" + }, + "./*": { + "import": "./dist/*.js", + "types": "./dist/types/*.d.ts" + } + }, "packageManager": "pnpm@9.1.0", "engines": { "node": ">=20", @@ -35,7 +42,7 @@ }, "scripts": { "prepare": "$npm_execpath run compile", - "compile": "tsc --sourceMap --declaration", + "compile": "tsc --declarationDir dist/types --sourceMap", "test": "node node_modules/jest/bin/jest.js", "lint": "eslint src/*.ts", "format": "prettier --write .", @@ -45,11 +52,9 @@ "postinstall": "$npm_execpath run bundle" }, "files": [ - "/dist/*.js", - "/dist/*.d.ts", - "/dist/*.js.map", - "/assets/*.css", - "/src/*.ts" + "/src", + "/dist", + "/assets/*.css" ], "jest": { "globals": { diff --git a/tsconfig.json b/tsconfig.json index aa90a88..f58087e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,20 +1,19 @@ { - "include": ["src/*.ts"], - "exclude": [], + "include": ["src"], "compilerOptions": { - "esModuleInterop": true, "declaration": true, - "outDir": "dist", - "noEmitOnError": false, - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitThis": true, - "noImplicitReturns": true, + "esModuleInterop": true, + "lib": ["DOM", "es2017"], "module": "esnext", "moduleResolution": "node", + "noEmitOnError": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "outDir": "dist", "resolveJsonModule": true, - "target": "es2017", - "lib": ["DOM", "es2017"] + "strict": true, + "target": "es2017" } } From 19741ab367193214e7604c19dfb3d0e9dc590bdf Mon Sep 17 00:00:00 2001 From: Vincent FROCHOT Date: Wed, 13 Nov 2024 20:25:43 +0100 Subject: [PATCH 03/22] chore: update README --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a157a6b..e93cc0c 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,9 @@ More? Please make a pull request to include it here. ## Development +Commands are listed in package.json. +In case you want to see the possibilities from the console, run `pnpm run` + Install build dependencies: ```sh @@ -106,7 +109,7 @@ Build the node module: pnpm prepare --watch ``` -Build the minified bundled dist files: +Build the minified bundled dist files: (NOTE: from lila you will likely then need to restart the build as it does not watch for changes on the minified file): ```sh pnpm dist From 3823272ed49d366d4389f1b45eefb5992844dc1f Mon Sep 17 00:00:00 2001 From: Vincent FROCHOT Date: Wed, 13 Nov 2024 21:35:34 +0100 Subject: [PATCH 04/22] feat: WIP PLA-940 Abalone - read FEN WIP - probably missing some checks - override several functions from several files in order to place marbles correctly on the board in analysis (& study) and match views. Current state is pos2key partially landing in case of even row number (because squares are paved in hexagon so some squares land precisely between 2 columns) - add @TODO annotations where there is more work or checks required --- src/board.ts | 6 + src/draw.ts | 13 ++- src/fen.ts | 6 +- src/premove.ts | 11 +- src/render.ts | 25 ++-- src/svg.ts | 33 ++++-- src/util.ts | 23 ++++ src/variants/abalone/README.md | 1 + src/variants/abalone/board.ts | 41 +++++++ src/variants/abalone/draw.ts | 51 ++++++++ src/variants/abalone/fen.ts | 37 ++++++ src/variants/abalone/premove.ts | 12 ++ src/variants/abalone/render.ts | 199 ++++++++++++++++++++++++++++++++ src/variants/abalone/svg.ts | 161 ++++++++++++++++++++++++++ src/variants/abalone/util.ts | 137 ++++++++++++++++++++++ src/wrap.ts | 15 +++ 16 files changed, 751 insertions(+), 20 deletions(-) create mode 100644 src/variants/abalone/README.md create mode 100644 src/variants/abalone/board.ts create mode 100644 src/variants/abalone/draw.ts create mode 100644 src/variants/abalone/fen.ts create mode 100644 src/variants/abalone/premove.ts create mode 100644 src/variants/abalone/render.ts create mode 100644 src/variants/abalone/svg.ts create mode 100644 src/variants/abalone/util.ts diff --git a/src/board.ts b/src/board.ts index 50a6b52..18b5ecd 100644 --- a/src/board.ts +++ b/src/board.ts @@ -24,6 +24,8 @@ import predrop from './predrop'; import * as cg from './types'; import * as T from './transformations'; +import { getKeyAtDomPos as abaloneGetKeyAtDomPos } from './variants/abalone/board'; + export function setOrientation(state: HeadlessState, o: cg.Orientation): void { state.orientation = o; state.animation.current = state.draggable.current = state.selected = undefined; @@ -181,6 +183,7 @@ function updatePocketPieces( state.pocketPieces = newPocketPieces; } +// @TODO VFR: add Abalone moves 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); @@ -632,6 +635,9 @@ export function getKeyAtDomPos( bd: cg.BoardDimensions, variant: cg.Variant = 'chess', ): cg.Key | undefined { + if (variant === 'abalone') { + return abaloneGetKeyAtDomPos(pos, orientation, bounds, bd); + } const bgBorder = 1 / 15; const file = variant === 'backgammon' || variant === 'hyper' || variant === 'nackgammon' diff --git a/src/draw.ts b/src/draw.ts index 7ae543f..c1cb6ca 100644 --- a/src/draw.ts +++ b/src/draw.ts @@ -3,6 +3,11 @@ import { unselect, cancelMove, getKeyAtDomPos, getSnappedKeyAtDomPos } from './b import { eventPosition, isRightButton } from './util'; import * as cg from './types'; +import { + start as abaloneStart, + processDraw as abaloneProcessDraw +} from './variants/abalone/draw'; + export interface DrawShape { orig: cg.Key; dest?: cg.Key; @@ -62,6 +67,9 @@ export interface DrawCurrent { const brushes = ['green', 'red', 'blue', 'yellow']; export function start(state: State, e: cg.MouchEvent): void { + if(state.variant === "abalone") { + return abaloneStart(state, e); + } // support one finger touch only if (e.touches && e.touches.length > 1) return; e.stopPropagation(); @@ -81,6 +89,9 @@ export function start(state: State, e: cg.MouchEvent): void { } export function processDraw(state: State): void { + if(state.variant === "abalone") { + return abaloneProcessDraw(state); + } requestAnimationFrame(() => { const cur = state.drawable.current; if (cur) { @@ -134,7 +145,7 @@ export function clear(state: State): void { } } -function eventBrush(e: cg.MouchEvent): string { +export function eventBrush(e: cg.MouchEvent): string { const modA = (e.shiftKey || e.ctrlKey) && isRightButton(e); const modB = e.altKey || e.metaKey || e.getModifierState?.('AltGraph'); return brushes[(modA ? 1 : 0) + (modB ? 2 : 0)]; diff --git a/src/fen.ts b/src/fen.ts index c53028e..bbf3971 100644 --- a/src/fen.ts +++ b/src/fen.ts @@ -1,11 +1,13 @@ import { pos2key, NRanks, invNRanks } from './util'; import * as cg from './types'; +import { read as abaloneRead } from './variants/abalone/fen'; + export const initial: cg.FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR'; const commaFenVariants: cg.Variant[] = ['oware', 'togyzkumalak', 'bestemshe', 'backgammon', 'hyper', 'nackgammon']; const mancalaFenVariants: cg.Variant[] = ['oware', 'togyzkumalak', 'bestemshe']; -function roles(letter: string) { +export function roles(letter: string) { return (letter.replace('+', 'p') + '-piece') as cg.Role; } @@ -15,6 +17,7 @@ function letters(role: cg.Role) { } export function read(fen: cg.FEN, dimensions: cg.BoardDimensions, variant: cg.Variant): cg.Pieces { + if(variant === "abalone") return abaloneRead(fen, dimensions); if (fen === 'start') fen = initial; if (fen.indexOf('[') !== -1) fen = fen.slice(0, fen.indexOf('[')); const pieces: cg.Pieces = new Map(); @@ -23,6 +26,7 @@ export function read(fen: cg.FEN, dimensions: cg.BoardDimensions, variant: cg.Va let promoted = false; let num = 0; + // @TODO: try to refactor using Higher Order Functions with a default square board if (!commaFenVariants.includes(variant)) { let skipNext = false; for (let i = 0; i < fen.length; i++) { diff --git a/src/premove.ts b/src/premove.ts index dccab50..014c38a 100644 --- a/src/premove.ts +++ b/src/premove.ts @@ -1,7 +1,9 @@ import * as util from './util'; import * as cg from './types'; -type Mobility = (x1: number, y1: number, x2: number, y2: number) => boolean; +import { marble as abaloneMarble } from './variants/abalone/premove'; + +export type Mobility = (x1: number, y1: number, x2: number, y2: number) => boolean; function diff(a: number, b: number): number { return Math.abs(a - b); @@ -1147,6 +1149,13 @@ export function premove( mobility = breakthroughPawn(pieces, playerIndex); break; + case 'abalone': + mobility = abaloneMarble( + pieces, + playerIndex + ); + break; + // Variants using standard pieces and additional fairy pieces like S-chess, Capablanca, etc. default: switch (role) { diff --git a/src/render.ts b/src/render.ts index 540749d..a9a4825 100644 --- a/src/render.ts +++ b/src/render.ts @@ -14,12 +14,19 @@ import { DragCurrent } from './drag'; import * as cg from './types'; import * as T from './transformations'; -type PieceName = string; // `$playerIndex $role` -type SquareClasses = Map; +import { render as renderAbalone } from './variants/abalone/render'; + +export type PieceName = string; // `$playerIndex $role` +export type SquareClasses = Map; // ported from https://github.com/veloce/lichobile/blob/master/src/js/chessground/view.js // in case of bugs, blame @veloce export function render(s: State): void { + if (s.variant === 'abalone') { + renderAbalone(s); + return; + } + const orientation = s.orientation, asP1: boolean = p1Pov(s), posToTranslate = s.dom.relative ? posToTranslateRel : posToTranslateAbs(s.dom.bounds(), s.dimensions, s.variant), @@ -215,25 +222,25 @@ export function updateBounds(s: State): void { } } -function isPieceNode(el: cg.PieceNode | cg.SquareNode): el is cg.PieceNode { +export function isPieceNode(el: cg.PieceNode | cg.SquareNode): el is cg.PieceNode { return el.tagName === 'PIECE'; } -function isSquareNode(el: cg.PieceNode | cg.SquareNode): el is cg.SquareNode { +export function isSquareNode(el: cg.PieceNode | cg.SquareNode): el is cg.SquareNode { return el.tagName === 'SQUARE'; } -function removeNodes(s: State, nodes: HTMLElement[]): void { +export function removeNodes(s: State, nodes: HTMLElement[]): void { for (const node of nodes) s.dom.elements.board.removeChild(node); } -function posZIndex(pos: cg.Pos, orientation: cg.Orientation, asP1: boolean, bd: cg.BoardDimensions): string { +export function posZIndex(pos: cg.Pos, orientation: cg.Orientation, asP1: boolean, bd: cg.BoardDimensions): string { pos = T.mapToP1[orientation](pos, bd); let z = 2 + (pos[1] - 1) * bd.height + (bd.width - pos[0]); if (asP1) z = 67 - z; return z + ''; } -function pieceNameOf( +export function pieceNameOf( piece: cg.Piece, myPlayerIndex: cg.PlayerIndex, orientation: cg.Orientation, @@ -260,7 +267,7 @@ function backgammonPosClass(k: cg.Key, orientation: cg.Orientation): string { return ` ${topOrBottom} ${leftOrRight}`; } -function computeSquareClasses(s: State): SquareClasses { +export function computeSquareClasses(s: State): SquareClasses { const squares: SquareClasses = new Map(); if (s.lastMove && s.highlight.lastMove) { let first = true; @@ -354,7 +361,7 @@ function addSquare(squares: SquareClasses, key: cg.Key, klass: string): void { else squares.set(key, klass); } -function appendValue(map: Map, key: K, value: V): void { +export function appendValue(map: Map, key: K, value: V): void { const arr = map.get(key); if (arr) arr.push(value); else map.set(key, [value]); diff --git a/src/svg.ts b/src/svg.ts index c97e259..207c6a5 100644 --- a/src/svg.ts +++ b/src/svg.ts @@ -4,11 +4,16 @@ import { Drawable, DrawShape, DrawShapePiece, DrawBrush, DrawBrushes, DrawModifi import * as cg from './types'; import * as T from './transformations'; +import { + renderPiece as abaloneRenderPiece, + renderShape as abaloneRenderShape +} from './variants/abalone/svg'; + export function createElement(tagName: string): SVGElement { return document.createElementNS('http://www.w3.org/2000/svg', tagName); } -interface Shape { +export interface Shape { shape: DrawShape; current: boolean; hash: Hash; @@ -16,7 +21,7 @@ interface Shape { type CustomBrushes = Map; // by hash -type ArrowDests = Map; // how many arrows land on a square +export type ArrowDests = Map; // how many arrows land on a square type Hash = string; @@ -185,6 +190,9 @@ function renderShape( arrowDests: ArrowDests, bounds: ClientRect, ): SVGElement { + if (state.variant === "abalone") { + return abaloneRenderShape(state, { shape, current, hash }, brushes, arrowDests, bounds); + } let el: SVGElement; if (shape.customSvg) { const orig = orient(key2pos(shape.orig), state.orientation, state.dimensions); @@ -297,6 +305,14 @@ function renderPiece( myPlayerIndex: cg.PlayerIndex, variant: cg.Variant, ): SVGElement { + if (variant === "abalone") { + return abaloneRenderPiece(baseUrl, + pos, + piece, + bounds, + bd, + myPlayerIndex); + } const o = pos2px(pos, bounds, bd), width = (bounds.width / bd.width) * (piece.scale || 1), height = (bounds.height / bd.height) * (piece.scale || 1), @@ -340,11 +356,11 @@ export function setAttributes(el: SVGElement, attrs: { [key: string]: any }): SV return el; } -function orient(pos: cg.Pos, orientation: cg.Orientation, bd: cg.BoardDimensions): cg.Pos { +export function orient(pos: cg.Pos, orientation: cg.Orientation, bd: cg.BoardDimensions): cg.Pos { return T.mapToP1Inverse[orientation](pos, bd); } -function makeCustomBrush(base: DrawBrush, modifiers: DrawModifiers): DrawBrush { +export function makeCustomBrush(base: DrawBrush, modifiers: DrawModifiers): DrawBrush { return { color: base.color, opacity: Math.round(base.opacity * 10) / 10, @@ -353,20 +369,20 @@ function makeCustomBrush(base: DrawBrush, modifiers: DrawModifiers): DrawBrush { }; } -function circleWidth(bounds: ClientRect, bd: cg.BoardDimensions): [number, number] { +export function circleWidth(bounds: ClientRect, bd: cg.BoardDimensions): [number, number] { const base = bounds.width / (bd.width * 64); return [3 * base, 4 * base]; } -function lineWidth(brush: DrawBrush, current: boolean, bounds: ClientRect, bd: cg.BoardDimensions): number { +export function lineWidth(brush: DrawBrush, current: boolean, bounds: ClientRect, bd: cg.BoardDimensions): number { return (((brush.lineWidth || 10) * (current ? 0.85 : 1)) / (bd.width * 64)) * bounds.width; } -function opacity(brush: DrawBrush, current: boolean): number { +export function opacity(brush: DrawBrush, current: boolean): number { return (brush.opacity || 1) * (current ? 0.9 : 1); } -function arrowMargin(bounds: ClientRect, shorten: boolean, bd: cg.BoardDimensions): number { +export function arrowMargin(bounds: ClientRect, shorten: boolean, bd: cg.BoardDimensions): number { return ((shorten ? 20 : 10) / (bd.width * 64)) * bounds.width; } @@ -419,6 +435,7 @@ function roleToSvgName(variant: cg.Variant, piece: DrawShapePiece): string { case 'go9x9': case 'go13x13': case 'go19x19': + case 'abalone': return (piece.playerIndex === 'p1' ? 'b' : 'w') + piece.role[0].toUpperCase(); case 'oware': case 'togyzkumalak': diff --git a/src/util.ts b/src/util.ts index abe15ac..9e2cc58 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,6 +1,8 @@ import * as cg from './types'; import * as T from './transformations'; +import { posToTranslateAbs as abalonePosToTranslateAbs } from './variants/abalone/util'; + export const playerIndexs: cg.PlayerIndex[] = ['p1', 'p2']; export const invRanks: readonly cg.Rank[] = [...cg.ranks19].reverse(); export const NRanks: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]; @@ -18,6 +20,18 @@ export function allKeys(bd: cg.BoardDimensions = { width: 8, height: 8 }) { return Array.prototype.concat(...files(bd.width).map(c => ranks(bd.height).map(r => c + r))); } +// @TODO VFR: example of how pos2key could be rewritten +// export const pos2key = (variant: cg.Variant, pos: cg.Pos): cg.Key => { +// switch(variant) { +// case "abalone": { +// return (cg.files[pos[0] - 1] + cg.ranks19[pos[1] - 1]) as cg.Key; +// } +// default: { +// return (cg.files[pos[0] - 1] + cg.ranks19[pos[1] - 1]) as cg.Key; +// } +// } +// } + export function pos2key(pos: cg.Pos): cg.Key { return (cg.files[pos[0] - 1] + cg.ranks19[pos[1] - 1]) as cg.Key; } @@ -150,6 +164,7 @@ const posToTranslateBase = ( return T.translateBase[orientation](pos, xFactor, yFactor, bt); }; +// @TODO VFR investigations : check if needed for Abalone export const posToTranslateAbs = ( bounds: ClientRect, bt: cg.BoardDimensions, @@ -159,9 +174,13 @@ export const posToTranslateAbs = ( (bounds.width / bt.width) * (variant === 'backgammon' || variant === 'hyper' || variant === 'nackgammon' ? 0.8 : 1), yFactor = bounds.height / bt.height; + if (variant === 'abalone') { + return (pos, orientation) => abalonePosToTranslateAbs(pos, orientation, xFactor, yFactor, bt); + } return (pos, orientation) => posToTranslateBase(pos, orientation, xFactor, yFactor, bt); }; +// @TODO VFR investigations : check if needed for Abalone export const posToTranslateRel = ( pos: cg.Pos, orientation: cg.Orientation, @@ -214,6 +233,7 @@ export const createEl = (tagName: string, className?: string): HTMLElement => { return el; }; +// @TODO VFR investigations: could be interesting to see if it can be used to move Abalone squares ?? export function computeSquareCenter( key: cg.Key, orientation: cg.Orientation, @@ -568,6 +588,9 @@ export function togyzkumalakUpdatePiecesFromMove( return updatedPieces; } +// @TODO VFR: update pieces for Abalone (could allow moving in local from analysis page ??) +// create here a function "updateAbalonePieceMap(pieces: cg.Pieces, bd: cg.BoardDimensions??): cg.PiecesDiff" or similar + function createMancalaBoardArrayFromPieces(pieces: cg.Pieces, bd: cg.BoardDimensions): number[] { function countAtKey(key: cg.Key): number { const piece = pieces.get(key); diff --git a/src/variants/abalone/README.md b/src/variants/abalone/README.md new file mode 100644 index 0000000..ad541cd --- /dev/null +++ b/src/variants/abalone/README.md @@ -0,0 +1 @@ +Ideas from mid-investigation : We need to define our own key2pos and pos2key functions as the grid is an hexagon ? \ No newline at end of file diff --git a/src/variants/abalone/board.ts b/src/variants/abalone/board.ts new file mode 100644 index 0000000..d02a026 --- /dev/null +++ b/src/variants/abalone/board.ts @@ -0,0 +1,41 @@ +import * as cg from '../../types'; +import * as T from '../../transformations'; +import { knight, queen } from '../../premove'; +import { distanceSq } from '../../util'; + +import { allPos, computeSquareCenter, key2pos, pos2key } from './util'; + +export const getKeyAtDomPos = ( + pos: cg.NumberPair, + orientation: cg.Orientation, + bounds: ClientRect, + bd: cg.BoardDimensions + ): cg.Key | undefined => { + let file = Math.ceil(bd.width * ((pos[0] - bounds.left) / bounds.width)); + const rank = Math.ceil(bd.height - bd.height * ((pos[1] - bounds.top) / bounds.height)); + + if (rank === undefined || file === undefined) return undefined; + pos = [file, rank]; + pos = T.mapToP1[orientation](pos, bd); + return pos[0] > 0 && pos[0] < bd.width + 1 && pos[1] > 0 && pos[1] < bd.height + 1 ? pos2key(pos) : undefined; +} + +export const getSnappedKeyAtDomPos = ( + orig: cg.Key, + pos: cg.NumberPair, + orientation: cg.Orientation, + bounds: ClientRect, + bd: cg.BoardDimensions, +): cg.Key | undefined => { + const origPos = key2pos(orig); + const validSnapPos = allPos(bd).filter(pos2 => { + return 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), orientation, bounds, bd)); + 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]); +} diff --git a/src/variants/abalone/draw.ts b/src/variants/abalone/draw.ts new file mode 100644 index 0000000..f9bc8ee --- /dev/null +++ b/src/variants/abalone/draw.ts @@ -0,0 +1,51 @@ +import { cancelMove, unselect } from "../../board"; +import { eventBrush } from "../../draw"; +import { State } from "../../state"; +import { eventPosition } from "../../util"; +import { getKeyAtDomPos, getSnappedKeyAtDomPos } from "./board"; +import * as cg from '../../types'; + +export const start = (state: State, e: cg.MouchEvent): void => { + // support one finger touch only + if (e.touches && e.touches.length > 1) return; + e.stopPropagation(); + e.preventDefault(); + e.ctrlKey ? unselect(state) : cancelMove(state); + const pos = eventPosition(e)!, + orig = getKeyAtDomPos(pos, state.orientation, state.dom.bounds(), state.dimensions); + if (!orig) return; + state.drawable.current = { + orig, + pos, + brush: eventBrush(e), + snapToValidMove: state.drawable.defaultSnapToValidMove, + }; + + processDraw(state); + } + + export const processDraw = (state: State): void => { + requestAnimationFrame(() => { + const cur = state.drawable.current; + if (cur) { + const keyAtDomPos = getKeyAtDomPos( + cur.pos, + state.orientation, + state.dom.bounds(), + state.dimensions + ); + if (!keyAtDomPos) { + cur.snapToValidMove = false; + } + const mouseSq = cur.snapToValidMove + ? getSnappedKeyAtDomPos(cur.orig, cur.pos, state.orientation, state.dom.bounds(), state.dimensions) + : keyAtDomPos; + if (mouseSq !== cur.mouseSq) { + cur.mouseSq = mouseSq; + cur.dest = mouseSq !== cur.orig ? mouseSq : undefined; + state.dom.redrawNow(); + } + processDraw(state); + } + }); + } \ No newline at end of file diff --git a/src/variants/abalone/fen.ts b/src/variants/abalone/fen.ts new file mode 100644 index 0000000..55b391c --- /dev/null +++ b/src/variants/abalone/fen.ts @@ -0,0 +1,37 @@ +import { roles } from '../../fen'; +import * as cg from '../../types'; +import { pos2key } from '../../util'; + +export const read = (fen: cg.FEN, dimensions: cg.BoardDimensions): cg.Pieces => { + + const pieces: cg.Pieces = new Map(); + + let row: number = dimensions.height; + const padding = [0,0,0,0,0,0,1,2,3,4]; + let file = padding[row]; + + for (let i = 0; i < fen.length; i++) { + const c = fen[i]; + if (c === ' ') break + else if (c == '/') { + file = padding[row - 1]; + --row; + } else { + const step = parseInt(c, 10); + if (step > 0) { + file += step; + } else { + file++; + const letter = c.toLowerCase(); + const playerIndex = (c === letter ? 'p2' : 'p1') as cg.PlayerIndex; + const piece = { + role: roles(letter), + playerIndex: playerIndex, + } as cg.Piece; + pieces.set(pos2key([file, row]), piece); // @TODO VFR: check all cases are correctly handled to prevent a js error in console + } + } + } + + return pieces; +} diff --git a/src/variants/abalone/premove.ts b/src/variants/abalone/premove.ts new file mode 100644 index 0000000..a9f8410 --- /dev/null +++ b/src/variants/abalone/premove.ts @@ -0,0 +1,12 @@ +import { Mobility } from '../../premove'; +import * as cg from '../../types'; +import { pos2key } from './util'; + +// @VFR: TODO: update this, the code is just a wrong copy paste +export const marble = (pieces: cg.Pieces, playerIndex: cg.PlayerIndex): Mobility => { + return (x1, y1 /*, x2, y2*/) => { + const pos = pos2key([x1, y1 + (playerIndex === 'p1' ? 1 : -1)]) as cg.Key; + if (pieces.has(pos) && pieces.get(pos)?.playerIndex === playerIndex) return false; + return false; + } + } \ No newline at end of file diff --git a/src/variants/abalone/render.ts b/src/variants/abalone/render.ts new file mode 100644 index 0000000..35647d9 --- /dev/null +++ b/src/variants/abalone/render.ts @@ -0,0 +1,199 @@ +import { p1Pov } from "../../board"; +import { State } from "../../state"; +import { createEl, posToTranslateAbs, posToTranslateRel, translateAbs, translateRel } from "../../util"; +import * as cg from '../../types'; +import { AnimCurrent, AnimFadings, AnimVector, AnimVectors } from "../../anim"; +import { DragCurrent } from "../../drag"; +import { PieceName, SquareClasses, appendValue, computeSquareClasses, isPieceNode, isSquareNode, pieceNameOf, posZIndex, removeNodes } from "../../render"; + +import { key2pos } from "./util"; + +// @TODO VFR: this code is an ugly copy paste, refactor etc +export const render = (s: State): void => { + const orientation = s.orientation, + asP1: boolean = p1Pov(s), + posToTranslate = s.dom.relative ? posToTranslateRel : posToTranslateAbs(s.dom.bounds(), s.dimensions, s.variant), + translate = s.dom.relative ? translateRel : translateAbs, + boardEl: HTMLElement = s.dom.elements.board, + pieces: cg.Pieces = s.pieces, + pocketPieces: cg.Piece[] = s.pocketPieces, + curAnim: AnimCurrent | undefined = s.animation.current, + anims: AnimVectors = curAnim ? curAnim.plan.anims : new Map(), + fadings: AnimFadings = curAnim ? curAnim.plan.fadings : new Map(), + curDrag: DragCurrent | undefined = s.draggable.current, + squares: SquareClasses = computeSquareClasses(s), + samePieces: Set = new Set(), + sameSquares: Set = new Set(), + movedPieces: Map = new Map(), + movedSquares: Map = new Map(); // by class name + + let k: cg.Key, + el: cg.PieceNode | cg.SquareNode | undefined, + pieceAtKey: cg.Piece | undefined, + elPieceName: PieceName, + anim: AnimVector | undefined, + fading: cg.Piece | undefined, + pMvdset: cg.PieceNode[] | undefined, + pMvd: cg.PieceNode | undefined, + sMvdset: cg.SquareNode[] | undefined, + sMvd: cg.SquareNode | undefined; + + // walk over all board dom elements, apply animations and flag moved pieces + el = boardEl.firstChild as cg.PieceNode | cg.SquareNode | undefined; + + while (el) { + k = el.cgKey; + if (isPieceNode(el)) { + pieceAtKey = pieces.get(k); + anim = anims.get(k); + fading = fadings.get(k); + elPieceName = el.cgPiece; + // if piece not being dragged anymore, remove dragging style + if (el.cgDragging && (!curDrag || curDrag.orig !== k)) { + el.classList.remove('dragging'); + translate(el, posToTranslate(key2pos(k), orientation, s.dimensions, s.variant)); + el.cgDragging = false; + } + // remove fading class if it still remains + if (!fading && el.cgFading) { + el.cgFading = false; + el.classList.remove('fading'); + } + // there is now a piece at this dom key + if (pieceAtKey) { + // continue animation if already animating and same piece + // (otherwise it could animate a captured piece) + if ( + anim && + el.cgAnimating && + elPieceName === pieceNameOf(pieceAtKey, s.myPlayerIndex, s.orientation, s.variant, k) + ) { + const pos = key2pos(k); + pos[0] += anim[2]; + pos[1] += anim[3]; + el.classList.add('anim'); + translate(el, posToTranslate(pos, orientation, s.dimensions, s.variant)); + } else if (el.cgAnimating) { + el.cgAnimating = false; + el.classList.remove('anim'); + translate(el, posToTranslate(key2pos(k), orientation, s.dimensions, s.variant)); + if (s.addPieceZIndex) el.style.zIndex = posZIndex(key2pos(k), orientation, asP1, s.dimensions); + } + // same piece: flag as same + if ( + elPieceName === pieceNameOf(pieceAtKey, s.myPlayerIndex, s.orientation, s.variant, k) && + (!fading || !el.cgFading) + ) { + samePieces.add(k); + } + // different piece: flag as moved unless it is a fading piece + else { + if (fading && elPieceName === pieceNameOf(fading, s.myPlayerIndex, s.orientation, s.variant, k)) { + el.classList.add('fading'); + el.cgFading = true; + } else { + appendValue(movedPieces, elPieceName, el); + } + } + } + // no piece: flag as moved + else { + appendValue(movedPieces, elPieceName, el); + } + } else if (isSquareNode(el)) { + const cn = el.className; + if (squares.get(k) === cn) sameSquares.add(k); + else if (movedSquares.has(cn)) appendValue(movedSquares, cn, el); + else movedSquares.set(cn, [el]); + } + el = el.nextSibling as cg.PieceNode | cg.SquareNode | undefined; + } + + // walk over all squares in current set, apply dom changes to moved squares + // or append new squares + for (const [sk, className] of squares) { + // if (!sameSquares.has(sk)) { + sMvdset = movedSquares.get(className); + sMvd = sMvdset && sMvdset.pop(); + const translation = posToTranslate(key2pos(sk), orientation, s.dimensions, s.variant); + if (sMvd) { + sMvd.cgKey = sk; + translate(sMvd, translation); + } else { + const squareNode = createEl('square', className) as cg.SquareNode; + squareNode.cgKey = sk; + translate(squareNode, translation); + boardEl.insertBefore(squareNode, boardEl.firstChild); + } + // } + } + + // walk over all pieces in current set, apply dom changes to moved pieces + // or append new pieces + for (const [k, p] of pieces) { + anim = anims.get(k); + if (!samePieces.has(k)) { + pMvdset = movedPieces.get(pieceNameOf(p, s.myPlayerIndex, s.orientation, s.variant, k)); + pMvd = pMvdset && pMvdset.pop(); + // a same piece was moved + if (pMvd) { + // apply dom changes + pMvd.cgKey = k; + if (pMvd.cgFading) { + pMvd.classList.remove('fading'); + pMvd.cgFading = false; + } + const pos = key2pos(k); + if (s.addPieceZIndex) pMvd.style.zIndex = posZIndex(pos, orientation, asP1, s.dimensions); + if (anim) { + pMvd.cgAnimating = true; + pMvd.classList.add('anim'); + pos[0] += anim[2]; + pos[1] += anim[3]; + } + translate(pMvd, posToTranslate(pos, orientation, s.dimensions, s.variant)); + } + // no piece in moved obj: insert the new piece + // assumes the new piece is not being dragged + else { + const pieceName = pieceNameOf(p, s.myPlayerIndex, s.orientation, s.variant, k), + pieceNode = createEl('piece', pieceName) as cg.PieceNode, + pos = key2pos(k); // used here to compute position + + pieceNode.cgPiece = pieceName; + pieceNode.cgKey = k; + if (anim) { + pieceNode.cgAnimating = true; + pos[0] += anim[2]; + pos[1] += anim[3]; + } + translate(pieceNode, posToTranslate(pos, orientation, s.dimensions, s.variant)); + + if (s.addPieceZIndex) pieceNode.style.zIndex = posZIndex(pos, orientation, asP1, s.dimensions); + + boardEl.appendChild(pieceNode); + } + } + // @TODO VFR: I added this "translate(x)" but it's probably useless. to remove. + translate( + createEl('piece', pieceNameOf(p, s.myPlayerIndex, s.orientation, s.variant, k)) as cg.PieceNode, + posToTranslate(key2pos(k), orientation, s.dimensions, s.variant) + ); + } + + // walk over all pocketPieces and set nodes + for (const p of pocketPieces) { + const classSpecificKey = p.playerIndex === 'p1' ? 'a1' : 'a2'; + const pieceName = pieceNameOf(p, s.myPlayerIndex, s.orientation, s.variant, classSpecificKey as cg.Key), + pieceNode = createEl('piece', 'pocket ' + pieceName) as cg.PieceNode; + + pieceNode.cgPiece = pieceName; + pieceNode.cgKey = s.orientation === 'p1' ? 'a2' : 'a1'; // always have 0 transform (top left corner) + + boardEl.appendChild(pieceNode); + } + + // remove any element that remains in the moved sets + for (const nodes of movedPieces.values()) removeNodes(s, nodes); + for (const nodes of movedSquares.values()) removeNodes(s, nodes); + } \ No newline at end of file diff --git a/src/variants/abalone/svg.ts b/src/variants/abalone/svg.ts new file mode 100644 index 0000000..15f650c --- /dev/null +++ b/src/variants/abalone/svg.ts @@ -0,0 +1,161 @@ +import { DrawShapePiece, DrawBrush, DrawBrushes } from '../../draw'; +import { State } from "../../state"; +import { ArrowDests, Shape, arrowMargin, circleWidth, createElement, lineWidth, makeCustomBrush, opacity, orient, setAttributes } from "../../svg"; +import * as cg from '../../types'; + +import { key2pos } from './util'; + +// pos2px is probably used to re-position elements correctly at the center of their square - an override is probably not needed +const pos2px = (pos: cg.Pos, bounds: ClientRect, bd: cg.BoardDimensions): cg.NumberPair => { + return [ + (((pos[0] * 4) + pos[0] - 0.5) * bounds.width) / bd.width, + ((bd.height + 0.5 - pos[1]) * bounds.height) / bd.height + ]; +} + +// @TODO: remove the lines below +// function pos2px(pos: cg.Pos, bounds: ClientRect, bd: cg.BoardDimensions): cg.NumberPair { +// return [((pos[0] - 0.5) * bounds.width) / bd.width, ((bd.height + 0.5 - pos[1]) * bounds.height) / bd.height]; +// } + +const roleToSvgName = (piece: DrawShapePiece): string => { + return (piece.playerIndex === 'p1' ? 'b' : 'w') + piece.role[0].toUpperCase(); +} + +export const renderPiece = ( + baseUrl: string, + pos: cg.Pos, + piece: DrawShapePiece, + bounds: ClientRect, + bd: cg.BoardDimensions, + myPlayerIndex: cg.PlayerIndex, + ): SVGElement => { + const o = pos2px(pos, bounds, bd), + width = (bounds.width / bd.width) * (piece.scale || 1), + height = (bounds.height / bd.height) * (piece.scale || 1), + //name = piece.playerIndex[0] + piece.role[0].toUpperCase(); + name = roleToSvgName(piece); + // If baseUrl doesn't end with '/' use it as full href + // This is needed when drop piece suggestion .svg image file names are different than "name" produces + const href = baseUrl.endsWith('/') ? baseUrl.slice('https://playstrategy.org'.length) + name + '.svg' : baseUrl; + const side = piece.playerIndex === myPlayerIndex ? 'ally' : 'enemy'; + return setAttributes(createElement('image'), { + className: `${piece.role} ${piece.playerIndex} ${side}`, + x: o[0] - width / 2, + y: o[1] - height / 2, + width: width, + height: height, + href: href, + }); + } + + export const renderShape = ( + state: State, + { shape, current, hash }: Shape, + brushes: DrawBrushes, + arrowDests: ArrowDests, + bounds: ClientRect, + ): SVGElement => { + let el: SVGElement; + if (shape.customSvg) { + const orig = orient(key2pos(shape.orig), state.orientation, state.dimensions); + el = renderCustomSvg(shape.customSvg, orig, bounds, state.dimensions); + } else if (shape.piece) + el = renderPiece( + state.drawable.pieces.baseUrl, + orient(key2pos(shape.orig), state.orientation, state.dimensions), + shape.piece, + bounds, + state.dimensions, + state.myPlayerIndex + ); + else { + const orig = orient(key2pos(shape.orig), state.orientation, state.dimensions); + if (shape.orig && shape.dest) { + let brush: DrawBrush = brushes[shape.brush!]; + if (shape.modifiers) brush = makeCustomBrush(brush, shape.modifiers); + el = renderArrow( + brush, + orig, + orient(key2pos(shape.dest), state.orientation, state.dimensions), + current, + (arrowDests.get(shape.dest) || 0) > 1, + bounds, + state.dimensions, + ); + } else el = renderCircle(brushes[shape.brush!], orig, current, bounds, state.dimensions); + } + el.setAttribute('cgHash', hash); + return el; + } + + + function renderCircle( + brush: DrawBrush, + pos: cg.Pos, + current: boolean, + bounds: ClientRect, + bd: cg.BoardDimensions, + ): SVGElement { + const o = pos2px(pos, bounds, bd), + widths = circleWidth(bounds, bd), + radius = (bounds.width + bounds.height) / (2 * (bd.height + bd.width)); + return setAttributes(createElement('circle'), { + stroke: brush.color, + 'stroke-width': widths[current ? 0 : 1], + fill: 'none', + opacity: opacity(brush, current), + cx: o[0], + cy: o[1], + r: radius - widths[1] / 2, + }); + } + + function renderArrow( + brush: DrawBrush, + orig: cg.Pos, + dest: cg.Pos, + current: boolean, + shorten: boolean, + bounds: ClientRect, + bd: cg.BoardDimensions, + ): SVGElement { + const m = arrowMargin(bounds, shorten && !current, bd), + a = pos2px(orig, bounds, bd), + b = pos2px(dest, bounds, bd), + dx = b[0] - a[0], + dy = b[1] - a[1], + angle = Math.atan2(dy, dx), + xo = Math.cos(angle) * m, + yo = Math.sin(angle) * m; + return setAttributes(createElement('line'), { + stroke: brush.color, + 'stroke-width': lineWidth(brush, current, bounds, bd), + 'stroke-linecap': 'round', + 'marker-end': 'url(#arrowhead-' + brush.key + ')', + opacity: opacity(brush, current), + x1: a[0], + y1: a[1], + x2: b[0] - xo, + y2: b[1] - yo, + }); + } + + +function renderCustomSvg(customSvg: string, pos: cg.Pos, bounds: ClientRect, bd: cg.BoardDimensions): SVGElement { + const { width, height } = bounds; + const w = width / bd.width; + const h = height / bd.height; + const x = (pos[0] - 1) * w; + const y = (bd.height - pos[1]) * h; + + // Translate to top-left of `orig` square + const g = setAttributes(createElement('g'), { transform: `translate(${x},${y})` }); + + // Give 100x100 coordinate system to the user for `orig` square + const svg = setAttributes(createElement('svg'), { width: w, height: h, viewBox: '0 0 100 100' }); + + g.appendChild(svg); + svg.innerHTML = customSvg; + return g; +} \ No newline at end of file diff --git a/src/variants/abalone/util.ts b/src/variants/abalone/util.ts new file mode 100644 index 0000000..d374efa --- /dev/null +++ b/src/variants/abalone/util.ts @@ -0,0 +1,137 @@ +import * as cg from '../../types'; +import * as T from '../../transformations'; + +const abaloneFiles = [ + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i', +] as const; + +const abaloneRanks = [ + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', +] as const; + +export const pos2key = (pos: cg.Pos): cg.Key => { + let posx = pos[0]; + if (pos[1] == 1) { + posx = pos[0] - 3; + } + if (pos[1] == 2) { + posx = pos[0] - 2; // -2.5 + } + if (pos[1] == 3) { + posx = pos[0] - 2; + } + if (pos[1] == 4) { + posx = pos[0] - 1; // -1.5 + } + if (pos[1] == 5) { + posx = pos[0] - 1; // - 1 + } + if (pos[1] == 6) { + posx = pos[0]; // -0.5 + } + if (pos[1] == 7) { + posx = pos[0]; // 0 + } + if (pos[1] == 8) { + posx = pos[0]; // 0.5 + } + if (pos[1] == 9) { + posx = pos[0] + 1; + } + + const key = (abaloneFiles[posx] + abaloneRanks[pos[1] - 1]) as cg.Key + // console.log("pos2key", pos, key); + return key; +} + + + +export const key2pos = (k: cg.Key): cg.Pos => { + // console.log("key2pos", k); + return [k.charCodeAt(0) - 96, parseInt(k.slice(1))] as cg.Pos; + + // @TODO VFR: rework this potentially - was used by mini boards to display them correctly - not sure yet if it's supposed to be used + // const shift = parseInt(k.slice(1)); + // const diff = (shift - 1) * 0.5; + // if (parseInt(k.slice(1)) < 5) { + // return [k.charCodeAt(0) + // - 96 + 2 - (diff), + // parseInt(k.slice(1))] as cg.Pos; + // } + // return [k.charCodeAt(0) - 96 - (diff-5) - 3, parseInt(k.slice(1))] as cg.Pos; +} + +const shift = [2,1.5,1,0.5,0,-0.5,-1,-1.5,-2] + +// @TODO VFR: translateBase should probably be in transformations.ts +// @TODO VFR: probably need to adapt the code here still anyway +const translateBase: Record = { + p1: (pos: cg.Pos, xScale: number, yScale: number, bt: cg.BoardDimensions) => [ + (pos[0] - 1 + shift[pos[1] - 1]) * xScale, + (bt.height - pos[1]) * yScale, + ], + p2: (pos: cg.Pos, xScale: number, yScale: number, bt: cg.BoardDimensions) => [ + (bt.width - pos[0] - shift[pos[1] - 1]) * xScale, + (pos[1] - 1) * yScale, + ], + right: (pos: cg.Pos, xScale: number, yScale: number, _) => [(pos[1] - 1) * xScale, (pos[0] - 1) * yScale], + left: (pos: cg.Pos, xScale: number, yScale: number, bt: cg.BoardDimensions) => [ + (bt.width - pos[0]) * xScale, + (pos[1] - 1) * yScale, + ], + p1vflip: (pos: cg.Pos, xScale: number, yScale: number, _) => [(pos[0] - 1) * xScale, (pos[1] - 1) * yScale], +} + + +export const posToTranslateAbs = ( + pos: cg.Pos, + orientation: cg.Orientation, + xFactor: number, + yFactor: number, + bt: cg.BoardDimensions, + ): cg.NumberPair => { + return translateBase[orientation](pos, xFactor, yFactor, bt); + }; + +function files(n: number) { + return abaloneFiles.slice(0, n); +} + +function ranks(n: number) { + return abaloneRanks.slice(0, n); +} + +export function allKeys(bd: cg.BoardDimensions = { width: 9, height: 9 }) { + return Array.prototype.concat(...files(bd.width).map(c => ranks(bd.height).map(r => c + r))); +} + +export const allPos = (bd: cg.BoardDimensions): cg.Pos[] => allKeys(bd).map(key2pos); + +export function computeSquareCenter( + key: cg.Key, + orientation: cg.Orientation, + bounds: ClientRect, + bd: cg.BoardDimensions, + ): cg.NumberPair { + const pos = T.mapToP1Inverse[orientation](key2pos(key), bd); + return [ + bounds.left + (bounds.width * (pos[0] - 1 + 0.5)) / bd.width, + bounds.top + (bounds.height * (bd.height - (pos[1] - 1 + 0.5))) / bd.height, + ]; + } \ No newline at end of file diff --git a/src/wrap.ts b/src/wrap.ts index 61481f6..f21d013 100644 --- a/src/wrap.ts +++ b/src/wrap.ts @@ -146,6 +146,17 @@ export function renderWrap(element: HTMLElement, s: HeadlessState, relative: boo ); } } + } else if (s.variant === 'abalone') { // @TODO VFR: probably can use the default else below - check + container.appendChild( + renderCoords( + ['1', '2', '3', '4', '5', '6', '7', '8', '9'], + 'ranks' + orientClass, + ), + ); + container.appendChild( + renderCoords(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'], 'files' + orientClass), + ); + } else { container.appendChild(renderCoords(ranks19.slice(0, s.dimensions.height), 'ranks' + orientClass)); container.appendChild(renderCoords(files.slice(0, s.dimensions.width), 'files' + orientClass)); @@ -177,6 +188,10 @@ export function renderWrap(element: HTMLElement, s: HeadlessState, relative: boo const boardScores = calculateBackgammonScores(s.pieces, s.pocketPieces, s.dimensions); container.appendChild(renderBoardScores(boardScores.p1.toString(), 'p1')); container.appendChild(renderBoardScores(boardScores.p2.toString(), 'p2')); + } else if (s.variant === 'abalone') { + const boardScores = calculateBackgammonScores(s.pieces, s.pocketPieces, s.dimensions); // @TODO VFR: ADAPT + container.appendChild(renderBoardScores(boardScores.p1.toString(), 'p1')); + container.appendChild(renderBoardScores(boardScores.p2.toString(), 'p2')); } else { container.appendChild(renderBoardScores('0', s.orientation)); } From 5f0bfdeb1a6337e76988edd5b4cac2eb1d4486a2 Mon Sep 17 00:00:00 2001 From: Vincent FROCHOT Date: Wed, 13 Nov 2024 21:41:04 +0100 Subject: [PATCH 05/22] chore: pnpm format --- src/draw.ts | 9 +- src/fen.ts | 2 +- src/premove.ts | 5 +- src/svg.ts | 16 +- src/variants/abalone/README.md | 2 +- src/variants/abalone/board.ts | 14 +- src/variants/abalone/draw.ts | 89 ++++---- src/variants/abalone/fen.ts | 55 +++-- src/variants/abalone/premove.ts | 12 +- src/variants/abalone/render.ts | 374 ++++++++++++++++---------------- src/variants/abalone/svg.ts | 251 ++++++++++----------- src/variants/abalone/util.ts | 131 +++++------ src/wrap.ts | 15 +- 13 files changed, 472 insertions(+), 503 deletions(-) diff --git a/src/draw.ts b/src/draw.ts index c1cb6ca..c53fd45 100644 --- a/src/draw.ts +++ b/src/draw.ts @@ -3,10 +3,7 @@ import { unselect, cancelMove, getKeyAtDomPos, getSnappedKeyAtDomPos } from './b import { eventPosition, isRightButton } from './util'; import * as cg from './types'; -import { - start as abaloneStart, - processDraw as abaloneProcessDraw -} from './variants/abalone/draw'; +import { start as abaloneStart, processDraw as abaloneProcessDraw } from './variants/abalone/draw'; export interface DrawShape { orig: cg.Key; @@ -67,7 +64,7 @@ export interface DrawCurrent { const brushes = ['green', 'red', 'blue', 'yellow']; export function start(state: State, e: cg.MouchEvent): void { - if(state.variant === "abalone") { + if (state.variant === 'abalone') { return abaloneStart(state, e); } // support one finger touch only @@ -89,7 +86,7 @@ export function start(state: State, e: cg.MouchEvent): void { } export function processDraw(state: State): void { - if(state.variant === "abalone") { + if (state.variant === 'abalone') { return abaloneProcessDraw(state); } requestAnimationFrame(() => { diff --git a/src/fen.ts b/src/fen.ts index bbf3971..f1b0ff0 100644 --- a/src/fen.ts +++ b/src/fen.ts @@ -17,7 +17,7 @@ function letters(role: cg.Role) { } export function read(fen: cg.FEN, dimensions: cg.BoardDimensions, variant: cg.Variant): cg.Pieces { - if(variant === "abalone") return abaloneRead(fen, dimensions); + if (variant === 'abalone') return abaloneRead(fen, dimensions); if (fen === 'start') fen = initial; if (fen.indexOf('[') !== -1) fen = fen.slice(0, fen.indexOf('[')); const pieces: cg.Pieces = new Map(); diff --git a/src/premove.ts b/src/premove.ts index 014c38a..8697d0e 100644 --- a/src/premove.ts +++ b/src/premove.ts @@ -1150,10 +1150,7 @@ export function premove( break; case 'abalone': - mobility = abaloneMarble( - pieces, - playerIndex - ); + mobility = abaloneMarble(pieces, playerIndex); break; // Variants using standard pieces and additional fairy pieces like S-chess, Capablanca, etc. diff --git a/src/svg.ts b/src/svg.ts index 207c6a5..d4fa23d 100644 --- a/src/svg.ts +++ b/src/svg.ts @@ -4,10 +4,7 @@ import { Drawable, DrawShape, DrawShapePiece, DrawBrush, DrawBrushes, DrawModifi import * as cg from './types'; import * as T from './transformations'; -import { - renderPiece as abaloneRenderPiece, - renderShape as abaloneRenderShape -} from './variants/abalone/svg'; +import { renderPiece as abaloneRenderPiece, renderShape as abaloneRenderShape } from './variants/abalone/svg'; export function createElement(tagName: string): SVGElement { return document.createElementNS('http://www.w3.org/2000/svg', tagName); @@ -190,7 +187,7 @@ function renderShape( arrowDests: ArrowDests, bounds: ClientRect, ): SVGElement { - if (state.variant === "abalone") { + if (state.variant === 'abalone') { return abaloneRenderShape(state, { shape, current, hash }, brushes, arrowDests, bounds); } let el: SVGElement; @@ -305,13 +302,8 @@ function renderPiece( myPlayerIndex: cg.PlayerIndex, variant: cg.Variant, ): SVGElement { - if (variant === "abalone") { - return abaloneRenderPiece(baseUrl, - pos, - piece, - bounds, - bd, - myPlayerIndex); + if (variant === 'abalone') { + return abaloneRenderPiece(baseUrl, pos, piece, bounds, bd, myPlayerIndex); } const o = pos2px(pos, bounds, bd), width = (bounds.width / bd.width) * (piece.scale || 1), diff --git a/src/variants/abalone/README.md b/src/variants/abalone/README.md index ad541cd..12501ab 100644 --- a/src/variants/abalone/README.md +++ b/src/variants/abalone/README.md @@ -1 +1 @@ -Ideas from mid-investigation : We need to define our own key2pos and pos2key functions as the grid is an hexagon ? \ No newline at end of file +Ideas from mid-investigation : We need to define our own key2pos and pos2key functions as the grid is an hexagon ? diff --git a/src/variants/abalone/board.ts b/src/variants/abalone/board.ts index d02a026..836b51f 100644 --- a/src/variants/abalone/board.ts +++ b/src/variants/abalone/board.ts @@ -6,11 +6,11 @@ import { distanceSq } from '../../util'; import { allPos, computeSquareCenter, key2pos, pos2key } from './util'; export const getKeyAtDomPos = ( - pos: cg.NumberPair, - orientation: cg.Orientation, - bounds: ClientRect, - bd: cg.BoardDimensions - ): cg.Key | undefined => { + pos: cg.NumberPair, + orientation: cg.Orientation, + bounds: ClientRect, + bd: cg.BoardDimensions, +): cg.Key | undefined => { let file = Math.ceil(bd.width * ((pos[0] - bounds.left) / bounds.width)); const rank = Math.ceil(bd.height - bd.height * ((pos[1] - bounds.top) / bounds.height)); @@ -18,7 +18,7 @@ export const getKeyAtDomPos = ( pos = [file, rank]; pos = T.mapToP1[orientation](pos, bd); return pos[0] > 0 && pos[0] < bd.width + 1 && pos[1] > 0 && pos[1] < bd.height + 1 ? pos2key(pos) : undefined; -} +}; export const getSnappedKeyAtDomPos = ( orig: cg.Key, @@ -38,4 +38,4 @@ export const getSnappedKeyAtDomPos = ( [validSnapDistances[0], 0], ); return pos2key(validSnapPos[closestSnapIndex]); -} +}; diff --git a/src/variants/abalone/draw.ts b/src/variants/abalone/draw.ts index f9bc8ee..7a40bc8 100644 --- a/src/variants/abalone/draw.ts +++ b/src/variants/abalone/draw.ts @@ -1,51 +1,46 @@ -import { cancelMove, unselect } from "../../board"; -import { eventBrush } from "../../draw"; -import { State } from "../../state"; -import { eventPosition } from "../../util"; -import { getKeyAtDomPos, getSnappedKeyAtDomPos } from "./board"; +import { cancelMove, unselect } from '../../board'; +import { eventBrush } from '../../draw'; +import { State } from '../../state'; +import { eventPosition } from '../../util'; +import { getKeyAtDomPos, getSnappedKeyAtDomPos } from './board'; import * as cg from '../../types'; export const start = (state: State, e: cg.MouchEvent): void => { - // support one finger touch only - if (e.touches && e.touches.length > 1) return; - e.stopPropagation(); - e.preventDefault(); - e.ctrlKey ? unselect(state) : cancelMove(state); - const pos = eventPosition(e)!, - orig = getKeyAtDomPos(pos, state.orientation, state.dom.bounds(), state.dimensions); - if (!orig) return; - state.drawable.current = { - orig, - pos, - brush: eventBrush(e), - snapToValidMove: state.drawable.defaultSnapToValidMove, - }; - - processDraw(state); - } - - export const processDraw = (state: State): void => { - requestAnimationFrame(() => { - const cur = state.drawable.current; - if (cur) { - const keyAtDomPos = getKeyAtDomPos( - cur.pos, - state.orientation, - state.dom.bounds(), - state.dimensions - ); - if (!keyAtDomPos) { - cur.snapToValidMove = false; - } - const mouseSq = cur.snapToValidMove - ? getSnappedKeyAtDomPos(cur.orig, cur.pos, state.orientation, state.dom.bounds(), state.dimensions) - : keyAtDomPos; - if (mouseSq !== cur.mouseSq) { - cur.mouseSq = mouseSq; - cur.dest = mouseSq !== cur.orig ? mouseSq : undefined; - state.dom.redrawNow(); - } - processDraw(state); + // support one finger touch only + if (e.touches && e.touches.length > 1) return; + e.stopPropagation(); + e.preventDefault(); + e.ctrlKey ? unselect(state) : cancelMove(state); + const pos = eventPosition(e)!, + orig = getKeyAtDomPos(pos, state.orientation, state.dom.bounds(), state.dimensions); + if (!orig) return; + state.drawable.current = { + orig, + pos, + brush: eventBrush(e), + snapToValidMove: state.drawable.defaultSnapToValidMove, + }; + + processDraw(state); +}; + +export const processDraw = (state: State): void => { + requestAnimationFrame(() => { + const cur = state.drawable.current; + if (cur) { + const keyAtDomPos = getKeyAtDomPos(cur.pos, state.orientation, state.dom.bounds(), state.dimensions); + if (!keyAtDomPos) { + cur.snapToValidMove = false; + } + const mouseSq = cur.snapToValidMove + ? getSnappedKeyAtDomPos(cur.orig, cur.pos, state.orientation, state.dom.bounds(), state.dimensions) + : keyAtDomPos; + if (mouseSq !== cur.mouseSq) { + cur.mouseSq = mouseSq; + cur.dest = mouseSq !== cur.orig ? mouseSq : undefined; + state.dom.redrawNow(); } - }); - } \ No newline at end of file + processDraw(state); + } + }); +}; diff --git a/src/variants/abalone/fen.ts b/src/variants/abalone/fen.ts index 55b391c..04d1b36 100644 --- a/src/variants/abalone/fen.ts +++ b/src/variants/abalone/fen.ts @@ -3,35 +3,34 @@ import * as cg from '../../types'; import { pos2key } from '../../util'; export const read = (fen: cg.FEN, dimensions: cg.BoardDimensions): cg.Pieces => { + const pieces: cg.Pieces = new Map(); - const pieces: cg.Pieces = new Map(); + let row: number = dimensions.height; + const padding = [0, 0, 0, 0, 0, 0, 1, 2, 3, 4]; + let file = padding[row]; - let row: number = dimensions.height; - const padding = [0,0,0,0,0,0,1,2,3,4]; - let file = padding[row]; - - for (let i = 0; i < fen.length; i++) { - const c = fen[i]; - if (c === ' ') break - else if (c == '/') { - file = padding[row - 1]; - --row; - } else { - const step = parseInt(c, 10); - if (step > 0) { - file += step; - } else { - file++; - const letter = c.toLowerCase(); - const playerIndex = (c === letter ? 'p2' : 'p1') as cg.PlayerIndex; - const piece = { - role: roles(letter), - playerIndex: playerIndex, - } as cg.Piece; - pieces.set(pos2key([file, row]), piece); // @TODO VFR: check all cases are correctly handled to prevent a js error in console - } - } + for (let i = 0; i < fen.length; i++) { + const c = fen[i]; + if (c === ' ') break; + else if (c == '/') { + file = padding[row - 1]; + --row; + } else { + const step = parseInt(c, 10); + if (step > 0) { + file += step; + } else { + file++; + const letter = c.toLowerCase(); + const playerIndex = (c === letter ? 'p2' : 'p1') as cg.PlayerIndex; + const piece = { + role: roles(letter), + playerIndex: playerIndex, + } as cg.Piece; + pieces.set(pos2key([file, row]), piece); // @TODO VFR: check all cases are correctly handled to prevent a js error in console + } } + } - return pieces; -} + return pieces; +}; diff --git a/src/variants/abalone/premove.ts b/src/variants/abalone/premove.ts index a9f8410..7d86080 100644 --- a/src/variants/abalone/premove.ts +++ b/src/variants/abalone/premove.ts @@ -4,9 +4,9 @@ import { pos2key } from './util'; // @VFR: TODO: update this, the code is just a wrong copy paste export const marble = (pieces: cg.Pieces, playerIndex: cg.PlayerIndex): Mobility => { - return (x1, y1 /*, x2, y2*/) => { - const pos = pos2key([x1, y1 + (playerIndex === 'p1' ? 1 : -1)]) as cg.Key; - if (pieces.has(pos) && pieces.get(pos)?.playerIndex === playerIndex) return false; - return false; - } - } \ No newline at end of file + return (x1, y1 /*, x2, y2*/) => { + const pos = pos2key([x1, y1 + (playerIndex === 'p1' ? 1 : -1)]) as cg.Key; + if (pieces.has(pos) && pieces.get(pos)?.playerIndex === playerIndex) return false; + return false; + }; +}; diff --git a/src/variants/abalone/render.ts b/src/variants/abalone/render.ts index 35647d9..aab79f8 100644 --- a/src/variants/abalone/render.ts +++ b/src/variants/abalone/render.ts @@ -1,199 +1,209 @@ -import { p1Pov } from "../../board"; -import { State } from "../../state"; -import { createEl, posToTranslateAbs, posToTranslateRel, translateAbs, translateRel } from "../../util"; +import { p1Pov } from '../../board'; +import { State } from '../../state'; +import { createEl, posToTranslateAbs, posToTranslateRel, translateAbs, translateRel } from '../../util'; import * as cg from '../../types'; -import { AnimCurrent, AnimFadings, AnimVector, AnimVectors } from "../../anim"; -import { DragCurrent } from "../../drag"; -import { PieceName, SquareClasses, appendValue, computeSquareClasses, isPieceNode, isSquareNode, pieceNameOf, posZIndex, removeNodes } from "../../render"; +import { AnimCurrent, AnimFadings, AnimVector, AnimVectors } from '../../anim'; +import { DragCurrent } from '../../drag'; +import { + PieceName, + SquareClasses, + appendValue, + computeSquareClasses, + isPieceNode, + isSquareNode, + pieceNameOf, + posZIndex, + removeNodes, +} from '../../render'; -import { key2pos } from "./util"; +import { key2pos } from './util'; // @TODO VFR: this code is an ugly copy paste, refactor etc export const render = (s: State): void => { - const orientation = s.orientation, - asP1: boolean = p1Pov(s), - posToTranslate = s.dom.relative ? posToTranslateRel : posToTranslateAbs(s.dom.bounds(), s.dimensions, s.variant), - translate = s.dom.relative ? translateRel : translateAbs, - boardEl: HTMLElement = s.dom.elements.board, - pieces: cg.Pieces = s.pieces, - pocketPieces: cg.Piece[] = s.pocketPieces, - curAnim: AnimCurrent | undefined = s.animation.current, - anims: AnimVectors = curAnim ? curAnim.plan.anims : new Map(), - fadings: AnimFadings = curAnim ? curAnim.plan.fadings : new Map(), - curDrag: DragCurrent | undefined = s.draggable.current, - squares: SquareClasses = computeSquareClasses(s), - samePieces: Set = new Set(), - sameSquares: Set = new Set(), - movedPieces: Map = new Map(), - movedSquares: Map = new Map(); // by class name - - let k: cg.Key, - el: cg.PieceNode | cg.SquareNode | undefined, - pieceAtKey: cg.Piece | undefined, - elPieceName: PieceName, - anim: AnimVector | undefined, - fading: cg.Piece | undefined, - pMvdset: cg.PieceNode[] | undefined, - pMvd: cg.PieceNode | undefined, - sMvdset: cg.SquareNode[] | undefined, - sMvd: cg.SquareNode | undefined; - - // walk over all board dom elements, apply animations and flag moved pieces - el = boardEl.firstChild as cg.PieceNode | cg.SquareNode | undefined; - - while (el) { - k = el.cgKey; - if (isPieceNode(el)) { - pieceAtKey = pieces.get(k); - anim = anims.get(k); - fading = fadings.get(k); - elPieceName = el.cgPiece; - // if piece not being dragged anymore, remove dragging style - if (el.cgDragging && (!curDrag || curDrag.orig !== k)) { - el.classList.remove('dragging'); + const orientation = s.orientation, + asP1: boolean = p1Pov(s), + posToTranslate = s.dom.relative ? posToTranslateRel : posToTranslateAbs(s.dom.bounds(), s.dimensions, s.variant), + translate = s.dom.relative ? translateRel : translateAbs, + boardEl: HTMLElement = s.dom.elements.board, + pieces: cg.Pieces = s.pieces, + pocketPieces: cg.Piece[] = s.pocketPieces, + curAnim: AnimCurrent | undefined = s.animation.current, + anims: AnimVectors = curAnim ? curAnim.plan.anims : new Map(), + fadings: AnimFadings = curAnim ? curAnim.plan.fadings : new Map(), + curDrag: DragCurrent | undefined = s.draggable.current, + squares: SquareClasses = computeSquareClasses(s), + samePieces: Set = new Set(), + sameSquares: Set = new Set(), + movedPieces: Map = new Map(), + movedSquares: Map = new Map(); // by class name + + let k: cg.Key, + el: cg.PieceNode | cg.SquareNode | undefined, + pieceAtKey: cg.Piece | undefined, + elPieceName: PieceName, + anim: AnimVector | undefined, + fading: cg.Piece | undefined, + pMvdset: cg.PieceNode[] | undefined, + pMvd: cg.PieceNode | undefined, + sMvdset: cg.SquareNode[] | undefined, + sMvd: cg.SquareNode | undefined; + + // walk over all board dom elements, apply animations and flag moved pieces + el = boardEl.firstChild as cg.PieceNode | cg.SquareNode | undefined; + + while (el) { + k = el.cgKey; + if (isPieceNode(el)) { + pieceAtKey = pieces.get(k); + anim = anims.get(k); + fading = fadings.get(k); + elPieceName = el.cgPiece; + // if piece not being dragged anymore, remove dragging style + if (el.cgDragging && (!curDrag || curDrag.orig !== k)) { + el.classList.remove('dragging'); + translate(el, posToTranslate(key2pos(k), orientation, s.dimensions, s.variant)); + el.cgDragging = false; + } + // remove fading class if it still remains + if (!fading && el.cgFading) { + el.cgFading = false; + el.classList.remove('fading'); + } + // there is now a piece at this dom key + if (pieceAtKey) { + // continue animation if already animating and same piece + // (otherwise it could animate a captured piece) + if ( + anim && + el.cgAnimating && + elPieceName === pieceNameOf(pieceAtKey, s.myPlayerIndex, s.orientation, s.variant, k) + ) { + const pos = key2pos(k); + pos[0] += anim[2]; + pos[1] += anim[3]; + el.classList.add('anim'); + translate(el, posToTranslate(pos, orientation, s.dimensions, s.variant)); + } else if (el.cgAnimating) { + el.cgAnimating = false; + el.classList.remove('anim'); translate(el, posToTranslate(key2pos(k), orientation, s.dimensions, s.variant)); - el.cgDragging = false; - } - // remove fading class if it still remains - if (!fading && el.cgFading) { - el.cgFading = false; - el.classList.remove('fading'); + if (s.addPieceZIndex) el.style.zIndex = posZIndex(key2pos(k), orientation, asP1, s.dimensions); } - // there is now a piece at this dom key - if (pieceAtKey) { - // continue animation if already animating and same piece - // (otherwise it could animate a captured piece) - if ( - anim && - el.cgAnimating && - elPieceName === pieceNameOf(pieceAtKey, s.myPlayerIndex, s.orientation, s.variant, k) - ) { - const pos = key2pos(k); - pos[0] += anim[2]; - pos[1] += anim[3]; - el.classList.add('anim'); - translate(el, posToTranslate(pos, orientation, s.dimensions, s.variant)); - } else if (el.cgAnimating) { - el.cgAnimating = false; - el.classList.remove('anim'); - translate(el, posToTranslate(key2pos(k), orientation, s.dimensions, s.variant)); - if (s.addPieceZIndex) el.style.zIndex = posZIndex(key2pos(k), orientation, asP1, s.dimensions); - } - // same piece: flag as same - if ( - elPieceName === pieceNameOf(pieceAtKey, s.myPlayerIndex, s.orientation, s.variant, k) && - (!fading || !el.cgFading) - ) { - samePieces.add(k); - } - // different piece: flag as moved unless it is a fading piece - else { - if (fading && elPieceName === pieceNameOf(fading, s.myPlayerIndex, s.orientation, s.variant, k)) { - el.classList.add('fading'); - el.cgFading = true; - } else { - appendValue(movedPieces, elPieceName, el); - } - } + // same piece: flag as same + if ( + elPieceName === pieceNameOf(pieceAtKey, s.myPlayerIndex, s.orientation, s.variant, k) && + (!fading || !el.cgFading) + ) { + samePieces.add(k); } - // no piece: flag as moved + // different piece: flag as moved unless it is a fading piece else { - appendValue(movedPieces, elPieceName, el); + if (fading && elPieceName === pieceNameOf(fading, s.myPlayerIndex, s.orientation, s.variant, k)) { + el.classList.add('fading'); + el.cgFading = true; + } else { + appendValue(movedPieces, elPieceName, el); + } } - } else if (isSquareNode(el)) { - const cn = el.className; - if (squares.get(k) === cn) sameSquares.add(k); - else if (movedSquares.has(cn)) appendValue(movedSquares, cn, el); - else movedSquares.set(cn, [el]); } - el = el.nextSibling as cg.PieceNode | cg.SquareNode | undefined; + // no piece: flag as moved + else { + appendValue(movedPieces, elPieceName, el); + } + } else if (isSquareNode(el)) { + const cn = el.className; + if (squares.get(k) === cn) sameSquares.add(k); + else if (movedSquares.has(cn)) appendValue(movedSquares, cn, el); + else movedSquares.set(cn, [el]); } - - // walk over all squares in current set, apply dom changes to moved squares - // or append new squares - for (const [sk, className] of squares) { - // if (!sameSquares.has(sk)) { - sMvdset = movedSquares.get(className); - sMvd = sMvdset && sMvdset.pop(); - const translation = posToTranslate(key2pos(sk), orientation, s.dimensions, s.variant); - if (sMvd) { - sMvd.cgKey = sk; - translate(sMvd, translation); - } else { - const squareNode = createEl('square', className) as cg.SquareNode; - squareNode.cgKey = sk; - translate(squareNode, translation); - boardEl.insertBefore(squareNode, boardEl.firstChild); - } - // } + el = el.nextSibling as cg.PieceNode | cg.SquareNode | undefined; + } + + // walk over all squares in current set, apply dom changes to moved squares + // or append new squares + for (const [sk, className] of squares) { + // if (!sameSquares.has(sk)) { + sMvdset = movedSquares.get(className); + sMvd = sMvdset && sMvdset.pop(); + const translation = posToTranslate(key2pos(sk), orientation, s.dimensions, s.variant); + if (sMvd) { + sMvd.cgKey = sk; + translate(sMvd, translation); + } else { + const squareNode = createEl('square', className) as cg.SquareNode; + squareNode.cgKey = sk; + translate(squareNode, translation); + boardEl.insertBefore(squareNode, boardEl.firstChild); } - - // walk over all pieces in current set, apply dom changes to moved pieces - // or append new pieces - for (const [k, p] of pieces) { - anim = anims.get(k); - if (!samePieces.has(k)) { - pMvdset = movedPieces.get(pieceNameOf(p, s.myPlayerIndex, s.orientation, s.variant, k)); - pMvd = pMvdset && pMvdset.pop(); - // a same piece was moved - if (pMvd) { - // apply dom changes - pMvd.cgKey = k; - if (pMvd.cgFading) { - pMvd.classList.remove('fading'); - pMvd.cgFading = false; - } - const pos = key2pos(k); - if (s.addPieceZIndex) pMvd.style.zIndex = posZIndex(pos, orientation, asP1, s.dimensions); - if (anim) { - pMvd.cgAnimating = true; - pMvd.classList.add('anim'); - pos[0] += anim[2]; - pos[1] += anim[3]; - } - translate(pMvd, posToTranslate(pos, orientation, s.dimensions, s.variant)); + // } + } + + // walk over all pieces in current set, apply dom changes to moved pieces + // or append new pieces + for (const [k, p] of pieces) { + anim = anims.get(k); + if (!samePieces.has(k)) { + pMvdset = movedPieces.get(pieceNameOf(p, s.myPlayerIndex, s.orientation, s.variant, k)); + pMvd = pMvdset && pMvdset.pop(); + // a same piece was moved + if (pMvd) { + // apply dom changes + pMvd.cgKey = k; + if (pMvd.cgFading) { + pMvd.classList.remove('fading'); + pMvd.cgFading = false; } - // no piece in moved obj: insert the new piece - // assumes the new piece is not being dragged - else { - const pieceName = pieceNameOf(p, s.myPlayerIndex, s.orientation, s.variant, k), - pieceNode = createEl('piece', pieceName) as cg.PieceNode, - pos = key2pos(k); // used here to compute position - - pieceNode.cgPiece = pieceName; - pieceNode.cgKey = k; - if (anim) { - pieceNode.cgAnimating = true; - pos[0] += anim[2]; - pos[1] += anim[3]; - } - translate(pieceNode, posToTranslate(pos, orientation, s.dimensions, s.variant)); - - if (s.addPieceZIndex) pieceNode.style.zIndex = posZIndex(pos, orientation, asP1, s.dimensions); - - boardEl.appendChild(pieceNode); + const pos = key2pos(k); + if (s.addPieceZIndex) pMvd.style.zIndex = posZIndex(pos, orientation, asP1, s.dimensions); + if (anim) { + pMvd.cgAnimating = true; + pMvd.classList.add('anim'); + pos[0] += anim[2]; + pos[1] += anim[3]; } + translate(pMvd, posToTranslate(pos, orientation, s.dimensions, s.variant)); + } + // no piece in moved obj: insert the new piece + // assumes the new piece is not being dragged + else { + const pieceName = pieceNameOf(p, s.myPlayerIndex, s.orientation, s.variant, k), + pieceNode = createEl('piece', pieceName) as cg.PieceNode, + pos = key2pos(k); // used here to compute position + + pieceNode.cgPiece = pieceName; + pieceNode.cgKey = k; + if (anim) { + pieceNode.cgAnimating = true; + pos[0] += anim[2]; + pos[1] += anim[3]; + } + translate(pieceNode, posToTranslate(pos, orientation, s.dimensions, s.variant)); + + if (s.addPieceZIndex) pieceNode.style.zIndex = posZIndex(pos, orientation, asP1, s.dimensions); + + boardEl.appendChild(pieceNode); } - // @TODO VFR: I added this "translate(x)" but it's probably useless. to remove. - translate( - createEl('piece', pieceNameOf(p, s.myPlayerIndex, s.orientation, s.variant, k)) as cg.PieceNode, - posToTranslate(key2pos(k), orientation, s.dimensions, s.variant) - ); - } - - // walk over all pocketPieces and set nodes - for (const p of pocketPieces) { - const classSpecificKey = p.playerIndex === 'p1' ? 'a1' : 'a2'; - const pieceName = pieceNameOf(p, s.myPlayerIndex, s.orientation, s.variant, classSpecificKey as cg.Key), - pieceNode = createEl('piece', 'pocket ' + pieceName) as cg.PieceNode; - - pieceNode.cgPiece = pieceName; - pieceNode.cgKey = s.orientation === 'p1' ? 'a2' : 'a1'; // always have 0 transform (top left corner) - - boardEl.appendChild(pieceNode); } - - // remove any element that remains in the moved sets - for (const nodes of movedPieces.values()) removeNodes(s, nodes); - for (const nodes of movedSquares.values()) removeNodes(s, nodes); - } \ No newline at end of file + // @TODO VFR: I added this "translate(x)" but it's probably useless. to remove. + translate( + createEl('piece', pieceNameOf(p, s.myPlayerIndex, s.orientation, s.variant, k)) as cg.PieceNode, + posToTranslate(key2pos(k), orientation, s.dimensions, s.variant), + ); + } + + // walk over all pocketPieces and set nodes + for (const p of pocketPieces) { + const classSpecificKey = p.playerIndex === 'p1' ? 'a1' : 'a2'; + const pieceName = pieceNameOf(p, s.myPlayerIndex, s.orientation, s.variant, classSpecificKey as cg.Key), + pieceNode = createEl('piece', 'pocket ' + pieceName) as cg.PieceNode; + + pieceNode.cgPiece = pieceName; + pieceNode.cgKey = s.orientation === 'p1' ? 'a2' : 'a1'; // always have 0 transform (top left corner) + + boardEl.appendChild(pieceNode); + } + + // remove any element that remains in the moved sets + for (const nodes of movedPieces.values()) removeNodes(s, nodes); + for (const nodes of movedSquares.values()) removeNodes(s, nodes); +}; diff --git a/src/variants/abalone/svg.ts b/src/variants/abalone/svg.ts index 15f650c..418b67f 100644 --- a/src/variants/abalone/svg.ts +++ b/src/variants/abalone/svg.ts @@ -1,17 +1,28 @@ import { DrawShapePiece, DrawBrush, DrawBrushes } from '../../draw'; -import { State } from "../../state"; -import { ArrowDests, Shape, arrowMargin, circleWidth, createElement, lineWidth, makeCustomBrush, opacity, orient, setAttributes } from "../../svg"; +import { State } from '../../state'; +import { + ArrowDests, + Shape, + arrowMargin, + circleWidth, + createElement, + lineWidth, + makeCustomBrush, + opacity, + orient, + setAttributes, +} from '../../svg'; import * as cg from '../../types'; import { key2pos } from './util'; // pos2px is probably used to re-position elements correctly at the center of their square - an override is probably not needed const pos2px = (pos: cg.Pos, bounds: ClientRect, bd: cg.BoardDimensions): cg.NumberPair => { - return [ - (((pos[0] * 4) + pos[0] - 0.5) * bounds.width) / bd.width, - ((bd.height + 0.5 - pos[1]) * bounds.height) / bd.height - ]; -} + return [ + ((pos[0] * 4 + pos[0] - 0.5) * bounds.width) / bd.width, + ((bd.height + 0.5 - pos[1]) * bounds.height) / bd.height, + ]; +}; // @TODO: remove the lines below // function pos2px(pos: cg.Pos, bounds: ClientRect, bd: cg.BoardDimensions): cg.NumberPair { @@ -19,128 +30,126 @@ const pos2px = (pos: cg.Pos, bounds: ClientRect, bd: cg.BoardDimensions): cg.Num // } const roleToSvgName = (piece: DrawShapePiece): string => { - return (piece.playerIndex === 'p1' ? 'b' : 'w') + piece.role[0].toUpperCase(); -} + return (piece.playerIndex === 'p1' ? 'b' : 'w') + piece.role[0].toUpperCase(); +}; export const renderPiece = ( - baseUrl: string, - pos: cg.Pos, - piece: DrawShapePiece, - bounds: ClientRect, - bd: cg.BoardDimensions, - myPlayerIndex: cg.PlayerIndex, - ): SVGElement => { - const o = pos2px(pos, bounds, bd), - width = (bounds.width / bd.width) * (piece.scale || 1), - height = (bounds.height / bd.height) * (piece.scale || 1), - //name = piece.playerIndex[0] + piece.role[0].toUpperCase(); - name = roleToSvgName(piece); - // If baseUrl doesn't end with '/' use it as full href - // This is needed when drop piece suggestion .svg image file names are different than "name" produces - const href = baseUrl.endsWith('/') ? baseUrl.slice('https://playstrategy.org'.length) + name + '.svg' : baseUrl; - const side = piece.playerIndex === myPlayerIndex ? 'ally' : 'enemy'; - return setAttributes(createElement('image'), { - className: `${piece.role} ${piece.playerIndex} ${side}`, - x: o[0] - width / 2, - y: o[1] - height / 2, - width: width, - height: height, - href: href, - }); - } + baseUrl: string, + pos: cg.Pos, + piece: DrawShapePiece, + bounds: ClientRect, + bd: cg.BoardDimensions, + myPlayerIndex: cg.PlayerIndex, +): SVGElement => { + const o = pos2px(pos, bounds, bd), + width = (bounds.width / bd.width) * (piece.scale || 1), + height = (bounds.height / bd.height) * (piece.scale || 1), + //name = piece.playerIndex[0] + piece.role[0].toUpperCase(); + name = roleToSvgName(piece); + // If baseUrl doesn't end with '/' use it as full href + // This is needed when drop piece suggestion .svg image file names are different than "name" produces + const href = baseUrl.endsWith('/') ? baseUrl.slice('https://playstrategy.org'.length) + name + '.svg' : baseUrl; + const side = piece.playerIndex === myPlayerIndex ? 'ally' : 'enemy'; + return setAttributes(createElement('image'), { + className: `${piece.role} ${piece.playerIndex} ${side}`, + x: o[0] - width / 2, + y: o[1] - height / 2, + width: width, + height: height, + href: href, + }); +}; - export const renderShape = ( - state: State, - { shape, current, hash }: Shape, - brushes: DrawBrushes, - arrowDests: ArrowDests, - bounds: ClientRect, - ): SVGElement => { - let el: SVGElement; - if (shape.customSvg) { - const orig = orient(key2pos(shape.orig), state.orientation, state.dimensions); - el = renderCustomSvg(shape.customSvg, orig, bounds, state.dimensions); - } else if (shape.piece) - el = renderPiece( - state.drawable.pieces.baseUrl, - orient(key2pos(shape.orig), state.orientation, state.dimensions), - shape.piece, +export const renderShape = ( + state: State, + { shape, current, hash }: Shape, + brushes: DrawBrushes, + arrowDests: ArrowDests, + bounds: ClientRect, +): SVGElement => { + let el: SVGElement; + if (shape.customSvg) { + const orig = orient(key2pos(shape.orig), state.orientation, state.dimensions); + el = renderCustomSvg(shape.customSvg, orig, bounds, state.dimensions); + } else if (shape.piece) + el = renderPiece( + state.drawable.pieces.baseUrl, + orient(key2pos(shape.orig), state.orientation, state.dimensions), + shape.piece, + bounds, + state.dimensions, + state.myPlayerIndex, + ); + else { + const orig = orient(key2pos(shape.orig), state.orientation, state.dimensions); + if (shape.orig && shape.dest) { + let brush: DrawBrush = brushes[shape.brush!]; + if (shape.modifiers) brush = makeCustomBrush(brush, shape.modifiers); + el = renderArrow( + brush, + orig, + orient(key2pos(shape.dest), state.orientation, state.dimensions), + current, + (arrowDests.get(shape.dest) || 0) > 1, bounds, state.dimensions, - state.myPlayerIndex ); - else { - const orig = orient(key2pos(shape.orig), state.orientation, state.dimensions); - if (shape.orig && shape.dest) { - let brush: DrawBrush = brushes[shape.brush!]; - if (shape.modifiers) brush = makeCustomBrush(brush, shape.modifiers); - el = renderArrow( - brush, - orig, - orient(key2pos(shape.dest), state.orientation, state.dimensions), - current, - (arrowDests.get(shape.dest) || 0) > 1, - bounds, - state.dimensions, - ); - } else el = renderCircle(brushes[shape.brush!], orig, current, bounds, state.dimensions); - } - el.setAttribute('cgHash', hash); - return el; - } - - - function renderCircle( - brush: DrawBrush, - pos: cg.Pos, - current: boolean, - bounds: ClientRect, - bd: cg.BoardDimensions, - ): SVGElement { - const o = pos2px(pos, bounds, bd), - widths = circleWidth(bounds, bd), - radius = (bounds.width + bounds.height) / (2 * (bd.height + bd.width)); - return setAttributes(createElement('circle'), { - stroke: brush.color, - 'stroke-width': widths[current ? 0 : 1], - fill: 'none', - opacity: opacity(brush, current), - cx: o[0], - cy: o[1], - r: radius - widths[1] / 2, - }); + } else el = renderCircle(brushes[shape.brush!], orig, current, bounds, state.dimensions); } + el.setAttribute('cgHash', hash); + return el; +}; - function renderArrow( - brush: DrawBrush, - orig: cg.Pos, - dest: cg.Pos, - current: boolean, - shorten: boolean, - bounds: ClientRect, - bd: cg.BoardDimensions, - ): SVGElement { - const m = arrowMargin(bounds, shorten && !current, bd), - a = pos2px(orig, bounds, bd), - b = pos2px(dest, bounds, bd), - dx = b[0] - a[0], - dy = b[1] - a[1], - angle = Math.atan2(dy, dx), - xo = Math.cos(angle) * m, - yo = Math.sin(angle) * m; - return setAttributes(createElement('line'), { - stroke: brush.color, - 'stroke-width': lineWidth(brush, current, bounds, bd), - 'stroke-linecap': 'round', - 'marker-end': 'url(#arrowhead-' + brush.key + ')', - opacity: opacity(brush, current), - x1: a[0], - y1: a[1], - x2: b[0] - xo, - y2: b[1] - yo, - }); - } +function renderCircle( + brush: DrawBrush, + pos: cg.Pos, + current: boolean, + bounds: ClientRect, + bd: cg.BoardDimensions, +): SVGElement { + const o = pos2px(pos, bounds, bd), + widths = circleWidth(bounds, bd), + radius = (bounds.width + bounds.height) / (2 * (bd.height + bd.width)); + return setAttributes(createElement('circle'), { + stroke: brush.color, + 'stroke-width': widths[current ? 0 : 1], + fill: 'none', + opacity: opacity(brush, current), + cx: o[0], + cy: o[1], + r: radius - widths[1] / 2, + }); +} +function renderArrow( + brush: DrawBrush, + orig: cg.Pos, + dest: cg.Pos, + current: boolean, + shorten: boolean, + bounds: ClientRect, + bd: cg.BoardDimensions, +): SVGElement { + const m = arrowMargin(bounds, shorten && !current, bd), + a = pos2px(orig, bounds, bd), + b = pos2px(dest, bounds, bd), + dx = b[0] - a[0], + dy = b[1] - a[1], + angle = Math.atan2(dy, dx), + xo = Math.cos(angle) * m, + yo = Math.sin(angle) * m; + return setAttributes(createElement('line'), { + stroke: brush.color, + 'stroke-width': lineWidth(brush, current, bounds, bd), + 'stroke-linecap': 'round', + 'marker-end': 'url(#arrowhead-' + brush.key + ')', + opacity: opacity(brush, current), + x1: a[0], + y1: a[1], + x2: b[0] - xo, + y2: b[1] - yo, + }); +} function renderCustomSvg(customSvg: string, pos: cg.Pos, bounds: ClientRect, bd: cg.BoardDimensions): SVGElement { const { width, height } = bounds; @@ -158,4 +167,4 @@ function renderCustomSvg(customSvg: string, pos: cg.Pos, bounds: ClientRect, bd: g.appendChild(svg); svg.innerHTML = customSvg; return g; -} \ No newline at end of file +} diff --git a/src/variants/abalone/util.ts b/src/variants/abalone/util.ts index d374efa..ae85d5c 100644 --- a/src/variants/abalone/util.ts +++ b/src/variants/abalone/util.ts @@ -1,29 +1,9 @@ import * as cg from '../../types'; import * as T from '../../transformations'; -const abaloneFiles = [ - 'a', - 'b', - 'c', - 'd', - 'e', - 'f', - 'g', - 'h', - 'i', -] as const; +const abaloneFiles = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'] as const; -const abaloneRanks = [ - '1', - '2', - '3', - '4', - '5', - '6', - '7', - '8', - '9', -] as const; +const abaloneRanks = ['1', '2', '3', '4', '5', '6', '7', '8', '9'] as const; export const pos2key = (pos: cg.Pos): cg.Key => { let posx = pos[0]; @@ -55,83 +35,80 @@ export const pos2key = (pos: cg.Pos): cg.Key => { posx = pos[0] + 1; } - const key = (abaloneFiles[posx] + abaloneRanks[pos[1] - 1]) as cg.Key - // console.log("pos2key", pos, key); - return key; -} - - + const key = (abaloneFiles[posx] + abaloneRanks[pos[1] - 1]) as cg.Key; + // console.log("pos2key", pos, key); + return key; +}; export const key2pos = (k: cg.Key): cg.Pos => { // console.log("key2pos", k); return [k.charCodeAt(0) - 96, parseInt(k.slice(1))] as cg.Pos; // @TODO VFR: rework this potentially - was used by mini boards to display them correctly - not sure yet if it's supposed to be used - // const shift = parseInt(k.slice(1)); - // const diff = (shift - 1) * 0.5; - // if (parseInt(k.slice(1)) < 5) { - // return [k.charCodeAt(0) - // - 96 + 2 - (diff), - // parseInt(k.slice(1))] as cg.Pos; - // } - // return [k.charCodeAt(0) - 96 - (diff-5) - 3, parseInt(k.slice(1))] as cg.Pos; -} - -const shift = [2,1.5,1,0.5,0,-0.5,-1,-1.5,-2] + // const shift = parseInt(k.slice(1)); + // const diff = (shift - 1) * 0.5; + // if (parseInt(k.slice(1)) < 5) { + // return [k.charCodeAt(0) + // - 96 + 2 - (diff), + // parseInt(k.slice(1))] as cg.Pos; + // } + // return [k.charCodeAt(0) - 96 - (diff-5) - 3, parseInt(k.slice(1))] as cg.Pos; +}; + +const shift = [2, 1.5, 1, 0.5, 0, -0.5, -1, -1.5, -2]; // @TODO VFR: translateBase should probably be in transformations.ts // @TODO VFR: probably need to adapt the code here still anyway const translateBase: Record = { - p1: (pos: cg.Pos, xScale: number, yScale: number, bt: cg.BoardDimensions) => [ - (pos[0] - 1 + shift[pos[1] - 1]) * xScale, - (bt.height - pos[1]) * yScale, - ], - p2: (pos: cg.Pos, xScale: number, yScale: number, bt: cg.BoardDimensions) => [ - (bt.width - pos[0] - shift[pos[1] - 1]) * xScale, - (pos[1] - 1) * yScale, - ], - right: (pos: cg.Pos, xScale: number, yScale: number, _) => [(pos[1] - 1) * xScale, (pos[0] - 1) * yScale], - left: (pos: cg.Pos, xScale: number, yScale: number, bt: cg.BoardDimensions) => [ - (bt.width - pos[0]) * xScale, - (pos[1] - 1) * yScale, - ], - p1vflip: (pos: cg.Pos, xScale: number, yScale: number, _) => [(pos[0] - 1) * xScale, (pos[1] - 1) * yScale], -} - + p1: (pos: cg.Pos, xScale: number, yScale: number, bt: cg.BoardDimensions) => [ + (pos[0] - 1 + shift[pos[1] - 1]) * xScale, + (bt.height - pos[1]) * yScale, + ], + p2: (pos: cg.Pos, xScale: number, yScale: number, bt: cg.BoardDimensions) => [ + (bt.width - pos[0] - shift[pos[1] - 1]) * xScale, + (pos[1] - 1) * yScale, + ], + right: (pos: cg.Pos, xScale: number, yScale: number, _) => [(pos[1] - 1) * xScale, (pos[0] - 1) * yScale], + left: (pos: cg.Pos, xScale: number, yScale: number, bt: cg.BoardDimensions) => [ + (bt.width - pos[0]) * xScale, + (pos[1] - 1) * yScale, + ], + p1vflip: (pos: cg.Pos, xScale: number, yScale: number, _) => [(pos[0] - 1) * xScale, (pos[1] - 1) * yScale], +}; export const posToTranslateAbs = ( - pos: cg.Pos, - orientation: cg.Orientation, - xFactor: number, - yFactor: number, - bt: cg.BoardDimensions, - ): cg.NumberPair => { - return translateBase[orientation](pos, xFactor, yFactor, bt); - }; + pos: cg.Pos, + orientation: cg.Orientation, + xFactor: number, + yFactor: number, + bt: cg.BoardDimensions, +): cg.NumberPair => { + return translateBase[orientation](pos, xFactor, yFactor, bt); +}; function files(n: number) { - return abaloneFiles.slice(0, n); + return abaloneFiles.slice(0, n); } function ranks(n: number) { - return abaloneRanks.slice(0, n); + return abaloneRanks.slice(0, n); } export function allKeys(bd: cg.BoardDimensions = { width: 9, height: 9 }) { - return Array.prototype.concat(...files(bd.width).map(c => ranks(bd.height).map(r => c + r))); + return Array.prototype.concat(...files(bd.width).map(c => ranks(bd.height).map(r => c + r))); } export const allPos = (bd: cg.BoardDimensions): cg.Pos[] => allKeys(bd).map(key2pos); export function computeSquareCenter( - key: cg.Key, - orientation: cg.Orientation, - bounds: ClientRect, - bd: cg.BoardDimensions, - ): cg.NumberPair { - const pos = T.mapToP1Inverse[orientation](key2pos(key), bd); - return [ - bounds.left + (bounds.width * (pos[0] - 1 + 0.5)) / bd.width, - bounds.top + (bounds.height * (bd.height - (pos[1] - 1 + 0.5))) / bd.height, - ]; - } \ No newline at end of file + key: cg.Key, + orientation: cg.Orientation, + bounds: ClientRect, + bd: cg.BoardDimensions, +): cg.NumberPair { + const pos = T.mapToP1Inverse[orientation](key2pos(key), bd); + return [ + bounds.left + (bounds.width * (pos[0] - 1 + 0.5)) / bd.width, + bounds.top + (bounds.height * (bd.height - (pos[1] - 1 + 0.5))) / bd.height, + ]; +} diff --git a/src/wrap.ts b/src/wrap.ts index f21d013..d219159 100644 --- a/src/wrap.ts +++ b/src/wrap.ts @@ -146,17 +146,10 @@ export function renderWrap(element: HTMLElement, s: HeadlessState, relative: boo ); } } - } else if (s.variant === 'abalone') { // @TODO VFR: probably can use the default else below - check - container.appendChild( - renderCoords( - ['1', '2', '3', '4', '5', '6', '7', '8', '9'], - 'ranks' + orientClass, - ), - ); - container.appendChild( - renderCoords(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'], 'files' + orientClass), - ); - + } else if (s.variant === 'abalone') { + // @TODO VFR: probably can use the default else below - check + container.appendChild(renderCoords(['1', '2', '3', '4', '5', '6', '7', '8', '9'], 'ranks' + orientClass)); + container.appendChild(renderCoords(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'], 'files' + orientClass)); } else { container.appendChild(renderCoords(ranks19.slice(0, s.dimensions.height), 'ranks' + orientClass)); container.appendChild(renderCoords(files.slice(0, s.dimensions.width), 'files' + orientClass)); From 41e60c285c80114f354bf54d0d6ad3765ef4ba8c Mon Sep 17 00:00:00 2001 From: Vincent FROCHOT Date: Tue, 19 Nov 2024 12:07:21 +0100 Subject: [PATCH 06/22] fix: drag pieces remains unshifted --- src/drag.ts | 3 +++ src/draw.ts | 8 ------- src/svg.ts | 8 ------- src/variants/abalone/board.ts | 5 ++--- src/variants/abalone/drag.ts | 39 +++++++++++++++++++++++++++++++++++ src/variants/abalone/util.ts | 28 +++++++++++++++---------- 6 files changed, 61 insertions(+), 30 deletions(-) create mode 100644 src/variants/abalone/drag.ts diff --git a/src/drag.ts b/src/drag.ts index 8369a55..f79b250 100644 --- a/src/drag.ts +++ b/src/drag.ts @@ -7,6 +7,8 @@ import { anim } from './anim'; import predrop from './predrop'; import * as T from './transformations'; +import { processDrag as abaloneProcessDrag } from './variants/abalone/drag'; + export interface DragCurrent { orig: cg.Key; // orig key of dragging piece origPos: cg.Pos; @@ -150,6 +152,7 @@ export function dragNewPiece(s: State, piece: cg.Piece, e: cg.MouchEvent, force? function processDrag(s: State): void { requestAnimationFrame(() => { + if (s.variant === 'abalone') return abaloneProcessDrag(s); const cur = s.draggable.current; if (!cur) return; // cancel animations while dragging diff --git a/src/draw.ts b/src/draw.ts index c53fd45..74586a9 100644 --- a/src/draw.ts +++ b/src/draw.ts @@ -3,8 +3,6 @@ import { unselect, cancelMove, getKeyAtDomPos, getSnappedKeyAtDomPos } from './b import { eventPosition, isRightButton } from './util'; import * as cg from './types'; -import { start as abaloneStart, processDraw as abaloneProcessDraw } from './variants/abalone/draw'; - export interface DrawShape { orig: cg.Key; dest?: cg.Key; @@ -64,9 +62,6 @@ export interface DrawCurrent { const brushes = ['green', 'red', 'blue', 'yellow']; export function start(state: State, e: cg.MouchEvent): void { - if (state.variant === 'abalone') { - return abaloneStart(state, e); - } // support one finger touch only if (e.touches && e.touches.length > 1) return; e.stopPropagation(); @@ -86,9 +81,6 @@ export function start(state: State, e: cg.MouchEvent): void { } export function processDraw(state: State): void { - if (state.variant === 'abalone') { - return abaloneProcessDraw(state); - } requestAnimationFrame(() => { const cur = state.drawable.current; if (cur) { diff --git a/src/svg.ts b/src/svg.ts index d4fa23d..d2e198a 100644 --- a/src/svg.ts +++ b/src/svg.ts @@ -4,8 +4,6 @@ import { Drawable, DrawShape, DrawShapePiece, DrawBrush, DrawBrushes, DrawModifi import * as cg from './types'; import * as T from './transformations'; -import { renderPiece as abaloneRenderPiece, renderShape as abaloneRenderShape } from './variants/abalone/svg'; - export function createElement(tagName: string): SVGElement { return document.createElementNS('http://www.w3.org/2000/svg', tagName); } @@ -187,9 +185,6 @@ function renderShape( arrowDests: ArrowDests, bounds: ClientRect, ): SVGElement { - if (state.variant === 'abalone') { - return abaloneRenderShape(state, { shape, current, hash }, brushes, arrowDests, bounds); - } let el: SVGElement; if (shape.customSvg) { const orig = orient(key2pos(shape.orig), state.orientation, state.dimensions); @@ -302,9 +297,6 @@ function renderPiece( myPlayerIndex: cg.PlayerIndex, variant: cg.Variant, ): SVGElement { - if (variant === 'abalone') { - return abaloneRenderPiece(baseUrl, pos, piece, bounds, bd, myPlayerIndex); - } const o = pos2px(pos, bounds, bd), width = (bounds.width / bd.width) * (piece.scale || 1), height = (bounds.height / bd.height) * (piece.scale || 1), diff --git a/src/variants/abalone/board.ts b/src/variants/abalone/board.ts index 836b51f..ecd4f4c 100644 --- a/src/variants/abalone/board.ts +++ b/src/variants/abalone/board.ts @@ -3,7 +3,7 @@ import * as T from '../../transformations'; import { knight, queen } from '../../premove'; import { distanceSq } from '../../util'; -import { allPos, computeSquareCenter, key2pos, pos2key } from './util'; +import { allPos, computeSquareCenter, key2posAlt, pos2key } from './util'; export const getKeyAtDomPos = ( pos: cg.NumberPair, @@ -13,7 +13,6 @@ export const getKeyAtDomPos = ( ): cg.Key | undefined => { let file = Math.ceil(bd.width * ((pos[0] - bounds.left) / bounds.width)); const rank = Math.ceil(bd.height - bd.height * ((pos[1] - bounds.top) / bounds.height)); - if (rank === undefined || file === undefined) return undefined; pos = [file, rank]; pos = T.mapToP1[orientation](pos, bd); @@ -27,7 +26,7 @@ export const getSnappedKeyAtDomPos = ( bounds: ClientRect, bd: cg.BoardDimensions, ): cg.Key | undefined => { - const origPos = key2pos(orig); + const origPos = key2posAlt(orig); const validSnapPos = allPos(bd).filter(pos2 => { return queen(origPos[0], origPos[1], pos2[0], pos2[1]) || knight(origPos[0], origPos[1], pos2[0], pos2[1]); }); diff --git a/src/variants/abalone/drag.ts b/src/variants/abalone/drag.ts new file mode 100644 index 0000000..7bbd46c --- /dev/null +++ b/src/variants/abalone/drag.ts @@ -0,0 +1,39 @@ +import { cancel } from "../../drag"; +import { State } from "../../state"; +import { distanceSq, posToTranslateAbs, samePiece } from "../../util"; +import { translateAbs } from "./util"; + +export function processDrag(s: State): void { + requestAnimationFrame(() => { + const cur = s.draggable.current; + if (!cur) return; + // cancel animations while dragging + if (s.animation.current?.plan.anims.has(cur.orig)) s.animation.current = undefined; + // if moving piece is gone, cancel + const origPiece = s.pieces.get(cur.orig); + if (!origPiece || !samePiece(origPiece, cur.piece)) cancel(s); + else { + if (!cur.started && distanceSq(cur.epos, cur.rel) >= Math.pow(s.draggable.distance, 2)) cur.started = true; + if (cur.started) { + // support lazy elements + if (typeof cur.element === 'function') { + console.log("LAZY => function"); + const found = cur.element(); + if (!found) return; + found.cgDragging = true; + found.classList.add('dragging'); + cur.element = found; + } + + cur.pos = [cur.epos[0] - cur.rel[0], cur.epos[1] - cur.rel[1]]; + + // move piece + const translation = posToTranslateAbs(s.dom.bounds(), s.dimensions, "chess")(cur.origPos, s.orientation); + translation[0] += cur.pos[0] + cur.dec[0]; + translation[1] += cur.pos[1] + cur.dec[1]; + translateAbs(cur.element, translation); + } + } + processDrag(s); + }); + } diff --git a/src/variants/abalone/util.ts b/src/variants/abalone/util.ts index ae85d5c..cc7f64d 100644 --- a/src/variants/abalone/util.ts +++ b/src/variants/abalone/util.ts @@ -40,19 +40,21 @@ export const pos2key = (pos: cg.Pos): cg.Key => { return key; }; +// @TODO VFR PART1: rework this potentially - was used by mini boards to display them correctly - not sure yet if it's supposed to be used export const key2pos = (k: cg.Key): cg.Pos => { - // console.log("key2pos", k); return [k.charCodeAt(0) - 96, parseInt(k.slice(1))] as cg.Pos; +} - // @TODO VFR: rework this potentially - was used by mini boards to display them correctly - not sure yet if it's supposed to be used - // const shift = parseInt(k.slice(1)); - // const diff = (shift - 1) * 0.5; - // if (parseInt(k.slice(1)) < 5) { - // return [k.charCodeAt(0) - // - 96 + 2 - (diff), - // parseInt(k.slice(1))] as cg.Pos; - // } - // return [k.charCodeAt(0) - 96 - (diff-5) - 3, parseInt(k.slice(1))] as cg.Pos; +export const key2posAlt = (k: cg.Key): cg.Pos => { +// @TODO VFR PART2: rework this potentially - was used by mini boards to display them correctly - not sure yet if it's supposed to be used + const shift = parseInt(k.slice(1)); + const diff = (shift - 1) * 0.5; + if (parseInt(k.slice(1)) < 5) { + return [k.charCodeAt(0) + - 96 + 2 - (diff), + parseInt(k.slice(1))] as cg.Pos; + } + return [k.charCodeAt(0) - 96 - (diff-5) - 3, parseInt(k.slice(1))] as cg.Pos; }; const shift = [2, 1.5, 1, 0.5, 0, -0.5, -1, -1.5, -2]; @@ -86,6 +88,10 @@ export const posToTranslateAbs = ( return translateBase[orientation](pos, xFactor, yFactor, bt); }; +export const translateAbs = (el: HTMLElement, pos: cg.NumberPair): void => { + el.style.transform = `translate(${pos[0]}px,${pos[1]}px)`; +}; + function files(n: number) { return abaloneFiles.slice(0, n); } @@ -108,7 +114,7 @@ export function computeSquareCenter( ): cg.NumberPair { const pos = T.mapToP1Inverse[orientation](key2pos(key), bd); return [ - bounds.left + (bounds.width * (pos[0] - 1 + 0.5)) / bd.width, + bounds.left + (bounds.width * (pos[0] + shift[pos[1] - 1] - 1 + 0.5)) / bd.width, bounds.top + (bounds.height * (bd.height - (pos[1] - 1 + 0.5))) / bd.height, ]; } From ef25ce9f5892c655f63b7ef0d5a4769d2ff6b1fc Mon Sep 17 00:00:00 2001 From: Vincent FROCHOT Date: Wed, 20 Nov 2024 16:02:21 +0100 Subject: [PATCH 07/22] fix: pla-1050 miniboard seem to be using relative pos functions instead of abs functions --- src/board.ts | 2 ++ src/util.ts | 13 ++++++++++--- src/variants/abalone/drag.ts | 1 - src/variants/abalone/render.ts | 7 +------ src/variants/abalone/svg.ts | 2 +- src/variants/abalone/util.ts | 9 ++++++++- 6 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/board.ts b/src/board.ts index 18b5ecd..8dc718d 100644 --- a/src/board.ts +++ b/src/board.ts @@ -628,6 +628,7 @@ export function stop(state: HeadlessState): void { cancelMove(state); } +// triggered when we click on the svg area (a piece, a square or just an area outside the hexagon can be below the cursor) export function getKeyAtDomPos( pos: cg.NumberPair, orientation: cg.Orientation, @@ -735,6 +736,7 @@ export function p1Pov(s: HeadlessState): boolean { return s.myPlayerIndex === 'p1'; } +// at least triggered when we use right click to draw arrows or highlight a square export function getSnappedKeyAtDomPos( orig: cg.Key, pos: cg.NumberPair, diff --git a/src/util.ts b/src/util.ts index 9e2cc58..7ae18df 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,7 +1,9 @@ import * as cg from './types'; import * as T from './transformations'; -import { posToTranslateAbs as abalonePosToTranslateAbs } from './variants/abalone/util'; +import { posToTranslateAbs as abalonePosToTranslateAbs, + posToTranslateRel as abalonePosToTranslateRel + } from './variants/abalone/util'; export const playerIndexs: cg.PlayerIndex[] = ['p1', 'p2']; export const invRanks: readonly cg.Rank[] = [...cg.ranks19].reverse(); @@ -186,8 +188,11 @@ export const posToTranslateRel = ( orientation: cg.Orientation, bt: cg.BoardDimensions, v: cg.Variant, -): cg.NumberPair => - posToTranslateBase( +): cg.NumberPair => { + if(v === 'abalone') { + return abalonePosToTranslateRel(pos, orientation, bt); + } + return posToTranslateBase( pos, orientation, 100, @@ -198,6 +203,8 @@ export const posToTranslateRel = ( : 100, bt, ); +} + export const translateAbs = (el: HTMLElement, pos: cg.NumberPair): void => { el.style.transform = `translate(${pos[0]}px,${pos[1]}px)`; diff --git a/src/variants/abalone/drag.ts b/src/variants/abalone/drag.ts index 7bbd46c..9510581 100644 --- a/src/variants/abalone/drag.ts +++ b/src/variants/abalone/drag.ts @@ -17,7 +17,6 @@ export function processDrag(s: State): void { if (cur.started) { // support lazy elements if (typeof cur.element === 'function') { - console.log("LAZY => function"); const found = cur.element(); if (!found) return; found.cgDragging = true; diff --git a/src/variants/abalone/render.ts b/src/variants/abalone/render.ts index aab79f8..8bd6a18 100644 --- a/src/variants/abalone/render.ts +++ b/src/variants/abalone/render.ts @@ -18,7 +18,7 @@ import { import { key2pos } from './util'; -// @TODO VFR: this code is an ugly copy paste, refactor etc +// @TODO VFR: this code is an ugly copy paste, refactor etc. export const render = (s: State): void => { const orientation = s.orientation, asP1: boolean = p1Pov(s), @@ -184,11 +184,6 @@ export const render = (s: State): void => { boardEl.appendChild(pieceNode); } } - // @TODO VFR: I added this "translate(x)" but it's probably useless. to remove. - translate( - createEl('piece', pieceNameOf(p, s.myPlayerIndex, s.orientation, s.variant, k)) as cg.PieceNode, - posToTranslate(key2pos(k), orientation, s.dimensions, s.variant), - ); } // walk over all pocketPieces and set nodes diff --git a/src/variants/abalone/svg.ts b/src/variants/abalone/svg.ts index 418b67f..2335911 100644 --- a/src/variants/abalone/svg.ts +++ b/src/variants/abalone/svg.ts @@ -16,7 +16,7 @@ import * as cg from '../../types'; import { key2pos } from './util'; -// pos2px is probably used to re-position elements correctly at the center of their square - an override is probably not needed +// pos2px is used to convert a position from the grid (board coordinates) to a position in pixels const pos2px = (pos: cg.Pos, bounds: ClientRect, bd: cg.BoardDimensions): cg.NumberPair => { return [ ((pos[0] * 4 + pos[0] - 0.5) * bounds.width) / bd.width, diff --git a/src/variants/abalone/util.ts b/src/variants/abalone/util.ts index cc7f64d..f4afde9 100644 --- a/src/variants/abalone/util.ts +++ b/src/variants/abalone/util.ts @@ -36,7 +36,6 @@ export const pos2key = (pos: cg.Pos): cg.Key => { } const key = (abaloneFiles[posx] + abaloneRanks[pos[1] - 1]) as cg.Key; - // console.log("pos2key", pos, key); return key; }; @@ -88,6 +87,14 @@ export const posToTranslateAbs = ( return translateBase[orientation](pos, xFactor, yFactor, bt); }; +export const posToTranslateRel = ( + pos: cg.Pos, + orientation: cg.Orientation, + bt: cg.BoardDimensions, +): cg.NumberPair => { + return translateBase[orientation](pos, 100, 100, bt); +} + export const translateAbs = (el: HTMLElement, pos: cg.NumberPair): void => { el.style.transform = `translate(${pos[0]}px,${pos[1]}px)`; }; From 6aeb9c824355663a5eb4c8bcaf78b52c1a912eba Mon Sep 17 00:00:00 2001 From: Vincent FROCHOT Date: Wed, 20 Nov 2024 16:02:55 +0100 Subject: [PATCH 08/22] chore: pnpm format --- src/util.ts | 12 +++---- src/variants/abalone/drag.ts | 68 ++++++++++++++++++------------------ src/variants/abalone/util.ts | 18 ++++------ 3 files changed, 46 insertions(+), 52 deletions(-) diff --git a/src/util.ts b/src/util.ts index 7ae18df..f36be68 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,9 +1,10 @@ import * as cg from './types'; import * as T from './transformations'; -import { posToTranslateAbs as abalonePosToTranslateAbs, - posToTranslateRel as abalonePosToTranslateRel - } from './variants/abalone/util'; +import { + posToTranslateAbs as abalonePosToTranslateAbs, + posToTranslateRel as abalonePosToTranslateRel, +} from './variants/abalone/util'; export const playerIndexs: cg.PlayerIndex[] = ['p1', 'p2']; export const invRanks: readonly cg.Rank[] = [...cg.ranks19].reverse(); @@ -189,7 +190,7 @@ export const posToTranslateRel = ( bt: cg.BoardDimensions, v: cg.Variant, ): cg.NumberPair => { - if(v === 'abalone') { + if (v === 'abalone') { return abalonePosToTranslateRel(pos, orientation, bt); } return posToTranslateBase( @@ -203,8 +204,7 @@ export const posToTranslateRel = ( : 100, bt, ); -} - +}; export const translateAbs = (el: HTMLElement, pos: cg.NumberPair): void => { el.style.transform = `translate(${pos[0]}px,${pos[1]}px)`; diff --git a/src/variants/abalone/drag.ts b/src/variants/abalone/drag.ts index 9510581..dc33d36 100644 --- a/src/variants/abalone/drag.ts +++ b/src/variants/abalone/drag.ts @@ -1,38 +1,38 @@ -import { cancel } from "../../drag"; -import { State } from "../../state"; -import { distanceSq, posToTranslateAbs, samePiece } from "../../util"; -import { translateAbs } from "./util"; +import { cancel } from '../../drag'; +import { State } from '../../state'; +import { distanceSq, posToTranslateAbs, samePiece } from '../../util'; +import { translateAbs } from './util'; export function processDrag(s: State): void { - requestAnimationFrame(() => { - const cur = s.draggable.current; - if (!cur) return; - // cancel animations while dragging - if (s.animation.current?.plan.anims.has(cur.orig)) s.animation.current = undefined; - // if moving piece is gone, cancel - const origPiece = s.pieces.get(cur.orig); - if (!origPiece || !samePiece(origPiece, cur.piece)) cancel(s); - else { - if (!cur.started && distanceSq(cur.epos, cur.rel) >= Math.pow(s.draggable.distance, 2)) cur.started = true; - if (cur.started) { - // support lazy elements - if (typeof cur.element === 'function') { - const found = cur.element(); - if (!found) return; - found.cgDragging = true; - found.classList.add('dragging'); - cur.element = found; - } - - cur.pos = [cur.epos[0] - cur.rel[0], cur.epos[1] - cur.rel[1]]; - - // move piece - const translation = posToTranslateAbs(s.dom.bounds(), s.dimensions, "chess")(cur.origPos, s.orientation); - translation[0] += cur.pos[0] + cur.dec[0]; - translation[1] += cur.pos[1] + cur.dec[1]; - translateAbs(cur.element, translation); + requestAnimationFrame(() => { + const cur = s.draggable.current; + if (!cur) return; + // cancel animations while dragging + if (s.animation.current?.plan.anims.has(cur.orig)) s.animation.current = undefined; + // if moving piece is gone, cancel + const origPiece = s.pieces.get(cur.orig); + if (!origPiece || !samePiece(origPiece, cur.piece)) cancel(s); + else { + if (!cur.started && distanceSq(cur.epos, cur.rel) >= Math.pow(s.draggable.distance, 2)) cur.started = true; + if (cur.started) { + // support lazy elements + if (typeof cur.element === 'function') { + const found = cur.element(); + if (!found) return; + found.cgDragging = true; + found.classList.add('dragging'); + cur.element = found; } + + cur.pos = [cur.epos[0] - cur.rel[0], cur.epos[1] - cur.rel[1]]; + + // move piece + const translation = posToTranslateAbs(s.dom.bounds(), s.dimensions, 'chess')(cur.origPos, s.orientation); + translation[0] += cur.pos[0] + cur.dec[0]; + translation[1] += cur.pos[1] + cur.dec[1]; + translateAbs(cur.element, translation); } - processDrag(s); - }); - } + } + processDrag(s); + }); +} diff --git a/src/variants/abalone/util.ts b/src/variants/abalone/util.ts index f4afde9..ed3f2b9 100644 --- a/src/variants/abalone/util.ts +++ b/src/variants/abalone/util.ts @@ -42,18 +42,16 @@ export const pos2key = (pos: cg.Pos): cg.Key => { // @TODO VFR PART1: rework this potentially - was used by mini boards to display them correctly - not sure yet if it's supposed to be used export const key2pos = (k: cg.Key): cg.Pos => { return [k.charCodeAt(0) - 96, parseInt(k.slice(1))] as cg.Pos; -} +}; export const key2posAlt = (k: cg.Key): cg.Pos => { -// @TODO VFR PART2: rework this potentially - was used by mini boards to display them correctly - not sure yet if it's supposed to be used + // @TODO VFR PART2: rework this potentially - was used by mini boards to display them correctly - not sure yet if it's supposed to be used const shift = parseInt(k.slice(1)); const diff = (shift - 1) * 0.5; if (parseInt(k.slice(1)) < 5) { - return [k.charCodeAt(0) - - 96 + 2 - (diff), - parseInt(k.slice(1))] as cg.Pos; + return [k.charCodeAt(0) - 96 + 2 - diff, parseInt(k.slice(1))] as cg.Pos; } - return [k.charCodeAt(0) - 96 - (diff-5) - 3, parseInt(k.slice(1))] as cg.Pos; + return [k.charCodeAt(0) - 96 - (diff - 5) - 3, parseInt(k.slice(1))] as cg.Pos; }; const shift = [2, 1.5, 1, 0.5, 0, -0.5, -1, -1.5, -2]; @@ -87,13 +85,9 @@ export const posToTranslateAbs = ( return translateBase[orientation](pos, xFactor, yFactor, bt); }; -export const posToTranslateRel = ( - pos: cg.Pos, - orientation: cg.Orientation, - bt: cg.BoardDimensions, -): cg.NumberPair => { +export const posToTranslateRel = (pos: cg.Pos, orientation: cg.Orientation, bt: cg.BoardDimensions): cg.NumberPair => { return translateBase[orientation](pos, 100, 100, bt); -} +}; export const translateAbs = (el: HTMLElement, pos: cg.NumberPair): void => { el.style.transform = `translate(${pos[0]}px,${pos[1]}px)`; From a2f23a5d518c84057a16bdaf0985b9f3d4bf9c90 Mon Sep 17 00:00:00 2001 From: Vincent FROCHOT Date: Thu, 21 Nov 2024 16:15:02 +0100 Subject: [PATCH 09/22] chore: remove some comments --- src/util.ts | 2 -- src/variants/abalone/util.ts | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/util.ts b/src/util.ts index f36be68..2a47f06 100644 --- a/src/util.ts +++ b/src/util.ts @@ -167,7 +167,6 @@ const posToTranslateBase = ( return T.translateBase[orientation](pos, xFactor, yFactor, bt); }; -// @TODO VFR investigations : check if needed for Abalone export const posToTranslateAbs = ( bounds: ClientRect, bt: cg.BoardDimensions, @@ -183,7 +182,6 @@ export const posToTranslateAbs = ( return (pos, orientation) => posToTranslateBase(pos, orientation, xFactor, yFactor, bt); }; -// @TODO VFR investigations : check if needed for Abalone export const posToTranslateRel = ( pos: cg.Pos, orientation: cg.Orientation, diff --git a/src/variants/abalone/util.ts b/src/variants/abalone/util.ts index ed3f2b9..38090c5 100644 --- a/src/variants/abalone/util.ts +++ b/src/variants/abalone/util.ts @@ -39,13 +39,11 @@ export const pos2key = (pos: cg.Pos): cg.Key => { return key; }; -// @TODO VFR PART1: rework this potentially - was used by mini boards to display them correctly - not sure yet if it's supposed to be used export const key2pos = (k: cg.Key): cg.Pos => { return [k.charCodeAt(0) - 96, parseInt(k.slice(1))] as cg.Pos; }; export const key2posAlt = (k: cg.Key): cg.Pos => { - // @TODO VFR PART2: rework this potentially - was used by mini boards to display them correctly - not sure yet if it's supposed to be used const shift = parseInt(k.slice(1)); const diff = (shift - 1) * 0.5; if (parseInt(k.slice(1)) < 5) { From e25da1cb715625cc71defdbecb3749804442d69b Mon Sep 17 00:00:00 2001 From: Vincent FROCHOT Date: Thu, 28 Nov 2024 12:16:36 +0100 Subject: [PATCH 10/22] feat: pla-1048 compute moves --- src/board.ts | 18 +++- src/util.ts | 3 - src/variants/abalone/board.ts | 1 - src/variants/abalone/directions.ts | 165 +++++++++++++++++++++++++++++ src/variants/abalone/util.ts | 79 ++++++++++++++ 5 files changed, 258 insertions(+), 8 deletions(-) create mode 100644 src/variants/abalone/directions.ts diff --git a/src/board.ts b/src/board.ts index 8dc718d..b5fca70 100644 --- a/src/board.ts +++ b/src/board.ts @@ -23,8 +23,8 @@ import { premove, queen, knight } from './premove'; import predrop from './predrop'; import * as cg from './types'; import * as T from './transformations'; - import { getKeyAtDomPos as abaloneGetKeyAtDomPos } from './variants/abalone/board'; +import { abaloneUpdatePiecesFromMove } from './variants/abalone/util'; export function setOrientation(state: HeadlessState, o: cg.Orientation): void { state.orientation = o; @@ -183,12 +183,17 @@ function updatePocketPieces( state.pocketPieces = newPocketPieces; } -// @TODO VFR: add Abalone moves +// returns false in case the move could not be processed 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 && state.variant !== 'togyzkumalak' && state.variant !== 'bestemshe') || !origPiece) return false; - const captured = isCapture(state.variant, destPiece, origPiece); + let abalonePieces: cg.PiecesDiff = state.pieces, // because captures are computed in abaloneUpdatePiecesFromMove, it is better to store the updated pieces and the capture before the switch responsible of setPieces + abaloneCapture: boolean = false; + if (state.variant === 'abalone') { + [abalonePieces, abaloneCapture] = abaloneUpdatePiecesFromMove(state.pieces, orig, dest); + } + const captured = isCapture(state.variant, destPiece, origPiece) || abaloneCapture; if (dest === state.selected) unselect(state); callUserFunction(state.events.move, orig, dest, captured); @@ -211,6 +216,9 @@ export function baseMove(state: HeadlessState, orig: cg.Key, dest: cg.Key): cg.P } setPieces(state, backgammonUpdatePiecesFromMove(state.pieces, orig, dest)); break; + case 'abalone': + setPieces(state, abalonePieces); + break; default: if (!tryAutoCastle(state, orig, dest)) { state.pieces.set(dest, origPiece); @@ -237,6 +245,8 @@ function isCapture(variant: cg.Variant, destPiece: cg.Piece | undefined, origPie case 'oware': //TODO this is more complicated to calculate... (but its only used for sound in lila atm) return destPiece && destPiece.playerIndex !== origPiece.playerIndex ? destPiece : undefined; + case 'abalone': + return undefined; // we compute it from abaloneUpdatePiecesFromMove instead default: return destPiece && destPiece.playerIndex !== origPiece.playerIndex ? destPiece : undefined; } @@ -628,7 +638,7 @@ export function stop(state: HeadlessState): void { cancelMove(state); } -// triggered when we click on the svg area (a piece, a square or just an area outside the hexagon can be below the cursor) +// triggered when we click on the svg area (a piece, a square or even an area outside the board drawn can be below the cursor) export function getKeyAtDomPos( pos: cg.NumberPair, orientation: cg.Orientation, diff --git a/src/util.ts b/src/util.ts index 2a47f06..b7302a6 100644 --- a/src/util.ts +++ b/src/util.ts @@ -593,9 +593,6 @@ export function togyzkumalakUpdatePiecesFromMove( return updatedPieces; } -// @TODO VFR: update pieces for Abalone (could allow moving in local from analysis page ??) -// create here a function "updateAbalonePieceMap(pieces: cg.Pieces, bd: cg.BoardDimensions??): cg.PiecesDiff" or similar - function createMancalaBoardArrayFromPieces(pieces: cg.Pieces, bd: cg.BoardDimensions): number[] { function countAtKey(key: cg.Key): number { const piece = pieces.get(key); diff --git a/src/variants/abalone/board.ts b/src/variants/abalone/board.ts index ecd4f4c..b5cbd0c 100644 --- a/src/variants/abalone/board.ts +++ b/src/variants/abalone/board.ts @@ -2,7 +2,6 @@ import * as cg from '../../types'; import * as T from '../../transformations'; import { knight, queen } from '../../premove'; import { distanceSq } from '../../util'; - import { allPos, computeSquareCenter, key2posAlt, pos2key } from './util'; export const getKeyAtDomPos = ( diff --git a/src/variants/abalone/directions.ts b/src/variants/abalone/directions.ts new file mode 100644 index 0000000..50a6bc3 --- /dev/null +++ b/src/variants/abalone/directions.ts @@ -0,0 +1,165 @@ +import * as cg from '../../types'; + +export enum DiagonalDirectionString { + UpLeft = 'UpLeft', + UpRight = 'UpRight', + DownRight = 'DownRight', + DownLeft = 'DownLeft', +} +enum HorizontalDirectionString { + Left = 'Left', + Right = 'Right', +} +type DirectionString = DiagonalDirectionString | HorizontalDirectionString; + +export const move = (key: cg.Key, direction: DirectionString): cg.Key | undefined => { + const transformedKey = directionMappings[direction](key); + if (isValidKey(transformedKey)) return transformedKey; + return undefined; +}; + +export const getDirectionString = (orig: cg.Key, dest: cg.Key): DirectionString | undefined => { + const fileDiff = orig[0].charCodeAt(0) - dest[0].charCodeAt(0); + const rankDiff = parseInt(orig[1], 10) - parseInt(dest[1], 10); + + if (fileDiff !== 0 && rankDiff === 0) { + return fileDiff > 0 ? HorizontalDirectionString.Left : HorizontalDirectionString.Right; + } else if (fileDiff === 0 && rankDiff !== 0) { + return rankDiff > 0 ? DiagonalDirectionString.DownRight : DiagonalDirectionString.UpLeft; + } else if (fileDiff !== 0 && rankDiff !== 0) { + if (fileDiff > 0 && rankDiff > 0) { + return DiagonalDirectionString.DownLeft; + } else if (fileDiff < 0 && rankDiff < 0) { + return DiagonalDirectionString.UpRight; + } else if (fileDiff < 0 && rankDiff > 0) { + return DiagonalDirectionString.DownRight; + } else { + return DiagonalDirectionString.UpLeft; + } + } + return undefined; +}; + +export const isMoveInLine = (orig: cg.Key, dest: cg.Key, directionString: DirectionString): boolean => { + const pathToEdge = traverseUntil( + orig, + (square: cg.Key) => move(square, directionString) == undefined, + directionString, + ); + return pathToEdge.includes(dest); +}; + +export const candidateLineDirs = (origToDestDirection: DiagonalDirectionString): DirectionString[] => { + const directionMap: { [key in DiagonalDirectionString]: DirectionString[] } = { + [DiagonalDirectionString.UpLeft]: [HorizontalDirectionString.Left, DiagonalDirectionString.UpLeft], + [DiagonalDirectionString.UpRight]: [ + DiagonalDirectionString.UpLeft, + DiagonalDirectionString.UpRight, + HorizontalDirectionString.Right, + ], + [DiagonalDirectionString.DownRight]: [HorizontalDirectionString.Right, DiagonalDirectionString.DownRight], + [DiagonalDirectionString.DownLeft]: [ + DiagonalDirectionString.DownRight, + DiagonalDirectionString.DownLeft, + HorizontalDirectionString.Left, + ], + }; + return directionMap[origToDestDirection]; +}; + +export const deducePotentialSideDirs = ( + origToDestDirection: DiagonalDirectionString, + lineDirection: DirectionString, +): DirectionString[] => { + switch (origToDestDirection) { + case DiagonalDirectionString.DownLeft: + switch (lineDirection) { + case DiagonalDirectionString.DownLeft: + return [HorizontalDirectionString.Left, DiagonalDirectionString.DownRight]; + case DiagonalDirectionString.DownRight: + return [DiagonalDirectionString.DownLeft]; + case HorizontalDirectionString.Left: + return [DiagonalDirectionString.DownLeft]; + default: + return []; + } + case DiagonalDirectionString.UpRight: + switch (lineDirection) { + case DiagonalDirectionString.UpLeft: + return [DiagonalDirectionString.UpRight]; + case DiagonalDirectionString.UpRight: + return [HorizontalDirectionString.Right, DiagonalDirectionString.UpLeft]; + case HorizontalDirectionString.Right: + return [DiagonalDirectionString.UpRight]; + default: + return []; + } + case DiagonalDirectionString.UpLeft: + switch (lineDirection) { + case DiagonalDirectionString.UpLeft: + return [HorizontalDirectionString.Left]; + case HorizontalDirectionString.Left: + return [DiagonalDirectionString.UpLeft]; + default: + return []; + } + case DiagonalDirectionString.DownRight: + switch (lineDirection) { + case DiagonalDirectionString.DownRight: + return [HorizontalDirectionString.Right]; + case HorizontalDirectionString.Right: + return [DiagonalDirectionString.DownRight]; + default: + return []; + } + default: + return []; + } +}; + +const traverseUntil = (pos: cg.Key, stop: (pos: cg.Key) => boolean, direction: DirectionString): cg.Key[] => { + const nextPos = move(pos, direction); + if (nextPos) { + const rest = stop(nextPos) ? [] : traverseUntil(nextPos, stop, direction); + return [nextPos, ...rest]; + } else { + return []; + } +}; + +const isValidKey = (key: cg.Key): boolean => { + if (key.length !== 2) return false; // e.g. 'e10' + + const num = Number(key[1]); + const charCode = key[0].charCodeAt(0); + + if (num < 1 || num > 9) return false; + if (charCode < 97 || charCode > 105) return false; // a-i + + const specificChecks: { [char: string]: [number, number] } = { + a: [1, 5], + b: [1, 6], + c: [1, 7], + d: [1, 8], + f: [2, 9], + g: [3, 9], + h: [4, 9], + i: [5, 9], + }; + + const range = specificChecks[key[0]]; + if (range && (num < range[0] || num > range[1])) return false; + + return true; +}; + +const directionMappings: { [key in DirectionString]: (key: cg.Key) => cg.Key } = { + UpLeft: (key: cg.Key) => (String.fromCharCode(key[0].charCodeAt(0)) + (parseInt(key[1]) + 1).toString()) as cg.Key, + UpRight: (key: cg.Key) => + (String.fromCharCode(key[0].charCodeAt(0) + 1) + (parseInt(key[1]) + 1).toString()) as cg.Key, + DownLeft: (key: cg.Key) => + (String.fromCharCode(key[0].charCodeAt(0) - 1) + (parseInt(key[1]) - 1).toString()) as cg.Key, + DownRight: (key: cg.Key) => (String.fromCharCode(key[0].charCodeAt(0)) + (parseInt(key[1]) - 1).toString()) as cg.Key, + Left: (key: cg.Key) => (String.fromCharCode(key[0].charCodeAt(0) - 1) + key[1]) as cg.Key, + Right: (key: cg.Key) => (String.fromCharCode(key[0].charCodeAt(0) + 1) + key[1]) as cg.Key, +}; diff --git a/src/variants/abalone/util.ts b/src/variants/abalone/util.ts index 38090c5..0db325b 100644 --- a/src/variants/abalone/util.ts +++ b/src/variants/abalone/util.ts @@ -1,5 +1,7 @@ import * as cg from '../../types'; import * as T from '../../transformations'; +import { candidateLineDirs, deducePotentialSideDirs, move, getDirectionString, isMoveInLine } from './directions'; +import type { DiagonalDirectionString } from './directions'; const abaloneFiles = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'] as const; @@ -117,3 +119,80 @@ export function computeSquareCenter( bounds.top + (bounds.height * (bd.height - (pos[1] - 1 + 0.5))) / bd.height, ]; } + +export const abaloneUpdatePiecesFromMove = ( + pieces: cg.Pieces, + orig: cg.Key, + dest: cg.Key, +): [cg.PiecesDiff, boolean] => { + const directionString = getDirectionString(orig, dest); + if (!directionString) return [pieces, false]; + const isAMoveInLine = isMoveInLine(orig, dest, directionString); + const diff: cg.PiecesDiff = new Map(pieces); + + if (isAMoveInLine) { + diff.set(dest, pieces.get(orig)); + diff.set(orig, undefined); + if (!pieces.get(dest)) { + // line move + return [diff, false]; + } + // push move + const landingSquare1 = move(dest, directionString); + if (landingSquare1 === undefined) return [diff, true]; // xxo\ xxxo\ + if (!pieces.get(landingSquare1)) { + // xxo. xxxo. + diff.set(landingSquare1, pieces.get(dest)); + return [diff, false]; + } + + const landingSquare2 = move(landingSquare1, directionString); + if (landingSquare2 === undefined) return [diff, true]; // xxxoo\ + if (!pieces.get(landingSquare2)) { + // xxxoo. + diff.set(landingSquare2, pieces.get(dest)); + return [diff, false]; + } + } + + // side move + const overAllDirection = directionString as DiagonalDirectionString; + for (const lineDir of candidateLineDirs(overAllDirection as DiagonalDirectionString)) { + const sideDirs = deducePotentialSideDirs(directionString as DiagonalDirectionString, lineDir); + const secondPos = move(orig, lineDir); + if (secondPos === undefined) continue; + for (const sideDir of sideDirs) { + const side2ndPos = move(secondPos, sideDir); + if (side2ndPos) { + const side1stPos = move(orig, sideDir); + if (side1stPos === undefined) continue; + if (side1stPos && pieces.get(secondPos)) { + if (side2ndPos === dest) { + diff.set(side1stPos, pieces.get(orig)); + diff.set(orig, undefined); + diff.set(dest, pieces.get(secondPos)); + diff.set(secondPos, undefined); + return [diff, false]; + } else { + // 3 marbles are moving + const thirdPos = move(secondPos, lineDir); + if (thirdPos === undefined) continue; + const side3rdPos = move(thirdPos, sideDir); + if (side3rdPos === undefined) continue; + if (pieces.get(thirdPos) && side3rdPos === dest) { + diff.set(side1stPos, pieces.get(orig)); + diff.set(orig, undefined); + diff.set(side2ndPos, pieces.get(secondPos)); + diff.set(secondPos, undefined); + diff.set(side3rdPos, pieces.get(thirdPos)); + diff.set(thirdPos, undefined); + return [diff, false]; + } + } + } + } + } + } + + return [diff, false]; +}; From dddd427f98b5eed0d53bcc9b1e739f9d1da919c0 Mon Sep 17 00:00:00 2001 From: Vincent FROCHOT Date: Wed, 18 Dec 2024 11:59:53 +0100 Subject: [PATCH 11/22] Abalone --- .gitignore | 1 + README.md | 41 +++- build.js | 21 ++ package.json | 3 + src/api.ts | 4 +- src/board.ts | 26 ++- src/chessground.ts | 4 +- src/config.ts | 9 +- src/drag.ts | 2 +- src/fen.ts | 1 - src/render.ts | 7 - src/state.ts | 6 + src/util.ts | 24 +-- src/variants/abalone/README.md | 1 - src/variants/abalone/board.ts | 153 ++++++++++++++- src/variants/abalone/config.ts | 14 ++ src/variants/abalone/directions.ts | 74 ++++--- src/variants/abalone/drag.ts | 4 +- src/variants/abalone/draw.ts | 9 +- src/variants/abalone/engine.ts | 208 ++++++++++++++++++++ src/variants/abalone/fen.ts | 2 +- src/variants/abalone/premove.ts | 5 +- src/variants/abalone/render.ts | 172 ++++++++++++----- src/variants/abalone/svg.ts | 4 +- src/variants/abalone/types.ts | 24 +++ src/variants/abalone/util.ts | 297 ++++++++++++++++++----------- src/wrap.ts | 14 +- 27 files changed, 853 insertions(+), 277 deletions(-) create mode 100644 build.js delete mode 100644 src/variants/abalone/README.md create mode 100644 src/variants/abalone/config.ts create mode 100644 src/variants/abalone/engine.ts create mode 100644 src/variants/abalone/types.ts diff --git a/.gitignore b/.gitignore index b947077..6559694 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ dist/ +.env.local \ No newline at end of file diff --git a/README.md b/README.md index e93cc0c..5e5a7b4 100644 --- a/README.md +++ b/README.md @@ -91,40 +91,69 @@ More? Please make a pull request to include it here. Commands are listed in package.json. In case you want to see the possibilities from the console, run `pnpm run` -Install build dependencies: +### Install build dependencies: ```sh pnpm install ``` -Update deps: +### Update deps: ```sh rm -rf node_modules pnpm-lock.yaml && pnpm store prune && pnpm install ``` -Build the node module: +### Build the node module: ```sh pnpm prepare --watch ``` -Build the minified bundled dist files: (NOTE: from lila you will likely then need to restart the build as it does not watch for changes on the minified file): +### Build the minified bundled dist files: (NOTE: from lila you will likely then need to restart the build as it does not watch for changes on the minified file): ```sh pnpm dist ``` -run tests: +### Run tests: ```sh pnpm run test --watch pnpm run test fen --watch ``` -Before committing: +### Before committing: ```sh pnpm run lint pnpm run format ``` + +### Watch chessground changes from another project: + +In other project: + +- declare the link towards chessground (from project's `package.json`) + +```json + "dependencies": { + ... + "chessground": "link:/path/to/chessground", + ... + } +``` + +In chessground: + +- create `.env.local` file based on .env.local.default +- link back: + +```sh + pnpm run link +``` + +- trigger compilation and generate minified file: + +```sh + pnpm run local-dist +``` diff --git a/build.js b/build.js new file mode 100644 index 0000000..b05ef24 --- /dev/null +++ b/build.js @@ -0,0 +1,21 @@ +import * as cps from 'node:child_process'; +import * as ps from 'node:process'; + +const consumerRoot = process.env.CONSUMER_ROOT; +const args = ps.argv.slice(2); + +if (args.includes('--link')) { + cps.execSync('pnpm link --dir ' + consumerRoot); + console.log('\x1b[36m%s\x1b[0m', 'Linked to ' + consumerRoot); +} +if (args.includes('--bundle')) { + cps.execSync( + 'esbuild src/chessground.ts --bundle --format=esm --outfile=' + + consumerRoot + + '/node_modules/chessground/dist/chessground.min.js', + ); + console.log( + '\x1b[32m%s\x1b[0m', + 'Compiled and built ' + consumerRoot + '/node_modules/chessground/dist/chessground.min.js', + ); +} diff --git a/package.json b/package.json index 5348e55..05bcbf5 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,9 @@ "check-format": "prettier --check .", "bundle": "esbuild src/chessground.ts --bundle --format=esm --outfile=dist/chessground.min.js --minify", "dist": "$npm_execpath run compile && $npm_execpath run bundle", + "link": "node --env-file=.env.local ./build.js --link", + "local-bundle": "node --env-file=.env.local ./build.js --bundle", + "local-dist": "$npm_execpath run compile && $npm_execpath run local-bundle", "postinstall": "$npm_execpath run bundle" }, "files": [ diff --git a/src/api.ts b/src/api.ts index dc4113a..d196fff 100644 --- a/src/api.ts +++ b/src/api.ts @@ -144,11 +144,11 @@ export function start(state: State, redrawAll: cg.Redraw): Api { }, move(orig, dest): void { - anim(state => board.baseMove(state, orig, dest), state); + anim(state => state.baseMove(state, orig, dest), state); }, moveNoAnim(orig, dest): void { - board.baseMove(state, orig, dest); + state.baseMove(state, orig, dest); state.dom.redraw(); }, diff --git a/src/board.ts b/src/board.ts index b5fca70..7ddba89 100644 --- a/src/board.ts +++ b/src/board.ts @@ -23,8 +23,8 @@ import { premove, queen, knight } from './premove'; import predrop from './predrop'; import * as cg from './types'; import * as T from './transformations'; + import { getKeyAtDomPos as abaloneGetKeyAtDomPos } from './variants/abalone/board'; -import { abaloneUpdatePiecesFromMove } from './variants/abalone/util'; export function setOrientation(state: HeadlessState, o: cg.Orientation): void { state.orientation = o; @@ -183,17 +183,15 @@ function updatePocketPieces( state.pocketPieces = newPocketPieces; } -// returns false in case the move could not be processed +/** + * called when a piece is moved from orig to dest + * @returns: false if the move is invalid, true if the move is valid but no capture happened, or the captured piece if a capture happened + */ 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 && state.variant !== 'togyzkumalak' && state.variant !== 'bestemshe') || !origPiece) return false; - let abalonePieces: cg.PiecesDiff = state.pieces, // because captures are computed in abaloneUpdatePiecesFromMove, it is better to store the updated pieces and the capture before the switch responsible of setPieces - abaloneCapture: boolean = false; - if (state.variant === 'abalone') { - [abalonePieces, abaloneCapture] = abaloneUpdatePiecesFromMove(state.pieces, orig, dest); - } - const captured = isCapture(state.variant, destPiece, origPiece) || abaloneCapture; + const captured = isCapture(state.variant, destPiece, origPiece); if (dest === state.selected) unselect(state); callUserFunction(state.events.move, orig, dest, captured); @@ -216,9 +214,6 @@ export function baseMove(state: HeadlessState, orig: cg.Key, dest: cg.Key): cg.P } setPieces(state, backgammonUpdatePiecesFromMove(state.pieces, orig, dest)); break; - case 'abalone': - setPieces(state, abalonePieces); - break; default: if (!tryAutoCastle(state, orig, dest)) { state.pieces.set(dest, origPiece); @@ -246,7 +241,7 @@ function isCapture(variant: cg.Variant, destPiece: cg.Piece | undefined, origPie //TODO this is more complicated to calculate... (but its only used for sound in lila atm) return destPiece && destPiece.playerIndex !== origPiece.playerIndex ? destPiece : undefined; case 'abalone': - return undefined; // we compute it from abaloneUpdatePiecesFromMove instead + return undefined; // we compute it from Abalone namespace using HOF default: return destPiece && destPiece.playerIndex !== origPiece.playerIndex ? destPiece : undefined; } @@ -276,7 +271,7 @@ export function baseNewPiece(state: HeadlessState, piece: cg.Piece, key: cg.Key, } function baseUserMove(state: HeadlessState, orig: cg.Key, dest: cg.Key): cg.Piece | boolean { - const result = baseMove(state, orig, dest); + const result = state.baseMove(state, orig, dest); if (result) { state.movable.dests = undefined; state.dropmode.dropDests = undefined; @@ -638,7 +633,8 @@ export function stop(state: HeadlessState): void { cancelMove(state); } -// triggered when we click on the svg area (a piece, a square or even an area outside the board drawn can be below the cursor) +// triggered when we click on the svg area (a piece, a square or even an area outside the board drawn can be below the cursor). +// @return the key of the square that was clicked, or undefined if the click was outside the board. export function getKeyAtDomPos( pos: cg.NumberPair, orientation: cg.Orientation, @@ -647,7 +643,7 @@ export function getKeyAtDomPos( variant: cg.Variant = 'chess', ): cg.Key | undefined { if (variant === 'abalone') { - return abaloneGetKeyAtDomPos(pos, orientation, bounds, bd); + return abaloneGetKeyAtDomPos(pos, orientation, bounds); } const bgBorder = 1 / 15; const file = diff --git a/src/chessground.ts b/src/chessground.ts index 84c6170..d328b9f 100644 --- a/src/chessground.ts +++ b/src/chessground.ts @@ -3,7 +3,7 @@ import { Config, configure } from './config'; import { HeadlessState, State, defaults } from './state'; import { renderWrap } from './wrap'; import * as events from './events'; -import { render, updateBounds } from './render'; +import { updateBounds } from './render'; import * as svg from './svg'; import * as util from './util'; @@ -18,7 +18,7 @@ export function Chessground(element: HTMLElement, config?: Config): Api { elements = renderWrap(element, maybeState, relative), bounds = util.memo(() => elements.board.getBoundingClientRect()), redrawNow = (skipSvg?: boolean): void => { - render(state); + maybeState.render(state); if (!skipSvg && elements.svg) svg.renderSvg(state, elements.svg, elements.customSvg!); }, boundsUpdated = (): void => { diff --git a/src/config.ts b/src/config.ts index f60d0af..64be347 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,8 +1,10 @@ +import * as cg from './types'; import { HeadlessState } from './state'; import { setSelected, setGoScore } from './board'; import { read as fenRead, readPocket as fenReadPocket } from './fen'; import { DrawShape, DrawBrush } from './draw'; -import * as cg from './types'; + +import { configure as abaloneConfigure } from './variants/abalone/config'; export interface Config { fen?: cg.FEN; // chess position in Forsyth notation @@ -181,6 +183,11 @@ export function configure(state: HeadlessState, config: Config): void { ), ); } + + // configure variants + if (state.variant === 'abalone') { + abaloneConfigure(state); + } } function setCheck(state: HeadlessState, playerIndex: cg.PlayerIndex | boolean): void { diff --git a/src/drag.ts b/src/drag.ts index f79b250..28c6374 100644 --- a/src/drag.ts +++ b/src/drag.ts @@ -152,7 +152,7 @@ export function dragNewPiece(s: State, piece: cg.Piece, e: cg.MouchEvent, force? function processDrag(s: State): void { requestAnimationFrame(() => { - if (s.variant === 'abalone') return abaloneProcessDrag(s); + if (s.variant === 'abalone') return abaloneProcessDrag(s); // "working" WIP: have to use HOF const cur = s.draggable.current; if (!cur) return; // cancel animations while dragging diff --git a/src/fen.ts b/src/fen.ts index f1b0ff0..9d01988 100644 --- a/src/fen.ts +++ b/src/fen.ts @@ -26,7 +26,6 @@ export function read(fen: cg.FEN, dimensions: cg.BoardDimensions, variant: cg.Va let promoted = false; let num = 0; - // @TODO: try to refactor using Higher Order Functions with a default square board if (!commaFenVariants.includes(variant)) { let skipNext = false; for (let i = 0; i < fen.length; i++) { diff --git a/src/render.ts b/src/render.ts index a9a4825..fdb39b1 100644 --- a/src/render.ts +++ b/src/render.ts @@ -14,19 +14,12 @@ import { DragCurrent } from './drag'; import * as cg from './types'; import * as T from './transformations'; -import { render as renderAbalone } from './variants/abalone/render'; - export type PieceName = string; // `$playerIndex $role` export type SquareClasses = Map; // ported from https://github.com/veloce/lichobile/blob/master/src/js/chessground/view.js // in case of bugs, blame @veloce export function render(s: State): void { - if (s.variant === 'abalone') { - renderAbalone(s); - return; - } - const orientation = s.orientation, asP1: boolean = p1Pov(s), posToTranslate = s.dom.relative ? posToTranslateRel : posToTranslateAbs(s.dom.bounds(), s.dimensions, s.variant), diff --git a/src/state.ts b/src/state.ts index fd116c9..5caf827 100644 --- a/src/state.ts +++ b/src/state.ts @@ -1,7 +1,9 @@ import * as fen from './fen'; import { AnimCurrent } from './anim'; +import { baseMove } from './board'; import { DragCurrent } from './drag'; import { Drawable } from './draw'; +import { render } from './render'; import { timer } from './util'; import * as cg from './types'; @@ -136,6 +138,8 @@ export interface HeadlessState { notation: cg.Notation; onlyDropsVariant: boolean; singleClickMoveVariant: boolean; + baseMove: (state: HeadlessState, orig: cg.Key, dest: cg.Key) => cg.Piece | boolean; + render: (state: State) => void; } export interface State extends HeadlessState { @@ -248,5 +252,7 @@ export function defaults(): HeadlessState { notation: cg.Notation.DEFAULT, onlyDropsVariant: false, singleClickMoveVariant: false, + baseMove, + render, }; } diff --git a/src/util.ts b/src/util.ts index b7302a6..c34e522 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,10 +1,7 @@ import * as cg from './types'; import * as T from './transformations'; -import { - posToTranslateAbs as abalonePosToTranslateAbs, - posToTranslateRel as abalonePosToTranslateRel, -} from './variants/abalone/util'; +import { posToTranslateRel as abalonePosToTranslateRel, posToTranslateBase2 } from './variants/abalone/util'; export const playerIndexs: cg.PlayerIndex[] = ['p1', 'p2']; export const invRanks: readonly cg.Rank[] = [...cg.ranks19].reverse(); @@ -23,18 +20,6 @@ export function allKeys(bd: cg.BoardDimensions = { width: 8, height: 8 }) { return Array.prototype.concat(...files(bd.width).map(c => ranks(bd.height).map(r => c + r))); } -// @TODO VFR: example of how pos2key could be rewritten -// export const pos2key = (variant: cg.Variant, pos: cg.Pos): cg.Key => { -// switch(variant) { -// case "abalone": { -// return (cg.files[pos[0] - 1] + cg.ranks19[pos[1] - 1]) as cg.Key; -// } -// default: { -// return (cg.files[pos[0] - 1] + cg.ranks19[pos[1] - 1]) as cg.Key; -// } -// } -// } - export function pos2key(pos: cg.Pos): cg.Key { return (cg.files[pos[0] - 1] + cg.ranks19[pos[1] - 1]) as cg.Key; } @@ -177,7 +162,8 @@ export const posToTranslateAbs = ( (variant === 'backgammon' || variant === 'hyper' || variant === 'nackgammon' ? 0.8 : 1), yFactor = bounds.height / bt.height; if (variant === 'abalone') { - return (pos, orientation) => abalonePosToTranslateAbs(pos, orientation, xFactor, yFactor, bt); + // "working" WIP: have to use HOF + return (pos, orientation) => posToTranslateBase2(bounds, pos, orientation); } return (pos, orientation) => posToTranslateBase(pos, orientation, xFactor, yFactor, bt); }; @@ -189,7 +175,8 @@ export const posToTranslateRel = ( v: cg.Variant, ): cg.NumberPair => { if (v === 'abalone') { - return abalonePosToTranslateRel(pos, orientation, bt); + // "working" WIP: have to use HOF + return abalonePosToTranslateRel(pos, orientation); } return posToTranslateBase( pos, @@ -238,7 +225,6 @@ export const createEl = (tagName: string, className?: string): HTMLElement => { return el; }; -// @TODO VFR investigations: could be interesting to see if it can be used to move Abalone squares ?? export function computeSquareCenter( key: cg.Key, orientation: cg.Orientation, diff --git a/src/variants/abalone/README.md b/src/variants/abalone/README.md deleted file mode 100644 index 12501ab..0000000 --- a/src/variants/abalone/README.md +++ /dev/null @@ -1 +0,0 @@ -Ideas from mid-investigation : We need to define our own key2pos and pos2key functions as the grid is an hexagon ? diff --git a/src/variants/abalone/board.ts b/src/variants/abalone/board.ts index b5cbd0c..bd3d34a 100644 --- a/src/variants/abalone/board.ts +++ b/src/variants/abalone/board.ts @@ -1,21 +1,154 @@ -import * as cg from '../../types'; -import * as T from '../../transformations'; +import type * as cg from '../../types'; import { knight, queen } from '../../premove'; import { distanceSq } from '../../util'; -import { allPos, computeSquareCenter, key2posAlt, pos2key } from './util'; +import { allPos, computeSquareCenter, getCoordinates, getSquareDimensions, key2posAlt, pos2key } from './util'; + +/* + from a position in pixels, returns the key of the square + by default squares positions in CG are computed like this : + ----------------- + |_ _ _ _ _ _ _ _ _| 8 + |_ _ _ _ _ _ _ _ _| 7 + |_ _ _ _ _ _ _ _ _| 6 + |_ _ _ _ _ _ _ _ _| 5 + |_ _ _ _ _ _ _ _ _| 4 + |_ _ _ _ _ _ _ _ _| 3 + |_ _ _ _ _ _ _ _ _| 2 + |_ _ _ _ _ _ _ _ _| 1 + ----------------- + a b c d e f g h i + + But as an hexagonal grid, we have to take into account the margin between the border of the board + ... and the limit of the area : + ---------------------- + | |margin + | _ _ _ _ _ | 9 + | /_ _ _ _ _ _\ | 8 + | /_ _ _ _ _ _ _\ | 7 + | /_ _ _ _ _ _ _ _\ | 6 + | /_ _ _ _ _ _ _ _ _\ | 5 + | \ _ _ _ _ _ _ _ _ / | 4 + | \ _ _ _ _ _ _ _ / | 3 + | \ _ _ _ _ _ _ / | 2 + | \ _ _ _ _ _ / | 1 + | |margin + ---------------------- + \ \ \ \ \ \ \ \ \ + a b c d e f g h i + + Used to know on which square the user clicked. +*/ export const getKeyAtDomPos = ( pos: cg.NumberPair, orientation: cg.Orientation, bounds: ClientRect, - bd: cg.BoardDimensions, ): cg.Key | undefined => { - let file = Math.ceil(bd.width * ((pos[0] - bounds.left) / bounds.width)); - const rank = Math.ceil(bd.height - bd.height * ((pos[1] - bounds.top) / bounds.height)); - if (rank === undefined || file === undefined) return undefined; - pos = [file, rank]; - pos = T.mapToP1[orientation](pos, bd); - return pos[0] > 0 && pos[0] < bd.width + 1 && pos[1] > 0 && pos[1] < bd.height + 1 ? pos2key(pos) : undefined; + const clickCenterX = pos[0] - bounds.left; + const clickCenterY = pos[1] - bounds.top; + const squareDimensions = getSquareDimensions(bounds); + const verticalCenter = bounds.height / 2; + const horizontalCenter = bounds.width / 2; + + if ( + clickCenterY > verticalCenter - 0.5 * squareDimensions.height && + clickCenterY < verticalCenter + 0.5 * squareDimensions.height + ) { + // line "e" + const columnIndex = Math.floor((clickCenterX - horizontalCenter) / squareDimensions.width + 4.5); + if (columnIndex < 0 || columnIndex > 8) return undefined; + return getCoordinates(columnIndex, 4, orientation); + } + if ( + clickCenterY > verticalCenter + 0.5 * squareDimensions.height && + clickCenterY < verticalCenter + 1.5 * squareDimensions.height + ) { + // line "d" + const columnIndex = Math.floor( + (clickCenterX - (horizontalCenter - 4 * squareDimensions.width)) / squareDimensions.width, + ); + if (columnIndex < 0 || columnIndex > 8) return undefined; + return getCoordinates(columnIndex, 3, orientation); + } + if ( + clickCenterY > verticalCenter + 1.5 * squareDimensions.height && + clickCenterY < verticalCenter + 2.5 * squareDimensions.height + ) { + // line "c" + const columnIndex = Math.floor( + (clickCenterX - (horizontalCenter - 3.5 * squareDimensions.width)) / squareDimensions.width, + ); + if (columnIndex < 0 || columnIndex > 6) return undefined; + return getCoordinates(columnIndex, 2, orientation); + } + if ( + clickCenterY > verticalCenter + 2.5 * squareDimensions.height && + clickCenterY < verticalCenter + 3.5 * squareDimensions.height + ) { + // line "b" + const columnIndex = Math.floor( + (clickCenterX - (horizontalCenter - 3 * squareDimensions.width)) / squareDimensions.width, + ); + if (columnIndex < 0 || columnIndex > 5) return undefined; + return getCoordinates(columnIndex, 1, orientation); + } + if ( + clickCenterY > verticalCenter + 3.5 * squareDimensions.height && + clickCenterY < verticalCenter + 4.5 * squareDimensions.height + ) { + // line "a" + const columnIndex = Math.floor( + (clickCenterX - (horizontalCenter - 2.5 * squareDimensions.width)) / squareDimensions.width, + ); + if (columnIndex < 0 || columnIndex > 4) return undefined; + return getCoordinates(columnIndex, 0, orientation); + } + if ( + clickCenterY < verticalCenter - 0.5 * squareDimensions.height && + clickCenterY > verticalCenter - 1.5 * squareDimensions.height + ) { + // line "f" + const columnIndex = Math.floor( + (clickCenterX - (horizontalCenter - 4 * squareDimensions.width)) / squareDimensions.width, + ); + if (columnIndex < 0 || columnIndex > 8) return undefined; + return getCoordinates(columnIndex + 1, 5, orientation); + } + if ( + clickCenterY < verticalCenter - 1.5 * squareDimensions.height && + clickCenterY > verticalCenter - 2.5 * squareDimensions.height + ) { + // line "g" + const columnIndex = Math.floor( + (clickCenterX - (horizontalCenter - 3.5 * squareDimensions.width)) / squareDimensions.width, + ); + if (columnIndex < 0 || columnIndex > 8) return undefined; + return getCoordinates(columnIndex + 2, 6, orientation); + } + if ( + clickCenterY < verticalCenter - 2.5 * squareDimensions.height && + clickCenterY > verticalCenter - 3.5 * squareDimensions.height + ) { + // line "h" + const columnIndex = Math.floor( + (clickCenterX - (horizontalCenter - 3 * squareDimensions.width)) / squareDimensions.width, + ); + if (columnIndex < 0 || columnIndex > 8) return undefined; + return getCoordinates(columnIndex + 3, 7, orientation); + } + if ( + clickCenterY < verticalCenter - 3.5 * squareDimensions.height && + clickCenterY > verticalCenter - 4.5 * squareDimensions.height + ) { + // line "i" + const columnIndex = Math.floor( + (clickCenterX - (horizontalCenter - 2.5 * squareDimensions.width)) / squareDimensions.width, + ); + if (columnIndex < 0 || columnIndex > 8) return undefined; + return getCoordinates(columnIndex + 4, 8, orientation); + } + + return undefined; }; export const getSnappedKeyAtDomPos = ( diff --git a/src/variants/abalone/config.ts b/src/variants/abalone/config.ts new file mode 100644 index 0000000..88d4964 --- /dev/null +++ b/src/variants/abalone/config.ts @@ -0,0 +1,14 @@ +import type { HeadlessState } from '../../state'; + +import { baseMove } from './engine'; +import { render } from './render'; + +export const configure = (state: HeadlessState): void => { + // HOF + state.baseMove = baseMove; + state.render = render; + + // these below could just have been overriden by a config object + state.coordinates = true; // same as default + state.animation.enabled = false; +}; diff --git a/src/variants/abalone/directions.ts b/src/variants/abalone/directions.ts index 50a6bc3..d8ef6a4 100644 --- a/src/variants/abalone/directions.ts +++ b/src/variants/abalone/directions.ts @@ -1,16 +1,34 @@ -import * as cg from '../../types'; +import type * as cg from '../../types'; + +import type { DirectionString } from './types'; export enum DiagonalDirectionString { - UpLeft = 'UpLeft', - UpRight = 'UpRight', - DownRight = 'DownRight', - DownLeft = 'DownLeft', + UpLeft = 'NW', + UpRight = 'NE', + DownRight = 'SE', + DownLeft = 'SW', } -enum HorizontalDirectionString { - Left = 'Left', - Right = 'Right', +export enum HorizontalDirectionString { + Left = 'W', + Right = 'E', } -type DirectionString = DiagonalDirectionString | HorizontalDirectionString; + +export const inverseDirection = (direction: DirectionString): DirectionString => { + switch (direction) { + case DiagonalDirectionString.UpLeft: + return DiagonalDirectionString.DownRight; + case DiagonalDirectionString.UpRight: + return DiagonalDirectionString.DownLeft; + case DiagonalDirectionString.DownRight: + return DiagonalDirectionString.UpLeft; + case DiagonalDirectionString.DownLeft: + return DiagonalDirectionString.UpRight; + case HorizontalDirectionString.Left: + return HorizontalDirectionString.Right; + case HorizontalDirectionString.Right: + return HorizontalDirectionString.Left; + } +}; export const move = (key: cg.Key, direction: DirectionString): cg.Key | undefined => { const transformedKey = directionMappings[direction](key); @@ -128,38 +146,14 @@ const traverseUntil = (pos: cg.Key, stop: (pos: cg.Key) => boolean, direction: D }; const isValidKey = (key: cg.Key): boolean => { - if (key.length !== 2) return false; // e.g. 'e10' - - const num = Number(key[1]); - const charCode = key[0].charCodeAt(0); - - if (num < 1 || num > 9) return false; - if (charCode < 97 || charCode > 105) return false; // a-i - - const specificChecks: { [char: string]: [number, number] } = { - a: [1, 5], - b: [1, 6], - c: [1, 7], - d: [1, 8], - f: [2, 9], - g: [3, 9], - h: [4, 9], - i: [5, 9], - }; - - const range = specificChecks[key[0]]; - if (range && (num < range[0] || num > range[1])) return false; - - return true; + return /^(a[1-5]|b[1-6]|c[1-7]|d[1-8]|e[1-9]|f[2-9]|g[3-9]|h[4-9]|i[5-9])$/.test(key); }; const directionMappings: { [key in DirectionString]: (key: cg.Key) => cg.Key } = { - UpLeft: (key: cg.Key) => (String.fromCharCode(key[0].charCodeAt(0)) + (parseInt(key[1]) + 1).toString()) as cg.Key, - UpRight: (key: cg.Key) => - (String.fromCharCode(key[0].charCodeAt(0) + 1) + (parseInt(key[1]) + 1).toString()) as cg.Key, - DownLeft: (key: cg.Key) => - (String.fromCharCode(key[0].charCodeAt(0) - 1) + (parseInt(key[1]) - 1).toString()) as cg.Key, - DownRight: (key: cg.Key) => (String.fromCharCode(key[0].charCodeAt(0)) + (parseInt(key[1]) - 1).toString()) as cg.Key, - Left: (key: cg.Key) => (String.fromCharCode(key[0].charCodeAt(0) - 1) + key[1]) as cg.Key, - Right: (key: cg.Key) => (String.fromCharCode(key[0].charCodeAt(0) + 1) + key[1]) as cg.Key, + NW: (key: cg.Key) => (String.fromCharCode(key[0].charCodeAt(0)) + (parseInt(key[1]) + 1).toString()) as cg.Key, + NE: (key: cg.Key) => (String.fromCharCode(key[0].charCodeAt(0) + 1) + (parseInt(key[1]) + 1).toString()) as cg.Key, + SW: (key: cg.Key) => (String.fromCharCode(key[0].charCodeAt(0) - 1) + (parseInt(key[1]) - 1).toString()) as cg.Key, + SE: (key: cg.Key) => (String.fromCharCode(key[0].charCodeAt(0)) + (parseInt(key[1]) - 1).toString()) as cg.Key, + W: (key: cg.Key) => (String.fromCharCode(key[0].charCodeAt(0) - 1) + key[1]) as cg.Key, + E: (key: cg.Key) => (String.fromCharCode(key[0].charCodeAt(0) + 1) + key[1]) as cg.Key, }; diff --git a/src/variants/abalone/drag.ts b/src/variants/abalone/drag.ts index dc33d36..bbcb22a 100644 --- a/src/variants/abalone/drag.ts +++ b/src/variants/abalone/drag.ts @@ -1,8 +1,10 @@ import { cancel } from '../../drag'; import { State } from '../../state'; import { distanceSq, posToTranslateAbs, samePiece } from '../../util'; + import { translateAbs } from './util'; +// @TODO: remove parts unrelated with Abalone if there are. I see a 'chess' line 32. export function processDrag(s: State): void { requestAnimationFrame(() => { const cur = s.draggable.current; @@ -27,7 +29,7 @@ export function processDrag(s: State): void { cur.pos = [cur.epos[0] - cur.rel[0], cur.epos[1] - cur.rel[1]]; // move piece - const translation = posToTranslateAbs(s.dom.bounds(), s.dimensions, 'chess')(cur.origPos, s.orientation); + const translation = posToTranslateAbs(s.dom.bounds(), s.dimensions, 'chess')(cur.origPos, s.orientation); // "working" WIP: have to use HOF translation[0] += cur.pos[0] + cur.dec[0]; translation[1] += cur.pos[1] + cur.dec[1]; translateAbs(cur.element, translation); diff --git a/src/variants/abalone/draw.ts b/src/variants/abalone/draw.ts index 7a40bc8..854032a 100644 --- a/src/variants/abalone/draw.ts +++ b/src/variants/abalone/draw.ts @@ -1,9 +1,12 @@ +import type * as cg from '../../types'; import { cancelMove, unselect } from '../../board'; import { eventBrush } from '../../draw'; import { State } from '../../state'; import { eventPosition } from '../../util'; + import { getKeyAtDomPos, getSnappedKeyAtDomPos } from './board'; -import * as cg from '../../types'; + +// @TODO: using HOF would allow not repeating the same code here. This code is generic, not Abalone specific. We just invoke getKeyAtDomPos and getSnappedKeyAtDomPos export const start = (state: State, e: cg.MouchEvent): void => { // support one finger touch only @@ -12,7 +15,7 @@ export const start = (state: State, e: cg.MouchEvent): void => { e.preventDefault(); e.ctrlKey ? unselect(state) : cancelMove(state); const pos = eventPosition(e)!, - orig = getKeyAtDomPos(pos, state.orientation, state.dom.bounds(), state.dimensions); + orig = getKeyAtDomPos(pos, state.orientation, state.dom.bounds()); if (!orig) return; state.drawable.current = { orig, @@ -28,7 +31,7 @@ export const processDraw = (state: State): void => { requestAnimationFrame(() => { const cur = state.drawable.current; if (cur) { - const keyAtDomPos = getKeyAtDomPos(cur.pos, state.orientation, state.dom.bounds(), state.dimensions); + const keyAtDomPos = getKeyAtDomPos(cur.pos, state.orientation, state.dom.bounds()); if (!keyAtDomPos) { cur.snapToValidMove = false; } diff --git a/src/variants/abalone/engine.ts b/src/variants/abalone/engine.ts new file mode 100644 index 0000000..b70145d --- /dev/null +++ b/src/variants/abalone/engine.ts @@ -0,0 +1,208 @@ +import { setPieces, unselect } from '../../board'; +import { HeadlessState } from '../../state'; +import { callUserFunction } from '../../util'; +import type * as cg from '../../types'; + +import { + candidateLineDirs, + deducePotentialSideDirs, + move, + getDirectionString, + isMoveInLine, + DiagonalDirectionString, + inverseDirection, +} from './directions'; +import type { MoveImpact, MoveVector } from './types'; + +export const computeMoveImpact = (pieces: cg.Pieces, orig: cg.Key, dest: cg.Key): MoveImpact | undefined => { + const directionString = getDirectionString(orig, dest); + if (!directionString) return undefined; + const isAMoveInLine = isMoveInLine(orig, dest, directionString); + const diff: cg.PiecesDiff = new Map(pieces); + + if (isAMoveInLine) { + diff.set(dest, pieces.get(orig)); + diff.set(orig, undefined); + if (!pieces.get(dest)) { + // line move + return { + diff, + capture: false, + moveVector: { + directionString, + landingSquares: [dest], + }, + }; + } + // push move + const landingSquare1 = move(dest, directionString); + if (landingSquare1 === undefined) + // xxo\ xxxo\ + return { + diff, + capture: true, + moveVector: { + directionString, + landingSquares: [dest], + }, + }; + if (!pieces.get(landingSquare1)) { + // xxo. xxxo. + diff.set(landingSquare1, pieces.get(dest)); + return { + diff, + capture: false, + moveVector: { + directionString, + landingSquares: [dest, landingSquare1], + }, + }; + } + + const landingSquare2 = move(landingSquare1, directionString); + if (landingSquare2 === undefined) + // xxxoo\ + return { + diff, + capture: true, + moveVector: { + directionString, + landingSquares: [dest, landingSquare1], + }, + }; + if (!pieces.get(landingSquare2)) { + // xxxoo. + diff.set(landingSquare2, pieces.get(dest)); + return { + diff, + capture: false, + moveVector: { + directionString, + landingSquares: [dest, landingSquare2], + }, + }; + } + } + + // side move + for (const lineDir of candidateLineDirs(directionString as DiagonalDirectionString)) { + const sideDirs = deducePotentialSideDirs(directionString as DiagonalDirectionString, lineDir); + const secondPos = move(orig, lineDir); + if (secondPos === undefined) continue; + for (const sideDir of sideDirs) { + const side2ndPos = move(secondPos, sideDir); + if (side2ndPos) { + const side1stPos = move(orig, sideDir); + if (side1stPos === undefined) continue; + if (side1stPos && pieces.get(secondPos)) { + if (side2ndPos === dest) { + diff.set(side1stPos, pieces.get(orig)); + diff.set(orig, undefined); + diff.set(dest, pieces.get(secondPos)); + diff.set(secondPos, undefined); + return { + diff, + capture: false, + moveVector: { + directionString: sideDir, + landingSquares: [side1stPos, dest], + }, + }; + } else { + // 3 marbles are moving + const thirdPos = move(secondPos, lineDir); + if (thirdPos === undefined) continue; + const side3rdPos = move(thirdPos, sideDir); + if (side3rdPos === undefined) continue; + if (pieces.get(thirdPos) && side3rdPos === dest) { + diff.set(side1stPos, pieces.get(orig)); + diff.set(orig, undefined); + diff.set(side2ndPos, pieces.get(secondPos)); + diff.set(secondPos, undefined); + diff.set(side3rdPos, pieces.get(thirdPos)); + diff.set(thirdPos, undefined); + return { + diff, + capture: false, + moveVector: { + directionString: sideDir, + landingSquares: [side1stPos, side2ndPos, side3rdPos], + }, + }; + } + } + } + } + } + } + + return undefined; +}; + +export const computeMoveVectorPostMove = (pieces: cg.Pieces, orig: cg.Key, dest: cg.Key): MoveVector | undefined => { + const directionString = getDirectionString(dest, orig); + if (!directionString) return undefined; + const isAMoveInLine = isMoveInLine(dest, orig, directionString); + const inverseDirectionString = inverseDirection(directionString); + + if (isAMoveInLine) { + return { + directionString: inverseDirectionString, + landingSquares: [dest], + }; + } + + // side move + for (const lineDir of candidateLineDirs(directionString as DiagonalDirectionString)) { + const sideDirs = deducePotentialSideDirs(directionString as DiagonalDirectionString, lineDir); + const secondPos = move(dest, lineDir); + if (secondPos === undefined) continue; + for (const sideDir of sideDirs) { + const side2ndPos = move(secondPos, sideDir); + if (side2ndPos) { + const side1stPos = move(dest, sideDir); + if (side1stPos === undefined) continue; + if (side1stPos && pieces.get(secondPos)) { + if (side2ndPos === orig) { + return { + directionString: inverseDirection(sideDir), + landingSquares: [secondPos, dest], + }; + } else { + // 3 marbles are moving + const thirdPos = move(secondPos, lineDir); + if (thirdPos === undefined) continue; + const side3rdPos = move(thirdPos, sideDir); + if (side3rdPos === undefined) continue; + if (pieces.get(thirdPos) && side3rdPos === orig) { + return { + directionString: inverseDirection(sideDir), + landingSquares: [secondPos, thirdPos, dest], + }; + } + } + } + } + } + } + + return undefined; +}; + +export function baseMove(state: HeadlessState, orig: cg.Key, dest: cg.Key): cg.Piece | boolean { + // Note: after you moved, you also receive the move from the API. But the piece is already gone, since you moved. + if (!state.pieces.get(orig)) return false; + + const moveImpact = computeMoveImpact(state.pieces, orig, dest); + if (!moveImpact) return false; + + if (dest === state.selected) unselect(state); + callUserFunction(state.events.move, orig, dest, moveImpact.capture); + + setPieces(state, moveImpact.diff); + + state.lastMove = [orig, dest]; + state.check = undefined; + callUserFunction(state.events.change); + return moveImpact.capture || true; +} diff --git a/src/variants/abalone/fen.ts b/src/variants/abalone/fen.ts index 04d1b36..c337a7a 100644 --- a/src/variants/abalone/fen.ts +++ b/src/variants/abalone/fen.ts @@ -1,6 +1,6 @@ import { roles } from '../../fen'; -import * as cg from '../../types'; import { pos2key } from '../../util'; +import type * as cg from '../../types'; export const read = (fen: cg.FEN, dimensions: cg.BoardDimensions): cg.Pieces => { const pieces: cg.Pieces = new Map(); diff --git a/src/variants/abalone/premove.ts b/src/variants/abalone/premove.ts index 7d86080..ebe4b96 100644 --- a/src/variants/abalone/premove.ts +++ b/src/variants/abalone/premove.ts @@ -1,8 +1,9 @@ +import type * as cg from '../../types'; import { Mobility } from '../../premove'; -import * as cg from '../../types'; + import { pos2key } from './util'; -// @VFR: TODO: update this, the code is just a wrong copy paste +// @VFR: TODO: update this later on for correct premoves : this code is just an ugly copy paste export const marble = (pieces: cg.Pieces, playerIndex: cg.PlayerIndex): Mobility => { return (x1, y1 /*, x2, y2*/) => { const pos = pos2key([x1, y1 + (playerIndex === 'p1' ? 1 : -1)]) as cg.Key; diff --git a/src/variants/abalone/render.ts b/src/variants/abalone/render.ts index 8bd6a18..3d11df6 100644 --- a/src/variants/abalone/render.ts +++ b/src/variants/abalone/render.ts @@ -1,32 +1,23 @@ +import type * as cg from '../../types'; +import type { PieceName, SquareClasses } from '../../render'; import { p1Pov } from '../../board'; import { State } from '../../state'; -import { createEl, posToTranslateAbs, posToTranslateRel, translateAbs, translateRel } from '../../util'; -import * as cg from '../../types'; +import { createEl } from '../../util'; import { AnimCurrent, AnimFadings, AnimVector, AnimVectors } from '../../anim'; import { DragCurrent } from '../../drag'; -import { - PieceName, - SquareClasses, - appendValue, - computeSquareClasses, - isPieceNode, - isSquareNode, - pieceNameOf, - posZIndex, - removeNodes, -} from '../../render'; - -import { key2pos } from './util'; - -// @TODO VFR: this code is an ugly copy paste, refactor etc. +import { appendValue, isPieceNode, isSquareNode, posZIndex, removeNodes } from '../../render'; + +import { key2pos, translateAbs, translateRel, posToTranslateAbs2, posToTranslateRel2 } from './util'; +import { computeMoveVectorPostMove } from './engine'; + +// @TODO: remove parts unrelated to Abalone export const render = (s: State): void => { const orientation = s.orientation, asP1: boolean = p1Pov(s), - posToTranslate = s.dom.relative ? posToTranslateRel : posToTranslateAbs(s.dom.bounds(), s.dimensions, s.variant), + posToTranslate = s.dom.relative ? posToTranslateRel2 : posToTranslateAbs2(), translate = s.dom.relative ? translateRel : translateAbs, boardEl: HTMLElement = s.dom.elements.board, pieces: cg.Pieces = s.pieces, - pocketPieces: cg.Piece[] = s.pocketPieces, curAnim: AnimCurrent | undefined = s.animation.current, anims: AnimVectors = curAnim ? curAnim.plan.anims : new Map(), fadings: AnimFadings = curAnim ? curAnim.plan.fadings : new Map(), @@ -61,7 +52,7 @@ export const render = (s: State): void => { // if piece not being dragged anymore, remove dragging style if (el.cgDragging && (!curDrag || curDrag.orig !== k)) { el.classList.remove('dragging'); - translate(el, posToTranslate(key2pos(k), orientation, s.dimensions, s.variant)); + translate(el, posToTranslate(s.dom.bounds(), key2pos(k), orientation)); el.cgDragging = false; } // remove fading class if it still remains @@ -73,32 +64,25 @@ export const render = (s: State): void => { if (pieceAtKey) { // continue animation if already animating and same piece // (otherwise it could animate a captured piece) - if ( - anim && - el.cgAnimating && - elPieceName === pieceNameOf(pieceAtKey, s.myPlayerIndex, s.orientation, s.variant, k) - ) { + if (anim && el.cgAnimating && elPieceName === pieceNameOf(pieceAtKey, s.myPlayerIndex, s.orientation)) { const pos = key2pos(k); pos[0] += anim[2]; pos[1] += anim[3]; el.classList.add('anim'); - translate(el, posToTranslate(pos, orientation, s.dimensions, s.variant)); + translate(el, posToTranslate(s.dom.bounds(), pos, orientation)); } else if (el.cgAnimating) { el.cgAnimating = false; el.classList.remove('anim'); - translate(el, posToTranslate(key2pos(k), orientation, s.dimensions, s.variant)); + translate(el, posToTranslate(s.dom.bounds(), key2pos(k), orientation)); if (s.addPieceZIndex) el.style.zIndex = posZIndex(key2pos(k), orientation, asP1, s.dimensions); } // same piece: flag as same - if ( - elPieceName === pieceNameOf(pieceAtKey, s.myPlayerIndex, s.orientation, s.variant, k) && - (!fading || !el.cgFading) - ) { + if (elPieceName === pieceNameOf(pieceAtKey, s.myPlayerIndex, s.orientation) && (!fading || !el.cgFading)) { samePieces.add(k); } // different piece: flag as moved unless it is a fading piece else { - if (fading && elPieceName === pieceNameOf(fading, s.myPlayerIndex, s.orientation, s.variant, k)) { + if (fading && elPieceName === pieceNameOf(fading, s.myPlayerIndex, s.orientation)) { el.classList.add('fading'); el.cgFading = true; } else { @@ -125,7 +109,7 @@ export const render = (s: State): void => { // if (!sameSquares.has(sk)) { sMvdset = movedSquares.get(className); sMvd = sMvdset && sMvdset.pop(); - const translation = posToTranslate(key2pos(sk), orientation, s.dimensions, s.variant); + const translation = posToTranslate(s.dom.bounds(), key2pos(sk), orientation); if (sMvd) { sMvd.cgKey = sk; translate(sMvd, translation); @@ -143,7 +127,7 @@ export const render = (s: State): void => { for (const [k, p] of pieces) { anim = anims.get(k); if (!samePieces.has(k)) { - pMvdset = movedPieces.get(pieceNameOf(p, s.myPlayerIndex, s.orientation, s.variant, k)); + pMvdset = movedPieces.get(pieceNameOf(p, s.myPlayerIndex, s.orientation)); pMvd = pMvdset && pMvdset.pop(); // a same piece was moved if (pMvd) { @@ -161,12 +145,12 @@ export const render = (s: State): void => { pos[0] += anim[2]; pos[1] += anim[3]; } - translate(pMvd, posToTranslate(pos, orientation, s.dimensions, s.variant)); + translate(pMvd, posToTranslate(s.dom.bounds(), pos, orientation)); } // no piece in moved obj: insert the new piece // assumes the new piece is not being dragged else { - const pieceName = pieceNameOf(p, s.myPlayerIndex, s.orientation, s.variant, k), + const pieceName = pieceNameOf(p, s.myPlayerIndex, s.orientation), pieceNode = createEl('piece', pieceName) as cg.PieceNode, pos = key2pos(k); // used here to compute position @@ -177,7 +161,7 @@ export const render = (s: State): void => { pos[0] += anim[2]; pos[1] += anim[3]; } - translate(pieceNode, posToTranslate(pos, orientation, s.dimensions, s.variant)); + translate(pieceNode, posToTranslate(s.dom.bounds(), pos, orientation)); if (s.addPieceZIndex) pieceNode.style.zIndex = posZIndex(pos, orientation, asP1, s.dimensions); @@ -186,19 +170,109 @@ export const render = (s: State): void => { } } - // walk over all pocketPieces and set nodes - for (const p of pocketPieces) { - const classSpecificKey = p.playerIndex === 'p1' ? 'a1' : 'a2'; - const pieceName = pieceNameOf(p, s.myPlayerIndex, s.orientation, s.variant, classSpecificKey as cg.Key), - pieceNode = createEl('piece', 'pocket ' + pieceName) as cg.PieceNode; + // remove any element that remains in the moved sets + for (const nodes of movedPieces.values()) removeNodes(s, nodes); + for (const nodes of movedSquares.values()) removeNodes(s, nodes); // do not compute movedSquares ??? +}; + +function pieceNameOf(piece: cg.Piece, myPlayerIndex: cg.PlayerIndex, orientation: cg.Orientation): string { + const side = + (piece.playerIndex === myPlayerIndex && orientation === myPlayerIndex) || + (piece.playerIndex !== myPlayerIndex && orientation !== myPlayerIndex) + ? 'ally' + : 'enemy'; + + return `${piece.playerIndex} ${piece.role} ${side}`; +} - pieceNode.cgPiece = pieceName; - pieceNode.cgKey = s.orientation === 'p1' ? 'a2' : 'a1'; // always have 0 transform (top left corner) +function addSquare(squares: SquareClasses, key: cg.Key, klass: string): void { + const classes = squares.get(key); + if (classes) squares.set(key, `${classes} ${klass}`); + else squares.set(key, klass); +} - boardEl.appendChild(pieceNode); +// @TODO: clean this up to remove any notion non related to Abalone +export function computeSquareClasses(s: State): SquareClasses { + const squares: SquareClasses = new Map(); + + if (s.lastMove && s.lastMove.length === 2) { + const moveImpact = computeMoveVectorPostMove(s.pieces, s.lastMove[0], s.lastMove[1]); + const player = s.turnPlayerIndex; + moveImpact?.landingSquares.forEach(coordinates => { + addSquare(squares, coordinates, `last-move to ${player}${moveImpact.directionString}`); + }); } - // remove any element that remains in the moved sets - for (const nodes of movedPieces.values()) removeNodes(s, nodes); - for (const nodes of movedSquares.values()) removeNodes(s, nodes); -}; + if (s.lastMove && s.highlight.lastMove) { + // if (s.variants.abalone.lastMove) { + // for (const k of s.variants.abalone.lastMove.piecesMoving) { + // addSquare(squares, k, 'last-move from ww'); + // } + // } + // let first = true; + // for (const k of s.lastMove) { + // if (k !== 'a0') { + // if (first) { + // console.log(s.variants); + // addSquare(squares, k, 'last-move from ww'); // + variantSpecificHighlightClass(s.variant, k, s.orientation)); + // first = false; + // } else { + // addSquare(squares, k, 'last-move to ww'); // + variantSpecificHighlightClass(s.variant, k, s.orientation)); + // } + // } else { + // first = false; + // } + // } + } + if (s.selectOnly) { + for (const key of s.selectedPieces.keys()) { + addSquare(squares, key, 'selected'); + } + } + if (s.selected) { + addSquare(squares, s.selected, 'selected'); + if (s.movable.showDests) { + const dests = s.movable.dests?.get(s.selected); + if (dests) + for (const k of dests) { + addSquare(squares, k, 'move-dest' + (s.pieces.has(k) ? ' oc' : '')); + } + const pDests = s.premovable.dests; + if (pDests) + for (const k of pDests) { + addSquare(squares, k, 'premove-dest' + (s.pieces.has(k) ? ' oc' : '')); + } + } + } else if (s.dropmode.active || s.draggable.current?.orig === 'a0') { + const piece = s.dropmode.active ? s.dropmode.piece : s.draggable.current?.piece; + + if (piece) { + // TODO: there was a function called isPredroppable that was used in drag.ts or drop.ts or both. + // Maybe use the same here to decide what to render instead of potentially making it possible both + // kinds of highlighting to happen if something was not cleared up in the state. + // In other place (pocket.ts) this condition is used ot decide similar question: ctrl.myplayerIndex === ctrl.turnPlayerIndex + if (s.dropmode.showDropDests) { + const dests = s.dropmode.dropDests?.get(piece.role); + if (dests) + for (const k of dests) { + addSquare(squares, k, 'move-dest'); + } + } + if (s.predroppable.showDropDests) { + const pDests = s.predroppable.dropDests; + if (pDests) + for (const k of pDests) { + addSquare(squares, k, 'premove-dest' + (s.pieces.has(k) ? ' oc' : '')); + } + } + } + } + const premove = s.premovable.current; + if (premove) for (const k of premove) addSquare(squares, k, 'current-premove'); + else if (s.predroppable.current) addSquare(squares, s.predroppable.current.key, 'current-premove'); + + const o = s.exploding; + if (o) for (const k of o.keys) addSquare(squares, k, 'exploding' + o.stage); + + return squares; +} diff --git a/src/variants/abalone/svg.ts b/src/variants/abalone/svg.ts index 2335911..812ea1b 100644 --- a/src/variants/abalone/svg.ts +++ b/src/variants/abalone/svg.ts @@ -1,3 +1,4 @@ +import type * as cg from '../../types'; import { DrawShapePiece, DrawBrush, DrawBrushes } from '../../draw'; import { State } from '../../state'; import { @@ -12,10 +13,11 @@ import { orient, setAttributes, } from '../../svg'; -import * as cg from '../../types'; import { key2pos } from './util'; +// @TODO: using HOF would probably fix the arrows etc. Currently not working. + // pos2px is used to convert a position from the grid (board coordinates) to a position in pixels const pos2px = (pos: cg.Pos, bounds: ClientRect, bd: cg.BoardDimensions): cg.NumberPair => { return [ diff --git a/src/variants/abalone/types.ts b/src/variants/abalone/types.ts new file mode 100644 index 0000000..94a0729 --- /dev/null +++ b/src/variants/abalone/types.ts @@ -0,0 +1,24 @@ +import type * as cg from '../../types'; + +import { DiagonalDirectionString, HorizontalDirectionString } from './directions'; + +export type DirectionString = DiagonalDirectionString | HorizontalDirectionString; + +export type MoveImpact = { + diff: cg.PiecesDiff; // diff applied to pieces after the move + capture: boolean; + moveVector: MoveVector; +}; + +// allow describing a move in terms of direction applied to some pieces +export type MoveVector = { + directionString: DirectionString; + landingSquares: cg.Key[]; +}; + +export type SquareDimensions = { + width: number; + height: number; +}; + +export type TranslateBase = (pos: cg.Pos, bounds: ClientRect) => cg.NumberPair; diff --git a/src/variants/abalone/util.ts b/src/variants/abalone/util.ts index 0db325b..561d65d 100644 --- a/src/variants/abalone/util.ts +++ b/src/variants/abalone/util.ts @@ -1,7 +1,6 @@ -import * as cg from '../../types'; +import type * as cg from '../../types'; import * as T from '../../transformations'; -import { candidateLineDirs, deducePotentialSideDirs, move, getDirectionString, isMoveInLine } from './directions'; -import type { DiagonalDirectionString } from './directions'; +import { SquareDimensions, TranslateBase } from './types'; const abaloneFiles = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'] as const; @@ -9,20 +8,23 @@ const abaloneRanks = ['1', '2', '3', '4', '5', '6', '7', '8', '9'] as const; export const pos2key = (pos: cg.Pos): cg.Key => { let posx = pos[0]; + if (pos[1] == 0) { + posx += 2; + } if (pos[1] == 1) { - posx = pos[0] - 3; + posx = pos[0] - 2; } if (pos[1] == 2) { posx = pos[0] - 2; // -2.5 } if (pos[1] == 3) { - posx = pos[0] - 2; + posx = pos[0] - 1; } if (pos[1] == 4) { posx = pos[0] - 1; // -1.5 } if (pos[1] == 5) { - posx = pos[0] - 1; // - 1 + posx = pos[0]; // - 1 } if (pos[1] == 6) { posx = pos[0]; // -0.5 @@ -37,36 +39,114 @@ export const pos2key = (pos: cg.Pos): cg.Key => { posx = pos[0] + 1; } - const key = (abaloneFiles[posx] + abaloneRanks[pos[1] - 1]) as cg.Key; + posx = pos[0]; + let posy = pos[1]; + + const key = (abaloneFiles[posx] + abaloneRanks[posy]) as cg.Key; return key; }; export const key2pos = (k: cg.Key): cg.Pos => { - return [k.charCodeAt(0) - 96, parseInt(k.slice(1))] as cg.Pos; + const rank = parseInt(k.slice(1)); + const file = k.charCodeAt(0) - 96; + + return [file, rank] as cg.Pos; }; -export const key2posAlt = (k: cg.Key): cg.Pos => { - const shift = parseInt(k.slice(1)); - const diff = (shift - 1) * 0.5; - if (parseInt(k.slice(1)) < 5) { - return [k.charCodeAt(0) - 96 + 2 - diff, parseInt(k.slice(1))] as cg.Pos; +const computeShift = (k: cg.Key): cg.Pos => { + const rank = parseInt(k.slice(1)); + const file = k.charCodeAt(0) - 96; + const xScale = 100; + const yScale = 100; + const bt = { width: 9, height: 9 }; + + if (rank == 1) { + // bottom left + return [(file + shift[rank - 1] - 1) * xScale, (bt.height - rank - 1) * yScale]; + } + if (rank == 2) { + return [(file + shift[rank - 1] - 1) * xScale, (bt.height - rank - 1) * yScale]; } - return [k.charCodeAt(0) - 96 - (diff - 5) - 3, parseInt(k.slice(1))] as cg.Pos; + if (rank == 3) { + return [(file + shift[rank - 1] - 1) * xScale, (bt.height - rank - 1) * yScale]; + } + if (rank == 4) { + return [(file + shift[rank - 1] - 1) * xScale, (bt.height - rank - 1) * yScale]; + } + if (rank == 5) { + return [(file + shift[rank - 1] - 1) * xScale, (bt.height - rank) * yScale]; + } + if (rank == 6) { + return [(file + shift[rank - 1] - 1) * xScale, (bt.height - rank + 1) * yScale]; + } + if (rank == 7) { + return [(file + shift[rank - 1] - 1) * xScale, (bt.height - rank + 1) * yScale]; + } + if (rank == 8) { + return [(file + shift[rank - 1] - 1) * xScale, (bt.height - rank + 1) * yScale]; + } + return [(file + shift[rank - 1] - 1) * xScale, (bt.height - rank + 1) * yScale * 10]; +}; + +// from a key, determine a position +export const key2posAlt = (k: cg.Key): cg.Pos => { + return computeShift(k); }; +// shift is used by analysis page and miniboards const shift = [2, 1.5, 1, 0.5, 0, -0.5, -1, -1.5, -2]; -// @TODO VFR: translateBase should probably be in transformations.ts -// @TODO VFR: probably need to adapt the code here still anyway +export function computeSquareCenter( + key: cg.Key, + orientation: cg.Orientation, + bounds: ClientRect, + bd: cg.BoardDimensions, +): cg.NumberPair { + const pos = T.mapToP1Inverse[orientation](key2posAlt(key), bd); + + return [ + (pos[0] + shift[pos[1] - 1] - 1) * 100, + bounds.top + (bounds.height * (2 * bd.height - (pos[1] + 0.5))) / bd.height, + ]; +} + +// translateBase defines where the translation of a piece should be placed on the board. +// It is used to render the piece at the correct place. const translateBase: Record = { - p1: (pos: cg.Pos, xScale: number, yScale: number, bt: cg.BoardDimensions) => [ - (pos[0] - 1 + shift[pos[1] - 1]) * xScale, - (bt.height - pos[1]) * yScale, - ], - p2: (pos: cg.Pos, xScale: number, yScale: number, bt: cg.BoardDimensions) => [ - (bt.width - pos[0] - shift[pos[1] - 1]) * xScale, - (pos[1] - 1) * yScale, - ], + p1: (pos: cg.Pos, _xScale: number, _yScale: number, _bt: cg.BoardDimensions) => { + const squareWidth = 102.5; + const squareHeight = 88; + const bottomLeft = [295, 854]; + const shift = [0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4]; + + if (pos[1] < 6) { + return [ + bottomLeft[0] + squareWidth * pos[0] - (shift[pos[1] - 1] + 1) * squareWidth, + bottomLeft[1] - (pos[1] - 1) * squareHeight, + ]; + } + return [ + bottomLeft[0] + squareWidth * pos[0] - (shift[pos[1] - 1] + 1) * squareWidth, + bottomLeft[1] - (pos[1] - 1) * squareHeight, + ]; + }, + p2: (pos: cg.Pos, _xScale: number, _yScale: number, _bt: cg.BoardDimensions) => { + const squareWidth = 102.5; + const squareHeight = 88; + const bottomLeft = [295, 854]; + const shift = [0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4]; + + if (pos[1] < 6) { + return [ + bottomLeft[0] + squareWidth * pos[0] - (shift[pos[1] - 1] + 1) * squareWidth, + bottomLeft[1] - (pos[1] - 1) * squareHeight, + ]; + } + return [ + bottomLeft[0] + squareWidth * pos[0] - (shift[pos[1] - 1] + 1) * squareWidth, + bottomLeft[1] - (pos[1] - 1) * squareHeight, + ]; + }, right: (pos: cg.Pos, xScale: number, yScale: number, _) => [(pos[1] - 1) * xScale, (pos[0] - 1) * yScale], left: (pos: cg.Pos, xScale: number, yScale: number, bt: cg.BoardDimensions) => [ (bt.width - pos[0]) * xScale, @@ -80,19 +160,22 @@ export const posToTranslateAbs = ( orientation: cg.Orientation, xFactor: number, yFactor: number, - bt: cg.BoardDimensions, ): cg.NumberPair => { - return translateBase[orientation](pos, xFactor, yFactor, bt); + return translateBase[orientation](pos, xFactor, yFactor, { width: 9, height: 9 }); }; -export const posToTranslateRel = (pos: cg.Pos, orientation: cg.Orientation, bt: cg.BoardDimensions): cg.NumberPair => { - return translateBase[orientation](pos, 100, 100, bt); +export const posToTranslateRel = (pos: cg.Pos, orientation: cg.Orientation): cg.NumberPair => { + return translateBase[orientation](pos, 100, 100, { width: 9, height: 9 }); }; export const translateAbs = (el: HTMLElement, pos: cg.NumberPair): void => { el.style.transform = `translate(${pos[0]}px,${pos[1]}px)`; }; +export const translateRel = (el: HTMLElement, percents: cg.NumberPair): void => { + el.style.transform = `translate(${percents[0]}%,${percents[1]}%)`; +}; + function files(n: number) { return abaloneFiles.slice(0, n); } @@ -105,94 +188,92 @@ export function allKeys(bd: cg.BoardDimensions = { width: 9, height: 9 }) { return Array.prototype.concat(...files(bd.width).map(c => ranks(bd.height).map(r => c + r))); } -export const allPos = (bd: cg.BoardDimensions): cg.Pos[] => allKeys(bd).map(key2pos); +export const allPos = (bd: cg.BoardDimensions): cg.Pos[] => allKeys(bd).map(key2posAlt); -export function computeSquareCenter( - key: cg.Key, - orientation: cg.Orientation, +export const posToTranslateBase2 = (bounds: ClientRect, pos: cg.Pos, orientation: cg.Orientation): cg.NumberPair => { + return translateBase2[orientation](pos, bounds); +}; +export const posToTranslateAbs2 = (): (( bounds: ClientRect, - bd: cg.BoardDimensions, -): cg.NumberPair { - const pos = T.mapToP1Inverse[orientation](key2pos(key), bd); - return [ - bounds.left + (bounds.width * (pos[0] + shift[pos[1] - 1] - 1 + 0.5)) / bd.width, - bounds.top + (bounds.height * (bd.height - (pos[1] - 1 + 0.5))) / bd.height, - ]; -} + pos: cg.Pos, + orientation: cg.Orientation, +) => cg.NumberPair) => { + return (bounds, pos, orientation) => posToTranslateBase2(bounds, pos, orientation); +}; +const translateBase2: Record = { + p1: (pos: cg.Pos, bounds: ClientRect) => { + const height = bounds.height; + const width = bounds.width; + const squareDimensions = getSquareDimensions(bounds); -export const abaloneUpdatePiecesFromMove = ( - pieces: cg.Pieces, - orig: cg.Key, - dest: cg.Key, -): [cg.PiecesDiff, boolean] => { - const directionString = getDirectionString(orig, dest); - if (!directionString) return [pieces, false]; - const isAMoveInLine = isMoveInLine(orig, dest, directionString); - const diff: cg.PiecesDiff = new Map(pieces); - - if (isAMoveInLine) { - diff.set(dest, pieces.get(orig)); - diff.set(orig, undefined); - if (!pieces.get(dest)) { - // line move - return [diff, false]; - } - // push move - const landingSquare1 = move(dest, directionString); - if (landingSquare1 === undefined) return [diff, true]; // xxo\ xxxo\ - if (!pieces.get(landingSquare1)) { - // xxo. xxxo. - diff.set(landingSquare1, pieces.get(dest)); - return [diff, false]; - } + const computedHeight = height * 0.4546 + squareDimensions.height * (5 - pos[1]); + let computedWidth = width * 0.4546 + squareDimensions.width * (5 - pos[0]); - const landingSquare2 = move(landingSquare1, directionString); - if (landingSquare2 === undefined) return [diff, true]; // xxxoo\ - if (!pieces.get(landingSquare2)) { - // xxxoo. - diff.set(landingSquare2, pieces.get(dest)); - return [diff, false]; + if (pos[1] > 5) { + computedWidth = + width * 0.4546 + squareDimensions.width * (pos[0] - 5) - 0.5 * (pos[1] - 5) * squareDimensions.width; + } else if (pos[1] < 5) { + computedWidth = + width * 0.4546 - squareDimensions.width * (5 - pos[0]) + 0.5 * (5 - pos[1]) * squareDimensions.width; + } else { + if (pos[0] >= 5) { + computedWidth = width * 0.4546 + squareDimensions.width * (pos[0] - 5); + } else if (pos[0] < 5) { + computedWidth = width * 0.4546 - squareDimensions.width * (5 - pos[0]); + } } - } + return [computedWidth, computedHeight]; + }, + p2: (pos: cg.Pos, bounds: ClientRect) => { + const height = bounds.height; + const width = bounds.width; + const squareDimensions = getSquareDimensions(bounds); - // side move - const overAllDirection = directionString as DiagonalDirectionString; - for (const lineDir of candidateLineDirs(overAllDirection as DiagonalDirectionString)) { - const sideDirs = deducePotentialSideDirs(directionString as DiagonalDirectionString, lineDir); - const secondPos = move(orig, lineDir); - if (secondPos === undefined) continue; - for (const sideDir of sideDirs) { - const side2ndPos = move(secondPos, sideDir); - if (side2ndPos) { - const side1stPos = move(orig, sideDir); - if (side1stPos === undefined) continue; - if (side1stPos && pieces.get(secondPos)) { - if (side2ndPos === dest) { - diff.set(side1stPos, pieces.get(orig)); - diff.set(orig, undefined); - diff.set(dest, pieces.get(secondPos)); - diff.set(secondPos, undefined); - return [diff, false]; - } else { - // 3 marbles are moving - const thirdPos = move(secondPos, lineDir); - if (thirdPos === undefined) continue; - const side3rdPos = move(thirdPos, sideDir); - if (side3rdPos === undefined) continue; - if (pieces.get(thirdPos) && side3rdPos === dest) { - diff.set(side1stPos, pieces.get(orig)); - diff.set(orig, undefined); - diff.set(side2ndPos, pieces.get(secondPos)); - diff.set(secondPos, undefined); - diff.set(side3rdPos, pieces.get(thirdPos)); - diff.set(thirdPos, undefined); - return [diff, false]; - } - } - } + const computedHeight = height * 0.4546 + squareDimensions.height * (pos[1] - 5); + let computedWidth = width * 0.4546 + squareDimensions.width * (5 - pos[0]); + + if (pos[1] < 5) { + computedWidth = + width * 0.4546 + squareDimensions.width * (5 - pos[0]) - 0.5 * (5 - pos[1]) * squareDimensions.width; + } else if (pos[1] > 5) { + computedWidth = + width * 0.4546 - squareDimensions.width * (pos[0] - 5) + 0.5 * (pos[1] - 5) * squareDimensions.width; + } else { + if (pos[0] <= 5) { + computedWidth = width * 0.4546 + squareDimensions.width * (5 - pos[0]); + } else if (pos[0] > 5) { + computedWidth = width * 0.4546 - squareDimensions.width * (pos[0] - 5); } } + return [computedWidth, computedHeight]; + }, + right: (pos: cg.Pos, bounds: ClientRect) => [(pos[1] - 1) * bounds.x, (pos[0] - 1) * bounds.x], + left: (pos: cg.Pos, bounds: ClientRect) => [(pos[1] - 1) * bounds.x, (pos[0] - 1) * bounds.x], + p1vflip: (pos: cg.Pos, bounds: ClientRect) => [(pos[1] - 1) * bounds.x, (pos[0] - 1) * bounds.x], +}; +export const posToTranslateRel2 = (_bounds: ClientRect, pos: cg.Pos, orientation: cg.Orientation): cg.NumberPair => { + return translateBase[orientation](pos, 100, 100, { width: 9, height: 9 }); +}; + +export const getSquareDimensions = (bounds: ClientRect): SquareDimensions => ({ + width: bounds.width * 0.093, + height: bounds.height * 0.081, +}); + +export const getCoordinates = (x: number, y: number, orientation: cg.Orientation): cg.Key => { + const file = abaloneFiles[x] as cg.File; + const rank = abaloneRanks[y] as cg.Rank; + if (orientation === 'p1') { + return (file + rank) as cg.Key; } - return [diff, false]; + return rotate180(file, rank) as cg.Key; +}; + +const rotate180 = (file: string, rank: string): string => { + const files = 'abcdefghi'; + const ranks = '123456789'; + const rotatedFile = files[files.length - 1 - files.indexOf(file)]; + const rotatedRank = ranks[ranks.length - 1 - ranks.indexOf(rank)]; + return rotatedFile + rotatedRank; }; diff --git a/src/wrap.ts b/src/wrap.ts index d219159..28a4cc4 100644 --- a/src/wrap.ts +++ b/src/wrap.ts @@ -147,9 +147,13 @@ export function renderWrap(element: HTMLElement, s: HeadlessState, relative: boo } } } else if (s.variant === 'abalone') { - // @TODO VFR: probably can use the default else below - check - container.appendChild(renderCoords(['1', '2', '3', '4', '5', '6', '7', '8', '9'], 'ranks' + orientClass)); - container.appendChild(renderCoords(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'], 'files' + orientClass)); + if (s.orientation === 'p1') { + container.appendChild(renderCoords(ranks.slice(0, s.dimensions.width), 'ranks' + orientClass)); + container.appendChild(renderCoords(files.slice(0, s.dimensions.width).reverse(), 'files' + orientClass)); + } else { + container.appendChild(renderCoords(ranks.slice(0, s.dimensions.width).reverse(), 'ranks' + orientClass)); + container.appendChild(renderCoords(files.slice(0, s.dimensions.width), 'files' + orientClass)); + } } else { container.appendChild(renderCoords(ranks19.slice(0, s.dimensions.height), 'ranks' + orientClass)); container.appendChild(renderCoords(files.slice(0, s.dimensions.width), 'files' + orientClass)); @@ -181,10 +185,6 @@ export function renderWrap(element: HTMLElement, s: HeadlessState, relative: boo const boardScores = calculateBackgammonScores(s.pieces, s.pocketPieces, s.dimensions); container.appendChild(renderBoardScores(boardScores.p1.toString(), 'p1')); container.appendChild(renderBoardScores(boardScores.p2.toString(), 'p2')); - } else if (s.variant === 'abalone') { - const boardScores = calculateBackgammonScores(s.pieces, s.pocketPieces, s.dimensions); // @TODO VFR: ADAPT - container.appendChild(renderBoardScores(boardScores.p1.toString(), 'p1')); - container.appendChild(renderBoardScores(boardScores.p2.toString(), 'p2')); } else { container.appendChild(renderBoardScores('0', s.orientation)); } From 1ca06ed555765d337c774fd79b8ad6117e82b2a5 Mon Sep 17 00:00:00 2001 From: JamesHeppell Date: Wed, 18 Dec 2024 15:27:39 +0000 Subject: [PATCH 12/22] support coordinates for abalone --- src/variants/abalone/config.ts | 1 - src/wrap.ts | 24 ++++++++++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/variants/abalone/config.ts b/src/variants/abalone/config.ts index 88d4964..f44afce 100644 --- a/src/variants/abalone/config.ts +++ b/src/variants/abalone/config.ts @@ -9,6 +9,5 @@ export const configure = (state: HeadlessState): void => { state.render = render; // these below could just have been overriden by a config object - state.coordinates = true; // same as default state.animation.enabled = false; }; diff --git a/src/wrap.ts b/src/wrap.ts index 28a4cc4..50eb905 100644 --- a/src/wrap.ts +++ b/src/wrap.ts @@ -148,11 +148,27 @@ export function renderWrap(element: HTMLElement, s: HeadlessState, relative: boo } } else if (s.variant === 'abalone') { if (s.orientation === 'p1') { - container.appendChild(renderCoords(ranks.slice(0, s.dimensions.width), 'ranks' + orientClass)); - container.appendChild(renderCoords(files.slice(0, s.dimensions.width).reverse(), 'files' + orientClass)); + container.appendChild(renderCoords(ranks.slice(0, 5), 'files' + orientClass + ' rank-1')); + container.appendChild(renderCoords(['6'], 'files' + orientClass + ' rank-2')); + container.appendChild(renderCoords(['7'], 'files' + orientClass + ' rank-3')); + container.appendChild(renderCoords(['8'], 'files' + orientClass + ' rank-4')); + container.appendChild(renderCoords(['9'], 'files' + orientClass + ' rank-5')); + container.appendChild(renderCoords(['e'], 'ranks' + orientClass + ' file-1')); + container.appendChild(renderCoords(['d', 'f'], 'ranks' + orientClass + ' file-2')); + container.appendChild(renderCoords(['c', 'g'], 'ranks' + orientClass + ' file-3')); + container.appendChild(renderCoords(['b', '', 'h'], 'ranks' + orientClass + ' file-4')); + container.appendChild(renderCoords(['a', '', '', '', 'i'], 'ranks' + orientClass + ' file-5')); } else { - container.appendChild(renderCoords(ranks.slice(0, s.dimensions.width).reverse(), 'ranks' + orientClass)); - container.appendChild(renderCoords(files.slice(0, s.dimensions.width), 'files' + orientClass)); + container.appendChild(renderCoords(ranks.slice(0, 5), 'files' + orientClass + ' rank-1')); + container.appendChild(renderCoords(['6'], 'files' + orientClass + ' rank-2')); + container.appendChild(renderCoords(['7'], 'files' + orientClass + ' rank-3')); + container.appendChild(renderCoords(['8'], 'files' + orientClass + ' rank-4')); + container.appendChild(renderCoords(['9'], 'files' + orientClass + ' rank-5')); + container.appendChild(renderCoords(['e'], 'ranks' + orientClass + ' file-1')); + container.appendChild(renderCoords(['d', 'f'], 'ranks' + orientClass + ' file-2')); + container.appendChild(renderCoords(['c', 'g'], 'ranks' + orientClass + ' file-3')); + container.appendChild(renderCoords(['b', '', 'h'], 'ranks' + orientClass + ' file-4')); + container.appendChild(renderCoords(['a', '', '', '', 'i'], 'ranks' + orientClass + ' file-5')); } } else { container.appendChild(renderCoords(ranks19.slice(0, s.dimensions.height), 'ranks' + orientClass)); From af730f920d64b7c860fd50d8d594d9699e8d90c7 Mon Sep 17 00:00:00 2001 From: Vincent FROCHOT Date: Thu, 19 Dec 2024 04:10:11 +0100 Subject: [PATCH 13/22] feat: use HOF for svg shapes some functions are still not as HOF, thus it still needs a bit more work --- src/drag.ts | 2 +- src/render.ts | 3 +- src/state.ts | 12 +- src/svg.ts | 26 ++-- src/util.ts | 2 +- src/variants/abalone/config.ts | 11 ++ src/variants/abalone/directions.ts | 5 +- src/variants/abalone/drag.ts | 4 +- src/variants/abalone/draw.ts | 49 ------- src/variants/abalone/render.ts | 24 ++-- src/variants/abalone/svg.ts | 197 +++++------------------------ src/variants/abalone/util.ts | 13 +- 12 files changed, 93 insertions(+), 255 deletions(-) delete mode 100644 src/variants/abalone/draw.ts diff --git a/src/drag.ts b/src/drag.ts index 28c6374..6aca6de 100644 --- a/src/drag.ts +++ b/src/drag.ts @@ -175,7 +175,7 @@ function processDrag(s: State): void { cur.pos = [cur.epos[0] - cur.rel[0], cur.epos[1] - cur.rel[1]]; // move piece - const translation = util.posToTranslateAbs(s.dom.bounds(), s.dimensions, s.variant)(cur.origPos, s.orientation); + const translation = util.posToTranslateAbs(s.dom.bounds(), s.dimensions, s.variant)(cur.origPos, s.orientation); // because of util.translateAbs, it has to remain invoked from util. translation[0] += cur.pos[0] + cur.dec[0]; translation[1] += cur.pos[1] + cur.dec[1]; util.translateAbs(cur.element, translation); diff --git a/src/render.ts b/src/render.ts index fdb39b1..f937b0a 100644 --- a/src/render.ts +++ b/src/render.ts @@ -2,7 +2,6 @@ import { State } from './state'; import { key2pos, createEl, - posToTranslateRel, posToTranslateAbs, translateRel, translateAbs, @@ -22,7 +21,7 @@ export type SquareClasses = Map; export function render(s: State): void { const orientation = s.orientation, asP1: boolean = p1Pov(s), - posToTranslate = s.dom.relative ? posToTranslateRel : posToTranslateAbs(s.dom.bounds(), s.dimensions, s.variant), + posToTranslate = s.dom.relative ? s.posToTranslateRelative : s.posToTranslateAbsolute(s.dom.bounds(), s.dimensions, s.variant), translate = s.dom.relative ? translateRel : translateAbs, boardEl: HTMLElement = s.dom.elements.board, pieces: cg.Pieces = s.pieces, diff --git a/src/state.ts b/src/state.ts index 5caf827..86af21e 100644 --- a/src/state.ts +++ b/src/state.ts @@ -4,7 +4,9 @@ import { baseMove } from './board'; import { DragCurrent } from './drag'; import { Drawable } from './draw'; import { render } from './render'; -import { timer } from './util'; +import { posToTranslateAbs, posToTranslateRel, timer } from './util'; +import { pos2px } from './svg'; +import { key2pos } from './util'; import * as cg from './types'; export interface HeadlessState { @@ -140,6 +142,10 @@ export interface HeadlessState { singleClickMoveVariant: boolean; baseMove: (state: HeadlessState, orig: cg.Key, dest: cg.Key) => cg.Piece | boolean; render: (state: State) => void; + posToTranslateRelative: (pos: cg.Pos, orientation: cg.Orientation, bt: cg.BoardDimensions, v: cg.Variant) => cg.NumberPair; + posToTranslateAbsolute: (bounds: ClientRect, bt: cg.BoardDimensions, variant: cg.Variant) => (pos: cg.Pos, orientation: cg.Orientation) => cg.NumberPair; + pos2px: (pos: cg.Pos, bounds: ClientRect, bd: cg.BoardDimensions) => cg.NumberPair; + key2pos: (k: cg.Key) => cg.Pos; } export interface State extends HeadlessState { @@ -254,5 +260,9 @@ export function defaults(): HeadlessState { singleClickMoveVariant: false, baseMove, render, + posToTranslateRelative: posToTranslateRel, + posToTranslateAbsolute: posToTranslateAbs, + pos2px, + key2pos }; } diff --git a/src/svg.ts b/src/svg.ts index d2e198a..5d5a4c4 100644 --- a/src/svg.ts +++ b/src/svg.ts @@ -1,5 +1,4 @@ import { State } from './state'; -import { key2pos } from './util'; import { Drawable, DrawShape, DrawShapePiece, DrawBrush, DrawBrushes, DrawModifiers } from './draw'; import * as cg from './types'; import * as T from './transformations'; @@ -187,12 +186,13 @@ function renderShape( ): SVGElement { let el: SVGElement; if (shape.customSvg) { - const orig = orient(key2pos(shape.orig), state.orientation, state.dimensions); + const orig = orient(state.key2pos(shape.orig), state.orientation, state.dimensions); el = renderCustomSvg(shape.customSvg, orig, bounds, state.dimensions); } else if (shape.piece) el = renderPiece( + state, state.drawable.pieces.baseUrl, - orient(key2pos(shape.orig), state.orientation, state.dimensions), + orient(state.key2pos(shape.orig), state.orientation, state.dimensions), shape.piece, bounds, state.dimensions, @@ -200,20 +200,21 @@ function renderShape( state.variant, ); else { - const orig = orient(key2pos(shape.orig), state.orientation, state.dimensions); + const orig = orient(state.key2pos(shape.orig), state.orientation, state.dimensions); if (shape.orig && shape.dest) { let brush: DrawBrush = brushes[shape.brush!]; if (shape.modifiers) brush = makeCustomBrush(brush, shape.modifiers); el = renderArrow( + state, brush, orig, - orient(key2pos(shape.dest), state.orientation, state.dimensions), + orient(state.key2pos(shape.dest), state.orientation, state.dimensions), current, (arrowDests.get(shape.dest) || 0) > 1, bounds, state.dimensions, ); - } else el = renderCircle(brushes[shape.brush!], orig, current, bounds, state.dimensions); + } else el = renderCircle(state, brushes[shape.brush!], orig, current, bounds, state.dimensions); } el.setAttribute('cgHash', hash); return el; @@ -238,13 +239,14 @@ function renderCustomSvg(customSvg: string, pos: cg.Pos, bounds: ClientRect, bd: } function renderCircle( + state: State, brush: DrawBrush, pos: cg.Pos, current: boolean, bounds: ClientRect, bd: cg.BoardDimensions, ): SVGElement { - const o = pos2px(pos, bounds, bd), + const o = state.pos2px(pos, bounds, bd), widths = circleWidth(bounds, bd), radius = (bounds.width + bounds.height) / (2 * (bd.height + bd.width)); return setAttributes(createElement('circle'), { @@ -259,6 +261,7 @@ function renderCircle( } function renderArrow( + state: State, brush: DrawBrush, orig: cg.Pos, dest: cg.Pos, @@ -268,8 +271,8 @@ function renderArrow( bd: cg.BoardDimensions, ): SVGElement { const m = arrowMargin(bounds, shorten && !current, bd), - a = pos2px(orig, bounds, bd), - b = pos2px(dest, bounds, bd), + a = state.pos2px(orig, bounds, bd), + b = state.pos2px(dest, bounds, bd), dx = b[0] - a[0], dy = b[1] - a[1], angle = Math.atan2(dy, dx), @@ -289,6 +292,7 @@ function renderArrow( } function renderPiece( + state: State, baseUrl: string, pos: cg.Pos, piece: DrawShapePiece, @@ -297,7 +301,7 @@ function renderPiece( myPlayerIndex: cg.PlayerIndex, variant: cg.Variant, ): SVGElement { - const o = pos2px(pos, bounds, bd), + const o = state.pos2px(pos, bounds, bd), width = (bounds.width / bd.width) * (piece.scale || 1), height = (bounds.height / bd.height) * (piece.scale || 1), //name = piece.playerIndex[0] + piece.role[0].toUpperCase(); @@ -370,7 +374,7 @@ export function arrowMargin(bounds: ClientRect, shorten: boolean, bd: cg.BoardDi return ((shorten ? 20 : 10) / (bd.width * 64)) * bounds.width; } -function pos2px(pos: cg.Pos, bounds: ClientRect, bd: cg.BoardDimensions): cg.NumberPair { +export function pos2px(pos: cg.Pos, bounds: ClientRect, bd: cg.BoardDimensions): cg.NumberPair { return [((pos[0] - 0.5) * bounds.width) / bd.width, ((bd.height + 0.5 - pos[1]) * bounds.height) / bd.height]; } diff --git a/src/util.ts b/src/util.ts index c34e522..c1380c0 100644 --- a/src/util.ts +++ b/src/util.ts @@ -176,7 +176,7 @@ export const posToTranslateRel = ( ): cg.NumberPair => { if (v === 'abalone') { // "working" WIP: have to use HOF - return abalonePosToTranslateRel(pos, orientation); + return abalonePosToTranslateRel(pos, orientation, bt, v); } return posToTranslateBase( pos, diff --git a/src/variants/abalone/config.ts b/src/variants/abalone/config.ts index f44afce..c0d18ae 100644 --- a/src/variants/abalone/config.ts +++ b/src/variants/abalone/config.ts @@ -1,12 +1,23 @@ import type { HeadlessState } from '../../state'; +import type * as cg from '../../types'; import { baseMove } from './engine'; import { render } from './render'; +import { pos2px } from './svg'; +import { key2pos, posToTranslateAbs2 as posToTranslateAbs2Original, posToTranslateRel } from './util'; + +const posToTranslateAbs2 = (bounds: ClientRect, _bt: cg.BoardDimensions, _variant: cg.Variant) => + (pos: cg.Pos, orientation: "p1" | "p2" | "left" | "right" | "p1vflip") => + posToTranslateAbs2Original()(bounds, pos, orientation); export const configure = (state: HeadlessState): void => { // HOF state.baseMove = baseMove; state.render = render; + state.posToTranslateRelative = posToTranslateRel; + state.posToTranslateAbsolute = posToTranslateAbs2; + state.pos2px = pos2px; + state.key2pos = key2pos; // these below could just have been overriden by a config object state.animation.enabled = false; diff --git a/src/variants/abalone/directions.ts b/src/variants/abalone/directions.ts index d8ef6a4..59ec517 100644 --- a/src/variants/abalone/directions.ts +++ b/src/variants/abalone/directions.ts @@ -1,6 +1,7 @@ import type * as cg from '../../types'; import type { DirectionString } from './types'; +import { isValidKey } from './util'; export enum DiagonalDirectionString { UpLeft = 'NW', @@ -145,10 +146,6 @@ const traverseUntil = (pos: cg.Key, stop: (pos: cg.Key) => boolean, direction: D } }; -const isValidKey = (key: cg.Key): boolean => { - return /^(a[1-5]|b[1-6]|c[1-7]|d[1-8]|e[1-9]|f[2-9]|g[3-9]|h[4-9]|i[5-9])$/.test(key); -}; - const directionMappings: { [key in DirectionString]: (key: cg.Key) => cg.Key } = { NW: (key: cg.Key) => (String.fromCharCode(key[0].charCodeAt(0)) + (parseInt(key[1]) + 1).toString()) as cg.Key, NE: (key: cg.Key) => (String.fromCharCode(key[0].charCodeAt(0) + 1) + (parseInt(key[1]) + 1).toString()) as cg.Key, diff --git a/src/variants/abalone/drag.ts b/src/variants/abalone/drag.ts index bbcb22a..d383831 100644 --- a/src/variants/abalone/drag.ts +++ b/src/variants/abalone/drag.ts @@ -29,10 +29,10 @@ export function processDrag(s: State): void { cur.pos = [cur.epos[0] - cur.rel[0], cur.epos[1] - cur.rel[1]]; // move piece - const translation = posToTranslateAbs(s.dom.bounds(), s.dimensions, 'chess')(cur.origPos, s.orientation); // "working" WIP: have to use HOF + const translation = posToTranslateAbs(s.dom.bounds(), s.dimensions, 'chess')(cur.origPos, s.orientation); // because of translateAbs, it has to remain invoked from util. translation[0] += cur.pos[0] + cur.dec[0]; translation[1] += cur.pos[1] + cur.dec[1]; - translateAbs(cur.element, translation); + translateAbs(cur.element, translation); // "working" WIP: have to use HOF } } processDrag(s); diff --git a/src/variants/abalone/draw.ts b/src/variants/abalone/draw.ts deleted file mode 100644 index 854032a..0000000 --- a/src/variants/abalone/draw.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type * as cg from '../../types'; -import { cancelMove, unselect } from '../../board'; -import { eventBrush } from '../../draw'; -import { State } from '../../state'; -import { eventPosition } from '../../util'; - -import { getKeyAtDomPos, getSnappedKeyAtDomPos } from './board'; - -// @TODO: using HOF would allow not repeating the same code here. This code is generic, not Abalone specific. We just invoke getKeyAtDomPos and getSnappedKeyAtDomPos - -export const start = (state: State, e: cg.MouchEvent): void => { - // support one finger touch only - if (e.touches && e.touches.length > 1) return; - e.stopPropagation(); - e.preventDefault(); - e.ctrlKey ? unselect(state) : cancelMove(state); - const pos = eventPosition(e)!, - orig = getKeyAtDomPos(pos, state.orientation, state.dom.bounds()); - if (!orig) return; - state.drawable.current = { - orig, - pos, - brush: eventBrush(e), - snapToValidMove: state.drawable.defaultSnapToValidMove, - }; - - processDraw(state); -}; - -export const processDraw = (state: State): void => { - requestAnimationFrame(() => { - const cur = state.drawable.current; - if (cur) { - const keyAtDomPos = getKeyAtDomPos(cur.pos, state.orientation, state.dom.bounds()); - if (!keyAtDomPos) { - cur.snapToValidMove = false; - } - const mouseSq = cur.snapToValidMove - ? getSnappedKeyAtDomPos(cur.orig, cur.pos, state.orientation, state.dom.bounds(), state.dimensions) - : keyAtDomPos; - if (mouseSq !== cur.mouseSq) { - cur.mouseSq = mouseSq; - cur.dest = mouseSq !== cur.orig ? mouseSq : undefined; - state.dom.redrawNow(); - } - processDraw(state); - } - }); -}; diff --git a/src/variants/abalone/render.ts b/src/variants/abalone/render.ts index 3d11df6..ea2d1e4 100644 --- a/src/variants/abalone/render.ts +++ b/src/variants/abalone/render.ts @@ -7,14 +7,14 @@ import { AnimCurrent, AnimFadings, AnimVector, AnimVectors } from '../../anim'; import { DragCurrent } from '../../drag'; import { appendValue, isPieceNode, isSquareNode, posZIndex, removeNodes } from '../../render'; -import { key2pos, translateAbs, translateRel, posToTranslateAbs2, posToTranslateRel2 } from './util'; +import { translateAbs, translateRel } from './util'; import { computeMoveVectorPostMove } from './engine'; // @TODO: remove parts unrelated to Abalone export const render = (s: State): void => { const orientation = s.orientation, asP1: boolean = p1Pov(s), - posToTranslate = s.dom.relative ? posToTranslateRel2 : posToTranslateAbs2(), + posToTranslate = s.dom.relative ? s.posToTranslateRelative : s.posToTranslateAbsolute(s.dom.bounds(), s.dimensions, s.variant), translate = s.dom.relative ? translateRel : translateAbs, boardEl: HTMLElement = s.dom.elements.board, pieces: cg.Pieces = s.pieces, @@ -52,7 +52,7 @@ export const render = (s: State): void => { // if piece not being dragged anymore, remove dragging style if (el.cgDragging && (!curDrag || curDrag.orig !== k)) { el.classList.remove('dragging'); - translate(el, posToTranslate(s.dom.bounds(), key2pos(k), orientation)); + translate(el, posToTranslate(s.key2pos(k), orientation, s.dimensions, s.variant)); el.cgDragging = false; } // remove fading class if it still remains @@ -65,16 +65,16 @@ export const render = (s: State): void => { // continue animation if already animating and same piece // (otherwise it could animate a captured piece) if (anim && el.cgAnimating && elPieceName === pieceNameOf(pieceAtKey, s.myPlayerIndex, s.orientation)) { - const pos = key2pos(k); + const pos = s.key2pos(k); pos[0] += anim[2]; pos[1] += anim[3]; el.classList.add('anim'); - translate(el, posToTranslate(s.dom.bounds(), pos, orientation)); + translate(el, posToTranslate(s.key2pos(k), orientation, s.dimensions, s.variant)); } else if (el.cgAnimating) { el.cgAnimating = false; el.classList.remove('anim'); - translate(el, posToTranslate(s.dom.bounds(), key2pos(k), orientation)); - if (s.addPieceZIndex) el.style.zIndex = posZIndex(key2pos(k), orientation, asP1, s.dimensions); + translate(el, posToTranslate(s.key2pos(k), orientation, s.dimensions, s.variant)); + if (s.addPieceZIndex) el.style.zIndex = posZIndex(s.key2pos(k), orientation, asP1, s.dimensions); } // same piece: flag as same if (elPieceName === pieceNameOf(pieceAtKey, s.myPlayerIndex, s.orientation) && (!fading || !el.cgFading)) { @@ -109,7 +109,7 @@ export const render = (s: State): void => { // if (!sameSquares.has(sk)) { sMvdset = movedSquares.get(className); sMvd = sMvdset && sMvdset.pop(); - const translation = posToTranslate(s.dom.bounds(), key2pos(sk), orientation); + const translation = posToTranslate(s.key2pos(sk), orientation, s.dimensions, s.variant); if (sMvd) { sMvd.cgKey = sk; translate(sMvd, translation); @@ -137,7 +137,7 @@ export const render = (s: State): void => { pMvd.classList.remove('fading'); pMvd.cgFading = false; } - const pos = key2pos(k); + const pos = s.key2pos(k); if (s.addPieceZIndex) pMvd.style.zIndex = posZIndex(pos, orientation, asP1, s.dimensions); if (anim) { pMvd.cgAnimating = true; @@ -145,14 +145,14 @@ export const render = (s: State): void => { pos[0] += anim[2]; pos[1] += anim[3]; } - translate(pMvd, posToTranslate(s.dom.bounds(), pos, orientation)); + translate(pMvd, posToTranslate(pos, orientation, s.dimensions, s.variant)); } // no piece in moved obj: insert the new piece // assumes the new piece is not being dragged else { const pieceName = pieceNameOf(p, s.myPlayerIndex, s.orientation), pieceNode = createEl('piece', pieceName) as cg.PieceNode, - pos = key2pos(k); // used here to compute position + pos = s.key2pos(k); // used here to compute position pieceNode.cgPiece = pieceName; pieceNode.cgKey = k; @@ -161,7 +161,7 @@ export const render = (s: State): void => { pos[0] += anim[2]; pos[1] += anim[3]; } - translate(pieceNode, posToTranslate(s.dom.bounds(), pos, orientation)); + translate(pieceNode, posToTranslate(pos, orientation, s.dimensions, s.variant)); if (s.addPieceZIndex) pieceNode.style.zIndex = posZIndex(pos, orientation, asP1, s.dimensions); diff --git a/src/variants/abalone/svg.ts b/src/variants/abalone/svg.ts index 812ea1b..e47633b 100644 --- a/src/variants/abalone/svg.ts +++ b/src/variants/abalone/svg.ts @@ -1,172 +1,33 @@ import type * as cg from '../../types'; -import { DrawShapePiece, DrawBrush, DrawBrushes } from '../../draw'; -import { State } from '../../state'; -import { - ArrowDests, - Shape, - arrowMargin, - circleWidth, - createElement, - lineWidth, - makeCustomBrush, - opacity, - orient, - setAttributes, -} from '../../svg'; -import { key2pos } from './util'; - -// @TODO: using HOF would probably fix the arrows etc. Currently not working. +import { abaloneFiles, getSquareDimensions, isValidKey } from './util'; // pos2px is used to convert a position from the grid (board coordinates) to a position in pixels -const pos2px = (pos: cg.Pos, bounds: ClientRect, bd: cg.BoardDimensions): cg.NumberPair => { - return [ - ((pos[0] * 4 + pos[0] - 0.5) * bounds.width) / bd.width, - ((bd.height + 0.5 - pos[1]) * bounds.height) / bd.height, - ]; -}; - -// @TODO: remove the lines below -// function pos2px(pos: cg.Pos, bounds: ClientRect, bd: cg.BoardDimensions): cg.NumberPair { -// return [((pos[0] - 0.5) * bounds.width) / bd.width, ((bd.height + 0.5 - pos[1]) * bounds.height) / bd.height]; -// } - -const roleToSvgName = (piece: DrawShapePiece): string => { - return (piece.playerIndex === 'p1' ? 'b' : 'w') + piece.role[0].toUpperCase(); -}; - -export const renderPiece = ( - baseUrl: string, - pos: cg.Pos, - piece: DrawShapePiece, - bounds: ClientRect, - bd: cg.BoardDimensions, - myPlayerIndex: cg.PlayerIndex, -): SVGElement => { - const o = pos2px(pos, bounds, bd), - width = (bounds.width / bd.width) * (piece.scale || 1), - height = (bounds.height / bd.height) * (piece.scale || 1), - //name = piece.playerIndex[0] + piece.role[0].toUpperCase(); - name = roleToSvgName(piece); - // If baseUrl doesn't end with '/' use it as full href - // This is needed when drop piece suggestion .svg image file names are different than "name" produces - const href = baseUrl.endsWith('/') ? baseUrl.slice('https://playstrategy.org'.length) + name + '.svg' : baseUrl; - const side = piece.playerIndex === myPlayerIndex ? 'ally' : 'enemy'; - return setAttributes(createElement('image'), { - className: `${piece.role} ${piece.playerIndex} ${side}`, - x: o[0] - width / 2, - y: o[1] - height / 2, - width: width, - height: height, - href: href, - }); -}; - -export const renderShape = ( - state: State, - { shape, current, hash }: Shape, - brushes: DrawBrushes, - arrowDests: ArrowDests, - bounds: ClientRect, -): SVGElement => { - let el: SVGElement; - if (shape.customSvg) { - const orig = orient(key2pos(shape.orig), state.orientation, state.dimensions); - el = renderCustomSvg(shape.customSvg, orig, bounds, state.dimensions); - } else if (shape.piece) - el = renderPiece( - state.drawable.pieces.baseUrl, - orient(key2pos(shape.orig), state.orientation, state.dimensions), - shape.piece, - bounds, - state.dimensions, - state.myPlayerIndex, - ); - else { - const orig = orient(key2pos(shape.orig), state.orientation, state.dimensions); - if (shape.orig && shape.dest) { - let brush: DrawBrush = brushes[shape.brush!]; - if (shape.modifiers) brush = makeCustomBrush(brush, shape.modifiers); - el = renderArrow( - brush, - orig, - orient(key2pos(shape.dest), state.orientation, state.dimensions), - current, - (arrowDests.get(shape.dest) || 0) > 1, - bounds, - state.dimensions, - ); - } else el = renderCircle(brushes[shape.brush!], orig, current, bounds, state.dimensions); - } - el.setAttribute('cgHash', hash); - return el; -}; - -function renderCircle( - brush: DrawBrush, - pos: cg.Pos, - current: boolean, - bounds: ClientRect, - bd: cg.BoardDimensions, -): SVGElement { - const o = pos2px(pos, bounds, bd), - widths = circleWidth(bounds, bd), - radius = (bounds.width + bounds.height) / (2 * (bd.height + bd.width)); - return setAttributes(createElement('circle'), { - stroke: brush.color, - 'stroke-width': widths[current ? 0 : 1], - fill: 'none', - opacity: opacity(brush, current), - cx: o[0], - cy: o[1], - r: radius - widths[1] / 2, - }); -} - -function renderArrow( - brush: DrawBrush, - orig: cg.Pos, - dest: cg.Pos, - current: boolean, - shorten: boolean, - bounds: ClientRect, - bd: cg.BoardDimensions, -): SVGElement { - const m = arrowMargin(bounds, shorten && !current, bd), - a = pos2px(orig, bounds, bd), - b = pos2px(dest, bounds, bd), - dx = b[0] - a[0], - dy = b[1] - a[1], - angle = Math.atan2(dy, dx), - xo = Math.cos(angle) * m, - yo = Math.sin(angle) * m; - return setAttributes(createElement('line'), { - stroke: brush.color, - 'stroke-width': lineWidth(brush, current, bounds, bd), - 'stroke-linecap': 'round', - 'marker-end': 'url(#arrowhead-' + brush.key + ')', - opacity: opacity(brush, current), - x1: a[0], - y1: a[1], - x2: b[0] - xo, - y2: b[1] - yo, - }); -} - -function renderCustomSvg(customSvg: string, pos: cg.Pos, bounds: ClientRect, bd: cg.BoardDimensions): SVGElement { - const { width, height } = bounds; - const w = width / bd.width; - const h = height / bd.height; - const x = (pos[0] - 1) * w; - const y = (bd.height - pos[1]) * h; - - // Translate to top-left of `orig` square - const g = setAttributes(createElement('g'), { transform: `translate(${x},${y})` }); - - // Give 100x100 coordinate system to the user for `orig` square - const svg = setAttributes(createElement('svg'), { width: w, height: h, viewBox: '0 0 100 100' }); - - g.appendChild(svg); - svg.innerHTML = customSvg; - return g; -} +export const pos2px = (pos: cg.Pos, bounds: ClientRect, _bd: cg.BoardDimensions): cg.NumberPair => { + const height = bounds.height; + const width = bounds.width; + const squareDimensions = getSquareDimensions(bounds); + + const computedHeight = height * 0.5 + squareDimensions.height * (5 - pos[1]); + let computedWidth = width * 0.5 + squareDimensions.width * (pos[0] - 5); + + if (!isValidKey(abaloneFiles[pos[0] - 1] + pos[1].toString() as cg.Key)) { + return [10, 10]; + } + + if (pos[1] > 5) { + computedWidth = + width * 0.4546 + squareDimensions.width * (pos[0] - 5) - 0.5 * (pos[1] - 5) * squareDimensions.width; + } else if (pos[1] < 5) { + computedWidth = + width * 0.4546 - squareDimensions.width * (5 - pos[0]) + 0.5 * (5 - pos[1]) * squareDimensions.width; + } else { + if (pos[0] >= 5) { + computedWidth = width * 0.4546 + squareDimensions.width * (pos[0] - 5); + } else if (pos[0] < 5) { + computedWidth = width * 0.4546 - squareDimensions.width * (5 - pos[0]); + } + } + + return [computedWidth + width / 22, computedHeight]; +}; \ No newline at end of file diff --git a/src/variants/abalone/util.ts b/src/variants/abalone/util.ts index 561d65d..baedddf 100644 --- a/src/variants/abalone/util.ts +++ b/src/variants/abalone/util.ts @@ -2,7 +2,7 @@ import type * as cg from '../../types'; import * as T from '../../transformations'; import { SquareDimensions, TranslateBase } from './types'; -const abaloneFiles = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'] as const; +export const abaloneFiles = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'] as const; const abaloneRanks = ['1', '2', '3', '4', '5', '6', '7', '8', '9'] as const; @@ -164,7 +164,7 @@ export const posToTranslateAbs = ( return translateBase[orientation](pos, xFactor, yFactor, { width: 9, height: 9 }); }; -export const posToTranslateRel = (pos: cg.Pos, orientation: cg.Orientation): cg.NumberPair => { +export const posToTranslateRel = (pos: cg.Pos, orientation: cg.Orientation, _bt: cg.BoardDimensions, _v: cg.Variant): cg.NumberPair => { return translateBase[orientation](pos, 100, 100, { width: 9, height: 9 }); }; @@ -184,7 +184,7 @@ function ranks(n: number) { return abaloneRanks.slice(0, n); } -export function allKeys(bd: cg.BoardDimensions = { width: 9, height: 9 }) { +function allKeys(bd: cg.BoardDimensions = { width: 9, height: 9 }) { return Array.prototype.concat(...files(bd.width).map(c => ranks(bd.height).map(r => c + r))); } @@ -193,10 +193,11 @@ export const allPos = (bd: cg.BoardDimensions): cg.Pos[] => allKeys(bd).map(key2 export const posToTranslateBase2 = (bounds: ClientRect, pos: cg.Pos, orientation: cg.Orientation): cg.NumberPair => { return translateBase2[orientation](pos, bounds); }; + export const posToTranslateAbs2 = (): (( bounds: ClientRect, pos: cg.Pos, - orientation: cg.Orientation, + orientation: cg.Orientation ) => cg.NumberPair) => { return (bounds, pos, orientation) => posToTranslateBase2(bounds, pos, orientation); }; @@ -277,3 +278,7 @@ const rotate180 = (file: string, rank: string): string => { const rotatedRank = ranks[ranks.length - 1 - ranks.indexOf(rank)]; return rotatedFile + rotatedRank; }; + +export const isValidKey = (key: cg.Key): boolean => { + return /^(a[1-5]|b[1-6]|c[1-7]|d[1-8]|e[1-9]|f[2-9]|g[3-9]|h[4-9]|i[5-9])$/.test(key); +}; \ No newline at end of file From 6ae8d37a0f210973a39f25a4bb5dc8810eed0472 Mon Sep 17 00:00:00 2001 From: Vincent FROCHOT Date: Thu, 19 Dec 2024 04:11:28 +0100 Subject: [PATCH 14/22] chore: pnpm format --- src/render.ts | 13 ++++------ src/state.ts | 15 +++++++++--- src/variants/abalone/config.ts | 5 ++-- src/variants/abalone/drag.ts | 2 +- src/variants/abalone/render.ts | 4 +++- src/variants/abalone/svg.ts | 44 +++++++++++++++++----------------- src/variants/abalone/util.ts | 11 ++++++--- 7 files changed, 53 insertions(+), 41 deletions(-) diff --git a/src/render.ts b/src/render.ts index f937b0a..7adf0eb 100644 --- a/src/render.ts +++ b/src/render.ts @@ -1,12 +1,5 @@ import { State } from './state'; -import { - key2pos, - createEl, - posToTranslateAbs, - translateRel, - translateAbs, - calculatePlayerEmptyAreas, -} from './util'; +import { key2pos, createEl, posToTranslateAbs, translateRel, translateAbs, calculatePlayerEmptyAreas } from './util'; import { p1Pov } from './board'; import { AnimCurrent, AnimVectors, AnimVector, AnimFadings } from './anim'; import { DragCurrent } from './drag'; @@ -21,7 +14,9 @@ export type SquareClasses = Map; export function render(s: State): void { const orientation = s.orientation, asP1: boolean = p1Pov(s), - posToTranslate = s.dom.relative ? s.posToTranslateRelative : s.posToTranslateAbsolute(s.dom.bounds(), s.dimensions, s.variant), + posToTranslate = s.dom.relative + ? s.posToTranslateRelative + : s.posToTranslateAbsolute(s.dom.bounds(), s.dimensions, s.variant), translate = s.dom.relative ? translateRel : translateAbs, boardEl: HTMLElement = s.dom.elements.board, pieces: cg.Pieces = s.pieces, diff --git a/src/state.ts b/src/state.ts index 86af21e..516e4ea 100644 --- a/src/state.ts +++ b/src/state.ts @@ -142,8 +142,17 @@ export interface HeadlessState { singleClickMoveVariant: boolean; baseMove: (state: HeadlessState, orig: cg.Key, dest: cg.Key) => cg.Piece | boolean; render: (state: State) => void; - posToTranslateRelative: (pos: cg.Pos, orientation: cg.Orientation, bt: cg.BoardDimensions, v: cg.Variant) => cg.NumberPair; - posToTranslateAbsolute: (bounds: ClientRect, bt: cg.BoardDimensions, variant: cg.Variant) => (pos: cg.Pos, orientation: cg.Orientation) => cg.NumberPair; + posToTranslateRelative: ( + pos: cg.Pos, + orientation: cg.Orientation, + bt: cg.BoardDimensions, + v: cg.Variant, + ) => cg.NumberPair; + posToTranslateAbsolute: ( + bounds: ClientRect, + bt: cg.BoardDimensions, + variant: cg.Variant, + ) => (pos: cg.Pos, orientation: cg.Orientation) => cg.NumberPair; pos2px: (pos: cg.Pos, bounds: ClientRect, bd: cg.BoardDimensions) => cg.NumberPair; key2pos: (k: cg.Key) => cg.Pos; } @@ -263,6 +272,6 @@ export function defaults(): HeadlessState { posToTranslateRelative: posToTranslateRel, posToTranslateAbsolute: posToTranslateAbs, pos2px, - key2pos + key2pos, }; } diff --git a/src/variants/abalone/config.ts b/src/variants/abalone/config.ts index c0d18ae..1671f0b 100644 --- a/src/variants/abalone/config.ts +++ b/src/variants/abalone/config.ts @@ -6,8 +6,9 @@ import { render } from './render'; import { pos2px } from './svg'; import { key2pos, posToTranslateAbs2 as posToTranslateAbs2Original, posToTranslateRel } from './util'; -const posToTranslateAbs2 = (bounds: ClientRect, _bt: cg.BoardDimensions, _variant: cg.Variant) => - (pos: cg.Pos, orientation: "p1" | "p2" | "left" | "right" | "p1vflip") => +const posToTranslateAbs2 = + (bounds: ClientRect, _bt: cg.BoardDimensions, _variant: cg.Variant) => + (pos: cg.Pos, orientation: 'p1' | 'p2' | 'left' | 'right' | 'p1vflip') => posToTranslateAbs2Original()(bounds, pos, orientation); export const configure = (state: HeadlessState): void => { diff --git a/src/variants/abalone/drag.ts b/src/variants/abalone/drag.ts index d383831..80cb880 100644 --- a/src/variants/abalone/drag.ts +++ b/src/variants/abalone/drag.ts @@ -32,7 +32,7 @@ export function processDrag(s: State): void { const translation = posToTranslateAbs(s.dom.bounds(), s.dimensions, 'chess')(cur.origPos, s.orientation); // because of translateAbs, it has to remain invoked from util. translation[0] += cur.pos[0] + cur.dec[0]; translation[1] += cur.pos[1] + cur.dec[1]; - translateAbs(cur.element, translation); // "working" WIP: have to use HOF + translateAbs(cur.element, translation); // "working" WIP: have to use HOF } } processDrag(s); diff --git a/src/variants/abalone/render.ts b/src/variants/abalone/render.ts index ea2d1e4..3e2ae17 100644 --- a/src/variants/abalone/render.ts +++ b/src/variants/abalone/render.ts @@ -14,7 +14,9 @@ import { computeMoveVectorPostMove } from './engine'; export const render = (s: State): void => { const orientation = s.orientation, asP1: boolean = p1Pov(s), - posToTranslate = s.dom.relative ? s.posToTranslateRelative : s.posToTranslateAbsolute(s.dom.bounds(), s.dimensions, s.variant), + posToTranslate = s.dom.relative + ? s.posToTranslateRelative + : s.posToTranslateAbsolute(s.dom.bounds(), s.dimensions, s.variant), translate = s.dom.relative ? translateRel : translateAbs, boardEl: HTMLElement = s.dom.elements.board, pieces: cg.Pieces = s.pieces, diff --git a/src/variants/abalone/svg.ts b/src/variants/abalone/svg.ts index e47633b..81d544d 100644 --- a/src/variants/abalone/svg.ts +++ b/src/variants/abalone/svg.ts @@ -4,30 +4,30 @@ import { abaloneFiles, getSquareDimensions, isValidKey } from './util'; // pos2px is used to convert a position from the grid (board coordinates) to a position in pixels export const pos2px = (pos: cg.Pos, bounds: ClientRect, _bd: cg.BoardDimensions): cg.NumberPair => { - const height = bounds.height; - const width = bounds.width; - const squareDimensions = getSquareDimensions(bounds); + const height = bounds.height; + const width = bounds.width; + const squareDimensions = getSquareDimensions(bounds); - const computedHeight = height * 0.5 + squareDimensions.height * (5 - pos[1]); - let computedWidth = width * 0.5 + squareDimensions.width * (pos[0] - 5); + const computedHeight = height * 0.5 + squareDimensions.height * (5 - pos[1]); + let computedWidth = width * 0.5 + squareDimensions.width * (pos[0] - 5); - if (!isValidKey(abaloneFiles[pos[0] - 1] + pos[1].toString() as cg.Key)) { - return [10, 10]; - } + if (!isValidKey((abaloneFiles[pos[0] - 1] + pos[1].toString()) as cg.Key)) { + return [10, 10]; + } - if (pos[1] > 5) { - computedWidth = - width * 0.4546 + squareDimensions.width * (pos[0] - 5) - 0.5 * (pos[1] - 5) * squareDimensions.width; - } else if (pos[1] < 5) { - computedWidth = - width * 0.4546 - squareDimensions.width * (5 - pos[0]) + 0.5 * (5 - pos[1]) * squareDimensions.width; - } else { - if (pos[0] >= 5) { - computedWidth = width * 0.4546 + squareDimensions.width * (pos[0] - 5); - } else if (pos[0] < 5) { - computedWidth = width * 0.4546 - squareDimensions.width * (5 - pos[0]); - } + if (pos[1] > 5) { + computedWidth = + width * 0.4546 + squareDimensions.width * (pos[0] - 5) - 0.5 * (pos[1] - 5) * squareDimensions.width; + } else if (pos[1] < 5) { + computedWidth = + width * 0.4546 - squareDimensions.width * (5 - pos[0]) + 0.5 * (5 - pos[1]) * squareDimensions.width; + } else { + if (pos[0] >= 5) { + computedWidth = width * 0.4546 + squareDimensions.width * (pos[0] - 5); + } else if (pos[0] < 5) { + computedWidth = width * 0.4546 - squareDimensions.width * (5 - pos[0]); } + } - return [computedWidth + width / 22, computedHeight]; -}; \ No newline at end of file + return [computedWidth + width / 22, computedHeight]; +}; diff --git a/src/variants/abalone/util.ts b/src/variants/abalone/util.ts index baedddf..3c53995 100644 --- a/src/variants/abalone/util.ts +++ b/src/variants/abalone/util.ts @@ -164,7 +164,12 @@ export const posToTranslateAbs = ( return translateBase[orientation](pos, xFactor, yFactor, { width: 9, height: 9 }); }; -export const posToTranslateRel = (pos: cg.Pos, orientation: cg.Orientation, _bt: cg.BoardDimensions, _v: cg.Variant): cg.NumberPair => { +export const posToTranslateRel = ( + pos: cg.Pos, + orientation: cg.Orientation, + _bt: cg.BoardDimensions, + _v: cg.Variant, +): cg.NumberPair => { return translateBase[orientation](pos, 100, 100, { width: 9, height: 9 }); }; @@ -197,7 +202,7 @@ export const posToTranslateBase2 = (bounds: ClientRect, pos: cg.Pos, orientation export const posToTranslateAbs2 = (): (( bounds: ClientRect, pos: cg.Pos, - orientation: cg.Orientation + orientation: cg.Orientation, ) => cg.NumberPair) => { return (bounds, pos, orientation) => posToTranslateBase2(bounds, pos, orientation); }; @@ -281,4 +286,4 @@ const rotate180 = (file: string, rank: string): string => { export const isValidKey = (key: cg.Key): boolean => { return /^(a[1-5]|b[1-6]|c[1-7]|d[1-8]|e[1-9]|f[2-9]|g[3-9]|h[4-9]|i[5-9])$/.test(key); -}; \ No newline at end of file +}; From d9fd8d83ef0ba8d20c3654fa81adca8bba6992d8 Mon Sep 17 00:00:00 2001 From: Vincent FROCHOT Date: Thu, 19 Dec 2024 04:14:41 +0100 Subject: [PATCH 15/22] fix: group import --- src/state.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/state.ts b/src/state.ts index 516e4ea..78180ad 100644 --- a/src/state.ts +++ b/src/state.ts @@ -4,9 +4,8 @@ import { baseMove } from './board'; import { DragCurrent } from './drag'; import { Drawable } from './draw'; import { render } from './render'; -import { posToTranslateAbs, posToTranslateRel, timer } from './util'; +import { key2pos, posToTranslateAbs, posToTranslateRel, timer } from './util'; import { pos2px } from './svg'; -import { key2pos } from './util'; import * as cg from './types'; export interface HeadlessState { From 1801d64cb64d0a5db19eb98a8c33cdf63f749f00 Mon Sep 17 00:00:00 2001 From: Vincent FROCHOT Date: Thu, 19 Dec 2024 04:45:03 +0100 Subject: [PATCH 16/22] chore: clean up --- src/drag.ts | 2 +- src/draw.ts | 2 +- src/render.ts | 4 ++-- src/svg.ts | 16 ++++++++-------- src/variants/abalone/board.ts | 24 +----------------------- src/variants/abalone/config.ts | 12 ++++++------ src/variants/abalone/drag.ts | 2 +- src/variants/abalone/engine.ts | 2 ++ src/variants/abalone/util.ts | 27 --------------------------- 9 files changed, 22 insertions(+), 69 deletions(-) diff --git a/src/drag.ts b/src/drag.ts index 6aca6de..4165ef0 100644 --- a/src/drag.ts +++ b/src/drag.ts @@ -175,7 +175,7 @@ function processDrag(s: State): void { cur.pos = [cur.epos[0] - cur.rel[0], cur.epos[1] - cur.rel[1]]; // move piece - const translation = util.posToTranslateAbs(s.dom.bounds(), s.dimensions, s.variant)(cur.origPos, s.orientation); // because of util.translateAbs, it has to remain invoked from util. + const translation = util.posToTranslateAbs(s.dom.bounds(), s.dimensions, s.variant)(cur.origPos, s.orientation); // until translateAbs becomes a HOF, it has to remain invoked from util. translation[0] += cur.pos[0] + cur.dec[0]; translation[1] += cur.pos[1] + cur.dec[1]; util.translateAbs(cur.element, translation); diff --git a/src/draw.ts b/src/draw.ts index 74586a9..7ae543f 100644 --- a/src/draw.ts +++ b/src/draw.ts @@ -134,7 +134,7 @@ export function clear(state: State): void { } } -export function eventBrush(e: cg.MouchEvent): string { +function eventBrush(e: cg.MouchEvent): string { const modA = (e.shiftKey || e.ctrlKey) && isRightButton(e); const modB = e.altKey || e.metaKey || e.getModifierState?.('AltGraph'); return brushes[(modA ? 1 : 0) + (modB ? 2 : 0)]; diff --git a/src/render.ts b/src/render.ts index 7adf0eb..32d9305 100644 --- a/src/render.ts +++ b/src/render.ts @@ -227,7 +227,7 @@ export function posZIndex(pos: cg.Pos, orientation: cg.Orientation, asP1: boolea return z + ''; } -export function pieceNameOf( +function pieceNameOf( piece: cg.Piece, myPlayerIndex: cg.PlayerIndex, orientation: cg.Orientation, @@ -254,7 +254,7 @@ function backgammonPosClass(k: cg.Key, orientation: cg.Orientation): string { return ` ${topOrBottom} ${leftOrRight}`; } -export function computeSquareClasses(s: State): SquareClasses { +function computeSquareClasses(s: State): SquareClasses { const squares: SquareClasses = new Map(); if (s.lastMove && s.highlight.lastMove) { let first = true; diff --git a/src/svg.ts b/src/svg.ts index 5d5a4c4..b5144ef 100644 --- a/src/svg.ts +++ b/src/svg.ts @@ -7,7 +7,7 @@ export function createElement(tagName: string): SVGElement { return document.createElementNS('http://www.w3.org/2000/svg', tagName); } -export interface Shape { +interface Shape { shape: DrawShape; current: boolean; hash: Hash; @@ -15,7 +15,7 @@ export interface Shape { type CustomBrushes = Map; // by hash -export type ArrowDests = Map; // how many arrows land on a square +type ArrowDests = Map; // how many arrows land on a square type Hash = string; @@ -344,11 +344,11 @@ export function setAttributes(el: SVGElement, attrs: { [key: string]: any }): SV return el; } -export function orient(pos: cg.Pos, orientation: cg.Orientation, bd: cg.BoardDimensions): cg.Pos { +function orient(pos: cg.Pos, orientation: cg.Orientation, bd: cg.BoardDimensions): cg.Pos { return T.mapToP1Inverse[orientation](pos, bd); } -export function makeCustomBrush(base: DrawBrush, modifiers: DrawModifiers): DrawBrush { +function makeCustomBrush(base: DrawBrush, modifiers: DrawModifiers): DrawBrush { return { color: base.color, opacity: Math.round(base.opacity * 10) / 10, @@ -357,20 +357,20 @@ export function makeCustomBrush(base: DrawBrush, modifiers: DrawModifiers): Draw }; } -export function circleWidth(bounds: ClientRect, bd: cg.BoardDimensions): [number, number] { +function circleWidth(bounds: ClientRect, bd: cg.BoardDimensions): [number, number] { const base = bounds.width / (bd.width * 64); return [3 * base, 4 * base]; } -export function lineWidth(brush: DrawBrush, current: boolean, bounds: ClientRect, bd: cg.BoardDimensions): number { +function lineWidth(brush: DrawBrush, current: boolean, bounds: ClientRect, bd: cg.BoardDimensions): number { return (((brush.lineWidth || 10) * (current ? 0.85 : 1)) / (bd.width * 64)) * bounds.width; } -export function opacity(brush: DrawBrush, current: boolean): number { +function opacity(brush: DrawBrush, current: boolean): number { return (brush.opacity || 1) * (current ? 0.9 : 1); } -export function arrowMargin(bounds: ClientRect, shorten: boolean, bd: cg.BoardDimensions): number { +function arrowMargin(bounds: ClientRect, shorten: boolean, bd: cg.BoardDimensions): number { return ((shorten ? 20 : 10) / (bd.width * 64)) * bounds.width; } diff --git a/src/variants/abalone/board.ts b/src/variants/abalone/board.ts index bd3d34a..ec2dad9 100644 --- a/src/variants/abalone/board.ts +++ b/src/variants/abalone/board.ts @@ -1,8 +1,6 @@ import type * as cg from '../../types'; -import { knight, queen } from '../../premove'; -import { distanceSq } from '../../util'; -import { allPos, computeSquareCenter, getCoordinates, getSquareDimensions, key2posAlt, pos2key } from './util'; +import { getCoordinates, getSquareDimensions } from './util'; /* from a position in pixels, returns the key of the square @@ -150,23 +148,3 @@ export const getKeyAtDomPos = ( return undefined; }; - -export const getSnappedKeyAtDomPos = ( - orig: cg.Key, - pos: cg.NumberPair, - orientation: cg.Orientation, - bounds: ClientRect, - bd: cg.BoardDimensions, -): cg.Key | undefined => { - const origPos = key2posAlt(orig); - const validSnapPos = allPos(bd).filter(pos2 => { - return 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), orientation, bounds, bd)); - 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]); -}; diff --git a/src/variants/abalone/config.ts b/src/variants/abalone/config.ts index 1671f0b..cc3d91c 100644 --- a/src/variants/abalone/config.ts +++ b/src/variants/abalone/config.ts @@ -6,20 +6,20 @@ import { render } from './render'; import { pos2px } from './svg'; import { key2pos, posToTranslateAbs2 as posToTranslateAbs2Original, posToTranslateRel } from './util'; -const posToTranslateAbs2 = - (bounds: ClientRect, _bt: cg.BoardDimensions, _variant: cg.Variant) => - (pos: cg.Pos, orientation: 'p1' | 'p2' | 'left' | 'right' | 'p1vflip') => - posToTranslateAbs2Original()(bounds, pos, orientation); - export const configure = (state: HeadlessState): void => { // HOF state.baseMove = baseMove; state.render = render; state.posToTranslateRelative = posToTranslateRel; - state.posToTranslateAbsolute = posToTranslateAbs2; + state.posToTranslateAbsolute = posToTranslateAbsBridge; state.pos2px = pos2px; state.key2pos = key2pos; // these below could just have been overriden by a config object state.animation.enabled = false; }; + +const posToTranslateAbsBridge = + (bounds: ClientRect, _bt: cg.BoardDimensions, _variant: cg.Variant) => + (pos: cg.Pos, orientation: 'p1' | 'p2' | 'left' | 'right' | 'p1vflip') => + posToTranslateAbs2Original()(bounds, pos, orientation); diff --git a/src/variants/abalone/drag.ts b/src/variants/abalone/drag.ts index 80cb880..d5bd632 100644 --- a/src/variants/abalone/drag.ts +++ b/src/variants/abalone/drag.ts @@ -29,7 +29,7 @@ export function processDrag(s: State): void { cur.pos = [cur.epos[0] - cur.rel[0], cur.epos[1] - cur.rel[1]]; // move piece - const translation = posToTranslateAbs(s.dom.bounds(), s.dimensions, 'chess')(cur.origPos, s.orientation); // because of translateAbs, it has to remain invoked from util. + const translation = posToTranslateAbs(s.dom.bounds(), s.dimensions, 'chess')(cur.origPos, s.orientation); // until translateAbs becomes a HOF, it has to remain invoked like this. translation[0] += cur.pos[0] + cur.dec[0]; translation[1] += cur.pos[1] + cur.dec[1]; translateAbs(cur.element, translation); // "working" WIP: have to use HOF diff --git a/src/variants/abalone/engine.ts b/src/variants/abalone/engine.ts index b70145d..37d59d8 100644 --- a/src/variants/abalone/engine.ts +++ b/src/variants/abalone/engine.ts @@ -14,6 +14,7 @@ import { } from './directions'; import type { MoveImpact, MoveVector } from './types'; +// compute the impact of a move on the board before it is made export const computeMoveImpact = (pieces: cg.Pieces, orig: cg.Key, dest: cg.Key): MoveImpact | undefined => { const directionString = getDirectionString(orig, dest); if (!directionString) return undefined; @@ -139,6 +140,7 @@ export const computeMoveImpact = (pieces: cg.Pieces, orig: cg.Key, dest: cg.Key) return undefined; }; +// compute a move vector after a move has been made export const computeMoveVectorPostMove = (pieces: cg.Pieces, orig: cg.Key, dest: cg.Key): MoveVector | undefined => { const directionString = getDirectionString(dest, orig); if (!directionString) return undefined; diff --git a/src/variants/abalone/util.ts b/src/variants/abalone/util.ts index 3c53995..e40c9b1 100644 --- a/src/variants/abalone/util.ts +++ b/src/variants/abalone/util.ts @@ -1,5 +1,4 @@ import type * as cg from '../../types'; -import * as T from '../../transformations'; import { SquareDimensions, TranslateBase } from './types'; export const abaloneFiles = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'] as const; @@ -96,20 +95,6 @@ export const key2posAlt = (k: cg.Key): cg.Pos => { // shift is used by analysis page and miniboards const shift = [2, 1.5, 1, 0.5, 0, -0.5, -1, -1.5, -2]; -export function computeSquareCenter( - key: cg.Key, - orientation: cg.Orientation, - bounds: ClientRect, - bd: cg.BoardDimensions, -): cg.NumberPair { - const pos = T.mapToP1Inverse[orientation](key2posAlt(key), bd); - - return [ - (pos[0] + shift[pos[1] - 1] - 1) * 100, - bounds.top + (bounds.height * (2 * bd.height - (pos[1] + 0.5))) / bd.height, - ]; -} - // translateBase defines where the translation of a piece should be placed on the board. // It is used to render the piece at the correct place. const translateBase: Record = { @@ -155,15 +140,6 @@ const translateBase: Record = { p1vflip: (pos: cg.Pos, xScale: number, yScale: number, _) => [(pos[0] - 1) * xScale, (pos[1] - 1) * yScale], }; -export const posToTranslateAbs = ( - pos: cg.Pos, - orientation: cg.Orientation, - xFactor: number, - yFactor: number, -): cg.NumberPair => { - return translateBase[orientation](pos, xFactor, yFactor, { width: 9, height: 9 }); -}; - export const posToTranslateRel = ( pos: cg.Pos, orientation: cg.Orientation, @@ -257,9 +233,6 @@ const translateBase2: Record = { left: (pos: cg.Pos, bounds: ClientRect) => [(pos[1] - 1) * bounds.x, (pos[0] - 1) * bounds.x], p1vflip: (pos: cg.Pos, bounds: ClientRect) => [(pos[1] - 1) * bounds.x, (pos[0] - 1) * bounds.x], }; -export const posToTranslateRel2 = (_bounds: ClientRect, pos: cg.Pos, orientation: cg.Orientation): cg.NumberPair => { - return translateBase[orientation](pos, 100, 100, { width: 9, height: 9 }); -}; export const getSquareDimensions = (bounds: ClientRect): SquareDimensions => ({ width: bounds.width * 0.093, From 86571eb117eb19888db99475af8651c453ee1b5d Mon Sep 17 00:00:00 2001 From: Vincent FROCHOT Date: Thu, 19 Dec 2024 04:59:33 +0100 Subject: [PATCH 17/22] chore: refacto translate func --- src/variants/abalone/util.ts | 65 ++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 33 deletions(-) diff --git a/src/variants/abalone/util.ts b/src/variants/abalone/util.ts index e40c9b1..59b6416 100644 --- a/src/variants/abalone/util.ts +++ b/src/variants/abalone/util.ts @@ -97,49 +97,48 @@ const shift = [2, 1.5, 1, 0.5, 0, -0.5, -1, -1.5, -2]; // translateBase defines where the translation of a piece should be placed on the board. // It is used to render the piece at the correct place. -const translateBase: Record = { - p1: (pos: cg.Pos, _xScale: number, _yScale: number, _bt: cg.BoardDimensions) => { - const squareWidth = 102.5; - const squareHeight = 88; - const bottomLeft = [295, 854]; - const shift = [0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4]; - - if (pos[1] < 6) { +const createTranslateBase = (): Record => { + const squareWidth = 102.5; + const squareHeight = 88; + const bottomLeft = [295, 854]; + const shift = [0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4]; + + return { + p1: (pos: cg.Pos, _xScale: number, _yScale: number, _bt: cg.BoardDimensions) => { + if (pos[1] < 6) { + return [ + bottomLeft[0] + squareWidth * pos[0] - (shift[pos[1] - 1] + 1) * squareWidth, + bottomLeft[1] - (pos[1] - 1) * squareHeight, + ]; + } return [ bottomLeft[0] + squareWidth * pos[0] - (shift[pos[1] - 1] + 1) * squareWidth, bottomLeft[1] - (pos[1] - 1) * squareHeight, ]; - } - return [ - bottomLeft[0] + squareWidth * pos[0] - (shift[pos[1] - 1] + 1) * squareWidth, - bottomLeft[1] - (pos[1] - 1) * squareHeight, - ]; - }, - p2: (pos: cg.Pos, _xScale: number, _yScale: number, _bt: cg.BoardDimensions) => { - const squareWidth = 102.5; - const squareHeight = 88; - const bottomLeft = [295, 854]; - const shift = [0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4]; - - if (pos[1] < 6) { + }, + p2: (pos: cg.Pos, _xScale: number, _yScale: number, _bt: cg.BoardDimensions) => { + if (pos[1] < 6) { + return [ + bottomLeft[0] + squareWidth * pos[0] - (shift[pos[1] - 1] + 1) * squareWidth, + bottomLeft[1] - (pos[1] - 1) * squareHeight, + ]; + } return [ bottomLeft[0] + squareWidth * pos[0] - (shift[pos[1] - 1] + 1) * squareWidth, bottomLeft[1] - (pos[1] - 1) * squareHeight, ]; - } - return [ - bottomLeft[0] + squareWidth * pos[0] - (shift[pos[1] - 1] + 1) * squareWidth, - bottomLeft[1] - (pos[1] - 1) * squareHeight, - ]; - }, - right: (pos: cg.Pos, xScale: number, yScale: number, _) => [(pos[1] - 1) * xScale, (pos[0] - 1) * yScale], - left: (pos: cg.Pos, xScale: number, yScale: number, bt: cg.BoardDimensions) => [ - (bt.width - pos[0]) * xScale, - (pos[1] - 1) * yScale, - ], - p1vflip: (pos: cg.Pos, xScale: number, yScale: number, _) => [(pos[0] - 1) * xScale, (pos[1] - 1) * yScale], + }, + right: (pos: cg.Pos, xScale: number, yScale: number, _) => [(pos[1] - 1) * xScale, (pos[0] - 1) * yScale], + left: (pos: cg.Pos, xScale: number, yScale: number, bt: cg.BoardDimensions) => [ + (bt.width - pos[0]) * xScale, + (pos[1] - 1) * yScale, + ], + p1vflip: (pos: cg.Pos, xScale: number, yScale: number, _) => [(pos[0] - 1) * xScale, (pos[1] - 1) * yScale], + }; }; +const translateBase = createTranslateBase(); + export const posToTranslateRel = ( pos: cg.Pos, orientation: cg.Orientation, From 95f9c08090016274ee26bff9a7d82bbf583ffeee Mon Sep 17 00:00:00 2001 From: Vincent FROCHOT Date: Thu, 19 Dec 2024 11:31:42 +0100 Subject: [PATCH 18/22] fix: remove traces of merge --- build.js | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/build.js b/build.js index 1f5ae4c..92e8638 100644 --- a/build.js +++ b/build.js @@ -1,25 +1,4 @@ import * as cps from 'node:child_process'; -<<<<<<< HEAD -import * as ps from 'node:process'; - -const consumerRoot = process.env.CONSUMER_ROOT; -const args = ps.argv.slice(2); - -if (args.includes('--link')) { - cps.execSync('pnpm link --dir ' + consumerRoot); - console.log('\x1b[36m%s\x1b[0m', 'Linked to ' + consumerRoot); -} -if (args.includes('--bundle')) { - cps.execSync( - 'esbuild src/chessground.ts --bundle --format=esm --outfile=' + - consumerRoot + - '/node_modules/chessground/dist/chessground.min.js', - ); - console.log( - '\x1b[32m%s\x1b[0m', - 'Compiled and built ' + consumerRoot + '/node_modules/chessground/dist/chessground.min.js', - ); -======= const red = '\x1b[31m%s\x1b[0m'; const green = '\x1b[32m%s\x1b[0m'; @@ -48,5 +27,4 @@ if (args.includes('--bundle')) { { stdio: 'inherit' }, ); console.log(green, `compiled and built ${linkedProject}/node_modules/chessground/dist/chessground.min.js`); ->>>>>>> origin/master } From af8e05028afc95c3ef7f9f5755f89f478ed11815 Mon Sep 17 00:00:00 2001 From: Vincent FROCHOT Date: Thu, 19 Dec 2024 11:33:23 +0100 Subject: [PATCH 19/22] fix: traces of merge <<< --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a2954f5..28b6588 100644 --- a/README.md +++ b/README.md @@ -92,10 +92,14 @@ Commands are listed in package.json. In case you want to see the possibilities from the console, run `pnpm run` <<<<<<< HEAD + ### Install build dependencies: + ======= + ### Install build dependencies:s ->>>>>>> origin/master + +> > > > > > > origin/master ```sh pnpm install From 182675c9eca2fab0f060fc5af06cd5e0742fff07 Mon Sep 17 00:00:00 2001 From: Vincent FROCHOT Date: Thu, 19 Dec 2024 12:21:03 +0100 Subject: [PATCH 20/22] v3.4 abalone --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 05bcbf5..100d7b9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chessground", - "version": "7.11.1-pstrat3.3", + "version": "7.11.1-pstrat3.4", "description": "playstrategy.org chess ui, forked from lichess.org", "type": "module", "module": "dist/chessground.js", From ef939d107d05c0756a5e7eae6388128579c39f7a Mon Sep 17 00:00:00 2001 From: Vincent FROCHOT Date: Thu, 19 Dec 2024 12:28:32 +0100 Subject: [PATCH 21/22] fix: merge conflict in readme --- README.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/README.md b/README.md index 28b6588..3b000c8 100644 --- a/README.md +++ b/README.md @@ -91,16 +91,8 @@ More? Please make a pull request to include it here. Commands are listed in package.json. In case you want to see the possibilities from the console, run `pnpm run` -<<<<<<< HEAD - -### Install build dependencies: - -======= - ### Install build dependencies:s -> > > > > > > origin/master - ```sh pnpm install ``` From a9d0a72e4a51ac9744d97958048ee801854e36dc Mon Sep 17 00:00:00 2001 From: Vincent FROCHOT Date: Thu, 19 Dec 2024 12:45:04 +0100 Subject: [PATCH 22/22] fix: update dirty function in wip so it becomes more obvious what it currently does --- src/variants/abalone/util.ts | 66 ++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/src/variants/abalone/util.ts b/src/variants/abalone/util.ts index 59b6416..0840085 100644 --- a/src/variants/abalone/util.ts +++ b/src/variants/abalone/util.ts @@ -6,39 +6,39 @@ export const abaloneFiles = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'] as con const abaloneRanks = ['1', '2', '3', '4', '5', '6', '7', '8', '9'] as const; export const pos2key = (pos: cg.Pos): cg.Key => { - let posx = pos[0]; - if (pos[1] == 0) { - posx += 2; - } - if (pos[1] == 1) { - posx = pos[0] - 2; - } - if (pos[1] == 2) { - posx = pos[0] - 2; // -2.5 - } - if (pos[1] == 3) { - posx = pos[0] - 1; - } - if (pos[1] == 4) { - posx = pos[0] - 1; // -1.5 - } - if (pos[1] == 5) { - posx = pos[0]; // - 1 - } - if (pos[1] == 6) { - posx = pos[0]; // -0.5 - } - if (pos[1] == 7) { - posx = pos[0]; // 0 - } - if (pos[1] == 8) { - posx = pos[0]; // 0.5 - } - if (pos[1] == 9) { - posx = pos[0] + 1; - } - - posx = pos[0]; + // let posx = pos[0]; + // if (pos[1] == 0) { + // posx += 2; + // } + // if (pos[1] == 1) { + // posx = pos[0] - 2; + // } + // if (pos[1] == 2) { + // posx = pos[0] - 2; // -2.5 + // } + // if (pos[1] == 3) { + // posx = pos[0] - 1; + // } + // if (pos[1] == 4) { + // posx = pos[0] - 1; // -1.5 + // } + // if (pos[1] == 5) { + // posx = pos[0]; // - 1 + // } + // if (pos[1] == 6) { + // posx = pos[0]; // -0.5 + // } + // if (pos[1] == 7) { + // posx = pos[0]; // 0 + // } + // if (pos[1] == 8) { + // posx = pos[0]; // 0.5 + // } + // if (pos[1] == 9) { + // posx = pos[0] + 1; + // } + + const posx = pos[0]; let posy = pos[1]; const key = (abaloneFiles[posx] + abaloneRanks[posy]) as cg.Key;