Skip to main content

Step 3 - Complete Code: Win Detection and Polish

Let's add move tracking, win detection, restart functionality, and final polish!

Step 1: Add Move Counter

Track how many moves the player has made:

let cols = 4;
let rows = 4;
let cardWidth;
let cardHeight;
let cards = [];
let flippedCards = [];
let canClick = true;
let moves = 0; // Track number of moves

Increment moves when checking cards:

function checkMatch() {
let card1 = flippedCards[0];
let card2 = flippedCards[1];

moves++; // Increment move counter

if (card1.value === card2.value) {
card1.matched = true;
card2.matched = true;
flippedCards = [];
} else {
canClick = false;

setTimeout(function() {
card1.flipped = false;
card2.flipped = false;
flippedCards = [];
canClick = true;
}, 1000);
}
}

Breaking it down

  • moves = 0 - Counter starts at zero
  • moves++ - Increment each time two cards are compared
  • One move = flipping two cards (whether they match or not)

Step 2: Display Move Counter

Show moves at the top of the screen:

function draw() {
background(50);

// Display moves at top
fill(255);
noStroke();
textSize(24);
textAlign(CENTER);
text("Moves: " + moves, width / 2, 30);

let anyHovering = false;

// ... rest of card drawing code ...
}

Breaking it down

  • fill(255) - White text
  • textSize(24) - Clear, readable size
  • textAlign(CENTER) - Centered at top
  • text("Moves: " + moves, width / 2, 30) - Display at top of canvas
    • width / 2 = center horizontally (300)
    • 30 = 30 pixels from top

Test it: Play the game - moves counter should increase!

Step 3: Check for Win

Detect when all cards are matched:

function checkMatch() {
let card1 = flippedCards[0];
let card2 = flippedCards[1];

moves++;

if (card1.value === card2.value) {
card1.matched = true;
card2.matched = true;
flippedCards = [];

// Check if all cards are matched
checkWin();
} else {
canClick = false;

setTimeout(function() {
card1.flipped = false;
card2.flipped = false;
flippedCards = [];
canClick = true;
}, 1000);
}
}

function checkWin() {
// Count matched cards
let matchedCount = 0;
for (let card of cards) {
if (card.matched) {
matchedCount++;
}
}

// If all 16 cards are matched, player wins
if (matchedCount === 16) {
console.log("You win! Moves:", moves);
}
}

Breaking it down

  • checkWin() - Called after each successful match
  • Loop through all cards and count how many are matched
  • if (matchedCount === 16) - All cards matched = win!
  • 16 cards total (8 pairs)
  • Console logs victory message

Test it: Match all pairs - console should say you won!

Step 4: Add Game Over State

Add variable to track if game is over:

let cols = 4;
let rows = 4;
let cardWidth;
let cardHeight;
let cards = [];
let flippedCards = [];
let canClick = true;
let moves = 0;
let gameOver = false; // Track if game is won

Update checkWin() to set game over:

function checkWin() {
let matchedCount = 0;
for (let card of cards) {
if (card.matched) {
matchedCount++;
}
}

if (matchedCount === 16) {
gameOver = true;
}
}

Prevent clicking when game is over:

function mousePressed() {
if (!canClick || gameOver) { // Also check gameOver
return;
}

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

Breaking it down

  • gameOver = false - Starts as false
  • Set to true when all cards matched
  • if (!canClick || gameOver) return; - Prevent clicks after win
  • No more moves can be made once game is won

Step 5: Display Victory Message

Show a congratulations message when game is won:

function draw() {
background(50);

// Display moves at top
fill(255);
noStroke();
textSize(24);
textAlign(CENTER);
text("Moves: " + moves, width / 2, 30);

let anyHovering = false;

// ... card drawing code ...

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

// Display victory message
if (gameOver) {
// Semi-transparent overlay
fill(0, 0, 0, 200);
noStroke();
rect(0, 0, width, height);

// Victory text
fill(255, 215, 0); // Gold color
textSize(48);
textAlign(CENTER, CENTER);
text("You Win!", width / 2, height / 2 - 40);

fill(255);
textSize(28);
text("Total Moves: " + moves, width / 2, height / 2 + 20);

textSize(20);
text("Press SPACE to play again", width / 2, height / 2 + 60);
}
}

Breaking it down

  • Semi-transparent overlay:

    • fill(0, 0, 0, 200) - Dark overlay (200 = mostly opaque)
    • rect(0, 0, width, height) - Cover entire canvas
    • Creates dramatic "game over" effect
  • Victory text:

    • fill(255, 215, 0) - Gold color for "You Win!"
    • textSize(48) - Large and celebratory
    • textAlign(CENTER, CENTER) - Centered both ways
    • Position: height / 2 - 40 (above center)
  • Move count:

    • White text, size 28
    • Shows final score
    • Position: height / 2 + 20 (below center)
  • Restart instructions:

    • Size 20, white text
    • Position: height / 2 + 60 (bottom of message)

Test it: Win the game - you should see a gold victory message!

Step 6: Add Restart Functionality

Let players restart with SPACE key:

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

function resetGame() {
// Reset game variables
moves = 0;
gameOver = false;
flippedCards = [];
canClick = true;

// Clear and recreate cards
cards = [];

let values = [1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8];
values = shuffle(values);

let index = 0;
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
let card = {
value: values[index],
x: i * (cardWidth + 20) + 10,
y: j * (cardHeight + 20) + 10,
flipped: false,
matched: false
};
cards.push(card);
index++;
}
}
}

Breaking it down

  • keyPressed() - Called when any key is pressed
  • if (key === ' ' && gameOver) - Only restart if SPACE pressed and game is over
  • resetGame() - Resets everything to starting state:
    • moves = 0 - Reset move counter
    • gameOver = false - Game is active again
    • flippedCards = [] - Clear flipped cards
    • canClick = true - Allow clicking
    • cards = [] - Clear card array
    • Recreate all cards with shuffled values (same as setup)
  • Cards are reshuffled, so new game has different layout!

Test it: Win a game, press SPACE - you can play again!

Step 7: Add Color Coding for Card Values

Let's make each card value have its own color:

function getCardColor(value) {
let colors = [
[255, 100, 100], // 1 - Red
[100, 255, 100], // 2 - Green
[100, 100, 255], // 3 - Blue
[255, 255, 100], // 4 - Yellow
[255, 100, 255], // 5 - Magenta
[100, 255, 255], // 6 - Cyan
[255, 150, 100], // 7 - Orange
[200, 100, 255] // 8 - Purple
];
return colors[value - 1]; // value is 1-8, array is 0-7
}

Update card drawing to use colors:

function draw() {
background(50);

fill(255);
noStroke();
textSize(24);
textAlign(CENTER);
text("Moves: " + moves, width / 2, 30);

let anyHovering = false;

for (let card of cards) {
let hovering = false;
if (canClick && !card.flipped && !card.matched) {
if (mouseX > card.x && mouseX < card.x + cardWidth &&
mouseY > card.y && mouseY < card.y + cardHeight) {
hovering = true;
anyHovering = true;
}
}

if (card.flipped || card.matched) {
// Get color for this card's value
let col = getCardColor(card.value);

if (hovering) {
fill(col[0] + 20, col[1] + 20, col[2] + 20); // Brighter
} else {
fill(col[0], col[1], col[2]);
}
stroke(255);
strokeWeight(3);
rect(card.x, card.y, cardWidth, cardHeight, 10);

fill(255);
noStroke();
textSize(40);
textAlign(CENTER, CENTER);
text(card.value, card.x + cardWidth / 2, card.y + cardHeight / 2);
} else {
if (hovering) {
fill(120, 170, 220);
} else {
fill(100, 150, 200);
}
stroke(255);
strokeWeight(3);
rect(card.x, card.y, cardWidth, cardHeight, 10);

stroke(80, 120, 160);
strokeWeight(2);
line(card.x + 10, card.y + 10,
card.x + cardWidth - 10, card.y + cardHeight - 10);
line(card.x + cardWidth - 10, card.y + 10,
card.x + 10, card.y + cardHeight - 10);
}
}

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

if (gameOver) {
fill(0, 0, 0, 200);
noStroke();
rect(0, 0, width, height);

fill(255, 215, 0);
textSize(48);
textAlign(CENTER, CENTER);
text("You Win!", width / 2, height / 2 - 40);

fill(255);
textSize(28);
text("Total Moves: " + moves, width / 2, height / 2 + 20);

textSize(20);
text("Press SPACE to play again", width / 2, height / 2 + 60);
}
}

Breaking it down

  • getCardColor(value) - Returns RGB array for each value 1-8
  • Each number gets unique color:
    • 1 = Red, 2 = Green, 3 = Blue, 4 = Yellow
    • 5 = Magenta, 6 = Cyan, 7 = Orange, 8 = Purple
  • let col = getCardColor(card.value) - Get color for this card
  • fill(col[0], col[1], col[2]) - Use the color
  • Adds visual variety and makes matching more interesting!

Test it: Flip cards - each number has its own color!

Step 8: Add Best Score Tracking

Track the best (lowest) score:

let cols = 4;
let rows = 4;
let cardWidth;
let cardHeight;
let cards = [];
let flippedCards = [];
let canClick = true;
let moves = 0;
let gameOver = false;
let bestScore = Infinity; // Start with infinity (any score is better)

Update when winning:

function checkWin() {
let matchedCount = 0;
for (let card of cards) {
if (card.matched) {
matchedCount++;
}
}

if (matchedCount === 16) {
gameOver = true;

// Update best score
if (moves < bestScore) {
bestScore = moves;
}
}
}

Display best score:

function draw() {
background(50);

// Display moves and best score
fill(255);
noStroke();
textSize(24);
textAlign(LEFT);
text("Moves: " + moves, 20, 30);

textAlign(RIGHT);
if (bestScore === Infinity) {
text("Best: --", width - 20, 30);
} else {
text("Best: " + bestScore, width - 20, 30);
}

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

Breaking it down

  • bestScore = Infinity - Start with "infinite" score
  • After winning, check if current moves < bestScore
  • If better, update bestScore
  • Display at top right:
    • Shows "--" if no games completed yet
    • Shows actual best score once at least one game finished
  • Persists across restarts (within same session)
  • Gives players a goal to beat!

Test it: Win a game - best score appears. Play again and try to beat it!

Step 9: Add Smooth Color Transitions

Add subtle animation to matched cards:

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

for (let card of cards) {
// ... hover detection code ...

if (card.flipped || card.matched) {
let col = getCardColor(card.value);

// Pulse effect for matched cards
if (card.matched) {
let pulse = sin(frameCount * 0.05) * 10;
fill(col[0] + pulse, col[1] + pulse, col[2] + pulse);
} else if (hovering) {
fill(col[0] + 20, col[1] + 20, col[2] + 20);
} else {
fill(col[0], col[1], col[2]);
}

// ... rest of card drawing ...
}
// ... rest of code ...
}
}

Breaking it down

  • sin(frameCount * 0.05) - Oscillates between -1 and 1
  • Multiply by 10 to get -10 to +10
  • Add to RGB values to create subtle pulse
  • Only applies to matched cards
  • Creates satisfying "completion" feeling
  • Uses frameCount (p5.js variable for frame number)

Test it: Match some pairs - they gently pulse!

Complete Final Code

Here's the complete, polished Matching Game:

let cols = 4;
let rows = 4;
let cardWidth;
let cardHeight;
let cards = [];
let flippedCards = [];
let canClick = true;
let moves = 0;
let gameOver = false;
let bestScore = Infinity;

function setup() {
createCanvas(600, 600);

cardWidth = width / cols - 20;
cardHeight = height / rows - 20;

createCards();
}

function createCards() {
cards = [];

let values = [1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8];
values = shuffle(values);

let index = 0;
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
let card = {
value: values[index],
x: i * (cardWidth + 20) + 10,
y: j * (cardHeight + 20) + 10,
flipped: false,
matched: false
};
cards.push(card);
index++;
}
}
}

function draw() {
background(50);

// Display moves and best score
fill(255);
noStroke();
textSize(24);
textAlign(LEFT);
text("Moves: " + moves, 20, 30);

textAlign(RIGHT);
if (bestScore === Infinity) {
text("Best: --", width - 20, 30);
} else {
text("Best: " + bestScore, width - 20, 30);
}

let anyHovering = false;

for (let card of cards) {
let hovering = false;
if (canClick && !card.flipped && !card.matched && !gameOver) {
if (mouseX > card.x && mouseX < card.x + cardWidth &&
mouseY > card.y && mouseY < card.y + cardHeight) {
hovering = true;
anyHovering = true;
}
}

if (card.flipped || card.matched) {
let col = getCardColor(card.value);

if (card.matched) {
let pulse = sin(frameCount * 0.05) * 10;
fill(col[0] + pulse, col[1] + pulse, col[2] + pulse);
} else if (hovering) {
fill(col[0] + 20, col[1] + 20, col[2] + 20);
} else {
fill(col[0], col[1], col[2]);
}
stroke(255);
strokeWeight(3);
rect(card.x, card.y, cardWidth, cardHeight, 10);

fill(255);
noStroke();
textSize(40);
textAlign(CENTER, CENTER);
text(card.value, card.x + cardWidth / 2, card.y + cardHeight / 2);
} else {
if (hovering) {
fill(120, 170, 220);
} else {
fill(100, 150, 200);
}
stroke(255);
strokeWeight(3);
rect(card.x, card.y, cardWidth, cardHeight, 10);

stroke(80, 120, 160);
strokeWeight(2);
line(card.x + 10, card.y + 10,
card.x + cardWidth - 10, card.y + cardHeight - 10);
line(card.x + cardWidth - 10, card.y + 10,
card.x + 10, card.y + cardHeight - 10);
}
}

if (anyHovering && !gameOver) {
cursor(HAND);
} else {
cursor(ARROW);
}

if (gameOver) {
fill(0, 0, 0, 200);
noStroke();
rect(0, 0, width, height);

fill(255, 215, 0);
textSize(48);
textAlign(CENTER, CENTER);
text("You Win!", width / 2, height / 2 - 40);

fill(255);
textSize(28);
text("Total Moves: " + moves, width / 2, height / 2 + 20);

textSize(20);
text("Press SPACE to play again", width / 2, height / 2 + 60);
}
}

function mousePressed() {
if (!canClick || gameOver) {
return;
}

if (flippedCards.length >= 2) {
return;
}

for (let card of cards) {
if (mouseX > card.x && mouseX < card.x + cardWidth &&
mouseY > card.y && mouseY < card.y + cardHeight) {

if (card.matched || card.flipped) {
continue;
}

card.flipped = true;
flippedCards.push(card);

if (flippedCards.length === 2) {
checkMatch();
}
}
}
}

function checkMatch() {
let card1 = flippedCards[0];
let card2 = flippedCards[1];

moves++;

if (card1.value === card2.value) {
card1.matched = true;
card2.matched = true;
flippedCards = [];
checkWin();
} else {
canClick = false;

setTimeout(function() {
card1.flipped = false;
card2.flipped = false;
flippedCards = [];
canClick = true;
}, 1000);
}
}

function checkWin() {
let matchedCount = 0;
for (let card of cards) {
if (card.matched) {
matchedCount++;
}
}

if (matchedCount === 16) {
gameOver = true;

if (moves < bestScore) {
bestScore = moves;
}
}
}

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

function resetGame() {
moves = 0;
gameOver = false;
flippedCards = [];
canClick = true;
createCards();
}

function getCardColor(value) {
let colors = [
[255, 100, 100], // 1 - Red
[100, 255, 100], // 2 - Green
[100, 100, 255], // 3 - Blue
[255, 255, 100], // 4 - Yellow
[255, 100, 255], // 5 - Magenta
[100, 255, 255], // 6 - Cyan
[255, 150, 100], // 7 - Orange
[200, 100, 255] // 8 - Purple
];
return colors[value - 1];
}

What You Built

Congratulations! You now have a fully functional Matching Game with:

  • ✅ 4×4 grid of cards (16 cards total)
  • ✅ 8 pairs with unique colors
  • ✅ Click to flip cards face-up
  • ✅ Match detection
  • ✅ Non-matching cards flip back after delay
  • ✅ Move counter tracking efficiency
  • ✅ Best score tracking
  • ✅ Win detection when all pairs found
  • ✅ Victory message with stats
  • ✅ Restart with SPACE key
  • ✅ Hover effects for better UX
  • ✅ Cursor changes on clickable cards
  • ✅ Color-coded card values
  • ✅ Subtle pulse animation on matched cards
  • ✅ Polished, professional appearance

What You Learned

Throughout this tutorial, you learned:

Game Development Concepts

  • Grid-based card layouts
  • Card state management (face-down, face-up, matched)
  • Match detection logic
  • Scoring and tracking
  • Win conditions

p5.js Skills

  • Canvas setup and drawing
  • Object creation and management
  • Array shuffling
  • Mouse input detection
  • Keyboard input
  • Text display and formatting
  • Color manipulation
  • Simple animations with sin()
  • setTimeout for delays

Programming Techniques

  • Object properties and states
  • Array manipulation
  • Boundary detection for clicks
  • State machines (can/can't click)
  • Best score tracking
  • Modular function design

Ideas to Expand

Want to keep improving your game? Try these challenges:

Easy Additions

  • Add sound effects for flips and matches
  • Change card back patterns
  • Different grid sizes (3×4, 6×6)
  • Use symbols or emojis instead of numbers
  • Add a timer for speed challenges

Medium Challenges

  • Save best score to localStorage
  • Add difficulty levels (different grid sizes)
  • Animate card flips (rotation effect)
  • Add star rating based on moves (3 stars = excellent)
  • Create themed card sets (animals, fruits, etc.)

Advanced Features

  • Multiplayer mode (take turns)
  • Progressive difficulty (start with fewer cards)
  • Memory training mode (show all cards briefly at start)
  • Leaderboard system
  • Card flip animations using transformations

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 a Matching Game, you're ready for more complex projects!

Check out our other tutorials:

Or explore the Common Elements reference to learn more techniques!

Congratulations! 🎉

You've successfully built a complete Matching Game! This is a fantastic achievement in your game development journey.

You started with an empty canvas and built:

  • A card grid system
  • State management for multiple cards
  • Match detection logic
  • Scoring and win detection
  • Beautiful, polished gameplay

These skills are fundamental to many types of games. Keep coding, keep experimenting, and most importantly - have fun! 🎴✨


Tutorial Complete! Check out more at the tutorials page