Creating a Memory Card Game with HTML, CSS, and JavaScript

javascriptacademy

Adam Nagy

Posted on February 10, 2023

Creating a Memory Card Game with HTML, CSS, and JavaScript

In this tutorial, we will learn how to build a beginner-friendly memory card game using HTML, CSS, and JavaScript. The game will have a grid of cards, where each card will have an image and a score system to keep track of the player's progress. The game will be restarted when all cards have been matched.

Video tutorial

If you would prefer to watch a beginner-friendly step-by-step video tutorial instead, here is the video tutorial that I made.

Project Set-Up

Before we jump into anything, check out my repository on GitHub for this project, where I prepared an assets folder with the fruit icons that we'll use for the cards, and also created a json file with all the card data that we'll need to implement this game.

To start, let's create a new directory for our project and set up the file structure. In your code editor, create a new folder called "memory-card-game". Inside the folder, create three files: index.html, style.css, and script.js.

HTML Structure

The HTML structure for our memory card game will consist of a main container for the cards and a restart button. Don't forget to add the link tag for the stylesheet in the head tag. This is a really simple HTML structure as we will create all the cards dynamically in javascript.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Memory Card Game</title>
    <link rel="stylesheet" type="text/css" href="style.css">
  </head>
  <body>
    <h1>Memory Cards</h1>
    <div class="grid-container">
    </div>
    <p>Score: <span class="score"></span></p>
    <div class="actions">
        <button onclick="restart()">Restart</button>
    </div>
    <script src="index.js"></script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

CSS Styles

In css we'll start with some general styling. The body tag should be as big as the viewport so I'll add a minimum of 100% width and height for the viewport.We'll set a dark background and a white color for the texts that appear on the page.

body {
  min-height: 100vh;
  min-width: 100vw;
  background-color: #12181f;
  color: white;
}

h1 {
    text-align: center;
    font-weight: 700;
    font-size: 50px;
}

p {
    text-align: center;
    font-size: 30px;
    font-weight: bold;
}
Enter fullscreen mode Exit fullscreen mode

The action area will be really simple, we'll center the .actions container using flexbox, then apply some basic nice styles to the button.

.actions {
    display: flex;
    justify-content: center;
}

.actions button {
    padding: 8px 16px;
    font-size: 30px;
    border-radius: 10px;
    background-color: #27ae60;
    color: white;
}
Enter fullscreen mode Exit fullscreen mode

For the card container I'll use a centered CSS grid with 16px of grid-gap. I'll create 6 140px wide coulmns and 3 rows. I'll calculate the height of the rows by dividing the width with two and multiply it by 3. I do this to have the perfect aspect ratio of 2 by 3 (which is the standard playing card aspect ratio) for the cards. For the cards I'll apply the same dimensions and add a little bit of border-radius. The important part is that we need to set position: relative, so we can later position the back fo the card absolutely. I'll apply a transform style of preserve-3d and apply a little eased transition so our flipping animations will be really smooth.

.grid-container {
  display: grid;
  justify-content: center;
  grid-gap: 16px;
  grid-template-columns: repeat(6, 140px);
  grid-template-rows: repeat(2, calc(140px / 2 * 3));
}

.card {
  height: calc(140px / 2 * 3);
  width: 140px;
  border-radius: 10px;
  background-color: white;
  position: relative;
  transform-style: preserve-3d;
  transition: all 0.5s ease-in-out;
}
Enter fullscreen mode Exit fullscreen mode

For the fruit icons I'll use a fix width of 60px by 60px. Then I'll create a css rule to rotates (flips) the card by 180 degrees when the .flipped class is applied to the card. I'll center the front face layout of the card both vertically and horizontally using flexbox. Will center everything on both the back and front using absolute positioning. It is also important to set backface-visibility: hidden;, because otherwise the back-face content of the card would still be visible when the card is flipped.

.front-image {
  width: 60px;
  height: 60px;
}

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

.front, .back {
    backface-visibility: hidden;
    position: absolute;
    border-radius: 10px;
    top: 0;
    left: 0;
    height: 100%;
    width: 100%;
}

.card .front {
  display: flex;
  justify-content: center;
  align-items: center;
}
Enter fullscreen mode Exit fullscreen mode

For the back of the card I'll use and SVG pattern, feel free to use the one you like the most from Pattern Monster. Also center the background image with background-position, and make it cover the full card by adding background-size: cover;

.card .back {
  background-image: url("data:image/svg+xml,<svg id='patternId' width='100%' height='100%' xmlns='http://www.w3.org/2000/svg'><defs><pattern id='a' patternUnits='userSpaceOnUse' width='25' height='25' patternTransform='scale(2) rotate(0)'><rect x='0' y='0' width='100%' height='100%' fill='hsla(0,0%,100%,1)'/><path d='M25 30a5 5 0 110-10 5 5 0 010 10zm0-25a5 5 0 110-10 5 5 0 010 10zM0 30a5 5 0 110-10 5 5 0 010 10zM0 5A5 5 0 110-5 5 5 0 010 5zm12.5 12.5a5 5 0 110-10 5 5 0 010 10z'  stroke-width='1' stroke='none' fill='hsla(174, 100%, 29%, 1)'/><path d='M0 15a2.5 2.5 0 110-5 2.5 2.5 0 010 5zm25 0a2.5 2.5 0 110-5 2.5 2.5 0 010 5zM12.5 2.5a2.5 2.5 0 110-5 2.5 2.5 0 010 5zm0 25a2.5 2.5 0 110-5 2.5 2.5 0 010 5z'  stroke-width='1' stroke='none' fill='hsla(187, 100%, 42%, 1)'/></pattern></defs><rect width='800%' height='800%' transform='translate(0,0)' fill='url(%23a)'/></svg>");
  background-position: center center;
  background-size: cover;
}

Enter fullscreen mode Exit fullscreen mode

Javascript implementation

Let's make this game interactive! We'll start by saving a reference to our grid container which will hold the cards using querySelector. Then we will create the global variables that we will use throughout the game. The cards variable will be an array holding all of our cards, we will have two card variables that will be used for comparison. The lockboard variable will be responsible for locking the board up while the comparison and the animations runs. Also we will keep track of the user's score and initialise it with zero.

const gridContainer = document.querySelector(".grid-container");
let cards = [];
let firstCard, secondCard;
let lockBoard = false;
let score = 0;

document.querySelector(".score").textContent = score;
Enter fullscreen mode Exit fullscreen mode

We use the fetch method to get the card information from the JSON file located at "./data/cards.json". This returns a promise that we convert into JSON using "res.json()".
Finally, we create a new array of cards by spreading the JSON data twice (as we need 2 of each card to be able to find duplicates) and shuffling it with the "shuffleCards" function. And voila! The cards are generated on the page with the help of the "generateCards" function.

fetch("./data/cards.json")
  .then((res) => res.json())
  .then((data) => {
    cards = [...data, ...data];
    shuffleCards();
    generateCards();
  });
Enter fullscreen mode Exit fullscreen mode

Now let's write the function that shuffles the card data so that the game is different every time it's played. It's an essential part of making your memory card game fun and unpredictable.

The function starts by setting the "currentIndex" to the length of the "cards" array. This allows us to shuffle all the cards in the array. Then, using a "while" loop, we swap the current card with a random card until all cards have been shuffled.

The "randomIndex" is generated using Math.floor and Math.random, which returns a random whole number between 0 and the length of the "cards" array. We store the current card value in a temporary variable, "temporaryValue", and then swap it with the random card.

This process continues until the "currentIndex" reaches 0, at which point all the cards will have been shuffled. This is called the 9Fisher-Yates algorithm. With this simple but effective function, you can ensure that your memory card game is never the same twice!

function shuffleCards() {
  let currentIndex = cards.length,
    randomIndex,
    temporaryValue;
  while (currentIndex !== 0) {
    randomIndex = Math.floor(Math.random() * currentIndex);
    currentIndex -= 1;
    temporaryValue = cards[currentIndex];
    cards[currentIndex] = cards[randomIndex];
    cards[randomIndex] = temporaryValue;
  }
Enter fullscreen mode Exit fullscreen mode

Let's write a function that will generate the cards for us.The function uses a for loop to iterate over the cards array and create a div element for each card. The cardElement is then given the class card and a data-name attribute with the card's name. We will use this data attribute for the comparsion. The innerHTML property is used to define the front and back of the card, including the card's image.

Finally, the cardElement is appended to the gridContainer and an event listener is added to listen for a click event. When the card is clicked, the flipCard function is called, allowing the player to flip the card and reveal its image.

function generateCards() {
  for (let card of cards) {
    const cardElement = document.createElement("div");
    cardElement.classList.add("card");
    cardElement.setAttribute("data-name", card.name);
    cardElement.innerHTML = `
      <div class="front">
        <img class="front-image" src=${card.image} />
      </div>
      <div class="back"></div>
    `;
    gridContainer.appendChild(cardElement);
    cardElement.addEventListener("click", flipCard);
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we'll write the flipcard function. The function starts by checking if the "lockBoard" variable is true. If it is, the function returns, preventing any cards from being flipped. If the player clicks on the same card twice, the function also returns, avoiding any unwanted behavior.

The card that was clicked is then given the class "flipped", which causes it to flip over and reveal its image. If this is the first card being flipped, the "firstCard" variable is set to the card that was clicked. If a second card is being flipped, the "secondCard" variable is set, and the score is increased by one. The score is displayed on the page by updating the text content of the ".score" element.

Finally, the "lockBoard" variable is set to true, preventing any additional cards from being flipped. The "checkForMatch" function is then called to see if the two flipped cards match.

function flipCard() {
  if (lockBoard) return;
  if (this === firstCard) return;

  this.classList.add("flipped");

  if (!firstCard) {
    firstCard = this;
    return;
  }

  secondCard = this;
  score++;
  document.querySelector(".score").textContent = score;
  lockBoard = true;

  checkForMatch();
}
Enter fullscreen mode Exit fullscreen mode

In checkForMatch the first line, let isMatch = firstCard.dataset.name === secondCard.dataset.name; compares the data-name attribute of the first and second cards to see if they match. If they do match, the isMatch variable is set to true.

Next, the isMatch ? disableCards() : unflipCards(); line uses a ternary operator to call either the "disableCards" or unflipCards function, depending on the result of the comparison.

The disableCards function removes the click event listener from the first and second cards, effectively disabling them. The resetBoard function is then called to reset the game for the next round.

The unflipCards function uses the setTimeout function to wait for 1 second before removing the flipped class from the first and second cards. This causes the cards to flip back over, allowing the player to try again. The resetBoard function is then called to reset the game for the next round.

function checkForMatch() {
  let isMatch = firstCard.dataset.name === secondCard.dataset.name;

  isMatch ? disableCards() : unflipCards();
}

function disableCards() {
  firstCard.removeEventListener("click", flipCard);
  secondCard.removeEventListener("click", flipCard);

  resetBoard();
}

function unflipCards() {
  setTimeout(() => {
    firstCard.classList.remove("flipped");
    secondCard.classList.remove("flipped");
    resetBoard();
  }, 1000);
}
Enter fullscreen mode Exit fullscreen mode

This resetBoard function is called to reset the state of the game after each match attempt. It sets the values of firstCard, secondCard, and lockBoard back to their default state, allowing for a new round to begin.

function resetBoard() {
  firstCard = null;
  secondCard = null;
  lockBoard = false;
}
Enter fullscreen mode Exit fullscreen mode

The restart function is used to start the game from scratch. It calls the resetBoard function, shuffles the cards, resets the score back to 0, clears the grid container and generates new cards for the game. This function makes it easy to play the game multiple times without having to refresh the page.

function restart() {
  resetBoard();
  shuffleCards();
  score = 0;
  document.querySelector(".score").textContent = score;
  gridContainer.innerHTML = "";
  generateCards();
}
Enter fullscreen mode Exit fullscreen mode

That is all you need to create a memory card game! Feel free to implement new features to it, like a scoreboard, and make it responsive!

Where can you learn more from me?

I create education content covering web-development on several platforms, feel free to πŸ‘€ check them out.

I also create a newsletter where I share the week's or 2 week's educational content that I created. No bullπŸ’© just educational content.

πŸ”— Links:

πŸ’– πŸ’ͺ πŸ™… 🚩
javascriptacademy
Adam Nagy

Posted on February 10, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related