Memory Game

Memory game

Building of JavaScript Memory Game

Using HTML, CSS and JavaScript, I built a two player memory game. The game rules follow:

  • Each player takes a turn and turns over two cards, one at a time.
  • If the cards are not a match, they get turned back over.
  • If the cards are a match, they stay flipped.
  • If you get a match, you get another turn.
  • The player with the most matches wins.
Memory Game

How I built it

To establish a structure for the game, I started with a standard HTML setup. I included a scoreboard and a gameboard.

              
            <div class="card" data-icon="cat">
                <i class="fas fa-cat fa-5x card-front"></i>
                <img class="card-back" src="img/question.png" alt="question mark">
            </div>
            <div class="card" data-icon="cat">
                <i class="fas fa-cat fa-5x card-front"></i>
                <img class="card-back" src="img/question.png" alt="question mark">
            </div>
        
            

I created a grid of divs which serve as the cards, a pair of them is shown in the code above. Each card has an image on the back, which is the side you see at the game start. The other side of each card has an icon. There are currently 16 cards total, and 8 unique icons for 8 matches. That's about it for the HTML!

The Challenges

This game serves up a handful of challenges, and here is how I tackled some of those.

Deal the cards

To begin the game, the cards must be shuffled. Below is my shuffleCards function.

              
            function shuffleCards() {
                cards.forEach(card => {
                    let randomPos = Math.floor(Math.random() * 16);
                    card.style.order = randomPos;
                });
            }
        
            

To shuffle the cards, I created a function that generates a random number between 1-16. First, it goes through each card in the cards array using forEach, and assigns it a random number. The random number is generated by first taking Math.random and multiplying it by our number of cards (16) to get a random number between 1 and 16. Then we use Math.floor to round this result down to an integer, which then gets applied to the order style on the card divs to change their displayed order on screen. This is called upon the start of a new game.

Flip the cards over when clicked

The next task is to make the cards display properly when clicked, and also to assign a "first" or "second" status to them to allow the match and win checks to take place.

              
            function flipCard() {
                if (lockBoard) return;

                if (this === firstCard) return; 

                this.classList.add('flip');

                if (!secondFlip) {
                    secondFlip = true;
                    firstCard = this;

                } else {
                    secondCard = this;
                    checkForMatch();
                    checkForWin();
                }
            }
        
            

In the flipCard function shown above, there are several things happening. First, we make sure that the cards aren't clickable when they're flipping back over. Secondly, we make sure that if the player clicks the same card a second time within a turn, nothing happens. When a card is clicked, it is has a flip class added to it. Each card div has both an image (shared with all other card backs), and an icon. You can see both in the HTML snippet included above. The icon is not visible upon loading the game, but when the flip class is added, it will add a rotation animation, and display the icon on the underside of the card. If the card is the first one clicked in a turn, it is assigned a firstCard class, otherwise, we know it's the secondCard . Once a second card is clicked, flipCard calls checkForMatch (to check for a match) and checkForWin (to check for a winner).

Test for matched cards

The next task is to see if the cards match.

              
            function checkForMatch() {
                let isMatch = firstCard.dataset.icon === secondCard.dataset.icon;

                if (isMatch) {
                    updateMatchColor();
                    disableCards(); 
                    updateScore();

                    setTimeout(() => {
                        updateScoreboard();
                    }, 500);
                } else {
                    unflipCards();

                    setTimeout(() => {
                        nextPlayer();
                    }, 1500);
                }
            }
    
            

When the flip class is added to two cards, checkForMatch is the function that tests to see if the dataset on each matches the other. If it does, then the paired match stays flipped and turns the color of the player to whom it is awarded. Those cards then get disabled from further game play, and the score is updated. If the cards are not a match, the flip class is removed from them, and they are turned back over to remain in play. The first setTimeout here allows the scoreboard to be updated before highlighting the next player. The second timeout is to allow the cards to finish flipping back over when it's not a match before changing players.

Alternate turns

When a match is not made, then it's the other player's turn.

              
            <div id="scoreboard" class="row player-panel">
            <div class="col player-0-panel active">
                <div class="player-name" id="player-0">BLUE PLAYER</div>
                <div class="player-score" id="score-0">0</div>
            </div>
            <div class="col">
                <button id="new-game">NEW GAME</button>
            </div>
            <div class="col player-1-panel">
                <div class="player-name" id="player-1">RED PLAYER</div>
                <div class="player-score" id="score-1">0</div>
            </div>
            </div>
        
            

In my HTML file, I included a div for each player. One of the player divs has a class of active added by default.

              
            function nextPlayer() {
                if (activePlayer === 0) {
                    activePlayer = 1;
                    document.querySelector('.player-0-panel').classList.toggle('active');
                    document.querySelector('.player-1-panel').classList.toggle('active');
                } else {
                    activePlayer = 0;
                    document.querySelector('.player-0-panel').classList.toggle('active');
                    document.querySelector('.player-1-panel').classList.toggle('active');
                }
            }
        
            

If the player has made a match, they get another turn. This is done by leaving them in active status with the active class. If they do not get a match, then the nextPlayer function is called, and the active class toggles to the other player. The active player is highlighted on the scoreboard at top of the game.

Calculate a winner

When the cards have all been flipped over, a winner must be declared.

              
            function checkForWin() {
                if (scores[0] + scores[1] == 8) {
                    // The Timeout function here is to allow the cards to finish
                    // flipping and updating color before showing the winner.
                    setTimeout(() => {
                        updateWinner();
                    }, 1000);
                }
            }

            function updateWinner() {
                if (scores[0] > scores[1]) {
                    document.getElementById('scoreboard').style.display = 'none';
                    document.getElementById('winner-alert').style.display = 'block';
                    document.getElementById('winner-alert').style.color = '#044497';
                    document.getElementById('winner-alert').textContent = 'BLUE WINS!';
                } else if (scores[0] < scores[1]) {
                    document.getElementById('scoreboard').style.display = 'none';
                    document.getElementById('winner-alert').style.display = 'block';
                    document.getElementById('winner-alert').style.color = 'red';
                    document.getElementById('winner-alert').textContent = 'RED WINS!';
                } else {
                    document.getElementById('scoreboard').style.display = 'none';
                    document.getElementById('winner-alert').style.display = 'block';
                    document.getElementById('winner-alert').textContent = 'TIE GAME, TRY AGAIN!';
                }

                setTimeout(() => {
                    resetGame();
                }, 1500);
            }
        
            

Each time there is a match, the checkForWin function is called. After all 8 pairs of cards have been matched, then a winner is calculated. Whichever player has a greater number of matches wins. The timeout shown in checkForWin allows the last card to finish flipping over and being assigned its color before declaring the winner. When the updateWinner function runs, it changes the style and text content of the winning player's scoreboard to show who won, or that it's a tie. The timeout there is to allow this message to display briefly before resetting the board for a new game.

Start a new game

The players need an ability to start a new game, or to have one load automatically when their current game is complete.

              
            function resetGame() {
                secondFlip = false;
                lockBoard = false;
                firstCard = null;
                secondCard = null;
                scores = [0,0];
                activePlayer = 0;

                cards.forEach(card => card.addEventListener('click', flipCard));
                cards.forEach(card => card.classList.remove('flip'));
                
                cards.forEach(card => card.classList.remove('blue'));
                cards.forEach(card => card.classList.remove('red'));

                lockBoard = true;

                setTimeout(() => {
                    shuffleCards();
                    lockBoard = false;
                }, 1000);

                document.getElementById('score-0').textContent = '0';
                document.getElementById('score-1').textContent = '0';
                document.getElementById('scoreboard').style.display = '';
                document.getElementById('winner-alert').style.display = 'none';
            }
        
            

When a player has won, or the "New Game" button has been clicked, the gameboard is reset. The scores are reset, and the added classes are removed from all cards. The timeout here allows the cards to finish flipping over before shuffling begins.

Timeouts

In order to make the game play more smoothly, I did have to add a timer to a lot of the functions included, as you can see above. I felt this was one of the fun challenges. Adding these in definitely improved the user experience of the game. Many were issues that I never anticpated having until I ran into them during game play. Some of the timers added are:

  • Displaying the cards for a second before flipping them back over if they aren't a match.
  • Allow the cards to finish flipping back over before changing players.
  • Allow the second card to diplay before flipping back over.
  • Allow the scoreboard to update before changing the display around the active player.
  • Allow the cards to finish shuffling before being clickable.
  • Allow the card to display the icon when clicked before assigning it a color if it's a match.
  • Allow the card to finish updating match color before showing the winner.

This was a fun exercise for me. I ran into some roadblocks that were a little tricky, but it gave me a good opportunity to learn some new tricks.

Please note, this game is best viewed in a desktop setting.