Skip to main content

Step 4 - Complete Code: Restart and Polish

Let's add restart functionality and final polish to complete our Tic-Tac-Toe game!

Step 1: Add Restart with Keyboard

Let's allow players to restart with the SPACE key:

function keyPressed() {
if (key === ' ' && gameOver) {
resetGame();
}
}

function resetGame() {
// Clear the board
board = [
['', '', ''],
['', '', ''],
['', '', '']
];

// Reset game state
currentPlayer = 'X';
gameOver = false;
winner = '';
winningLine = null;
}

Breaking it down

  • keyPressed() - Called when any key is pressed
  • if (key === ' ') - Check if SPACE bar was pressed
  • && gameOver - Only allow restart when game is over
  • resetGame() - Resets everything to starting state:
    • Clear all symbols from board
    • Set currentPlayer back to 'X'
    • Set gameOver to false
    • Clear winner and winningLine

Test it: Finish a game, then press SPACE - you should be able to play again!

Step 2: Add Restart Instructions

Update the game over message to tell players they can restart:

function draw() {
// ... existing code ...

// Display current player or game over message
fill(0);
noStroke();
textSize(24);
textAlign(CENTER);

if (gameOver) {
if (winner === 'tie') {
text("It's a Tie!", width / 2, 30);
} else {
text(winner + " Wins!", width / 2, 30);
}

// Add restart instructions
textSize(16);
text("Press SPACE to play again", width / 2, height - 20);
} else {
text("Current Player: " + currentPlayer, width / 2, 30);
}
}

Breaking it down

  • After displaying winner/tie message
  • textSize(16) - Smaller text for instructions
  • text("Press SPACE to play again", width / 2, height - 20) - Bottom of screen
    • height - 20 = 20 pixels from bottom

Test it: Finish a game - you should see the restart message at the bottom!

Step 3: Add Fade Animation for Game Over

Let's add a semi-transparent overlay when game ends:

function draw() {
background(255);

// Highlight hovered cell
if (!gameOver && mouseX >= 0 && mouseX < width && mouseY >= 0 && mouseY < height) {
let i = floor(mouseX / cellSize);
let j = floor(mouseY / cellSize);

if (i >= 0 && i < 3 && j >= 0 && j < 3) {
if (board[i][j] === '') {
fill(200, 200, 200, 100);
noStroke();
rect(i * cellSize, j * cellSize, cellSize, cellSize);
}
}
}

// Draw grid lines
stroke(0);
strokeWeight(4);
line(cellSize, 0, cellSize, height);
line(cellSize * 2, 0, cellSize * 2, height);
line(0, cellSize, width, cellSize);
line(0, cellSize * 2, width, cellSize * 2);

// Draw X's and O's
drawBoard();

// Draw winning line if game is over
if (gameOver) {
drawWinningLine();

// Add semi-transparent overlay
fill(255, 255, 255, 200);
noStroke();
rect(0, 0, width, height);

// Redraw the board on top of overlay
drawBoard();
drawWinningLine();
}

// Display current player or game over message
fill(0);
noStroke();
textSize(24);
textAlign(CENTER);

if (gameOver) {
if (winner === 'tie') {
text("It's a Tie!", width / 2, 30);
} else {
text(winner + " Wins!", width / 2, 30);
}

textSize(16);
text("Press SPACE to play again", width / 2, height - 20);
} else {
text("Current Player: " + currentPlayer, width / 2, 30);
}
}

Breaking it down

Hover effect:

if (!gameOver && mouseX >= 0 && mouseX < width && mouseY >= 0 && mouseY < height) {
let i = floor(mouseX / cellSize);
let j = floor(mouseY / cellSize);

if (i >= 0 && i < 3 && j >= 0 && j < 3 && board[i][j] === '') {
fill(200, 200, 200, 100);
rect(i * cellSize, j * cellSize, cellSize, cellSize);
}
}
  • Only shows when game is still active
  • Highlights which cell the mouse is over

Game over overlay:

if (gameOver) {
drawWinningLine();
fill(255, 255, 255, 200); // Semi-transparent white
rect(0, 0, width, height); // Cover entire board
drawBoard(); // Redraw on top of overlay
drawWinningLine();
}
  • When game ends, creates a fade effect
  • Overlay makes board appear slightly faded
  • Redraws board and winning line on top to highlight winner

Game over message:

if (gameOver) {
if (winner === 'tie') {
text("It's a Tie!", width / 2, 30);
} else {
text(winner + " Wins!", width / 2, 30);
}
text("Press SPACE to play again", width / 2, height - 20);
} else {
text("Current Player: " + currentPlayer, width / 2, 30);
}
  • Displays winner or current player
  • Shows restart instructions when game is over

Step 4: Improve Symbol Drawing

Let's make X's and O's slightly more animated:

function drawX(x, y) {
let offset = cellSize / 4;
stroke(255, 0, 0);
strokeWeight(8);
strokeCap(ROUND); // Rounded ends
line(x - offset, y - offset, x + offset, y + offset);
line(x + offset, y - offset, x - offset, y + offset);
}

function drawO(x, y) {
stroke(0, 0, 255);
strokeWeight(8);
strokeCap(ROUND); // Rounded ends
noFill();
circle(x, y, cellSize / 2);
}

Breaking it down

  • strokeCap(ROUND) - Makes line ends rounded instead of square
  • Gives symbols a smoother, more polished look

Step 5: Add Score Tracking

Let's keep track of wins across multiple games:

let cellSize = 400 / 3;
let board = [
['', '', ''],
['', '', ''],
['', '', '']
];
let currentPlayer = 'X';
let gameOver = false;
let winner = '';
let winningLine = null;
let xWins = 0;
let oWins = 0;
let ties = 0;

Update mousePressed() to increment scores:

function mousePressed() {
if (gameOver) return;

let i = floor(mouseX / cellSize);
let j = floor(mouseY / cellSize);

if (i >= 0 && i < 3 && j >= 0 && j < 3) {
if (board[i][j] === '') {
board[i][j] = currentPlayer;

// Check for winner
let result = checkWinner();
if (result !== null) {
gameOver = true;
winner = result;

// Update scores
if (winner === 'X') {
xWins++;
} else if (winner === 'O') {
oWins++;
}
} else if (checkTie()) {
gameOver = true;
winner = 'tie';
ties++; // Update tie count
} else {
// Switch to other player
if (currentPlayer === 'X') {
currentPlayer = 'O';
} else {
currentPlayer = 'X';
}
}
}
}
}

Display scores at the bottom:

function draw() {
// ... existing code ...

// Display current player or game over message
fill(0);
noStroke();
textSize(24);
textAlign(CENTER);

if (gameOver) {
if (winner === 'tie') {
text("It's a Tie!", width / 2, 30);
} else {
text(winner + " Wins!", width / 2, 30);
}

textSize(16);
text("Press SPACE to play again", width / 2, height - 20);
} else {
text("Current Player: " + currentPlayer, width / 2, 30);
}

// Display score at bottom
textSize(18);
textAlign(LEFT);
text("X: " + xWins, 10, height - 10);
textAlign(CENTER);
text("Ties: " + ties, width / 2, height - 10);
textAlign(RIGHT);
text("O: " + oWins, width - 10, height - 10);
}

Breaking it down

  • Three score variables: xWins, oWins, ties
  • Increment appropriate score when game ends
  • Display at bottom:
    • Left side: X wins
    • Center: Ties
    • Right side: O wins
  • Scores persist across game restarts!

Step 6: Add Cursor Change on Hover

Make cursor change to pointer when hovering empty cells:

function draw() {
background(255);

// Change cursor based on hover
let hovering = false;
if (!gameOver && mouseX >= 0 && mouseX < width && mouseY >= 0 && mouseY < height) {
let i = floor(mouseX / cellSize);
let j = floor(mouseY / cellSize);

if (i >= 0 && i < 3 && j >= 0 && j < 3) {
if (board[i][j] === '') {
hovering = true;
fill(200, 200, 200, 100);
noStroke();
rect(i * cellSize, j * cellSize, cellSize, cellSize);
}
}
}

// Set cursor
if (hovering) {
cursor(HAND);
} else {
cursor(ARROW);
}

// ... rest of draw code ...
}

Breaking it down

  • Track if mouse is over an empty cell with hovering variable
  • cursor(HAND) - Shows pointing hand cursor
  • cursor(ARROW) - Normal arrow cursor
  • Gives visual feedback that cells are clickable

Complete Final Code

Here's the complete, polished Tic-Tac-Toe game:

let cellSize = 400 / 3;
let board = [
['', '', ''],
['', '', ''],
['', '', '']
];
let currentPlayer = 'X';
let gameOver = false;
let winner = '';
let winningLine = null;
let xWins = 0;
let oWins = 0;
let ties = 0;

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

function draw() {
background(255);

// Change cursor and highlight hovered cell
let hovering = false;
if (!gameOver && mouseX >= 0 && mouseX < width && mouseY >= 0 && mouseY < height) {
let i = floor(mouseX / cellSize);
let j = floor(mouseY / cellSize);

if (i >= 0 && i < 3 && j >= 0 && j < 3) {
if (board[i][j] === '') {
hovering = true;
fill(200, 200, 200, 100);
noStroke();
rect(i * cellSize, j * cellSize, cellSize, cellSize);
}
}
}

if (hovering) {
cursor(HAND);
} else {
cursor(ARROW);
}

// Draw grid lines
stroke(0);
strokeWeight(4);
line(cellSize, 0, cellSize, height);
line(cellSize * 2, 0, cellSize * 2, height);
line(0, cellSize, width, cellSize);
line(0, cellSize * 2, width, cellSize * 2);

// Draw X's and O's
drawBoard();

// Draw winning line and overlay if game is over
if (gameOver) {
drawWinningLine();

fill(255, 255, 255, 200);
noStroke();
rect(0, 0, width, height);

drawBoard();
drawWinningLine();
}

// Display current player or game over message
fill(0);
noStroke();
textSize(24);
textAlign(CENTER);

if (gameOver) {
if (winner === 'tie') {
text("It's a Tie!", width / 2, 30);
} else {
text(winner + " Wins!", width / 2, 30);
}

textSize(16);
text("Press SPACE to play again", width / 2, height - 20);
} else {
text("Current Player: " + currentPlayer, width / 2, 30);
}

// Display scores
textSize(18);
textAlign(LEFT);
text("X: " + xWins, 10, height - 10);
textAlign(CENTER);
text("Ties: " + ties, width / 2, height - 10);
textAlign(RIGHT);
text("O: " + oWins, width - 10, height - 10);
}

function drawBoard() {
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
let x = i * cellSize + cellSize / 2;
let y = j * cellSize + cellSize / 2;
let symbol = board[i][j];

if (symbol === 'X') {
drawX(x, y);
} else if (symbol === 'O') {
drawO(x, y);
}
}
}
}

function drawX(x, y) {
let offset = cellSize / 4;
stroke(255, 0, 0);
strokeWeight(8);
strokeCap(ROUND);
line(x - offset, y - offset, x + offset, y + offset);
line(x + offset, y - offset, x - offset, y + offset);
}

function drawO(x, y) {
stroke(0, 0, 255);
strokeWeight(8);
strokeCap(ROUND);
noFill();
circle(x, y, cellSize / 2);
}

function mousePressed() {
if (gameOver) return;

let i = floor(mouseX / cellSize);
let j = floor(mouseY / cellSize);

if (i >= 0 && i < 3 && j >= 0 && j < 3) {
if (board[i][j] === '') {
board[i][j] = currentPlayer;

let result = checkWinner();
if (result !== null) {
gameOver = true;
winner = result;

if (winner === 'X') {
xWins++;
} else if (winner === 'O') {
oWins++;
}
} else if (checkTie()) {
gameOver = true;
winner = 'tie';
ties++;
} else {
if (currentPlayer === 'X') {
currentPlayer = 'O';
} else {
currentPlayer = 'X';
}
}
}
}
}

function keyPressed() {
if (key === ' ' && gameOver) {
resetGame();
}
}

function resetGame() {
board = [
['', '', ''],
['', '', ''],
['', '', '']
];
currentPlayer = 'X';
gameOver = false;
winner = '';
winningLine = null;
}

function checkWinner() {
// Check rows
for (let i = 0; i < 3; i++) {
if (board[i][0] !== '' &&
board[i][0] === board[i][1] &&
board[i][1] === board[i][2]) {
winningLine = {type: 'row', index: i};
return board[i][0];
}
}

// Check columns
for (let j = 0; j < 3; j++) {
if (board[0][j] !== '' &&
board[0][j] === board[1][j] &&
board[1][j] === board[2][j]) {
winningLine = {type: 'col', index: j};
return board[0][j];
}
}

// Check diagonal (top-left to bottom-right)
if (board[0][0] !== '' &&
board[0][0] === board[1][1] &&
board[1][1] === board[2][2]) {
winningLine = {type: 'diag', index: 0};
return board[0][0];
}

// Check diagonal (top-right to bottom-left)
if (board[2][0] !== '' &&
board[2][0] === board[1][1] &&
board[1][1] === board[0][2]) {
winningLine = {type: 'diag', index: 1};
return board[2][0];
}

return null;
}

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

function drawWinningLine() {
if (winningLine === null) return;

stroke(0, 255, 0);
strokeWeight(8);

if (winningLine.type === 'row') {
let x = winningLine.index * cellSize + cellSize / 2;
line(x, 0, x, height); // Vertical line through column
} else if (winningLine.type === 'col') {
let y = winningLine.index * cellSize + cellSize / 2;
line(0, y, width, y); // Horizontal line through row
} else if (winningLine.type === 'diag') {
if (winningLine.index === 0) {
line(0, 0, width, height); // Top-left to bottom-right
} else {
line(width, 0, 0, height); // Top-right to bottom-left
}
}
}

What You Built

Congratulations! You now have a fully functional Tic-Tac-Toe game with:

  • ✅ Clean 3×3 game board with grid lines
  • ✅ Red X's and blue O's
  • ✅ Click to place symbols
  • ✅ Hover effects showing which cell you're about to click
  • ✅ Turn switching between players
  • ✅ Win detection for all 8 possible winning lines
  • ✅ Tie detection when board is full
  • ✅ Green line highlighting the winning symbols
  • ✅ Semi-transparent overlay on game over
  • ✅ Score tracking across multiple games
  • ✅ Restart with SPACE key
  • ✅ Clear instructions and feedback
  • ✅ Polished visuals with rounded line caps
  • ✅ Cursor changes on hover

What You Learned

Throughout this tutorial, you learned:

Game Development Concepts

  • Grid-based coordinate systems
  • 2D arrays for game boards
  • Turn-based game logic
  • Win condition checking
  • Game state management

p5.js Skills

  • Canvas setup and drawing
  • Mouse input with mousePressed()
  • Keyboard input with keyPressed()
  • Drawing shapes and lines
  • Text styling and alignment
  • Cursor customization
  • Transparency and overlays

Programming Techniques

  • Nested loops for 2D grids
  • Converting pixel coordinates to grid positions
  • Comprehensive condition checking
  • Modular function design
  • State management with boolean flags

Ideas to Expand

Want to keep improving your game? Try these challenges:

Easy Additions

  • Add sound effects for clicks and wins
  • Change colors and themes
  • Make the board bigger (4×4 or 5×5)
  • Add animations when symbols appear
  • Different symbols instead of X and O

Medium Challenges

  • Add a timer for each turn
  • Highlight the last move made
  • Add player names instead of X and O
  • Save high scores to localStorage
  • Add different difficulty levels

Advanced Features

  • Add computer AI opponent
  • Make AI unbeatable (minimax algorithm)
  • Create a tournament mode
  • Add online multiplayer
  • Make different board sizes selectable

Sharing Your Game

Ready to share? Here's how:

  1. Save your code - Click File → Save in the p5.js editor
  2. Share the link - Copy the URL from your browser
  3. Embed in website - Use File → Share → Embed
  4. Download - File → Download to save locally

What's Next?

Now that you've built Tic-Tac-Toe, you're ready for more complex games!

Check out our other tutorials:

Or explore the Common Elements reference to learn more techniques!

Congratulations! 🎉

You've successfully built a complete Tic-Tac-Toe game! This is a major milestone in your game development journey.

You started with an empty canvas and built:

  • A grid system
  • Interactive gameplay
  • Win detection logic
  • Polish and feedback

These skills transfer to any game you want to build. Keep coding, keep experimenting, and most importantly - have fun! ⭕❌


Tutorial Complete! Check out more at the tutorials page