How to Create a Memory Game in JavaScript

How to Create a Memory Game in JavaScript

Improve your memory, and your JavaScript
Ferenc Almasi • 2022 February 04 • Read time 17 min read
In this tutorial, we will be looking into how you can recreate a memory game with flipping cards in vanially JavaScript. GitHub link is also provided.

Building a memory game can not only improve your JavaScript knowledge but can also help you improve your memory. In this project, we will be looking at creating a grid of cards, that can be flipped over and matched. If a match is found, the cards stay flipped, if not, they will turn back. The game continues until all cards are flipped.

The entire project is hosted on GitHub if you would like to get the code in one piece. By the end of this tutorial, we will have the following game:

Memory game created in JavaScript

Setting Up the Project

Let’s start off by setting up the project. In a new folder, create the layout for the game, in a new index.html file:

Copied to clipboard! Playground
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>🧠 Memory Game in JavaScript</title>

        <link rel="stylesheet" href="assets/styles.css" />
        <script src="assets/game.js" defer></script>
    </head>
    <body>
        <div class="game">
            <div class="controls">
                <button>Start</button>
                <div class="stats">
                    <div class="moves">0 moves</div>
                    <div class="timer">time: 0 sec</div>
                </div>
            </div>
            <div class="board-container">
                <div class="board" data-dimension="4"></div>
                <div class="win">You won!</div>
            </div>
        </div>
    </body>
</html>
index.html

Everything will go into a .game container. We will show the number of moves (each click will represent one move), and the time it takes to complete the game. There are two other things to note for this project:

  • The .board element has a data-dimension attribute. We will use this to programmatically generate the board so that we can quickly configure the size of it. By default, we will go with 4x4.
  • There are also two assets linked to the document, a stylesheet and the entry point for the game. Make sure you have these files ready in a folder called assets.

Styling the Board

Let’s focus our attention on styling the board. I won’t be covering all CSS rules, you can find the full stylesheet in the GitHub repository, but I will point out some important parts that we need to keep in mind.

For one, I’m using a custom playful font that you can either download from the GitHub repository or get from Google Fonts. To use fonts in CSS, you want to declare a new font face in the following way:

Copied to clipboard!
@font-face {
    font-family: Fredoka;
    src: url(./FredokaOne-Regular.ttf);
}

// Later you can reference it like so:
font-family: Fredoka;
assets/styles.css

We are also dead-centering the board to the screen, this can be done using absolute positioning and transforms:

Copied to clipboard! Playground
.game {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

Lastly, make sure that for the “Start” button, you add the following rules to make it show the correct cursor when it is being hovered:

Copied to clipboard! Playground
button {
    cursor: pointer;
}

.disabled {
    color: #757575;
}

I have also created a .disabled state, as we are going to disable the button later on when we start the game through JavaScript.

Flipping Cards with CSS

The last thing we need to cover from the CSS side is how to flip the cards. First, let’s see what will be generated into the .board to understand what elements we will need to work with:

Copied to clipboard! Playground
<div class="board" data-dimension="4">
    <div class="card">
        <div class="card-front"></div>
	<div class="card-back">🥔</div>
    </div>
</div>

For each card, we are going to need 3 div elements:

  • One for the container for which we are going to add some dimensions
  • One for the front of the card — this will be facing us initially, a blank card
  • One for the back of the card — this will be facing away from us, hiding what emoji is written on the card

To flip the cards around, we will be using the transform property. For the .card itself, we can add some dimensions:

Copied to clipboard! Playground
.card {
    position: relative;
    width: 100px;
    height: 100px;
    cursor: pointer;
}

Make sure you also add cursor: pointer here as well to show that the cards are interactive. For the front and back of the cards, we will need the following rules:

Copied to clipboard! Playground
.card-front,
.card-back {
    position: absolute;
    border-radius: 5px;
    width: 100%;
    height: 100%;
    background: #282A3A;
    transition: transform .6s cubic-bezier(0.4, 0.0, 0.2, 1);
    backface-visibility: hidden;
}

The most important part here is the last two rules. We need to use a transition on the transform property to create an animation once we flip the cards. I’m using a cubic-bezier here for the easing. I recommend checking out easings.net and cubic-bezier.com if you want to experiment with different easing functions. The other important rule is backface-visibility. This is used for telling CSS that the back of an element should not be visible to us. This means we won’t see the emoji if the card is flipped.

How backface-visibility affects an element

To rotate the cards into the correct position, add the following rules to your CSS file:

Copied to clipboard! Playground
.card-back {
    font-size: 28pt;
    text-align: center;
    line-height: 100px;
    background: #FDF8E6;
    transform: rotateY(180deg) rotateZ(50deg);
    user-select: none;
}

.card.flipped .card-front {
    transform: rotateY(180deg) rotateZ(50deg);
}

.card.flipped .card-back {
    transform: rotateY(0) rotateZ(0);
}

Make sure you set user-select to none as we don’t want the text to be selectable. This will rotate the back of the card 180 degrees away from us. We can use a helper class called .flipped to set the rotate back to 0 and set the rotate for the front of the card to 180 degrees.

Illustration of cards in 3d space

The same rules are used for the entire board so that when the player wins, the entire board is flipped to its back.

Looking to improve your skills? Check out our interactive course to master JavaScript from start to finish.
Master JavaScriptinfo Remove ads

Creating the Game

We have the layout, we have the styles, now let’s look into implementing the game itself. First, let’s define some selectors, and the game’s state:

Copied to clipboard! Playground
const selectors = {
    boardContainer: document.querySelector('.board-container'),
    board: document.querySelector('.board'),
    moves: document.querySelector('.moves'),
    timer: document.querySelector('.timer'),
    start: document.querySelector('button'),
    win: document.querySelector('.win')
}

const state = {
    gameStarted: false,
    flippedCards: 0,
    totalFlips: 0,
    totalTime: 0,
    loop: null
}
assets/game.js

This will help us reuse selectors multiple times. For the state of the game, we need to keep track of 5 different values:

  • A boolean flag for telling if the game has started (by either clicking on the “Start” button, or on one of the cards)
  • flippedCards will be used to keep track of the number of flips. Only two cards can be flipped at a time. If the flips are a match, they will be kept flipped over. If it’s a mismatch, they will be flipped back.
  • totalFlips and totalTime will keep track of the total number of moves and the elapsed time since the game started, respectively.
  • The loop is for the game loop itself, that will update the timer every 1 second.

Generating the board

The next step would be to generate the board based on the data-dimension attribute. For this, we can create a new function called generateGame, and call it at the end of the file:

Copied to clipboard! Playground
const generateGame = () => {
    const dimensions = selectors.board.getAttribute('data-dimension')

    if (dimensions % 2 !== 0) {
        throw new Error("The dimension of the board must be an even number.")
    }

    const emojis = ['🥔', '🍒', '🥑', '🌽', '🥕', '🍇', '🍉', '🍌', '🥭', '🍍']
    const picks = pickRandom(emojis, (dimensions * dimensions) / 2) 
    const items = shuffle([...picks, ...picks])
    const cards = `
        <div class="board" style="grid-template-columns: repeat(${dimensions}, auto)">
            ${items.map(item => `
                <div class="card">
                    <div class="card-front"></div>
                    <div class="card-back">${item}</div>
                </div>
            `).join('')}
       </div>
    `
    
    const parser = new DOMParser().parseFromString(cards, 'text/html')

    selectors.board.replaceWith(parser.querySelector('.board'))
}
Make sure to call the function at the end of your file

As a first step inside the function, we want to make sure that the dimension we passed is an even number. To check this, we can simply use a remainder. This variable can be used later to dynamically set the style of the board inside a template literal. To add this to the DOM, I’m using a DOMParser

And to generate random items from the array of emojis, I’m using two custom functions here:

  • pickRandom: for picking random items from an array. We want to pick items from emojis, and we want to pick half the items of the dimension. For example, if the dimension is set to 4, then the grid will have 16 items (4x4), so we want to pick 8 random elements from the array. This is because we need two pairs for each emoji, so we pass [...picks, ...picks] for shuffling.
  • shuffle: for shuffling the array, so their order will be random each time the page is reloaded.

Shuffling arrays

Copied to clipboard! Playground
const pickRandom = (array, items) => {
    const clonedArray = [...array]
    const randomPicks = []

    for (let index = 0; index < items; index++) {
        const randomIndex = Math.floor(Math.random() * clonedArray.length)
        
        randomPicks.push(clonedArray[randomIndex])
        clonedArray.splice(randomIndex, 1)
    }

    return randomPicks
}

The pickRandom function — after cloning the original array — loops through the passed array for n number of times (represented by items), and gets an item from it at a random position, then returns the randomPicks at the end of the function. This is then forwarded to the shuffle function, which uses the Fisher-Yates shuffling algorithm. Let’s first see the pseudo-code for the algorithm, so we know how to implement it in JavaScript:

-- To shuffle an array a of n elements (indices 0..n-1):
for i from n−1 downto 1 do
   j ← random integer such that 0 ≤ j ≤ i
   exchange a[j] and a[i]

This means we need three main steps:

  • From i to the <length of the array> - 1, all the way down to 1, we want to:
    • create a random integer, one that is greater than 0, but less than the index. Let’s call it j
    • then exchange the random index with i so that array[i] = array[j] and array[j] = array[i]

This means the shuffle function should look like the following:

Copied to clipboard! Playground
const shuffle = array => {
    const clonedArray = [...array]

    for (let index = clonedArray.length - 1; index > 0; index--) {
        const randomIndex = Math.floor(Math.random() * (index + 1))
        const original = clonedArray[index]

        clonedArray[index] = clonedArray[randomIndex]
        clonedArray[randomIndex] = original
    }

    return clonedArray
}

So we get a descending for loop, in which we create a random index using Math.random and Math.floor (since Math.random returns a random number between 0 and 1). This can be used to switch the position of two items in the array.

Note that we need to store the original indexed value in a variable, to prevent it overwriting with clonedArray[randomIndex].

This will get us the board generated with random emojis every time, but we still can’t flip the cards, so let’s add the necessary event listeners.

Random emojis generated each time

Adding event listeners

We are going to need two different event listeners, one for the cards, and one for the start button. Add a new function at the end of your file, and call it after generating the game:

Copied to clipboard! Playground
const attachEventListeners = () => {
    document.addEventListener('click', event => {
        const eventTarget = event.target
        const eventParent = eventTarget.parentElement

        if (eventTarget.className.includes('card') && !eventParent.className.includes('flipped')) {
            flipCard(eventParent)
        } else if (eventTarget.nodeName === 'BUTTON' && !eventTarget.className.includes('disabled')) {
            startGame()
        }
    })
}

generateGame()
attachEventListeners()

Since the cards will be added dynamically to the DOM, we want to delegate the event listeners, instead of adding them directly on the cards. This is why we need to attach it on the document, and then based on the class names, we can decide which function to call:

  • flipCard with the .card itself. We only call this function if the card hasn’t been flipped yet.
  • startGame, if the “Start” button is clicked, and it is not disabled.

Note that we have the following in our DOM, so the click event will be triggered on .card-front, instead of .card, that’s why we need to pass eventParent, instead of eventTarget to the flipCard function.

Copied to clipboard!
<div class="card">
    <div class="card-front"></div>
    <div class="card-back">${item}</div>
</div>

Starting the game

Let’s first take a look at starting the game, and then we will see how to flip the cards. For this, we are going to need a new startGame function, that sets the correct game state:

Copied to clipboard! Playground
const startGame = () => {
    state.gameStarted = true
    selectors.start.classList.add('disabled')

    state.loop = setInterval(() => {
        state.totalTime++

        selectors.moves.innerText = `${state.totalFlips} moves`
        selectors.timer.innerText = `time: ${state.totalTime} sec`
    }, 1000)
}

First, we set the gameStarted flag to true. We can use this flag in other parts of our application to tell if the game is running. We also want to disable the “Start” button to prevent starting the game over and over again, after it has already been started.

We are also going to need a loop, running every 1 second to update the UI with the total number of flips, and total time elapsed. Note that you will need to assign this interval to state.loop, so that later when the game is finished, we can clear it.

Flipping cards functionality

To correctly flip the cards, we are going to need a little bit more code, but nothing too complicated:

Copied to clipboard! Playground
const flipCard = card => {
    state.flippedCards++
    state.totalFlips++

    if (!state.gameStarted) {
        startGame()
    }

    if (state.flippedCards <= 2) {
        card.classList.add('flipped')
    }

    if (state.flippedCards === 2) {
        const flippedCards = document.querySelectorAll('.flipped:not(.matched)')

        if (flippedCards[0].innerText === flippedCards[1].innerText) {
            flippedCards[0].classList.add('matched')
            flippedCards[1].classList.add('matched')
        }

        setTimeout(() => {
            flipBackCards()
        }, 1000)
    }
}

First, we need to update the state. Every time there is a flip, we want to increment both flippedCards and totalFlips. Then for the rest of the logic, we need to define individual if statements:

  • First, we want to check if the game has already started. If not, we can call the startGame function. This part is optional, but this way, the player can also start the game by simply clicking on one of the cards.
  • Then we want to check if flippedCards is less than, or equal to two. At any given time, we don’t want to allow the player to flip more than 2 cards simultaneously. If there are no more than two cards flipped, we can flip it by adding the .flipped class to the card that we pass to the function.
  • Lastly, we want to check if there are exactly two cards flipped over. In this case, we need to do two different things. Either it will be a match, or a mismatch:
    • First, we get the .flipped cards that are not matched already, then check the emoji inside the first card, against the emoji inside the second card using innerText. If they match, we match the cards by adding a .matched class to them.
    • Secondly, we flip back all cards after 1 second. So what is this function doing?
Copied to clipboard! Playground
const flipBackCards = () => {
    document.querySelectorAll('.card:not(.matched)').forEach(card => {
        card.classList.remove('flipped')
    })

    state.flippedCards = 0
}

Nothing more, than getting all the cards that are not matched, and removing the .flipped class from it then sets the flippedCards state back to 0, so that we can continue flipping up two cards at a time.

Flipping cards in the memory game

Winning

All that’s left to do is to create the winning state for the game. For this, extend your flipCard function with the following if statement:

Copied to clipboard! Playground
// If there are no more cards that we can flip, we won the game
if (!document.querySelectorAll('.card:not(.flipped)').length) {
    setTimeout(() => {
        selectors.boardContainer.classList.add('flipped')
        selectors.win.innerHTML = `
            <span class="win-text">
                You won!<br />
                with <span class="highlight">${state.totalFlips}</span> moves<br />
                under <span class="highlight">${state.totalTime}</span> seconds
            </span>
        `

        clearInterval(state.loop)
    }, 1000)
}

This will check if all cards have been flipped already. If there are no more cards that haven’t been flipped, we know the game has been completed, so we can show the winning state, by flipping the entire board. Don’t forget to clear the loop at the end of the function.

Winning the memory game

Summary

And that’s how you can create your own memory game in JavaScript. I hope you learned some new things in this tutorial. If you are further interested in game development, make sure to also check out the following tutorials that dive into other aspects of game development:

If you would like to get the code in one piece, you can clone the project from its GitHub repository. Thank you for reading through, happy coding!

Did you find this page helpful?
📚 More Webtips
Mentoring

Rocket Launch Your Career

Speed up your learning progress with our mentorship program. Join as a mentee to unlock the full potential of Webtips and get a personalized learning experience by experts to master the following frontend technologies:

Courses

Recommended

This site uses cookies We use cookies to understand visitors and create a better experience for you. By clicking on "Accept", you accept its use. To find out more, please see our privacy policy.