fuzzyworld

dao-game / commit

Lots of changes:

773cf7f10b6f5dee2a0e8fb2b7d662444392c274 · Pavel Safronov · 2022-02-26 18:32:00 -0800 · parents 6a9630b23603

Lots of changes:
- animations
- checking for valid moves
 game.js    | 224 +++++++++++++++++++++++++++++++++++++++++++++++++------------
 index.html |   8 +--
 styles.css |  62 ++++++++++++++---
 3 files changed, 236 insertions(+), 58 deletions(-)

diff --git a/game.js b/game.js
index aa4619b..0efce2a 100644
--- a/game.js
+++ b/game.js
@@ -1,10 +1,8 @@
-// DOM references
-const cells = () => Array.from(document.querySelectorAll("td"));
-const redPieces = () => Array.from(document.querySelectorAll(".red-piece"));
-const bluePieces = () => Array.from(document.querySelectorAll(".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"));
+// Constants
+const selectedPieceClassName = 'selected-piece';
+const selectedPlayerTextClassName = 'selected-player-text';
+const activePlayerPieceClassName = 'active-player-piece';
+const blinkingClassName = 'blinking';
 
 // board
 const board = [
@@ -13,16 +11,24 @@ const board = [
     null, 4, 5, null,
     6, null, null, 7
 ];
+const rows = 4;
+const cols = 4;
 
-/** @type {{element: Element, type: string, listener: (event: Event) => void}[]} */
-const listeners = [];
+// DOM references
+const cells = () => Array.from(document.querySelectorAll("td"));
+const redPieces = () => Array.from(document.querySelectorAll(".red-piece"));
+const bluePieces = () => Array.from(document.querySelectorAll(".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"));
 
 // player properties
 let isRedTurn = false;
-/** @type {NodeListOf<Element>} */
-let playerPieces;
 let selectedPieceId = -1;
 
+/** @type {{element: Element, type: string, listener: (event: Event) => void}[]} */
+const listeners = [];
+
 /**
  * @param {Element} element
  * @param {string} eventName
@@ -40,19 +46,50 @@ const removeListeners = () => {
     listeners.length = 0;
 }
 
+class Coord {
+    /**
+     * Creates an instance of Coord.
+     * @param {number} row
+     * @param {number} col
+     * @memberof Coord
+     */
+    constructor(row, col) {
+        this.row = row;
+        this.col = col;
+    }
+}
+/**
+ * @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 { row, col };
+};
+
 const resetBoard = () => {
     // resetPieces
     /** @param {Element} piece */
     const resetPiece = (piece) => {
-        piece.classList.remove(selectedPieceClassName);
-        piece.classList.remove(activePlayerPieceClassName);
+        //piece.classList.remove(selectedPieceClassName);
+        // piece.classList.remove(activePlayerPieceClassName);
     };
     redPieces().forEach(resetPiece);
     bluePieces().forEach(resetPiece);
 
     // resetCells
     cells().forEach(cell => {
-        cell.classList.remove('blinking');
+        cell.classList.remove(blinkingClassName);
+        cell.style.zIndex = 0
     });
 
     // remove all listeners
@@ -61,65 +98,165 @@ const resetBoard = () => {
 
 const setActivePlayerPieces = () => {
     // mark active pieces
-    playerPieces = isRedTurn ? redPieces() : bluePieces()
+    const playerPieces = isRedTurn ? redPieces() : bluePieces()
     playerPieces.forEach(pp => {
         pp.classList.add(activePlayerPieceClassName);
-        // pp.addEventListener('click', selectPiece);
         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) => {
+    // clock face directions
+    const directions = [
+        { 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 coord = boardIndexToCoord(boardIndex);
+
+    const farthestCoords = directions
+        .map(direction => getFarthestCoord(coord, direction))
+        .filter(farCoord => farCoord !== undefined);
+
+    const farthestIndices = farthestCoords.map(coordToBoardIndex);
+    return farthestIndices;
+};
+/**
+ *
+ *
+ * @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;
+        }
+    }
+    return farthestPos;
+};
+
+/**
+ *
+ *
+ * @param {number} pieceId
+ * @return {Element} 
+ */
+const getPieceElementById = (pieceId) => {
+    return document.getElementById(pieceId);
+}
+
 /** @param {Event} event */
 const selectPiece = (event) => {
-    resetBoard();
-    setActivePlayerPieces();
+    // resetBoard();
+    // setActivePlayerPieces();
+    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);
-    const pieceIndex = board.indexOf(pieceId);
 
     // set as selected
     selectedPieceId = pieceId;
     piece.classList.add(selectedPieceClassName);
 
-    // blink allowed moves
-    const allowedMoveIndices = board
-        .map((value, index) => {
-            const isOccupied = index === pieceIndex || value !== null;
-            return isOccupied ? -1 : index;
-        })
-        .filter(v => v >= 0);
+    // 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];
-        // cell.addEventListener('click', () => makeMove(i))
         addListener(cell, 'click', () => makeMove(i));
-        cell.classList.add('blinking');
+        cell.classList.add(blinkingClassName);
     });
 };
 
-const selectedPieceClassName = 'selected-piece';
-const selectedPlayerTextClassName = 'selected-player-text';
-const activePlayerPieceClassName = 'active-player-piece';
-
 const makeMove = (destIndex) => {
     // find current piece
-    const piece = document.getElementById(selectedPieceId);
+    const piece = getPieceElementById(selectedPieceId);
     const pieceId = Number.parseInt(piece.id)
     const pieceIndex = board.indexOf(pieceId);
 
-    // move piece to destination cell
-    const pieceHtml = cells()[pieceIndex].innerHTML;
-    cells()[pieceIndex].innerHTML = '';
-    cells()[destIndex].innerHTML = pieceHtml;
+    // do animation logic
+    const startCoords = boardIndexToCoord(pieceIndex);
+    const endCoords = boardIndexToCoord(destIndex);
+    const coordDiff = new Coord(
+        endCoords.row - startCoords.row,
+        endCoords.col - startCoords.col);
+
+    const cellWidth = cells()[0].clientWidth;
+    const cellHeight = cells()[0].clientHeight;
+
+    const diffX = coordDiff.col * cellWidth;
+    const diffY = coordDiff.row * cellHeight;
 
-    // move piece on board
-    board[pieceIndex] = null;
-    board[destIndex] = pieceId;
+    // const startCell = cells()[pieceIndex];
+    // const destCell = cells()[destIndex];
 
-    checkForWin();
-    nextMove();
+    removeListeners();
+
+    const animation = piece.animate([
+        // keyframes
+        { opacity: 1 },
+        { opacity: 0 },
+    ], {
+        duration: 500,
+        iterations: 1
+    });
+
+    animation.onfinish = () => {
+        // move piece to destination cell
+        const pieceHtml = cells()[pieceIndex].innerHTML;
+        cells()[pieceIndex].innerHTML = '';
+        cells()[destIndex].innerHTML = pieceHtml;
+
+        // move piece on board
+        board[pieceIndex] = null;
+        board[destIndex] = pieceId;
+
+        checkForWin();
+        const elem = getPieceElementById(pieceId);
+        elem.classList.remove(selectedPieceClassName);
+        nextMove();
+    };
 }
 
 const nextMove = () => {
@@ -140,12 +277,13 @@ const nextMove = () => {
         redTurnText().forEach(t => t.classList.add(selectedPlayerTextClassName));
     }
 
+    selectedPieceId = -1;
     resetBoard();
     setActivePlayerPieces();
 };
 
 const checkForWin = () => {
-
+    console.error(`testing win for ${isRedTurn ? 'red' : 'blue'}`);
 };
 
 
diff --git a/index.html b/index.html
index 50e6c89..817ac73 100644
--- a/index.html
+++ b/index.html
@@ -12,7 +12,7 @@
     <body>
         <main>
             <div class="red-turn-text mobile">
-                Red's turn
+                Red
             </div>
 
             <table>
@@ -44,16 +44,16 @@
 
             <div class="desktop">
                 <div class="red-turn-text">
-                    Red's turn
+                    Red
                 </div>
                 <p id="divider">|</p>
                 <div class="blue-turn-text">
-                    Blue's turn
+                    Blue
                 </div>
             </div>
 
             <div class="blue-turn-text mobile">
-                Blue's turn
+                Blue
             </div>
         </main>
     </body>
diff --git a/styles.css b/styles.css
index cd48150..777906d 100644
--- a/styles.css
+++ b/styles.css
@@ -19,6 +19,7 @@ body {
 div {
     display: flex;
     justify-content: space-between;
+    z-index: 0;
 }
 
 .mobile {
@@ -71,26 +72,28 @@ p {
 .red-piece {
     width: 20px;
     height: 20px;
-    padding: 12px;
+    padding: 10px;
     background-color: crimson;
-    border: 1px solid white;
     border-radius: 50px;
+    animation: activeToDisabled 1s 1;
 }
 .blue-piece {
     width: 20px;
     height: 20px;
-    padding: 12px;
+    padding: 10px;
     background-color: mediumblue;
-    border: 1px solid white;
     border-radius: 50px;
+    animation: activeToDisabled 1s 1;
 }
 
 .active-player-piece {
-    border: 5px solid white;
+    width: 35px;
+    height: 35px;
+    animation: disabledToActive 1s 1;
 }
 
 .selected-piece {
-    border-color: greenyellow;
+    animation: activePulsing 1s infinite;
 }
 
 .selected-player-text {
@@ -98,17 +101,54 @@ p {
 }
 
 .blinking {
-    /* opacity: .5; */
-    /* background-color: brown; */
-    /* color: aqua; */
-    animation: blinkingText 1s infinite;
+    animation: blinkingText 1.5s infinite;
 }
 @keyframes blinkingText {
     0%{opacity: 1;}
-    50%{opacity: .5;}
+    50%{opacity: .3;}
     100%{opacity: 1;}
 }
 
+@keyframes disabledToActive {
+    from {
+        width: 20px;
+        height: 20px;
+    }
+    to {
+        width: 35px;
+        height: 35px;
+    }
+}
+@keyframes activeToDisabled {
+    from {
+        width: 35px;
+        height: 35px;
+    }
+    to {
+        width: 20px;
+        height: 20px;
+    }
+}
+
+
+/* .pulsing {
+    animation: pulsingSize 1.5s infinite;
+} */
+@keyframes activePulsing {
+    from {
+        width: 35px;
+        height: 35px;
+    }
+    50% {
+        width: 25px;
+        height: 25px;
+    }
+    to {
+        width: 35px;
+        height: 35px;
+    }
+}
+
 /* breaks */
 
 /* @media screen and (max-width: 560px) {