DEV Community

Cover image for Adding a high score table to #JavaScript30 Whack-a-mole
Minna N.
Minna N.

Posted on • Updated on

Adding a high score table to #JavaScript30 Whack-a-mole

#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>
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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];
  (...)
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
  (...)
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

At the end of the script, we add event listeners for each mole.

moles.forEach(mole => mole.addEventListener('click', bonk));
</script>
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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('');
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
  (...)
Enter fullscreen mode Exit fullscreen mode

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});
  }
  (...)
Enter fullscreen mode Exit fullscreen mode

Then the array is sorted from best to worst score.

  (...)
  hiscores.sort((a, b) => a.score > b.score ? -1 : 1);
  (...)
Enter fullscreen mode Exit fullscreen mode

If the array is longer than 5 items, the last score is removed.

  (...)
  if (hiscores.length > 5) {
    hiscores.pop();
  }
  (...)
Enter fullscreen mode Exit fullscreen mode

Then the HTML table is refreshed and the scoreboard is also saved to local storage.

  (...)
  populateTable();
  localStorage.setItem('hiscores', JSON.stringify(hiscores));
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
lauravuo profile image
Laura Vuorenoja

Great job! And deployment as well 💪 Thanks also for the tip to the challenge, sounds like a fun thing to try out!