It is difficult to become an effective web developer. One way to make it easier is to leverage the technology available to you, like your browser’s developer tools.
Learning how to use your browser’s developer tools is very important. You will find these tools very helpful when working through my JavaScript tutorials. They can be used for experimenting, debugging issues, and generally seeing what’s going on behind the scenes. In this post I’m going to show you some of the most useful things you can do with them.
I’ll be using Google Chrome, but any browser will have similar tools available. In Chrome, you can open up your browser’s developer tools using several different methods. The quickest method is probably using the keyboard shortcut, Ctrl + Shift + I. Since you’re presumably reading this post in your web browser, go ahead and do that right now.
Once you have the developer tools opened up you’ll notice a toolbar at the top. There are many tabs, and all have their uses. The ones I use most often are the Elements, Console, and Sources tabs.
There are also two little icons in the top left. One let’s you jump to any HTML element on the page, and the other let’s you see what the page would look like on a mobile device. I find those little buttons helpful too.
Here is a screenshot pointing all this out.
Elements
The Elements tab will let you make changes in real time to the HTML and CSS. This is handy because it let’s you try out changes before adding them to your HTML or CSS file. It speeds up development and allows you to iterate faster.
Just make sure you don’t get carried away. It’s possible to make a whole bunch of changes in here, but they will get reset if you refresh the page. Make sure you add your changes to the code itself if you want to keep them permanently.
Console
The Console tab lets you execute arbitrary JavaScript code in the console. This is helpful for testing things out and inspecting the contents of variables in real time.
For example, we can see below what is contained inside the answers array in the code I wrote for the ear trainer tutorial. Just type the name of the variable you want to inspect in the console and press enter.
In my opinion, the most useful thing about this tab is that you can put break points in your JavaScript code. Break points allow you to pause the execution in the middle of your running program and step through it line by line. You can also inspect the contents of different JavaScript variables that are in scope.
This is very helpful when debugging problems with your code. You can see here how I’ve set a break point at the line where we evaluate if an answer is correct. I’ve also evaluated the variables in the console so I can see if they match.
Mobile Friendly
Finally, by clicking the mobile device icon we can see a preview of what my crossword puzzle generator post looks like on a mobile device. You can choose from different device types and orientations. Testing on real devices is always the best, but this is a handy feature to make sure the page generally looks good.
You should take the time to explore more of these buttons and tabs on your own, as there is a lot of really cool and useful functionality for you to discover.
The best way to get good at using your browser’s developer tools is good old fashioned practice. Keep that in mind as a silver lining the next time you’re frustrated by a bug in your JavaScript code.
As always, thanks for reading. Follow me on Twitter if you’d like to keep up with what I’m up to.
You can also subscribe to my blog if you’d like some tips on writing functions and to have my blog posts show up in your inbox.
It’s time once again for a JavaScript tutorial. This will be the most complex code I’ve introduced to date, so I hope you’ve been paying attention and not just copy and pasting! In this post you will learn how to build a crossword puzzle generator with JavaScript.
First I’ll give an introduction into what inspired this project, next I’ll discuss what the different components are and how they fit together, and finally I’ll talk about the performance optimizations I made.
You can try it out by clicking this link. Be patient after clicking the button though, as sometimes it takes a moment to create a crossword puzzle. You’ll see why soon enough. While you’re there, go ahead and use your developer tools to familiarize yourself a bit with the code.
I’ve been programming for many years now and have made lots of mistakes along the way. I like to think of myself as somebody who knows a thing or two about building software. There is an enormous number of things I don’t know, but I’m confident in my skill set and my ability to learn new concepts and technologies as needed.
However, I didn’t get that confidence overnight. I used to be pretty bad, and when I get the chance to look at some of my code from years back it makes me cringe at the silly things I did.
So if that’s you right now don’t feel bad! We all have to start from the beginning to become good at anything worthwhile.
The Source of Inspiration
And this brings me to a little a story. I created this crossword puzzle generator by porting an old Java program I wrote back when I first got out of college. I hadn’t looked at it in years, and let me tell you, it was pretty rough. You can check it out on GitHub if you want to laugh at me.
I began the porting process by going one Java method at a time. I replaced each one with a shiny new JavaScript function that replicated its logic. As I progressed I began to refactor it, fix bugs, and improve the performance.
The Fruits of My Labor
The finished product isn’t perfectly clean code, but it’s a whole lot better than it was before.
After finishing the JavaScript version of my crossword puzzle generator I began wondering to myself if it’s worth writing a tutorial for. I’m 100% certain there are better algorithms than the one I used, and I’m also 100% certain there are more readability and efficiency improvements I could make. I don’t want to teach you guys any bad habits.
It’s also very complex and not the easiest material to teach.
In the end I decided it would be a good idea. So many tutorials out there are really trivial, and don’t give you a good sense of what a typical company code base looks like. This will give you a better approximation of that. It’s a collaboration of multiple developers with varying ability levels on a fairly complex problem over many years. It just so happens that both of the developers are me. 😂
It will also give you an opportunity add your own improvements, which is key to growing as a developer.
What Exactly Are We Building?
We are building a crossword puzzle generator with JavaScript, HTML, and CSS. The idea is to take a big list of words, pick some random ones, and try to make a crossword puzzle out of them.
We’ll start by placing a word on a grid. Then we’ll get another word that is a possible candidate to connect to that word. Then we’ll do the same for another word. We’ll continue this process on an on, picking a different word each time the a word is placed or doesn’t fit anywhere in the puzzle.
When do we stop trying to place words? That’s a complicated answer determined by two main factors:
Have we already placed a bunch of words on the crossword puzzle?
Do we have a lot of word intersections on the crossword puzzle?
Once a crossword puzzle is made, we’ll go ahead and create some more. Then we’ll find the one with the most word intersections and show it on the screen!
Breaking Down The Problem
I won’t go into the weeds as much in this post as I do in some of my previous tutorials. There are just too many functions and edge cases to go through. Instead, I think it will be most helpful if I explain in detail the high-level components we will be creating. Knowing how all the pieces fit together is half the battle.
I really encourage you to study what each function is accomplishing while keeping in mind which variables are being changed. The comment section is a great way to reach out for help if you don’t understand something. Others will probably thank you for it.
The Big List of Words
The first thing we need is a big list of words to pick from.
In my original version I pulled the King James Bible in from a text file.
In the updated version I just made a JavaScript array with a bunch of words in it. You can check it out here.
Representing a Word
To place a word on the grid we’ll need to know a few things about it. Obviously we’ll need to know the text of the word itself, but we’ll also need to know about it’s positioning. We’ll need a row and a column to mark it’s starting position. We’ll also need a boolean value to represent whether the word is horizontal or vertical in orientation.
We’ll represent words using a word object we create ourselves. You can check it out here.
Representing the Crossword Puzzle
The crossword puzzle objects we’re creating are representations of fully completed crossword puzzles. Each one will have various functions serving different purposes. Here is the comprehensive list of each function and what it does:
update: Try to add a word to the grid.
canBePlaced: Check if a word can be added to the grid.
getIntersections: Returns a count of the number of word intersections in the grid.
placementLegal: Determines if a word can legally be placed at a specific row/column position.
invadingTerritory: Determines if a word will invade another word’s territory at a certain position.
endOfWord: Determines if a particular row/column position corresponds to the end of the word.
doesCharacterExist: Determines if a character exists at a certain position.
overwritingHorizontalWord: Determines if placing a character at a particular row/column would be overwriting a horizontal word.
overwritingVerticalWord: Determines if placing a character at a particular row/column would be overwriting a vertical word
isInterference: Checks for interference at a set of row/column positions.
isLetter: Checks if there is a letter at a row/column position.
isEmptyCell: Checks if a row/column position is empty.
addWord: Adds a word to the grid.
fitsOnGrid: Checks if a word fits within the bounds of the grid.
isValidPosition: Checks if a row/column position is a valid one for the grid.
Once we have the ability to place words on a grid we need to start making a whole bunch of crossword puzzles. Then, we need to pick the best puzzle and display it on the screen. The top-level function that does all this is called createCrosswordPuzzle.
The createCrosswordPuzzle function has several nested functions that help accomplish its goals. Here is the comprehensive list of each function and what it does:
generateGrids: Generate a bunch of crossword puzzles.
attemptToPlaceWordOnGrid: Take a given a word and try to place it on the crossword puzzle.
getAWordToTry: Fetch a word that we want to try placing on the crossword puzzle.
getBestGrid: Pick the best crossword puzzle from the ones we generated.
isGoodWord: Determine if a word is a good candidate to try placing on crossword puzzle based on the letters on the board.
displayCrosswordPuzzle: Show the crossword puzzle on the screen.
pushUsedWords: Mark a word as used and add its letters to a list of ones present on the crossword puzzle.
In addition to the functions above, this file contains some helper functions for getting unused words and various random values.
I hope the previous sections have helped you see how all the pieces fit together. A crossword puzzle generator is a non-trivial problem, but breaking it down into smaller problems is a good way to approach it. In fact, that’s a good approach for any programming task.
I would like to cover one more aspect in further detail. By default, this code isn’t very fast. There is a very large amount of nested looping.
I’m going to share with you the steps I took to make it faster. If you know of better algorithms I’d love for you to write me a comment. This is a life long craft, and I’m as much of a student as I am a teacher.
There are 4 steps I took to improve the performance. I’ll refer to them as Adjusting The Inputs, Smart Word Picking, Knowing When To Quit, and Calling It Good Enough.
Adjusting The Inputs
There are two variables that can have a major affect on the speed of the program. One controls the number of grids to make, and the other controls the number of attempts to fit words on the grid.
I found that a high number of attempts meant that I could lower the total number of grids to make. A high number of attempts helps ensure that the crossword puzzle will be densely packed with words.
Play around with these inputs yourself and notice how they affect the speed of the program and what the crossword puzzle looks like. I tried to optimize for creating a good looking crossword puzzle while not causing the page to timeout.
Smart Word Picking
One way to speed things up is to limit our word selection to words that have a chance at being placed on the grid.
At first I picked each word totally at random, but that resulted in trying to place words on the grid that had no possible way of fitting. So I wasted a bunch of time looping through the grid, and I also wasted attempts at fitting words on the grid.
To fix this I started keeping a list of which letters exist on the crossword puzzle. That way I could limit my word selection to only words that start with one of those letters. I created the function below to aid in identifying good words to try. It isn’t perfect, but it prevents some unnecessary looping.
Knowing When To Quit
Sometimes you just gotta know when to quit. As more words are added to the crossword puzzle, the likelihood of successfully placing another word goes down. There isn’t as much space for it, and the requirements for placement become stricter.
Eventually we’ll start failing for an exceedingly long time to place a word on the grid. I decided it would be better to just end things at that point. If we are having trouble placing words on the grid it is probably pretty full anyways.
Here is the section of code where this takes place. The number 470 is pretty arbitrary. It seemed about right after doing some testing.
Calling It Good Enough
The final step I took to improve performance was to stop generating crossword puzzles once I had created one with at least 4 word intersections.
A crossword puzzle with 4 word intersections usually looks pretty good, so stopping once we have one of those cuts down on our average generation time.
See below.
Conclusion
Wow that was a long and tedious post. I really hope somebody actually reads this far. If you do, leave me a comment. It will make me feel better about writing all of this 😂
I hope you’ve been able to get your own crossword puzzle generator up and running. It’s a fun project with a lot of details to consider. If you can implement it yourself and understand how all the pieces work you’ll definitely be a better developer for it.
As always, thanks for reading. Follow me on Twitter if you’d like to keep up with what I’m up to.
You can also subscribe to my blog if you’d like some tips on writing functions and to have my blog posts delivered to your inbox.
Hey all you cool cats and kittens! Since we are all stuck at home trying to avoid the apocalypse, I thought it would be a good time for a new programming tutorial.
I’ve heard some encouraging news that at least one teacher has been using these tutorials to keep their kids doing something constructive during the quarantines. So that has made me happy and increased my motivation to be productive.
Instead of building another game like minesweeper or hangman, this tutorial will focus on another one of my passions: music.
I’m no professional by any stretch of the imagination, but I’ve always had love for music. Whether it was church hymns or classic rock, music was an important part of every car ride as kid, and I’m still trying to get better at singing and playing guitar (You can check out my progress if you’re interested).
Ear Training
One area I’ve been working on is my ability to recognize common music intervals, like major thirds, perfect fifths, and octaves. A common term for this is “ear training”. You can start doing this by associating the different intervals with the beginning of certain songs ( fourth = Amazing Grace, fifth = Star Wars Theme, octave = The Office Theme, etc). But my goal is for my ear to eventually recognize these intervals instantaneously.
I’ve downloaded various apps to help me do this, but it occurred to me that I could combine my passions and build one myself!
So that’s what we’ll be covering in this tutorial: how to build an ear trainer with JavaScript. For those less musically inclined, we’re also going to be using two programming techniques I haven’t covered in previous tutorials: playing audio and local client-side storage. Let’s get started! If you get lost, the full project is on github.
Making the Music
The first thing that I had to do was create the audio files. I used a program called Audacity to record myself playing various intervals on my guitar. I used a naming convention of {intervalName}{integer} in order to make the programming task easier.
You can record your own intervals and follow my naming convention, or if you’re in a hurry you can just download or point your code to the files I created. You can find them all here on my blog, or here on github.
Structure and Style
I’m not going to focus too much on describing how the page is structured and styled. I’ll leave it as an exercise for you to explore the HTML and CSS on your own. You’re welcome to ask questions in the comment section if you run into any issues with the files provided.
The main takeaways are that we have a start button, a replay interval button, a confirm answer button, a drop down list to pick our answer, and an area for displaying messages. Our JavaScript code will interact with each of these elements as the state of the test progresses.
Also, you’ll want to make sure you update any paths so that they are pointed to where your files are located. For example, if you add your own audio files, you’ll have to make sure your code is looking in the right directory for them.
Variables
Alright it is time to move on and finally start talking about the JavaScript. We’ll begin by discussing the variables that will be changed as the player progresses through the test:
let questions = []; let answers = []; let questionIndex = 0; let correctAnswers = 0; let questionCount = 20; let beginTime = "";
As you can see, we’re initializing an two empty arrays. One array is for keeping track of the questions, and the other is for keeping track of the answers.
We’re also initializing three variables holding integers. The questionIndex variable keeps track of which question we are on, while the questionCount variable keeps track of the total number of questions. The correctAnswers variable will be updated every time an interval is guessed correctly.
Also, we have a beginTime variable which will be set when the quiz begins. This variable will help us determine if the player has beaten his/her previous high score.
Helper Functions
Next, let’s talk about some helper functions we are going to create to make our lives easier. We’ll reuse them throughout the code, so it’s best to familiarize yourself with them early on.
Play Interval
let playInterval = function( id ) { document.getElementById(id).play(); }
This function takes the id of an html element as a parameter. It attempts to get the element by id and call play on it. This will work if the id belongs to an audio element, but you won’t have much luck if it doesn’t. The audio elements are present in the HTML document, and each one is associated with a unique audio file.
Hide
let hide = function( id )
{
document.getElementById(id).style.display="none";
}
The hide function takes an id and sets its display property to none. We could write this every time we need it, but “hide” is much more terse and easier to read.
Show
let show = function( id, value ) { let element = document.getElementById(id); element.style.display = ""; if( value !== undefined ) { element.innerHTML = value; } }
The show function is the opposite of the hide function in that it makes an element visible. It also has the optional ability of setting the inner HTML content of the element. If no content is provided it won’t set it to anything and the element’s content will remain unchanged.
Get Random Integer
let getRandomInteger = function( min, max )
{
return Math.floor(
Math.random() * ( max - min ) ) + min;
}
This function takes a min and max value as parameters and returns a random integer lying somewhere in between them. It will include the min but not the max as possible outputs.
This function builds on the getRandomInteger function. It’s purpose is to return a random valid index. I’ve created ten audio files for each interval, with indexes ranging from 0 to 9. So we’ll pass in 0 as the min value and 10 as the max value to satisfy our requirements.
Get Random Interval
let getRandomInterval = function()
{
let interval = getRandomInteger( 0, 5 );
return getInterval( interval );
}
This function is similar to getRandomIndex. You’ll call this function when you want to retrieve a random interval. In the select drop down you’ll notice that we have mapped each interval type to an integer. We have five intervals: major thirds, minor thirds, perfect fourths, perfect fifths, and octaves. These correspond to the integers 0 through 4.
After getting the random integer we call a function called getInterval to get the interval object. Keep reading to find out what that function does.
Get Interval
let getInterval = function( id )
{
id = id + "";
let interval = {
code: "third",
name: "Major Third"}
switch( id )
{
case "0":
interval = {
code: "third",
name: "Major Third"
};
break;
case "1":
interval = {
code: "minorthird",
name: "Minor Third"};
break;
case "2":
interval = {
code: "fourth",
name: "Perfect Fourth"
};
break;
case "3":
interval = {
code: "fifth",
name: "Perfect Fifth" };
break;
case "4":
interval = {
code: "octave",
name: "Octave" };
break;
default:
// Return whatever was set as the default
}
return interval;
}
The purpose of this function is to retrieve the interval object for whatever interval id was passed in. An interval object consists of a code and a name. The code matches the name of the audio file without the attached index number. The name is for display purposes.
Generating the Test
Now that we have some helpful functions created, let’s start building the test itself. For starters, we are going to need some values in our questions and answers arrays. Let’s look at where this get populated, in a function called, generateTest.
This function is called when the player clicks on the start test button. The first thing that happens is we hide and show some elements on the screen. We show the question area, which includes our buttons for replaying sounds and choosing answers. We hide the start test button because at this point the test is already underway. Also, we show the message area with a non-breaking space. This is to keep the size of the container the same whether there is a message to display or not.
The next thing we do, after initializing a few local variables, is start adding questions and answers until we have reached the number set by the questionCount variable. We set each question to a random index appended to a random interval code. The random interval is then stored at the same index in the parallel answers array.
Finally, we set the beginTime of the test, and proceed to the next question by calling the appropriately named, nextQuestion function.
Proceeding to the Next Question
The nextQuestion function is a pretty simple one. It does two things. First, it will show which question the player is on. Next it will play the audio file corresponding to the interval in question.
Replaying the Interval
What if the player needs to hear the interval again before answering? In that case they can trigger the replayInterval function by clicking on the replay interval button. Notice how boring my variable and function names are? That’s a good thing.
This function is even simpler than nextQuestion. All we do is call play interval and pass in the current question that we are on.
Choosing an Answer
The player selects his/her desired answer from the drop down list, and then clicks on the confirm answer button to guess what the interval is. When this happens, the confirmAnswer function is called.
The first thing that happens is the answer is evaluated to see if the player guess correctly. We’ll cover that function in a bit.
Next, the questionIndex is updated. If the questionIndex is less than the questionCount, we call the nextQuestion function. Otherwise, we call the finishGame function.
Evaluating the Answer
The purpose of the evaluateAnswer function is to determine if the player answered correctly and update the state of the game appropriately. Fortunately, the consequences for answering incorrectly will not be nearly as dire as in Indiana Jones.
The first thing we do is retrieve the answer from the selected option in the drop down list. Then we get the interval object by calling getInterval.
Finally, we check if the chosen interval’s code matches the one we have stored in our answers array. If the answer is correct, we increment the correctAnswers count and don’t show a message. If the answer is wrong, we show a message to the player informing them of what the correct answer was.
Finishing the Game
After the last question has been answered we need to show the start button again and hide the question area.
Next, we need to determine the player’s score, which consists of the number of correct questions and the total time it took him/her to complete the test.
Before showing the score to the player we will determine if he/she beat his/her previous high score. This will help us determine which message to show on the screen.
We find this out by calling the saveHighScore function.
Saving a High Score
We will be storing the high score client side in the player’s web browser. This means that the high score will persist even if the player closes their browser. This feature will allow players to compete against themselves and become faster and more accurate in their ability to guess intervals.
We start out by assuming that the player did not beat his/her high score. After grabbing the score and time out of local storage, we check if they did beat it. We consider a score better if the player answered more questions correctly or answered the same number correctly in less time. If the player beat his/her high score, we override the values in local storage.
Conclusion
That’s it! You have successfully created your own ear trainer. You should be proud of yourself.
From here you can go in lots of different directions. I can think of so many features to add that are beyond the scope of this tutorial. You could add more intervals, descending and ascending intervals, or the ability to choose a subset of intervals you want to train on. The list goes on and on.
Have you implemented other cool feature ideas? Are you stuck on anything you saw here? I’d love to hear from you in the comments. Be sure to subscribe to stay up to date with the latest content.
In my last tutorial I described the snake game I built using JavaScript, and before that I built a version of minesweeper. For this week’s post I decided to show you guys how to build hangman with JavaScript. I’m sure there are plenty of coding choices I’ve made that can be criticized, but hopefully I’ve still accomplished my goals. Namely, to have fun making little games and teach people a little bit about JavaScript and programming.
I already know what you’re thinking. How did I afford such an outstanding artist? Believe it or not, I actually created that condemned bandit artwork all on my own. Like most professional programmers, I can wear multiple hats.
I loved playing hangman when I was growing up as a kid. It’s one of those rare games that teaches you important concepts without you even realizing it. In order to become good at hangman, you have to become proficient with language. Another thing I loved as a kid (and still do) was watching movies. So naturally, I decided to combine these two passions and create “Movie Hangman”.
Unlike my previous game tutorials, I decided to build hangman with mobile users foremost in mind. To accomplish this I used something called CSS media queries. We use these to apply different CSS rules for different screen sizes. Be sure to check it out in the CSS file if you’re interested, as the bulk of this post covers the JavaScript game logic.
Alright, let’s start learning how to build hangman with JavaScript.
Rules of Hangman
A word or phrase is chosen for the player to guess.
Initially, the player can only see how many letters and words make up the full word or phrase.
The player begins by guessing letters. Each correct guess reveals every location where the letter exists in the word or phrase. Each incorrect guess results in another piece of the man getting added to the gallows.
If he thinks he knows it, the player is also allowed to guess the entire word or phrase.
The player wins if he successfully guess the word or phrase. He loses if he completes the picture of the condemned man, resulting in his grim demise.
Game Object
The Game function is responsible for creating our game object. This object will be responsible for holding the data about the current state of the game and the methods for acting upon that data. The game object will have 7 internal variables for handling state: word, guessedLetters, maskedWord, incorrectGuesses, possibleGuesses, won, and lost.
The word variable stores the word that the player is trying to guess. We assign it a random movie from our list of movies when we initialize the game object.
The guessedLetters variable is an array. We use it to store letters as the player guesses them.
The maskedWord variable is the word or phrase as it is displayed to the player. It starts out as a bunch of underscores with spaces separating the words, and it slowly gets filled in as the player makes more correct guesses. We initialize it via a for-loop.
The incorrectGuesses variable is a counter for storing the number of times the player has guessed incorrectly. Too many wrong guesses and the player will lose the game. Naturally, this counter starts at zero.
The possibleGuesses variable is a string containing all of the letters in the alphabet. Letters are removed as they are guessed. These are displayed to the player so he doesn’t have to remember what he has already guessed.
The won and lost variables keep track of whether the player has won or lost the game. These are displayed to the player when the game is over.
getMaskedWord: A simple getter for the maskedWord variable.
guess: We call this function whenever a player guesses a letter. After capitalizing the letter for consistency we make sure it hasn’t already been guessed. Then we add it to the the array of guessed letters and remove it from our list of possible guesses. If the word contains the guessed letter we determine what the indexes of those matches are. Then, we reveal those letters in the masked word that is displayed to the player. If the word does not contain the guessed letter we call the private handleIncorrectGuess function to increment the number of incorrect guesses and check for a loss.
getPossibleGuesses: The possibleGuesses variable returned as an array using the spread syntax.
getIncorrectGuesses: A simple getter for the incorrectGuesses variable.
guessWord: We call this function whenever the player wants to take a guess at the entire word or phrase.
isWon: A simple getter for the won variable.
isLost: A simple getter for the lost variable.
Private Methods of the Game Object
guessAllLetters: This function loops through all of the letters in the word and calls guess on them. Its purpose is to display the movie title after the player guesses it correctly, or alternatively when the player has lost the game.
handleIncorrectGuess: This function takes care of a few things whenever a player guesses wrongly. It updates the number of incorrect guesses and checks for a loss. If a loss has a occurred, it calls guessAllLetters.
There is one more function worth talking about in this section called replace. It doesn’t really belong in our game object because it has utility outside of that context. We are gonna keep it on its own in case we ever decide to use it later.
The function takes a string, an index, and a replacement string as parameters. It substitutes the replacement string for the character at the specified index. We use it to replace underscores with characters in our masked word.
That wraps up our discussion about the game object. Let’s move on and talk about how we handle player input.
Handling Player Input
Our goal is to make this game a fun experience on a desktop computer as well as a mobile phone. In order to do that we need to offer a few different ways to play.
One way to play involves using the keyboard to enter letters. This works great on a full-fledged hardware setup, but not so great on a mobile phone. For mobile users we ought to offer a way to use the touchscreen to make guesses.
Guessing a letter
Both of these input methods resolve down to the same basic action: sending a letter to our game object as a guess. This is a pretty good indicator that we should have a common function for handling this logic.
We will call this function guessLetter. It resides inside the listenForInput function which will be used for setting up our input handling logic. When called, and as long as the game is still going on, this function calls guess on our game object and then calls render to update the screen. We’ll talk more about the render function momentarily.
For both the keyboard and touchscreen input we attach event listeners to the document body.
Handling Touchscreen Input
When the player touches a letter (or clicks on it with his mouse), the handleClick function is called. This function checks the class list of the HTML element that was clicked. If the class list contains the “guess” class, that letter is passed to the guessLetter function.
Handling Keyboard Input
Handling keyboard input is a bit more complicated because there are more features to implement and more validation is necessary. The function that takes care of all this is called handleKeyPress.
The first thing we do is calculate some values that will help us take appropriate action. One of the first things we do is determine if the key that was pressed is a letter. Then, we grab HTML elements representing the guess word button, the new game button, and the guess box. We also check to see if the guess box is already populated with a win/loss message.
Finally, we take action. If the cursor is not in the guess box and player pressed a letter, we make a guess. If the game is over and the player pressed enter, we start a new game. And if the player pressed enter, the game is still going on, and there is content inside the guess box, we take a guess at the full movie title.
Guessing the Word
The player can take a guess at the full word by clicking the guess word button. He can also do so by pressing enter, as we saw in the previous section. When this occurs, the guessWord function is called.
This function first checks if the game is still going on. If it is, the guessWord function of the game object is called, and then render is called to update the screen.
Rendering the Graphics
Let’s discuss how to update the screen after changes are made to the game object. This is handled by the render function, and it is called after every guess that the player makes.
Overall it’s a pretty simple function. We set the innerHTML of the different elements that correspond to the data in our game object. We also set the image by using a neat little naming convention hack.
The trickiest part of this function is how we render the possible remaining guesses to the screen. We have to clear out the div and then loop through the possible guesses variable, appending the appropriate span as we go along.
Starting a New Game of Hangman
Okay, we’re almost done building hangman. Soon you’ll have all of the pieces needed for learning how to build hangman with JavaScript. The last thing to cover is kicking off a new game!
When the page loads, we call the Game function to create a new game object. Then we call render to display the game data to the screen, and call listenForInput to, well, start listening for input from the player.
We also have a function called newGame. If you observe the HTML, you’ll see that this function is attached to the new game button. Instead of going to the trouble of resetting our game object and event listeners, we use a neat little trick to go back to the most recent URL in the browser history. This should redirect the player to the URL of the hangman game, and reinitialize all of the JavaScript.
Conclusion
Congrats on learning how to build hangman with JavaScript! Hopefully it makes sense after breaking it up into modules like this. If you have questions or see ways to improve the code, I’d love to hear from you in the comments!
For your next steps, you could try adding some more features, like different, selectable word categories or supporting characters besides just letters.
Let me know in the comments what other simple games like this you would like to see, and don’t forget to drop me your email so you don’t miss the next one that comes out. You will also receive my free checklist for writing great functions.
In my last post I showed you guys the minesweeper game I built using JavaScript, and before that I built tic tac toe. For this week’s post I decided to show you guys how to build snake with JavaScript.
Now, the DOM was not originally designed for quickly updating graphics. There are better mediums to work in, but I wanted build something using basic web technologies. Still, I wanted it to be a little more dynamic than the games featured in my last tutorials. The snake game seemed to strike that balance well.
I also wanted to incorporate some of the feedback I received on my last post. I tried to use more of the modern features that JavaScript provides us, and I left jQuery behind.
With that out of the way, let’s get started. If you want to learn how to build snake with JavaScript, the first step is understanding how the game works. Let’s jump right in and talk about the rules.
Rules of the Game
The board is grid of cells through which the snake can traverse.
The player controls the head of the snake. It can only move in four directions: left, right, up, or down.
The snake is always moving, and the body follows the path set by the head.
Running into a wall or the tail of the snake causes the player to lose the game.
The goal of the game is to eat as much food as possible.
The snake grows by one in length whenever it eats a piece of food. Then, the game places a new piece of food in a random cell of the board.
Enums
JavaScript doesn’t really have the concept of an enum built in, but we can make our own version that will suffice. We are going to create two of them. They will prove themselves useful throughout the rest of our programming task.
Cell Type
The first enum to discuss is the CELL_TYPE. This an object containing three possible values: EMPTY, FOOD, and SNAKE. These are the only three values that can be assigned to a cell of the game board.
We declare it using the const keyword, which means we can’t assign the CELL_TYPE variable a different value. We also freeze the object to further lock it down. Our aim here is to create an immutable object. If the purpose of an object is to provide constant values we might as well try to prevent somebody else from changing it. This implementation gets us pretty far down that road while still being readable.
Direction
The second enum to discuss is the DIRECTION. This an object containing five possible values: NONE, LEFT, RIGHT, UP, and DOWN. These represent the directions in which the snake can be traveling. Its structure is very similar to the CELL_TYPE enum.
Objects
Our game consists of four main objects: the cell, the board, the snake, and the game. We have many cell objects that make up our board, but we only have one board, snake, and game object. Let’s discuss the functions we will use to create these objects.
Cell
We call the Cell function to create a very simple object representing one cell of our game board. The board consists of a grid of cells, and each cell has a few properties:
row: An integer representing which row on the board the cell belongs to.
column: An integer representing which column on the board the cell belongs to.
cellType: The value associated with the cell. This could be one of three values: EMPTY, SNAKE, or FOOD. These values come from the CELL_TYPE enum we discussed earlier.
Board
Okay now let’s talk about the board. The Board function is a blueprint for creating our board object. The board object we create should contain all of the logic for representing and modifying our game board.
The function takes two parameters: rowCount and columnCount. We use these two parameters to specify the size of the game board. We initialize our lone internal variable using these parameters, and end up with a two dimensional array named cells. Then we loop through our cells variable and create a cell object for each slot in the array. When starting out, each cell should be EMPTY.
That takes care of the board initialization. Now let’s talk about the methods and data that we publicly expose.
Public Methods of the Board
cells: Our two dimensional array of cells. We just talked about how to initialize the cells variable, and now we need to allow other parts of our program to access it.
render: This method handles the graphics of our game. It updates the HTML document on every iteration of the game loop. Each cell has the appropriate CSS class added or removed based on its cell type.
placeFood: This method randomly places food on the board. It leverages a private function we created, called getAvailableCells. This private function grabs a list of currently empty cells. Then, we place food in a randomly chosen one. We call placeFood as soon as the snake has enjoyed a nice warm meal.
getColumnCount: A simple method that returns the number of columns. You may be wondering why we don’t just expose the variable itself, like we did with our cells array. The reason is because we pass this number by value, not reference. When we return the cells object, we are passing by value as well, but the value happens to be a reference to the object. For further details about this topic, see this post and this other post.
getRowCount: A simple method that returns the number of rows.
Now let’s talk about our snake object.
Snake
The Snake function is a blueprint for creating our snake object. The snake object we create should contain all of the logic for representing and modifying our snake.
We pass three arguments to our Snake function: cell, startingLength, and board. The cell is the starting Cell of the head of the snake, the startingLength is the initial size of the snake, and the board is the game board object that we have created by calling the Board function.
We use these three arguments to initialize two internal variables of our snake object: its head cell and its snakeParts array. We simply assign the head variable to the cell that we passed in, and then set its cell type to SNAKE.
For the snakeParts, we first push the head of the snake onto the array. Then, using a for loop, we grab the cell from the next row of the board, set its cell type to SNAKE, and push it onto the array. We do this until the length of our snake matches the startingLength. This results in a vertical snake whose head is closest to the top of the game board. Of course, we are free to change this to suit our preferences.
Before moving on to talk about the publicly exposed methods of our snake, I’d like to point something out explicitly. We have designed the board and snake objects to share the same cell objects. This is an important detail to grasp if you want to understand how these objects work together.
Public Methods of the Snake
grow: This method simply pushes the internal head variable onto the internal snakeParts array. We call it after the snake eats a piece of delicious food.
move: This method is the snake’s most complicated one. First, it removes the tail of our snake and sets its cell type to EMPTY. Next, it sets the head of the snake to the cell being moved into, and sets that cell’s type to SNAKE. Finally, it loops through all of the snakeParts and sets the cell type to SNAKE for each. This last step handles the situation where the snake is moving right after growing in size. We call the move method once per iteration of our game loop.
checkCrash: This method checks if our snake has crashed into a wall or itself. If the cell is undefined then we know the snake has stepped out of the bounds of the game board and crashed. If the cell is not undefined then we check if the snake has collided with itself.
getHead: A simple getter for our head variable.
Now let’s talk about our game object.
Game
The Game function is a blueprint for creating our game object, and the game object we create should contain all of the logic for handling the interactions between the snake and the game board. We pass the snake and board objects to the Game function as arguments.
While the publicly exposed methods of our game object are quite complex, the initialization of the object is very simple. We have four internal variables: directions, direction, gameOver, and score.
The directions array starts out empty. We use it to queue up directions that the player has pressed. The queue helps us avoid frustrating behavior when a player inputs multiple directions before the game is able to update. We do not want to punish the player for quick fingers! Using a single variable to store the direction would result in a pretty annoying control scheme.
We set the initial direction of the snake to NONE. This makes the snake motionless at the start of a new game. The directions array will be used to feed in new values to this variable.
We initialize the gameOver variable to false. We don’t want to end the game before it has begun.
Finally, we initialize the score variable to zero. We will increase the score as the snake eats food.
Public Methods of the Game
update: This method puts all of the pieces together. We call it once per iteration of the game loop, and it contains the bulk of the logic needed for the snake and the board to work together. After checking to make sure the game is still going on, this method calls the private getNextCell function. This function figures out which direction the snake is headed, removes that direction from the queue, and returns the cell that the snake is about to enter. Then, the method checks if the snake has crashed. If the snake has crashed, we end the game and display the final score. Otherwise, the snake moves to the next cell. If there was food there, the snake eats it and grows and we place another piece of food on the board.
addDirection: This method takes a direction as an argument and pushes it onto the directions queue.
getLastDirection: This method returns the value of the last direction in the directions queue.
exceededMaxDirections: We set the maximum number of directions allowed in the queue to three. This prevents the player from spamming the arrow keys a huge number of times and building up a list of directions that are no longer relevant.
Snake Game Initialization
So far we have covered the building blocks necessary for putting together a game of snake with JavaScript. However, we still have a few more pieces to talk about that revolve around initializing a new game. These pieces include initializing the HTML, initializing the input handler, and starting a new game.
One thing to keep in mind while reading this code is that we want to be able create a fresh game state with ease. Modifying our objects directly seems cumbersome and error prone if we make more changes later. Instead, we are going to throw away our old objects and create new ones.
Initializing the HTML
The purpose of this function is to initialize the HTML elements that represent our snake game board. We start by grabbing all of the elements containing the “cell” CSS class. This should get us all of the cells. Then, we loop through each one of them.
Each cell has its id set to a value that represents its row and column number. Also, we remove the CSS classes from each cell and then add the “cell” class back. This ensures that we remove any classes that were added by the previous run of the game.
Initializing the Input Handler
The purpose of the listenForInput function is to handle the keyboard input provided by the player. For our game, the controls are the arrow keys. This function takes the game object as an argument.
The bulk of the logic takes place inside the changeDirection function. This function is responsible for determining when to add a direction to the directions queue of the game object. We add an event listener to our document that executes the changeDirection function on every keydown event. We clear and reset it each time we call the listenForInput function.
The first time changeDirection is triggered we add the UP direction to the queue. This helps us ensure that the player will not die on the first move. On subsequent calls to the function we listen for arrow key presses.
When the snake is moving vertically we do not allow the player to add vertical directions, and when the snake is moving horizontally we do not allow the player to add horizontal directions. This prevents the player from building up a queue of redundant directions or killing the snake inadvertently. We also prevent adding directions to the queue if it is already full. The movingHorizontally and movingVertically helper functions handle these checks, and make use of the publicly exposed methods of the game object to do so.
Starting A New Game
Alright we are getting close now. Just a few more steps and you will have all of the pieces for learning how to build snake with JavaScript! Let’s talk about how to start a new game using all of the building blocks we have constructed thus far.
We have a function called newGame that we call to start a new game. Inside this function we need to set up some constants for the row and column count of our board, as well as for the starting length of our snake. We will use the constants to initialize our game objects.
Once we have our constants, the first thing we do is initialize our board by calling the Board function and passing in our row and column counts as arguments. Then, we use those counts to calculate a good starting position for the head of our snake. We could hard-code this value, but I chose to programmatically place the snake’s head in the center of the board. We pass in this center cell, the board, and the starting length value to the Snake function to create our snake object. Finally, we use the board and snake objects to create our game object using the Game function.
Next, we call initializeCells to assign ids and CSS classes to our HTML cells (represented by div tags). Then we placeFood on the board and call render to show it to the player. After that we call listenForInput to start handling the player’s keyboard input. Finally, we start our game loop. On every iteration we update the board and then render the results on the screen.
The last bit of code attaches a click handler to a button in our pop up modal. We use this click handler for starting another game once the last one has ended. It hides the modal, stops the game loop, and calls the newGame function. We call the newGame function as the first thing we do, and then again every time the player clicks the play again button in the modal.
Conclusion
Congrats on learning how to build snake with JavaScript! That was a lot of code, but hopefully it makes sense after breaking it up into modules like this. If you have questions or see ways to improve the code, I’d love to hear from you in the comments!
I hope you guys enjoyed learning about how to build snake with JavaScript. Let me know what other simple games like this you would like to see, and don’t forget to drop me your email so you don’t miss the next one that comes out.
In my last post I showed you guys a tic tac toe game I built using JavaScript, and before that I built a matching game. For this week’s post I decided to ramp up the complexity a bit. You guys are going to learn how to build minesweeper with JavaScript. I also used jQuery, a JavaScript library that is helpful for interacting with html. Whenever you see a function call with a leading dollar sign, that is jQuery at work. If you want to learn more about it, the documentation for it is very good.
Click here to play minesweeper! You will want to play it on your desktop computer because of the control scheme.
If you want to learn how to build minesweeper with JavaScript, the first step is understanding how the game works. Let’s jump right in and talk about the rules.
Rules of the Game
The minesweeper board is a 10 x 10 square. We could make it other sizes, like the classic Windows version, but for demonstration purposes we will stick to the smaller, “beginner” version of the game.
The board has a predetermined number of randomly placed mines. The player cannot see them.
Cells can exist in one of two states: opened or closed. Clicking on a cell opens it. If a mine was lurking there, the game ends in failure. If there is no mine in the cell, but there are mines in one or more of its neighboring cells, then the opened cell shows the neighboring mine count. When none of the cell’s neighbors are mined, each one of those cells is opened automatically.
Right clicking on a cell marks it with a flag. The flag indicates that the player knows there is a mine lurking there.
Holding down the ctrl button while clicking on an opened cell has some slightly complicated rules. If the number of flags surrounding the cell match its neighbor mine count, and each flagged cell actually contains a mine, then all closed, unflagged neighboring cells are opened automatically. However, if even one of these flags was placed on the wrong cell, the game ends in failure.
The player wins the game if he/she opens all cells without mines.
Data Structures
Cell
Each cell is an object that has several properties:
id: A string containing the row and column. This unique identifier makes it easier to find cells quickly when needed. If you pay close attention you will notice that there are some shortcuts I take related to the ids. I can get away with these shortcuts because of the small board size, but these techniques will not scale to larger boards. See if you can spot them. If you do, point them out in the comments!
row: An integer representing the horizontal position of the cell within the board.
column: An integer representing the vertical position of the cell within the board.
opened: This is a boolean property indicating whether the cell has been opened.
flagged: Another boolean property indicating whether a flag has been placed on the cell.
mined: Yet another boolean property indicating whether the cell has been mined.
neighborMineCount: An integer indicating the number of neighboring cells containing a mine.
Board
Much like in the snake game I made, our board is a collection of cells. We could represent our board data in many different ways. I chose to represent it as an object with key value pairs. As we saw earlier, each cell has an id. The board is just a mapping between these unique keys and their corresponding cells.
After creating the board we have to do two more tasks: randomly assign the mines and calculate the neighboring mine counts. We’ll talk more about these algorithms in the next section.
Algorithms
Randomly Assign Mines
One of the first things we have to do before a game of minesweeper can be played is assign mines to cells. For this, I created a function that takes the board and the desired mine count as parameters.
One of the first things we have to do before a game of minesweeper can be played is assign mines to cells. For this, I created a function that takes the board and the desired mine count as parameters.
For every mine we place, we must generate a random row and column. Furthermore, the same row and column combination should never appear more than once. Otherwise we would end up with less than our desired number of mines. We must repeat the random number generation if a duplicate appears.
As each random cell coordinate is generated we set the mined property to true of the corresponding cell in our board.
I created a helper function in order to help with the task of generating random numbers within our desired range. See below:
Calculate Neighbor Mine Count
Now let’s look at what it takes to calculate the neighboring mine count of each cell in our board.
You’ll notice that we start by looping through each row and column on the board, a very common pattern. This will allow us to execute the same code on each of our cells.
We first check if each cell is mined. If it is, there is no need to check the neighboring mine count. After all, if the player clicks on it he/she will lose the game!
If the cell is not mined then we need to see how many mines are surrounding it. The first thing we do is call our getNeighbors helper function, which returns a list of ids of the neighboring cells. Then we loop through this list, add up the number of mines, and update the cell’s neighborMineCount property appropriately.
Won’t you be my neighbor?
Let’s take a closer look at that getNeighbors function, as it will be used several more times throughout the code. I mentioned earlier that some of my design choices won’t scale to larger board sizes. Now would be a good time to try and spot them.
The function takes a cell id as a parameter. Then we immediately split it into two pieces so that we have variables for the row and the column. We use the parseInt function, which is built into the JavaScript language, to turn these variables into integers. Now we can perform math operations on them.
Next, we use the row and column to calculate potential ids of each neighboring cell and push them onto a list. Our list should have eight ids in it before cleaning it up to handle special scenarios.
While this is fine for the general case, there are some special cases we have to worry about. Namely, cells along the borders of our game board. These cells will have less than eight neighbors.
In order to take care of this, we loop through our list of neighbor ids and remove any id that is greater than 2 in length. All invalid neighbors will either be -1 or 10, so this little check solves the problem nicely.
We also have to decrement our index variable whenever we remove an id from our list in order to keep it in sync.
Is it mined?
Okay, we have one last function to talk about in this section: isMined.
The isMined function is pretty simple. It just checks if the cell is mined or not. The function returns a 1 if it is mined, and a 0 if it is not mined. This feature allows us to sum up the function’s return values as we call it repeatedly in the loop.
That wraps up the algorithms for getting our minesweeper game board set up. Let’s move on to the actual game play.
Opening A Cell
Alright let’s dive right into this bad boy. We execute this function whenever a player clicks on a cell. It does a lot of work, and it also uses something called recursion. If you are unfamiliar with the concept, see the definition below:
Recursion: See recursion.
Ah, computer science jokes. They always go over so well at bars and coffee shops. You really ought to try them out on that cutie you’ve been crushing on.
Anyways, a recursive function is just a function that calls itself. Sounds like a stack overflow waiting to happen, right? That’s why you need a base case that returns a value without making any subsequent recursive calls. Our function will eventually stop calling itself because there will be no more cells that need to be opened.
Recursion is rarely the right choice in a real world project, but it is a useful tool to have in your toolbox. We could have written this code without recursion, but I thought you all might want to see an example of it in action.
Handle Click Explained
The handleClick function takes a cell id as a parameter. We need to handle the case where the player pressed the ctrl button while clicking on the cell, but we will talk about that in a later section.
Assuming the game isn’t over and we are handling a basic left click event, there are a few checks we need to make. We want to ignore the click if the player already opened or flagged the cell. It would be frustrating for the player if an inaccurate click on an already flagged cell ended the game.
If neither of those are true then we will proceed. If a mine is present in the cell we need to initiate the game over logic and display the exploded mine in red. Otherwise, we will open the cell.
If the opened cell has mines surrounding it we will display the neighboring mine count to the player in the appropriate font color. If there are no mines surrounding the cell, then it is time for our recursion to kick in. After setting the background color of the cell to a slightly darker shade of gray, we call handleClick on each unopened neighboring cell without a flag.
Helper Functions
Let’s take a look at the helper functions we are using inside the handleClick function. We’ve already talked about getNeighbors, so we’ll skip that one. Let’s start with the loss function.
When a loss occurs, we set the variable that tracks this and then display a message letting the player know that the game is over. We also loop through each cell and display the mine locations. Then we stop the clock.
Second, we have the getNumberColor function. This function is responsible for giving us the color corresponding to the neighboring mine count.
I tried to match up the colors just like the classic Windows version of minesweeper does it. Maybe I should have used a switch statement here, but I already took the screen shot, and it’s not really a big deal. Let’s move on to what the code looks like for putting a flag on a cell.
Flagging A Cell
Right clicking on a cell will place a flag on it. If the player right clicks on an empty cell and we have more mines that need to be flagged we will display the red flag on the cell, update its flagged property to true, and decrement the number of mines remaining. We do the opposite if the cell already had a flag. Finally, we update the GUI to display the number of mines remaining.
Opening Neighboring Cells
We have covered the actions of opening cells and marking them with flags, so let’s talk about the last action a player can take: opening an already opened cell’s neighboring cells. The handleCtrlClick function contains the logic for this. This player can perform this action by holding ctrl and left clicking on an opened cell that contains neighboring mines.
The first thing we do after checking those conditions is build up a list of the neighboring flagged cells. If the number of flagged cells matches the actual number of surrounding mines then we can proceed. Otherwise, we do nothing and exit the function.
If we were able to proceed, the next thing we do is check if any of the flagged cells did not contain a mine. If this is true, we know that the player predicted the mine locations incorrectly, and clicking on all of the non-flagged, neighboring cells will end in a loss. We will need to set the local lost variable and call the loss function. We talked about the loss function earlier in the article.
If the player did not lose, then we will need to open up the non-flagged neighboring cells. We simply need to loop through them and call the handleClick function on each. However, we must first set the ctrlIsPressed variable to false to prevent falling into the handleCtrlClick function by mistake.
Starting A New Game
We are almost done analyzing all of the JavaScript necessary to build minesweeper! All that we have left to cover are the initialization steps necessary for starting a new game.
The first thing we do is initialize a few variables. We need some constants for storing the html codes for the flag and mine icons. We also need some constants for storing the board size, the number of mines, the timer value, and the number of mines remaining.
Additionally, we need a variable for storing if the player is pushing the ctrl button. We utilize jQuery to add the event handlers to the document, and these handlers are responsible for setting the ctrlIsPressed variable.
Finally, we call the newGame function and also bind this function to the new game button.
Helper Functions
Th newGame function is responsible for resetting our variables so that our game is in a ready-to-play state. This includes resetting the values that are displayed to the player, calling initializeCells, and creating a new random board. It also includes resetting the clock, which gets updated every second.
Let’s wrap things up by looking at initializeCells.
The main purpose of this function is to add additional properties to our html game cells. Each cell needs the appropriate id added so that we can access it easily from the game logic. Every cell also needs a background image applied for stylistic reasons.
We also need to attach a click handler to every cell so that we can detect left and right clicks.
The function that handles left clicks calls handleClick, passing in the appropriate id. Then it checks to see if every cell without a mine has been opened. If this is true then the player has won the game and we can congratulate him/her appropriately.
The function that handles right clicks calls handleRightClick, passing in the appropriate id. Then it simply returns false. This causes the context menu not to pop up, which is the default behavior of a right click on a web page. You wouldn’t want to do this sort of thing for a standard business CRUD application, but for minesweeper it is appropriate.
Conclusion
Congrats on learning how to build minesweeper with JavaScript! That was a lot of code, but hopefully it makes sense after breaking it up into modules like this. We could definitely make more improvements to this program’s reusability, extensibility, and readability. We also did not cover the HTML or CSS in detail. If you have questions or see ways to improve the code, I’d love to hear from you in the comments!
I hope you guys enjoyed learning about how to build minesweeper with JavaScript. Let me know what other simple games like this you would like to see, and don’t forget to drop me your email so you don’t miss the next one that comes out.
Update (7/13/2019): This post became more popular than I thought it would, which is awesome! I’ve received a lot of great feedback from readers about areas that could be improved. I work daily in a code base that until recently was stuck in Internet Explorer quirks mode. Many of my daily habits there transferred to my work on minesweeper, resulting in some code that doesn’t take advantage of the bleeding edge of JavaScript technology. At some point I would like to do another post where I refactor the code. I plan to remove jQuery entirely and use the ES6 syntax instead of the ES5 syntax where appropriate. But you don’t have to wait for me! See if you can make these changes yourself! And let me know how it goes in the comments.
Update (10/13/2020): I finally got around to making some improvements! Check out part 2 of this project. I removed jQuery entirely and added a feature to prevent immediate losses due to bad luck on the first click. I hope you enjoy it!