Task

Completing the GHW Games challenge - Create a Memory Card Game, and Build a JavaScript Game

What it does

A user can play memory card game on a browser.

Node Dependencies

  • react
  • react-dom
  • react-scripts

How we built it

I've created it on codesandbox.io, using the react.js template. Firstly, import the fontawesome kit script in the /public/index.html as,

<script src="https://kit.fontawesome.com/6f42fc440c.js" crossorigin="anonymous" ></script>

Then in /src/App.js, add class names of 9 icons

const cardIcons = [
  "fa-brands fa-facebook",
  "fa-brands fa-instagram",
  "fa-brands fa-twitter",
  "fa-brands fa-x-twitter",
  "fa-brands fa-twitch",
  "fa-brands fa-github",
  "fa-brands fa-gitlab",
  "fa-brands fa-whatsapp",
  "fa-brands fa-linkedin",
];

Now shuffle the array and generate a array of objects from it with data such as isFlipped and isMatched.

const generateCards = () => {
  const shuffledIcons = shuffle(cardIcons.concat(cardIcons));
  return shuffledIcons.map((icon, index) => ({
    icon,
    isFlipped: false,
    isMatched: false,
    id: index,
  }));
};

function shuffle(array) {
  for (let i = array.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [array[i], array[j]] = [array[j], array[i]];
  }
  return array;
}

Create a few state variables such as:

  • cards: to store the generated cards data
  • flippedCards: to store which cards are flipped
  • score: to store and update score
  • timer: to store and run timer
import React, { useState, useEffect } from "react";
import "./styles.css";
// Rest of the code
function App() {
  const [cards, setCards] = useState(generateCards());
  const [flippedCards, setFlippedCards] = useState([]);
  const [matchedCards, setMatchedCards] = useState([]);
  const [score, setScore] = useState(0);
  const [timer, setTimer] = useState(90);
}

Place the title, score, timer, cards and reset button on screen. There are 18 cards and will be placed as 6x3.

  return (
    <div className="App">
      <h3>Memory Card Game</h3>
      <div className="details">
        <div className="score">Score: {score}</div>
        <div className="timer">Time Left: {timer} seconds</div>
      </div>
      <div className="board">
        {cards.map((card) => (
          <div
            key={card.id}
            className={`card ${
              card.isFlipped || matchedCards.includes(card.id) ? "flipped" : ""
            }`}
            onClick={() => handleCardClick(card.id)}
          >
            <div className="card-display">
              {!card.isFlipped ? (
                <div className="back">Flip Me</div>
              ) : (
                <div className="front">
                  <i className={card.icon}></i>
                </div>
              )}
            </div>
          </div>
        ))}
      </div>
      <button className="reset-btn" onClick={resetGame}>
        Reset Game
      </button>
    </div>
  );
body {
  background-color: rgb(29, 29, 29);
}

.App {
  text-align: center;
  font-family: Arial, sans-serif;
}

h3,
.details {
  color: white;
}

.details {
  width: 40%;
  margin: 0 auto;
  display: flex;
  justify-content: space-between;
}

.board {
  display: grid;
  width: 50%;
  grid-template-columns: repeat(6, 1fr);
  gap: 30px;
  margin: 20px auto 0;
}

.card {
  width: 100px;
  height: 150px;
}

.card div {
  width: 100%;
  height: 100%;
  border-radius: 10px;
  background-color: rgb(100, 149, 237);
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 24px;
  color: white;
  cursor: pointer;
  transition: background-color 0.3s ease;
  position: relative;
}

.card:hover div {
  background-color: rgb(65, 105, 225);
}

.card .back {
  position: absolute;
}

.card .front {
  position: absolute;
}

.reset-btn {
  width: 150px;
  height: 50px;
  margin: 50px 0;
  border: none;
  border-radius: 5px;
  background-color: rgb(65, 105, 225);
  color: white;
  cursor: pointer;
  font-size: 18px;
}

.reset-btn:hover {
  color: rgb(65, 105, 225);
  background-color: transparent;
}

When a card is clicked flip them, keep them flipped if length of flippedCards < 2.

    const handleCardClick = (id) => {
        if (
          !flippedCards.includes(id) &&
          flippedCards.length < 2 &&
          !matchedCards.includes(id)
        ) {
          setFlippedCards([...flippedCards, id]);
          setCards((prevCards) =>
            prevCards.map((card) =>
              card.id === id ? { ...card, isFlipped: true } : card
            )
          );
        }
  };

Once length of flippedCards is equals 2, check if both are matching. If matched then store them in matchedCards.

    const checkForMatch = () => {
        const [firstCard, secondCard] = flippedCards;
        if (cards[firstCard].icon === cards[secondCard].icon) {
          setMatchedCards([...matchedCards, firstCard, secondCard]);
          setScore(score + 1);
        } else {
          setCards((prevCards) =>
            prevCards.map((card) =>
              flippedCards.includes(card.id) ? { ...card, isFlipped: false } : card
            )
          );
        }
        setFlippedCards([]);
    };

If flippedCards length equals 2 and they are not matching then flip them back.

useEffect(() => {
    if (flippedCards.length === 2) {
      setTimeout(() => checkForMatch(), 1000);
    }
  }, [flippedCards]);

Handle timer, and end game once time is 0.

useEffect(() => {
    if (timer === 0) {
      endGame();
    } else {
      const timerInterval = setInterval(() => {
        setTimer(timer - 1);
      }, 1000);
      return () => clearInterval(timerInterval);
    }
  }, [timer]);

  const endGame = () => {
    alert("Game Over! Your final score is: " + score);
    resetGame();
  };

Add reset game function

const resetGame = () => {
    setCards(generateCards());
    setFlippedCards([]);
    setMatchedCards([]);
    setScore(0);
    setTimer(90);
  };

Full code:

// App.js
import React, { useState, useEffect } from "react";
import "./styles.css";

const cardIcons = [
  "fa-brands fa-facebook",
  "fa-brands fa-instagram",
  "fa-brands fa-twitter",
  "fa-brands fa-x-twitter",
  "fa-brands fa-twitch",
  "fa-brands fa-github",
  "fa-brands fa-gitlab",
  "fa-brands fa-whatsapp",
  "fa-brands fa-linkedin",
];

const generateCards = () => {
  const shuffledIcons = shuffle(cardIcons.concat(cardIcons));
  return shuffledIcons.map((icon, index) => ({
    icon,
    isFlipped: false,
    isMatched: false,
    id: index,
  }));
};

function shuffle(array) {
  for (let i = array.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [array[i], array[j]] = [array[j], array[i]];
  }
  return array;
}

function App() {
  const [cards, setCards] = useState(generateCards());
  const [flippedCards, setFlippedCards] = useState([]);
  const [matchedCards, setMatchedCards] = useState([]);
  const [score, setScore] = useState(0);
  const [timer, setTimer] = useState(90);

  useEffect(() => {
    if (flippedCards.length === 2) {
      setTimeout(() => checkForMatch(), 1000);
    }
  }, [flippedCards]);

  useEffect(() => {
    if (timer === 0) {
      endGame();
    } else {
      const timerInterval = setInterval(() => {
        setTimer(timer - 1);
      }, 1000);
      return () => clearInterval(timerInterval);
    }
  }, [timer]);

  const handleCardClick = (id) => {
    if (
      !flippedCards.includes(id) &&
      flippedCards.length < 2 &&
      !matchedCards.includes(id)
    ) {
      setFlippedCards([...flippedCards, id]);
      setCards((prevCards) =>
        prevCards.map((card) =>
          card.id === id ? { ...card, isFlipped: true } : card
        )
      );
    }
  };

  const checkForMatch = () => {
    const [firstCard, secondCard] = flippedCards;
    if (cards[firstCard].icon === cards[secondCard].icon) {
      setMatchedCards([...matchedCards, firstCard, secondCard]);
      setScore(score + 1);
    } else {
      setCards((prevCards) =>
        prevCards.map((card) =>
          flippedCards.includes(card.id) ? { ...card, isFlipped: false } : card
        )
      );
    }
    setFlippedCards([]);
  };

  const resetGame = () => {
    setCards(generateCards());
    setFlippedCards([]);
    setMatchedCards([]);
    setScore(0);
    setTimer(90);
  };

  const endGame = () => {
    alert("Game Over! Your final score is: " + score);
    resetGame();
  };

  return (
    <div className="App">
      <h3>Memory Card Game</h3>
      <div className="details">
        <div className="score">Score: {score}</div>
        <div className="timer">Time Left: {timer} seconds</div>
      </div>
      <div className="board">
        {cards.map((card) => (
          <div
            key={card.id}
            className={`card ${
              card.isFlipped || matchedCards.includes(card.id) ? "flipped" : ""
            }`}
            onClick={() => handleCardClick(card.id)}
          >
            <div className="card-display">
              {!card.isFlipped ? (
                <div className="back">Flip Me</div>
              ) : (
                <div className="front">
                  <i className={card.icon}></i>
                </div>
              )}
            </div>
          </div>
        ))}
      </div>
      <button className="reset-btn" onClick={resetGame}>
        Reset Game
      </button>
    </div>
  );
}

export default App;
/* styles.css */
body {
  background-color: rgb(29, 29, 29);
}

.App {
  text-align: center;
  font-family: Arial, sans-serif;
}

h3,
.details {
  color: white;
}

.details {
  width: 40%;
  margin: 0 auto;
  display: flex;
  justify-content: space-between;
}

.board {
  display: grid;
  width: 50%;
  grid-template-columns: repeat(6, 1fr);
  gap: 30px;
  margin: 20px auto 0;
}

.card {
  width: 100px;
  height: 150px;
}

.card div {
  width: 100%;
  height: 100%;
  border-radius: 10px;
  background-color: rgb(100, 149, 237);
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 24px;
  color: white;
  cursor: pointer;
  transition: background-color 0.3s ease;
  position: relative;
}

.card:hover div {
  background-color: rgb(65, 105, 225);
}

.card .back {
  position: absolute;
}

.card .front {
  position: absolute;
}

.reset-btn {
  width: 150px;
  height: 50px;
  margin: 50px 0;
  border: none;
  border-radius: 5px;
  background-color: rgb(65, 105, 225);
  color: white;
  cursor: pointer;
  font-size: 18px;
}

.reset-btn:hover {
  color: rgb(65, 105, 225);
  background-color: transparent;
}

including the remaining react files such as /public/index.html, /src/index.js, and package.json.

Share this project:

Updates