-
Section 1: Getting Started
-
Section 2: Core Syntax and Types
-
Section 3: Strings and Numbers in Depth
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Objects and Arrays
-
Section 7: Maps, Sets, and Symbols
-
Section 8: Asynchronous JavaScript
-
Section 9: Object-Oriented and Prototypes
-
Section 10: The DOM
-
Section 11: Browser APIs
-
Section 12: Modern JavaScript (ES2015-ES2025)
-
Section 13: Functional Programming Patterns
-
Section 14: Error Handling and Debugging
-
Section 15: Testing
-
Section 16: Accessibility for JavaScript Developers
-
Section 17: Internationalization and Localization
-
Section 18: Performance
-
Section 19: Node.js Fundamentals
-
Section 20: Regular Expressions
-
Section 21: Design Patterns in JavaScript
-
Section 22: Security Basics
-
Section 23: Data Structures and Algorithms in JavaScript
-
Section 24: Practical Projects
-
Section 25: More Advanced Async Patterns
-
Section 26: More Object and Class Practice
-
Section 27: Working with Dates and Internationalization
-
Section 28: Web Components
-
Section 29: More DOM and Browser Practice
-
Section 30: Build Tooling for Vanilla JavaScript
-
Section 31: More Practice Projects
-
Section 32: Interview and Algorithm Practice
-
Section 33: Error Objects (MDN Reference)
-
Section 34: TypedArrays and Binary Data
-
Section 35: Reflection and Metaprogramming (MDN Reference)
-
Section 36: More Global Functions (MDN Reference)
187: Building a Memory Card Matching Game
We've spent a lot of time talking about arrays and DOM events in isolation. Now, we're going to actually glue those concepts together. We're building a Memory Card Matching game. It sounds simple, but this is where you start to see how "state"βthe current condition of your appβreally drives the user experience.
Defining the Deck and the Shuffle
First, I need a set of pairs. I'll use an array of emojis because it saves us from having to deal with external image assets. Since we need pairs, I'll create a list and then duplicate it. To make the game playable, we obviously can't have the cards in the same order every time.
const symbols = ['π', 'π', 'π', 'π', 'π₯', 'π'];
const deck = [...symbols, ...symbols];
// A quick and dirty shuffle
deck.sort(() => Math.random() - 0.5);
Now, I'm going to loop through this shuffled array and generate the HTML elements. I'll wrap each card in a div and give it a data-attribute to store its symbol. This is a trick I use all the time: keep the "truth" of the element in the HTML attributes so the JavaScript doesn't have to maintain a massive secondary mapping object.
Wiring Up the Flip Logic
The core of the game is the click handler. When a card is clicked, we flip it over. But we can't just flip everything; we need to keep track of which cards are currently "active" to see if they match.
let flippedCards = [];
let matchedPairs = 0;
function flipCard(card) {
if (card === this.previousElementSibling || card.classList.contains('matched')) return;
card.classList.add('flipped');
flippedCards.push(card);
if (flippedCards.length === 2) {
checkMatch();
}
}
The "Three-Card" Bug
Here is where I usually trip up when I first build these. I'll be honest: when I first wrote this logic, I noticed a glitch. If I clicked two cards and they didn't match, there was a half-second delay before they flipped back. During that tiny window, I could click a third or fourth card. This completely broke the `flippedCards` array, and the game would just stop working because it was trying to match the wrong pairs.
The fix is a "lock." I need a boolean variable that tells the game, "Hey, we are currently processing a match, stop listening to clicks for a moment."
let lockBoard = false;
function flipCard(card) {
if (lockBoard) return; // This is the fix!
if (card === this.previousElementSibling || card.classList.contains('matched')) return;
card.classList.add('flipped');
flippedCards.push(card);
if (flippedCards.length === 2) {
checkMatch();
}
}
Evaluating the Match
Now that we've protected the board from rapid-fire clicking, we can handle the actual comparison. If the symbols match, we leave them flipped and mark them as matched. If they don't, we use setTimeout to flip them back after a short delay so the player can actually see what the second card was.
function checkMatch() {
lockBoard = true;
const [card1, card2] = flippedCards;
if (card1.dataset.symbol === card2.dataset.symbol) {
card1.classList.add('matched');
card2.classList.add('matched');
matchedPairs++;
resetTurn();
if (matchedPairs === symbols.length) alert('You won!');
} else {
setTimeout(() => {
card1.classList.remove('flipped');
card2.classList.remove('flipped');
resetTurn();
}, 1000);
}
}
function resetTurn() {
flippedCards = [];
lockBoard = false;
}
By isolating the resetTurn logic, I ensure that the lockBoard flag and the flippedCards array are always cleaned up together. This prevents the game from getting stuck in a "locked" state forever.
π Practical Task
Implement a Move Counter and Win-Condition Timer
Take the memory game logic from this lesson and add a "Scoreboard" to the top of the page. You need to implement the following:
- Move Counter: Create a variable that increments every time a pair is checked (regardless of whether it was a match or not). Display this number in the HTML.
- Game Timer: Start a timer the moment the first card is clicked. Stop the timer when the final pair is matched.
- Final Result: When the user wins, instead of a simple alert, display a message saying: "You won in [X] moves and [Y] seconds!"
There are no comments for now.