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
- How to structure a Tic-Tac-Toe game using HTML for the layout, CSS for styling, and JavaScript for the game logic.
- How to implement turn-based gameplay where players take turns placing their marks (X or O).
-
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.
- 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! 🚀