When I first joined Google, I learned that a common interview question was “How would you code the game, Simon?” For those aren’t familiar with this 80s masterpiece of technological entertainment: Simon is a game in which you are shown a sequence of one of four colors. The game starts with a sequence of one color, and your job is to repeat the sequence. If you get the sequence right, the game adds a new color to the sequence. The game continues until you get the sequence wrong. I don’t know if reading this description makes the game seem fun, but it was fun.
At the time I joined Google, I had little experience writing code. The idea that I could have been asked that question terrified me. (I was not asked that question. The story of how I dodged the technical portion of my interview is a story for another time, however.) If I had been asked that question, I don’t know how I would have answered. So, you can imagine my delight when I came across a JavaScript code example that built a basic version of Simon. The code doesn’t go into a ton of detail about what it’s doing, so I thought it would be fun to try to document my interpretation here.
Note that I don’t have the experience to judge if this example is coded well. So I’ll just focus on what the code does, and leave discussions on better implementations to wiser heads than mine.
Before we get into the code, we should start by understanding how Simon actually works. I know that I explained the gameplay, but it’s helpful to think about what the game actually needs to do. I’m learning that how a user (in this case, the player) experiences an application doesn’t necessarily reflect what the application is doing. (I wonder how much of any application is smoke and mirrors, but again, that’s a topic for another time.) As I mentioned, in a Simon game you need to repeat a sequence of colors. The number of colors in each sequence increases by one each time you repeat a sequence correctly. (As long as the sequence is greater than 0, of course.) The next color in the sequence is randomly generated, but it’s limited to one of 4 colors.
With that in mind, we can start to build our application.
Accessing HTML elements
In this topic, I’m not going to dive deeply into HTML and CSS, as I’d rather focus on the JavaScript. You can look at the source code for the application if you’re interested in looking at those files further. But I have learned that, when I’m working with vanilla JavaScript–that is, JavaScript that isn’t used within a framework like React–it’s always good to get programmatic access to any of the elements I’m going to need. Here’s what that looks like for this application:
const scoreEl = document.getElementById("score");<br>const colorParts = document.querySelectorAll(".colors");<br>const containerEl = document.querySelector(".container");<br>const startBtn = document.querySelector("#start-btn");
const resultEl = document.querySelector("#score-result");
const wrapperEl = document.querySelector(".wrapper");
Each of these variables corresponds either to a specific element, like scoreEl
, or a group of elements, like colorParts
. I admit that I do wonder why some of these variables use document.querySelector
to get an element by its class name, while others use document.getElementById
to get an element by its id
value. I would think that the latter method is better, but I’m still learning.
Setting up game colors
Now that we have variables representing the HTML elements in the game, the next task we can complete is to create some sort of collection that specifies which colors are available in the game. This code uses an object for this purpose:
const colorObj = {
color1: { current: "#006400", new: "#00ff00" },
color2: { current: "#800000", new: "#ff0000" },
color3: { current: "#0000b8", new: "#0000ff" },
color4: { current: "#808000", new: "#ffff00" },
};
This object has four properties, one for each color. Each property has another object as a value, and that object has two additional properties: a string representing the current
color, and a string representing a new
color. Later on, the code uses these properties to visual indicate to the player the colors in the sequence, or to indicate that the player clicked on a color.
Controlling gameflow with a flag
But we get into more of the game logic, I want to call out one thing that is the code that I hadn’t thought of when I was thinking through this code on my own: it creates a flag. Remember that, in a Simon game, the game displays a sequence of colors. The player then clicks on colors in an attemp to match the sequence. But it would get very messy if the player started clicking on colors while the game logic was creating or displaying a new sequence. To avoid that scenario, the application uses a flag–basically, a variable that it uses to control the flow of the game:
let isPathGenerating = false;
This flag will be useful in a little bit.
Creating a delay function
In addition to this flag, we need a way to control the speed at which the game displays the colors. Otherwise, the game will cycle through the sequence so fast that no human could hope to keep up. In this code, this control is implemented with a delay
function.
const delay = async (time) => {
return await new Promise((resolve) => setTimeout(resolve, time));
};
Basically, this function takes a parameter, time
, and uses that value to delay the game by that amount of time. Now we have a way of controlling how long of a delay we want at different points in the application.
Keeping score and tracking clicks
One last thing before we get too far ahead of ourselves: the application also includes two additional variables: score
, which is used to track the player’s score, and clickCount
which is used to track how many times the player has clicked one of the colors.
let score = 0;
let clickCount = 0;
Nothing too fancy here, but I’m talking about them now because, like the isPathGenerating
flag, they become important later.
Getting a random color
Okay! With the code framework in place, we can talk about how code stores the color sequence generated during a game. To start, the code creates an empty array:
let randomColors = [];
Notice that the code uses JavaScript’s let
keyword, so we can change the contents of this array as the game progresses.
Moving on, the application adds a function to select a random color from the colorObj
defined earlier.
const getRandomColor = (colorsObj) => {
const colorKeys = Object.keys(colorsObj);
return colorKeys[Math.floor(Math.random() * colorKeys.length)];
};
When the application calls this function, it passes in the colorObj
object. The function then gets an array of keys from that object–in this case, the name of the object: color1
, color2
, and so on. Then the function uses two Math
functions to randomly select a color object. With this code in place, the application now has a way to get a random color.
That’s great.
Generating a color sequence
Now that we have the ability to generate a random color, we can create a function that generates a color sequence. In this code, that capability is a function, generateRandomPath
:
const generateRandomPath = async() => {
randomColors.push(getRandomColor(colorObj));
score = randomColors.length;
isPathGenerating = true;
await showPath(randomColors);
};
This function is an asynchronous function that performs a few tasks:
- It calls
getRandomColor
and adds that new color to ourrandomColors
array. - It updates the game score, which will always e the same value as the length of the
randomColors
array. - It sets the
isPathGenerating
flag totrue
. That will be important later. - It uses the
await
keyword to wait until theshowPath
function finsihes. We’ll define that function next.
Displaying the color sequence to the player
The function, generateRandomPath
, asynchronously waits for a new function, showPath
to complete. That function is responsible for showing the latest color sequence to the player.
const showPath = async(colors) => {
scoreEl.innerText = score;<br> for (let color of colors) {
const currentColor = document.querySelector(`.${color}`);
await delay(500);
currentColor.style.backgroundColor = colorObj[color].new;
await delay(600);
currentColor.style.backgroundColor = colorObj[color].current;
await delay(600);
}
isPathGenerating = false;
}
Again, we have another async
function. This time, the function takes an array of colors (our randomColors
array, in fact). Then, the code loops through the array. For each color in the array, the code:
- Creates a variable, currentColor, and sets it to the appropriate HTML element.
- Waits 500 milliseconds.
- Sets the background color for the element to the colors
new
property. This change provides a visual cue to the player of what the next color in the sequence is. - Waits again.
- Sets the color of the element back.
- Waits just a little more.
After the loop completes, the function sets the isPathGenerating
flag to false
.
With the generateRandomPath
and showPath
functions complete, we can turn our focus to handling the user inputs. Oh, and we’ll finally put that isPathGenerating
flag to work.
Creating an event handler
We want each of the 4 colors in the game to handle click events the same way, so we should create a function. This time, let’s walk through the function bit by bit.
To start, we create (you guessed it) an async
function, which we’ll call handleColorClick
.
const handleColorClick = async (e) => {
We don’t want to handle clicks if the game is generating a new color sequence. So we’ll use our isPathGenerating
flag:
if (isPathGenerating) {
return false;
}
Now, if the application is generating a sequence, the game will ignore clicks (on those colors, of course).
It’s this next bt of code that I find fascinating–even if it’s pretty straightforward once you see it. It’s a conditional statement:
If (e.target.classList.contains(randomColors[clickCount])) {
// ...more to come...
} else {
endGame();
}
To understand this code, it helped me to think about what information was available. First, I know what the correct color is in the sequence. I know it, because I can use the clickCount
value as an index in the randomColors
array to find it. If clickCount
is 5, then randomColors[clickCount]
will return the fifth color in the sequence. Second, we know the color of the HTML element that the user clicked. That information is available through e.target.classList
. These two pieces of information taken together, tell us if the user clicked on the right color. Each time the user clicks a color, the code uses the clickCount
value to find what the correct color is, then sees if the HTML element that the user clicked has a class that matches that color. If there is a match, the user clicked the right color, and the game continues. If not, then the user clicked the wrong color, and the endGame()
function executes.
I find this code interesting because, to be honest, I don’t know if I would have thought of it. I think it’s kind of a cool way of consistently tracking if the user is following the sequence correctly. Ask me in a few years if I think there’s a better way to do this. (Plot twist: There are a few other ways to do this for sure, but I couldn’t tell you if any of them are better.)
If the user makes the right selection, the code does a few other things:
- It changes the color of the element to it’s new value, waits, then changes it back. Just like when the game displays the current color sequence, this code gives a visual cue to the user that they clicked a specific color.
- It increments the
clickCount
value. - It checks to see if the current
clickCount
value equals the current score. If it does, we know the user has completed the current sequence. The game then setsclickCount
back to 0 and callsgenerateRandomPath()
, continuing the game.
Here’s the entire handleColorClick
function in its entirety.
const handleColorClick = async (e) => {
if (isPathGenerating) {
return false;
}
if (e.target.classList.contains(randomColors[clickCount])) {
e.target.style.backgroundColor = colorObj[randomColors[clickCount]].new;
await delay(500);
e.target.style.backgroundColor = colorObj[randomColors[clickCount]].current;
clickCount++;
if (clickCount == score) {
clickCount = 0;
generateRandomPath();
}
} else {
endGame();
}
};
Adding the event handler
Of course, a function to handle events isn’t very useful unless you actually add it to the corresponding event listener. In this code, that’s handled like this:
colorParts.forEach((color) => color.addEventListener("click", handleColorClick));
This line uses the colorParts
variable we created earlier. That variable is an array of HTML elements that represent the colors of our game. It then loops through each of these elements and adds the handleClickColor
function to the element’s click event listener.
Final touches
There just a few more details to make the game functional. First, we need an endGame()
function, to handle how each game ends.
const endGame = () => {
resultEl.innerHTML = `<span> Your Score : </span> ${score}`;
resultEl.classList.remove("hide");
containerEl.classList.remove("hide");
wrapperEl.classList.add("hide");
startBtn.innerText = "Play Again";
startBtn.classList.remove("hide");
};
Remember that the code calls this function if the user clicks the wrong color.
Next is a resetGame()
function, which sets all of the application’s variables back to their original states.
const resetGame = () => {
score = 0;
clickCount = 0;
randomColors = [];
isPathGenerating = false;
wrapperEl.classList.remove("hide");
containerEl.classList.add("hide");
generateRandomPath();
};
The game calls this function when the user clicks the Start button.
startBtn.addEventListener("click", resetGame);
With that, we have a working Simon game!
Dave’s thoughts
A few final thoughts I have about this code example:
- I’m not sure why the code uses
querySelector
in some cases andgetElementById
in others. You’d think thatgetElementId
would be better/easier. I understand why it usesquerySelectorAll
, though. - In the real Simon game, the sequence gets faster and faster as the sequence gets longer and longer. I bet this is way to guarantee that the user will fail before any mechanical limitations are met. That would be a nice addition to this code, and I might add that someday.
- Another nice addition would be to play a sound with each color–that’s another aspect of the original Simon game that’s absent here.
- I’d be really curious what more experienced developers think of this code. I wonder what improvements could be made to make the code more readable and maintainable.
- This experience was really, really fun.
Finally, I’d like to call out ASMR programmer for the code. Check out their GitHub repository for this and other code examples. You’re welcome to check out my GitHub repository as well.
Until next time.