Tic-Tac-Toe is a classic game that almost everyone has played at some point. But have you ever wondered how to create your own version of the game, complete with an AI opponent that can challenge you at different difficulty levels? In this blog, we’ll walk through the process of building a Tic-Tac-Toe game from scratch using HTML, CSS, and JavaScript.

We’ll also add an AI opponent with three difficulty levels: Easy, Medium, and Hard. By the end, you’ll have a fully functional game that you can play and share with your friends!

🕹️ Play Now: Tic-Tac-Toe AI


🎯 What You’ll Learn

  1. How to structure a Tic-Tac-Toe game using HTML for the layout, CSS for styling, and JavaScript for the game logic.
  2. How to implement turn-based gameplay where players take turns placing their marks (X or O).
  3. How to create an AI opponent with three difficulty levels:
    • Easy: The AI makes random moves.
    • Medium: The AI mixes random moves with smart moves.
    • Hard: The AI uses the Minimax algorithm to make the best possible move every time.
  4. How to track scores and display the game status dynamically.

🏗️ Setting Up the Project

Before diving into the game logic, let’s set up the basic structure of our project using HTML and CSS.

📜 HTML Structure

The game layout consists of:

  • A title
  • A difficulty selector
  • A game board
  • A status message (e.g., "Your turn" or "AI wins!")
  • A score display

Here’s the basic structure of our HTML file:

</span>
 lang="en">

     charset="UTF-8">
     name="viewport" content="width=device-width, initial-scale=1.0">
    Tic-Tac-Toe AI
     rel="stylesheet" href="styles.css">


     class="container">
         class="game-wrapper">
            Tic-Tac-Toe
             class="controls">
                 id="difficulty">
                     value="easy">Easy
                     value="medium">Medium
                     value="hard" selected>Hard
                
                 id="reset">New Game
            
             class="status" id="status">Your turn (X)
             class="board" id="board">
             class="score">
                Player (X):  id="player-score">0
                AI (O):  id="ai-score">0
            
        
    
    <span class="na">src="script.js">





    Enter fullscreen mode
    


    Exit fullscreen mode
    





  
  
  🎨 Styling the Game with CSS
To make our game visually appealing, we’ll use CSS to create a modern, dark-themed design with glowing effects for the X and O marks. Here’s how the styling is done:

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font-family: 'Arial', sans-serif;
}

body {
    background: linear-gradient(135deg, #1e1e2f 0%, #2a2a3d 100%);
    min-height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
}

.container {
    padding: 20px;
}

.game-wrapper {
    background: rgba(255, 255, 255, 0.05);
    border-radius: 20px;
    padding: 30px;
    backdrop-filter: blur(10px);
    box-shadow: 0 0 40px rgba(0, 0, 0, 0.2);
}

h1 {
    color: #fff;
    text-align: center;
    margin-bottom: 20px;
    font-size: 2.5em;
    text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}

.controls {
    display: flex;
    gap: 15px;
    margin-bottom: 20px;
    justify-content: center;
}

/* Replace the existing select, button styling with this */
select, button {
    padding: 10px 20px;
    border: none;
    border-radius: 25px;
    background: rgba(255, 255, 255, 0.1);
    color: #fff;
    cursor: pointer;
    transition: all 0.3s ease;
    -webkit-appearance: none; /* Remove default arrow in some browsers */
    -moz-appearance: none;
    appearance: none;
    position: relative;
}

/* Add these new rules */
select {
    padding-right: 30px; /* Make room for custom arrow */
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%23ffffff' viewBox='0 0 16 16'%3E%3Cpath d='M7.247 11.14 2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 10px center;
}

select option {
    background: #2a2a3d; /* Dark background for options */
    color: #fff; /* White text for options */
}

select:hover, button:hover {
    background: rgba(255, 255, 255, 0.2);
    transform: translateY(-2px);
}

.board {
    display: grid;
    grid-template-columns: repeat(3, 100px);
    grid-gap: 10px;
    margin: 0 auto;
    width: 320px;
}

.cell {
    width: 100px;
    height: 100px;
    background: rgba(255, 255, 255, 0.05);
    border-radius: 15px;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 2.5em;
    color: #fff;
    cursor: pointer;
    transition: all 0.3s ease;
}

.cell:hover {
    background: rgba(255, 255, 255, 0.1);
}

.cell.x {
    color: #00ffcc;
    text-shadow: 0 0 10px rgba(0, 255, 204, 0.5);
}

.cell.o {
    color: #ff3366;
    text-shadow: 0 0 10px rgba(255, 51, 102, 0.5);
}

.status {
    text-align: center;
    color: #fff;
    margin: 20px 0;
    font-size: 1.2em;
}

.score {
    display: flex;
    justify-content: space-between;
    color: #fff;
    margin-top: 20px;
    font-size: 1.1em;
}

@keyframes pulse {
    0% { transform: scale(1); }
    50% { transform: scale(1.05); }
    100% { transform: scale(1); }
}

.winning-cell {
    animation: pulse 1s infinite;
    background: rgba(255, 255, 255, 0.2);
}



    Enter fullscreen mode
    


    Exit fullscreen mode
    





  
  
  🧠 Implementing the Game Logic
The core of our Tic-Tac-Toe game is written in JavaScript. Let’s break it down step by step.  
  
  
  1️⃣ Initializing the Game
We’ll create a TicTacToe class to manage the game board, track turns, and update the UI. The constructor initializes the board and sets up event listeners.

class TicTacToe {
    constructor() {
        this.board = Array(9).fill(null);
        this.currentPlayer = 'X';
        this.gameActive = true;
        this.playerScore = 0;
        this.aiScore = 0;
        this.difficulty = 'hard';

        this.initGame();
    }

    initGame() {
        this.createBoard();
        this.setupEventListeners();
        this.updateScore();
    }

    createBoard() {
        const board = document.getElementById('board');
        board.innerHTML = '';
        for (let i = 0; i < 9; i++) {
            const cell = document.createElement('div');
            cell.classList.add('cell');
            cell.dataset.index = i;
            board.appendChild(cell);
        }
    }

    setupEventListeners() {
        document.getElementById('board').addEventListener('click', (e) => {
            if (!e.target.classList.contains('cell') || !this.gameActive) return;
            const index = e.target.dataset.index;
            if (this.board[index]) return;
            this.makeMove(index, 'X');
        });

        document.getElementById('reset').addEventListener('click', () => this.resetGame());
        document.getElementById('difficulty').addEventListener('change', (e) => {
            this.difficulty = e.target.value;
            this.resetGame();
        });
    }

    makeMove(index, player) {
        this.board[index] = player;
        const cell = document.querySelector(`[data-index="${index}"]`);
        cell.classList.add(player.toLowerCase());
        cell.textContent = player;

        const winner = this.checkWinner();
        if (winner) {
            this.endGame(winner);
            return;
        }
        if (!this.board.includes(null)) {
            this.endGame('draw');
            return;
        }

        this.currentPlayer = this.currentPlayer === 'X' ? 'O' : 'X';
        this.updateStatus();

        if (this.currentPlayer === 'O') {
            this.aiMove();
        }
    }

    aiMove() {
        let move;
        switch (this.difficulty) {
            case 'easy':
                move = this.getRandomMove();
                break;
            case 'medium':
                move = Math.random() > 0.5 ? this.getBestMove() : this.getRandomMove();
                break;
            case 'hard':
                move = this.getBestMove();
                break;
        }
        setTimeout(() => this.makeMove(move, 'O'), 500);
    }

    getRandomMove() {
        const available = this.board.reduce((acc, val, idx) => 
            val === null ? [...acc, idx] : acc, []);
        return available[Math.floor(Math.random() * available.length)];
    }

    getBestMove() {
        let bestScore = -Infinity;
        let move;
        for (let i = 0; i < 9; i++) {
            if (this.board[i] === null) {
                this.board[i] = 'O';
                const score = this.minimax(this.board, 0, false);
                this.board[i] = null;
                if (score > bestScore) {
                    bestScore = score;
                    move = i;
                }
            }
        }
        return move;
    }

    minimax(board, depth, isMaximizing) {
        const winner = this.checkWinner();
        if (winner === 'O') return 10 - depth;
        if (winner === 'X') return -10 + depth;
        if (!board.includes(null)) return 0;

        if (isMaximizing) {
            let bestScore = -Infinity;
            for (let i = 0; i < 9; i++) {
                if (board[i] === null) {
                    board[i] = 'O';
                    const score = this.minimax(board, depth + 1, false);
                    board[i] = null;
                    bestScore = Math.max(score, bestScore);
                }
            }
            return bestScore;
        } else {
            let bestScore = Infinity;
            for (let i = 0; i < 9; i++) {
                if (board[i] === null) {
                    board[i] = 'X';
                    const score = this.minimax(board, depth + 1, true);
                    board[i] = null;
                    bestScore = Math.min(score, bestScore);
                }
            }
            return bestScore;
        }
    }

    checkWinner() {
        const wins = [
            [0, 1, 2], [3, 4, 5], [6, 7, 8],
            [0, 3, 6], [1, 4, 7], [2, 5, 8],
            [0, 4, 8], [2, 4, 6]
        ];

        for (const [a, b, c] of wins) {
            if (this.board[a] && this.board[a] === this.board[b] && this.board[a] === this.board[c]) {
                return this.board[a];
            }
        }
        return null;
    }

    endGame(result) {
        this.gameActive = false;
        if (result === 'X') {
            this.playerScore++;
            this.updateStatus('You win!');
        } else if (result === 'O') {
            this.aiScore++;
            this.updateStatus('AI wins!');
        } else {
            this.updateStatus('Draw!');
        }
        this.updateScore();
    }

    resetGame() {
        this.board = Array(9).fill(null);
        this.currentPlayer = 'X';
        this.gameActive = true;
        this.createBoard();
        this.updateStatus();
    }

    updateStatus(text) {
        document.getElementById('status').textContent = text || 
            `${this.currentPlayer === 'X' ? 'Your' : 'AI'} turn (${this.currentPlayer})`;
    }

    updateScore() {
        document.getElementById('player-score').textContent = this.playerScore;
        document.getElementById('ai-score').textContent = this.aiScore;
    }
}

document.addEventListener('DOMContentLoaded', () => new TicTacToe());



    Enter fullscreen mode
    


    Exit fullscreen mode
    





  
  
  2️⃣ Handling Player Moves
When the player clicks on a cell, we check if the move is valid, update the board, and check for a winner.

makeMove(index, player) {
    this.board[index] = player; // Update the board
    const cell = document.querySelector(`[data-index="${index}"]`);
    cell.classList.add(player.toLowerCase()); // Add X or O class
    cell.textContent = player; // Display X or O

    const winner = this.checkWinner(); // Check if the move resulted in a win
    if (winner) {
        this.endGame(winner);
        return;
    }
    if (!this.board.includes(null)) { // Check for a draw
        this.endGame('draw');
        return;
    }

    this.currentPlayer = this.currentPlayer === 'X' ? 'O' : 'X'; // Switch turns
    this.updateStatus();

    if (this.currentPlayer === 'O') { // AI's turn
        this.aiMove();
    }
}



    Enter fullscreen mode
    


    Exit fullscreen mode
    





  
  
  3️⃣ Implementing AI Logic
The AI makes a move based on the selected difficulty level:  

Easy: Picks a random available move.


Medium: 50% chance of choosing the best move, otherwise picks randomly.


Hard: Uses the Minimax algorithm to always make the best possible move.


aiMove() {
    let move;
    switch (this.difficulty) {
        case 'easy':
            move = this.getRandomMove(); // Random move
            break;
        case 'medium':
            move = Math.random() > 0.5 ? this.getBestMove() : this.getRandomMove(); // Mix of random and smart moves
            break;
        case 'hard':
            move = this.getBestMove(); // Always the best move
            break;
    }
    setTimeout(() => this.makeMove(move, 'O'), 500); // Simulate AI "thinking"
}



    Enter fullscreen mode
    


    Exit fullscreen mode
    





  
  
  4️⃣ The Minimax Algorithm
The Minimax algorithm is the brain behind the Hard AI. It simulates all possible moves and chooses the one with the highest score.

minimax(board, depth, isMaximizing) {
    const winner = this.checkWinner();
    if (winner === 'O') return 10 - depth; // AI wins
    if (winner === 'X') return -10 + depth; // Player wins
    if (!board.includes(null)) return 0; // Draw

    if (isMaximizing) {
        let bestScore = -Infinity;
        for (let i = 0; i < 9; i++) {
            if (board[i] === null) {
                board[i] = 'O';
                const score = this.minimax(board, depth + 1, false);
                board[i] = null;
                bestScore = Math.max(score, bestScore);
            }
        }
        return bestScore;
    } else {
        let bestScore = Infinity;
        for (let i = 0; i < 9; i++) {
            if (board[i] === null) {
                board[i] = 'X';
                const score = this.minimax(board, depth + 1, true);
                board[i] = null;
                bestScore = Math.min(score, bestScore);
            }
        }
        return bestScore;
    }
}



    Enter fullscreen mode
    


    Exit fullscreen mode
    





  
  
  5️⃣ Checking for a Winner
After every move, we check if a player has won or if the game has ended in a draw.

checkWinner() {
    const wins = [
        [0, 1, 2], [3, 4, 5], [6, 7, 8], // Rows
        [0, 3, 6], [1, 4, 7], [2, 5, 8], // Columns
        [0, 4, 8], [2, 4, 6] // Diagonals
    ];

    for (const [a, b, c] of wins) {
        if (this.board[a] && this.board[a] === this.board[b] && this.board[a] === this.board[c]) {
            return this.board[a]; // Return the winning player (X or O)
        }
    }
    return null; // No winner yet
}



    Enter fullscreen mode
    


    Exit fullscreen mode
    





  
  
  6️⃣ Resetting the Game
A reset button allows players to start a new game without reloading the page.

resetGame() {
    this.board = Array(9).fill(null); // Clear the board
    this.currentPlayer = 'X'; // Reset to player's turn
    this.gameActive = true; // Reactivate the game
    this.createBoard(); // Rebuild the board UI
    this.updateStatus(); // Reset status message
}



    Enter fullscreen mode
    


    Exit fullscreen mode
    





  
  
  🏆 The AI: How It Works
The AI uses the Minimax algorithm to analyze all possible moves and choose the optimal one. Here’s a brief explanation:  

The AI simulates every possible move and assigns a score:  


Win = +10 points

Loss = -10 points

Draw = 0 points



It recursively calculates the best move based on maximizing its own score and minimizing the player’s score.  
On Hard mode, the AI always selects the move with the highest score, making it unbeatable!  

  
  
  🎉 Wrapping Up
Congratulations! 🎊 You’ve just built an AI-powered Tic-Tac-Toe game! Now you can:  
Experiment with different AI strategies

Improve the UI with new colors and animations

Add multiplayer functionality

🕹️ Play Now: Tic-Tac-Toe AI  Happy coding! 🚀