I have a tic tac toe game with minimax algorithm. It works by comparing combinations to a winning ones, adds scores and calls itself recursively, until the score is the highest and then makes it's move. In my snippet the program doesn't work due to a bug in it's function that gives me an infinite loop
const statusDiv = document.querySelector(".status");
const resetDiv = document.querySelector(".reset");
const cellDivs = document.querySelectorAll(".game-cell");
const activeCell = document.querySelector(".active");
document.addEventListener("keydown", (e) => move(e.keyCode));
let matrix = [
["1", "2", "3"],
["4", "5", "6"],
["7", "8", "9"],
];
let winCombos = [
["0", "1", "2"],
["3", "4", "5"],
["6", "7", "8"],
["0", "3", "6"],
["1", "4", "7"],
["2", "5", "8"],
["0", "4", "8"],
["6", "4", "2"],
];
let currentX = 0;
let currentY = 0;
let newElem = matrix[currentY][currentX];
let huPlayer = "x";
let aiPlayer = "o";
// game constants
const xSymbol = "✗";
const oSymbol = "○";
let gameIsRunning = true;
let xIsNext = true;
let winner = null;
// functions
const move = (keyCode) => {
if (gameIsRunning) {
let key = null;
switch (event.keyCode) {
case 38:
key = "up";
break;
case 40:
key = "down";
break;
case 37:
key = "left";
break;
case 39:
key = "right";
break;
case 13:
key = "return";
}
if (key === "up") {
currentY--;
if (currentY < 0) {
currentY = 0;
}
newElem = matrix[currentY][currentX];
} else if (key === "down") {
currentY++;
if (currentY > 2) {
currentY = 2;
}
newElem = matrix[currentY][currentX];
} else if (key === "right") {
currentX++;
if (currentX > 2) {
currentX = 2;
}
newElem = matrix[currentY][currentX];
} else if (key === "left") {
currentX--;
if (currentX < 0) {
currentX = 0;
}
newElem = matrix[currentY][currentX];
} else if (key === "return") {
handleCellClick(false, newElem);
}
cellDivs.forEach((div) => {
div.classList.replace("active", "inactive");
if (div.classList[1] === newElem) {
div.classList.replace("inactive", "active");
}
});
}
};
const letterToSymbol = (letter) => {
return letter === "x" ? xSymbol : oSymbol;
};
const handleWin = (letter) => {
gameIsRunning = false;
winner = letter;
if (winner === "x") {
statusDiv.innerHTML = `${letterToSymbol(winner)} has won!`;
} else {
statusDiv.innerHTML = `<span>${letterToSymbol(winner)} has won!</span>`;
}
};
const checkGameStatus = () => {
const topLeft = cellDivs[0].classList[3];
const topMiddle = cellDivs[1].classList[3];
const topRight = cellDivs[2].classList[3];
const middleLeft = cellDivs[3].classList[3];
const middleMiddle = cellDivs[4].classList[3];
const middleRight = cellDivs[5].classList[3];
const bottomLeft = cellDivs[6].classList[3];
const bottomMiddle = cellDivs[7].classList[3];
const bottomRight = cellDivs[8].classList[3];
// check a winner
if (topLeft && topLeft === topMiddle && topLeft === topRight) {
handleWin(topLeft);
cellDivs[0].classList.add("won");
cellDivs[1].classList.add("won");
cellDivs[2].classList.add("won");
} else if (
middleLeft &&
middleLeft === middleMiddle &&
middleLeft === middleRight
) {
handleWin(middleLeft);
cellDivs[3].classList.add("won");
cellDivs[4].classList.add("won");
cellDivs[5].classList.add("won");
} else if (
bottomLeft &&
bottomLeft === bottomMiddle &&
bottomLeft === bottomRight
) {
handleWin(bottomLeft);
cellDivs[6].classList.add("won");
cellDivs[7].classList.add("won");
cellDivs[8].classList.add("won");
} else if (topLeft && topLeft === middleLeft && topLeft === bottomLeft) {
handleWin(topLeft);
cellDivs[0].classList.add("won");
cellDivs[3].classList.add("won");
cellDivs[6].classList.add("won");
} else if (
topMiddle &&
topMiddle === middleMiddle &&
topMiddle === bottomMiddle
) {
handleWin(topMiddle);
cellDivs[1].classList.add("won");
cellDivs[4].classList.add("won");
cellDivs[7].classList.add("won");
} else if (topRight && topRight === middleRight && topRight === bottomRight) {
handleWin(topRight);
cellDivs[2].classList.add("won");
cellDivs[5].classList.add("won");
cellDivs[8].classList.add("won");
} else if (topLeft && topLeft === middleMiddle && topLeft === bottomRight) {
handleWin(topLeft);
cellDivs[0].classList.add("won");
cellDivs[4].classList.add("won");
cellDivs[8].classList.add("won");
} else if (topRight && topRight === middleMiddle && topRight === bottomLeft) {
handleWin(topRight);
cellDivs[2].classList.add("won");
cellDivs[4].classList.add("won");
cellDivs[6].classList.add("won");
} else if (
topLeft &&
topMiddle &&
topRight &&
middleLeft &&
middleMiddle &&
middleRight &&
bottomMiddle &&
bottomRight &&
bottomLeft
) {
gameIsRunning = false;
statusDiv.innerHTML = "Game is tied!";
} else {
xIsNext = !xIsNext;
if (xIsNext) {
statusDiv.innerHTML = `${letterToSymbol("x")} is next`;
} else {
statusDiv.innerHTML = `<span>${letterToSymbol(oSymbol)} is next</span>`;
}
}
};
// event Handlers
const handleReset = (e) => {
xIsNext = true;
statusDiv.innerHTML = `${letterToSymbol(xSymbol)} is next`;
winner = null;
gameIsRunning = true;
cellDivs.forEach((div) => {
div.classList.replace("active", "inactive");
});
for (const cellDiv of cellDivs) {
cellDiv.classList.remove("x");
cellDiv.classList.remove("o");
cellDiv.classList.remove("won");
}
cellDivs[0].classList.replace("inactive", "active");
};
// event listeners
resetDiv.addEventListener("click", handleReset);
const handleCellClick = (e, elem) => {
if (gameIsRunning) {
let classList;
if (e) {
classList = e.target.classList;
} else if (elem) {
cellDivs.forEach((div) => {
if (Array.from(div.classList).includes(elem)) {
classList = div.classList;
}
});
}
if (!gameIsRunning || classList[3] === "x" || classList[3] === "o") {
return;
}
if (xIsNext) {
classList.add("x");
checkGameStatus();
} else {
classList.add("o");
checkGameStatus();
}
if (e.target) {
cellDivs.forEach((div) => {
div.classList.replace("active", "inactive");
});
e.target.classList.replace("inactive", "active");
}
aiTurn();
}
};
const aiTurn = () => {
window.setTimeout(() => {
if (gameIsRunning) {
let availableSpots = emptySquares(cellDivs);
if (xIsNext) {
availableSpots[0].classList.add("x");
checkGameStatus();
} else {
availableSpots[bestSpot(availableSpots)].classList.add("o");
checkGameStatus();
}
}
}, 300);
};
const emptySquares = (elems) => {
return Array.from(elems).filter((div, index) => {
return !div.classList[3];
});
};
const bestSpot = (origBoard) => {
let isAi = xIsNext ? "x" : "o";
return minimax([...cellDivs], isAi);
};
const checkWin = (board, player) => {
const aiMoves = board.reduce(
(a, e, i) => (e.classList[3] === player ? a.concat(i) : a),
[]
);
console.log(aiMoves);
let gameWon = null;
for (let [index, win] of winCombos.entries()) {
if (win.every((elem) => aiMoves.indexOf(elem) > -1)) {
gameWon = { index: index, player: player };
break;
}
}
console.log(gameWon);
return gameWon;
};
function minimax(newBoard, player) {
var availSpots = emptySquares(newBoard, player);
if (checkWin([...newBoard], huPlayer)) {
return { score: -10 };
} else if (checkWin([...newBoard], aiPlayer)) {
return { score: 10 };
} else if (availSpots.length === 0) {
return { score: 0 };
}
var moves = [];
for (var i = 0; i < availSpots.length; i++) {
var move = {};
move.index = newBoard[availSpots[i]];
newBoard[availSpots[i]] = player;
if (player == aiPlayer) {
var result = minimax(newBoard, huPlayer);
move.score = result.score;
} else {
var result = minimax(newBoard, aiPlayer);
move.score = result.score;
}
newBoard[availSpots[i]] = move.index;
moves.push(move);
}
var bestMove;
if (player === aiPlayer) {
var bestScore = -10000;
for (var i = 0; i < moves.length; i++) {
if (moves[i].score > bestScore) {
bestScore = moves[i].score;
bestMove = i;
}
}
} else {
var bestScore = 10000;
for (var i = 0; i < moves.length; i++) {
if (moves[i].score < bestScore) {
bestScore = moves[i].score;
bestMove = i;
}
}
}
return moves[bestMove];
}
for (const cellDiv of cellDivs) {
cellDiv.addEventListener("click", handleCellClick);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
transition: 0.5s all;
}
body {
display: flex;
font-family: sans-serif;
justify-content: center;
align-items: flex-start;
color: #545454;
}
.container {
background: #39cccc;
padding: 50px;
border-radius: 25px;
}
.title {
text-align: center;
}
.title {
span {
color: #f2ebd3;
}
}
.status-action {
margin-top: 25px;
font-size: 25px;
display: flex;
justify-content: space-around;
height: 30px;
}
.reset {
cursor: pointer;
}
.reset:hover {
color: #f2ebd3;
}
.game-grid {
background: #0da192;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
grid-gap: 15px;
margin-top: 30px;
}
.game-cell {
width: 150px;
height: 150px;
background: #39cccc;
display: flex;
align-items: center;
justify-content: center;
transition: 0.5s all;
border: 5px solid transparent;
&:hover {
cursor: pointer;
}
}
.x,
.o {
cursor: default;
}
.x::after {
content: "✗";
font-size: 100px;
}
.o::after {
content: "○";
color: #f2ebd3;
font-size: 130px;
font-weight: bold;
}
.status {
span {
color: #f2ebd3;
}
}
.won::after {
color: #bd022f;
}
@media only screen and (max-width: 1024px) {
.game-grid {
margin-top: 25px;
grid-gap: 16px;
gap: 10px;
}
.game-cell {
height: 100px;
width: 100px;
}
.x::after {
font-size: 85px;
}
.o::after {
font-size: 85px;
}
}
@media only screen and (max-width: 540px) {
.container {
margin: 25px;
padding: 25px;
}
.game-grid {
gap: 5px;
}
.game-cell {
height: 75px;
width: 75px;
}
.x::after {
font-size: 50px;
}
.o::after {
font-size: 50px;
}
}
.active {
border: 5px solid goldenrod;
}
<div class="container">
<h1 class="title">Tic <span>Tac</span> Toe</h1>
<div class="status-action">
<div class="status">✗ is next</div>
<div class="reset">Reset</div>
</div>
<div class="game-grid">
<div class="game-cell 1 active"></div>
<div class="game-cell 2 inactive"></div>
<div class="game-cell 3 inactive"></div>
<div class="game-cell 4 inactive"></div>
<div class="game-cell 5 inactive"></div>
<div class="game-cell 6 inactive"></div>
<div class="game-cell 7 inactive"></div>
<div class="game-cell 8 inactive"></div>
<div class="game-cell 9 inactive"></div>
</div>
</div>
. I cannot find the exact place where the stop is missing. Please look where I might have missed something.