0

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.

Not-a-Whale
  • 99
  • 2
  • 8
  • What did you find out using debugging? – MrSmith42 Dec 22 '20 at 10:30
  • Well, after first player move it throws an error that it has not classList[3], so the class isn't assigned yet. – Not-a-Whale Dec 22 '20 at 10:34
  • Is there an error thrown or is there an infinite loop? – MrSmith42 Dec 22 '20 at 10:39
  • If i assign newBoard[availSpots[i].classList[3]] = player; - like so, it throws an error the there is no such an element and doesn't work further. Without this declaration it loops forever. – Not-a-Whale Dec 22 '20 at 10:43
  • Comment: don't use deprecated `event.keyCode`. Use `event.key`. – trincot Dec 22 '20 at 11:42
  • Comment2: your code has too much repetition of similar code. You should avoid that. – trincot Dec 22 '20 at 11:43
  • Well, could you please still look into it? I really need this fixed – Not-a-Whale Dec 22 '20 at 12:49
  • There is just too much wrong here. Another example: you should not use the DOM as the board representation to pass to `minimax`. Your minimax function should work without any connection to the DOM. There are working implementations on the internet. If you ask my opinion: you need to rework most of this code, and that is just too much to list & explain in an answer here. Look instead at some successful implementations. – trincot Dec 22 '20 at 16:23
  • For a working implementation, have a look [here](https://stackoverflow.com/questions/64882717/solving-tictactoe-with-minimax-algorithm-in-javascript/65417503#65417503) – trincot Dec 23 '20 at 00:18

0 Answers0