Building a Memory Matching Game in JavaScript

Memory Matching Game : Demo / Code

I recently stumbled across Sandra Israel's article on how she built a memory matching game for her FEND project(found here) and it looked like it was fun to make so I decided to follow through and replicate something like it. I However, there was a twist to mine. I wanted to keep the application as light as possible — no responsive frameworks, no JavaScript libraries, no special icon fonts libraries (I love you font awesome but…) — just HTML, CSS and vanilla JavaScript and also work on my DOM manipulation skills.

So how did I do it? What did I do differently? Let’s delve in.

As Sandra indicated in her article, she had a folder of starter files to begin with. I didn’t have that so I had to make mine. I started with creating a 4 x 4 grid game board to hold 16 cards using the html <table> element. I created a table with 4 rows and 4 columns. Next, I styled the game board such that each cell had a black background. I didn’t want to use icons for my game like in her tutorial so I used an image element with 8 images (each repeated once to make 16 images) within each cell like this: <td class='game-card><img src=”img/1.jpg” class='game-card-img' alt="cake1"></td>and fit images into the cell using the CSS attribute object-fit:cover. So after styling my board looked something like this:

Game Board with Images Shown

Then I set the images visibility to hidden which turned the game board into a grid with 16 black cells.

So right now, it looks like the images are behind the card and will flip to display the images when clicked. How did I implement this click event? First I collected all the cards i.e all the <td class=”game-card”> and stored them in a variable thus: let cardElements = document.getElementByClassName(‘game-card’). Then I used a for loop to loop through them and add a click event listener.

for(let i = 0; i < cardElementsArray.length; i++) {cardElementsArray[i].addEventListener("click", displayCard)}

displayCard is the function which will be called when the card is clicked. It’s almost the same as hers but with a slight difference. Since I was using images instead of icons, I had to target those images as well. So I added this line: this.children[0].classList.toggle(‘show-img’). show-img sets the visibility of the image to visible.

The game requires that cards had to be shuffled on page load (when the game starts) or on restart. Since I was using images instead of icons, how I acheived this varied slightly from the example. First, I made an array called cardElementsArray with all the cards I collected since cardElements was a nodeList not necessarily an array. I made use of the spread operator [...]. Next, I collected all image elements like before: let imgElements = document.getElementsByClassName('game-card-img') and made an array as well let imgElementsArray = [...imgElements].

A shuffle function called the Fisher-Yates (aka Knuth) Shuffle was provided already. Next, I created a startGame() function as thus:

function startGame() {//shuffle cards using the Fisher-Yates (aka Knuth) Shuffle functionlet shuffledImages = shuffle(imgElementsArray);for(i=0; i<shuffledImages.length; i++) {//add the shuffled images to each cardcardElements[i].appendChild(shuffledImages[i]);}}

Basically what this does is, create a shuffledImages array, then loop through the array and append each image to a card. Next, I call the startGame() function on page load i.e window.onload = startGame().

For this stage, each card has to be unique. Since each card had different images, I gave each card object a type property that corresponds to the alt text of each image to distinguish the card from others. So in my startGame() function, after the line that appends each image to each card, I added this line: cardElements[i].type = `${shuffledImages[i].alt}`; Then I created my cardOpen(), matched(), unmatched(), disable() and enable() functions as she did.

The cardOpen() function is supposed to run on every click of a card just like the displayCard() function. However, since I was appending images to each card i.e <td> element instead of just making icons visible and enlarging them as in the tutorial, I could not create a click event listeners and run both functions within that event. Something like this: cardElement[i].addEventlistener('click', function(e){ displayCard(); cardOpen(); }). Neither could I create two separate click event listeners to run each function.

Why? The click event targeted a card and when that card is targeted the image element becomes visible. Clicking again on that card doesn’t target the card <td> element anymore but the <img> element so toggling the classes in displayCard() function returns an error.

How did I address this issue? I decided to call the cardOpen() function on the last line of the displayCard() function. The cardOpen() function works thus as she explained:

  1. Add opened cards to an array called openedCards
  2. If there are two cards in the array, i.e if openCards.length == 2, it checks to see if they match by comparing the type property of the two cards using an if-else statement.
  3. If the type properties match or are equal i.e openedCards[0].type === openedCards[1].type a matched() function is called. This function adds a match class to the classList of each card, removes the show and open classes, pushes the two cards into an array called matchedCards and empties the openedCards.
  4. If the type properties do not match, an unmatched() function is called. The function adds the class unmatched to each card, temporarily disables clicking on the card using the disable() function by setting a time out of 1100ms after which the classes show, open, and unmatched are removed, the images are made invisible, then the cards are enabled using the enable() function. Finally the openedCards array is emptied.

I did the same thing she did in the tutorial: call a moveCounter() function which increments the number of moves a player has done when two cards have been selected, then sets the innerHTML of my counter element to that value.

For my star icons, i used a div parent element and gave it a class name of rating then I created 5 span elements with a star 🌟 icon text from (remember I said I didn’t want to use any font libraries) and gave each of them a class of star. So my rating system was going to be quite different from hers. I collected all .star elements in an array called starElementsArray as I did for the images and cards. Then I modified my moveCounter() function; reducing opacity instead of removing visibility like in the tutorial:

function moveCounter() {moves++;counter.innerHTML = `${moves} move(s)`;//setting rating based on movesif(moves > 8 && moves <= 12) {for(let i=0; i<5; i++) {starElementsArray[i].opacity = 1;}} else if(moves > 12 && moves <= 16) {for(let i=0; i<5; i++) {if(i > 3) {starElementsArray[i].style.opacity = 0.1;}}} else if(moves > 16 && moves <= 20) {for(let i=0; i<5; i++) {if(i > 2) {starElementsArray[i].style.opacity = 0.1;}}} else if(moves > 20 && moves <= 24) {for(let i=0; i<5; i++) {if(i > 1) {starElementsArray[i].style.opacity = 0.1;}}} else if(moves > 24){for(let i=0; i<5; i++) {if(i > 0) {starElementsArray[i].style.opacity = 0.1;}}}}

Nothing much to see here. Same as in the tutorial.

I provided two restart buttons for refreshing or restarting the game; one in the game status bar — along side the star rating, move counter and timer — and one underneath the game board. On click of either of this buttons, a modified startGame() function is called. It removes all the extra classes, removes images, resets moves to 0, resets the timer and restores the rating to 5 stars with an opacity of 1. The event listener for each card click is also now in the startGame() function.

function startGame() {//shuffle cardslet shuffledImages = shuffle(imgElementsArray);for(i=0; i<shuffledImages.length; i++) {//remove all images from previous games from each card (if any)cardElements[i].innerHTML = "";//add the shuffled images to each cardcardElements[i].appendChild(shuffledImages[i]);cardElements[i].type = `${shuffledImages[i].alt}`;//remove all extra classes for new game playcardElements[i].classList.remove("show", "open", "match", "disabled");cardElements[i].children[0].classList.remove("show-img");}//listen for events on the cardsfor(let i = 0; i < cardElementsArray.length; i++) {cardElementsArray[i].addEventListener("click", displayCard)}//reset movesmoves = 0;counter.innerText = `${moves} move(s)`;//reset star ratingfor(let i=0; i<starElementsArray.length; i++) {starElementsArray[i].style.opacity = 1;}//Reset Timer on game resettimer.innerHTML = '0 mins 0 secs';clearInterval(interval);}
Modal Displayed When Game Ends.

When all cards have been correctly matched; the game ends and a modal should appear to alert the user. In my own case; in the matched() function, when matchedCards.length == 16 I call the endGame() function. What this does is that it clears the interval used to created the timer, retrieves the value of the move counter, timer and rating then shows a modal with those details. It shows a playAgain button which calls the playAgain()function that closes the modal and refreshes the game.

It also clears the matchedCards array so that when a game is restarted/refreshed without reloading the page, the matched cards from the previous games are not left there. This prevents the game from disabling all the cells in the board when two cards are clicked. There’s also a closeModal() function to which is called when the close button X is clicked to close the modal.

function endGame() {clearInterval(interval);totalGameTime = timer.innerHTML;starRating = document.querySelector('.rating').innerHTML;//show modal on game endmodalElement.classList.add("show-modal");//show totalGameTime, moves and finalStarRating in ModaltotalGameTimeElement.innerHTML = totalGameTime;totalGameMovesElement.innerHTML = moves;finalStarRatingElement.innerHTML = starRating;matchedCards = [];closeModal();}
  1. I was trying to work on my CSS animation skills as well so I added a CSS animation such that there’s something like a flip effect when the images are shown.
.show-img {visibility: visible;animation: animateShowImage 0.4s linear alternate;}@keyframes animateShowImage {0% { transform: rotateY(90deg); opacity: 0;}100%{ transform: rotateY(0); opacity: 1; }}

2. I animated my message in my modal. 😃 My congratulations emojis 🎊 and 🎉 as well as the modal close icon were made to bounce. Here are some resources from which you can get these icons without a special icon library: altcodeunicode, alt-codes and emojipedia. Just copy the icons from the site as is and paste them in your editor.

A word of caution, however: Make sure they are visible across and not showing something like this 🥰 in a web browser before you use them.

3. I wanted all the cards to flash at once, once the page loaded so I created a function called flashCards() and immediately after the click event listener for each card in startGame(). Then instead of calling startGame() immediately page loads, I delayed it for 1200ms using a setTimeOut() so that the user can see when the cards flash at the beginning of the game.

function flashCards() {for(i=0; i<cardElements.length; i++) {cardElements[i].children[0].classList.add("show-img")}setTimeout(function(){for(i=0; i<cardElements.length; i++) {cardElements[i].children[0].classList.remove("show-img")}}, 1000)}
// wait for some milliseconds before game startswindow.onload = function () {setTimeout(function() {startGame()}, 1200);}

4. I added a help modal which contained instructions on how to play the game. It can be accessed by clicking the ? button on the top-right hand corner of the page. I was looking for an icon which had the circle question mark for that button but I didn’t find any so I made mine using svg. An example can be found here.

5. I added media queries for web responsiveness of the page.

I really enjoyed working on this during the weekend despite the fact I was a little bit under the weather. I learnt a quite a number of new things that I can apply to other projects and I was able to recollect some other things I have learnt from previous courses I took. If you want to see what the demo of my game looks like, you can click here. The files for the game are hosted on my github.

Are there any additions or features I can add? Is there any way I can make my code better? Kindly let me know in the comments.

P.S This is my first technical article on Medium (I hope I didn’t do too badly 😊).

Front End Dev || Writer || Blogger

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store