Home/Blog/Learn to Code/Polish your JavaScript: create a memory game
Home/Blog/Learn to Code/Polish your JavaScript: create a memory game

Polish your JavaScript: create a memory game

Saqib Ilyas
Jan 19, 2024
19 min read

This blog assumes basic knowledge of HTML, CSS, and JavaScript. It's targeted toward individuals who are learning to create web applications.

When learning to code, practicing by creating applications is extremely important. Early-stage learners often get discouraged by the complexity of it all. Creating games is a great way to practice coding for various reasons. First, we're familiar with the game rules, which removes some of the complexity from the process. Second, games are fun, so we enjoy ourselves while coding them.

In this blog, we'll create a memory game in JavaScript. Here is a preview of the game:

A preview of the game we’ll create

Here are the game requirements:

  • The game displays 12 cards face down.

  • The cards are displayed in a grid with four rows and three columns.

  • There are a total of 12 cards with six unique photos, i.e., each unique photo appears on exactly two cards.

  • When the player clicks on a card, the corresponding photo will be revealed momentarily.

  • If the player consecutively clicks on two cards that have the same photo, both cards will be turned over. Otherwise, the cards will revert to facing down.

  • The player wins once all cards are facing up.

While creating the memory game, we’ll learn how to use CSS Flexbox to position elements in a 2D grid, create rectangular shapes with the canvas API, the :not() pseudo-class, the nth-child() selector, and various CSS properties. We’ll also learn how to create, add, and remove mouse event handlers, as well as manipulate the DOM in JavaScript.

The HTML template#

Let’s start with a simple HTML template, shown below:

HTML template for our game

We link to the CSS file (style.css) on line 5 and the JavaScript file (app.js) on line 24. We create a span element (line 9) to show the score and a div element (line 10) to display the card deck. This div element contains 12 child div elements with a class of card. We’ll use these later to display the card images. Currently, these are blank.

When we view the output in the above coding playground, all we’ll see is “Score: 0.” Why don’t we see the cards despite having the div elements? It’s because they don’t have any content, and we haven’t defined any dimensions for them either. Let’s fix that.

CSS styling to lay out the cards

We start with a CSS reset on lines 1–5 where we select all the elements and set the margins and padding to zero. We also set box-sizing to border-box so that the border widths and heights are included in element sizes.

We use the CSS class selector on line 7 to select the div elements with the class of card. We give each card a width and height of 100 pixels (lines 8–9). We give each card a greenish background color (line 10) and a grayish rounded border (lines 11–12). To set each card apart from the card to its right, we give it a right margin of five pixels (line 13).

We use an ID selector to select the div element with an ID of grid. We set its margin to 0 auto to center it horizontally (line 17). To set the grid comfortably apart from the score, we set the top margin to 50 pixels (line 18). We give the grid a grayish border (line 19). We give it rounded edges using the border-radius property (line 20). We declare that we are using Flexbox to lay out the cards within this div element (line 21). By default, all the card elements are placed by Flexbox in a row. We configure Flexbox to wrap the contents onto the next row when the width of the container runs out (line 22).

Finally, we have an nth-child selector on line 28. The selector can be read from right to left as “Select the 1st, 4th, 7th, and 10th child img elements of any element that has an ID of grid.” The 3n + 1 part can be interpreted by replacing positive integer values (0, 1, 2, ...) in place of n. That gives us 1, 4, 7, 10. Any higher values are invalid because the div element with an ID of grid has 12 child div elements. These are the cards that are on the left of the 3×43 \times 4 grid. We want to set these cards apart from the left edge of the parent div element. So, for this selector, we set a left margin of five pixels.

Let’s decipher the width and height for grid set on lines 24 and 25, respectively. Please refer to the slides shown below:

canvasAnimation-image
1 of 5

On slide 1, we show that we want each card to be 100 pixels wide and 100 pixels high. Since we have set the box-sizing property to border-box (line 4), the border and padding on either side are included in this size of 100 x 100 pixels for the card elements. Any margin or padding on the grid element, however, will be additional to this. On slide 2, observe that placing three card elements in a row, with a five-pixel margin on the right of each card element and left of the first card element, which results in a row being 3×100+5×4=3203\times 100 + 5\times 4 = 320 pixels wide. On slide 3, observe that four rows stacked up with a padding of five pixels on the top for the grid element results in a height of 405 pixels. Finally, on slide 4, observe that adding a symmetric gap below the rows of card elements and a border around the grid element requires increasing the grid element’s height by at least 5×4=205\times 4 = 20 pixels to 425 pixels. We also account for the two pixels of the exterior border on the right and left in the total width of the grid element, making it 324 pixels.

Displaying the cards#

Now, let’s work on displaying the actual card images. Recall that we have a total of 12 cards to display with six unique photos. We’re referring to each pair of identical cards as a set. So, there are six sets of cards.

We need to display some random permutation of the cards. Here’s how we can approach solving this problem:

  • Associate an integer from 00 to 55 with each set of cards.

  • Create an array with the values [0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5]. There are 12 elements in this array, each corresponding to a unique card. The value of each element in this array represents the set of the corresponding card.

  • Create a random permutation of the above array.

  • Use the shuffled array above as indexes into an array holding the URLs for the card images.

Generating the shuffled array of integers#

First things first. Let’s generate the random shuffled array of integers. Here’s how we can do this: click the “Run” button to see that the following code generates a random shuffled array that has two occurrences of each integer from 00 to 55.

let indices = [0, 1, 2, 3, 4, 5]
indices = [...indices, ...indices]
indices.sort(() => Math.random())
console.log(indices)

If we press the “Run” button multiple times, the same shuffled array is generated every time. This is because the pseudorandom number generator is seeded by the same value every time. There are ways around it, but we’ll keep it simple for this blog.

First, we declare an array named indices with integers from 00 to 55 (line 1). Since we need two copies of each of these integers (there are two cards in each set with the same image), we next concatenate this array with itself (line 2) using the spread operator. At this point, the array looks like [0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5].

Next, we randomly shuffle this array using the sort() method (line 3). If you’re wondering what sort() has to do with random shuffling, please check the explanation by clicking the button below.

Now that we have the random shuffled array, its elements can be used as indexes into another array that holds paths for the image files.

Displaying the image files randomly#

To display the cards, we could modify our existing HTML structure by replacing the child div elements of the grid element with img elements so that the images are displayed:

<div id="grid">
<img src="imagePath">
<img src="imagePath">
<img src="imagePath">
<img src="imagePath">
<img src="imagePath">
<img src="imagePath">
<img src="imagePath">
<img src="imagePath">
<img src="imagePath">
<img src="imagePath">
<img src="imagePath">
<img src="imagePath">
</div>
HTML structure to be created in code

Rather than hardcoding the images, let’s write a JavaScript code to create those img tags and insert them in the div element with the id of grid:

Dynamically displaying the cards

First off, notice that there are no cards defined in HTML anymore. We’ll create all the cards with code.

First, we switch over to the “JavaScript” tab. We start by defining an object corresponding to the div element with the id of grid using document.getElementById() on line 1. We’ll need this object to insert the img elements into the Document Object Model (DOM). We define an array of image paths named cardsArr (lines 3–10). Each element in this array is the path to a unique image file. Next, we define the array of indexes on line 12. Afterwards, we call a function named init() on line 14. This function does the random shuffling of the indices array (lines 17–18) and calls another function called createBoard() (line 19). The createBoard() function (lines 22–29) iterates over the random shuffled indices array using the forEach() method. We define an anonymous function that is called on each element of the indices array in sequence, starting from the first. The anonymous function accepts the element’s value in the variable i. On line 24, we create a new img element and store it in the variable img. We take the array element value as an index into the array cardsArr, extracting the path to one of the images, and set the img element’s src attribute to it (line 25).

Next, we set up a data attribute on the img element (line 26) named data-id to store the set to which the current image belongs. This attribute will be handy when comparing two cards as the player turns them over. Two cards with the same value for data-id have the same image. On line 27, we add a class of card to the img element. Finally, we insert the img element into the DOM as a child of the div element with an id of grid on line 28.

We also modify the CSS file in two places. Firstly, on line 14, we apply a bit of padding to the card element so that the image it contains doesn’t touch the edges. Secondly, on line 29, we change the nth-child selector to select img child elements. This should nicely display 12 images in the “Output” tab.

Cards facing down#

The cards should initially be facing down. Let’s do that now:

Getting the cards facing down

In JavaScript, we create a canvas element on line 3 and set its dimensions in the init() function on lines 21–22. We obtain a 2D drawing context on line 23 through the canvas API. We set a greenish fill color on line 24 and fill a 100-pixel by 100-pixel rectangle with that background color on line 25. Finally, to put this rectangle as the image for the img elements that serve as cards, we change line 33. Instead of setting the src attribute to the URL of an image, we now set it to canvas.toDataURL(). Now, in the “Output” tab, we’ll see all cards are hidden.

As an additional step, in CSS, we change the cursor property to pointer on the card element (line 15). With this, hovering over the card elements gives the player a visual clue that this is an active element.

Flipping the card by clicking it#

Let’s now enable the flipping of the card to reveal the image when a player clicks on the card element.

Flipping cards

We register a function named cardFlip as the click event handler on the img elements on line 35. On lines 40–42, we define the function. Since this is a click event handler for an img element, the this pointer in this function refers to the img element that the player clicked. We use the this pointer to change the src attribute of the img element to the image that we hid earlier (line 41). To do this, we access the cardsArr with an index equal to the data-id attribute of the img element. Now, in the “Output” tab, any card that we click will be flipped over.

Matching two consecutively flipped cards#

Now, we’ll check if the two consecutive flipped cards matched. If they match, we’ll keep them facing up. Otherwise, we’ll flip them both around. Here’s our line of action:

  • Initialize a variable named score to 0. We’ll use this to hold the score.

  • Declare two variables, card1 and card2, to hold the flipped cards. Initialize both to null.

  • On the first card flip, store the flipped card in card1. On the next card flip, store the flipped card in card2.

  • Compare card1 and card2. If they match, then increment the score variable by 1. Otherwise, flip both cards back.

  • Repeat until the score becomes 6.

Let’s write the code.

Matching flipped cards

On line 18, we initialize card1 and card2, while on line 20, we initialize the score. We extend the functionality in the cardFlip() function (lines 46–56). First, we check if card1 is null, which means that this is the first card flip (line 49). If that is true, we store the card’s object in the variable card1. If this isn’t the first card to be flipped, we store the flipped card’s object in the variable card2 (line 53).

Next, we call the checkMatch() function on line 54. This function compares the two cards that were flipped and is defined on lines 58–69. We compare the img element’s data-id attributes (line 61). In case of a match, we assign a class of matched to the card images (lines 60–61). This class is defined in CSS on lines 35–38. We set the opacity to 0.5 (line 36) and restore the default cursor (line 37). The first action dims the images, giving an indication to the player that these cards are sorted out. The second action ensures that hovering the cursor over these cards does not indicate an active element underneath. Since two matching cards have been found, the player would start the card matching effort all over. We enable this by resetting card1 and card2 to null (lines 62–63). We also increment the score variable on line 64.

If the cards don’t match, they should both be flipped over. We do that in the restoreCards() function (lines 71–76) by setting the src attributes of card1 and card2 to the rectangle image (lines 72–73) that we created with the canvas API and resetting the card1 and card2 variables (lines 74–75).

Switch to the “Output” tab in the above coding playground and try the game out. There’s a problem with the game. If we flip two consecutive cards that don’t match, both card images disappear before we’ve had a chance to actually see what image the second card has. Let’s fix that now.

Fixing the mismatched card flip glitch#

Both cards disappear if they don’t match because we call the restoreCards() function immediately. If we delay that call somewhat, the glitch is removed.

Fixing the card flip glitch

On line 69, we change the call to restoreCards() with a call to the setTimeout() function to invoke the restoreCards() function after a delay of half a second. That gives the player enough time to see the card and memorize its location.

Note: Observe the seemingly redundant resetting of card1 and card2, first in the if part on lines 64–65 and then through the else part on lines 76–77. Couldn’t we have refactored these and put them before the end of the checkMatch() function outside both the if and the else clauses? The answer is no because if we did that, card1 and card2 would be null by the time we reach lines 74 and 75, resulting in an exception.

There are two problems with the game in the coding playground above. Firstly, we can click a card twice and get a score. Secondly, if we click three cards in quick succession before restoreCards() is called, the second card to be clicked remains revealed. We’ll fix these issues shortly as we implement the rest of the game logic.

Displaying the score#

We are keeping track of the score, but we haven’t displayed it. Let’s do that.

Displaying the score

It is simply a matter of setting the innerText property of the span element with the ID of score. We obtain an object referring to that span element on line 5 and then set its text to the value of the score variable once we have confirmed a card match on line 65.

Finishing touches#

In the coding playground above, the following problems occurred:

  • We can continue clicking already matched cards and increase our score.

  • Before two consecutively flipped cards with different images disappear, we can click another card, which messes up the gameplay. We noticed this earlier as well.

  • We need a “Game over” logic.

The solution to the first and second problem is to remove the click event handler from the div elements. If we remove the event handlers (and remove the cursor: pointer; style) after the second card flip until the two cards have been flipped, the problems are resolved.

The completed game code

We start by declaring a const variable named MAX_SCORE on line 7, initialized to 6. We use this constant in a comparison on line 70 to see if the player has matched all the cards. If so, we end the game by calling a function named gameOver() (lines 104–109). In this function, we create a new div element (line 105) and set its text to “Game over” (line 106). We add a class of gameover to this div element (line 107) and add it to the DOM (line 108). The class of gameover is defined in CSS on lines 54–66. We set the dimensions for this div element to 100×50100\times 50 pixels (lines 56–57). We want to position the div at the center of the game board. So, we take the div out of the normal flow of the page by setting its position property to absolute (line 58), and set its top and left properties to 50% (lines 59–60). This places the top left corner of the div element at the center, which looks odd. To fix that, we translate the div element 50% toward the left and 50% toward the top (line 61). We center the contents of this div horizontally and vertically using CSS Flexbox (lines 62–64). Finally, we set the background and text colors (lines 65–66).

Now, back to JavaScript. If the player clicks two images that don’t match, clicking a card in the next half-second puts three cards face up, which isn’t desirable. To avoid that, we call a function named disableClicks() on line 75. This function (lines 88–94) finds all card elements that don’t have a class of matched using the document.querySelectorAll() method on line 89. Here, we use the :not() pseudo-class. We iterate over the collection returned on line 89 using the forEach() method on lines 90–93. For each img element in the collection, we remove the click event listener and set the cursor property to its default.

Note: If we make a seemingly harmless change by changing img.onclick = cardFlip on line 43, we won’t get the desired result. This is because removeEventListener() only works with event listeners that are added using addEventListener().

Since we disabled clicks on all cards once the player clicks two cards that don’t match, we need to enable the clicks once the cards are flipped back over. We do that through a call to the function named enableClicks() on line 85. In this function, we first filter out all the card elements that don’t have the class of matched (line 97). Then, iterating over the cards collection, we enable click on line 99 and set the cursor property to the distinctive pointer to give the player a visual cue when they hover an active card element.

Cover
Game Development with JavaScript: Creating Tetris

In this course, you will get hands-on game development experience with JavaScript. Using the classic game of Tetris, you are going to cover concepts like graphics, game loops, and collision detection. By the end of this course, we will have a fully functioning game with points and levels. Try it out with your friends and put it in your portfolio for employers to see.

5hrs 30mins
Beginner
12 Playgrounds
10 Quizzes

That's all, folks!#

With that, our game is complete. Try to tweak the game’s look and feel. Implement other features, such as the ability to start a new game and CSS animations when flipping the cards. Did you notice that we haven’t implemented the effect of gradually decreasing opacity when cards are matched? How would you implement that? How about making the cards appear like they are rotating when you click them? Implement these and other things in the above playground. Keep trying things and keep learning.


  

Free Resources