1

I'm currently trying to make a tic-tac-toe game using minimax algorithm. I'm using JavaScript with p5js framework for this project.

Here is the sketch.js code:

var rows = 3;
var cols = 3;
var scl = 1;
var board_width = cols * 100 * scl;
var board_height = rows * 100 * scl;
var board;
let player = 'x';
let ai = 'o';
let playerTurn;
let winner;

var resetBtn;

function setup() {
  
  createCanvas(400, 300);

  resetGame();

  resetBtn = createButton("Reset");
  resetBtn.position(320, 50);
  resetBtn.size(60, 20);
}

function draw() {

  resetBtn.mouseClicked(resetGame);

  winner = checkWinner();

  
  if (winner == 'x' || winner == 'o') {
    console.log(winner);
  }
  else if (winner == 't') {
    console.log("Tie");
  }
  else {
    if (playerTurn == false) {
      aiMove();
      playerTurn = true;
    }
  }

  drawBoard();
  
  
}

function mousePressed() {
  if (playerTurn == true) {
    for (let i = 0; i < rows; i++) {
      for (let j = 0; j < cols; j++) {
        if (mouseX > j * 100 && mouseX < (j * 100 + 100) && mouseY > i * 100 && mouseY < (i * 100 + 100)) {
          board[i][j] = player;
          playerTurn = false;
        }
      }
    }
  }
}

function aiMove() {
  let bestScore = -2;
  let score;
  let moveSpot = [];

  for (let i = 0; i < rows; i++) {
    for (let j = 0; j < cols; j++) {
      if (board[i][j] == ' ') {
        board[i][j] = ai;
        score = miniMax(board, false);
        board[i][j] = ' ';
        if (score > bestScore) {
          bestScore = score;
          moveSpot = {i, j};
        }
      }
    }
  }
  board[moveSpot.i][moveSpot.j] = ai;

}

function miniMax(board, isMaximizing) {
  let result = checkWinner();

  if (result == 'x') {
    return -1;
  }
  else if (result == 'o') {
    return 1;
  }
  else if (result == 't') {
    return 0;
  }

  else if (isMaximizing) {
    let bestScore = -2;
    let score;
    for (let i = 0; i < rows; i++) {
      for (let j = 0; j < cols; j++) {
        if (board[i][j] == ' ') {
          board[i][j] = ai;
          score = miniMax(board, false);
          board[i][j] = ' ';
          bestScore = max(score, bestScore);
        }
      }
    }
    return bestScore;
  }
  else {
    let bestScore = 2;
    let score;
    for (let i = 0; i < rows; i++) {
      for (let j = 0; j < cols; j++) {
        if (board[i][j] == ' ') {
          board[i][j] = player;
          score = miniMax(board, true);
          board[i][j] = ' ';
          bestScore = min(score, bestScore);
        }
      }
    }
    return bestScore;
  }

}

function checkWinner() {
  
  let w = null;
  
  if (board[0][0] == 'x' && board[0][1] == 'x' && board[0][2] == 'x' ||
            board[1][0] == 'x' && board[1][1] == 'x' && board[1][2] == 'x' ||
            board[2][0] == 'x' && board[2][1] == 'x' && board[2][2] == 'x' ||
            board[0][0] == 'x' && board[1][0] == 'x' && board[2][0] == 'x' ||
            board[0][1] == 'x' && board[1][1] == 'x' && board[2][1] == 'x' ||
            board[0][2] == 'x' && board[1][2] == 'x' && board[2][2] == 'x' ||
            board[0][0] == 'x' && board[1][1] == 'x' && board[2][2] == 'x' ||
            board[2][0] == 'x' && board[1][1] == 'x' && board[0][2] == 'x') {
                return 'x';
        }
        else if (board[0][0] == 'o' && board[0][1] == 'o' && board[0][2] == 'o' ||
            board[1][0] == 'o' && board[1][1] == 'o' && board[1][2] == 'o' ||
            board[2][0] == 'o' && board[2][1] == 'o' && board[2][2] == 'o' ||
            board[0][0] == 'o' && board[1][0] == 'o' && board[2][0] == 'o' ||
            board[0][1] == 'o' && board[1][1] == 'o' && board[2][1] == 'o' ||
            board[0][2] == 'o' && board[1][2] == 'o' && board[2][2] == 'o' ||
            board[0][0] == 'o' && board[1][1] == 'o' && board[2][2] == 'o' ||
            board[2][0] == 'o' && board[1][1] == 'o' && board[0][2] == 'o') {
                return 'o';
        }

        let full = 0;
        for (let i = 0; i < 3; i++) {
            for (let j = 0; j < 3; j++) {
                if (board[i][j] == 'x' || board[i][j] == 'o') {
                    full ++;
                }
            }
        }

        if (full == 9) {
            return 't';
        }

        return w;
    
  return w;
}

function drawBoard() {
  // draw board
  stroke(0);
  strokeWeight(7);
  line(board_width / 3, 0, board_width / 3, board_height);
  line(board_width / 3 * 2, 0, board_width / 3 * 2, board_height);
  line(0, board_height / 3, board_width, board_height / 3);
  line(0, board_height / 3 * 2, board_width, board_height / 3 * 2);

  for (let i = 0; i < rows; i++) {
    for (let j = 0; j < cols; j++) {
      if (board[i][j] == 'o') {
        circle(j * 100 + 50, i * 100 + 50, 80);
      }
      else if (board[i][j] == 'x') {
        line(j * 100 + 10, i * 100 + 10, j * 100 + 80, i * 100 + 80);
        line(j * 100 + 80, i * 100 + 10, j * 100 + 10, i * 100 + 80);
      }
    }
  }
}

function resetGame() {
  clear();
  winner = null;
  board = [];
  playerTurn = true;
  background(255);

  for (let i = 0; i < rows; i++) {
    let row = [];
    for (let j = 0; j < cols; j++) {
      row[j] = ' ';
    }
    board.push(row);
  }
}

The problem is shown in the picture. As you, the player make first move, if you choose spot 2 then 4 then 6, the ai doesn't go to spot 9 to win but go to spot 3 instead. screenshot

I tried this tactic in an example on this website:

https://www.freecodecamp.org/news/how-to-make-your-tic-tac-toe-game-unbeatable-by-using-the-minimax-algorithm-9d690bad4b37/

and got similar result.

Is this a problem with the algorithm implementation or something else?

Thinh Pham
  • 23
  • 3
  • 2
    In `checkWinner()` you do `return winner; return w;`. You never update either of these variables in the function. And the second `return` can never be executed, since the first `return` ends the function. You should simply do `return false;` if none of the `if` statements return. – Barmar Apr 27 '23 at 22:14
  • The return part is indeed a bit messy, I also took your suggestions and did some modifications to the code but it doesn't change the outcome that I was referring to. Thanks. – Thinh Pham Apr 27 '23 at 22:59
  • No, I didn't think it would. I'm not sure why the minimax algorithm is failing. – Barmar Apr 27 '23 at 23:01
  • 1
    Try adding `print` statements so you can see how the scores are changing as you go through the algorithm. – Barmar Apr 27 '23 at 23:02
  • https://ericlippert.com/2014/03/05/how-to-debug-small-programs/ – Barmar Apr 27 '23 at 23:02
  • I'm still not very good at keeping track of a recursive function so pardon me. At least I can see the all of the spots received the same score. So I think the program will just go with the first spot it tried. – Thinh Pham Apr 28 '23 at 03:10
  • I also notice examples for this program on the internet behaves the same way. The one I mentioned in the original post does that and also this one: https://github.com/Cledersonbc/tic-tac-toe-minimax – Thinh Pham Apr 28 '23 at 03:13

1 Answers1

1

The algorithm is correct. For the example you gave, the third move for player O may not look good, but it is actually a winning move. Not the fastest win, but still ensuring a win. Your algorithm does not have any notion of preferring faster wins over slower wins, so that may give the false impression that the minimax algorithm is not doing it right -- but it does.

To favour faster wins over slower wins, make these changes:

  • Make your score range larger: between -9 and 9.
  • Reduce the absolute magnitude of a score by 1 each time you backtrack. This diminishes the value of a win when it is deep in the search tree.

Here is the relevant code with those changes made (comments indicate where):

function miniMax(board, isMaximizing) {
  let result = checkWinner();
  if (result == 'x') {
    return -9; // extreme
  }
  else if (result == 'o') {
    return 9; // extreme
  }
  else if (result == 't') {
    return 0;
  }

  else if (isMaximizing) {
    let bestScore = -10; // extreme
    let score;
    for (let i = 0; i < rows; i++) {
      for (let j = 0; j < cols; j++) {
        if (board[i][j] == ' ') {
          board[i][j] = ai;
          score = miniMax(board, false);
          board[i][j] = ' ';
          bestScore = Math.max(score, bestScore);
        }
      }
    }
    // Diminish the magnitude of the score by one point 
    return bestScore - Math.sign(bestScore);
  }
  else {
    let bestScore = 10; // extreme
    let score;
    for (let i = 0; i < rows; i++) {
      for (let j = 0; j < cols; j++) {
        if (board[i][j] == ' ') {
          board[i][j] = player;
          score = miniMax(board, true);
          board[i][j] = ' ';
          bestScore = Math.min(score, bestScore);
        }
      }
    }
    // Diminish the magnitude of the score by one point 
    return bestScore - Math.sign(bestScore);
  }
}

Now the AI will play a win-in-1 move in the example game.

trincot
  • 317,000
  • 35
  • 244
  • 286