Search code examples
javascriptalgorithmtic-tac-toecallstackminimax

Tic tac toe in JavaScript with minimax algorithm giving error maximum call stack size exceed


I am doing an assignment tic tac toe game. My minimax algorithm works fine on its own with an array and two players provided but when working with html collections of divs it gives an error of maximum call stack size exceed. I don't understand why it is happening. By searching google I know that my function somewhere calling itself over and over again but can't figure it out what to do solve this issue. I tried converting the .cells div collection into an array but still the same error occurred. Anybody please help me with this. Thank you.

var humanPlayer, aiPlayer;
humanPlayer = 'x';
aiPlayer = 'o';
var cells = document.querySelectorAll(".cell");

/** Tic Tac Toe game object **/
var TicTacToe = {
  checkWinner : function(arr, player){
    if(
      (arr[0] === player && arr[1] === player && arr[2] === player)||
      (arr[3] === player && arr[4] === player && arr[5] === player)||
      (arr[6] === player && arr[7] === player && arr[8] === player)||
      (arr[0] === player && arr[3] === player && arr[6] === player)||
      (arr[1] === player && arr[4] === player && arr[7] === player)||
      (arr[2] === player && arr[5] === player && arr[8] === player)||
      (arr[0] === player && arr[4] === player && arr[8] === player)||
      (arr[2] === player && arr[4] === player && arr[6] === player)
    ){
      return true;
    }
    else{
      return false;
    }
  },//checkWinner function.

  getAvailableSpots : function(arr){
    var spots = [];
    for(var i = 0; i < arr.length; i++){
      if(arr[i].textContent !== "x" && arr[i].textContent !== "o"){
        spots.push(i);
      }
    }
    return spots;
  },// getAvailableSpots function.

  minimax : function(newBoard, player){
    newBoard = Array.from(newBoard);
    var availSpots = this.getAvailableSpots(newBoard);
    if(this.checkWinner(newBoard, aiPlayer)){
      return {score: 10};
    }
    else if(this.checkWinner(newBoard, humanPlayer)){
      return {score: -10};
    }
    else if(availSpots.length === 0){
      return {score: 0};
    }

    var moves = [];

    for(var j = 0; j < availSpots.length; j++){
      var move = {};
      move.index = availSpots[j];
      newBoard[availSpots[j]] = player;
      if(player === aiPlayer){
        var result1 = this.minimax(newBoard, humanPlayer);
        move.score = result1.score;
      }
      else{
        var result2 = this.minimax(newBoard, aiPlayer);
        move.score = result2.score;
      }

      newBoard[availSpots[j]] = move.index;
      moves.push(move);
    }

    var bestMove;
    if(player === aiPlayer){
      var bestScore1 = -100000;
      for(var k = 0; k < moves.length; k++){
        if(moves[k].score > bestScore1){
          bestScore1 = moves[k].score;
          bestMove = k;
        }
      }
    }
    else{
      var bestScore2 = 100000;
      for(var l = 0; l < moves.length; l++){
        if(moves[l].score < bestScore2){
          bestScore2 = moves[l].score;
          bestMove = l;
        }
      }
    }

    return moves[bestMove];
  }// minimax function.


};//TicTacToe object literal.

function move(e){
  if(e.target.className === "cell" && e.target.textContent === ""){
    e.target.textContent = humanPlayer;
    var result = TicTacToe.minimax(cells, aiPlayer);
    cells[result.index].textContent = aiPlayer;
  }
}


document.querySelector('.cells').addEventListener('click', move);
.*{
  -webkit-box-sizing: border-box;
  -moz-box-sizing: border-box;
  box-sizing: border-box;
}
.cells  {
  width: 390px;
  height: 390px;
  margin: 50px auto;
}

.cell {
  width:128px;
  height: 128px;
  border: 1px solid #dedede;
  float: left;
  text-align: center;
  line-height: 128px;
  font-size: 80px;
  font-weight: bold;
}
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>

  <div class="cells">
    <div class="cell"></div>
    <div class="cell"></div>
    <div class="cell"></div>
    <div class="cell"></div>
    <div class="cell"></div>
    <div class="cell"></div>
    <div class="cell"></div>
    <div class="cell"></div>
    <div class="cell"></div>
  </div>
</body>
</html>


Solution

  • It's because your minmax function is recursive, meaning it tries every possible combination chain. To not repeat the same chain over and over, it creates a clone of the board on every recursion (via Array.from(newBoard)).

    Now, back when you were testing it with primitve arrays, that copying of the board worked fine. However, now your board array is actually containing DOM objects, which will not be cloned by Array.from(). So your AI is basically altering the cells for every attempt which causes a stack overflow.

    My suggestion would be to convert the current board to a primitive array before passing it to the minmax function.

    var humanPlayer, aiPlayer;
    humanPlayer = 'x';
    aiPlayer = 'o';
    var cells = document.querySelectorAll(".cell");
    
    /** Tic Tac Toe game object **/
    var TicTacToe = {
      checkWinner : function(arr, player){
        if(
          (arr[0] === player && arr[1] === player && arr[2] === player)||
          (arr[3] === player && arr[4] === player && arr[5] === player)||
          (arr[6] === player && arr[7] === player && arr[8] === player)||
          (arr[0] === player && arr[3] === player && arr[6] === player)||
          (arr[1] === player && arr[4] === player && arr[7] === player)||
          (arr[2] === player && arr[5] === player && arr[8] === player)||
          (arr[0] === player && arr[4] === player && arr[8] === player)||
          (arr[2] === player && arr[4] === player && arr[6] === player)
        ){
          return true;
        }
        else{
          return false;
        }
      },//checkWinner function.
    
      getAvailableSpots : function(arr){
        var spots = [];
        for(var i = 0; i < arr.length; i++){
          if(arr[i] !== "x" && arr[i] !== "o"){
            spots.push(i);
          }
        }
        return spots;
      },// getAvailableSpots function.
    
      minimax : function(newBoard, player){
        newBoard = Array.from(newBoard);
        var availSpots = this.getAvailableSpots(newBoard);
        if(this.checkWinner(newBoard, aiPlayer)){
          return {score: 10};
        }
        else if(this.checkWinner(newBoard, humanPlayer)){
          return {score: -10};
        }
        else if(availSpots.length === 0){
          return {score: 0};
        }
    
        var moves = [];
    
        for(var j = 0; j < availSpots.length; j++){
          var move = {};
          move.index = availSpots[j];
          newBoard[availSpots[j]] = player;
          if(player === aiPlayer){
            var result1 = this.minimax(newBoard, humanPlayer);
            move.score = result1.score;
          }
          else{
            var result2 = this.minimax(newBoard, aiPlayer);
            move.score = result2.score;
          }
    
          newBoard[availSpots[j]] = move.index;
          moves.push(move);
        }
    
        var bestMove;
        if(player === aiPlayer){
          var bestScore1 = -100000;
          for(var k = 0; k < moves.length; k++){
            if(moves[k].score > bestScore1){
              bestScore1 = moves[k].score;
              bestMove = k;
            }
          }
        }
        else{
          var bestScore2 = 100000;
          for(var l = 0; l < moves.length; l++){
            if(moves[l].score < bestScore2){
              bestScore2 = moves[l].score;
              bestMove = l;
            }
          }
        }
    
        return moves[bestMove];
      }// minimax function.
    
    
    };//TicTacToe object literal.
    
    function move(e){
      if(e.target.className === "cell" && e.target.textContent === ""){
        e.target.textContent = humanPlayer;
        var result = TicTacToe.minimax(domBoardToArray(cells), aiPlayer);
        cells[result.index].textContent = aiPlayer;
      }
    }
    
    function domBoardToArray(cells){
      return Array.prototype.slice.call(cells).map(function (cell){
        return cell.textContent
      })
    }
    
    
    document.querySelector('.cells').addEventListener('click', move);
    .*{
      -webkit-box-sizing: border-box;
      -moz-box-sizing: border-box;
      box-sizing: border-box;
    }
    .cells  {
      width: 390px;
      height: 390px;
      margin: 50px auto;
    }
    
    .cell {
      width:128px;
      height: 128px;
      border: 1px solid #dedede;
      float: left;
      text-align: center;
      line-height: 128px;
      font-size: 80px;
      font-weight: bold;
    }
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width">
      <title>JS Bin</title>
    </head>
    <body>
    
      <div class="cells">
        <div class="cell"></div>
        <div class="cell"></div>
        <div class="cell"></div>
        <div class="cell"></div>
        <div class="cell"></div>
        <div class="cell"></div>
        <div class="cell"></div>
        <div class="cell"></div>
        <div class="cell"></div>
      </div>
    </body>
    </html>