fuzzyworld

dao-game / commit

More refactoring to make everything an object and isolate everything.

cc5cd8495d4ccedc60f37ecdbaf0f53cd2fcc449 · Pavel Safronov · 2022-03-01 00:41:25 -0800 · parents 8f71d3dbcbf2

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