Tic-Tac-Toe AI with Minimax Algorithm in JavaScript

Tic-Tac-Toe AI with Minimax Algorithm in JavaScript

In this article, we will implement a simple Tic-Tac-Toe game where an AI opponent uses the Minimax Algorithm to play optimally against a human player.


Definition:
Minimax is a recursive algorithm to minimize the possible loss in a worst-case scenario, or equivalently, to maximize the minimum gain.

Idea:

  • One player tries to maximize their score (MAX player).
  • The other tries to minimize the score (MIN player).

Each player assumes that the opponent plays optimally.


📌 How it Works?

The algorithm builds a game tree of possible moves:

  • Starting from the current state, generate all possible moves.
  • Recursively simulate each move and its consequences down to a terminal state (win, lose, draw, or depth limit).
  • Assign a score to each terminal state.
  • scores:
    • MAX player chooses the move with the highest score.
    • MIN player chooses the move with the lowest score.

Initial Setup and Constants

const PLAYER = "X";
const AI = "O";
const PLAYER_SVG = "❌";
const AI_SVG = "â­•";

let board = [
  ["", "", ""],
  ["", "", ""],
  ["", "", ""]
];
let currentPlayer = PLAYER;
let flag = false;

const resetBtn = document.querySelector(".restartBtn");
const aiStartsBtn = document.querySelector(".aiStarts");

resetBtn.addEventListener("click", () => resetGame());
aiStartsBtn.addEventListener("click", () => toggleAIStart());

Utility Functions

Check if More Moves Are Available

function isMoreMovesAvailable(board) {
  for (let i = 0; i < 3; i++) {
    for (let j = 0; j < 3; j++) {
      if (board[i][j] === "") {
        return true;
      }
    }
  }
  return false;
}

Toggle AI Starting Player

function toggleAIStart() {
  aiStartsBtn.disabled = true;
  flag = !flag;
  startGame(flag);
}

Check Equality for Winning Conditions

function isEqual(a, b, c) {
  return a === b && b === c && a !== "";
}

Game Over Detection

function gameOver(board) {
  let winner = null;

  // Check rows
  for (let i = 0; i < 3; i++) {
    if (isEqual(board[i][0], board[i][1], board[i][2])) {
      winner = board[i][0];
    }
  }
  // Check columns
  for (let i = 0; i < 3; i++) {
    if (isEqual(board[0][i], board[1][i], board[2][i])) {
      winner = board[0][i];
    }
  }
  // Check diagonals
  if (isEqual(board[0][0], board[1][1], board[2][2])) {
    winner = board[0][0];
  }
  if (isEqual(board[2][0], board[1][1], board[0][2])) {
    winner = board[2][0];
  }

  if (winner === null && !isMoreMovesAvailable(board)) {
    return "tie";
  } else {
    return winner;
  }
}

Game Evaluation Function

Assign scores to game outcomes to guide the minimax algorithm:

function gameEval(board) {
  if (gameOver(board) === PLAYER) return 10;
  else if (gameOver(board) === AI) return -10;
  else return 0;
}

Minimax Algorithm Implementation

function minimax(board, depth, isMax) {
  const result = gameOver(board);
  if (result !== null) return gameEval(board);

  if (isMax) {
    let bestScore = -Infinity;
    for (let i = 0; i < 3; i++) {
      for (let j = 0; j < 3; j++) {
        if (board[i][j] === "") {
          board[i][j] = PLAYER;
          let score = minimax(board, depth - 1, false);
          board[i][j] = "";
          bestScore = Math.max(score, bestScore);
        }
      }
    }
    return bestScore;
  } else {
    let bestScore = Infinity;
    for (let i = 0; i < 3; i++) {
      for (let j = 0; j < 3; j++) {
        if (board[i][j] === "") {
          board[i][j] = AI;
          let score = minimax(board, depth - 1, true);
          board[i][j] = "";
          bestScore = Math.min(score, bestScore);
        }
      }
    }
    return bestScore;
  }
}

Handling Player Turn

Add click event listeners to cells and update the board state:

function playerTurn(board) {
  const cells = document.querySelectorAll(".cell");
  cells.forEach((cell) => {
    const row = parseInt(cell.id[0]);
    const col = parseInt(cell.id[1]);
    cell.addEventListener("click", () => {
      if (board[row][col] === "") {
        cell.innerHTML = PLAYER_SVG;
        board[row][col] = PLAYER;
        const result = gameOver(board);
        if (result) {
          endGame(result);
        } else {
          currentPlayer = AI;
          aiTurn(board);
        }
      }
    });
  });
}

Handling AI Turn

Use the minimax algorithm to choose the best move:

function aiTurn(board) {
  let bestScore = Infinity;
  let bestMove = null;

  for (let i = 0; i < 3; i++) {
    for (let j = 0; j < 3; j++) {
      if (board[i][j] === "") {
        board[i][j] = AI;
        const score = minimax(board, 3, true);
        board[i][j] = "";
        if (score < bestScore) {
          bestScore = score;
          bestMove = { i, j };
        }
      }
    }
  }

  const cell = document.getElementById(`${bestMove.i}${bestMove.j}`);
  cell.innerHTML = AI_SVG;
  board[bestMove.i][bestMove.j] = AI;

  const result = gameOver(board);
  if (result) endGame(result);
}

Ending the Game

Disable all cells and show the result:

function endGame(winner) {
  const score = document.querySelector(".score");
  const cells = document.querySelectorAll(".cell");
  cells.forEach((cell) => {
    cell.disabled = true;
  });

  if (winner === "tie") {
    score.textContent = "Tie game!";
  } else {
    score.textContent = `${winner} is the winner!`;
  }
}

Resetting the Game

Clear the board and reset UI:

function resetGame() {
  const cells = document.querySelectorAll(".cell");
  const score = document.querySelector(".score");

  cells.forEach((cell) => {
    cell.disabled = false;
    cell.innerHTML = "";
  });

  score.textContent = "";

  for (let i = 0; i < 3; i++) {
    for (let j = 0; j < 3; j++) {
      board[i][j] = "";
    }
  }

  flag = false;
  aiStartsBtn.disabled = false;
}

Starting the Game

function startGame(flag) {
  if (flag) {
    playerTurn(board);
    aiTurn(board);
  } else {
    playerTurn(board);
  }
}

// Start game initially
startGame();