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.
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:
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
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:
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.
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>
);
}
}
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>
);
}
}
When we are done compiling our Board.js code, we will be able to hide/reveal square objects:
And display a game won alert if all kittens have been found (flagged):
Or display a game over alert if a dog has been clicked on (detonated):
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.
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
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;
Our Main.js will then render our game individually.
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:
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;
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
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!😊