Creating a Memory Card Game with HTML, CSS, and JavaScript
Tags: webdev, javascript, beginners, tutorial
Published: 2023-02-10 19:48:41

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.

{% youtube xWdkt6KSirw %}

## Project Set-Up

Before we jump into anything, check out my [repository on GitHub](https://github.com/javascriptacademy-stash/memory-card-game/) 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.
```html
<!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>
```
## 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.

```css
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;
}
```

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

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

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

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.

```css
.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;
}
```
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.

```css
.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;
}
```
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](https://pattern.monster/). Also center the background image with `background-position`, and make it cover the full card by adding `background-size: cover;`

```css
.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;
}

```

## 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.

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

document.querySelector(".score").textContent = score;
```
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.
```js
fetch("./data/cards.json")
  .then((res) => res.json())
  .then((data) => {
    cards = [...data, ...data];
    shuffleCards();
    generateCards();
  });
```
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](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle). With this simple but effective function, you can ensure that your memory card game is never the same twice!

```js
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;
  }
```

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.

```js
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);
  }
}
```
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.

```js
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();
}
```
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.

```js
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);
}
```

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.

```js
function resetBoard() {
  firstCard = null;
  secondCard = null;
  lockBoard = false;
}
```

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.

```js
function restart() {
  resetBoard();
  shuffleCards();
  score = 0;
  document.querySelector(".score").textContent = score;
  gridContainer.innerHTML = "";
  generateCards();
}
```

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:
* 🍺 Support free education and [buy me a beer](https://ko-fi.com/adamnagy)
* 💬 Join our [community on Discord](https://discord.gg/GuxmcEgAjB)
* 📧 Newsletter [Subscribe here](https://www.getrevue.co/profile/dev_adamnagy)
* 🎥 YouTube [Javascript Academy](https://www.youtube.com/channel/UCicOwFWkuMX0ddFaKCc_E5Q)
* 🐦 Twitter: [@dev_adamnagy](https://twitter.com/dev_adamnagy)
* 📷 Instagram [@javascriptacademy](https://www.instagram.com/javascriptacademy/)