How to create a memory game in NEXT JS ?

thumbnail

Hello friends, Today in this post we are going to see how we can create a memory game using NEXT JS. Memory Game is an excellent project for beginners to get started with learning NEXT/REACT JS, doing this project will help you learn basic concepts such as lifecycle methods, useEffect & useState hooks, functional component, etc. A basic understanding of HTML, CSS & Javascript is enough to follow up with this tutorial. So get ready with your vscode and let’s get started.

Step1 — Create a NEXT JS Project

Open vscode, click on terminal > new terminal or plus Ctrl + J and type below command

> npx create-next-app memory-game

By the time it gets created let's look at what our final result would be like.

https://codewithmarish.com/products/memory-game

Step2 — Initialization

Once the project will be created, clean up some default-created codes delete the Home.module.css under styles, under pages open index.js, remove everything under the return statement and add only an empty div inside it. We will initialize a constant variable board containing data that we show up on the board, and boardData to maintain values of the board, flippedCards to list the cards which are flipped, matchedCards to maintain the state of the cards which are matched, moves & gameOver initialised to 0.

import { useState, useEffect } from "react";
const board = ["🤖", "👽", "👻", "🤡", "🐧", "🦚", "😄", "🚀"];
export default function Home() {
  const [boardData, setBoardData] = useState([]);
  const [flippedCards, setFlippedCards] = useState([]);
  const [matchedCards, setMatchedCards] = useState([]);
  const [moves, setMoves] = useState(0);
  const [gameOver, setGameOver] = useState(false);
  
  return <div>
     
  </div>

}

Step3 — Writing JSX

Now under that div we will add a menu div with the number of moves, Reset button & Gameover state, and after that, we have a 4 x4 grid container which has cards in it. Each card will have a front & back.

For cards, we will create an empty array with the length of 16 (for 4 x 4 = 16), and use the map function to iterate over it and display cards.

...
return (
<div className="container">
      <div className="menu">
        <p>{`Moves - ${moves}`}</p>
      </div>

      <div className="board">
        {boardData.map((data, i) => {
          return (
            <div
              onClick={() => {
                
              }}
              key={i}
              className={`card`}
            >
              <div className="card-front">{data}</div>
              <div className="card-back"></div>
            </div>
          );
        })}
      </div>
      <div className="menu">
        <p>{`GameOver - ${gameOver}`}</p>
        <button onClick={() => {}} className="reset-btn">
          Reset
        </button>
      </div>
    </div>
)
...

For styles, we will add flex with space between for menu, for the board we use a display grid. For card front & back, we use absolute position backface visibility to hidden to hide the card when the card is rotated and other styles for flipped and found.

* {
  margin:0;
  padding:0;
  box-sizing: border-box;
}
body {
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
}
.board {
  display: grid;
  grid-template-columns: repeat(4, 75px);
  grid-gap: 8px;
}
.card {
  border-radius: 4px;
  text-align: center;
  height: 75px;
  font-size: 36px;
  font-weight: bold;
  position: relative;
  transform-style: preserve-3d;
  transition: all 0.2s;
  user-select: none;
}
.card.gameover {
  pointer-events: none;
}
.card-0 {
  transform: rotateY(0deg);
}
.card.active {
  transform: rotateY(180deg);
}
.card.matched .card-front {
  background-color: lightgoldenrodyellow;
  color: white;
}
.card-front,
.card-back {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  backface-visibility: hidden;
  height: 100%;
  border-radius: 50%;
}
.card-front {
  transform: rotateY(180deg);
  background-color: #eee;
  line-height: 70px;
  vertical-align: middle;
  text-align: center;
  font-size: 50px;
  z-index: 2;
}
.card-back {
  background-color: #ddd;

  transform: rotateY(0);
  z-index: 1;
}
.card-back::after {
  position: absolute;
  content: "";
  top: 0;
  left: 0;
  width: 75%;
  height: 75%;
  border-radius: 50%;
}
.menu {
  display: flex;
  justify-content: space-between;
}

.menu p {
  font-size: 20px;
  font-weight: 600;
}

Step4 — Functionality

We will have a function to shuffle to boardData on the mount, for that we will use the sort function and for comparison we use Math.random() — 0.5 for random sorting and map it to new variable shuffled Cards which we will then set to boardData. Since we need pair of data so we will use the static board values twice using the spread operator.

.... 
const shuffle = () => {
    const shuffledCards = [...board, ...board]
      .sort(() => Math.random() - 0.5)
      .map((v) => v);

    setBoardData(shuffledCards);
  };
...

Also create a initialize function, which will be used on the component mount as well as gameOver/ reset, inside this function, we will call shuffle function, gameOver to false, flippedCards & matchedCards to empty list, and moves to 0.

...
const initialize = () => {
    shuffle();
    setGameOver(false);
    setFlippedCards([]);
    setMatchedCards([]);
    setMoves(0);
  };
...

Now we need a function to update cards state to active and other functionalities when a card is clicked, first condition is to check whether the clicked card is in flippedCards list. Inside that, we have following conditions before updating the state.

  1. if flippedCards contains only 1 data then we check the value at the current index and check whether the first value is equal to the current value then we will set matchedCards to the index of the first and current value.
  2. If flippedCards contains 2 data then simply assign current index to flippedCards.
  3. If flippedCards is empty then we are appending current index to the list.

In the end, we are incrementing the moves by 1. Now, we call this function when a card is clicked.

...
const updateActiveCards = (i) => {
    if (!flippedCards.includes(i)) {
      if (flippedCards.length == 1) {
        const firstIdx = flippedCards[0];
        const secondIdx = i;
        if (boardData[firstIdx] == boardData[secondIdx]) {
          setMatchedCards((prev) => [...prev, firstIdx, secondIdx]);
        }

        setFlippedCards([...flippedCards, i]);
      } else if (flippedCards.length == 2) {
        setFlippedCards([i]);
      } else {
        setFlippedCards([...flippedCards, i]);
      }

      setMoves((v) => v + 1);
    }
  };
...

We will have an useEffect to check matchedCards length whether it is equal to total boardData length which is 16 here, then we need to setGameOver to true and we will check this on every update on moves, so we will add moves variable as a dependency for the useEffect. And another useEffect to call initialize function which will be called only on mount with dependency as a empty bracket.

...
useEffect(() => {
    if (matchedCards.length == 16) {
      setGameOver(true);
    }
  }, [moves]);
useEffect(() => {
    initialize();
  }, []);

...

Now our functionalities are ready we will use it in our map function where we iterated over the boardData and return cards.

We will add CSS class to the card by checking flippedCards contains index which we get from map function which we used to iterate on boardData to return cards, similarly check for matchedCards also contains index and add this class to card div.

Finally,call the initialize function when reset button is clicked and show whether gameover state in a paragraph tag.

...
return (
    <div className="container">
      <div className="menu">
        <p>{`Moves - ${moves}`}</p>
      </div>

      <div className="board">
        {boardData.map((data, i) => {
          const flipped = flippedCards.includes(i) ? true : false;
          const matched = matchedCards.includes(i) ? true : false;
          return (
            <div
              onClick={() => {
                updateActiveCards(i);
              }}
              key={i}
              className={`card ${flipped || matched ? "active" : ""} ${
                matched ? "matched" : ""
              } ${gameOver ? "gameover" : ""}`}
            >
              <div className="card-front">{data}</div>
              <div className="card-back"></div>
            </div>
          );
        })}
      </div>
      <div className="menu">
        <p>{`GameOver - ${gameOver}`}</p>
        <button onClick={() => initialize()} className="reset-btn">
          Reset
        </button>
      </div>
    </div>
  );
...

Now you can run your app using ` npm run dev ` and the final result would look similar to this

https://codewithmarish.com/products/memory-game

Thanks for reading this post, if you found this post helpful please share maximum, Thanks for reading 😊 Stay tuned.

You can refer to these official React docs to learn more about hooks

Introducing Hooks – React

You can find the code repository on Github

Memory Game | GIthub

If you are facing any issues please contact us from our contact section.

Contact Us | CodeWithMarish

Also please don’t forget to subscribe to our youtube channel codewithmarish for all web development-related challenges.

Code With Marish | Youtube

Related Posts