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 dataflippedCards: to store which cards are flippedscore: to store and update scoretimer: 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.
Log in or sign up for Devpost to join the conversation.