Whack a mole game in JavaScript
Let’s develop a game of whack a mole in JavaScript. The game will have a 3 x 3 square grid. A mole will randomly appear in one of the cells, randomly picked. If the user clicks on the mole, the score gets incremented, and the mole disappears. If the user doesn’t click on the mole, it will disappear after a random amount of time. After another random amount of time, another mole will appear in another randomly picked cell. The game will be playable for one minute and then it will stop.
The basic HTML and CSS styling
Let’s start by creating an index.html
file with the following content:
<!DOCTYPE HTML>
<HTML>
<head>
<title>Whack a mole</title>
<link rel="stylesheet" type="text/css" href="style.css"/>
</head>
<body>
<h2>Score: <span id="score">0</span></h2>
<div id="grid">
<div class="square" id="1"></div>
<div class="square" id="2"></div>
<div class="square" id="3"></div>
<div class="square" id="4"></div>
<div class="square" id="5"></div>
<div class="square" id="6"></div>
<div class="square" id="7"></div>
<div class="square" id="8"></div>
<div class="square" id="9"></div>
</div>
<script src="app.js"></script>
</body>
</HTML>
We’ve set up an h2
element and a span
element to display the score. We’ve set up a div
element with and id
of grid
, inside which we have the div
elements corresponding to the 9 cells. For consistent styling of all the cells, we have set a class
of square
. We’ve assigned each cell a unique id
so that we can access them individually programmatically.
Here’s the CSS file:
#grid {
width: 320px;
height: 318px;
background-color: #abcdab;
margin: 0 auto;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.square {
width: 100px;
height: 100px;
border: 1px solid black;
margin-bottom: 6px;
}
I imagined a 100px x 100px cell, with some spacing horizontally and vertically between the cells. I wanted the grid to be horizontally centred on the screen, hence margin: 0 auto;
. I wanted to layout the div
elements inside the container nicely spread out, and wrapping around into 3 rows, so I went for CSS Flexbox with justify-content: space-between;
and flex-wrap: wrap;
.
If you open the page in a browser, you’ll see the cells laid out nicely in the centre of the page, with a background color.
Displaying the mole
I download a mole image from pixabay. Here’s how we can display the mole image in a randomly picked cell:
- Create an
img
element for the mole - Pick a cell number at random
- Select the DOM element corresponding the randomly picked cell
- Add the
img
element as a child to the abovediv
Let’s get to work:
// Create an img element for the mole image
const mole = document.createElement('img')
mole.setAttribute('src', 'animal.png')
// Pick a cell at random
let randCell = Math.floor(Math.random()*9) + 1
// Display the mole in the randomly picked cell
const cell = document.getElementById(randCell)
cell.appendChild(mole)
With this code, when you open the page, the mole image will be rendered in a random cell. If the image size is greater than 100px x 100px, as was the case for me, the image wouldn’t actually show up inside a cell.
To fix the dimensions, you can set a width: 90%;
on the img
tag. To have the mole image appear centred inside the cell, we’ll change the div
elements for the 9 cells to be Flexbox containers with align-items:center;
and justify-content:center;
. Here’s the modified CSS:
#grid {
width: 320px;
height: 318px;
background-color: #abcdab;
margin: 0 auto;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.square {
width: 100px;
height: 100px;
border: 1px solid black;
margin-bottom: 6px;
display: flex;
align-items: center;
justify-content: center;
}
img {
width:90%;
}
Making the mole disappear after a random time
We have the variable named cell
that holds the div
DOM element in which we displayed the mole, so removing the mole from view is a matter of a cell.removeChild(cell.firstChild)
.
How do we make that happen after a random amount of time? We’ll need to use setTimeout()
. To draw a random interval, we’ll use Math.random()
, again. But Math.random()
returns a value between 0 and 1, whereas setTimeout()
accepts an interval in milliseconds. So, our randomly drawn value for timeout using Math.random()
wouldn’t be enough. The animation would be too fast.
I want the randomly drawn interval to be as big as 2 seconds, so I’ll multiply Math.random()
with 2000, and convert it to an integer using Math.floor()
. But, the randomly drawn integer could still be close to 0, which would be too fast. So, I’ll add 800 to the randomly drawn value, so that the mole sticks around for at least 0.8 seconds. I now have 800 + Math.floor(Math.random()*2000))
. Sure, now the interval can go up to 2.8 seconds, rather than my desired 2 seconds. We can fix that by reducing the 2000 in the formula to 1200. But, I don’t care. Here’s our JavaScript file so far:
// Create an img element for the mole image
const mole = document.createElement('img')
mole.setAttribute('src', 'animal.png')
// Pick a cell at random
let randCell = Math.floor(Math.random()*9) + 1
// Display the mole in the randomly picked cell
const cell = document.getElementById(randCell)
cell.appendChild(mole)
let timeout = 800 + Math.floor(Math.random()*2000)
setTimeout(removeMole, timeout)
function removeMole() {
cell.removeChild(cell.firstChild)
}
Making the mole re-appear
Once the mole has disappeared, it should re-appear at another random cell, after another random amount of time. To do this, we’ll need to call another function from removeMole
after a random interval. This function will display the mole in another random cell, and call removeMole
after another random interval. So, we’ll have created a cycle like the following:
Here’s the code:
// Create an img element for the mole image
const mole = document.createElement('img')
mole.setAttribute('src', 'animal.png')
// Pick a cell at random
let randCell = Math.floor(Math.random()*9) + 1
// Display the mole in the randomly picked cell
let cell = document.getElementById(randCell)
cell.appendChild(mole)
let timeout = 800 + Math.floor(Math.random()*2000)
setTimeout(removeMole, timeout)
function removeMole() {
cell.removeChild(cell.firstChild)
timeout = 800 + Math.floor(Math.random()*2000)
setTimeout(addMole, timeout)
}
function addMole() {
randCell = Math.floor(Math.random()*9) + 1
cell = document.getElementById(randCell)
cell.appendChild(mole)
timeout = 800 + Math.floor(Math.random()*2000)
setTimeout(removeMole, timeout)
}
Now, if you open the page, you’ll see mole appearing and disappearing in random cells after random timeouts.
There’s a bit of redundancy in the above code. We are adding a mole in addMole
as well as globally. Let’s refactor the code:
let randCell
let cell
// Create an img element for the mole image
const mole = document.createElement('img')
mole.setAttribute('src', 'animal.png')
// Display the first mole after a random amount of time
let timeout = 800 + Math.floor(Math.random()*2000)
setTimeout(addMole, timeout)
function removeMole() {
cell.removeChild(cell.firstChild)
timeout = 800 + Math.floor(Math.random()*2000)
setTimeout(addMole, timeout)
}
function addMole() {
randCell = Math.floor(Math.random()*9) + 1
cell = document.getElementById(randCell)
cell.appendChild(mole)
timeout = 800 + Math.floor(Math.random()*2000)
setTimeout(removeMole, timeout)
}
Click click
On to handling the mouse clicks. We could either add the click event listener on the img
element or the div
element that contains it. The difference is that the img
element occupies 90% of the width of the div
element. So, if we add the listener to the div
element, the user has a little bit of room for error. Let’s go with that.
We can add that event listener in the addMole()
function, while adding the img
element to the div
. The callback can call the removeMole()
function to get rid of the mole. Here’s the code:
let randCell
let cell
// Create an img element for the mole image
const mole = document.createElement('img')
mole.setAttribute('src', 'animal.png')
// Display the first mole after a random amount of time
let timeout = 800 + Math.floor(Math.random()*2000)
setTimeout(addMole, timeout)
function removeMole() {
cell.removeChild(cell.firstChild)
timeout = 800 + Math.floor(Math.random()*2000)
setTimeout(addMole, timeout)
}
function addMole() {
randCell = Math.floor(Math.random()*9) + 1
cell = document.getElementById(randCell)
cell.appendChild(mole)
cell.addEventListener('click', hitit)
timeout = 800 + Math.floor(Math.random()*2000)
setTimeout(removeMole, timeout)
}
function hitit() {
removeMole()
}
However, with this code, you’ll see an error message in the console complaining on the first line in removeMole()
, whenever you hit a mole. The problem is that removeMole()
was set to be called on a timeout in addMole()
, and we called it explicitly as well. Whichever call happens first, works, the second call is unable to remove the firstChild
, because none exists. The child element was removed in the previous call.
How do we solve this problem? We’ll need to cancel the earlier scheduled call to removeMole()
. For that, we need to store the timeout ID. Here’s the modified code:
let randCell
let cell
let timeoutID
// Create an img element for the mole image
const mole = document.createElement('img')
mole.setAttribute('src', 'animal.png')
// Display the first mole after a random amount of time
let timeout = 800 + Math.floor(Math.random()*2000)
setTimeout(addMole, timeout)
function removeMole() {
cell.removeChild(cell.firstChild)
timeout = 800 + Math.floor(Math.random()*2000)
timeoutID = setTimeout(addMole, timeout)
}
function addMole() {
randCell = Math.floor(Math.random()*9) + 1
cell = document.getElementById(randCell)
cell.appendChild(mole)
cell.addEventListener('click', hitit)
timeout = 800 + Math.floor(Math.random()*2000)
timeoutID = setTimeout(removeMole, timeout)
}
function hitit() {
clearTimeout(timeoutID)
removeMole()
}
Score!
We still need to keep track of and update the score. Here’s the modified app.js
file.
let randCell
let cell
let timeoutID
let score = 0
// Create an img element for the mole image
const mole = document.createElement('img')
mole.setAttribute('src', 'animal.png')
const scoreSpan = document.getElementById('score')
// Display the first mole after a random amount of time
let timeout = 800 + Math.floor(Math.random()*2000)
setTimeout(addMole, timeout)
function removeMole() {
cell.removeChild(cell.firstChild)
timeout = 800 + Math.floor(Math.random()*2000)
timeoutID = setTimeout(addMole, timeout)
}
function addMole() {
randCell = Math.floor(Math.random()*9) + 1
cell = document.getElementById(randCell)
cell.appendChild(mole)
cell.addEventListener('click', hitit)
timeout = 800 + Math.floor(Math.random()*2000)
timeoutID = setTimeout(removeMole, timeout)
}
function hitit() {
score = score + 1
scoreSpan.innerHTML = score
clearTimeout(timeoutID)
removeMole()
}
We defined a variable to hold the score, and updated it in the hitit()
function. We also defined a variable to hold the reference to the span
element to display the score and updated it in the hitit()
function.
You might notice one problem with the game, though. If you click on a cell where a mole has disappeared from, you still get a score. Why is that? It is because we added hitit()
as an event listener to that div
and it is still registered as a callback. We need to remove the event listener.
let randCell
let cell
let timeoutID
let score = 0
// Create an img element for the mole image
const mole = document.createElement('img')
mole.setAttribute('src', 'animal.png')
const scoreSpan = document.getElementById('score')
// Display the first mole after a random amount of time
let timeout = 800 + Math.floor(Math.random()*2000)
setTimeout(addMole, timeout)
function removeMole() {
cell.removeChild(cell.firstChild)
this.removeEventListener('click', hitit)
timeout = 800 + Math.floor(Math.random()*2000)
timeoutID = setTimeout(addMole, timeout)
}
function addMole() {
randCell = Math.floor(Math.random()*9) + 1
cell = document.getElementById(randCell)
cell.appendChild(mole)
cell.addEventListener('click', hitit)
timeout = 800 + Math.floor(Math.random()*2000)
timeoutID = setTimeout(removeMole, timeout)
}
function hitit() {
score = score + 1
scoreSpan.innerHTML = score
clearTimeout(timeoutID)
removeMole()
}
End game
We need to give the user a 1 minute interval to play a game. For that, we’ll maintain a timer, and .
You may download the code from this github repository, if you want.
Here’s app.js
:
let randCell
let cell
let timeoutID
let score = 0
// Create an img element for the mole image
const mole = document.createElement('img')
mole.setAttribute('src', 'animal.png')
const scoreSpan = document.getElementById('score')
setTimeout(gameOver, 60000)
// Display the first mole after a random amount of time
let timeout = 800 + Math.floor(Math.random()*2000)
setTimeout(addMole, timeout)
function removeMole() {
cell.removeEventListener('click', hitit)
cell.removeChild(cell.firstChild)
timeout = 800 + Math.floor(Math.random()*2000)
timeoutID = setTimeout(addMole, timeout)
}
function addMole() {
randCell = Math.floor(Math.random()*9) + 1
cell = document.getElementById(randCell)
cell.appendChild(mole)
cell.addEventListener('click', hitit)
timeout = 800 + Math.floor(Math.random()*2000)
timeoutID = setTimeout(removeMole, timeout)
}
function hitit() {
this.removeEventListener('click', hitit)
clearTimeout(timeoutID)
removeMole()
score = score + 1
scoreSpan.innerHTML = score
}
function gameOver() {
clearTimeout(timeoutID)
scoreSpan.innerHTML = scoreSpan.innerHTML + ' Game over'
}
Here’s index.html
:
<!DOCTYPE HTML>
<HTML>
<head>
<title>Whack a mole</title>
<link rel="stylesheet" type="text/css" href="style.css"/>
</head>
<body>
<h2>Score: <span id="score">0</span></h2>
<div id="grid">
<div class="square" id="1"></div>
<div class="square" id="2"></div>
<div class="square" id="3"></div>
<div class="square" id="4"></div>
<div class="square" id="5"></div>
<div class="square" id="6"></div>
<div class="square" id="7"></div>
<div class="square" id="8"></div>
<div class="square" id="9"></div>
</div>
<script src="app.js"></script>
</body>
</HTML>
Here’s style.css
:
#grid {
width: 320px;
height: 318px;
background-color: #abcdab;
margin: 0 auto;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.square {
width: 100px;
height: 100px;
border: 1px solid black;
margin-bottom: 6px;
display: flex;
align-items: center;
justify-content: center;
}
img {
width:90%;
}