Codementor Events

How to Build a Card Match Up Game using React

Published Dec 02, 2018Last updated May 30, 2019

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:

  1. components folder: This folder will have added two folders. They are:
    a. cardfolder: Contains the Card and GameOver JSX files
    b. header folder: Contains the Header JSX file.
  2. stylesfolder: Where we will put our custom CSS file.

We also need to delete these files in the srcfolder.

  1. App.css
  2. index.css
  3. 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.jsfiles and remove the lines where we are importing logo.svg, App.css, and index.css.

Setup the Application Files

  1. In the src > components > card folder, create Card.jsx and GameOver.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;
  1. In the src > components > header folder, createHeader.jsx file. Type the below code snippet inside of the Header.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;
  1. Replace the contents of App.js in the src 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;
  1. Finally, in the src > styles folder, create main.cssfile. 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 startand 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 isFlippedstate 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

Discover and read more posts from Kene Nnamani
get started
post comments2Replies
TheITWebCare
6 years ago

Great, sample how to implement card matching game with ReactJS, ~TheITWebCare

Ebuka Umeh
6 years ago

Awesome, the article is pretty detailed