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 zeromoves++- 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 texttextSize(24)- Clear, readable sizetextAlign(CENTER)- Centered at toptext("Moves: " + moves, width / 2, 30)- Display at top of canvaswidth / 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
truewhen 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 celebratorytextAlign(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 pressedif (key === ' ' && gameOver)- Only restart if SPACE pressed and game is overresetGame()- Resets everything to starting state:moves = 0- Reset move countergameOver = false- Game is active againflippedCards = []- Clear flipped cardscanClick = true- Allow clickingcards = []- 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 cardfill(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:
- Save your code - Click File → Save in the p5.js editor
- Share the link - Copy the URL from your browser
- Embed in website - Use File → Share → Embed
- 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:
- Cookie Clicker - Incremental game mechanics
- Worm/Snake - Movement and collision
- Pong - Two-player arcade action
- Tic-Tac-Toe - Classic strategy game
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