DEV Community

Cover image for Let's Make Catsweeper, a Minesweeper Game with React and Cats!
Christine Coomans
Christine Coomans

Posted on

Let's Make Catsweeper, a Minesweeper Game with React and Cats!

With a busy schedule, assignments, and little time, it feels like there's just not been enough minutes in the day to make mini Reactjs projects. With a little break, I decided that I should take the time to make a simple game just to keep myself on track. Thus, CATSWEEPER was born!πŸ±πŸ’£πŸ±

What is Catsweeper? Well, it's a play on the classic Minesweeper game. If you've ever played Minesweeper before, it is a game that contains mini squares with hidden mines or "bombs". You have to clear the board containing hidden flags without detonating any of the bombs. If you click on a square that reveals or detonates a mine, it is game over.

Minesweeper

Easy enough, right? With Catsweeper, it works the same, but instead of avoiding bombs, you are tasked to run from apartment to apartment to find fifteen lost kitties (flags) - but you have to avoid the angry neighborhood dogs (mines) or else you will lose your kitties forever (ie. you get detonated, game over).

Here is a little preview of what we will be building today:

catsweeper

Now, I would like to suggest that you code along with me. After all, it is best to type in the code yourself than to copy it because it builds up that muscle memory. When you're ready, let's get started - future React Master! πŸ˜‰

All explanations for the project are in the code itself via the comments, but if you get stuck or want to download my CSS file, or even images, check it out on my GitHub Repository.

Pre Setup - Installing Packages

To complete this project as I did, you will need to do the following in your preferred command line at a comfortable location on your computer:

npx create-react-app minesweeper
npm i bootstrap react-bootstrap sweetalert --save
cd minesweeper
Enter fullscreen mode Exit fullscreen mode

If you want to look more into the libraries used for this project, please refer below:

  • React-Bootstrap: We will need this for our introduction modal.
  • Sweetalert: SweetAlert makes popup messages easy and pretty. We need it for our win/lose notifications.

Now you can go ahead and open up your newly created project in your favorite code editor. πŸ‘Ύ

Step 1 - Initial Setup

Now that we are in our project folder, it is time to set up our project frame.

In your ./src folder, modify it to meet the following file structure:

minesweeper file structure

Step 2 - Square.js

Our Square Component will render our board grid. Simply put, it will compile the "squares" that we usually see in Minesweeper-like games. We will assign our cats, dogs, and hidden(default) doors to individual randomized squares on the grid.

We will need our images for this, so make sure you have some, or replace it with numbers, letters or even emoji's. To render the squares properly, we need to add some CSS as well, so remember to copy my CSS from the GitHub repository above.

squares grid

In your Square.js file, make the following changes:

//necessary dependencies
import React from 'react';
//our cat, dog and basic door images
import Fail from "../assets/fail.png";
import Win from "../assets/win.png";
import Misc from "../assets/main.png";

//main Square component that will render our "apartment" cells
export default class Square extends React.Component {
    //will get the value of the cell upon state changes
    getValue(){    
        //if the cell is revealed and a cat, then a cat will be shown
        if (!this.props.value.isRevealed){
            return this.props.value.isCat ? <img src={Win} alt=""/> : null;
        }
        //if the cell is revealed and a dog, then a dog will be shown
        if (this.props.value.isDog) {
            return  <img src={Fail} alt="" className="image-loss"/>
        }
        //if the cell is revealed as a default door and is not a dog, then the cats closest to it will be revealed
        if(this.props.value.neighbour){
            return  <img src={Win} alt="" className="image-win"/>;
        }
        //by default a closed door will be shown
        if(this.props.value.neighbour === 0 ){
            return <img src={Misc} alt="" className="image-misc" />;
        }
        //return this.props.value.neighbour;
    }

    render(){
        //changes the className of each square upon randomisation of cats, doors, or default
        let className = "square" + (this.props.value.isRevealed ? " " : " hidden") + (this.props.value.isDog ? "is-dog" : " ") + (this.props.value.isCat ? "is-cat" : " ");
        //render the squares with data as props
        return (
            <div ref="square" onClick={this.props.onClick} className={className}>
                {this.getValue()}
            </div>
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3 - Board.js

Now that we have our standard grid set up, we will turn to our Board.js file. Now, the Board.js will contain all of our game functionality, and it can get a little complex. In this component file, we will add the ability of our game to randomize dogs, cats and default doors each round, by traversing across the board. When the objects have been assigned to each square, we will then keep track of and reveal the objects and game status using event handlers.

In your Board.js add the following:

//necessary dependencies
import React from 'react';
import Square from './Square';
//our popup for when a player wins/loses
import swal from 'sweetalert';

//will compile our main board and game functionalities
export default class Board extends React.Component {
    state = {
        //sets the initial state of our board (the height, width, and num of dogs will be passed to it in Game.js)
        boardSettings: this.initBoardSettings(this.props.height, this.props.width, this.props.dogs),
        //sets the initial state of our Game Status as undefined (not won/loss)
        gameWon: false,
        //number of dogs
        dogCount: this.props.dogs,
    };

    // This function will allow us to get the dog data from our squares
    getDogs(data) {
        //initial array of squares
        let squareArray = [];
        //map over our array of squares to push dogs to it
        data.map(datarow => {
            datarow.map((dataitem) => {
                if (dataitem.isDog) {
                    squareArray.push(dataitem);
                } 
                //explicit return statement
                return "";
            });
            //explicit return statement
            return "";
        });
        //returns our dogs in our squares
        return squareArray;
    }

    // This function will allow us to get the cat data from our squares
    getCats(data) {
        //initial array of squares
        let squareArray = [];
        //map over our array of squares to push cats to it
        data.map(datarow => {
            datarow.map((dataitem) => {
                if (dataitem.isCat) {
                    squareArray.push(dataitem);
                } 
                //explicit return statement
                return "";
            }); 
            //explicit return statement
            return "";
        });
        //returns our cats in our squares
        return squareArray;
    }


    // This function will allow us to get the default door data from our squares
    getHidden(data) {
        //initial array of squares
        let squareArray = [];
        //map over our array of squares to push doors to it
        data.map(datarow => {
            datarow.map((dataitem) => {
                if (!dataitem.isRevealed) {
                    squareArray.push(dataitem);
                }
                 //explicit return statement
                 return "";
                }); 
            //explicit return statement
            return "";
        });
        //returns our cats in our squares
        return squareArray;
    }

    //This function will generate a random number that we can assign to each square (to randomise placement of cats and dogs)
    getRandomNumber(dimension) {
        return Math.floor((Math.random() * 1000) + 1) % dimension;
    }

    // This function gets the initial board settings, where everything will be reverted to hidden
    initBoardSettings(height, width, dogs) {
        //data is undefined array to be reused again
        let data = [];
        //will map through height(y) 
        for (let i = 0; i < height; i++) {
            //and push our data values to it
            data.push([]);
            //will map through width(x) 
            for (let j = 0; j < width; j++) {
                //and hide everything at first (we make a clean board)
                data[i][j] = {
                    x: i,
                    y: j,
                    isDog: false,
                    neighbour: 0,
                    isRevealed: false,
                    isEmpty: false,
                    isCat: false,
                };
            }
        }
        //will add dogs and doors to our board when defined
        data = this.addDogs(data, height, width, dogs);
        data = this.getNeighbours(data, height, width);
        return data;
    }

    // This function will place actual dogs on our empty board
    addDogs(data, height, width, dogs) {
        //for each x or y value, we will have no dogs initially
        let valx, valy, dogsAdded = 0;
        //while our dogsAdded (0) are less than our dogs (10)
        while (dogsAdded < dogs) {
            //randomise their position on our x and y positions on the board
            valx = this.getRandomNumber(width);
            valy = this.getRandomNumber(height);
            //and add them until 10 squares are filles 
            if (!(data[valx][valy].isDog)) {
                data[valx][valy].isDog = true;
                dogsAdded++;
            }
        }
        //render this on our board array
        return (data);
    }

    // Gets the number of default doors on our empty board
    getNeighbours(data, height, width) {
        let updatedData = data;

        //will loop through board records to add values randomly
        for (let i = 0; i < height; i++) {
            for (let j = 0; j < width; j++) {
                //if there is no dog
                if (data[i][j].isDog !== true) {
                    let dog = 0;
                    //will find areas on the squares to add new dogs
                    const area = this.traverseBoard(data[i][j].x, data[i][j].y, data);
                    //move across the board in a randomised motion to add dogs
                    area.map(value => {
                        if (value.isDog) {
                            dog++;
                        }
                        //explicit return statement
                        return "";
                    });
                    if (dog === 0) {
                        updatedData[i][j].isEmpty = true;
                    }
                    updatedData[i][j].neighbour = dog;
                }
            }
        }
        //return board with added dogs
        return (updatedData);
    };

    // Looks across squares to find dogs
    traverseBoard(x, y, data) {
        //initial postition of traverse is null
        const pos = [];
        //traverse up
        if (x > 0) {
            pos.push(data[x - 1][y]);
        }
        //traverse down
        if (x < this.props.height - 1) {
            pos.push(data[x + 1][y]);
        }
        //traverse left
        if (y > 0) {
            pos.push(data[x][y - 1]);
        }
        //traverse right
        if (y < this.props.width - 1) {
            pos.push(data[x][y + 1]);
        }
        //traverse top left
        if (x > 0 && y > 0) {
            pos.push(data[x - 1][y - 1]);
        }
        //traverse top right
        if (x > 0 && y < this.props.width - 1) {
            pos.push(data[x - 1][y + 1]);
        }
        //traverse bottom right
        if (x < this.props.height - 1 && y < this.props.width - 1) {
            pos.push(data[x + 1][y + 1]);
        }
        //traverse bottom left
        if (x < this.props.height - 1 && y > 0) {
            pos.push(data[x + 1][y - 1]);
        }
        return pos;
    }

    // Function will reveal the whole board
    revealBoard() {
        //render the updated data in the new board
        let updatedData = this.state.boardSettings;
        //reveal new data items
        updatedData.map((datarow) => {
            datarow.map((dataitem) => {
                dataitem.isRevealed = true; 
                //explicit return statement
                return "";
            }); 
            //explicit return statement
            return "";
        });
        //update the state of the board from initial state to current state
        this.setState({
            boardSettings: updatedData
        })
    }

    // Function will help us identify empty squares
    revealEmpty(x, y, data) {
        //will look across the board 
        let area = this.traverseBoard(x, y, data);
        //and map to find where positions have not yet been revealed/taken
        area.map(value => {
            if (!value.isRevealed && (value.isEmpty || !value.isDog)) {
                data[value.x][value.y].isRevealed = true;
                if (value.isEmpty) {
                    //reveal empty squares
                    this.revealEmpty(value.x, value.y, data);
                }
            }
            //explicit return statement
            return "";
        });
        return data;
    }

    //Function to enable click events for winning/losing states
    handleCellClick(x, y) {
        let win = false;
        // check if revealed. return if true.
        if (this.state.boardSettings[x][y].isRevealed) return null;
        // Alert for when a player clicks on a dog to display game over
        if (this.state.boardSettings[x][y].isDog) {
            this.revealBoard();
            swal("Oopsie, we opened a door and a dog chased away all the kittens! It seems that in our defeat, the dog left us a present. What do you want to do? πŸ™€", {  
                title: "GAME OVER!",
                buttons: {
                    quit: {
                    text: "Retry",
                    value: "quit",
                    className: "retry-btn"
                    },
                    finish: {
                    text: "Accept Prize? 🎁",
                    value: "finish",
                    className: "retry-btn"
                    }
                },
                })
                .then((value) => {
                switch (value) {
                    case "quit":
                    window.location.reload();         
                    break;

                    case "finish":
                    window.location = "https://youtu.be/gu3KzCWoons";         
                    break;

                    default:
                    swal("Let's Catch More Kittens!");
                }
            });
        }
        //updates game state and displays losing alert
        let updatedData = this.state.boardSettings;
        updatedData[x][y].isCat = false;
        updatedData[x][y].isRevealed = true;
        // Alert for when a player clicks on door to display empty square
        if (updatedData[x][y].isEmpty) {
            updatedData = this.revealEmpty(x, y, updatedData);
        }
        // Alert for when a player clicks on all the cats to display game won
        if (this.getHidden(updatedData).length === this.props.dogs) {
            win = true;
            this.revealBoard();
            swal("Yay, we found all the kittens! Now Ms. Crumblebottom can't yell at me. Here's a little thank you.", {  
                title: "GAME WON!",
                buttons: {
                    quit: {
                        text: "Quit Game",
                        value: "quit",
                        className: "retry-btn"
                    },
                    finish: {
                    text: "Accept Prize",
                    value: "finish",
                    className: "retry-btn"
                    }
                },
                })
                .then((value) => {
                switch (value) {
                    case "quit":
                    window.location.reload();         
                    break;

                    case "finish":
                    window.location = "https://youtu.be/QH2-TGUlwu4";         
                    break;

                    default:
                    swal("Let's Catch More Kittens!");
                }
            });
        }
        //updates game state and displays winning alert
        this.setState({
            boardSettings: updatedData,
            dogCount: this.props.dogs - this.getCats(updatedData).length,
            gameWon: win,
        });
    }

    //renders our final board to play the game on
    renderBoard(data) {
        //will map over Squares to return data items and event handlers for each square
        return data.map((datarow) => {
            return datarow.map((dataitem) => {
                return (
                <div key={dataitem.x * datarow.length + dataitem.y}>
                    <Square onClick={() => this.handleCellClick(dataitem.x, dataitem.y)} value={dataitem}/>
                    {(datarow[datarow.length - 1] === dataitem) ? <div className="clear" /> : ""}
                </div>);
            })
        });
    }

    // Component method to pass in predefined props
    componentWillReceiveProps(nextProps) {
        if (JSON.stringify(this.props) !== JSON.stringify(nextProps)) {
            this.setState({
                boardSettings: this.initBoardSettings(nextProps.height, nextProps.width, nextProps.dogs),
                gameWon: false,
                dogCount: nextProps.dogs,
            });
        }
    }

    // Complete Board rendering 
    render() {
        return (
            <div className="board">
                {this.renderBoard(this.state.boardSettings)}
            </div>
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

When we are done compiling our Board.js code, we will be able to hide/reveal square objects:
square objects

And display a game won alert if all kittens have been found (flagged):
image

Or display a game over alert if a dog has been clicked on (detonated):
image

Step 4 - Game.js

With our grid creation and functionality out of the way, we can now pass our game props to our board and assign the number of squares we want, and how many dogs (mines) we want to be randomized. For this game, we will create a 5x5 grid with ten dogs. A 5x5 grid will compile 25 squares with 10 dogs and 15 kittens/default doors.

grid example

In your Game.js file do the following:

//necessary dependencies
import React from 'react';
import Board from './Board';

//our main game component will tie everything together
class Game extends React.Component {

    //initial state of our squares on the board (ie. it will be a 5x5 board with 10 dogs)
    state = {
        //will give us 25 squares, with 10 dogs and 15 cats
        height: 5,
        width: 5,
        dogs: 10,
    };

    render() {
        //render the state of our hard coded values
        const { height, width, dogs } = this.state;
        //will render our fullly functional game board
        return (
            <div className="game">
                <div className="game-board">
                    {/*will pass in the height, width and num of dogs as props*/}
                    <Board height={height} width={width} dogs={dogs} />
                </div>
            </div>
        );
    }
}

//exports for use in other files
export default Game
Enter fullscreen mode Exit fullscreen mode

Step 5 - Main.js

We can separate our game from our modal by rendering our Game component in our Main.js file. I did this because I wanted the modal to act as our "main page" without introducing some of the time-consuming complexities of react-routing. You can eliminate this step if you just want to render the game, or opt for routing instead.

In your Main.js file do the following:

import Game from './Game';
import React from 'react';

//We separate the Main component from the App component so that our Game can be rendered separately from the modal.
function Main() {
  return (
    <div className="Main">
        <Game/>
    </div>
  );
}

//exports for use in other files
export default Main;
Enter fullscreen mode Exit fullscreen mode

Our Main.js will then render our game individually.

image

Step 6 - App.js

We can now create our modal which will act as our "introductory screen". As said in the previous step, you can skip this if you want. Our modal should look similar to this:

image

In your App.js file do the following:

//necessary packages for our game
import React, { useState } from 'react';
import { Modal, Button } from 'react-bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import Main from './components/Main';

//main React component
function App() {
  //initial state of our modal is true, so when the page loads it will pop up
  const [show, setShow] = useState(true);
  //will close the modal upon button click
  const handleClose = () => setShow(false);
  //will show our main modal, and render the game upon modal confirm
  return (
    <> 
    {/*renders the modal*/}
      <Modal show={show} onHide={handleClose} modalTransition={{ timeout: 2000 }} centered className="modal">
        <div className="modal-main">
        <Modal.Header closeButton className="modal-header">
          <Modal.Title className="modal-title" >Oh No, I've Lost My Kitties!</Modal.Title>
        </Modal.Header >
        <Modal.Body className="modal-body" >
          <p>Can you please help me? I was petsitting Grumpy Ms. Crumblebottom's cats when a dog came in and chased them away. 
          </p> 
          <p> 
          I think they are scattered accross the apartment building, but we have to be careful of the dogs or else the cats will be gone forever! Please help me find them!
          </p>
          <div className="rules">
          <h5>Game Rules</h5>
          <ul>
            <li>The game works similar to minesweeper.</li>
            <li>Click on an apartment door to find a kitty.</li>
            <li>Each door will either have a kitty or a doggo.</li>
            <li>Each game outcome will give you a prize.</li>
            <li>If you find a doggo, it's GAME OVER!</li>
            <li>If you find all fifteen kitties, you WIN!</li>
          </ul>
          </div>
          </Modal.Body>
          <Modal.Footer className="modal-footer">
          <Button variant="secondary" onClick={handleClose} className="btn modal-btn">
            Okay, Let's Find The Kittens
          </Button>
        </Modal.Footer>
        </div>
      </Modal>
      {/*renders the game*/}
      <Main/>
    </>
  );
}

//exports it for use in other files
export default App;
Enter fullscreen mode Exit fullscreen mode

Step 7 - Final Touches

Now that we have created all the components, and added the necessary CSS styling, it is time to test our application. I do this frequently during project creation to test my code, but during this tutorial we only test it at the end - however you want to do it, is up to you! Run your project with the following command:

npm start
Enter fullscreen mode Exit fullscreen mode

Good job for reaching the end of this tutorial. When you're done, deploy your new project to GitHub and take a well-deserved break. Did you learn something new? What would you do different? Let me know in the comments down below!😊

Discussion (1)

Collapse
miguelcarrillo profile image
Miguel Carrillo

Love your react projects, kudos!