JS MineSweeper in RN

Jed Helmers
8 min readJan 11, 2020

MineSweeper’s the bomb. 3rd-party dependencies are not. Let us build this baby from scratch!

Let’s first create the MineSweeper class.

MineSweeper class

class MineSweeper {
constructor(N, M, bombCnt) {
this.N = N
this.M = M
this.totalBombs = bombCnt
this.bombCnt = this.totalBombs
this.grid = []
this.bombs = []
}
}

N is width, M is height and totalBombs is bombCnt. We also declare grid and bombs as empty arrays. bombCnt is decremented later during bomb creation so this value is initialized into variables (bombCnt, totalBombs).

Lets make a grid

We create a method called createGrid that well… creates a grid (M x N) of zeros.

class MineSweeper {
...
createGrid() {
for(let i = 0; i < this.M; i++) {
let row = []
for(let j = 0; j < this.N; j++) {
row.push(0)
}
this.grid.push(row)
}
}
}

Lets make some bombs

Next we make some bombs! Strategy: create the bomb list as a CSV list with a space separating the (x, y) points. I do it this way to utilize string functions to ensure we’re not duplicating bomb locations. BOOM! This is a recursive function.

class MineSweeper {
...
createBombs(x, y) {
if(this.bombCnt === 0) {
this.bombs = this.bombs
.split(" ")
.map(row => row
.split(",").map(n => parseInt(n)))
this.bombs.pop()
this.placeBombs()
} else if(this.bombCnt > 0 && !this.bombs.includes(`${x},${y}`)){
let x1 = Math.floor(Math.random() * Math.floor(this.M))
let y1 = Math.floor(Math.random() * Math.floor(this.N))
this.bombCnt--
this.bombs += (`${x},${y} `)
this.createBombs(x1, y1)
} else if(this.bombCnt > 0 && this.bombs.includes(`${x},${y}`)){
let x1 = Math.floor(Math.random() * Math.floor(this.M))
let y1 = Math.floor(Math.random() * Math.floor(this.N))
this.createBombs(x1, y1)
}
}
}

Planting Bombs

Lets plant some bombs. Now we’ll fat arrow through our bomb array placing a -1 on our grid

class MineSweeper {
...
placeBombs() {
this.bombs.forEach(b => this.grid[b[0]][b[1]] = -1)
}
}

SetUp

To keep stuff clean, create a setUp function that runs in the constructor

class MineSweeper {
...
setUp() {
this.createGrid()
this.createBombs(0, 0)
this.fillMap()
}
}

OK. Lets count stuff

In classic MineSweeper, the squares around a bomb show how many bombs touch it, so we’ll do that too.

this.circum lets us check all the squares around a single square

class MineSweeper {
constructor(N, M, bombCnt) {
this.N = N
this.M = M
this.totalBombs = bombCnt
this.bombCnt = this.totalBombs
this.grid = []
this.bombs = []
this.circum = [
[-1, -1], [-1, 0], [-1, 1],
[0, -1], [0, 1],
[1, -1], [1, 0], [1, 1]
]
this.setUp()
}
...
}

…followed by

class MineSweeper {
...
fillMap() {
for (let i = 0; i < this.bombs.length; i++) {
this.fillSpot(this.bombs[i][0], this.bombs[i][1])
}
}
fillSpot(x, y) {
for (let i = 0; i < this.circum.length; i++) {
let point = this.circum[i]
let x1 = x + point[0]
let y1 = y + point[1]
if (!!this.grid[x1] && this.grid[x1][y1] > -1) {
// Increment bomb count on grid square
this.grid[x1][y1]++
}
}
}
}

fillMap() loops through this.bombs and passes the indices of it to fillSpot(x, y). It then increments the squares that aren’t bombs and that fall within the boundaries of this.grid.

Print method

I do alot of dev work in console so having a handy print method come in handy. Use console.table! console.log is cool and all… but console.table is cooler.

class MineSweeper {
...
printTable() {
console.table(this.grid)
}
}

At this point you should see something like this:

WOOOOOO! We have bombs (-1). We have accurate counts. Now we need a click handler.

Click Handling

This is another recursive method. In the old MineSweeper, the field would open up when user clicks on a square where no bombs exist (I hope that made sense). Basically, if no bombs are around, then that square and every other square it touches AND the squares THOSE touch open up. See? Recursive.

class MineSweeper {
...

gameOver(x, y) {
console.log("BOOM!")
this.revealBoard()
this.grid[x][y] = "💥"
return "sorry"
}
youWin() {
console.log("YOU WIN!")
return "congrats"
}
clickHandler(x, y) {
if(this.grid[x][y] === -1) {
return this.gameOver(x, y)
} else if (this.grid[x][y] > 0) {
this.grid[x][y] = `${this.grid[x][y]}`
if(this.getSquareCount() === this.totalBombs){
return this.youWin()
}
} else if (this.grid[x][y] === 0) {
this.grid[x][y] = null
if(this.getSquareCount() === this.totalBombs){
return this.youWin()
}
if (x < this.M - 1) {
this.clickHandler(x + 1, y)
}
if (y < this.N - 1) {
this.clickHandler(x, y + 1)
}
if (x > 0) {
this.clickHandler(x - 1, y)
}
if (y > 0) {
this.clickHandler(x, y - 1)
}
}
}
flag(x, y) {
if(typeof this.grid[x][y] === "string"
&& this.grid[x][y].includes('f')){
this.grid[x][y] = parseInt(this.grid[x][y].slice(1))
} else {
this.grid[x][y] = `f${this.grid[x][y]}`
}
}
getSquareCount() {
remainingSquares = 0
for(let i = 0; i < this.M; i++) {
for(let j = 0; j < this.N; j++) {
if(typeof this.grid[i][j] === 'number') {
remainingSquares++
}
}
}
return remainingSquares
}
revealBoard() {
this.grid.forEach((r, i) => r.forEach((s, j) => {
if(s === -1){
this.grid[i][j] = "💣"
} else if(s === 0) {
this.grid[i][j] = null
} else {
this.grid[i][j] = `${this.grid[i][j]}`
}
}))
}
}

Walking through this IF:

  1. is it a bomb? then BOOM! Game over.
  2. is there a bomb close? then cast that value as a String. I love using var types to hold logic!
  3. compare remaining squares with original bombs count. If same then YOU WIN!
  4. if remaining squares and original bomb count are equivalent during the recursive bomb sniffing stuff then YOU WIN!
  5. else set this square to null and rerun this method in all directions.

The flag method is also included here. To preserve the bomb count when a flag is placed, I’m recasting it as a string and appending an f in front. The if statement toggles the flag on/off.

The getSquareCount method… well… gets the count of remaining squares. Its checking the var type of each square in this.grid.

This revealBoard method loops through this.grid and replaces -1 with a 💣 then recasts all remaining numbers as strings. This UI can grab onto this logic for styling and other logic.

You Win!

Notice how both gameOver and youWin both console log and return a value. Console log is just for testing this stuff in the inspector. The return value will be used in the React Native side. It’s basically just a way to plug into whatever framework you like.

Now for React Native

Since I wanted to focus more on a vanilla JS class and method creation, I won’t exhaustively walk through the React Native stuff.

import React from 'react';
import { StyleSheet, View, Text, TouchableOpacity, ScrollView } from 'react-native';
import { Header, LearnMoreLinks, Colors, DebugInstructions, ReloadInstructions } from 'react-native/Libraries/NewAppScreen';
import { MineSweeper } from './MineSweeper'class App extends React.Component {
constructor() {
super()
this.dimensions = [10, 20, 5]
this.board = new MineSweeper(this.dimensions[0], this.dimensions[1], this.dimensions[2])
this.state = {
grid: this.board.getGrid(),
count: 0,
timer: 0,
disabled: false
}
}
componentDidMount() {
// this.board.printTable()
this.timer = setInterval(() => { this.setState({ timer: ++this.state.timer })}, 1000)
}
componentWillUnmount() {
clearInterval(this.timer)
}
createNewBoard() {
this.board = new MineSweeper(this.dimensions[0], this.dimensions[1], this.dimensions[2])
this.setState({ grid: this.board.getGrid(), count: 0, timer: 0, disabled: false })
clearInterval(this.timer)
this.timer = setInterval(() => { this.setState({ timer: ++this.state.timer })}, 1000)
}
mineClickHandler = (i, j) => {
const { disabled } = this.state
if(this.board.clickHandler(i, j) === "sorry") {
clearInterval(this.timer)
this.setState({ disabled: true })
}
if(this.board.clickHandler(i, j) === "congrats") {
clearInterval(this.timer)
this.setState({ disabled: true })
alert("YOU WIN!")
}
console.log(this.board.clickHandler(i, j))
if(!disabled){
this.setState({ count: ++this.state.count })
this.setState({ grid: this.board.getGrid()})
}
}
flagClickHandler(i, j) {
const { disabled } = this.state
if(!disabled) {
this.flagSquare(i, j)
this.setState({ grid: this.board.getGrid()})
}
}
flagSquare(i, j) {
this.board.flag(i, j)
this.setState({ grid: this.board.getGrid()})
// this.setState({ count: this.board.countBombs()}, () => this.youWin())
}
render() {
const { grid, count, timer } = this.state
return (
<>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={{ flex: 1, backgroundColor: 'lightgray', padding: 10 }}>
<View style={styles.body}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
<View style={styles.block}>
<Text style={styles.flag}>{count}</Text>
</View>
<TouchableOpacity onPress={() => this.createNewBoard(20, 10)} style={styles.playAgain}>
<Text style={styles.text}>😀</Text>
</TouchableOpacity>
<View style={styles.block}>
<Text style={styles.flag}>{timer}</Text>
</View>
</View>
{grid.map((r, i) => <View key={i} style={{ flexDirection: 'row' }}>{r.map((b, j) => (
<TouchableOpacity
key={`${i}${j}`}
style={typeof grid[i][j] === 'number' || `${grid[i][j]}`.includes('f') ? styles.hide : styles.show}
onPress={() => this.mineClickHandler(i, j)}
onLongPress={() => this.flagClickHandler(i, j)}
enabled={typeof grid[i][j] !== 'number' || !`${grid[i][j]}`.includes('f')}
>
{
!!grid[i][j]
&& grid[i][j] !== 'null'
&& typeof grid[i][j] === 'string'
&& <Text style={[styles.text, !isNaN(parseInt(grid[i][j])) && styles[`num${grid[i][j]}`], grid[i][j] === '💣' && styles.bomb, grid[i][j].includes('f') && styles.flag ]}>
{grid[i][j].includes('f') ? '🏳' : grid[i][j]}
</Text>
}
</TouchableOpacity>
))}</View>
)}
</View>
</ScrollView>
</>
);
}
};
const styles = StyleSheet.create({
scrollView: {
backgroundColor: Colors.lighter,
flex: 1
},
body: {
flex: 1
},
hide: {
backgroundColor: 'gray',
height: 36,
borderWidth: 1,
borderColor: 'rgba(0, 0, 0, 0.25)',
flex: 1,
justifyContent: 'center'
},
show: {
backgroundColor: 'rgba(0, 0, 0, 0.1)',
height: 36,
borderWidth: 1,
borderColor: 'rgba(0, 0, 0, 0.25)',
flex: 1,
justifyContent: 'center'
},
text: {
textAlign: 'center',
fontWeight: '700',
fontSize: 20
},
num1: {
color: 'blue'
},
num2: {
color: 'green'
},
num3: {
color: 'red'
},
num4: {
color: 'purple'
},
num5: {
color: 'darkred'
},
num6: {
color: 'lightblue'
},
num7: {
color: '#0072cf'
},
num8: {
color: 'white'
},
bomb: {
color: 'black',
fontSize: 20
},
flag: {
color: 'white',
fontSize: 16,
textAlign: 'center',
},
playAgain: {
borderRadius: 4,
borderColor: 'rgba(0, 0, 0, .25)',
backgroundColor: 'rgba(255, 255, 255, .5)',
borderWidth: 1,
height: 35,
width: 80,
marginBottom: 5,
justifyContent: 'center'
},
block: {
height: 35,
backgroundColor: 'black',
width: 90,
justifyContent: 'center',
marginBottom: 5,
borderRadius: 2
}
});
export default App;

JS MineSweeper Class

export class MineSweeper {
constructor(N, M, bombCnt = 12) {
this.N = N
this.M = M
this.totalBombs = bombCnt
this.bombCnt = this.totalBombs
this.grid = []
this.bombs = []
this.circum = [
[-1, -1], [-1, 0], [-1, 1],
[0, -1], [0, 1],
[1, -1], [1, 0], [1, 1]
]
this.setUp()
}
createGrid() {
for(let i = 0; i < this.M; i++) {
let row = []
for(let j = 0; j < this.N; j++) {
row.push(0)
}
this.grid.push(row)
}
}
createBombs(x, y) {
if(this.bombCnt === 0) {
this.bombs = this.bombs
.split(" ")
.map(row => row
.split(",").map(n => parseInt(n)))
this.bombs.pop()
this.placeBombs()
} else if(this.bombCnt > 0 && !this.bombs.includes(`${x},${y}`)){
let x1 = Math.floor(Math.random() * Math.floor(this.M))
let y1 = Math.floor(Math.random() * Math.floor(this.N))
this.bombCnt--
this.bombs += (`${x},${y} `)
this.createBombs(x1, y1)
} else if(this.bombCnt > 0 && this.bombs.includes(`${x},${y}`)){
let x1 = Math.floor(Math.random() * Math.floor(this.M))
let y1 = Math.floor(Math.random() * Math.floor(this.N))
this.createBombs(x1, y1)
}
}
placeBombs() {
this.bombs.forEach(b => this.grid[b[0]][b[1]] = -1)
}
fillMap() {
for (let i = 0; i < this.bombs.length; i++) {
this.fillSpot(this.bombs[i][0], this.bombs[i][1])
}
}
fillSpot(x, y) {
for (let i = 0; i < this.circum.length; i++) {
let point = this.circum[i]
let x1 = x + point[0]
let y1 = y + point[1]
if (!!this.grid[x1] && this.grid[x1][y1] > -1) {
// Increment bomb count on grid square
this.grid[x1][y1]++
}
}
}
getSquareCount() {
remainingSquares = 0
for(let i = 0; i < this.M; i++) {
for(let j = 0; j < this.N; j++) {
if(typeof this.grid[i][j] === 'number') {
remainingSquares++
}
}
}
return remainingSquares
}
revealBoard() {
this.grid.forEach((r, i) => r.forEach((s, j) => {
if(s === -1){
this.grid[i][j] = "💣"
} else if(s === 0) {
this.grid[i][j] = null
} else {
this.grid[i][j] = `${this.grid[i][j]}`
}
}))
}
gameOver(x, y) {
console.log("BOOM!")
this.revealBoard()
this.grid[x][y] = "💥"
return "sorry"
}
youWin() {
console.log("YOU WIN!")
return "congrats"
}
clickHandler(x, y) {
if(this.grid[x][y] === -1) {
return this.gameOver(x, y)
} else if (this.grid[x][y] > 0) {
this.grid[x][y] = `${this.grid[x][y]}`
if(this.getSquareCount() === this.totalBombs){
return this.youWin()
}
} else if (this.grid[x][y] === 0) {
this.grid[x][y] = null
if(this.getSquareCount() === this.totalBombs){
return this.youWin()
}
if (x < this.M - 1) {
this.clickHandler(x + 1, y)
}
if (y < this.N - 1) {
this.clickHandler(x, y + 1)
}
if (x > 0) {
this.clickHandler(x - 1, y)
}
if (y > 0) {
this.clickHandler(x, y - 1)
}
}
}
flag(x, y) {
if(typeof this.grid[x][y] === "string"
&& this.grid[x][y].includes('f')){
this.grid[x][y] = parseInt(this.grid[x][y].slice(1))
} else {
this.grid[x][y] = `f${this.grid[x][y]}`
}
}
getGrid() {
return this.grid
}
setUp() {
this.createGrid()
this.createBombs(0, 0)
this.fillMap()
}
printTable() {
console.table(this.grid)
}
}

--

--

Jed Helmers

Former NSA linguist & intelligence analyst. Art school dropout. I'm a software engineer. Super fun shit.