Whack a mole game in JavaScript

Muhammad Saqib Ilyas
8 min readJul 29, 2023

--

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 above div

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 removeMoleafter 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:

removeMole and addMole will call each other after random timeouts

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%;
}

--

--

Muhammad Saqib Ilyas

A computer science teacher by profession. I love teaching and learning programming. I like to write about frontend development, and coding interview preparation