How to create your first Chrome extension
In this article, you'll learn how Chrome extensions work and how to create your own for the first time
Creating games in the browser is a great way to practice your JavaScript skills and have fun at the same time!
In this tutorial, we’ll develop the classic Simon Game with JavaScript. The object of the game is to repeat a series of random tile clicks created by the game. After each round, the sequence becomes progressively longer and more complex which makes it harder to remember.
Play a live version of the game.
Typically, you’ll have four different tiles, each with a unique colour and sound which is activated when pressed. The sound aids the player in remembering the sequence and the game ends if the player misses a step in the sequence.
This tutorial assumes a basic knowledge of JavaScript and the DOM.
Grab the HTML and CSS files for the game on GitHub. The instructions for setting up the project are in the included README file. You can also follow the tutorial using JSFiddle if you prefer.
Here's the starting point on JSFiddle
As mentioned earlier, a round begins when the game activates one or more tiles in a random order and ends when the player reproduces the order by pressing the tiles. On the next round, the number of tiles in the sequence increases by one.
Let’s start by creating an array to keep track of the original sequence of tile clicks and a second array for the human sequence:
let sequence = [];
let humanSequence = [];Next, select the start button and create a new startGame() function that will be
executed when this button is clicked.
const startButton = document.querySelector('.js-start');
function startGame() {
startButton.classList.add('hidden');
}
startButton.addEventListener('click', startGame);At this point, once the start button is pressed, it will be hidden. The .info
element also needs to come into view because that is where status messages will
be displayed. It has the hidden class by applied default in the HTML which has been styled with
display: none; in the CSS so
that’s what needs to be removed once the game starts.
<span class="info js-info hidden"></span>Update your JavaScript file as shown below:
const startButton = document.querySelector('.js-start');
const info = document.querySelector('.js-info');
function startGame() {
startButton.classList.add('hidden');
info.classList.remove('hidden');
info.textContent = 'Wait for the computer';
}Now, once the start button is clicked, the .info element displays a message
telling the user to wait for the sequence to finish.
Take a breather, and see the complete code at the end of this step.
Create a new level variable below humanSequence. It’s how we’ll keep track
of the number of rounds that have been played so far.
let level = 0;Next, create a new nextRound() function just above startGame() as shown in the
snippet below. The purpose of this function is to start the next sequence of
tile clicks.
function nextRound() {
level += 1;
// copy all the elements in the `sequence` array to `nextSequence`
const nextSequence = [...sequence];
}Each time nextRound() invoked, the level variable is incremented by 1 and the
next sequence is prepared. Each new round builds upon the previous one so what
we need to do is copy the existing order of button presses and add a new random
one to it. We’ve done the former on the last line of nextRound(), so let’s now
add a new random button press to the sequence.
Create a new nextStep() function just above nextRound():
function nextStep() {
const tiles = ['red', 'green', 'blue', 'yellow'];
const random = tiles[Math.floor(Math.random() * tiles.length)];
return random;
}The tiles variable contains the colours for each button on the game board.
Notice how the values correspond with the values of the data-tile property in
the HTML.
<div class="tile tile-red" data-tile="red"></div>
<div class="tile tile-green" data-tile="green"></div>
<div class="tile tile-blue" data-tile="blue"></div>
<div class="tile tile-yellow" data-tile="yellow"></div>We need to get a random value from the array each time nextStep() is executed,
and we’re able to achieve that by using the Math.random() function in
combination with Math.floor(). The former returns a floating-point,
pseudo-random number in the range 0 to less than 1.
That’s not very useful to us at the moment. It needs to be converted to a valid
index for the tiles array (0, 1, 2, or 3 in this case) so that a random value
is from the array can be retrieved each time. Multiplying the value from
Math.random() with the length of tiles (which is 4) ensures that the range
of the random number is now between 0 and less than 4 (instead of 0 and less
than 1).
Still, those fractional values are not valid array indexes, so Math.floor() is
used to round the numbers down to the largest integer less than or equal the
given value. This gives us whole integers between 0 and 3 which can be used to
retrieve a random value from the tiles array.
Let’s utilise the return value of the nextStep() function in nextRound() as
shown below:
function nextRound() {
level += 1;
const nextSequence = [...sequence];
nextSequence.push(nextStep());
}What happens here is that when nextStep() is executed, it returns a random value
from the tiles array (“red”, “blue”, “green”, or “yellow”), and the value is
added to the end of the nextSequence() array alongside any values from the
previous round.
Take a breather, and see the complete code at the end of this step.
The next step is to actually play the next round by activating the tiles on the
screen in the right order. Add the following functions above nextStep() in your
JavaScript file:
function activateTile(color) {
const tile = document.querySelector(`[data-tile='${color}']`);
const sound = document.querySelector(`[data-sound='${color}']`);
tile.classList.add('activated');
sound.play();
setTimeout(() => {
tile.classList.remove('activated');
}, 300);
}
function playRound(nextSequence) {
nextSequence.forEach((color, index) => {
setTimeout(() => {
activateTile(color);
}, (index + 1) * 600);
});
}The playRound() function takes a sequence array and iterates over it. It then
uses the setTimeout() function to call the activateTile() at 600 millisecond
intervals for each value in the sequence. The reason setTimeout() is used here
is to add an artificial delay between each button press. Without it, the tiles
in the sequence will be activated all at once.
The specified number of milliseconds in the setTimeout() function changes on
each iteration. The first button in the sequence is activated after 600ms, the
next one after 1200ms (600ms after the first), the third one after 1800ms, and
so on.
In the activateTile() function, the value of color is used to select the
appropriate tile and audio elements. In the HTML file, notice how
the data-sound attribute on the audio elements correspond to the button
colours.
<audio src="https://s3.amazonaws.com/freecodecamp/simonSound1.mp3" data-sound="red" ></audio>
<audio src="https://s3.amazonaws.com/freecodecamp/simonSound2.mp3" data-sound="green" ></audio>
<audio src="https://s3.amazonaws.com/freecodecamp/simonSound3.mp3" data-sound="blue" ></audio>
<audio src="https://s3.amazonaws.com/freecodecamp/simonSound4.mp3" data-sound="yellow" ></audio>The activated class is added to the selected tile, and the play() method is
triggered on the selected audio element causing the linked mp3 file in the src
attribute to be played. After 300 milliseconds, the activated class is removed
again. The effect is that each tile is activated for 300ms, and there are
300ms between tile activations in the sequence.
Finally, call playRound() in the nextRound() function and call nextRound()
in the startGame() function as shown below:
function nextRound() {
level += 1;
const nextSequence = [...sequence];
nextSequence.push(nextStep());
playRound(nextSequence);
}
function startGame() {
startButton.classList.add('hidden');
info.classList.remove('hidden');
info.textContent = 'Wait for the computer';
nextRound();
}Now, once you hit the start button, the first round will begin and a random button will be activated on the board.
Take a breather, and see the complete code at the end of this step.
Once the computer begins a round by activating the next sequence of tiles, the player needs to end the round by repeating the pattern of tile activations in the right order. If a step is missed along the way, the game ends and resets.
Select the heading and tile container elements below the info variable:
const heading = document.querySelector('.js-heading');
const tileContainer = document.querySelector('.js-container');Next, create a humanTurn function that indicates that the computer is finished
with the round, and that it’s time for the player to repeat the sequence:
function humanTurn(level) {
tileContainer.classList.remove('unclickable');
info.textContent = `Your turn: ${level} Tap${level > 1 ? 's' : ''}`;
}The first step is to remove the unclickable class from the tile container.
This class prevents the buttons from being pressed when the game has not started
and when the AI is not finished with the sequence of presses.
.unclickable {
pointer-events: none;
}On the next line, the contents of the info element is changed to indicate that
the player can begin to repeat the sequence. It also shows how many taps needs
to be entered.
The humanTurn() function needs to be executed after the computer’s sequence is
over so we cannot call it immediately. We need to add an artificial delay and
calculate when the computer will be done with the sequence of button taps.
function nextRound() {
level += 1;
const nextSequence = [...sequence];
nextSequence.push(nextStep());
playRound(nextSequence);
sequence = [...nextSequence];
setTimeout(() => {
humanTurn(level);
}, level * 600 + 1000);
}The setTimeout() function above executes humanTurn() one second after the
the last button in the sequence is activated. The total duration of the sequence
corresponds to the current level multiplied by 600ms which is the duration for
each tile in the sequence. The sequence variable is also assigned to the
updated sequence.
In the next update to nextRound(), the unclickable class is added to the
tile container when the round starts, and the contents of the info and
heading elements are updated.
function nextRound() {
level += 1;
tileContainer.classList.add('unclickable');
info.textContent = 'Wait for the computer';
heading.textContent = `Level ${level} of 20`;
const nextSequence = [...sequence];
nextSequence.push(nextStep());
playRound(nextSequence);
sequence = [...nextSequence];
setTimeout(() => {
humanTurn(level);
}, level * 600 + 1000);
}
The heading now reflects the current level
The next step is to detect the player’s button taps and decide whether to move
to the next round or end the game. Add the following event listener just below
the one for startButton:
tileContainer.addEventListener('click', event => {
const { tile } = event.target.dataset;
if (tile) handleClick(tile);
});In the event listener above, the value of data-tile on the element that was
clicked is accessed and stored in the tile variable. If the value is not an
empty string (for elements without the data-tile attribute), the
handleClick() function is executed with the tile value as its only argument.
Create the handleClick() function just above startGame() as shown below:
function handleClick(tile) {
const index = humanSequence.push(tile) - 1;
const sound = document.querySelector(`[data-sound='${tile}']`);
sound.play();
const remainingTaps = sequence.length - humanSequence.length;
if (humanSequence.length === sequence.length) {
humanSequence = [];
info.textContent = 'Success! Keep going!';
setTimeout(() => {
nextRound();
}, 1000);
return;
}
info.textContent = `Your turn: ${remainingTaps} Tap${
remainingTaps > 1 ? 's' : ''
}`;
}This function pushes the tile value to the humanSequence array and stores its
index in the index variable. The corresponding sound for the button is played
and the remaining steps in the sequence is calculated and updated on the screen.
The if block compares the length of the humanSequence array to sequence
array. If they’re equal, it means that the round is over and the next round can
begin. At that point, the humanSequence array is reset and the nextRound()
function is called after one second. The delay is to allow the user to see the
success message, otherwise, it will not appear at all because it will get
overwritten immediately.
Take a breather, and see the complete code at the end of this step.
We need to compare the order in which the player taps the buttons to the order of the sequence generated by the game. If the order does not match, the game resets and a message is show alerting the player to the failure.
Create a new resetGame() function for this purpose above humanTurn():
function resetGame(text) {
alert(text);
sequence = [];
humanSequence = [];
level = 0;
startButton.classList.remove('hidden');
heading.textContent = 'Simon Game';
info.classList.add('hidden');
tileContainer.classList.add('unclickable');
}This function displays an alert and restores the game to its original state.
Let’s use it in handleClick as shown below:
function handleClick(tile) {
const index = humanSequence.push(tile) - 1;
const sound = document.querySelector(`[data-sound='${tile}']`);
sound.play();
const remainingTaps = sequence.length - humanSequence.length;
if (humanSequence[index] !== sequence[index]) {
resetGame('Oops! Game over, you pressed the wrong tile');
return;
}
if (humanSequence.length === sequence.length) {
humanSequence = [];
info.textContent = 'Success! Keep going!';
setTimeout(() => {
nextRound();
}, 1000);
return;
}
info.textContent = `Your turn: ${remainingTaps} Tap${
remainingTaps > 1 ? 's' : ''
}`;
}If the value of the element retrieved by the index in both the sequence and
humanSequence arrays do not match, it means the player made a wrong turn. At
that point, an alert is displayed and the game resets.
Take a breather, and see the complete code at the end of this step.
The game mostly works but we need to introduce an end state where the player wins the game. I’ve picked 20 rounds, but you can use any number you like. The classic Simon Game ended after 35 rounds.
Here’s the bit that ends the game if the user reaches and completes the 20th round:
function handleClick(tile) {
const index = humanSequence.push(tile) - 1;
const sound = document.querySelector(`[data-sound='${tile}']`);
sound.play();
const remainingTaps = sequence.length - humanSequence.length;
if (humanSequence[index] !== sequence[index]) {
resetGame('Oops! Game over, you pressed the wrong tile');
return;
}
if (humanSequence.length === sequence.length) {
if (humanSequence.length === 20) {
resetGame('Congrats! You completed all the levels');
return
}
humanSequence = [];
info.textContent = 'Success! Keep going!';
setTimeout(() => {
nextRound();
}, 1000);
return;
}
info.textContent = `Your turn: ${remainingTaps} Tap${
remainingTaps > 1 ? 's' : ''
}`;
}Once the 20th round is completed, a congratulatory message is displayed and the game resets. Make sure to try it out and see if you can reach level 20 without failing.
Take a breather, and see the complete code at the end of this step.
In this tutorial, we developed a functioning Simon game with JavaScript. I hope you had a lot of fun building it. You can take it further by experimenting with different designs or by adding extra features. There are also several clones you can learn from and use as an inspiration.
Thanks for reading, and happy coding!
Comments
Ground rules
Please keep your comments relevant to the topic, and respectful. I reserve the right to delete any comments that violate this rule. Feel free to request clarification, ask questions or submit feedback.