Skip to content

Instantly share code, notes, and snippets.

@cagataycali
Created October 31, 2021 03:54
Show Gist options
  • Select an option

  • Save cagataycali/bf9ec64725ffa400ba9a0a601afa5115 to your computer and use it in GitHub Desktop.

Select an option

Save cagataycali/bf9ec64725ffa400ba9a0a601afa5115 to your computer and use it in GitHub Desktop.
[JavaScript] Tic-tac-to NxN
<!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>
@cagataycali

Copy link
Copy Markdown
Author

@cagataycali

Copy link
Copy Markdown
Author

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment