How to Build a Card Match Up Game using React
Introduction
React is a JavaScript library used for building user interfaces. It is a frontend library that is fast and easy to learn.
In this tutorial, we will be building a Card Match Up Game using React.
card-match-up game
Game Overview
The game comprises of 8 pairs of cards (i.e 16 cards). The cards are arranged randomly and are faced down. A user flips a card by clicking on it. If the two flipped cards are a match they will disappear from our game board otherwise they will be flipped back. The game ends when all cards are successfully matched with their pairs.
Prerequisite
- Node JS (v6 and above)
- NPM (v5.2 and above)
Let the coding begin.
To begin, we have to first install the packages that we require for building the application.
First, we install the create-react-app NPM package. This package enables us to quickly set up a ReactJs project with just one command.
In your terminal or command line, type:
npm i create-react-app
Now that we have the create-react-app package installed, we can set up our project.
npx create-react-app card-match-up
Open the card-match-up project folder with any text editor. I will be making use of VSCode Editor.
card-match-up project setup
Start the application with:
npm start
The application should be automatically opened in your browser on port 3000. The interface should be similar to this:
Default create-react-app page
We need to install another NPM package for the game.
npm install react-card-flip --save
ReactCardFlip allows us to use card flip animation. I find it easier to use rather than writing custom CSS animation for the card components.
Setup the Application Folder Structure
In the src
folder, create two folders:
components
folder: This folder will have added two folders. They are:
a.card
folder: Contains the Card and GameOver JSX files
b.header
folder: Contains the Header JSX file.styles
folder: Where we will put our custom CSS file.
We also need to delete these files in the src
folder.
- App.css
- index.css
- logo.svg
With the creation of the folders and the removal of some files, our folder structure should look like this:
the folder structure of the game
The application is currently broken because we are still referencing some files that we have deleted. To resolve this, open App.js
and index.js
files and remove the lines where we are importing logo.svg, App.css, and index.css
.
Setup the Application Files
- In the
src > components > card
folder, createCard.jsx
andGameOver.jsx
files.
Write the following code snippet in your created files. We will go over them shortly.
Card.jsx
import React from 'react';
import ReactCardFlip from "react-card-flip";
const Card = ({ id, isFlipped, handleClick, cardNumber }) => (
<ReactCardFlip isFlipped={isFlipped} flipSpeedBackToFront={1} flipSpeedFrontToBack={1} >
<button id={id} className={`card card-front ${cardNumber !== -1 ? "" : "hide-card"}`} onClick={handleClick} key="front">
</button>
<button id={id} className={`card card-back ${cardNumber !== -1 ? "" : "hide-card"}`} onClick={handleClick} key="back">
{ cardNumber }
</button>
</ReactCardFlip>
);
export default Card;
GameOver.jsx
import React from 'react';
const GameOver = ({ restartGame }) => (
<div className="justify-center">
<h1>Game Over!</h1>
<h3>If you enjoyed playing this game, follow me @iamkenec for more...</h3>
<button className="restart-button" onClick={restartGame}>Restart Game</button>
</div>
);
export default GameOver;
- In the
src > components > header
folder, createHeader.jsx
file. Type the below code snippet inside of theHeader.jsx
file.
Header.jsx
import React from 'react';
const Header = ({ restartGame }) => (
<div className="grid-header-container">
<div className="justify-left timer"></div>
<div className="justify-center game-status-text"></div>
<div className="justify-end">
<button onClick={restartGame} className="restart-button">Restart Game</button>
</div>
</div>
);
export default Header;
- Replace the contents of
App.js
in thesrc
folder with the code snippet below.
App.js
import React, { PureComponent } from 'react';
import Header from './components/header/Header';
import Card from './components/card/Card';
import GameOver from './components/card/GameOver';
import './styles/main.css';
class App extends PureComponent {
state = {
isFlipped: Array(16).fill(false),
shuffledCard: App.duplicateCard().sort(() => Math.random() - 0.5),
clickCount: 1,
prevSelectedCard: -1,
prevCardId: -1
};
static duplicateCard = () => {
return [0,1,2,3,4,5,6,7].reduce((preValue, current, index, array) => {
return preValue.concat([current, current])
},[]);
};
handleClick = event => {
event.preventDefault();
const cardId = event.target.id;
const newFlipps = this.state.isFlipped.slice();
this.setState({
prevSelectedCard: this.state.shuffledCard[cardId],
prevCardId: cardId
});
if (newFlipps[cardId] === false) {
newFlipps[cardId] = !newFlipps[cardId];
this.setState(prevState => ({
isFlipped: newFlipps,
clickCount: this.state.clickCount + 1
}));
if (this.state.clickCount === 2) {
this.setState({ clickCount: 1 });
const prevCardId = this.state.prevCardId;
const newCard = this.state.shuffledCard[cardId];
const previousCard = this.state.prevSelectedCard;
this.isCardMatch(previousCard, newCard, prevCardId, cardId);
}
}
};
isCardMatch = (card1, card2, card1Id, card2Id) => {
if (card1 === card2) {
const hideCard = this.state.shuffledCard.slice();
hideCard[card1Id] = -1;
hideCard[card2Id] = -1;
setTimeout(() => {
this.setState(prevState => ({
shuffledCard: hideCard
}))
}, 1000);
} else {
const flipBack = this.state.isFlipped.slice();
flipBack[card1Id] = false;
flipBack[card2Id] = false;
setTimeout(() => {
this.setState(prevState => ({ isFlipped: flipBack }));
}, 1000);
}
};
restartGame = () => {
this.setState({
isFlipped: Array(16).fill(false),
shuffledCard: App.duplicateCard().sort(() => Math.random() - 0.5),
clickCount: 1,
prevSelectedCard: -1,
prevCardId: -1
});
};
isGameOver = () => {
return this.state.isFlipped.every((element, index, array) => element !== false);
};
render() {
return (
<div>
<Header restartGame={this.restartGame} />
{ this.isGameOver() ? <GameOver restartGame={this.restartGame} /> :
<div className="grid-container">
{
this.state.shuffledCard.map((cardNumber, index) =>
<Card
key={index}
id={index}
cardNumber={cardNumber}
isFlipped={this.state.isFlipped[index]}
handleClick={this.handleClick}
/>
)
}
</div>
}
</div>
);
}
}
export default App;
- Finally, in the
src > styles
folder, createmain.css
file. Paste these styles inside of it.
main.css
body {
margin: 0;
}
.grid-container {
display: grid;
grid-gap: 1px;
grid-template-columns: auto auto auto auto;
padding: 10px 150px 10px 150px;
}
.grid-header-container {
display: grid;
grid-template-columns: auto auto auto;
background-color: #c9d1f5;
padding: 10px;
}
.grid-item {
border: 1px solid rgba(0, 0, 0, 0.8);
}
.justify-left {
text-align: left
}
.justify-end {
text-align: right;
}
.justify-center {
text-align: center;
}
.timer {
color: #5168e9;
font-size: 20px;
font-weight: bold;
}
.restart-button {
width: 15em;
height: 3em;
color: white;
background-color: #5168e9;
border: 0;
}
.game-status-text {
font-family: Verdana, Geneva, Tahoma, sans-serif;
color: rgb(24, 21, 16);
font-weight: bold;
}
.card {
width: 100%;
height: 10em;
background-color: rgb(75, 42, 165);
color: white;
font-size: 15px;
}
.hide-card {
visibility: hidden;
}
.card-front {
background-color: #999999;
box-shadow: 5px 5px 5px rgba(68, 68, 68, 0.6);
}
.card-back {
font-weight: bold;
}
.react-card-front {
position: unset !important;
}
Before we go over the lines of code that above, let us make sure that our application is working properly. Run the application with npm start
and try to play the game till the end.
Let us now have an in-depth look at the code.
Code Walk Through
App.js
This file contains the application logic. It also houses three other React components which are Header.jsx
, GameOver.jsx
and Card.jsx
.
App.js contains an internal state object and five different methods.
Line 18–22 contains the method for duplicating the array of card numbers. This is because each card number should have a duplicate. The duplicateCard
method will return an array of length 16 which is [0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7]. This array is randomised when it is passed to the state object shuffledCard.
duplicateCard method
static duplicateCard = () => {
return [0,1,2,3,4,5,6,7].reduce((preValue, current, index, array) => {
return preValue.concat([current, current])
},[]);
};
Line 24 — 49 contains the method for flipping card when it is clicked on to reveal the card number. The method changes the isFlipped
state of the card to true and prevents a card that is already flipped from responding to the click event. From line 40 , we check if the number of flipped cards is two so we can check if the two cards are a match.
handleClick method
handleClick = event => {
event.preventDefault();
const cardId = event.target.id;
const newFlipps = this.state.isFlipped.slice();
this.setState({
prevSelectedCard: this.state.shuffledCard[cardId],
prevCardId: cardId
});
if (newFlipps[cardId] === false) {
newFlipps[cardId] = !newFlipps[cardId];
this.setState(prevState => ({
isFlipped: newFlipps,
clickCount: this.state.clickCount + 1
}));
if (this.state.clickCount === 2) {
this.setState({ clickCount: 1 });
const prevCardId = this.state.prevCardId;
const newCard = this.state.shuffledCard[cardId];
const previousCard = this.state.prevSelectedCard;
this.isCardMatch(previousCard, newCard, prevCardId, cardId);
}
}
};
Line 51–69 is the method that checks if the two flipped cards are a match. This method is called in line 46 as seen above. The setTimeout
method used while setting the state is so that the card flip will not be abrupt.
isCardMatch method
isCardMatch = (card1, card2, card1Id, card2Id) => {
if (card1 === card2) {
const hideCard = this.state.shuffledCard.slice();
hideCard[card1Id] = -1;
hideCard[card2Id] = -1;
setTimeout(() => {
this.setState(prevState => ({
shuffledCard: hideCard
}))
}, 1000);
} else {
const flipBack = this.state.isFlipped.slice();
flipBack[card1Id] = false;
flipBack[card2Id] = false;
setTimeout(() => {
this.setState(prevState => ({ isFlipped: flipBack }));
}, 1000);
}
};
Line 71–79 is the restartGame
method. This method basically resets the game’s state.
restartGame method
restartGame = () => {
this.setState({
isFlipped: Array(16).fill(false),
shuffledCard: App.duplicateCard().sort(() => Math.random() - 0.5),
clickCount: 1,
prevSelectedCard: -1,
prevCardId: -1
});
};
Line 81–83 checks if the game is over. If the game is over, the GameOver
component is displayed.
isGameOver method
isGameOver = () => {
return this.state.isFlipped.every((element, index, array) => element !== false);
};
Line 85–106 The last block of code in App.js
is the render method. In Line 88, The Header
component is passed restartGame
props. The isGameOver
method is used to render the GameOver
component when the game is over otherwise, the Card
component is rendered.
render method
render() {
return (
<div>
<Header restartGame={this.restartGame} />
{ this.isGameOver() ? <GameOver restartGame={this.restartGame} /> :
<div className="grid-container">
{
this.state.shuffledCard.map((cardNumber, index) =>
<Card
key={index}
id={index}
cardNumber={cardNumber}
isFlipped={this.state.isFlipped[index]}
handleClick={this.handleClick}
/>
)
}
</div>
}
</div>
);
}
}
The Card.jsx
, GameOver.jsx
and Header.jsx
are all presentational components. They do not contain any application logic rather they contain props passed down to them from the App.js
parent component.
There we go! Our fun little game is done.
Thank you for reading. Clap if you had fun building the card game using React.
Follow me on twitter @iamkenec
Great, sample how to implement card matching game with ReactJS, ~TheITWebCare
Awesome, the article is pretty detailed