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:
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.
Let’s start with a simple HTML template, shown below:
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.
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 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:
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 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
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
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.
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
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 [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.
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>
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
:
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.
The cards should initially be facing down. Let’s do that now:
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.
Let’s now enable the flipping of the card to reveal the image when a player clicks on the card element.
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.
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.
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.
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.
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
andcard2
, first in theif
part on lines 64–65 and then through theelse
part on lines 76–77. Couldn’t we have refactored these and put them before the end of thecheckMatch()
function outside both theif
and theelse
clauses? The answer is no because if we did that,card1
andcard2
would benull
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.
We are keeping track of the score, but we haven’t displayed it. Let’s do that.
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.
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.
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 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 becauseremoveEventListener()
only works with event listeners that are added usingaddEventListener()
.
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.
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.
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