Building a Simple Vue.js Interactive Memory Game: A Beginner’s Guide

Written by

in

In the world of web development, creating engaging and interactive user experiences is paramount. One effective way to learn and master a framework like Vue.js is by building small, practical projects. These projects allow you to apply the concepts you learn and solidify your understanding through hands-on experience. This article will guide you through building a classic Memory Game using Vue.js. This project is perfect for beginners, offering a fun and accessible way to grasp core Vue.js principles like component creation, data binding, event handling, and conditional rendering.

Why Build a Memory Game?

The Memory Game, also known as Pairs or Concentration, is an ideal project for several reasons:

  • It’s Beginner-Friendly: The core logic is straightforward, making it easy to understand and implement, even if you’re new to Vue.js.
  • It Reinforces Key Concepts: You’ll practice using components, data, event listeners, and conditional rendering. These are fundamental to building any Vue.js application.
  • It’s Interactive and Engaging: Users enjoy playing the game, making the learning process more enjoyable.
  • It’s a Good Portfolio Piece: A working Memory Game is a tangible demonstration of your Vue.js skills.

By building this game, you’ll gain valuable experience and a solid foundation for more complex Vue.js projects. Let’s get started!

Setting Up Your Vue.js Project

Before diving into the code, you’ll need a basic Vue.js project set up. We’ll use the Vue CLI (Command Line Interface) for this, which streamlines the project creation process. If you don’t have the Vue CLI installed, you’ll need to install Node.js and npm (Node Package Manager) first. Then, open your terminal and run:

npm install -g @vue/cli

Once the Vue CLI is installed, create a new project:

vue create memory-game-vue

You’ll be prompted to select a preset. Choose the “default” preset (Babel, ESLint) or manually select features if you prefer. Navigate into your project directory:

cd memory-game-vue

Now, you have a basic Vue.js project ready to go. You can run it with:

npm run serve

This will start a development server, and you can view your project in your browser (usually at http://localhost:8080/).

Project Structure and Core Components

Let’s plan out the structure of our Memory Game. We’ll break it down into several components:

  • App.vue: The main component that serves as the entry point for our application. It will contain the overall game layout and state.
  • Card.vue: Represents an individual card. It will handle the display of the card (face-up or face-down) and the click event.
  • Gameboard.vue: This component will manage the game logic, including card shuffling, matching, and game state.

We’ll start by creating these components within the src/components directory of your project.

Creating the Card Component (Card.vue)

Create a file named Card.vue in your src/components directory. This component will be responsible for rendering each individual card. Here’s the basic structure:

<template>
 <div class="card" :class="{ 'flipped': isFlipped, 'matched': isMatched }" @click="flipCard">
 <div class="card-inner">
 <div class="card-front">
 ?
 </div>
 <div class="card-back">
 <img :src="imageSrc" alt="Card Image">
 </div>
 </div>
 </div>
</template>

<script>
 export default {
 name: 'Card',
 props: {
 cardData: {
 type: Object,
 required: true
 },
 },
 data() {
 return {
 isFlipped: false,
 isMatched: false
 };
 },
 computed: {
 imageSrc() {
 return this.cardData.image;
 }
 },
 methods: {
 flipCard() {
 if (!this.isFlipped && !this.isMatched) {
 this.isFlipped = true;
 this.$emit('card-flipped', this.cardData.id, this);
 }
 }
 }
 };
</script>

<style scoped>
 .card {
 width: 100px;
 height: 100px;
 perspective: 1000px;
 margin: 5px;
 }

 .card-inner {
 position: relative;
 width: 100%;
 height: 100%;
 transition: transform 0.8s;
 transform-style: preserve-3d;
 }

 .card.flipped .card-inner {
 transform: rotateY(180deg);
 }

 .card-front, .card-back {
 position: absolute;
 width: 100%;
 height: 100%;
 backface-visibility: hidden;
 border-radius: 5px;
 }

 .card-front {
 background-color: #ccc;
 display: flex;
 justify-content: center;
 align-items: center;
 font-size: 2em;
 }

 .card-back {
 background-color: #fff;
 transform: rotateY(180deg);
 }

 .card-back img {
 width: 100%;
 height: 100%;
 object-fit: cover;
 border-radius: 5px;
 }

 .card.matched {
 opacity: 0.5;
 }
</style>

Let’s break down the code:

  • Template: Defines the structure of the card. It includes a div with the class card, which contains the front and back of the card. The :class directive dynamically applies classes based on the component’s data (flipped and matched). The @click="flipCard" binds the flipCard method to the click event.
  • Script: Contains the component’s logic.
    • props: Defines the cardData prop, which is an object containing information about the card (e.g., image source, unique identifier).
    • data: Defines the component’s data: isFlipped (whether the card is face-up) and isMatched (whether the card has been matched).
    • computed: A computed property imageSrc to get the image source from the cardData.
    • methods: The flipCard method handles the card flipping. It checks if the card is already flipped or matched. If not, it sets isFlipped to true. It also emits a custom event card-flipped with the card’s id and the component instance. This event will be listened to by the parent component (Gameboard.vue).
  • Style: Contains the CSS styles for the card, including its size, appearance, and animations.

Creating the Gameboard Component (Gameboard.vue)

Now, let’s create the Gameboard.vue component. This is where the core game logic will reside. Create a file named Gameboard.vue in your src/components directory.

<template>
 <div class="gameboard">
 <div class="card-grid">
 <Card
 v-for="card in cards"
 :key="card.id"
 :cardData="card"
 @card-flipped="handleCardFlip"
 ></Card
 >
 </div>
 <p v-if="gameOver">Game Over! You won in {{ moves }} moves.</p>
 <button v-if="gameOver" @click="resetGame">Play Again</button>
 <p>Moves: {{ moves }}</p>
 </div>
</template>

<script>
 import Card from './Card.vue';

 export default {
 name: 'Gameboard',
 components: {
 Card
 },
 data() {
 return {
 cards: [],
 flippedCards: [],
 matchedCards: [],
 gameOver: false,
 moves: 0,
 cardImages: [
 { id: 1, image: require('@/assets/image1.jpg') },
 { id: 2, image: require('@/assets/image2.jpg') },
 { id: 3, image: require('@/assets/image3.jpg') },
 { id: 4, image: require('@/assets/image4.jpg') },
 { id: 5, image: require('@/assets/image5.jpg') },
 { id: 6, image: require('@/assets/image6.jpg') }
 ],
 };
 },
 mounted() {
 this.initializeGame();
 },
 methods: {
 initializeGame() {
 this.gameOver = false;
 this.moves = 0;
 this.flippedCards = [];
 this.matchedCards = [];
 const duplicatedCards = [...this.cardImages, ...this.cardImages];
 this.cards = this.shuffleArray(duplicatedCards.map((card, index) => ({
 ...card,
 id: index,
 })));
 },
 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]];
 }
 return array;
 },
 handleCardFlip(cardId, cardComponent) {
 if (this.flippedCards.length < 2 && !this.matchedCards.includes(cardId)) {
 this.flippedCards.push({ cardId, cardComponent });

 if (this.flippedCards.length === 2) {
 this.moves++;
 this.checkForMatch();
 }
 }
 },
 checkForMatch() {
 const [card1, card2] = this.flippedCards;
 const card1Data = this.cards.find(card => card.id === card1.cardId);
 const card2Data = this.cards.find(card => card.id === card2.cardId);

 if (card1Data && card2Data && card1Data.image === card2Data.image) {
 this.matchedCards.push(card1.cardId, card2.cardId);
 this.flippedCards = [];
 this.checkGameOver();
 } else {
 setTimeout(() => {
 card1.cardComponent.isFlipped = false;
 card2.cardComponent.isFlipped = false;
 this.flippedCards = [];
 }, 1000);
 }
 },
 checkGameOver() {
 if (this.matchedCards.length === this.cards.length) {
 this.gameOver = true;
 }
 },
 resetGame() {
 this.initializeGame();
 }
 }
 };
</script>

<style scoped>
 .gameboard {
 display: flex;
 flex-direction: column;
 align-items: center;
 }

 .card-grid {
 display: grid;
 grid-template-columns: repeat(4, 1fr);
 gap: 10px;
 margin-bottom: 20px;
 }

 @media (max-width: 600px) {
 .card-grid {
 grid-template-columns: repeat(2, 1fr);
 }
 }
</style>

Let’s break down the code:

  • Template:
    • Uses a <div> with the class card-grid to display the cards in a grid layout.
    • Uses a v-for loop to iterate through the cards data array and render a Card component for each card.
    • Passes the cardData to each Card component using the :cardData prop.
    • Listens for the card-flipped event from the Card component and calls the handleCardFlip method.
    • Displays a “Game Over” message and a “Play Again” button when the game is over.
    • Displays the number of moves.
  • Script:
    • components: Imports and registers the Card component.
    • data:
      • cards: An array that stores the card data.
      • flippedCards: An array to hold the currently flipped cards (up to two).
      • matchedCards: An array to store the IDs of matched cards.
      • gameOver: A boolean to indicate whether the game is over.
      • moves: A counter for the number of moves the player has made.
      • cardImages: An array of objects, where each object holds an id and the image source.
    • mounted(): Calls the initializeGame method when the component is mounted.
    • methods:
      • initializeGame(): Initializes the game by shuffling the cards and resetting the game state.
      • shuffleArray(array): Shuffles an array using the Fisher-Yates shuffle algorithm.
      • handleCardFlip(cardId, cardComponent): Handles the card-flipped event emitted by the Card component. It adds the flipped card to the flippedCards array. If two cards are flipped, it calls checkForMatch().
      • checkForMatch(): Checks if the two flipped cards match. If they match, it adds them to the matchedCards array and resets the flippedCards array. If they don’t match, it flips the cards back after a delay.
      • checkGameOver(): Checks if all cards have been matched and sets gameOver to true if they have.
      • resetGame(): Resets the game to start a new round.
  • Style: Defines the CSS styles for the game board, including the grid layout for the cards.

Integrating the Components in App.vue

Now, let’s integrate these components into our main application component, App.vue. Open App.vue in your src directory and modify it as follows:

<template>
 <div id="app">
 <h1>Memory Game</h1>
 <Gameboard />
 </div>
</template>

<script>
 import Gameboard from './components/Gameboard.vue';

 export default {
 name: 'App',
 components: {
 Gameboard
 }
 };
</script>

<style>
 #app {
 font-family: Avenir, Helvetica, Arial, sans-serif;
 -webkit-font-smoothing: antialiased;
 -moz-osx-font-smoothing: grayscale;
 text-align: center;
 color: #2c3e50;
 margin-top: 60px;
 }
</style>

Here, we import the Gameboard component and render it within the App.vue template. This sets up the overall structure of our application.

Adding Images

For the card images, you’ll need to add image files to your project. Create an “assets” folder in the src directory. Place six different images (e.g., JPEG, PNG) inside the “assets” folder. You can name them something like image1.jpg, image2.png, etc. Make sure the file paths in the cardImages data array in Gameboard.vue match the names and locations of your image files.

Testing and Debugging

After implementing these components and their logic, test your game thoroughly. Use your browser’s developer tools to check for errors and to inspect the component’s state and props. Make sure cards flip correctly, matches are detected, and the game resets as expected. Common issues include:

  • Incorrect File Paths: Double-check the image file paths in Gameboard.vue.
  • Event Handling Issues: Verify that the @click event is correctly bound to the flipCard method and that the card-flipped event is emitted and handled properly.
  • Logic Errors: Carefully review the logic in checkForMatch and handleCardFlip to ensure matches are detected correctly and cards are flipped back when they don’t match.
  • CSS Issues: Ensure the CSS is applied correctly and the cards are displayed as intended.

Common Mistakes and How to Fix Them

When building the Memory Game, beginners often encounter these common mistakes:

  • Incorrect Data Binding: Ensure you are correctly using the : (v-bind) directive to bind data to attributes in your template (e.g., :src="imageSrc", :class="{ 'flipped': isFlipped }").
  • Scope Issues: Make sure you understand how component scopes work. Data defined in one component is not automatically available in another. You need to pass data as props or use events to communicate between components.
  • Incorrect Event Handling: Double-check that you’re using the @ (v-on) directive to listen for events (e.g., @click="flipCard") and that your event handlers are correctly defined in the methods section of your component.
  • Missing or Incorrect Imports: Ensure you are correctly importing components using the import statement (e.g., import Card from './Card.vue').
  • CSS Conflicts: Be aware of CSS specificity and potential conflicts. Use the scoped attribute in your style tags to limit the scope of your CSS to the current component.

Debugging these issues often involves using the browser’s developer tools (right-click, then “Inspect”) to examine the component’s data, props, and emitted events. Console logging can also be very helpful (e.g., console.log(this.cardData)).

Enhancements and Next Steps

Once you have a working Memory Game, consider these enhancements to take your project further:

  • Add Difficulty Levels: Allow the player to choose the number of card pairs (e.g., easy: 4 pairs, medium: 8 pairs, hard: 12 pairs).
  • Implement a Timer: Add a timer to track how long it takes the player to complete the game.
  • Add a Scoreboard: Store and display the player’s scores.
  • Improve the UI: Enhance the visual design with CSS to make the game more appealing.
  • Add Sound Effects: Use the Web Audio API to add sound effects for flipping cards, matching pairs, and winning the game.
  • Use a Different Card Set: Use an API to fetch images for the cards.

Key Takeaways

Building a Memory Game with Vue.js provides an excellent learning experience. You’ve gained practical experience with:

  • Component Composition: Breaking down the game into reusable components (Card, Gameboard, App).
  • Data Binding: Using directives like :class and :src to dynamically update the UI based on data.
  • Event Handling: Handling user interactions (card clicks) and communicating between components using custom events.
  • Conditional Rendering: Using v-if to show/hide elements based on game state.
  • State Management: Managing the game’s state (flipped cards, matched cards, game over) using the component’s data.

By completing this project, you’ve taken a significant step towards mastering Vue.js and building interactive web applications. Remember to experiment, explore, and continue practicing to improve your skills. Happy coding!

The Memory Game serves as a stepping stone. As you build more projects, you’ll naturally become more comfortable with Vue.js, allowing you to tackle increasingly complex challenges. The journey of learning web development is continuous, and each project you complete builds upon the last, solidifying your knowledge and expanding your capabilities. Keep exploring, experimenting, and most importantly, keep building!