From e477c4e92083f200ec3d7c518ba28c493f1deff1 Mon Sep 17 00:00:00 2001 From: Sh Raj Date: Tue, 20 Feb 2024 17:32:41 +0530 Subject: [PATCH] Add files via upload --- index.html | 71 ++++ script.js | 1041 ++++++++++++++++++++++++++++++++++++++++++++++++++++ style.css | 331 +++++++++++++++++ 3 files changed, 1443 insertions(+) create mode 100644 index.html create mode 100644 script.js create mode 100644 style.css diff --git a/index.html b/index.html new file mode 100644 index 0000000..65d32cf --- /dev/null +++ b/index.html @@ -0,0 +1,71 @@ + + + + + CodePen - Cheap AI Chess! + + + + + + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/script.js b/script.js new file mode 100644 index 0000000..1448b6a --- /dev/null +++ b/script.js @@ -0,0 +1,1041 @@ +"use strict"; +console.clear(); +let PIECE_DIR_CALC = 0; +class Utils { + static colToInt(col) { + return Board.COLS.indexOf(col); + } + static rowToInt(row) { + return Board.ROWS.indexOf(row); + } + static intToCol(int) { + return Board.COLS[int]; + } + static intToRow(int) { + return Board.ROWS[int]; + } + static getPositionsFromShortCode(shortCode) { + const positions = Utils.getInitialPiecePositions(); + const overrides = {}; + const defaultPositionMode = shortCode.charAt(0) === "X"; + if (defaultPositionMode) { + shortCode = shortCode.slice(1); + } + shortCode.split(",").forEach((string) => { + const promoted = string.charAt(0) === "P"; + if (promoted) { + string = string.slice(1); + } + if (defaultPositionMode) { + const inactive = string.length === 3; + const id = string.slice(0, 2); + const col = inactive ? undefined : string.charAt(2); + const row = inactive ? undefined : string.charAt(3); + const moves = string.charAt(4) || "1"; + overrides[id] = { + col, + row, + active: !inactive, + _moves: parseInt(moves), + _promoted: promoted, + }; + } + else { + const moved = string.length >= 4; + const id = string.slice(0, 2); + const col = string.charAt(moved ? 2 : 0); + const row = string.charAt(moved ? 3 : 1); + const moves = string.charAt(4) || moved ? "1" : "0"; + overrides[id] = { col, row, active: true, _moves: parseInt(moves), _promoted: promoted }; + } + }); + for (let id in positions) { + if (overrides[id]) { + positions[id] = overrides[id]; + } + else { + positions[id] = defaultPositionMode ? positions[id] : { active: false }; + } + } + return positions; + } + static getInitialBoardPieces(parent, pieces) { + const boardPieces = {}; + const container = document.createElement("div"); + container.className = "pieces"; + parent.appendChild(container); + for (let pieceId in pieces) { + const boardPiece = document.createElement("div"); + boardPiece.className = `piece ${pieces[pieceId].data.player.toLowerCase()}`; + boardPiece.innerHTML = pieces[pieceId].shape(); + container.appendChild(boardPiece); + boardPieces[pieceId] = boardPiece; + } + return boardPieces; + } + static getInitialBoardTiles(parent, handler) { + const tiles = { 1: {}, 2: {}, 3: {}, 4: {}, 5: {}, 6: {}, 7: {}, 8: {} }; + const board = document.createElement("div"); + board.className = "board"; + parent.appendChild(board); + for (let i = 0; i < 8; i++) { + const row = document.createElement("div"); + row.className = "row"; + board.appendChild(row); + for (let j = 0; j < 8; j++) { + const tile = document.createElement("button"); + tile.className = "tile"; + const r = Utils.intToRow(i); + const c = Utils.intToCol(j); + tile.addEventListener("click", () => handler({ row: r, col: c })); + row.appendChild(tile); + tiles[r][c] = tile; + } + } + return tiles; + } + static getInitialBoardState(construct = () => undefined) { + const blankRow = () => ({ + A: construct(), + B: construct(), + C: construct(), + D: construct(), + E: construct(), + F: construct(), + G: construct(), + H: construct(), + }); + return { + 1: Object.assign({}, blankRow()), + 2: Object.assign({}, blankRow()), + 3: Object.assign({}, blankRow()), + 4: Object.assign({}, blankRow()), + 5: Object.assign({}, blankRow()), + 6: Object.assign({}, blankRow()), + 7: Object.assign({}, blankRow()), + 8: Object.assign({}, blankRow()), + }; + } + static getInitialPiecePositions() { + return { + A8: { active: true, row: "8", col: "A" }, + B8: { active: true, row: "8", col: "B" }, + C8: { active: true, row: "8", col: "C" }, + D8: { active: true, row: "8", col: "D" }, + E8: { active: true, row: "8", col: "E" }, + F8: { active: true, row: "8", col: "F" }, + G8: { active: true, row: "8", col: "G" }, + H8: { active: true, row: "8", col: "H" }, + A7: { active: true, row: "7", col: "A" }, + B7: { active: true, row: "7", col: "B" }, + C7: { active: true, row: "7", col: "C" }, + D7: { active: true, row: "7", col: "D" }, + E7: { active: true, row: "7", col: "E" }, + F7: { active: true, row: "7", col: "F" }, + G7: { active: true, row: "7", col: "G" }, + H7: { active: true, row: "7", col: "H" }, + A2: { active: true, row: "2", col: "A" }, + B2: { active: true, row: "2", col: "B" }, + C2: { active: true, row: "2", col: "C" }, + D2: { active: true, row: "2", col: "D" }, + E2: { active: true, row: "2", col: "E" }, + F2: { active: true, row: "2", col: "F" }, + G2: { active: true, row: "2", col: "G" }, + H2: { active: true, row: "2", col: "H" }, + A1: { active: true, row: "1", col: "A" }, + B1: { active: true, row: "1", col: "B" }, + C1: { active: true, row: "1", col: "C" }, + D1: { active: true, row: "1", col: "D" }, + E1: { active: true, row: "1", col: "E" }, + F1: { active: true, row: "1", col: "F" }, + G1: { active: true, row: "1", col: "G" }, + H1: { active: true, row: "1", col: "H" }, + }; + } + static getInitialPieces() { + return { + A8: new Piece({ id: "A8", player: "BLACK", type: "ROOK" }), + B8: new Piece({ id: "B8", player: "BLACK", type: "KNIGHT" }), + C8: new Piece({ id: "C8", player: "BLACK", type: "BISHOP" }), + D8: new Piece({ id: "D8", player: "BLACK", type: "QUEEN" }), + E8: new Piece({ id: "E8", player: "BLACK", type: "KING" }), + F8: new Piece({ id: "F8", player: "BLACK", type: "BISHOP" }), + G8: new Piece({ id: "G8", player: "BLACK", type: "KNIGHT" }), + H8: new Piece({ id: "H8", player: "BLACK", type: "ROOK" }), + A7: new Piece({ id: "A7", player: "BLACK", type: "PAWN" }), + B7: new Piece({ id: "B7", player: "BLACK", type: "PAWN" }), + C7: new Piece({ id: "C7", player: "BLACK", type: "PAWN" }), + D7: new Piece({ id: "D7", player: "BLACK", type: "PAWN" }), + E7: new Piece({ id: "E7", player: "BLACK", type: "PAWN" }), + F7: new Piece({ id: "F7", player: "BLACK", type: "PAWN" }), + G7: new Piece({ id: "G7", player: "BLACK", type: "PAWN" }), + H7: new Piece({ id: "H7", player: "BLACK", type: "PAWN" }), + A2: new Piece({ id: "A2", player: "WHITE", type: "PAWN" }), + B2: new Piece({ id: "B2", player: "WHITE", type: "PAWN" }), + C2: new Piece({ id: "C2", player: "WHITE", type: "PAWN" }), + D2: new Piece({ id: "D2", player: "WHITE", type: "PAWN" }), + E2: new Piece({ id: "E2", player: "WHITE", type: "PAWN" }), + F2: new Piece({ id: "F2", player: "WHITE", type: "PAWN" }), + G2: new Piece({ id: "G2", player: "WHITE", type: "PAWN" }), + H2: new Piece({ id: "H2", player: "WHITE", type: "PAWN" }), + A1: new Piece({ id: "A1", player: "WHITE", type: "ROOK" }), + B1: new Piece({ id: "B1", player: "WHITE", type: "KNIGHT" }), + C1: new Piece({ id: "C1", player: "WHITE", type: "BISHOP" }), + D1: new Piece({ id: "D1", player: "WHITE", type: "QUEEN" }), + E1: new Piece({ id: "E1", player: "WHITE", type: "KING" }), + F1: new Piece({ id: "F1", player: "WHITE", type: "BISHOP" }), + G1: new Piece({ id: "G1", player: "WHITE", type: "KNIGHT" }), + H1: new Piece({ id: "H1", player: "WHITE", type: "ROOK" }), + }; + } +} +class Shape { + static shape(player, piece) { + return ` + + `; + } + static shapeBishop(player) { + return Shape.shape(player, "bishop"); + } + static shapeKing(player) { + return Shape.shape(player, "king"); + } + static shapeKnight(player) { + return Shape.shape(player, "knight"); + } + static shapePawn(player) { + return Shape.shape(player, "pawn"); + } + static shapeQueen(player) { + return Shape.shape(player, "queen"); + } + static shapeRook(player) { + return Shape.shape(player, "rook"); + } +} +class Constraints { + static generate(args, resultingChecks) { + let method; + const { piecePositions, piece } = args; + if (piecePositions[piece.data.id].active) { + switch (piece.data.type) { + case "BISHOP": + method = Constraints.constraintsBishop; + break; + case "KING": + method = Constraints.constraintsKing; + break; + case "KNIGHT": + method = Constraints.constraintsKnight; + break; + case "PAWN": + method = Constraints.constraintsPawn; + break; + case "QUEEN": + method = Constraints.constraintsQueen; + break; + case "ROOK": + method = Constraints.constraintsRook; + break; + } + } + const result = method ? method(args) : { moves: [], captures: [] }; + if (resultingChecks) { + const moveIndex = args.moveIndex + 1; + result.moves = result.moves.filter((location) => !resultingChecks({ piece, location, capture: false, moveIndex }).length); + result.captures = result.captures.filter((location) => !resultingChecks({ piece, location, capture: true, moveIndex }).length); + } + return result; + } + static constraintsBishop(args) { + return Constraints.constraintsDiagonal(args); + } + static constraintsDiagonal(args) { + const response = { moves: [], captures: [] }; + const { piece } = args; + Constraints.runUntil(piece.dirNW.bind(piece), response, args); + Constraints.runUntil(piece.dirNE.bind(piece), response, args); + Constraints.runUntil(piece.dirSW.bind(piece), response, args); + Constraints.runUntil(piece.dirSE.bind(piece), response, args); + return response; + } + static constraintsKing(args) { + const { piece, kingCastles, piecePositions } = args; + const moves = []; + const captures = []; + const locations = [ + piece.dirN(1, piecePositions), + piece.dirNE(1, piecePositions), + piece.dirE(1, piecePositions), + piece.dirSE(1, piecePositions), + piece.dirS(1, piecePositions), + piece.dirSW(1, piecePositions), + piece.dirW(1, piecePositions), + piece.dirNW(1, piecePositions), + ]; + if (kingCastles) { + const castles = kingCastles(piece); + castles.forEach((position) => moves.push(position)); + } + locations.forEach((location) => { + const value = Constraints.relationshipToTile(location, args); + if (value === "BLANK") { + moves.push(location); + } + else if (value === "ENEMY") { + captures.push(location); + } + }); + return { moves, captures }; + } + static constraintsKnight(args) { + const { piece, piecePositions } = args; + const moves = []; + const captures = []; + const locations = [ + piece.dir(1, 2, piecePositions), + piece.dir(1, -2, piecePositions), + piece.dir(2, 1, piecePositions), + piece.dir(2, -1, piecePositions), + piece.dir(-1, 2, piecePositions), + piece.dir(-1, -2, piecePositions), + piece.dir(-2, 1, piecePositions), + piece.dir(-2, -1, piecePositions), + ]; + locations.forEach((location) => { + const value = Constraints.relationshipToTile(location, args); + if (value === "BLANK") { + moves.push(location); + } + else if (value === "ENEMY") { + captures.push(location); + } + }); + return { moves, captures }; + } + static constraintsOrthangonal(args) { + const { piece } = args; + const response = { moves: [], captures: [] }; + Constraints.runUntil(piece.dirN.bind(piece), response, args); + Constraints.runUntil(piece.dirE.bind(piece), response, args); + Constraints.runUntil(piece.dirS.bind(piece), response, args); + Constraints.runUntil(piece.dirW.bind(piece), response, args); + return response; + } + static constraintsPawn(args) { + const { piece, piecePositions } = args; + const moves = []; + const captures = []; + const locationN1 = piece.dirN(1, piecePositions); + const locationN2 = piece.dirN(2, piecePositions); + if (Constraints.relationshipToTile(locationN1, args) === "BLANK") { + moves.push(locationN1); + if (!piece.moves.length && Constraints.relationshipToTile(locationN2, args) === "BLANK") { + moves.push(locationN2); + } + } + [ + [piece.dirNW(1, piecePositions), piece.dirW(1, piecePositions)], + [piece.dirNE(1, piecePositions), piece.dirE(1, piecePositions)], + ].forEach(([location, enPassant]) => { + const standardCaptureRelationship = Constraints.relationshipToTile(location, args); + const enPassantCaptureRelationship = Constraints.relationshipToTile(enPassant, args); + if (standardCaptureRelationship === "ENEMY") { + captures.push(location); + } + else if (piece.moves.length > 0 && enPassantCaptureRelationship === "ENEMY") { + const enPassantRow = enPassant.row === (piece.playerWhite() ? "5" : "4"); + const other = Constraints.locationToPiece(enPassant, args); + if (enPassantRow && other && other.data.type === "PAWN") { + if (other.moves.length === 1 && other.moves[0] === args.moveIndex - 1) { + location.capture = Object.assign({}, enPassant); + captures.push(location); + } + } + } + }); + return { moves, captures }; + } + static constraintsQueen(args) { + const diagonal = Constraints.constraintsDiagonal(args); + const orthagonal = Constraints.constraintsOrthangonal(args); + return { + moves: diagonal.moves.concat(orthagonal.moves), + captures: diagonal.captures.concat(orthagonal.captures), + }; + } + static constraintsRook(args) { + return Constraints.constraintsOrthangonal(args); + } + static locationToPiece(location, args) { + if (!location) { + return undefined; + } + const { state, pieces } = args; + const row = state[location.row]; + const occupyingId = row === undefined ? undefined : row[location.col]; + return pieces[occupyingId]; + } + static relationshipToTile(location, args) { + if (!location) { + return undefined; + } + const { piece } = args; + const occupying = Constraints.locationToPiece(location, args); + if (occupying) { + return occupying.data.player === piece.data.player ? "FRIEND" : "ENEMY"; + } + else { + return "BLANK"; + } + } + static runUntil(locationFunction, response, args) { + const { piecePositions } = args; + let inc = 1; + let location = locationFunction(inc++, piecePositions); + while (location) { + let abort = false; + const relations = Constraints.relationshipToTile(location, args); + if (relations === "ENEMY") { + response.captures.push(location); + abort = true; + } + else if (relations === "FRIEND") { + abort = true; + } + else { + response.moves.push(location); + } + if (abort) { + location = undefined; + } + else { + location = locationFunction(inc++, piecePositions); + } + } + } +} +class Piece { + constructor(data) { + this.moves = []; + this.promoted = false; + this.updateShape = false; + this.data = data; + } + get orientation() { + return this.data.player === "BLACK" ? -1 : 1; + } + dirN(steps, positions) { + return this.dir(steps, 0, positions); + } + dirS(steps, positions) { + return this.dir(-steps, 0, positions); + } + dirW(steps, positions) { + return this.dir(0, -steps, positions); + } + dirE(steps, positions) { + return this.dir(0, steps, positions); + } + dirNW(steps, positions) { + return this.dir(steps, -steps, positions); + } + dirNE(steps, positions) { + return this.dir(steps, steps, positions); + } + dirSW(steps, positions) { + return this.dir(-steps, -steps, positions); + } + dirSE(steps, positions) { + return this.dir(-steps, steps, positions); + } + dir(stepsRow, stepsColumn, positions) { + PIECE_DIR_CALC++; + const row = Utils.rowToInt(positions[this.data.id].row) + this.orientation * stepsRow; + const col = Utils.colToInt(positions[this.data.id].col) + this.orientation * stepsColumn; + if (row >= 0 && row <= 7 && col >= 0 && col <= 7) { + return { row: Utils.intToRow(row), col: Utils.intToCol(col) }; + } + return undefined; + } + move(moveIndex) { + this.moves.push(moveIndex); + } + options(moveIndex, state, pieces, piecePositions, resultingChecks, kingCastles) { + return Constraints.generate({ moveIndex, state, piece: this, pieces, piecePositions, kingCastles }, resultingChecks); + } + playerBlack() { + return this.data.player === "BLACK"; + } + playerWhite() { + return this.data.player === "WHITE"; + } + promote(type = "QUEEN") { + this.data.type = type; + this.promoted = true; + this.updateShape = true; + } + shape() { + const player = this.data.player.toLowerCase(); + switch (this.data.type) { + case "BISHOP": + return Shape.shapeBishop(player); + case "KING": + return Shape.shapeKing(player); + case "KNIGHT": + return Shape.shapeKnight(player); + case "PAWN": + return Shape.shapePawn(player); + case "QUEEN": + return Shape.shapeQueen(player); + case "ROOK": + return Shape.shapeRook(player); + } + } +} +class Board { + constructor(pieces, piecePositions) { + this.checksBlack = []; + this.checksWhite = []; + this.piecesTilesCaptures = {}; + this.piecesTilesMoves = {}; + this.tilesPiecesBlackCaptures = Utils.getInitialBoardState(() => []); + this.tilesPiecesBlackMoves = Utils.getInitialBoardState(() => []); + this.tilesPiecesWhiteCaptures = Utils.getInitialBoardState(() => []); + this.tilesPiecesWhiteMoves = Utils.getInitialBoardState(() => []); + this.pieceIdsBlack = []; + this.pieceIdsWhite = []; + this.state = Utils.getInitialBoardState(); + this.pieces = pieces; + for (let id in pieces) { + if (pieces[id].playerWhite()) { + this.pieceIdsWhite.push(id); + } + else { + this.pieceIdsBlack.push(id); + } + } + this.initializePositions(piecePositions); + } + initializePositions(piecePositions) { + this.piecePositions = piecePositions; + this.initializeState(); + this.piecesUpdate(0); + } + initializeState() { + for (let pieceId in this.pieces) { + const { row, col, active, _moves, _promoted } = this.piecePositions[pieceId]; + if (_moves) { + delete this.piecePositions[pieceId]._moves; + // TODO: come back to this + // this.pieces[pieceId].moves = new Array(_moves); + } + if (_promoted) { + delete this.piecePositions[pieceId]._promoted; + this.pieces[pieceId].promote(); + } + if (active) { + this.state[row] = this.state[row] || []; + this.state[row][col] = pieceId; + } + } + } + kingCastles(king) { + const castles = []; + // king has to not have moved + if (king.moves.length) { + return castles; + } + const kingIsWhite = king.playerWhite(); + const moves = kingIsWhite ? this.tilesPiecesBlackMoves : this.tilesPiecesWhiteMoves; + const checkPositions = (row, rookCol, castles) => { + const cols = rookCol === "A" ? ["D", "C", "B"] : ["F", "G"]; + // rook has to not have moved + const rookId = `${rookCol}${row}`; + const rook = this.pieces[rookId]; + const { active } = this.piecePositions[rookId]; + if (active && rook.moves.length === 0) { + let canCastle = true; + cols.forEach((col) => { + // each tile has to be empty + if (this.state[row][col]) { + canCastle = false; + // each tile cant be in the path of the other team + } + else if (moves[row][col].length) { + canCastle = false; + } + }); + if (canCastle) { + castles.push({ col: cols[1], row, castles: rookCol }); + } + } + }; + const row = kingIsWhite ? "1" : "8"; + if (!this.pieces[`A${row}`].moves.length) { + checkPositions(row, "A", castles); + } + if (!this.pieces[`H${row}`].moves.length) { + checkPositions(row, "H", castles); + } + return castles; + } + kingCheckStates(kingPosition, captures, piecePositions) { + const { col, row } = kingPosition; + return captures[row][col].map((id) => piecePositions[id]).filter((pos) => pos.active); + } + pieceCalculateMoves(pieceId, moveIndex, state, piecePositions, piecesTilesCaptures, piecesTilesMoves, tilesPiecesCaptures, tilesPiecesMoves, resultingChecks, kingCastles) { + const { captures, moves } = this.pieces[pieceId].options(moveIndex, state, this.pieces, piecePositions, resultingChecks, kingCastles); + piecesTilesCaptures[pieceId] = Array.from(captures); + piecesTilesMoves[pieceId] = Array.from(moves); + captures.forEach(({ col, row }) => tilesPiecesCaptures[row][col].push(pieceId)); + moves.forEach(({ col, row }) => tilesPiecesMoves[row][col].push(pieceId)); + } + pieceCapture(piece) { + const pieceId = piece.data.id; + const { col, row } = this.piecePositions[pieceId]; + this.state[row][col] = undefined; + delete this.piecePositions[pieceId].col; + delete this.piecePositions[pieceId].row; + this.piecePositions[pieceId].active = false; + } + pieceMove(piece, location) { + const pieceId = piece.data.id; + const { row, col } = this.piecePositions[pieceId]; + this.state[row][col] = undefined; + this.state[location.row][location.col] = pieceId; + this.piecePositions[pieceId].row = location.row; + this.piecePositions[pieceId].col = location.col; + if (piece.data.type === "PAWN" && (location.row === "8" || location.row === "1")) { + piece.promote(); + } + } + piecesUpdate(moveIndex) { + this.tilesPiecesBlackCaptures = Utils.getInitialBoardState(() => []); + this.tilesPiecesBlackMoves = Utils.getInitialBoardState(() => []); + this.tilesPiecesWhiteCaptures = Utils.getInitialBoardState(() => []); + this.tilesPiecesWhiteMoves = Utils.getInitialBoardState(() => []); + this.pieceIdsBlack.forEach((id) => this.pieceCalculateMoves(id, moveIndex, this.state, this.piecePositions, this.piecesTilesCaptures, this.piecesTilesMoves, this.tilesPiecesBlackCaptures, this.tilesPiecesBlackMoves, this.resultingChecks.bind(this), this.kingCastles.bind(this))); + this.pieceIdsWhite.forEach((id) => this.pieceCalculateMoves(id, moveIndex, this.state, this.piecePositions, this.piecesTilesCaptures, this.piecesTilesMoves, this.tilesPiecesWhiteCaptures, this.tilesPiecesWhiteMoves, this.resultingChecks.bind(this), this.kingCastles.bind(this))); + this.checksBlack = this.kingCheckStates(this.piecePositions.E1, this.tilesPiecesBlackCaptures, this.piecePositions); + this.checksWhite = this.kingCheckStates(this.piecePositions.E8, this.tilesPiecesWhiteCaptures, this.piecePositions); + } + resultingChecks({ piece, location, capture, moveIndex }) { + const tilesPiecesCaptures = Utils.getInitialBoardState(() => []); + const tilesPiecesMoves = Utils.getInitialBoardState(() => []); + const piecesTilesCaptures = {}; + const piecesTilesMoves = {}; + const state = JSON.parse(JSON.stringify(this.state)); + const piecePositions = JSON.parse(JSON.stringify(this.piecePositions)); + if (capture) { + const loc = location.capture || location; + const capturedId = state[loc.row][loc.col]; + if (this.pieces[capturedId].data.type === "KING") { + // this is a checking move + } + else { + delete piecePositions[capturedId].col; + delete piecePositions[capturedId].row; + piecePositions[capturedId].active = false; + } + } + const pieceId = piece.data.id; + const { row, col } = piecePositions[pieceId]; + state[row][col] = undefined; + state[location.row][location.col] = pieceId; + piecePositions[pieceId].row = location.row; + piecePositions[pieceId].col = location.col; + const ids = piece.playerWhite() ? this.pieceIdsBlack : this.pieceIdsWhite; + const king = piece.playerWhite() ? piecePositions.E1 : piecePositions.E8; + ids.forEach((id) => this.pieceCalculateMoves(id, moveIndex, state, piecePositions, piecesTilesCaptures, piecesTilesMoves, tilesPiecesCaptures, tilesPiecesMoves)); + return this.kingCheckStates(king, tilesPiecesCaptures, piecePositions); + } + tileEach(callback) { + Board.ROWS.forEach((row) => { + Board.COLS.forEach((col) => { + const piece = this.tileFind({ row, col }); + const moves = piece ? this.piecesTilesMoves[piece.data.id] : undefined; + const captures = piece ? this.piecesTilesCaptures[piece.data.id] : undefined; + callback({ row, col }, piece, moves, captures); + }); + }); + } + tileFind({ row, col }) { + const id = this.state[row][col]; + return this.pieces[id]; + } + toShortCode() { + const positionsAbsolute = []; + const positionsDefaults = []; + for (let id in this.piecePositions) { + const { active, col, row } = this.piecePositions[id]; + const pos = `${col}${row}`; + const moves = this.pieces[id].moves; + const promotedCode = this.pieces[id].promoted ? "P" : ""; + const movesCode = moves > 9 ? "9" : moves > 1 ? moves.toString() : ""; + if (active) { + positionsAbsolute.push(`${promotedCode}${id}${id === pos ? "" : pos}${movesCode}`); + if (id !== pos || moves > 0) { + positionsDefaults.push(`${promotedCode}${id}${pos}${movesCode}`); + } + } + else { + if (id !== "BQ" && id !== "WQ") { + positionsDefaults.push(`${promotedCode}${id}X`); + } + } + } + const pA = positionsAbsolute.join(","); + const pD = positionsDefaults.join(","); + return pA.length > pD.length ? `X${pD}` : pA; + } +} +Board.COLS = ["A", "B", "C", "D", "E", "F", "G", "H"]; +Board.ROWS = ["1", "2", "3", "4", "5", "6", "7", "8"]; +class Game { + constructor(pieces, piecePositions, turn = "WHITE") { + this.active = null; + this.activePieceOptions = []; + this.moveIndex = 0; + this.moves = []; + this.turn = turn; + this.board = new Board(pieces, piecePositions); + } + activate(location) { + const tilePiece = this.board.tileFind(location); + if (tilePiece && !this.active && tilePiece.data.player !== this.turn) { + this.active = null; + return { type: "INVALID" }; + // a piece is active rn + } + else if (this.active) { + const activePieceId = this.active.data.id; + this.active = null; + const validatedPosition = this.activePieceOptions.find((option) => option.col === location.col && option.row === location.row); + const positionIsValid = !!validatedPosition; + this.activePieceOptions = []; + const capturePiece = (validatedPosition === null || validatedPosition === void 0 ? void 0 : validatedPosition.capture) ? this.board.tileFind(validatedPosition.capture) : tilePiece; + // a piece is on the tile + if (capturePiece) { + const capturedPieceId = capturePiece.data.id; + // cancelling the selected piece on invalid location + if (capturedPieceId === activePieceId) { + return { type: "CANCEL" }; + } + else if (positionIsValid) { + // capturing the selected piece + this.capture(activePieceId, capturedPieceId, location); + return { + type: "CAPTURE", + activePieceId, + capturedPieceId, + captures: [location], + }; + // cancel + } + else if (capturePiece.data.player !== this.turn) { + return { type: "CANCEL" }; + } + else { + // proceed to TOUCH or CANCEL + } + } + else if (positionIsValid) { + // moving will return castled if that happens (only two move) + const castledId = this.move(activePieceId, location); + return { type: "MOVE", activePieceId, moves: [location], castledId }; + // invalid spot. cancel. + } + else { + return { type: "CANCEL" }; + } + } + // no piece selected or new CANCEL + TOUCH + if (tilePiece) { + const tilePieceId = tilePiece.data.id; + const moves = this.board.piecesTilesMoves[tilePieceId]; + const captures = this.board.piecesTilesCaptures[tilePieceId]; + if (!moves.length && !captures.length) { + return { type: "INVALID" }; + } + this.active = tilePiece; + this.activePieceOptions = moves.concat(captures); + return { type: "TOUCH", captures, moves, activePieceId: tilePieceId }; + // cancelling + } + else { + this.activePieceOptions = []; + return { type: "CANCEL" }; + } + } + capture(capturingPieceId, capturedPieceId, location) { + const captured = this.board.pieces[capturedPieceId]; + this.board.pieceCapture(captured); + this.move(capturingPieceId, location, true); + } + handleCastling(piece, location) { + if (piece.data.type !== "KING" || + piece.moves.length || + location.row !== (piece.playerWhite() ? "1" : "8") || + (location.col !== "C" && location.col !== "G")) { + return; + } + return `${location.col === "C" ? "A" : "H"}${location.row}`; + } + move(pieceId, location, capture = false) { + const piece = this.board.pieces[pieceId]; + const castledId = this.handleCastling(piece, location); + piece.move(this.moveIndex); + if (castledId) { + const castled = this.board.pieces[castledId]; + castled.move(this.moveIndex); + this.board.pieceMove(castled, { col: location.col === "C" ? "D" : "F", row: location.row }); + this.moves.push(`${pieceId}O${location.col}${location.row}`); + } + else { + this.moves.push(`${pieceId}${capture ? "x" : ""}${location.col}${location.row}`); + } + this.moveIndex++; + this.board.pieceMove(piece, location); + this.turn = this.turn === "WHITE" ? "BLACK" : "WHITE"; + this.board.piecesUpdate(this.moveIndex); + const state = this.moveResultState(); + console.log(state); + if (!state.moves && !state.captures) { + alert(state.stalemate ? "Stalemate!" : `${this.turn === "WHITE" ? "Black" : "White"} Wins!`); + } + return castledId; + } + moveResultState() { + let movesWhite = 0; + let capturesWhite = 0; + let movesBlack = 0; + let capturesBlack = 0; + this.board.tileEach(({ row, col }) => { + movesWhite += this.board.tilesPiecesWhiteMoves[row][col].length; + capturesWhite += this.board.tilesPiecesWhiteCaptures[row][col].length; + movesBlack += this.board.tilesPiecesBlackMoves[row][col].length; + capturesBlack += this.board.tilesPiecesBlackCaptures[row][col].length; + }); + const activeBlack = this.board.pieceIdsBlack.filter((pieceId) => this.board.piecePositions[pieceId].active).length; + const activeWhite = this.board.pieceIdsWhite.filter((pieceId) => this.board.piecePositions[pieceId].active).length; + const moves = this.turn === "WHITE" ? movesWhite : movesBlack; + const captures = this.turn === "WHITE" ? capturesWhite : capturesBlack; + const noMoves = movesWhite + capturesWhite + movesBlack + capturesBlack === 0; + const checked = !!this.board[this.turn === "WHITE" ? "checksBlack" : "checksWhite"].length; + const onlyKings = activeBlack === 1 && activeWhite === 1; + const stalemate = onlyKings || noMoves || ((moves + captures === 0) && !checked); + const code = this.board.toShortCode(); + return { turn: this.turn, checked, moves, captures, code, stalemate }; + } + randomMove() { + if (this.active) { + if (this.activePieceOptions.length) { + const { col, row } = this.activePieceOptions[Math.floor(Math.random() * this.activePieceOptions.length)]; + return { col, row }; + } + else { + const { col, row } = this.board.piecePositions[this.active.data.id]; + return { col, row }; + } + } + else { + const ids = this.turn === "WHITE" ? this.board.pieceIdsWhite : this.board.pieceIdsBlack; + const positions = ids.map((pieceId) => { + const moves = this.board.piecesTilesMoves[pieceId]; + const captures = this.board.piecesTilesCaptures[pieceId]; + return (moves.length || captures.length) ? this.board.piecePositions[pieceId] : undefined; + }).filter((position) => position === null || position === void 0 ? void 0 : position.active); + const remaining = positions[Math.floor(Math.random() * positions.length)]; + const { col, row } = remaining || { col: "E", row: "1" }; + return { col, row }; + } + } +} +class View { + constructor(element, game, perspective) { + this.element = element; + this.game = game; + this.setPerspective(perspective || this.game.turn); + this.tiles = Utils.getInitialBoardTiles(this.element, this.handleTileClick.bind(this)); + this.pieces = Utils.getInitialBoardPieces(this.element, this.game.board.pieces); + this.drawPiecePositions(); + } + drawActivePiece(activePieceId) { + const { row, col } = this.game.board.piecePositions[activePieceId]; + this.tiles[row][col].classList.add("highlight-active"); + this.pieces[activePieceId].classList.add("highlight-active"); + } + drawCapturedPiece(capturedPieceId) { + const piece = this.pieces[capturedPieceId]; + piece.style.setProperty("--transition-delay", "var(--transition-duration)"); + piece.style.removeProperty("--pos-col"); + piece.style.removeProperty("--pos-row"); + piece.style.setProperty("--scale", "0"); + } + drawPiecePositions(moves = [], moveInner = "") { + document.body.style.setProperty("--color-background", `var(--color-${this.game.turn.toLowerCase()}`); + const other = this.game.turn === "WHITE" ? "turn-black" : "turn-white"; + const current = this.game.turn === "WHITE" ? "turn-white" : "turn-black"; + this.element.classList.add(current); + this.element.classList.remove(other); + if (moves.length) { + this.element.classList.add("touching"); + } + else { + this.element.classList.remove("touching"); + } + const key = (row, col) => `${row}-${col}`; + const moveKeys = moves.map(({ row, col }) => key(row, col)); + this.game.board.tileEach(({ row, col }, piece, pieceMoves, pieceCaptures) => { + const tileElement = this.tiles[row][col]; + const move = moveKeys.includes(key(row, col)) ? moveInner : ""; + const format = (id, className) => this.game.board.pieces[id].shape(); + tileElement.innerHTML = ` +
${move}
+
+ ${this.game.board.tilesPiecesBlackMoves[row][col].map((id) => format(id, "black")).join("")} + ${this.game.board.tilesPiecesWhiteMoves[row][col].map((id) => format(id, "white")).join("")} +
+
+ ${this.game.board.tilesPiecesBlackCaptures[row][col].map((id) => format(id, "black")).join("")} + ${this.game.board.tilesPiecesWhiteCaptures[row][col].map((id) => format(id, "white")).join("")} +
+ `; + if (piece) { + tileElement.classList.add("occupied"); + const pieceElement = this.pieces[piece.data.id]; + pieceElement.style.setProperty("--pos-col", Utils.colToInt(col).toString()); + pieceElement.style.setProperty("--pos-row", Utils.rowToInt(row).toString()); + pieceElement.style.setProperty("--scale", "1"); + pieceElement.classList[(pieceMoves === null || pieceMoves === void 0 ? void 0 : pieceMoves.length) ? "add" : "remove"]("can-move"); + pieceElement.classList[(pieceCaptures === null || pieceCaptures === void 0 ? void 0 : pieceCaptures.length) ? "add" : "remove"]("can-capture"); + if (piece.updateShape) { + piece.updateShape = false; + pieceElement.innerHTML = piece.shape(); + } + } + else { + tileElement.classList.remove("occupied"); + } + }); + } + drawPositions(moves, captures) { + moves === null || moves === void 0 ? void 0 : moves.forEach(({ row, col }) => { + var _a, _b; + this.tiles[row][col].classList.add("highlight-move"); + (_b = this.pieces[(_a = this.game.board.tileFind({ row, col })) === null || _a === void 0 ? void 0 : _a.data.id]) === null || _b === void 0 ? void 0 : _b.classList.add("highlight-move"); + }); + captures === null || captures === void 0 ? void 0 : captures.forEach(({ row, col, capture }) => { + var _a, _b; + if (capture) { + row = capture.row; + col = capture.col; + } + this.tiles[row][col].classList.add("highlight-capture"); + (_b = this.pieces[(_a = this.game.board.tileFind({ row, col })) === null || _a === void 0 ? void 0 : _a.data.id]) === null || _b === void 0 ? void 0 : _b.classList.add("highlight-capture"); + }); + } + drawResetClassNames() { + document.querySelectorAll(".highlight-active").forEach((element) => element.classList.remove("highlight-active")); + document.querySelectorAll(".highlight-capture").forEach((element) => element.classList.remove("highlight-capture")); + document.querySelectorAll(".highlight-move").forEach((element) => element.classList.remove("highlight-move")); + } + handleTileClick(location) { + const { activePieceId, capturedPieceId, moves = [], captures = [], type } = this.game.activate(location); + this.drawResetClassNames(); + if (type === "TOUCH") { + const enPassant = captures.find((capture) => !!capture.capture); + const passingMoves = enPassant ? moves.concat([enPassant]) : moves; + this.drawPiecePositions(passingMoves, this.game.board.pieces[activePieceId].shape()); + } + else { + this.drawPiecePositions(); + } + if (type === "CANCEL" || type === "INVALID") { + return; + } + if (type === "MOVE" || type === "CAPTURE") { + } + else { + this.drawActivePiece(activePieceId); + } + if (type === "TOUCH") { + this.drawPositions(moves, captures); + } + else if (type === "CAPTURE") { + this.drawCapturedPiece(capturedPieceId); + } + // crazy town + // this.setPerspective(this.game.turn); + } + setPerspective(perspective) { + const other = perspective === "WHITE" ? "perspective-black" : "perspective-white"; + const current = perspective === "WHITE" ? "perspective-white" : "perspective-black"; + this.element.classList.add(current); + this.element.classList.remove(other); + } +} +class Control { + constructor(game, view) { + this.inputSpeedAsap = document.getElementById("speed-asap"); + this.inputSpeedFast = document.getElementById("speed-fast"); + this.inputSpeedMedium = document.getElementById("speed-medium"); + this.inputSpeedSlow = document.getElementById("speed-slow"); + this.inputRandomBlack = document.getElementById("black-random"); + this.inputRandomWhite = document.getElementById("white-random"); + this.inputPerspectiveBlack = document.getElementById("black-perspective"); + this.inputPerspectiveWhite = document.getElementById("white-perspective"); + this.game = game; + this.view = view; + this.inputPerspectiveBlack.addEventListener("change", this.updateViewPerspective.bind(this)); + this.inputPerspectiveWhite.addEventListener("change", this.updateViewPerspective.bind(this)); + this.updateViewPerspective(); + } + get speed() { + if (this.inputSpeedAsap.checked) { + return 50; + } + if (this.inputSpeedFast.checked) { + return 250; + } + if (this.inputSpeedMedium.checked) { + return 500; + } + if (this.inputSpeedSlow.checked) { + return 1000; + } + } + autoplay() { + const input = this.game.turn === "WHITE" ? this.inputRandomWhite : this.inputRandomBlack; + if (!input.checked) { + setTimeout(this.autoplay.bind(this), this.speed); + return; + } + const position = this.game.randomMove(); + this.view.handleTileClick(position); + setTimeout(this.autoplay.bind(this), this.speed); + } + updateViewPerspective() { + this.view.setPerspective(this.inputPerspectiveBlack.checked ? "BLACK" : "WHITE"); + } +} +const DEMOS = { + castle1: "XD8B3,B1X,C1X,D1X,F1X,G1X", + castle2: "XD8B3,B1X,C1X,C2X,D1X,F1X,G1X", + castle3: "XD8E3,B1X,C1X,F2X,D1X,F1X,G1X", + promote1: "E1,E8,C2C7", + promote2: "E1,E8E7,PC2C8", + start: "XE7E6,F7F5,D2D4,E2E5", + test2: "C8E2,E8,G8H1,D7E4,H7H3,PA2H7,PB2G7,D2D6,E2E39,A1H2,E1B3", + test: "C8E2,E8,G8H1,D7E4,H7H3,D1H7,PB2G7,D2D6,E2E39,A1H2,E1B3", +}; +const initialPositions = Utils.getInitialPiecePositions(); +// const initialPositions = Utils.getPositionsFromShortCode(DEMOS.castle1); +const initialTurn = "WHITE"; +const perspective = "WHITE"; +const game = new Game(Utils.getInitialPieces(), initialPositions, initialTurn); +const view = new View(document.getElementById("board"), game, perspective); +const control = new Control(game, view); +control.autoplay(); \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 0000000..6e798b4 --- /dev/null +++ b/style.css @@ -0,0 +1,331 @@ +:root { + --border-width: calc(var(--diameter-tile) / 60); + --diameter-board: min(85vw, 85vh); + --diameter-tile: calc(1 / 8 * var(--diameter-board)); + --edge-width: calc((min(100vw, 100vh) - var(--diameter-board)) * 0.3); + --color-danger: tomato; + --color-success: #1d83e0; + --color-white: #f0f0f0; + --color-black: #222; + --color-board-hue: 30; + --color-board-sat: 40%; + --color-shadow: hsl(var(--color-board-hue), var(--color-board-sat), 50%); + --color-shadow-lighter: hsl(var(--color-board-hue), var(--color-board-sat), 55%); + --transition-ease: cubic-bezier(0.25, 1, 0.5, 1); + --color-background: var(--color-black); +} + +aside { + display: flex; + justify-content: space-between; + left: 0; + position: absolute; + top: calc(var(--edge-width) * -0.55); + transform: translateY(-50%); + width: 100%; + z-index: 999; +} +aside div { + align-items: center; + color: white; + display: flex; +} +aside div > * { + align-items: center; + display: flex; +} +aside div > * + * { + margin-left: calc(var(--border-width) * 2); +} +aside div h3, +aside div label { + font-size: calc(var(--edge-width) * 0.3); + height: calc(var(--edge-width) * 0.3); + line-height: 1; + margin-bottom: 0; + margin-top: 0; + text-transform: uppercase; +} +aside div label { + cursor: pointer; +} +aside div input { + left: -99999px; + position: absolute; +} + +aside div input + * { + opacity: 0.5; +} +aside div input:checked + * { + font-weight: bold; + opacity: 1; +} + +aside div svg { + height: calc(var(--edge-width) * 0.5); + width: auto; +} + +html, +body { + height: 100%; +} + +body { + background: var(--color-background); + overflow: hidden; + transition: background-color 250ms ease-in-out; +} + +#view { + background: var(--color-shadow-lighter); + box-shadow: 0 0 0 calc(var(--border-width) * 3) var(--color-shadow-lighter), + 0 0 0 var(--edge-width) var(--color-shadow); + height: var(--diameter-board); + margin: calc((100vh - var(--diameter-board)) * 0.5) + calc((100vw - var(--diameter-board)) * 0.5); + position: relative; + width: var(--diameter-board); +} + +.board { + display: flex; + flex-direction: column-reverse; + height: 100%; + width: 100%; +} + +.board .row { + display: flex; + height: var(--diameter-tile); + width: 100%; +} + +.perspective-black .board .row { + flex-direction: row-reverse; +} + +.perspective-black .board { + flex-direction: column; +} + +.board .row .tile { + background-color: currentcolor; + border: none; + box-shadow: inset 0 0 0 var(--border-width) var(--color-shadow-lighter); + display: flex; + flex-direction: column; + height: var(--diameter-tile); + justify-content: space-between; + padding: 0; + position: relative; + transition: background-color 350ms var(--transition-ease); + width: var(--diameter-tile); +} + +.perspective-black .board .row:nth-child(even) .tile:nth-child(odd), +.perspective-black .board .row:nth-child(odd) .tile:nth-child(even), +.perspective-white .board .row:nth-child(even) .tile:nth-child(even), +.perspective-white .board .row:nth-child(odd) .tile:nth-child(odd) { + color: hsl(var(--color-board-hue), var(--color-board-sat), 62%); +} + +.perspective-black .board .row:nth-child(even) .tile:nth-child(even), +.perspective-black .board .row:nth-child(odd) .tile:nth-child(odd), +.perspective-white .board .row:nth-child(even) .tile:nth-child(odd), +.perspective-white .board .row:nth-child(odd) .tile:nth-child(even) { + color: hsl(var(--color-board-hue), var(--color-board-sat), 70%); +} +.board .row .tile.highlight-active {} +.board .row .tile.highlight-capture {} +.board .row .tile.highlight-move {} + +.board .row .tile .move, +.board .row .tile .moves, +.board .row .tile .captures { + box-sizing: border-box; + display: flex; + flex-wrap: wrap; + height: var(--diameter-tile); + justify-content: center; + left: 0; + padding: calc(var(--diameter-tile) * 0.025); + position: absolute; + top: 0; + width: var(--diameter-tile); + z-index: 9; +} + +.board .row .tile .move, +.board .row .tile .moves { + align-content: center; + align-items: center; +} +.board .row .tile .captures { + align-items: flex-start; + justify-content: space-between; +} +.board .row .tile:not(.occupied) .captures { + align-items: center; + justify-content: center; +} + +.board .row .tile > div > svg { + --stroke: transparent; + box-sizing: border-box; + height: var(--di); + line-height: var(--di); + width: var(--di); +} + +.board .row .tile .move svg { + --di: calc(var(--diameter-tile) / 4); + --fill: var(--color-shadow); +} + +.board .row .tile .moves svg, +.board .row .tile .captures svg { + --di: calc(var(--diameter-tile) / 4); + --fill: var(--color-shadow); + opacity: 0.4; +} + +.board .row .tile.occupied .captures svg { position: absolute; } +.board .row .tile.occupied .captures svg:nth-child(1) { top: 0; left: 0; } +.board .row .tile.occupied .captures svg:nth-child(2) { top: 0; right: 0; } +.board .row .tile.occupied .captures svg:nth-child(3) { bottom: calc(var(--di) * 0.1); left: 0; } +.board .row .tile.occupied .captures svg:nth-child(4) { bottom: calc(var(--di) * 0.1); right: 0; } +.board .row .tile.occupied .captures svg:nth-child(5) { top: calc(50% - var(--di) * 0.55); left: 0; } +.board .row .tile.occupied .captures svg:nth-child(6) { top: calc(50% - var(--di) * 0.55); right: 0; } +.board .row .tile.occupied .captures svg:nth-child(7) { top: 0; left: calc(50% - var(--di) * 0.5); } +.board .row .tile.occupied .captures svg:nth-child(8) { bottom: calc(var(--di) * 0.1); left: calc(50% - var(--di) * 0.5); } + +.touching .board .row .tile .moves, +.touching .board .row .tile .captures, +.turn-black .board .row .tile .moves .white, +.turn-black .board .row .tile .captures .white, +.turn-white .board .row .tile .moves .black, +.turn-white .board .row .tile .captures .black { + display: none; +} + +.board .row .tile[class*="highlight-"] .moves, +.board .row .tile[class*="highlight-"] .captures { + display: none; +} + +button:focus { + outline: none; + position: relative; + z-index: 9; +} + +svg { + --fill: var(--color-black); + --stroke: var(--color-shadow); + fill: var(--fill); +} + +svg.white { --fill: var(--color-white); } +svg.black { --fill: var(--color-black); } + +.pieces { + display: block; + height: var(--diameter-board); + left: 0; + pointer-events: none; + position: absolute; + top: 0; + width: var(--diameter-board); + z-index: 99; +} + +.pieces .piece.white { + --pos-row: -1; +} +.pieces .piece.black { + --pos-row: 8; +} +.pieces .piece { + --pos-col: 3.5; + --scale: 0; + --transition-delay: 0ms; + --transition-duration: 200ms; + bottom: 0; + display: block; + height: var(--diameter-tile); + position: absolute; + left: 0; + transform: translate( + calc(var(--pos-col) * 100%), + calc(var(--pos-row) * -100%) + ) + translateZ(0); + transform-origin: 50% 50%; + transition: all var(--transition-duration) var(--transition-ease) + var(--transition-delay); + width: var(--diameter-tile); +} +.perspective-black .pieces .piece { + transform: translate( + calc((7 - var(--pos-col)) * 100%), + calc((7 - var(--pos-row)) * -100%) + ) + translateZ(0); +} +.pieces .piece svg { + display: block; + left: 50%; + opacity: 1; + position: absolute; + top: 50%; + transform: translate(-50%, -50%) translateZ(0) scale(var(--scale)); + transform-origin: 50% 50%; + transition: transform var(--transition-duration) var(--transition-ease), + fill var(--transition-duration) var(--transition-ease), + opacity var(--transition-duration) var(--transition-ease); +} +.turn-white .pieces .piece:not(.highlight-capture) svg.black, +.turn-black .pieces .piece:not(.highlight-capture) svg.white, +.turn-black .pieces .piece:not(.can-move):not(.can-capture) svg.black, +.turn-white .pieces .piece:not(.can-move):not(.can-capture) svg.white { + --stroke: transparent; + opacity: 0.8; +} + +@-webkit-keyframes wobble { + 0%, 50%, 100% { transform: translate(-50%, -50%) translateZ(0) scale(1) rotate(0deg); } + 25% { transform: translate(-50%, -50%) translateZ(0) scale(1.1) rotate(-2deg); } + 75% { transform: translate(-50%, -50%) translateZ(0) scale(1.1) rotate(2deg); } +} + +@keyframes wobble { + 0%, 50%, 100% { transform: translate(-50%, -50%) translateZ(0) scale(1) rotate(0deg); } + 25% { transform: translate(-50%, -50%) translateZ(0) scale(1.1) rotate(-2deg); } + 75% { transform: translate(-50%, -50%) translateZ(0) scale(1.1) rotate(2deg); } +} +.pieces .piece.highlight-active svg { + -webkit-animation: wobble 500ms linear infinite; + animation: wobble 500ms linear infinite; + --stroke: var(--color-success); +} + +.pieces .piece.highlight-capture svg { + --stroke: var(--color-danger); +} + +.piece svg { + --svg-di: calc(var(--diameter-tile) * 0.666); + display: block; + font-weight: bold; + height: var(--svg-di); + left: 50%; + line-height: var(--svg-di); + position: absolute; + stroke-linejoin: round; + text-align: center; + top: 50%; + transform: translate(-50%, -50%); + width: var(--svg-di); +} \ No newline at end of file