Search code examples
javascriptartificial-intelligenceminimax

Javascript Tic Tac Toe AI - not selecting optimum space


I have been struggling with creating the unbeatable AI for the Tic Tac Toe project.

I have been googling & using The Coding Train tutorial - however, the AI appears to still be selecting a random square. Although there must be logic behind which square it is choosing, I can't work out what the logic is!

Here is my codepen - https://codepen.io/kath-ldn/pen/oNLZqrp , and I have copied the relevant bits of code below.

If anyone could take a look at this I would be so grateful. I have been stuck for quite a few days now and can’t work out what the issue is.

//functions to assess winning combos
function equals3(a, b, c) {
    return a === b && b === c && a !== "";
}

function checkWinner(){
    let winner = null;

    if (equals3(gameBoard[0], gameBoard[1], gameBoard[2])) {
        winner = gameBoard[0];
    };
    if (equals3(gameBoard[3], gameBoard[4], gameBoard[5])) {
        winner = gameBoard[3];
    };
    if (equals3(gameBoard[6], gameBoard[7], gameBoard[8])) {
        winner = gameBoard[6];
    };
    if (equals3(gameBoard[0], gameBoard[3], gameBoard[6])) {
        winner = gameBoard[0];
    };
    if (equals3(gameBoard[1], gameBoard[4], gameBoard[7])) {
        winner = gameBoard[1];
    };
    if (equals3(gameBoard[2], gameBoard[5], gameBoard[8])) {
        winner = gameBoard[0];
    };
    if (equals3(gameBoard[0], gameBoard[4], gameBoard[8])) {
        winner = gameBoard[0];
    };
    if (equals3(gameBoard[2], gameBoard[4], gameBoard[6])) {
        winner = gameBoard[2];
    };

    let openSpots = 0;
    for (let i = 0; i < gameBoard.length; i++) {
        if (gameBoard[i] === "") {
            openSpots = openSpots + 1;
        };
    };

    if (winner === null && openSpots === 0) {
        return 'tie';
    } else {
        return winner;
    };
};

let scores = {
  X: 10,
  O: -10,
  tie: 0
};

//function to create impossible-to-beat AI using minimax algorithm
function bestMove() {
    let bestScore = -Infinity;
    let move;
    for (let i = 0; i < gameBoard.length; i++) {
      if (gameBoard[i] === "") {
        gameBoard[i] = AI;
        let score = minimax(gameBoard, 0, false);
        gameBoard[i] = "";
        if (score > bestScore) {
        bestScore = score;
        move = i;
        }
      }    
    }
    gameBoard[move] = AI;
};

//minimax function
function minimax(gameBoard, depth, isMaximizing){
  let result = checkWinner();
  if (result !== null) {
    return scores[result];
  }

  if (isMaximizing) {
    let bestScore = -Infinity;
    for (let i = 0; i < gameBoard.length; i++) {
      if (gameBoard[i] === "") {
        gameBoard[i] = AI;
        let score = minimax(gameBoard, depth + 1, false);
        gameBoard[i] = "";
        bestScore = Math.max(score, bestScore);
      }
    }
    return bestScore;
  } else {
    let bestScore = Infinity;
    for (let i = 0; i < gameBoard.length; i++) {
      if (gameBoard[i] === "") {
        gameBoard[i] = human;
        let score = minimax(gameBoard, depth + 1, true);
        gameBoard[i] = "";
        bestScore = Math.min(score, bestScore);
      }
    }
    return bestScore;
  }
};

Solution

  • You are looping through the board and calling minimax for each square, this is not necessary and will be super slow. You just need to call it once like this:

    move, score = minimax(gameBoard, 8, true)
    

    I am not sure if it is needed, but I would return a 0 from the checkWinner function instead of returning a 'tie' string. It seems hard to chose a max/min valie from e.g. a 1 and a 'te' otherwise.

    Then in your minimax function you need to change a few things to actually return the best move it finds. Sorry for any programming language issues, I am used to Python. I hope you get what I mean anyway:

    //minimax function
    function minimax(gameBoard, depth, isMaximizing){
      let result = checkWinner();
      if (result !== null) {
        return None, scores[result];
      }
    
      if (isMaximizing) {
        let bestScore = -Infinity;
        for (let i = 0; i < gameBoard.length; i++) {
          if (gameBoard[i] === "") {
            gameBoard[i] = AI;
            let score = minimax(gameBoard, depth - 1, false)[1];
            gameBoard[i] = "";
            if score > bestScore:
                bestScore = score
                bestMove = gameBoard[i]
          }
        }
        return bestMove, bestScore;
      } else {
        let bestScore = Infinity;
        for (let i = 0; i < gameBoard.length; i++) {
          if (gameBoard[i] === "") {
            gameBoard[i] = human;
            let score = minimax(gameBoard, depth - 1, true)[1];
            gameBoard[i] = "";
            if score < bestScore:
                bestScore = score
                bestMove = gameBoard[i]
          }
        }
        return bestMove, bestScore;
      }
    };