Building a JavaScript-Powered Interactive Simple Web-Based Memory Game: A Beginner’s Guide

Memory games, also known as concentration or pairs, are a classic for a reason. They’re fun, easy to learn, and provide a great way to challenge your memory. But have you ever thought about building one yourself? In this tutorial, we’ll dive into creating a simple, yet engaging, memory game using JavaScript. This project is perfect for beginners to intermediate developers looking to solidify their JavaScript skills, learn about event handling, DOM manipulation, and basic game logic. By the end, you’ll have a fully functional memory game you can play and even customize.

Why Build a Memory Game?

Building a memory game offers a fantastic opportunity to practice fundamental JavaScript concepts. You’ll gain hands-on experience with:

  • DOM Manipulation: Changing the content, style, and structure of your HTML elements.
  • Event Handling: Responding to user interactions like clicks.
  • Arrays and Data Structures: Storing and manipulating game data (like card images and their positions).
  • Control Flow: Using if/else statements, loops, and functions to manage game logic.
  • Basic Game Logic: Implementing rules for matching pairs, tracking scores, and determining game over.

Beyond the technical skills, it’s a fun project that provides immediate visual feedback. You see the results of your code directly, making it an incredibly rewarding learning experience.

Setting Up Your Project

Before we start coding, let’s set up the basic structure of our project. We’ll need three files:

  • index.html: This file will contain the HTML structure of our game (the cards, the game board, and any score displays).
  • style.css: This file will hold the CSS styling to make our game visually appealing.
  • script.js: This file will contain the JavaScript code that brings our game to life.

Create these three files in a new folder on your computer. In your index.html file, add the basic HTML structure:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Memory Game</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <h1>Memory Game</h1>
        <div class="game-board">
            <!-- Cards will be added here dynamically -->
        </div>
        <div class="score-board">
            <p>Matches: <span id="matches">0</span></p>
            <p>Attempts: <span id="attempts">0</span></p>
        </div>
    </div>
    <script src="script.js"></script>
</body>
</html>

This HTML provides the basic structure: a title, a container to hold everything, a game board where the cards will be displayed, and a score board to track matches and attempts. Note the <script> tag linking to our script.js file at the end of the <body>. This is best practice to ensure the HTML elements are loaded before the script tries to manipulate them.

Styling the Game with CSS

Next, let’s add some basic styling to our style.css file to make the game visually appealing. Here’s a starting point:

body {
    font-family: sans-serif;
    background-color: #f0f0f0;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    margin: 0;
}

.container {
    background-color: #fff;
    padding: 20px;
    border-radius: 8px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
    text-align: center;
}

.game-board {
    display: grid;
    grid-template-columns: repeat(4, 100px);
    gap: 10px;
    margin-top: 20px;
    justify-content: center;
}

.card {
    width: 100px;
    height: 100px;
    border: 1px solid #ccc;
    border-radius: 4px;
    cursor: pointer;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 2em;
    background-color: #ddd;
}

.card.flipped {
    background-color: #fff;
    border-color: #007bff;
}

.score-board {
    margin-top: 20px;
}

This CSS sets up the basic layout. It centers the game on the page, defines the grid layout for the cards, and provides some styling for the cards themselves, including a “flipped” state. You can customize the colors, fonts, and card sizes to your liking.

Building the JavaScript Logic (script.js)

Now, let’s write the JavaScript code in script.js to make our memory game interactive. This is where the core game logic resides.

1. Defining the Cards and Game State

First, we need to define our cards. We’ll use an array of objects, where each object represents a card. Each card will have an id, image, and potentially a ‘matched’ status.

const cards = [
    { id: 1, image: "image1.png", matched: false },
    { id: 1, image: "image1.png", matched: false },
    { id: 2, image: "image2.png", matched: false },
    { id: 2, image: "image2.png", matched: false },
    { id: 3, image: "image3.png", matched: false },
    { id: 3, image: "image3.png", matched: false },
    { id: 4, image: "image4.png", matched: false },
    { id: 4, image: "image4.png", matched: false },
    { id: 5, image: "image5.png", matched: false },
    { id: 5, image: "image5.png", matched: false },
    { id: 6, image: "image6.png", matched: false },
    { id: 6, image: "image6.png", matched: false },
    { id: 7, image: "image7.png", matched: false },
    { id: 7, image: "image7.png", matched: false },
    { id: 8, image: "image8.png", matched: false },
    { id: 8, image: "image8.png", matched: false }
];

let flippedCards = [];
let matches = 0;
let attempts = 0;

const gameBoard = document.querySelector(".game-board");
const matchesDisplay = document.getElementById("matches");
const attemptsDisplay = document.getElementById("attempts");

In this code:

  • cards: This array holds the card data. Notice that we have pairs of cards with the same `id` and `image`. The `matched` property will track whether a card has been successfully matched. You’ll need to replace “image1.png”, “image2.png”, etc., with the actual file names or URLs of your card images.
  • flippedCards: This array stores the cards that are currently flipped over. We’ll use this to check for matches.
  • matches and attempts: These variables keep track of the player’s progress.
  • We get references to the HTML elements we’ll be manipulating.

2. Shuffle the Cards

Before displaying the cards, we need to shuffle them so they appear in a random order. Here’s a simple shuffle function:

function shuffleArray(array) {
    for (let i = array.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [array[i], array[j]] = [array[j], array[i]]; // Swap elements
    }
}

shuffleArray(cards);

This function uses the Fisher-Yates shuffle algorithm, a common and effective way to randomize an array.

3. Create the Card Elements and Display Them

Now, let’s create the HTML elements for each card and add them to the game board. We’ll loop through the `cards` array and dynamically create `<div>` elements for each card. We’ll also add event listeners to handle card clicks.

function createCardElement(card) {
    const cardElement = document.createElement("div");
    cardElement.classList.add("card");
    cardElement.dataset.id = card.id;
    cardElement.dataset.image = card.image;

    // Initially, the card shows a back (e.g., a question mark or a generic image)
    cardElement.innerHTML = "?"; // Or <img src="back.png" alt="Back of card">

    cardElement.addEventListener("click", flipCard);
    return cardElement;
}

function displayCards() {
    cards.forEach(card => {
        const cardElement = createCardElement(card);
        gameBoard.appendChild(cardElement);
    });
}

displayCards();

In this code:

  • createCardElement() creates a `<div>` element for each card, adds the “card” class for styling, and sets `data-` attributes to store the card’s `id` and `image`. The initial content of the card is a question mark (or any image you want to use as the card back). It also adds a click event listener to the card.
  • displayCards() loops through the `cards` array, calls `createCardElement()` for each card, and appends the resulting element to the `gameBoard`.

4. Flip the Cards

The flipCard function handles what happens when a card is clicked:

function flipCard() {
    if (flippedCards.length < 2 && !this.classList.contains("flipped")) {
        const cardId = this.dataset.id;
        const cardImage = this.dataset.image;

        this.innerHTML = "<img src="" + cardImage + "" alt="Card Image">"; // Or the image directly
        this.classList.add("flipped");
        flippedCards.push({ element: this, id: cardId, image: cardImage });

        if (flippedCards.length === 2) {
            attempts++;
            attemptsDisplay.textContent = attempts;
            checkForMatch();
        }
    }
}

This function does the following:

  • Checks if the card can be flipped: It ensures that no more than two cards are flipped at a time and that the card hasn’t already been flipped.
  • Gets the card’s ID and image: It retrieves the `id` and `image` from the `data-` attributes.
  • Displays the card’s content: It changes the card’s content to display the image.
  • Adds the “flipped” class: This class is used to visually indicate that the card is flipped.
  • Adds the card to the `flippedCards` array: This array keeps track of the flipped cards.
  • If two cards are flipped, checks for a match: Calls the checkForMatch() function.

5. Check for Matches

The checkForMatch function determines if the two flipped cards match:

function checkForMatch() {
    const [card1, card2] = flippedCards;

    if (card1.id === card2.id) {
        // Match!
        matches++;
        matchesDisplay.textContent = matches;

        // Mark matched cards
        card1.element.dataset.matched = true;
        card2.element.dataset.matched = true;

        // Clear flippedCards
        flippedCards = [];

        // Check if the game is over
        if (matches === cards.length / 2) {
            gameOver();
        }

    } else {
        // No match
        setTimeout(() => {
            card1.element.innerHTML = "?"; // Or back image
            card2.element.innerHTML = "?"; // Or back image
            card1.element.classList.remove("flipped");
            card2.element.classList.remove("flipped");
            flippedCards = [];
        }, 1000); // Flip cards back after 1 second
    }
}

This function:

  • Gets the two flipped cards: It retrieves the two cards from the flippedCards array.
  • Checks if the cards match: It compares the id of the two cards.
  • If there’s a match:
    • Increments the `matches` count.
    • Updates the `matchesDisplay`.
    • Marks the cards as matched by adding a ‘matched’ data attribute.
    • Clears the flippedCards array.
    • Calls the gameOver() function if all pairs have been matched.
  • If there’s no match:
    • Uses setTimeout() to flip the cards back after a short delay (e.g., 1 second) so the player can see them.
    • Resets the card content to the question mark (or back image).
    • Removes the “flipped” class.
    • Clears the flippedCards array.

6. Game Over

The gameOver function is called when all card pairs have been matched. It displays a message congratulating the player and optionally provides a way to restart the game.

function gameOver() {
    alert(`Congratulations! You matched all the cards in ${attempts} attempts!`);
    // Optionally, add a restart button here
}

You can expand this function to include a more sophisticated game-over screen, a score display, and a restart button.

Putting it All Together

Here’s the complete code for script.js:

const cards = [
    { id: 1, image: "image1.png", matched: false },
    { id: 1, image: "image1.png", matched: false },
    { id: 2, image: "image2.png", matched: false },
    { id: 2, image: "image2.png", matched: false },
    { id: 3, image: "image3.png", matched: false },
    { id: 3, image: "image3.png", matched: false },
    { id: 4, image: "image4.png", matched: false },
    { id: 4, image: "image4.png", matched: false },
    { id: 5, image: "image5.png", matched: false },
    { id: 5, image: "image5.png", matched: false },
    { id: 6, image: "image6.png", matched: false },
    { id: 6, image: "image6.png", matched: false },
    { id: 7, image: "image7.png", matched: false },
    { id: 7, image: "image7.png", matched: false },
    { id: 8, image: "image8.png", matched: false },
    { id: 8, image: "image8.png", matched: false }
];

let flippedCards = [];
let matches = 0;
let attempts = 0;

const gameBoard = document.querySelector(".game-board");
const matchesDisplay = document.getElementById("matches");
const attemptsDisplay = document.getElementById("attempts");

function shuffleArray(array) {
    for (let i = array.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [array[i], array[j]] = [array[j], array[i]]; // Swap elements
    }
}

function createCardElement(card) {
    const cardElement = document.createElement("div");
    cardElement.classList.add("card");
    cardElement.dataset.id = card.id;
    cardElement.dataset.image = card.image;

    // Initially, the card shows a back (e.g., a question mark or a generic image)
    cardElement.innerHTML = "?"; // Or <img src="back.png" alt="Back of card">

    cardElement.addEventListener("click", flipCard);
    return cardElement;
}

function displayCards() {
    cards.forEach(card => {
        const cardElement = createCardElement(card);
        gameBoard.appendChild(cardElement);
    });
}

function flipCard() {
    if (flippedCards.length < 2 && !this.classList.contains("flipped")) {
        const cardId = this.dataset.id;
        const cardImage = this.dataset.image;

        this.innerHTML = "<img src="" + cardImage + "" alt="Card Image">"; // Or the image directly
        this.classList.add("flipped");
        flippedCards.push({ element: this, id: cardId, image: cardImage });

        if (flippedCards.length === 2) {
            attempts++;
            attemptsDisplay.textContent = attempts;
            checkForMatch();
        }
    }
}

function checkForMatch() {
    const [card1, card2] = flippedCards;

    if (card1.id === card2.id) {
        // Match!
        matches++;
        matchesDisplay.textContent = matches;

        // Mark matched cards
        card1.element.dataset.matched = true;
        card2.element.dataset.matched = true;

        // Clear flippedCards
        flippedCards = [];

        // Check if the game is over
        if (matches === cards.length / 2) {
            gameOver();
        }

    } else {
        // No match
        setTimeout(() => {
            card1.element.innerHTML = "?"; // Or back image
            card2.element.innerHTML = "?"; // Or back image
            card1.element.classList.remove("flipped");
            card2.element.classList.remove("flipped");
            flippedCards = [];
        }, 1000); // Flip cards back after 1 second
    }
}

function gameOver() {
    alert(`Congratulations! You matched all the cards in ${attempts} attempts!`);
    // Optionally, add a restart button here
}

shuffleArray(cards);
displayCards();

Remember to replace the placeholder image file names (e.g., “image1.png”) with your actual image file names or URLs. You can also customize the number of cards, the card images, and the styling to create a unique memory game.

Common Mistakes and How to Fix Them

Here are some common mistakes beginners encounter when building this type of game, along with solutions:

  • Incorrect Image Paths: The most common issue is that the image paths in your JavaScript (e.g., "image1.png") are incorrect. Make sure the paths are relative to your HTML file, or use absolute URLs. Use your browser’s developer tools (right-click, “Inspect”) to check for broken image links in the console.
  • Event Listener Issues: Make sure your event listeners are correctly attached to the card elements. Double-check the spelling of the event (e.g., “click”) and that the function you’re calling (e.g., flipCard) is defined correctly.
  • Incorrect Logic for Checking Matches: Carefully review your checkForMatch() function. Ensure you’re comparing the correct properties of the cards (e.g., card1.id === card2.id). Use console.log() statements to debug and see the values of the cards being compared.
  • Unclear Game State: It can be easy to lose track of which cards are flipped, matched, and which are waiting to be matched. Use console.log() statements to monitor the values of flippedCards, matches, and attempts throughout the game.
  • Asynchronous Behavior Issues: The use of setTimeout() to flip the cards back after a mismatch can sometimes lead to unexpected behavior. Ensure you’re handling the timing correctly and that your logic is robust enough to handle the delay.

Key Takeaways

This tutorial has walked you through the process of building a simple memory game using JavaScript. You’ve learned about:

  • HTML Structure: Setting up the basic layout of your game using HTML elements.
  • CSS Styling: Styling the game to make it visually appealing.
  • JavaScript Logic: Implementing the core game logic, including shuffling cards, handling clicks, checking for matches, and managing the game state.
  • Event Handling: Responding to user interactions.
  • DOM Manipulation: Dynamically creating and modifying HTML elements.

By building this game, you’ve gained practical experience with fundamental JavaScript concepts, which will be invaluable as you continue your journey in web development.

FAQ

Here are some frequently asked questions about building a memory game:

  1. Can I use different types of cards? Yes! You can use images, text, or any other content you want to display on the cards. Just modify the HTML within the createCardElement() function accordingly.
  2. How can I add a score? You can easily add a score counter to track the player’s progress. Increment the score each time a match is found and display it using JavaScript.
  3. How do I add a timer? You can use setInterval() to create a timer that counts down or up. Display the timer value in the game interface.
  4. How can I make the game responsive? Use CSS media queries to adjust the game’s layout and styling for different screen sizes.
  5. How do I add sound effects? You can use the HTML <audio> element or the JavaScript Audio API to play sound effects when cards are flipped or matched.

This memory game project provides a solid foundation for further exploration. Feel free to experiment with different card designs, add more features, and customize the game to your liking. Building this game is a fantastic step in your development journey.

The beauty of this project lies not just in the final product but in the journey of building it. You’ve now experienced the power of JavaScript in creating interactive and engaging experiences. Continue to experiment, explore, and most importantly, have fun coding. With each project, you’ll gain a deeper understanding of the language and a greater appreciation for the art of web development.