Created
October 31, 2021 03:54
-
-
Save cagataycali/bf9ec64725ffa400ba9a0a601afa5115 to your computer and use it in GitHub Desktop.
[JavaScript] Tic-tac-to NxN
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta http-equiv="X-UA-Compatible" content="IE=edge" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Tic-Tac-Toe NxN | Cagatay Cali</title> | |
| <!-- HTML & CSS Boilerplate author: https://dev.to/bornasepic/pure-and-simple-tic-tac-toe-with-javascript-4pgn --> | |
| <style> | |
| section { | |
| text-align: center; | |
| } | |
| .cell { | |
| font-family: "Permanent Marker", cursive; | |
| width: 100px; | |
| height: 100px; | |
| box-shadow: 0 0 0 1px #333333; | |
| border: 1px solid #333333; | |
| cursor: pointer; | |
| line-height: 100px; | |
| font-size: 60px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="app"> | |
| <section> | |
| <div id="game-title">Tic-Tac-Toe</div> | |
| <div id="game-container"></div> | |
| <div id="game-status"> | |
| <button id="game-reset" onclick="handleClickReset()"> | |
| Reset game | |
| </button> | |
| </div> | |
| </section> | |
| </div> | |
| <script> | |
| const app = document.getElementById("app"); | |
| const gameInfo = document.getElementById("game-info"); | |
| const gameStatus = document.getElementById("game-status"); | |
| let gameContainer = document.getElementById("game-container"); | |
| let turn = "red"; | |
| const gameState = { | |
| red: {}, | |
| blue: {}, | |
| }; | |
| /* | |
| Game initials | |
| */ | |
| const size = prompt("What size game do you want?", 3); | |
| // const size = 4; | |
| function generateCells() { | |
| for (let index = 0; index < size * size; index++) { | |
| const cell = document.createElement("div"); | |
| cell.classList.add("cell"); | |
| cell.appendChild = document.createTextNode(""); | |
| cell.setAttribute("data-cell-index", index); | |
| cell.addEventListener("click", handleClickCell); | |
| gameContainer.appendChild(cell); | |
| } | |
| } | |
| function injectStylesForCellSize() { | |
| const sheet = document.createElement("style"); | |
| sheet.innerHTML = ` | |
| #game-container { | |
| display: | |
| grid; | |
| grid-template-columns: | |
| repeat(${Number(size)},auto); | |
| width: ${Number(size) * 100}px; | |
| margin: 50px auto; | |
| }`; | |
| document.body.appendChild(sheet); | |
| } | |
| generateCells(); | |
| injectStylesForCellSize(); | |
| /* | |
| Game functionality | |
| */ | |
| function changeTurn() { | |
| turn = turn === "red" ? "blue" : "red"; | |
| } | |
| // Calculate the winning spots for this player | |
| // Our turn is red and the player clicked 4. cell (fifth cell) | |
| /* | |
| size = 3 | |
| per row basis: | |
| gameState = { | |
| red: { | |
| 3: true, | |
| // 4: true, 4 clicked | |
| 5: true, | |
| } | |
| calculate with = cell % size = 1 | |
| 1 means, 1 left 1 right 1 top 1 down cell exists. | |
| In tic-tac-toe, there's 3 ways to win, | |
| left to right, | |
| top to bottom, | |
| diagonal | |
| How we can check: | |
| calculate row start and end | |
| if row start and end already selected, loop whole row for validation | |
| calculate diagonal start and end | |
| if diagonal start and end already selected loop whole row for validation | |
| calculate column start and end | |
| if column start and end already selected loop whole column for validation | |
| } | |
| // Example | |
| gameState = { | |
| red: {0: true, 1: true, 2: true, 3: true, 4: true} | |
| } | |
| turn = 'red' | |
| cellIndex = 1 | |
| size = 4 | |
| calculateRow(gameState, turn, cellIndex, size) | |
| calculateColumn(gameState, turn, cellIndex, size) | |
| calculateDiagonal(gameState, turn, cellIndex, size) | |
| if we are in this if condition, the player selected the cell and left side is already selected, | |
| */ | |
| function calculateWinningSpots(gameState, turn, cellIndex, size) { | |
| // Let's make the player sad first. :) | |
| let isWinner = false; | |
| // Calculate the given row's start and end | |
| function calculateRow(gameState, turn, cellIndex, size) { | |
| const cellModulus = cellIndex % size; | |
| let rowStart = cellIndex - cellModulus; // 4 % 1 means 1 cell left is the row start. | |
| let rowEnd = rowStart + size - 1; | |
| return { rowStart, rowEnd }; | |
| } | |
| function calculateColumn(gameState, turn, cellIndex, size) { | |
| const columnStart = cellIndex % size; // 9 % 4 => 1, 1 is the starting point of the column. | |
| const columnEnd = size * (size - 1) + columnStart; | |
| return { columnStart, columnEnd }; | |
| } | |
| function calculateDiagonal(gameState, turn, cellIndex, size) { | |
| // Check am I on diagonal or in path of diagonal? | |
| const rigthToLeftDiagonal = size - 1; // 4 - 1 = 3 is the right top diagonal corner. | |
| const leftToRigthDiagonal = size + 1; // 4 - 1 = 3 is the right top diagonal corner. | |
| let [onRightToLeftDiagonalPath, onLeftToRightDiagonalPath] = [ | |
| false, | |
| false, | |
| ]; | |
| if (cellIndex % rigthToLeftDiagonal === 0) { | |
| // Our cell is in right top to left bottom diagonal's path. | |
| onRightToLeftDiagonalPath = true; | |
| } else if (cellIndex % leftToRigthDiagonal === 0) { | |
| // Our cell is in left top to right bottom diagonal's path. | |
| onLeftToRightDiagonalPath = true; | |
| } else { | |
| // Nothing have to be considered right now, our selection is not in any diagonal path. | |
| } | |
| return { | |
| onRightToLeftDiagonalPath, | |
| onLeftToRightDiagonalPath, | |
| rightTop: size - 1, // right top cell index | |
| leftTop: 0, // left top cell index | |
| leftBottom: size * (size - 1), | |
| rightBottom: size * (size - 1) + size - 1, | |
| }; | |
| } | |
| // ---------- Row calculation ---------- | |
| // check the row is already in hashmap? | |
| const { rowStart, rowEnd } = calculateRow( | |
| gameState, | |
| turn, | |
| cellIndex, | |
| size | |
| ); | |
| let rowWin = true; | |
| // Use two pointer for next optimizations, | |
| // Left to right can be checked in the same loop | |
| for (let i = rowStart; i <= rowEnd; i++) { | |
| if (gameState[turn][i] !== true) { | |
| rowWin = false; | |
| break; | |
| } | |
| } | |
| if (rowWin) return true; | |
| // ---------- Column calculation ---------- | |
| // check the column is already in hashmap? | |
| const { columnStart, columnEnd } = calculateColumn( | |
| gameState, | |
| turn, | |
| cellIndex, | |
| size | |
| ); | |
| let columnWin = true; | |
| // Use two pointer for next optimizations, | |
| // Left to right can be checked in the same loop | |
| for (let i = columnStart; i <= columnEnd; i += Number(size)) { | |
| if (gameState[turn][i] !== true) { | |
| columnWin = false; | |
| break; | |
| } | |
| } | |
| if (columnWin) return true; | |
| // ---------- Diagonal calculation ---------- | |
| let rightToLeftDiagonalWin = false; | |
| let leftToRightDiagonalWin = false; | |
| const { | |
| onRightToLeftDiagonalPath, | |
| onLeftToRightDiagonalPath, | |
| rightTop, | |
| leftTop, | |
| rightBottom, | |
| leftBottom, | |
| } = calculateDiagonal(gameState, turn, cellIndex, size); | |
| if (onRightToLeftDiagonalPath) { | |
| // Let's give a change | |
| rightToLeftDiagonalWin = true; | |
| // Use two pointer for next optimizations, | |
| // Left to right can be checked in the same loop | |
| for (let i = rightTop; i <= leftBottom; i += size - 1) { | |
| if (gameState[turn][i] !== true) { | |
| // Not your time, sorry. | |
| rightToLeftDiagonalWin = false; | |
| break; | |
| } | |
| } | |
| if (rightToLeftDiagonalWin) return true; | |
| } | |
| if (onLeftToRightDiagonalPath) { | |
| // Let's give a change | |
| leftToRightDiagonalWin = true; | |
| // Use two pointer for next optimizations, | |
| // Left to right can be checked in the same loop | |
| for (let i = leftTop; i <= rightBottom; i += size + 1) { | |
| if (gameState[turn][i] !== true) { | |
| // Not your time, sorry. | |
| leftToRightDiagonalWin = false; | |
| break; | |
| } | |
| } | |
| if (leftToRightDiagonalWin) return true; | |
| } | |
| return false; | |
| } | |
| function handleClickCell(e) { | |
| const element = e.target; | |
| const cellIndex = element.getAttribute("data-cell-index"); | |
| // Prevent double click for each cell. | |
| if (element.getAttribute("data-click")) { | |
| return false; // prevent default | |
| } | |
| // Paint | |
| element.setAttribute("data-click", turn); | |
| element.style.backgroundColor = turn; | |
| // Save | |
| gameState[turn][cellIndex] = true; | |
| // Calculate all possible solutions for this turn | |
| if (calculateWinningSpots(gameState, turn, cellIndex, size)) { | |
| console.log(gameState, "We have a winner"); | |
| alert(`Winner is ${turn}`); | |
| handleClickReset(); | |
| } else { | |
| changeTurn(); | |
| } | |
| } | |
| function handleClickReset() { | |
| // Remove all game container, | |
| gameContainer.parentNode.removeChild(gameContainer); | |
| gameContainer = document.createElement("div"); | |
| gameContainer.id = "game-container"; | |
| gameStatus.prepend(gameContainer); | |
| // Reset game state | |
| gameState.red = {}; | |
| gameState.blue = {}; | |
| // Reset turn | |
| turn = "red"; | |
| // Re-generate cells | |
| generateCells(size); | |
| } | |
| </script> | |
| <!-- 2021 Cagatay Cali --> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment

Gist preview: https://gistpreview.github.io/?bf9ec64725ffa400ba9a0a601afa5115