dao-game / commit
Merge branch 'undo-reset' into 'main'
game.js | 569 +++++++++++++++++++++++++++++++++---------------------------- index.html | 20 +-- styles.css | 66 ++++--- 3 files changed, 364 insertions(+), 291 deletions(-) diff --git a/game.js b/game.js index 74909e5..19547fc 100644 --- a/game.js +++ b/game.js @@ -6,7 +6,9 @@ class Coord { * @memberof Coord */ constructor(row, col) { + /** @type {number} */ this.row = row; + /** @type {number} */ this.col = col; } @@ -36,235 +38,143 @@ class Coord { const areEqual = (this.col === otherCoord.col && this.row === otherCoord.row); return areEqual; } + + // clock face directions + static get allDirections() { + const allDirections = [ + { row: 1, col: 0 }, // up + { row: 1, col: 1 }, // up-right + { row: 0, col: 1 }, // right + { row: -1, col: 1 }, // right-down + { row: -1, col: 0 }, // down + { row: -1, col: -1 }, // down-left + { row: 0, col: -1 }, // left + { row: 1, col: -1 }, // left-up + ] + return allDirections; + } + } -// Constants -const selectedPieceClassName = 'selected-piece'; -const selectedPlayerTextClassName = 'selected-player-text'; -const activePlayerPieceClassName = 'active-player-piece'; -const blinkingClassName = 'blinking'; - -const standardBoard = [ - 0, null, null, 4, - null, 1, 5, null, - null, 6, 2, null, - 7, null, null, 3 -]; -const testBoard = [ - 4, null, null, null, - null, 0, 1, 5, - null, 2, 3, null, - 6, null, null, 7 -]; - -const board = standardBoard; -const rows = 4; -const cols = 4; -// clock face directions -const allDirections = [ - { row: 1, col: 0 }, // up - { row: 1, col: 1 }, // up-right - { row: 0, col: 1 }, // right - { row: -1, col: 1 }, // right-down - { row: -1, col: 0 }, // down - { row: -1, col: -1 }, // down-left - { row: 0, col: -1 }, // left - { row: 1, col: -1 }, // left-up -] - -let isWhite = true; -const table = document.getElementById('table'); -for (let r = 0; r < rows; r++) { - const tr = document.createElement('tr'); - table.appendChild(tr); - for (let c = 0; c < cols; c++) { - const td = document.createElement('td'); - tr.appendChild(td); - td.className = isWhite ? 'white' : 'black'; - isWhite = !isWhite; +const constants = { + selectedPieceClassName: 'selected-piece', + selectedPlayerTextClassName: 'selected-player-text', + activePlayerPieceClassName: 'active-player-piece', + blinkingClassName: 'blinking', + rows: 4, + cols: 4, +} + +const gameBoard = { + board: [ + 0, null, null, 4, + null, 1, 5, null, + null, 6, 2, null, + 7, null, null, 3 + ], + // testBoard: [ + // 4, null, null, null, + // null, 0, 1, 5, + // null, 2, 3, null, + // 6, null, null, 7 + // ], + /** + * @param {Coord} data + * @return {number} + */ + coordToBoardIndex: ({ row, col }) => { + const index = row * 4 + col; + return index; + }, + /** + * @param {number} index + * @return {Coord} + */ + boardIndexToCoord: (index) => { + const col = index % 4; + const row = Math.floor(index / 4); + return new Coord(row, col); + } +} + +class Move { + /** + * Creates an instance of Move. + * @param {number} sourceIndex + * @param {number} destIndex + * @memberof Move + */ + constructor(sourceIndex, destIndex) { + /** @type {number} */ + this.sourceIndex = sourceIndex; + /** @type {number} */ + this.destIndex = destIndex; } - isWhite = !isWhite; } +const player = { + isRedTurn: false, + /** @type {number | undefined} */ + selectedPieceId: undefined, + /** @type {Move[]} */ + moveHistory: [], +} + // DOM references const defaultRedPiece = document.getElementById('default-red-piece'); const defaultBluePiece = document.getElementById('default-blue-piece'); const cells = () => Array.from(document.querySelectorAll("td")); - -board.forEach((value, index) => { - if (value !== null) { - const isRed = value < 4; - const defaultPiece = isRed ? defaultRedPiece : defaultBluePiece; - const newPiece = defaultPiece.cloneNode(true); - newPiece.id = value; - cells()[index].appendChild(newPiece); - } -}); - -const redPieces = () => Array.from(document.querySelectorAll(".red-piece")); -const bluePieces = () => Array.from(document.querySelectorAll(".blue-piece")); +const redPieces = () => Array.from(document.querySelectorAll('.red-piece:not(#default-red-piece')); +const bluePieces = () => Array.from(document.querySelectorAll('.blue-piece:not(#default-blue-piece')); const redTurnText = () => Array.from(document.querySelectorAll(".red-turn-text")); const blueTurnText = () => Array.from(document.querySelectorAll(".blue-turn-text")); -const divider = () => Array.from(document.querySelector("#divider")); +const resetButton = document.getElementById('reset-button'); +const undoButton = document.getElementById('undo-button'); -// player properties -let isRedTurn = false; -let selectedPieceId = -1; +const listenerManager = { + /** @type {{element: Element, type: string, listener: (event: Event) => void}[]} */ + listeners: [], -/** @type {{element: Element, type: string, listener: (event: Event) => void}[]} */ -const listeners = []; - -/** - * @param {Element} element - * @param {string} eventName - * @param {Event => void} callback - */ -const addListener = (element, type, listener) => { - element.addEventListener(type, listener); - listeners.push({ element, type, listener }); -} -const removeListeners = () => { - listeners.forEach(value => { - const { element, type, listener } = value; - element.removeEventListener(type, listener); - }); - listeners.length = 0; + /** + * @param {Element} element + * @param {string} eventName + * @param {Event => void} callback + */ + addListener: (element, type, listener) => { + const { listeners } = listenerManager; + element.addEventListener(type, listener); + listeners.push({ element, type, listener }); + }, + + removeListeners: () => { + const { listeners } = listenerManager; + listeners.forEach(value => { + const { element, type, listener } = value; + element.removeEventListener(type, listener); + }); + listeners.length = 0; + } } -/** - * @param {Coord} data - * @return {number} - */ -const coordToBoardIndex = ({ row, col }) => { - const index = row * 4 + col; - return index; -}; -/** - * @param {number} index - * @return {Coord} - */ -const boardIndexToCoord = (index) => { - const col = index % 4; - const row = Math.floor(index / 4); - return new Coord(row, col); -}; - const resetBoard = () => { // resetCells cells().forEach(cell => { - cell.classList.remove(blinkingClassName); + cell.classList.remove(constants.blinkingClassName); }); // remove all listeners - removeListeners(); + listenerManager.removeListeners(); }; -const setActivePlayerPieces = () => { - // mark active pieces - const playerPieces = isRedTurn ? redPieces() : bluePieces() - playerPieces.forEach(pp => { - pp.classList.add(activePlayerPieceClassName); - addListener(pp, 'click', selectPiece); - }); - const otherPieces = isRedTurn ? bluePieces() : redPieces(); - otherPieces.forEach(op => { - if (op.classList.contains(activePlayerPieceClassName)) { - op.classList.toggle(activePlayerPieceClassName); - } - }); -}; - -/** - * @param {number} pieceId - * @returns {number[]} - */ -const getAllowedMoveIndicesForIndex = (boardIndex) => { - const coord = boardIndexToCoord(boardIndex); - - const farthestCoords = allDirections - .map(direction => getFarthestCoord(coord, direction)) - .filter(farCoord => farCoord !== undefined); - - const farthestIndices = farthestCoords.map(coordToBoardIndex); - return farthestIndices; -}; /** - * - * - * @param {Coord} startCoord - * @param {Coord} direction - * @returns {Coord} + * @param {number} sourceIndex + * @param {number} destIndex */ -const getFarthestCoord = (startCoord, direction) => { - let farthestPos = undefined; - let testPos = startCoord; - while (true) { - const pos = new Coord(testPos.row + direction.row, testPos.col + direction.col); - const index = coordToBoardIndex(pos); - const withinBounds = - pos.row >= 0 && pos.row < rows && - pos.col >= 0 && pos.col < cols; - const isOccupied = Number.isInteger(board[index]); - if (isOccupied || !withinBounds) { - break; - } else { - farthestPos = pos; - testPos = pos; - } - } - return farthestPos; -}; - -/** - * - * - * @param {number} pieceId - * @return {Element} - */ -const getPieceElementById = (pieceId) => { - return document.getElementById(pieceId); -} - -/** @param {Event} event */ -const selectPiece = (event) => { - const previousSelectedPieceId = selectedPieceId; - if (previousSelectedPieceId >= 0) { - const previousPiece = getPieceElementById(previousSelectedPieceId); - if (previousPiece.classList.contains(selectedPieceClassName)) { - previousPiece.classList.toggle(selectedPieceClassName); - } - cells().forEach(cell => { - cell.classList.remove(blinkingClassName); - }); - } - - // read event data - /** @type {Element} */ - const piece = event.target; - const pieceId = parseInt(piece.id); - - // set as selected - selectedPieceId = pieceId; - piece.classList.add(selectedPieceClassName); - - // hook up allowed moves for: - // 1. blink - // 2. complete the move - const pieceIndex = board.indexOf(pieceId); - const allowedMoveIndices = getAllowedMoveIndicesForIndex(pieceIndex); - allowedMoveIndices.forEach(i => { - const cell = cells()[i]; - addListener(cell, 'click', async () => await makeMove(i)); - cell.classList.add(blinkingClassName); - }); -}; - -const makeMove = async (destIndex) => { - removeListeners(); - +const movePiece = async (sourceIndex, destIndex) => { + const pieceId = gameBoard.board[sourceIndex]; // find current piece - const piece = getPieceElementById(selectedPieceId); + const piece = document.getElementById(pieceId); // unselect piece - piece.classList.remove(selectedPieceClassName); + piece.classList.remove(constants.selectedPieceClassName); // create clone for new position const destPiece = piece.cloneNode(true); @@ -288,78 +198,178 @@ const makeMove = async (destIndex) => { // remove old piece piece.parentNode.removeChild(piece); - board[board.indexOf(selectedPieceId)] = null; + gameBoard.board[sourceIndex] = null; // set new piece - board[destIndex] = selectedPieceId; - - checkForWin(); - nextMove(); -} + gameBoard.board[destIndex] = pieceId; +}; const nextMove = () => { + const setActivePlayerPieces = () => { + /** @param {Event} event */ + const selectPiece = (event) => { + /** + * @param {number} pieceId + * @returns {number[]} + */ + const getAllowedMoveIndicesForIndex = (boardIndex) => { + /** + * + * + * @param {Coord} startCoord + * @param {Coord} direction + * @returns {Coord} + */ + const getFarthestCoord = (startCoord, direction) => { + let farthestPos = undefined; + let testPos = startCoord; + while (true) { + const pos = new Coord(testPos.row + direction.row, testPos.col + direction.col); + const index = gameBoard.coordToBoardIndex(pos); + const withinBounds = + pos.row >= 0 && pos.row < constants.rows && + pos.col >= 0 && pos.col < constants.cols; + const isOccupied = Number.isInteger(gameBoard.board[index]); + if (isOccupied || !withinBounds) { + break; + } else { + farthestPos = pos; + testPos = pos; + } + } + return farthestPos; + }; + + const coord = gameBoard.boardIndexToCoord(boardIndex); + + const farthestCoords = Coord.allDirections + .map(direction => getFarthestCoord(coord, direction)) + .filter(farCoord => farCoord !== undefined); + + const farthestIndices = farthestCoords.map(c => gameBoard.coordToBoardIndex(c)); + return farthestIndices; + }; + + const previousSelectedPieceId = player.selectedPieceId; + if (previousSelectedPieceId !== undefined) { + const previousPiece = document.getElementById(previousSelectedPieceId); + if (previousPiece.classList.contains(constants.selectedPieceClassName)) { + previousPiece.classList.toggle(constants.selectedPieceClassName); + } + cells().forEach(cell => { + cell.classList.remove(constants.blinkingClassName); + }); + } + + // read event data + /** @type {Element} */ + const piece = event.target; + const pieceId = parseInt(piece.id); + + // set as selected + player.selectedPieceId = pieceId; + piece.classList.add(constants.selectedPieceClassName); + + // hook up allowed moves for: + // 1. blink + // 2. complete the move + const pieceIndex = gameBoard.board.indexOf(pieceId); + const allowedMoveIndices = getAllowedMoveIndicesForIndex(pieceIndex); + allowedMoveIndices.forEach(i => { + const cell = cells()[i]; + listenerManager.addListener(cell, 'click', async () => await makeMove(i)); + cell.classList.add(constants.blinkingClassName); + }); + }; + + const makeMove = async (destIndex) => { + listenerManager.removeListeners(); + + const sourceIndex = gameBoard.board.indexOf(player.selectedPieceId); + player.moveHistory.push(new Move(sourceIndex, destIndex)); + + await movePiece(sourceIndex, destIndex); + + checkForWin(); + nextMove(); + } + + // mark active pieces + const playerPieces = player.isRedTurn ? redPieces() : bluePieces() + playerPieces.forEach(pp => { + pp.classList.add(constants.activePlayerPieceClassName); + listenerManager.addListener(pp, 'click', selectPiece); + }); + const otherPieces = player.isRedTurn ? bluePieces() : redPieces(); + otherPieces.forEach(op => { + if (op.classList.contains(constants.activePlayerPieceClassName)) { + op.classList.toggle(constants.activePlayerPieceClassName); + } + }); + }; + // reset text - redTurnText().forEach(t => t.classList.remove(selectedPlayerTextClassName)); - blueTurnText().forEach(t => t.classList.remove(selectedPlayerTextClassName)); + redTurnText().forEach(t => t.classList.remove(constants.selectedPlayerTextClassName)); + blueTurnText().forEach(t => t.classList.remove(constants.selectedPlayerTextClassName)); // changePlayer // 1. flip flag // 2. set text // 3. set click callbacks - if (isRedTurn) { - isRedTurn = false; - blueTurnText().forEach(t => t.classList.add(selectedPlayerTextClassName)); + if (player.isRedTurn) { + player.isRedTurn = false; + blueTurnText().forEach(t => t.classList.add(constants.selectedPlayerTextClassName)); } else { - isRedTurn = true; - redTurnText().forEach(t => t.classList.add(selectedPlayerTextClassName)); + player.isRedTurn = true; + redTurnText().forEach(t => t.classList.add(constants.selectedPlayerTextClassName)); } - selectedPieceId = -1; + player.selectedPieceId = undefined; resetBoard(); setActivePlayerPieces(); }; -/** - * @param {Element[]} elements - * @returns {number[]} - */ -const elementsToBoardIndexes = (elements) => { - const indices = elements.map(pp => { - const pieceId = Number.parseInt(pp.id); - const pieceIndex = board.indexOf(pieceId); - return pieceIndex; - }); - return indices; -}; -/** - * @param {Element[]} elements - * @return {Coord[]} - */ -const elementsToCoords = (elements) => { - const indices = elementsToBoardIndexes(elements); - const coords = indices.map(pieceIndex => boardIndexToCoord(pieceIndex)); - return coords; -}; - -/** - * @param {Coord[]} coords - * @returns {boolean} - */ -const doCoordsFormSquare = (coords) => { - const allRows = coords.map(c => c.row); - const allCols = coords.map(c => c.col); - const min = new Coord(Math.min(...allRows), Math.min(...allCols)); - const max = new Coord(Math.max(...allRows), Math.max(...allCols)); - const diff = max.sub(min); - const isSquare = (diff.row === 1 && diff.col === 1); - return isSquare; -}; - const checkForWin = () => { + /** + * @param {Element[]} elements + * @return {Coord[]} + */ + const elementsToCoords = (elements) => { + /** + * @param {Element[]} elements + * @returns {number[]} + */ + const elementsToBoardIndexes = (elements) => { + const indices = elements.map(pp => { + const pieceId = Number.parseInt(pp.id); + const pieceIndex = gameBoard.board.indexOf(pieceId); + return pieceIndex; + }); + return indices; + }; + + const indices = elementsToBoardIndexes(elements); + const coords = indices.map(pieceIndex => gameBoard.boardIndexToCoord(pieceIndex)); + return coords; + }; + /** + * @param {Coord[]} coords + * @returns {boolean} + */ + const doCoordsFormSquare = (coords) => { + const allRows = coords.map(c => c.row); + const allCols = coords.map(c => c.col); + const min = new Coord(Math.min(...allRows), Math.min(...allCols)); + const max = new Coord(Math.max(...allRows), Math.max(...allCols)); + const diff = max.sub(min); + const isSquare = (diff.row === 1 && diff.col === 1); + return isSquare; + }; + const playerCoords = elementsToCoords( - isRedTurn ? redPieces() : bluePieces()) + player.isRedTurn ? redPieces() : bluePieces()) const otherCoords = elementsToCoords( - isRedTurn ? bluePieces() : redPieces()); + player.isRedTurn ? bluePieces() : redPieces()); /** @type {Coord[]} */ const squareCoords = [ new Coord(0, 0), @@ -382,7 +392,7 @@ const checkForWin = () => { const allInCorners = playerCoords.every(pc => squareCoords.findIndex(sc => pc.equals(sc)) >= 0); const allInSquare = doCoordsFormSquare(playerCoords); - const winnerName = isRedTurn ? 'Red' : 'Blue'; + const winnerName = player.isRedTurn ? 'Red' : 'Blue'; let winningMessage = undefined; if (allInRow) { winningMessage = `${winnerName} wins by forming a row`; @@ -403,7 +413,52 @@ const checkForWin = () => { } }; +const init = () => { + // create board and pieces + let isWhite = true; + const table = document.getElementById('table'); + for (let r = 0; r < constants.rows; r++) { + const tr = document.createElement('tr'); + table.appendChild(tr); + for (let c = 0; c < constants.cols; c++) { + const td = document.createElement('td'); + tr.appendChild(td); + td.className = isWhite ? 'white' : 'black'; + isWhite = !isWhite; + } + isWhite = !isWhite; + } + gameBoard.board.forEach((value, index) => { + if (value !== null) { + const isRed = value < 4; + const defaultPiece = isRed ? defaultRedPiece : defaultBluePiece; + const newPiece = defaultPiece.cloneNode(true); + newPiece.id = value; + cells()[index].appendChild(newPiece); + } + }); + + // hook up buttons + resetButton.addEventListener('click', () => { + if (player.moveHistory.length === 0) { + return; + } + + if (confirm('Reset?')) { + window.location.reload(); + } + }); + undoButton.addEventListener('click', () => { + if (player.moveHistory.length === 0) { + return; + } + + const { destIndex, sourceIndex } = player.moveHistory.pop(); + movePiece(destIndex, sourceIndex); + nextMove(); + }); +}; // start! -// checkForWin(); +init(); nextMove(); \ No newline at end of file diff --git a/index.html b/index.html index 64b6d31..db50b20 100644 --- a/index.html +++ b/index.html @@ -16,26 +16,26 @@ <p class='piece blue-piece' id='default-blue-piece'></p> </div> <main> - <div class="red-turn-text mobile"> - Red's turn - </div> - <table id="table"> </table> - <div class="desktop"> + <div class="controls"> <span class="red-turn-text"> Red's turn </span> - <span id="divider">|</span> + <span class="divider">|</span> + <button type="button" class="button" id="undo-button"> + Undo + </button> + <span class="divider">|</span> + <button type="button" class="button" id="reset-button"> + Reset + </button> + <span class="divider">|</span> <span class="blue-turn-text"> Blue's turn </span> </div> - - <div class="blue-turn-text mobile"> - Blue's turn - </div> </main> <hr /> <div class="title"> diff --git a/styles.css b/styles.css index 9fcb6de..1cd85a7 100644 --- a/styles.css +++ b/styles.css @@ -1,6 +1,7 @@ :root { --default-size: 5px; --active-size: 40px; + --cell-size: 80px; /* --p1-color: crimson; --p2-color: mediumblue; */ } @@ -40,39 +41,54 @@ div { font-size: 40px; } -.mobile { - display: none; -} - -.desktop { - margin: 0; - width: 100%; -} - td { - width: 65px; - height: 65px; + width: var(--cell-size); + height: var(--cell-size); text-align: center; } -#divider { +.controls { font-size: 90px; margin-left: 15px; margin-right: 15px; -} - -.red-turn-text { - text-align: center; - color: lightgray; font-family: "open sans"; font-size: 55px; + border-radius: 10px; + padding: 10px; +} + +.divider { + font-size: 90px; + margin-left: 15px; + margin-right: 15px; } -.blue-turn-text { +.red-turn-text, .blue-turn-text { text-align: center; - font-family: "open sans"; - font-size: 55px; color: lightgray; + background-color: white; + /* border: 1px solid black; */ + border-radius: 10px; + padding: 10px; +} + +.red-turn-text.selected-player-text { + background-color: red; +} + +.blue-turn-text.selected-player-text { + background-color: blue; +} + +.selected-player-text { + color: white; +} + +.button { + border-radius: 10px; + padding: 10px; + background-color: darkseagreen; + font-size: 60px; } .white { @@ -110,10 +126,6 @@ td { animation: activePulsing 1s infinite; } -.selected-player-text { - color: black; -} - .info { text-align: center; font-family: "open sans"; @@ -129,6 +141,12 @@ td { 100%{opacity: 1;} } +@keyframes fade-in { + 0%{opacity: 0;} + 50%{opacity: .5;} + 100%{opacity: 1;} +} + @keyframes disabledToActive { from { width: var(--default-size);