diff --git a/src/Games/chess/README.md b/src/Games/chess/README.md new file mode 100644 index 000000000..ec4a83b14 --- /dev/null +++ b/src/Games/chess/README.md @@ -0,0 +1,22 @@ +game: +This is chess game + +Description: +Chess is a strategic board game played between two opponents on a square board divided into 64 squares of alternating colors. +The objective of the game is to checkmate the opponent's king, putting it in a position where, +it is under attack and cannot escape capture. + +Functionalities: +1.Piece Movement: + A chess game allows players to move their pieces according to the rules of the game. +Each piece has its specific movement capabilities, and players can select and move their pieces across the board during their turn. + +2.Capturing Opponent's Pieces: +The game enables players to capture their opponent's pieces by moving their own pieces to the same square occupied by an opponent's piece. + +3.Check and Checkmate: +The chess game incorporates the concept of "check" and "checkmate." When a player's king is under direct attack, it is in "check." +The objective is to create a situation where the opponent's king is in checkmate, meaning it is under attack and cannot escape capture. +The game notifies players when a check or checkmate occurs. + + ![Screenshot](https://github.com/Yashoda2003/Games-and-Go/assets/116747256/10fac2bd-dd0c-409a-b04a-d7f54f3f997f) diff --git a/src/Games/chess/drs/index.html b/src/Games/chess/drs/index.html new file mode 100644 index 000000000..e75dcb2b4 --- /dev/null +++ b/src/Games/chess/drs/index.html @@ -0,0 +1,71 @@ + + + + + Chess! + + + + + + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Games/chess/drs/script.js b/src/Games/chess/drs/script.js new file mode 100644 index 000000000..1448b6aa3 --- /dev/null +++ b/src/Games/chess/drs/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/src/Games/chess/drs/style.css b/src/Games/chess/drs/style.css new file mode 100644 index 000000000..5a0734c4a --- /dev/null +++ b/src/Games/chess/drs/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 diff --git a/src/Games/chess/src/index.html b/src/Games/chess/src/index.html new file mode 100644 index 000000000..838f14c37 --- /dev/null +++ b/src/Games/chess/src/index.html @@ -0,0 +1,56 @@ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Games/chess/src/script.ts b/src/Games/chess/src/script.ts new file mode 100644 index 000000000..8b57e76ef --- /dev/null +++ b/src/Games/chess/src/script.ts @@ -0,0 +1,1237 @@ +console.clear(); + +// TODO: url based + +type MoveType = "CANCEL" | "CAPTURE" | "INVALID" | "MOVE" | "TOUCH"; +// prettier-ignore +type PieceIdBlack = "A8" | "B8" | "C8" | "D8" | "E8" | "F8" | "G8" | "H8" | + "A7" | "B7" | "C7" | "D7" | "E7" | "F7" | "G7" | "H7"; +// prettier-ignore +type PieceIdWhite = "A2" | "B2" | "C2" | "D2" | "E2" | "F2" | "G2" | "H2" | + "A1" | "B1" | "C1" | "D1" | "E1" | "F1" | "G1" | "H1"; +type PieceId = PieceIdWhite | PieceIdBlack; +type PieceType = "PAWN" | "ROOK" | "KNIGHT" | "BISHOP" | "QUEEN" | "KING"; +type PlayerId = "WHITE" | "BLACK"; +type PositionColumn = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H"; +type PositionRow = "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8"; + +interface ActivateResponse { + activePieceId?: PieceId; + capturedPieceId?: PieceId; + captures?: Position[]; + castledId?: PieceId; + moves?: Position[]; + type: MoveType; +} +interface Options { + captures: Position[]; + moves: Position[]; +} +interface PieceData { + id: PieceId; + type: PieceType; + player: PlayerId; +} +interface PiecePositionOnBoard extends Position { + active: true; +} +interface PiecePositionOffBoard { + active: false; + row?: undefined; + col?: undefined; +} +interface Position { + row: PositionRow; + col: PositionColumn; + capture?: Position; + castles?: PositionColumn; + _moves?: number; + _promoted?: boolean; +} + +type BoardPieces = { [K in PieceId]: HTMLElement }; +type BoardState = { [K in PositionRow]?: { [K in PositionColumn]?: PieceId } }; +type BoardTiles = { + [K in PositionRow]: { [K in PositionColumn]: HTMLElement }; +}; +type PiecePosition = PiecePositionOnBoard | PiecePositionOffBoard; +type Pieces = { [K in PieceId]: Piece }; +type PieceDirResponse = Position | undefined; +type PiecePositions = { [K in PieceId]: PiecePosition }; +type PiecesToTiles = { [K in PieceId]?: Position[] }; +type TilesToPieces = { + [K in PositionRow]: { [K in PositionColumn]: PieceId[] }; +}; +type TileRelation = "FRIEND" | "ENEMY" | "BLANK"; + +type ConstraintArguments = { + moveIndex: number; + piece: Piece; + pieces: Pieces; + piecePositions: PiecePositions; + state: BoardState; + kingCastles?: (king: Piece) => Position[]; +}; + +type ResultingChecksArguments = { + piece: Piece; + location: Position; + capture: boolean; + moveIndex: number; +}; + +let PIECE_DIR_CALC = 0; + +class Utils { + static colToInt(col: PositionColumn): number { + return Board.COLS.indexOf(col); + } + static rowToInt(row: PositionRow): number { + return Board.ROWS.indexOf(row); + } + static intToCol(int: number): PositionColumn { + return Board.COLS[int]; + } + static intToRow(int: number): PositionRow { + return Board.ROWS[int]; + } + + static getPositionsFromShortCode(shortCode: string): PiecePositions { + 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: HTMLElement, pieces: Pieces): BoardPieces { + 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 as BoardPieces; + } + + static getInitialBoardTiles(parent: HTMLElement, handler: (params: Position) => void): BoardTiles { + 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 as BoardTiles; + } + + static getInitialBoardState(construct = () => undefined): any { + const blankRow = () => ({ + A: construct(), + B: construct(), + C: construct(), + D: construct(), + E: construct(), + F: construct(), + G: construct(), + H: construct(), + }); + return { + 1: { ...blankRow() }, + 2: { ...blankRow() }, + 3: { ...blankRow() }, + 4: { ...blankRow() }, + 5: { ...blankRow() }, + 6: { ...blankRow() }, + 7: { ...blankRow() }, + 8: { ...blankRow() }, + }; + } + + static getInitialPiecePositions(): PiecePositions { + 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(): Pieces { + 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: string, piece: string) { + return ` + + `; + } + static shapeBishop(player: string) { + return Shape.shape(player, "bishop"); + } + static shapeKing(player: string) { + return Shape.shape(player, "king"); + } + static shapeKnight(player: string) { + return Shape.shape(player, "knight"); + } + static shapePawn(player: string) { + return Shape.shape(player, "pawn"); + } + static shapeQueen(player: string) { + return Shape.shape(player, "queen"); + } + static shapeRook(player: string) { + return Shape.shape(player, "rook"); + } +} + +class Constraints { + static generate(args: ConstraintArguments, resultingChecks?: (args: ResultingChecksArguments) => PiecePosition[]): Options { + 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: ConstraintArguments): Options { + return Constraints.constraintsDiagonal(args); + } + + static constraintsDiagonal(args: ConstraintArguments): Options { + 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: ConstraintArguments): Options { + 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: ConstraintArguments): Options { + 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: ConstraintArguments): Options { + 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: ConstraintArguments): Options { + 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 = { ...enPassant }; + captures.push(location); + } + } + } + }); + return { moves, captures }; + } + + static constraintsQueen(args: ConstraintArguments): Options { + 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: ConstraintArguments): Options { + return Constraints.constraintsOrthangonal(args); + } + + static locationToPiece(location: Position, args: ConstraintArguments): Piece | undefined { + 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: Position, args: ConstraintArguments): TileRelation | undefined { + 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: (integer: number, positions: PiecePositions) => PieceDirResponse, response: Options, args: ConstraintArguments) { + 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 { + data: PieceData; + moves = []; + promoted = false; + updateShape = false; + + constructor(data: PieceData) { + this.data = data; + } + + get orientation() { + return this.data.player === "BLACK" ? -1 : 1; + } + + dirN(steps: number, positions: PiecePositions): PieceDirResponse { + return this.dir(steps, 0, positions); + } + dirS(steps: number, positions: PiecePositions): PieceDirResponse { + return this.dir(-steps, 0, positions); + } + dirW(steps: number, positions: PiecePositions): PieceDirResponse { + return this.dir(0, -steps, positions); + } + dirE(steps: number, positions: PiecePositions): PieceDirResponse { + return this.dir(0, steps, positions); + } + dirNW(steps: number, positions: PiecePositions): PieceDirResponse { + return this.dir(steps, -steps, positions); + } + dirNE(steps: number, positions: PiecePositions): PieceDirResponse { + return this.dir(steps, steps, positions); + } + dirSW(steps: number, positions: PiecePositions): PieceDirResponse { + return this.dir(-steps, -steps, positions); + } + dirSE(steps: number, positions: PiecePositions): PieceDirResponse { + return this.dir(-steps, steps, positions); + } + dir(stepsRow: number, stepsColumn: number, positions: PiecePositions): PieceDirResponse { + 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: number) { + this.moves.push(moveIndex); + } + + options( + moveIndex: number, + state: BoardState, + pieces: Pieces, + piecePositions: PiecePositions, + resultingChecks?: (args: ResultingChecksArguments) => PiecePosition[], + kingCastles?: (king: Piece) => Position[] + ): Options { + 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: PieceType = "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 { + checksBlack: PiecePosition[] = []; + checksWhite: PiecePosition[] = []; + piecesTilesCaptures: PiecesToTiles = {}; + piecesTilesMoves: PiecesToTiles = {}; + tilesPiecesBlackCaptures: TilesToPieces = Utils.getInitialBoardState(() => []); + tilesPiecesBlackMoves: TilesToPieces = Utils.getInitialBoardState(() => []); + tilesPiecesWhiteCaptures: TilesToPieces = Utils.getInitialBoardState(() => []); + tilesPiecesWhiteMoves: TilesToPieces = Utils.getInitialBoardState(() => []); + + pieceIdsBlack: PieceIdBlack[] = []; + pieceIdsWhite: PieceIdWhite[] = []; + piecePositions: PiecePositions; + pieces: Pieces; + state: BoardState = Utils.getInitialBoardState() as BoardState; + + static COLS: PositionColumn[] = ["A", "B", "C", "D", "E", "F", "G", "H"]; + static ROWS: PositionRow[] = ["1", "2", "3", "4", "5", "6", "7", "8"]; + + constructor(pieces: Pieces, piecePositions: PiecePositions) { + this.pieces = pieces; + for (let id in pieces) { + if (pieces[id].playerWhite()) { + this.pieceIdsWhite.push(id as PieceIdWhite); + } else { + this.pieceIdsBlack.push(id as PieceIdBlack); + } + } + this.initializePositions(piecePositions); + } + + initializePositions(piecePositions: 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: Piece): Position[] { + 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: PositionRow, rookCol: PositionColumn, castles: Position[]) => { + const cols: PositionColumn[] = 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: PiecePosition, captures: TilesToPieces, piecePositions: PiecePositions): PiecePosition[] { + const { col, row } = kingPosition; + return captures[row][col].map((id) => piecePositions[id]).filter((pos) => pos.active); + } + + pieceCalculateMoves( + pieceId: PieceId, + moveIndex: number, + state: BoardState, + piecePositions: PiecePositions, + piecesTilesCaptures: PiecesToTiles, + piecesTilesMoves: PiecesToTiles, + tilesPiecesCaptures: TilesToPieces, + tilesPiecesMoves: TilesToPieces, + resultingChecks?: (args: ResultingChecksArguments) => PiecePosition[], + kingCastles?: (king: Piece) => Position[] + ) { + 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: 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: Piece, location: Position) { + 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: number) { + 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 }: ResultingChecksArguments) { + 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: (position: Position, piece?: Piece, pieceMoves?: Position[], pieceCaptures?: Position[]) => void) { + 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 }: Position): Piece | undefined { + 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; + } +} + +class Game { + active: Piece | null = null; + activePieceOptions: Position[] = []; + board: Board; + moveIndex = 0; + moves = []; + turn: PlayerId; + + constructor(pieces: Pieces, piecePositions: PiecePositions, turn: PlayerId = "WHITE") { + this.turn = turn; + this.board = new Board(pieces, piecePositions); + } + + activate(location: Position): ActivateResponse { + 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: Piece | undefined = 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: PieceId, capturedPieceId: PieceId, location: Position) { + const captured = this.board.pieces[capturedPieceId]; + this.board.pieceCapture(captured); + this.move(capturingPieceId, location, true); + } + + handleCastling(piece: Piece, location: Position): PieceId | undefined { + 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}` as PieceId; + } + + move(pieceId: PieceId, location: Position, capture = false): PieceId | undefined { + 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(): Position { + 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: PieceId[] = this.turn === "WHITE" ? this.board.pieceIdsWhite : this.board.pieceIdsBlack; + const positions = ids.map((pieceId: 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?.active); + const remaining = positions[Math.floor(Math.random() * positions.length)]; + const { col, row } = remaining || { col: "E", row: "1" }; + return { col, row }; + } + } +} + +class View { + element: HTMLElement; + game: Game; + pieces: BoardPieces; + tiles: BoardTiles; + + constructor(element: HTMLElement, game: Game, perspective?: PlayerId) { + 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: PieceId) { + 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: PieceId) { + 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: Position[] = [], moveInner: string = "") { + 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?.length ? "add" : "remove"]("can-move"); + pieceElement.classList[pieceCaptures?.length ? "add" : "remove"]("can-capture"); + if (piece.updateShape) { + piece.updateShape = false; + pieceElement.innerHTML = piece.shape(); + } + } else { + tileElement.classList.remove("occupied"); + } + }); + } + + drawPositions(moves: Position[], captures: Position[]) { + moves?.forEach(({ row, col }) => { + this.tiles[row][col].classList.add("highlight-move"); + this.pieces[this.game.board.tileFind({ row, col })?.data.id]?.classList.add("highlight-move"); + }); + captures?.forEach(({ row, col, capture }) => { + if (capture) { + row = capture.row; + col = capture.col; + } + this.tiles[row][col].classList.add("highlight-capture"); + this.pieces[this.game.board.tileFind({ row, col })?.data.id]?.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: Position) { + 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: PlayerId) { + 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 { + game: Game; + inputSpeedAsap: HTMLInputElement = document.getElementById("speed-asap") as HTMLInputElement; + inputSpeedFast: HTMLInputElement = document.getElementById("speed-fast") as HTMLInputElement; + inputSpeedMedium: HTMLInputElement = document.getElementById("speed-medium") as HTMLInputElement; + inputSpeedSlow: HTMLInputElement = document.getElementById("speed-slow") as HTMLInputElement; + inputRandomBlack: HTMLInputElement = document.getElementById("black-random") as HTMLInputElement; + inputRandomWhite: HTMLInputElement = document.getElementById("white-random") as HTMLInputElement; + inputPerspectiveBlack: HTMLInputElement = document.getElementById("black-perspective") as HTMLInputElement; + inputPerspectiveWhite: HTMLInputElement = document.getElementById("white-perspective") as HTMLInputElement; + view: View; + + constructor(game: Game, view: View) { + 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/src/Games/chess/src/style.css b/src/Games/chess/src/style.css new file mode 100644 index 000000000..d917822f2 --- /dev/null +++ b/src/Games/chess/src/style.css @@ -0,0 +1,325 @@ +: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; + } + + @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 { + 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