Making a Browser Based Game With Vanilla JS and CSS

Developing for the net these days can seem overwhelming. There is an almost endlessly rich selection of libraries and systems to pick from.

You’ll likely also need to employ a build phase, version control, and a build pipeline. All before you’ve written a single line of code. How about a fun idea? This take a step back and tell ourselves just how precise and powerful present JavaScript and CSS can be, without the need for any gleaming extras.

Interested? Come with me then, on a journey to make a browser-based activity using just chocolate JS and CSS.

The Plan

We’ll get building a symbol guessing game. The participant is presented with a symbol and a multiple-choice design list of solutions.

Step 1. Basic composition

First of, we’re going to have a list of countries and their particular colors. Fortunately, we can harness the power of emoticons to show the flags, significance we don’t have to supply or, even worse, create them ourselves. I’ve prepared this in JSON shape.

At its simplest the program is going to display a flag emoticon and five switches:

1741723629screenshot1 294x300 1

A dash of CSS using the grid to heart all and comparative sizes so it displays properly from the smallest display up to the biggest monitor.

Then get a copy of our basic shim, we will be building on this throughout
the article.

The document framework for our project looks like this:

 step1.html step2.html  js/ data.json  helpers/  css/ i/

At the end of each area, there will be a connection to our script in its latest state.

Phase 2. A Simple Prototype

Come get breaking. Second off, we need to get our information. xml file.

 async function loadCountries(file) { try { const response = await fetch(file); return await response.json(); } catch (error) { throw new Error(error); } }   loadCountries('./js/data.json') .then((data) => { startGame(data.countries) });

Now that we have the information, we may start the game. The subsequent code is freely commented on. Get a couple of minutes to learn through and get a handle on what is happening.

 function startGame(countries) {    shuffle(countries);  let answer = countries.shift();  let selected = shuffle([answer, ...countries.slice(0, 4)]);  document.querySelector('h2.flag').innerText = answer.flag;  document.querySelectorAll('.suggestions button') .forEach((button, index) => { const countryName = selected[index].name; button.innerText = countryName;   button.dataset.correct = (countryName === answer.name); button.onclick = checkAnswer; }) }

And some reasoning to test the truth:

 function checkAnswer(e) { const button = e.target; if (button.dataset.correct === 'true') { button.classList.add('correct'); alert('Correct! Well done!'); } else { button.classList.add('wrong'); alert('Wrong answer try again'); } }

You’ve probably noticed that our startGame function calls a shuffle function. Here is a simple implementation of the Fisher-Yates algorithm:

   function shuffle(array) { var m = array.length, t, i;  while (m) {  i = Math.floor(Math.random() * m--);  t = array[m]; array[m] = array[i]; array[i] = t; } return array; }

Move 3. A bit of course

Day for a bit of laundry. Modern books and frameworks usually force certain norms that help utilize structure to apps. As things start to grow this makes sense and having all script in one folder quickly gets messy.

This leverage the power of modules to maintain our script, errm, flexible. Update your HTML document, replacing the inline text with this:

 <script type="module" src="./js/step3.js"></script>

Then, in js/step3. java we can fill our companions:

 import loadCountries from "./helpers/loadCountries.js"; import shuffle from "./helpers/shuffle.js";

Be sure to walk the walk and loadCountries features to their separate files.

Note: Ultimately we may even buy our information. csv as a module but, alas, Firefox does not help import assertions.

You’ll also need to begin each performance with trade definition. For instance:

 export default function shuffle(array) { ...

We’ll also represent our game logic in a Game group. This helps maintain the integrity of the data and makes the password more safe and viable. Take a moment to learn through the code responses.

loadCountries('js/data.json') .then((data) => { const countries = data.countries; const game = new Game(countries); game.start(); });class Game { constructor(countries) {   this.masterCountries = countries;  this.DOM = { flag: document.querySelector('h2.flag'), answerButtons: document.querySelectorAll('.suggestions button') }  this.DOM.answerButtons.forEach((button) => { button.onclick = (e) => { this.checkAnswer(e.target); } }) } start() {     this.countries = shuffle([...this.masterCountries]);   const answer = this.countries.shift();  const selected = shuffle([answer, ...this.countries.slice(0, 4)]);  this.DOM.flag.innerText = answer.flag;  selected.forEach((country, index) => { const button = this.DOM.answerButtons[index];  button.classList.remove('correct', 'wrong'); button.innerText = country.name; button.dataset.correct = country.name === answer.name; }); } checkAnswer(button) { const correct = button.dataset.correct === 'true'; if (correct) { button.classList.add('correct'); alert('Correct! Well done!'); this.start(); } else { button.classList.add('wrong'); alert('Wrong answer try again'); } }}

Step 4. Rating And A Gameover Monitor

This update the Game builder to manage many rounds:

class Game { constructor(countries, numTurns = 3) { // number of turns in a game this.numTurns = numTurns; ...

Our DOM will need to be updated so we can control the game over position, include a record button and show the score.

 <main> <div class="score">0</div> <section class="play"> ... </section> <section class="gameover hide"> <h2>Game Over</h2> <p>You scored: <span class="result"> </span> </p> <button class="replay">Play again</button> </section> </main>

We simply hide the sport over the area until it is required.

Then, add references to these new DOM components in our game designer:

 this.DOM = { score: document.querySelector('.score'), play: document.querySelector('.play'), gameover: document.querySelector('.gameover'), result: document.querySelector('.result'), flag: document.querySelector('h2.flag'), answerButtons: document.querySelectorAll('.suggestions button'), replayButtons: document.querySelectorAll('button.replay'), }

We’ll even tidy up our Game stop approach, moving the reasoning for displaying the nations to a distinct method. This will help keep things tidy and manageable.

 start() { this.countries = shuffle([...this.masterCountries]); this.score = 0; this.turn = 0; this.updateScore(); this.showCountries(); } showCountries() { // get our answer const answer = this.countries.shift(); // pick 4 more countries, merge our answer and shuffle const selected = shuffle([answer, ...this.countries.slice(0, 4)]); // update the DOM, starting with the flag this.DOM.flag.innerText = answer.flag; // update each button with a country name selected.forEach((country, index) => { const button = this.DOM.answerButtons[index]; // remove any classes from previous turn button.classList.remove('correct', 'wrong'); button.innerText = country.name; button.dataset.correct = country.name === answer.name; }); } nextTurn() { const wrongAnswers = document.querySelectorAll('button.wrong') .length; this.turn += 1; if (wrongAnswers === 0) { this.score += 1; this.updateScore(); } if (this.turn === this.numTurns) { this.gameOver(); } else { this.showCountries(); } } updateScore() { this.DOM.score.innerText = this.score; } gameOver() { this.DOM.play.classList.add('hide'); this.DOM.gameover.classList.remove('hide'); this.DOM.result.innerText = `${this.score} out of ${this.numTurns}`; }

At the bottom of the Game designer technique, we may
listen for clicks to the replay button ( s ). In the
function of a visit, we restart by calling the launch process.

 this.DOM.replayButtons.forEach((button) => { button.onclick = (e) => { this.start(); } });

Finally, let’s put a jump of design to the buttons, place the score and
include our.hide group to switch game over as needed.

button.correct { background: darkgreen; color: #fff; }button.wrong { background: darkred; color: #fff; }.score { position: absolute; top: 1rem; left: 50%; font-size: 2rem; }.hide { display: none; }

Progress! We now have a very simple activity.
It is a small generic, though. This handle that
in the next stage.

Step 5. Bring The Bling!

CSS movies are a very simple and precise manner to
provide dynamic aspects and interfaces to life.

Keyframes
enable us to identify keyframes of an video sequence with changing
CSS qualities. Ponder this for sliding our nation record on and off display:

.slide-off { animation: 0.75s slide-off ease-out forwards; animation-delay: 1s;}.slide-on { animation: 0.75s slide-on ease-in; }@keyframes slide-off { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(50vw); }}@keyframes slide-on { from { opacity: 0; transform: translateX(-50vw); } to { opacity: 1; transform: translateX(0); }}

We can use the sliding effect when starting the activity…

 start() { // reset dom elements this.DOM.gameover.classList.add('hide'); this.DOM.play.classList.remove('hide'); this.DOM.play.classList.add('slide-on'); ... }

…and in the nextTurn process

 nextTurn() { ... if (this.turn === this.numTurns) { this.gameOver(); } else { this.DOM.play.classList.remove('slide-on'); this.DOM.play.classList.add('slide-off'); } }

We also need to contact the nextTurn approach when we’ve checked the answer. Update the checkAnswer method to achieve this:

 checkAnswer(button) { const correct = button.dataset.correct === 'true'; if (correct) { button.classList.add('correct'); this.nextTurn(); } else { button.classList.add('wrong'); } }

When the slide-off animation has finished we need to roll it up on and upgrade the country list. We had set a delay, based on graphics length, and the perform this reasoning. Fortunately, there is an easier method using the animationend occasion:

 // listen to animation end events // in the case of .slide-on, we change the card, // then move it back on screen this.DOM.play.addEventListener('animationend', (e) => { const targetClass = e.target.classList; if (targetClass.contains('slide-off')) { this.showCountries(); targetClass.remove('slide-off', 'no-delay'); targetClass.add('slide-on'); } });

Step 6. Final Touches

Wouldn’t it be great to put a name display? This means the person is given a bit of perspective and not thrown directly into the game.

Our premium will look like this:

  <div class="score hide">0</div> <section class="intro fade-in"> <h1> Guess the flag </h1> <p class="guess">🌍</p> <p>How many can you recognize?</p> <button class="replay">Start</button> </section>  <section class="play hide"> ...

This rope the prologue screen into the game.
We’ll have to include a reference to it in the DOM components:

  this.DOM = { intro: document.querySelector('.intro'), ....

Then just hide it when starting the activity:

 start() {  this.DOM.intro.classList.add('hide');  this.DOM.score.classList.remove('hide'); ...

Also, don’t forget to put the new style:

section.intro p { margin-bottom: 2rem; }section.intro p.guess { font-size: 8rem; }.fade-in { opacity: 0; animation: 1s fade-in ease-out forwards; }@keyframes fade-in { from { opacity: 0; } to { opacity: 1; }}

Now wouldn’t it be good to provide the person with a score based on their report to? This is extremely easy to implement. As can be seen, in the updated gameOver strategy:

 const ratings = ['πŸ’©','🀣','😴','πŸ€ͺ','πŸ‘Ž','πŸ˜“','πŸ˜…','πŸ˜ƒ','πŸ€“','πŸ”₯','⭐']; const percentage = (this.score / this.numTurns) * 100;  const rating = Math.ceil(percentage / ratings.length); this.DOM.play.classList.add('hide'); this.DOM.gameover.classList.remove('hide');  this.DOM.gameover.classList.add('fade-in'); this.DOM.result.innerHTML = ` ${this.score} out of ${this.numTurns}  Your rating: ${this.ratings[rating]} `; }

One last finishing touch, a great animation when the player guesses properly. We can change once more to CSS graphics to achieve this result.

button::before { content: ' '; background: url(../i/star.svg); height: 32px; width: 32px; position: absolute; bottom: -2rem; left: -1rem; opacity: 0; }button::after { content: ' '; background: url(../i/star.svg); height: 32px; width: 32px; position: absolute; bottom: -2rem; right: -2rem; opacity: 0; }button { position: relative; }button.correct::before { animation: sparkle .5s ease-out forwards; }button.correct::after { animation: sparkle2 .75s ease-out forwards; }@keyframes sparkle { from { opacity: 0; bottom: -2rem; scale: 0.5 } to { opacity: 0.5; bottom: 1rem; scale: 0.8; left: -2rem; transform: rotate(90deg); }}@keyframes sparkle2 { from { opacity: 0; bottom: -2rem; scale: 0.2} to { opacity: 0.7; bottom: -1rem; scale: 1; right: -3rem; transform: rotate(-45deg); }}

We use the:: before and:: after wannabe components to add background image ( sun. svg ) but keep it hidden via setting opacity to 0. It is then activated by invoking the brightness video when the switch has the school brand right. Remember, we presently apply this class to the box when the right solution is selected.

Wrap-Up And Some Extra Thoughts

In less than 200 lines of ( liberally commented ) javascript, we have a fully
working, mobile-friendly sport. And hardly a single dominance or libraries in sight!

Of course, there are endless features and improvements we may add to our sport.
If you imagination a problem here are a few ideas:

    Put standard audio results for correct and incorrect answers.

  • Make the game accessible online using internet workers
  • Store statistics such as the amount of plays, total ratings in localstorage, and display
  • Put a way to promote your report and issue friends on social media.

Leave a Comment