#JavaScript30 – the 30-day vanilla JS coding challenge
I heard about Wes Bos's JavaScript30 challenge from @magdapoppins. I've been learning bits and pieces of JavaScript but nothing very extensive yet, and the short video tutorials appealed to me: it was something I'd be able to do almost any night (no matter how tired I was) after my kid had finally gone to sleep. I ended up completing the challenge in a bit over a month: from August 10 to September 15. 💪
Untrue to my style, I wanted to keep some sort of a study diary (also inspired by Magda) so I could easily come back to it and remember the key points in each video. I used the readme.md of the repo for that.
There were some interesting apps that I wish to develop further (speech detection and speech synthesis) and one that required using a phone (geolocation) so I ended up creating a main page using GitHub Pages and link to the most interesting ones for easy access.
A couple of the videos encouraged trying to solve a task first on your own and then checking the rest of the tutorial. Many of my solutions weren't that elegant so I don't dare display them, but I was quite proud – as still quite a rookie with all this – of the high score table that I created for the Whack-a-mole game. I'm sure there are things to improve but I'll go through my solution here and am happy to receive improvement suggestions. This time the extra task was just one of the suggested ways you could develop the game further, and there was no solution provided in the video.
Whack-a-mole
Whack-a-mole is a game where you try to click as many gophers popping out of holes as you can.
The tutorial sets up the page and the base game
We have a group of holes and moles inside the game HTML.
<div class="game">
<div class="hole hole1">
<div class="mole"></div>
</div>
<div class="hole hole2">
<div class="mole"></div>
</div>
<div class="hole hole3">
<div class="mole"></div>
</div>
<div class="hole hole4">
<div class="mole"></div>
</div>
<div class="hole hole5">
<div class="mole"></div>
</div>
<div class="hole hole6">
<div class="mole"></div>
</div>
</div>
Then we begin the script by declaring constants for our main elements.
<script>
const holes = document.querySelectorAll('.hole');
const scoreBoard = document.querySelector('.score');
const moles = document.querySelectorAll('.mole');
We set a couple of variables: We are keeping tabs of the latest hole a gopher popped up from because we don't want to get the same hole twice in a row. We also have a Boolean flag for ending the game, and we are keeping score.
let lastHole;
let timeUp = false;
let score = 0;
The moles are peeking from the holes at various speeds so we create random timers.
function randomTime(min, max) {
return Math.round(Math.random() * (max - min) + min);
}
Naturally, the moles are peeking from random holes so we create a hole randomizer.
function randomHole(holes) {
const index = Math.floor(Math.random() * holes.length);
const hole = holes[index];
(...)
We don't want the same hole twice in a row so we check for it.
(...)
if (hole === lastHole) {
return randomHole(holes);
}
lastHole = hole;
return hole;
}
The peeking (or peeping as this function is called) will last for a set time (set later in the startGame
function).
function peep() {
const time = randomTime(200, 1000);
const hole = randomHole(holes);
(...)
The gopher going up and down is animated using CSS transitions so we add and remove a class here.
(...)
hole.classList.add('up');
setTimeout(() => {
hole.classList.remove('up');
if (!timeUp) peep();
}, time);
}
The game is started, naturally, with a score of 0. Here the game lasts for 10000 milliseconds, or 10 seconds.
function startGame() {
score = 0;
scoreBoard.textContent = 0;
timeUp = false;
peep();
setTimeout(() => timeUp = true, 10000);
}
In order for the click to be counted, it has to be done by a user and not a script so we check for cheaters. Once the click lands, the up
class is removed and the gopher starts returning to its hole. We also update the scoreboard.
function bonk(e) {
if(!e.isTrusted) return; // cheater!
score++;
this.classList.remove('up');
scoreBoard.textContent = score;
}
At the end of the script, we add event listeners for each mole.
moles.forEach(mole => mole.addEventListener('click', bonk));
</script>
I added a high score table
At the end of the video, Wes gives some ideas of additional features for the game. One of them is a high score table that is saved in local storage. I wanted to try and create it.
I'm saving the high scores in an array in local storage and I've added a table element for the scoreboard.
const hiscores = JSON.parse(localStorage.getItem('hiscores')) || [];
const scoreList = document.querySelector('.scoretable');
My table shows the best 5 players and also has a clear button.
<div class="hiscore">
<h2>Top 5 clickers</h2>
<h3>(on this device)</h3>
<button onClick="clearScores()">Clear</button>
<table class="scoretable">
</table>
</div>
I populate it from the local storage.
function populateTable() {
scoreList.innerHTML = hiscores.map((row) => {
return `<tr><td>${row.clicker}</td><td>${row.score}</tr>`;
}).join('');
}
In the code snippet that keeps the game going until time is up, I added a checkScore
function which is run at the end of the game.
if (!timeUp) {
peep();
} else {
checkScore();
}
What the function does, is it eventually starts kicking out worst scores because I didn't want the list to be very long.
function checkScore() {
let worstScore = 0;
if (hiscores.length > 4) {
worstScore = hiscores[hiscores.length - 1].score;
}
(...)
If the score is better than the last one, the user is prompted to enter a name. The score and the name are added to the high score array.
(...)
if (score > worstScore) {
const clicker = window.prompt(`${score} – Top score! What's your name?`);
hiscores.push({score, clicker});
}
(...)
Then the array is sorted from best to worst score.
(...)
hiscores.sort((a, b) => a.score > b.score ? -1 : 1);
(...)
If the array is longer than 5 items, the last score is removed.
(...)
if (hiscores.length > 5) {
hiscores.pop();
}
(...)
Then the HTML table is refreshed and the scoreboard is also saved to local storage.
(...)
populateTable();
localStorage.setItem('hiscores', JSON.stringify(hiscores));
}
I wanted to add a button that empties the list (now that I think about it, I should've probably used removeItem
instead of setting an empty array, because it would remove my entry from the storage completely).
At this point, I ran into an issue with emptying a constant
array. I solved it by using splice
to cut out items from the very first to the very last.
function clearScores() {
hiscores.splice(0, hiscores.length);
localStorage.setItem('hiscores', JSON.stringify(hiscores));
populateTable();
}
It was a lot of fun using stuff I'd learned during this challenge (such as local storage and populating an HTML table from an array in local storage) to create the additional feature. Playing the game was a lot of fun too. I think the best score I've got so far is 11. How good a clicker are you? 😊
Top comments (1)
Great job! And deployment as well 💪 Thanks also for the tip to the challenge, sounds like a fun thing to try out!