dao-game / commit
More refactoring to make everything an object and isolate everything.
game.js | 492 ++++++++++++++++++++++++++++++++-------------------------------- 1 file changed, 247 insertions(+), 245 deletions(-) diff --git a/game.js b/game.js index ba642c7..5f4b8ac 100644 --- a/game.js +++ b/game.js @@ -36,45 +36,72 @@ 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; + } + +} + +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); + } } -// Constants -const selectedPieceClassName = 'selected-piece'; -const selectedPlayerTextClassName = 'selected-player-text'; -const activePlayerPieceClassName = 'active-player-piece'; -const blinkingClassName = 'blinking'; -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 -] -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; - -// player properties -let isRedTurn = false; -let selectedPieceId = -1; -/** @type {Coord[]} */ -const moveHistory = []; +const player = { + isRedTurn: false, + /** @type {number | undefined} */ + selectedPieceId: undefined, + /** @type {Coord[]} */ + moveHistory: [], +} // DOM references const defaultRedPiece = document.getElementById('default-red-piece'); @@ -92,241 +119,218 @@ resetButton.addEventListener('click', () => { }); // create board and pieces -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'; +(() => { + 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; } - isWhite = !isWhite; -} -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); - } -}); - -/** @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); + 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); + } }); - 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 listenerManager = { + /** @type {{element: Element, type: string, listener: (event: Event) => void}[]} */ + listeners: [], + + /** + * @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; + } +} const resetBoard = () => { // resetCells cells().forEach(cell => { - cell.classList.remove(blinkingClassName); + cell.classList.remove(constants.blinkingClassName); }); // remove all listeners - 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); - } - }); + listenerManager.removeListeners(); }; -/** - * - * - * @param {number} pieceId - * @return {Element} - */ -const getPieceElementById = (pieceId) => { - return document.getElementById(pieceId); -} - -/** @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 = 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; +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); + }); } - return farthestPos; - }; - - const coord = boardIndexToCoord(boardIndex); - - const farthestCoords = allDirections - .map(direction => getFarthestCoord(coord, direction)) - .filter(farCoord => farCoord !== undefined); - - const farthestIndices = farthestCoords.map(coordToBoardIndex); - return farthestIndices; - }; + // 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 previousSelectedPieceId = selectedPieceId; - if (previousSelectedPieceId >= 0) { - const previousPiece = getPieceElementById(previousSelectedPieceId); - if (previousPiece.classList.contains(selectedPieceClassName)) { - previousPiece.classList.toggle(selectedPieceClassName); + const makeMove = async (destIndex) => { + listenerManager.removeListeners(); + + // find current piece + const piece = document.getElementById(player.selectedPieceId); + // unselect piece + piece.classList.remove(constants.selectedPieceClassName); + + // create clone for new position + const destPiece = piece.cloneNode(true); + // add to destination cell + cells()[destIndex].appendChild(destPiece); + + // start animations + const destAnimation = cells()[destIndex].animate([{ opacity: 0 }, { opacity: 1 }], { duration: 1000, iterations: 1 }) + const sourceAnimation = piece.animate([{ + opacity: 1, + width: '45px', + height: '45px' + }, { + opacity: 0, + width: '5px', + height: '5px' + },], { duration: 1000, iterations: 1 }); + + // await animations + await Promise.all([sourceAnimation.finished, destAnimation.finished]); + + // remove old piece + piece.parentNode.removeChild(piece); + gameBoard.board[gameBoard.board.indexOf(player.selectedPieceId)] = null; + // set new piece + gameBoard.board[destIndex] = player.selectedPieceId; + + checkForWin(); + nextMove(); } - 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(); - - // find current piece - const piece = getPieceElementById(selectedPieceId); - // unselect piece - piece.classList.remove(selectedPieceClassName); - - // create clone for new position - const destPiece = piece.cloneNode(true); - // add to destination cell - cells()[destIndex].appendChild(destPiece); - - // start animations - const destAnimation = cells()[destIndex].animate([{ opacity: 0 }, { opacity: 1 }], { duration: 1000, iterations: 1 }) - const sourceAnimation = piece.animate([{ - opacity: 1, - width: '45px', - height: '45px' - }, { - opacity: 0, - width: '5px', - height: '5px' - },], { duration: 1000, iterations: 1 }); - - // await animations - await Promise.all([sourceAnimation.finished, destAnimation.finished]); - - // remove old piece - piece.parentNode.removeChild(piece); - board[board.indexOf(selectedPieceId)] = null; - // set new piece - board[destIndex] = selectedPieceId; - - 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); + } + }); + }; -const nextMove = () => { // 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(); }; - const checkForWin = () => { /** * @param {Element[]} elements @@ -340,14 +344,14 @@ const checkForWin = () => { const elementsToBoardIndexes = (elements) => { const indices = elements.map(pp => { const pieceId = Number.parseInt(pp.id); - const pieceIndex = board.indexOf(pieceId); + const pieceIndex = gameBoard.board.indexOf(pieceId); return pieceIndex; }); return indices; }; const indices = elementsToBoardIndexes(elements); - const coords = indices.map(pieceIndex => boardIndexToCoord(pieceIndex)); + const coords = indices.map(pieceIndex => gameBoard.boardIndexToCoord(pieceIndex)); return coords; }; /** @@ -365,9 +369,9 @@ const checkForWin = () => { }; 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), @@ -390,7 +394,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`; @@ -411,7 +415,5 @@ const checkForWin = () => { } }; - // start! -// checkForWin(); nextMove(); \ No newline at end of file