How to Build a Memory Matching Game in JavaScript

Sandra Israel-Ovirih
👁️ 2,580 views
💬 comments

I recently built a matching game as one of my FEND (Front-end NanoDegree) projects, and in this post, I will be documenting the process. I guess this can be a simple tutorial for people learning javascript and want to build a real project.

Let’s get to it:

  • What is the memory game
  • Requirements of the game
  • Flow

What is the memory game

The memory game is a basic matching game to test the player's memory. In a deck of paired cards, the player needs to match each pair to win the game.

Requirements for the game

A checklist of things I needed to do. I like the idea cause it just clears out your thinking:

  1. Cards are to be shuffled on load or restart
  2. Game should know how to handle matched and unmatched cards
  3. Game should display the current number of moves a user has made
  4. The game should show a star rating (from 1–3) that reflects the player’s performance based on the number of moves made. 
  5. When a player starts a game, a displayed timer should also start and once the player wins the game, the timer stops.
  6. A restart button should allow the player reset the game board, the timer, and the star rating.
  7. A congratulations modal should appear when the player wins while showing a button to play again and modal should show: How much time it took, and star rating.

Process / Flow

Creating the structure of Gameboard

The structure of the gameboard was already provided in the repo, and it can be achieved with HTML and CSS. Create something like a grid. The parent div will be the deck containing all cards. The cards can be created as an unordered list and styled to look like the image below using flexbox.

Handling how a click on a card would display the card icon

It looks like the icons are behind the card and it flips to show the icon when it’s clicked. If you examine the styles for each card you’ll get a better understanding of what exactly is happening. When closed, card background color is black and the font size is zero and when opened background color changes to blue and font size increase*…not so mysterious right? Great!

Now, we have a deck of cards what next? We need to ensure that on each click of a card the card displays it’s icon. To achieve this, event listeners come in handy! Unfortunately, adding event listeners individually to each card would be stressful and doesn't promote code reuse. for loops will be best to use case here. To achieve this, a list of our cards is needed -- we can use an array.

// cards array holds all cards
let card = document.getElementsByClassName("card");
let cards = [...card];
// loop to add event listeners to each card
for (var i = 0; i < cards.length; i++){
   cards[i].addEventListener("click", displayCard);
};
//displayCard is a function we'll talk about this soon

In the code block above, the cards' array[1] was created and the for loop helps to loop through each card till the full length of cards array is covered. Each loop will add an event listener which listens for a click on the card and runs the displayCard function on click.

// toggles open and show class to display cards
var displayCard = function (){
   this.classList.toggle("open");
   this.classList.toggle("show");
   this.classList.toggle("disabled");
}

The displayCard function here toggles ‘open’, ‘show’ and ‘disabled’ classes. This lets the card icon show and disables the card when it’s opened. Hence, when a card is shown it can’t be clicked on again till it is closed. Now we’ve sorted clicking to see the cards; we can start working on the game requirements better.

1. Shuffing Cards

Cards are to be shuffled on load or restart

There’s really no game if cards can’t shuffle. In this project, a function to shuffle an array was already provided from here. This is known as Fisher-Yates (aka Knuth) Shuffle. With this function, we should be able to shuffle our cards on the game board:

// Fisher-Yates (aka Knuth) Shuffle
function shuffle(array) {
  var currentIndex = array.length, temporaryValue, randomIndex;

  while (currentIndex !== 0) {
    randomIndex = Math.floor(Math.random() * currentIndex);
    currentIndex -= 1;
    temporaryValue = array[currentIndex];
    array[currentIndex] = array[randomIndex];
    array[randomIndex] = temporaryValue;
  }
  return array;
}

From the function above, our cards array will be the parameter. Like so: shuffle(cards);

Although that is sufficient to shuffle our cards, it will not change the position of cards on the game board. Hence, we need to loop through the generated shuffle array and display each card deck with:

// deck of all cards in game
const deck = document.querySelector(".deck");
function startGame(){
   var shuffledCards = shuffle(cards);
   for (var i= 0; i < shuffledCards.length; i++){
      [].forEach.call(shuffledCards, function(item){
         deck.appendChild(item);
      });
   }
}

In the code above is the startGame function and it will shuffle cards and display each card in the deck on game board.  Since we know the startGame function shuffles the card in order to shuffle the cards on load, we add this to our JS:

window.onload = startGame();

Simply saying once this window (page) is loaded, run the startGame function.

Requirement 1: completed Great!

2. Matching Cards

Handling matched and unmatched cards

For this part, we need to make each card unique. Since icons on each cards are different, I gave each card object a type property that corresponds to the icon of the card to distinguish each card.

//add opened cards to OpenedCards list and check if cards are match or not
function cardOpen() {
    openedCards.push(this);
    var len = openedCards.length;
    if(len === 2){
        moveCounter();
        if(openedCards[0].type === openedCards[1].type){
            matched();
        } else {
            unmatched();
        }
    }
};

//for when cards match
function matched(){
    openedCards[0].classList.add("match");
    openedCards[1].classList.add("match");
    openedCards[0].classList.remove("show", "open");
    openedCards[1].classList.remove("show", "open");
    openedCards = [];
}

//for when cards don't match
function unmatched(){
    openedCards[0].classList.add("unmatched");
    openedCards[1].classList.add("unmatched");
    disable();
    setTimeout(function(){
        openedCards[0].classList.remove("show", "open", "unmatched");
        openedCards[1].classList.remove("show", "open", "unmatched");
        enable();
        openedCards = [];
    },1100);
}

//disable cards temporarily
function disable(){
    Array.prototype.filter.call(cards, function(card){
        card.classList.add('disabled');
    });
}

//enable cards and disable matched cards
function enable(){
    Array.prototype.filter.call(cards, function(card){
        card.classList.remove('disabled');
        for(var i = 0; i < matchedCard.length; i++){
            matchedCard[i].classList.add("disabled");
        }
    });
}

The cardOpen function runs on every click of a card just like the displayCard function. The function adds the selected cards to an openedCards array which we can use to know which cards are opened. An if-else statement runs when two cards are selected and checks to see if cards match or don’t match. The key to identifying different cards is the type attribute I added to each card. The disable and enable functions enables a player to make only two selections at once by disabling or enabling cards when required.

3. Moves

Game should display the current number of moves a user has made

We can achieve this with a moveCounter function that counts a move on selecting two cards. In the snippet above the moveCounter function is called only when openedCards.length === 2

function moveCounter(){    
    moves++;    
    counter.innerHTML = moves;
}

4. Star Rating

The game should display a star rating (from 1–3) that reflects the player’s performance based on number of moves made

You should provide star icons in your score panel for this feature. From the requirement above the star rating is dependent on the number of moves made, we can modify the moveCounter function to accommodate handling of the star rating.

function moveCounter(){
    moves++;
    counter.innerHTML = moves;

// setting rates based on moves
    if (moves > 8 && moves < 12){
        for( i= 0; i < 3; i++){
            if(i > 1){
                stars[i].style.visibility = "collapse";
            }
        }
    }
    else if (moves > 13){
        for( i= 0; i < 3; i++){
            if(i > 0){
                stars[i].style.visibility = "collapse";
            }
        }
    }
}

Here, I used if else statements to create a range depending on number of moves that will give some of the stars a style of visibility: collapse

5. The Timer

When the player starts a game, a displayed timer should also start. Once the player wins the game, the timer stops.

We can create a timer using JS with the code below. This will be sufficient and will be displayed in the score panel just above the game board:

//game timer
var second = 0, minute = 0;
var timer = document.querySelector(".timer");
var interval;
function startTimer(){
    interval = setInterval(function(){
        timer.innerHTML = minute+"mins "+second+"secs";
        second++;
        if(second == 60){
            minute++;
            second = 0;
        }
        if(minute == 60){
            hour++;
            minute = 0;
        }
    },1000);
}
function moveCounter(){
    moves++;
    counter.innerHTML = moves;
    //start timer on first move
    if(moves == 1){
        second = 0;
        minute = 0; 
        hour = 0;
        startTimer();
    }

We would update the moveCounter function to start timer on first move.

startGame(){
. . .
//reset timer
var timer = document.querySelector(".timer");
timer.innerHTML = "0 mins 0 secs";
clearInterval(interval);
}

Here also, is an updated startGame function so when the game is reset the timer is reset too. Beyond, changing the HTML of timer to ‘0 mins 0secs’ the clearInterval clears the previous running interval.

6. Restart button

A restart button should allow the player reset the game board, the timer, and the star rating

Some of this requirements have already been fulfilled, but I’ll show what the code looks like. The restart icon should also be available in your score panel.

//My restart button
<div class="restart" onclick="startGame()">
   <i class="fa fa-repeat"></i>
</div>

Above is a snippet of what the restart button looks like in HTML. On click of the restart button startGame function runs.

StartGame(){
     // shuffle deck
     cards = shuffle(cards);
     // remove all existing classes from each card
    for (var i = 0; i < cards.length; i++){
        deck.innerHTML = "";
        [].forEach.call(cards, function(item) {
            deck.appendChild(item);
        });
        cards[i].classList.remove("show", "open", "match",            "disabled");
     }
     // reset moves
     moves = 0;
     counter.innerHTML = moves;
    // reset star rating
    for (var i= 0; i < stars.length; i++){
        stars[i].style.color = "#FFD700";
        stars[i].style.visibility = "visible";
    }
    //reset timer
    var timer = document.querySelector(".timer");
    timer.innerHTML = "0 mins 0 secs";
    clearInterval(interval);
}

Above update to the function shuffles deck , removes existing classes, resets the timer, star rating and number of moves

7. A Congratulations Modal

A congratulations modal should appear when user wins and ask the player if they want to play again, Modal should show: How much time it took, and star rating

This is the rounding up section of this project. What’s the point if you’re playing a game and you win but you don’t even know when you do. Logically, you’ll know you’ve won when all your cards are matched. To proceed, we need to alert the player when all cards are matched. This alert will be in form of a modal that shows the time spent, the rating and . To get this done, we need to add a modal in our html file, then style the modal with css and hide it. We will us JS to make the modal visible.

//modal
let modal = document.getElementById("popup1")
//stars list
 let starsList = document.querySelectorAll(".stars li");
//close icon in modal
 let closeicon = document.querySelector(".close");
//congratulations when all cards match, show modal and moves, time and rating
function congratulations(){
    if (matchedCard.length == 16){
        clearInterval(interval);
        finalTime = timer.innerHTML;
    //show congratulations modal
    modal.classList.add("show");
    //declare star rating variable
    var starRating = document.querySelector(".stars").innerHTML;
    //showing move, rating, time on modal
    document.getElementById("finalMove").innerHTML = moves;
    document.getElementById("starRating").innerHTML = starRating;
    document.getElementById("totalTime").innerHTML = finalTime;
    //closeicon on modal
    closeModal();
    };
}

The congratulations function checks to see if all cards are matched, if they are matched then it stops the timer. gets number of moves, star rating and time spent. and displays on the congratulation modal. There are also some functions that close the modal and reset the game on clicking the close icon and play again button provided in the modal.

//close icon on modal
function closeModal(){
    closeicon.addEventListener("click", function(e){
        modal.classList.remove("show");
        startGame();
    });
}
//for player to play Again 
function playAgain(){
    modal.classList.remove("show");
    startGame();
}

Conclusion

I am so glad we’ve reached this point. This project helped me to recollect all I’ve learnt from the numerous basics /fundamentals of Javascript courses I’ve taken. I enjoyed working on this project and I’d love if you try your hands on it too.  And if you followed through:

--

[1] Notice, I made another array called cards, which is an array of cards. This is because if you log “card” it will look like an array in your console but it is not; it is a NodeList being so, it will not shuffle. But the “cards” will shuffle because it is an array.  Other things to consider that might bring errors: Hoisting, Scopes 


PS- I finished this project a while ago, and I am only just completing this article today. I look back at this code, and I’m well aware that I could make it better. I’m afraid I won't be able to work on other things if I keep iterating it. I’d love to hear your suggestions on other features or additions I can make. Thanks for getting to this point.