DEV Community

loading...
Cover image for Build a Card Memory Game with React

Build a Card Memory Game with React

Shubham Khatri
Passionate about Javascript, React, and Web Development | Active Stackoverflow contributor | Follow on Twitter for more web development content
Originally published at javascript.plainenglish.io Updated on ・5 min read

Very often we come across small games and wonder how complex is it? Can we build it? More often than not we do not go beyond it. In this post however we will build a simple memory game which is easy to play and also easy to develop.

The card memory game is a simple game to test the player’s memory. In a deck of paired cards, the player needs to select a matching pair in consecutive turns. The player wins the game when all matching pairs are selected.

A simple UI of it may look like this:

UI reference for our final demo

Let us define the Rules of the Game

We can’t make a game unless we know the rules. So lets state them here:

  • We need a shuffled set of cards. There must be a pair of each card in our deck.

  • The game must flip the cards clicked by the player. Maximum of two cards will show at a time.

  • The game will handle matched and unmatched cards. Unmatched cards are flipped back after a short duration. Matched cards are removed from the deck.

  • Every time a player selects a pair, the game will increment the current move count

  • Once all pairs are found out, players sees a confirmation dialog with the score.

  • Game provides a functionality to restart.

So what are we waiting for... Lets get into it.

We first define our card structure. For a card we create an object with the type attribute and an image source.

{
   type: 'Pickachu',
   image: require('../images/Pickachu.png')
}
Enter fullscreen mode Exit fullscreen mode

Now the next step is to shuffle the deck of cards. Ahh yes, this is the most important step. It is not really a memory game if we don’t shuffle

1. Shuffle

I will use Fisher-Yates shuffle algorithm for shuffling an array of cards.

// Fisher Yates Shuffle
function swap(array, i, j) {
   const temp = array[i];
   array[i] = array[j];
   array[j] = temp;
}
function shuffleCards(array) {
   const length = array.length;
   for (let i = length; i > 0; i--) {
      const randomIndex = Math.floor(Math.random() * i);
      const currentIndex = i - 1;
      swap(array, currIndex, randomIndex)
   }
   return array;
}
Enter fullscreen mode Exit fullscreen mode

2. Render board for the deck of cards

In this example we are using 12 cards(6 pairs). After shuffling our cards, we render them as a grid of 3x4. You can either choose to split your card deck into 3 arrays of 4 items each and render using a nested map or use CSS flexbox or grid. I will be using CSS Grid to render it since it is easier to handle updates with a one dimension array.


export default function App({ uniqueCardsArray }) {
  const [cards, setCards] = useState(
    () => shuffleCards(uniqueCardsArray.concat(uniqueCardsArray))
  );

  const handleCardClick = (index) => {
    // We will handle it later
  };


  return (
    <div className="App">
      <header>
        <h3>Play the Flip card game</h3>
        <div>
          Select two cards with same content consequtively to make them vanish
        </div>
      </header>
      <div className="container">
        {cards.map((card, index) => {
          return (
            <Card
              key={index}
              card={card}
              index={index}
              onClick={handleCardClick}
            />
          );
        })}
      </div>
   </div>
  )
}
Enter fullscreen mode Exit fullscreen mode
  .container {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    grid-template-rows: repeat(3, 1fr);
    justify-items: center;
    align-items: stretch;
    gap: 1rem;
  }
Enter fullscreen mode Exit fullscreen mode

3. Flip Cards, evaluate match and count moves

The next step is to provide an interaction for the user to flip cards and evaluate if there is a match. For it we maintain the following states

  • openCards to track the cards that have been flipped by the player

  • clearedCards to track the cards that have matched and need to be removed from the deck

  • moves to keep track of the moves made by the player.

import { useEffect, useState, useRef } from "react";
import Card from "./card";
import uniqueElementsArray from './data';
import "./app.scss";

export default function App() {
  const [cards, setCards] = useState(
    () => shuffleCards(uniqueCardsArray.concat(uniqueCardsArray))
  );
  const [openCards, setOpenCards] = useState([]);
  const [clearedCards, setClearedCards] = useState({});
  const [moves, setMoves] = useState(0);
  const [showModal, setShowModal] = useState(false);
  const timeout = useRef(null);

  // Check if both the cards have same type. If they do, mark them inactive
  const evaluate = () => {
    const [first, second] = openCards;
    if (cards[first].type === cards[second].type) {
      setClearedCards((prev) => ({ ...prev, [cards[first].type]: true }));
      setOpenCards([]);
      return;
    }
    // Flip cards after a 500ms duration
    timeout.current = setTimeout(() => {
      setOpenCards([]);
    }, 500);
  };

  const handleCardClick = (index) => {
    // Have a maximum of 2 items in array at once.
    if (openCards.length === 1) {
      setOpenCards((prev) => [...prev, index]);
      // increase the moves once we opened a pair
      setMoves((moves) => moves + 1);
    } else {
      // If two cards are already open, we cancel timeout set for flipping cards back
      clearTimeout(timeout.current);
      setOpenCards([index]);
    }
  };

  useEffect(() => {
    if (openCards.length === 2) {
      setTimeout(evaluate, 500);
    }
  }, [openCards]);

  const checkIsFlipped = (index) => {
    return openCards.includes(index);
  };

  const checkIsInactive = (card) => {
    return Boolean(clearedCards[card.type]);
  };

  return (
    <div className="App">
      <header>
        <h3>Play the Flip card game</h3>
        <div>
          Select two cards with same content consequtively to make them vanish
        </div>
      </header>
      <div className="container">
        {cards.map((card, index) => {
          return (
            <Card
              key={index}
              card={card}
              index={index}
              isDisabled={shouldDisableAllCards}
              isInactive={checkIsInactive(card)}
              isFlipped={checkIsFlipped(index)}
              onClick={handleCardClick}
            />
          );
        })}
      </div>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

At a time we shall only keep a maximum of two cards in openCards state. Since we have a static array and we aren’t actually deleting anything from our original cards array we can just store the index of the opened card in openCards state. Based on openCards and clearedCards state we pass a prop isFlipped or isInactive respectively to our Card component which it will then use to add the respective class.

Do look at this wonderful blog which explains how to handle Flip Card Animation.
Note: Since we add an animation to our cards for flipping, we evaluate a match after few seconds to allow for the flip transition.

4. Check for game completion

Every time we evaluate for a match, we check if all pairs have been found. If yes, we show the player a completion modal.

  const checkCompletion = () => {
    // We are storing clearedCards as an object since its more efficient 
    //to search in an object instead of an array
    if (Object.keys(clearedCards).length === uniqueCardsArray.length) {
      setShowModal(true);
    }
  };
Enter fullscreen mode Exit fullscreen mode

5. And finally, our restart functionality

Well restarting is simple, we just reset our states and reshuffle our cards.

<Button onClick={handleRestart} color="primary" variant="contained">
    Restart
</Button>
Enter fullscreen mode Exit fullscreen mode
  const handleRestart = () => {
    setClearedCards({});
    setOpenCards([]);
    setShowModal(false);
    setMoves(0);
    // set a shuffled deck of cards
    setCards(shuffleCards(uniqueCardsArray.concat(uniqueCardsArray)));
  };

Enter fullscreen mode Exit fullscreen mode

Hurray! There we have our basic memory card game.

You can find the CodeSandbox Playground for the demo below

Conclusion

I am so glad we’ve reached this point. We created a shuffled deck, rendered it on a board, added a flip functionality and evaluated for a matching pair. We can extend this example to add a timer, add best score of the player and support level for higher numbers of cards as well.

You can check this Github repository for the full code.

If you liked this post do share it with your friends and if you have any suggestions or feedbacks feel free to add comments or DM me on Twitter.

Thank you for Reading

Discussion (8)

Collapse
pengeszikra profile image
Peter Vivo • Edited

Functional shuffle array

const shuffle = () => Math.random() > 0.5 ? 1 : -1;

const shuffledCards = cards.sort(shuffle);
Enter fullscreen mode Exit fullscreen mode
Collapse
cswalker21 profile image
cswalker21

Interesting. It’s certainly elegant, and definitely suitable for many applications, including this memory game. I’m not schooled up enough on combinatorics to know at first blush if this method produces an unbiased distribution compared to the algorithm mentioned in the article.

Collapse
crisarji profile image
crisarji

Love it!, I'll post one in Angular with Observables, do you mind whether I use these images?

Collapse
shubhamreacts profile image
Shubham Khatri Author • Edited

Absolutely not! You can use them freely 😁

Collapse
negreanucalin profile image
Negreanu Calin

Nice :)

Collapse
beautifulcoup profile image
beautifulcoup • Edited

the images in github seem to be corrupted also when I replace it with other images with the same name, it doesnt work.

Collapse
shubhamreacts profile image
Shubham Khatri Author

Yes, there is an issue rendering image in the github repository. I will fix it soon.

Collapse
Sloan, the sloth mascot
Comment deleted