Open-Source Internship opportunity by OpenGenus for programmers. Apply now.
You might have guessed from the title already what we will discuss in this article. We will build a minesweeper game using React.JS. This is article-cum-tutorial.
MineSweeper was a classic game that we used to play in old days on old pc. This game is so uncertain to win. One who doesn't know about the game.
Let me tell you, the game is about revealing all cell , without clicking on the mines location cells.
- Left click for revealing the cell.
- Right Click to flag the cell(that you don't want to reveal, you are guessing, it might have mines).
- You will win if non-mines reveal become zero.
This is the example ,what you will have at the end of this article .
Let's Begin
We will divide the game into three components
1.App Component(The main component will render the MineSweeper Board)
2.Board Component(Sort of all functionalities will be defined here)
3.Cell Component(handling Click and reveal on each cell of the board)
We will start with installing react app using create-react-app
Open up the terminal and go to the folder wherever you want to code
npx create-react-app minesweeper-react
This will create a minesweeper-react folder in your desired directories. This folder will have an initial project structure and transitive dependencies.
minesweeper-react
βββ README.md
βββ node_modules
βββ package.json
βββ .gitignore
βββ public
β βββ favicon.ico
β βββ index.html
β βββ manifest.json
βββ src
βββ App.css
βββ App.js
βββ App.test.js
βββ index.css
βββ index.js
βββ logo.svg
βββ serviceWorker.js
βββ setupTests.js
Once the installation is done, run the cd minesweeper-react
.
We will do all work inside the src folder.
Make components folder into the src folder.
components folder will contain two files Board.js & Cell.js
Create two files inside utils folder for directly importing the function like createBoard and revealed.
|__src
|__components
|__Board.js
|__Cell.js
|__utils
|__CreateBoard.js
|__Reveal.js
|__App.js
|__App.css
First Step toward writing code.
In the App.js write this .
import React from 'react';
import Board from './components/Board';
import './App.css';
function App() {
return (
<div >
<Board/>
</div>
);
}
export default App;
Inside utils/CreateBoard.js will create util function for board and minelocation.
export default function CreateBoard(row, col, bombs){
// Board for storing the values for each cell
let board = [];
// Tracking the minelocation
let mineLocation = [];
// Create blank board
for (let x = 0; x < row; x++) {
let subCol = [];
for (let y = 0; y < col; y++) {
subCol.push({
value: 0,
revealed: false,
x: x,
y: y,
flagged: false,
});
}
board.push(subCol);
}
// Randomize Bomb Placement
let bombsCount = 0;
while (bombsCount < bombs) {
// Implementing random function
let x = random(0, row - 1);
let y = random(0, col - 1);
// placing bomb at random location(x,y) on board[x][y]
if (board[x][y].value === 0) {
board[x][y].value = "X";
mineLocation.push([x, y]);
bombsCount++;
}
}
// Increasing the value of specific cell
// If the cell has mines increasing the cell value by 1.
// Add Numbers
for (let i = 0; i < row; i++) {
for (let j = 0; j < col; j++) {
if (board[i][j].value === "X") {
continue;
}
// Top
if (i > 0 && board[i - 1][j].value === "X") {
board[i][j].value++;
}
// Top Right
if (
i > 0 &&
j < col - 1 &&
board[i - 1][j + 1].value === "X"
) {
board[i][j].value++;
}
// Right
if (j < col - 1 && board[i][j + 1].value === "X") {
board[i][j].value++;
}
// Botoom Right
if (
i < row - 1 &&
j < col - 1 &&
board[i + 1][j + 1].value === "X"
) {
board[i][j].value++;
}
// Bottom
if (i < row - 1 && board[i + 1][j].value === "X") {
board[i][j].value++;
}
// Bottom Left
if (
i < row - 1 &&
j > 0 &&
board[i + 1][j - 1].value === "X"
) {
board[i][j].value++;
}
// LEft
if (j > 0 && board[i][j - 1].value === "X") {
board[i][j].value++;
}
// Top Left
if (i > 0 && j > 0 && board[i - 1][j - 1].value === "X") {
board[i][j].value++;
}
}
}
return { board, mineLocation };
};
// Random function used for generating random value of x & y
function random(min = 0, max) {
// min and max included
return Math.floor(Math.random() * (max - min + 1) + min);
}
So let's see, what is going on above.
(i) function CreateBoard has three parameters row, col, and bombs that we will pass after some time for creating the board and setting the values of bombs or mines.
(ii)For creating a simple board, we have created a 2-D array. Each cell will have two types of values X for mines and 0,1,2,3...8 numbers for telling the mines around each cell.Flagged and revealed are boolean values for putting flag using right-click & revealed for revealing the cell one by one on click we will this see later.
(iii)At the start, all cell values are initialized 0. All bombs/mines have been placed at a random location.
(iv)Each cell has 8 neighbors (or adjacent cell) so will calculate all mine locations around that cell using condition and increase the value at that position. (See the boundary condition carefully).
Using the CreateBoard function inside the components/Board.js. We'll start first by rendering the simple board with cells.
Create a react functional component inside Board.js.
import React,{useState,useEffect} from "react";
import CreateBoard from '../utils/CreateBoard';
function Board() {
const [grid,setGrid]=useState([]);
const style={
display : 'flex',
flexDirection : 'row',
}
useEffect(()=>{
function freshBoard(){
const newBoard=CreateBoard(10,10,20);
setGrid(newBoard);
}
freshBoard();
},[]);
return (
<div className="parent">
{grid.map(singlerow=>{
return (
<div style={style}>
{singlerow.map(singlecol=>{
return <div
style={{
width : 30,
height : 30,
padding : '5px' ,
border : '3px solid red'}}
>
{JSON.stringify(singlecol.value)}
</div>
})}
</div>
)
})}
</div>
)
}
export default Board;
}
- So above, we have created the Board as a react functional component. We have useState and useEffect react hook.
- This Board.js will be imported into App.js file and App component will render Board.
useEffect react hook used during the lifecycle of react component. - Everytime, Board component mount ,we are creating new Board by calling createBoard function from ../utils/CreateBoard.js inside useEffect and passing the values of row=10,col=10 and minesvalues=20.
- We are storing the state value in grid using useState react hook. Then we are setting up the each cell using map from the javascript. You will see some of the values associated with each cell on the browser screen.
Now, create the Cell Component for handling the click event on a specific cell and revealing the value in that.
components/Cell.js
import React from 'react'
export default function Cell({details,updateFlag}) {
const style={
cellStyle:{
width:40,height:40,backgroundColor:'grey',border:'1px solid white',
display:'flex',
justifyContent:'center',
alignItems:'center',
fontSize:'20px',
},
}
const handleClick=()=>{
console.log(details);
}
return (
<div style={style.cellStyle} onClick={handleClick} onContextMenu={(e)=>updateFlag(e)}>
{details.value}
</div>
)
}
and in components/Board.js
const updateFlag=(e)=>{
e.preventDefault();
console.log("Right Click");
}
return (
<div className="parent">
{grid.map(singlerow=>{
return (
<div style={style}>
{singlerow.map(singlecol=>{
return <Cell details={singlecol} updateFlag={updateFlag}/>
})}
</div>
)
})}
</div>
)
}
export default Board;
- Above we are rendering the cell passing the details and updateFlag function as props.
- for left click we have onClick in Cell component for handling the right click we have onContextMenu.
Now we will work on revealing each cell.
In components/Board.js we will pass revealCell function as third prop in Cell component.
Making Changes in updateFlag function, whenever the right-click occurs update the flagged value of the grid state.
Revealcell will check, you have clicked on non-mine or mine cell.
const updateFlag=(e,x,y)=>{
e.preventDefault();
// deep copy of the object
let newGrid=JSON.parse(JSON.stringify(grid));
newGrid[x][y].flagged=true;
console.log(newGrid[x][y]);
setGrid(newGrid);
}
const revealcell=(x,y)=>{
let newGrid=JSON.parse(JSON.stringify(grid));
if(newGrid[x][y].value==="X"){
alert("clicked on mine");
}
else{
newGrid[x][y].revealed=true;
setGrid(newGrid);
}
-
Now here as you can see we are making the newGrid with the deep copy means both the newGrid and grid will have different memory reference and on changing newGrid value, Grid value will not be changed.
-
In components/Cell.js we do these changes accordingly.
import React from 'react'
export default function Cell({details,updateFlag,revealcell}) {
const style={
cellStyle:{
width:40,height:40,backgroundColor:'#bdc3c7',border:'1px solid white',
display:'flex',
justifyContent:'center',
alignItems:'center',
fontSize:'20px',
cursor:'pointer',
},
}
return (
<div style={style.cellStyle} onClick={()=>{revealcell(details.x,details.y)}} onContextMenu={(e)=>updateFlag(e,details.x,details.y)}>
{details.revealed ? details.value : ""}
</div>
)
}
Now we will see another util function, we will use for revealing the mines and revealing cells when we click on zero. On clicking zero it will reveal all non-zero mines adjacent to that cells.
export const revealed=(arr,x,y,newNonMines)=>{
// all the cells which are adjaced to zero must be stored in the array
// so that it can be revealed later
let show=[];
show.push(arr[x][y]);
while(show.length!=0){
let one=show.pop();
let i=one.x;
let j=one.y;
if(!one.revealed){
newNonMines--;
one.revealed=true;
}
if(one.value !==0){
break;
}
// top left
if(
i>0 &&
j>0 &&
arr[i-1][j-1].value==0 &&
!arr[i-1][j-1].revealed
)
{
show.push(arr[i-1][j-1]);
}
// bottom right
if(
i<arr.length-1 &&
j<arr[0].length-1 &&
arr[i+1][j+1].value===0 &&
!arr[i+1][j+1].revealed
){
show.push(arr[i+1][j+1]);
}
// top right
if(
i>0 &&
j<arr[0].length-1 &&
arr[i-1][j+1].value===0 &&
!arr[i-1][j+1].revealed
){
show.push(arr[i-1][j+1]);
}
// bottom left
if(
i<arr.length-1 &&
j>0 &&
arr[i+1][j-1].value===0 &&
!arr[i+1][j-1].revealed
){
show.push(arr[i+1][j-1]);
}
// top
if(
i>0 &&
arr[i-1][j].value===0 &&
!arr[i-1][j].revealed
){
show.push(arr[i-1][j]);
}
// right
if(
j<arr[0].length-1 &&
arr[i][j+1].value===0 &&
!arr[i][j+1].revealed
){
show.push(arr[i][j+1]);
}
// bottom
if(
i<arr.length-1 &&
arr[i+1][j].value===0 &&
!arr[i+1][j].revealed
){
show.push(arr[i+1][j]);
}
// left
if(
j>0 &&
arr[i][j-1].value===0 &&
!arr[i][j-1].revealed
){
show.push(arr[i][j-1]);
}
// start revealing the item
if (
i > 0 &&
j > 0 &&
!arr[i - 1][j - 1].revealed
) {
//Top Left Reveal
arr[i - 1][j - 1].revealed = true;
newNonMines--;
}
if (j > 0 && !arr[i][j - 1].revealed) {
// Left Reveal
arr[i][j - 1].revealed = true;
newNonMines--;
}
if (
i < arr.length - 1 &&
j > 0 &&
!arr[i + 1][j - 1].revealed
) {
//Bottom Left Reveal
arr[i + 1][j - 1].revealed = true;
newNonMines--;
}
if (i > 0 && !arr[i - 1][j].revealed) {
//Top Reveal
arr[i - 1][j].revealed = true;
newNonMines--;
}
if (i < arr.length - 1 && !arr[i + 1][j].revealed) {
// Bottom Reveal
arr[i + 1][j].revealed = true;
newNonMines--;
}
if (
i > 0 &&
j < arr[0].length - 1 &&
!arr[i - 1][j + 1].revealed
) {
// Top Right Reveal
arr[i - 1][j + 1].revealed = true;
newNonMines--;
}
if (j < arr[0].length - 1 && !arr[i][j + 1].revealed) {
//Right Reveal
arr[i][j + 1].revealed = true;
newNonMines--;
}
if (
i < arr.length - 1 &&
j < arr[0].length - 1 &&
!arr[i + 1][j + 1].revealed
) {
// Bottom Right Reveal
arr[i + 1][j + 1].revealed = true;
newNonMines--;
}
}
return {arr,newNonMines}
}
Above we are revealing all zero value cells and it's adjacent.
Changing the revealCell function in components/Board.js
function Board() {
const [grid,setGrid]=useState([]);
const [nonMinecount,setNonMinecount]=useState(0);
const [mineLocation,setmineLocation]=useState([]);
useEffect(()=>{
const freshBoard = () => {
const newBoard=CreateBoard(10,10,20);
setNonMinecount(10*10-20);
// console.log(newBoard.mineLocation);
setmineLocation(newBoard.mineLocation);
setGrid(newBoard.board);
}
freshBoard();
},[]);
const updateFlag=(e,x,y)=>{
e.preventDefault();
// deep copy of the object
let newGrid=JSON.parse(JSON.stringify(grid));
newGrid[x][y].flagged=true;
console.log(newGrid[x][y]);
setGrid(newGrid);
}
//revealing all cells and the minelocation with all mines when clicked on mines
const revealcell=(x,y)=>{
let newGrid=JSON.parse(JSON.stringify(grid));
if(newGrid[x][y].value==="X"){
alert("you clicked mine")
for(let i=0;i<mineLocation.length;i++){
newGrid[mineLocation[i][0]][mineLocation[i][1]].revealed=true;
}
setGrid(newGrid);
}
else{
let revealedboard=revealed(newGrid,x,y,nonMinecount);
setGrid(revealedboard.arr);
setNonMinecount(revealedboard.newNonMines);
}
}
return (
<div className="parent">
<div style={{color:"white",textAlign:"center",fontSize:"35px"}}>Non-Mines : {nonMinecount}</div>
<div>
{grid.map((singlerow,index1)=>{
return (
<div style={style} key={index1}>
{singlerow.map((singlecol,index2)=>{
return <Cell details={singlecol} key={index2} updateFlag={updateFlag} revealcell={revealcell}/>
})}
</div>
)
})}
</div>
</div>
)
}
export default Board;
we have used two more states for managing the mineLocation and nonMines count.
This is the main core of the Minesweeper, Now what remaining, you can customize the styling and adding the sound on click (using the use-sound hook or something else) as I have used in my project
You can see the source code of this game:
Source Code on GitHub