How to Build Hangman in JavaScript From Scratch

How to Build Hangman in JavaScript From Scratch

Use the learnings so far to build a hangman with the help of NodeJS
Ferenc Almasi • 2023 January 26 • Read time 22 min read
Learn how you can build the famous hangman game in JavaScript with the help of NodeJS as a command line game.
  • twitter
  • facebook
JavaScript

This lesson is a preview from our interactive course

In the very first lesson, we briefly touched on NodeJS when we were talking about how to execute JavaScript. As a reminder, NodeJS is a JavaScript engine that lets us easily run JavaScript code without a browser. If you haven’t already, download and install the recommended version for NodeJS, then restart your code editor to make it aware of Node.

You will find the entire code in one piece at the end of this lesson.


Setting up NodeJS

NodeJS is already set up in the interactive widget. This section only describes how to set up a new NodeJS project on your local machine.

Once you have it installed, we will need to create a new NodeJS project in order to create the game. Create a new project folder in your IDE, then inside your terminal, type in npm init -y. This will bootstrap a new NodeJS project at the root of our project directory. It will automatically create a package.json. We also have this file inside the interactive widget. Open the sidebar and open package.json to view its content.

This holds some meta-information about the project, such as its version, or the different packages this project depends on. The NodeJS ecosystem is full of prebuilt JavaScript packages that we can import into our project for use. This means we don’t have to reinvent the wheel and start everything from ground zero. In this lesson, we are going to use one package to help with CLI (command-line interface) commands. It is called Inquirer.

To know what packages are available for use, you can look around at npmjs.com.  

Inquirer is a tool that can help us create command-line user interfaces. To install this package as a dependency, type in your terminal the following: npm install inquirer. We can use the npm install <package-name>, or npm i <package-name> commands to install new packages.

Copied to clipboard!
npm install inquirer
Run the command inside the terminal of the interactive widget

This will create a new line inside our package.json file, where we now have Inquirer added as a dependency. It will also create a node_modules folder, where all the dependencies of the project are stored. We can ignore this folder at all times, as you should.

Never make any changes to third-party packages inside node_modules.

Notice that this folder is hidden by default in the interactive widget. Now that we have NodeJS and all necessary packages up and ready, we can focus our attention on building the game. Create a new file at the root of your project called hangman.js. This file is already created inside the interactive widget.

The main logic of the game will go into this file. But first, let’s understand how the game will work.


Understanding the Game Logic

Before jumping straight into coding anything, it is important to first understand the logic behind any project. Generally speaking, we will have the following logic for the game:

Copied to clipboard!
Game starts -> Guessing letters -> All letters guessed --> You won!
                       \---------> No remaining guesses -> You lose!

We will start off by asking for guesses for letters from the player. From here, the game’s state can branch into two ways. If all letters are guessed, the player wins the game. If there are no remaining guesses, the player loses.

On a deeper level, we will start off with a random word that needs to be guessed. We can have 10 wrong guesses before we lose the game. For each letter guessed, we want to reveal where the letter is positioned in the word, and how many letters the word has. We want to keep going until all letters are guessed correctly, or we run out of wrong guesses.

Based on this information, we can already create some pseudocode to understand what we need to implement:

Copied to clipboard! Playground
// Starting variables
word = randomWord
guessesRemaining = 10

// We want to verify the input, eg don't accept numbers
askForLetter:
    if inputNotLetter
        dontAccept
    else
        accept -> handleInput

handleInput:
    letterToLowerCase // To match words stored in the game
  
    // Make sure the same letter cannot be guessed multiple times
    if letterNotGuessedYet
        addLettertoGuesses
        showObscuredWord
		
        if guessedRight
            allLettersGuessed ? "You won!" : askForLetter
        else
            guessesRemaining--
            guessesRemaining == 0 ? "Game over!" : askForLetter
    else
        "Already guessed, try another letter." -> askForLetter
Creating the pseudocode for the game

To summarize:

Looking to improve your skills? Check out our interactive course to master JavaScript from start to finish.
Master JavaScriptinfo Remove ads

Setting up Variables and Functions

Let’s start setting up the variables and functions we need. First, we want to import Inquirer to our project as we will use it. Inside the hangman.js file, add the following first line:

Copied to clipboard!
import inquirer from 'inquirer';
Importing the inquirer package

This tells JavaScript that the inquirer variable now should hold the Inquirer package. We can use the import-from syntax to import modules in NodeJS.

We can also import our own files too. There is a data.js file created next to hangman.js where we can store the words and the state of the hangman, which will be represented by ASCII art.

ASCII is a character encoding standard for communication. It stands for “American Standard Code for Information Interchange”.

Open the sidebar and inspect the contents of data.js. The words array will hold all words that we can guess. We can add as many words as we would like here as we are going to programmatically choose a random word from this array. The state array on the other hand will hold 10 different ASCII art about the hangman, at each index getting closer to the final state.

Notice that for the right arm and leg, we need to escape the backslash, and because of the double characters, we also need to offset the base of the hanger too by two spaces. Another thing you may notice is that the indentation starts from the very left side. This is so that in the CLI, everything will be indented correctly. For this reason, make sure you only use spaces, and not tabs, as they can mess up the formatting of the art.

At the end of the file, we can use export default to export these variables inside an array. This export is required in order to import it inside hangman.js. Now we can also import this data into our main game file under Inquirer:

Copied to clipboard!
import inquirer from 'inquirer';
import data from './data.js';

const [words, state] = data;
Import data into our main file

We can use destructuring to retrieve the two variables from the array that we exported.

As we are using a default export, we can name our import in any way we like. If we were to have a named export, we would need to use the same name in the import too. This is illustrated in the following example:

Copied to clipboard! Playground
// Default exports can take any name when imported
export default []

// ✔️ This will all work
import data from './data.js';
import hangman from './data.js';
import whatever from './data.js';

// ------------------------------------------------
// Named exports can only be imported by their name
export const data = []

// ❌ This will not work
import hangman from './data.js';
import whatever from './data.js';

// ✔️ Only this will work
import data from './data.js';

CJS vs ESM format

If we try to run the application with node hangman.js inside the terminal, we will get the following error:

Copied to clipboard!
SyntaxError: Cannot use import statement outside a module

This is because NodeJS doesn't treat our files as modules. It currently uses the require keyword for imports, and module.exports for exports. This is often referred to as CommonJS format. The import/exportkeywords are using ES module format. In order to make our app accept the ESM (ES Module) format, we need to add a new field to our package.json to mark our files as modules.

Copied to clipboard!
{
    "type": "module"
}
Add the new field to your package.json

With this ready, we can continue with setting up our variables. We are also going to need a couple of other things, namely the following:

Copied to clipboard!
const guessedLetters = [];
const word = words[Math.floor(Math.random() * words.length)];
let guessesRemaining = 10;
Add the variables to the interactive widget

guessedLetters will be an array containing all letters that have been guessed. For the word that needs to be guessed, we want a random word from the words array. For this, we can use Math.random in combination with Math.floor. Notice that we need to multiply it by the length of the words, as by default, Math.random returns a pseudo-random number between 0 and 1.

We also need to keep track of the remaining guesses, which in our case will be 10. Apart from this, we will have one main function, one for asking for guesses:

Copied to clipboard! Playground
const askForLetter = () => {
    // The main logic of the game will go here
}

console.log('Welcome to Hangman!');
console.log('___________________');

askForLetter();
Define the main function

Let’s also not forget to welcome our players. We can test this file by running node hangman.js to execute it. At the moment, nothing else will happen apart from a welcome message appearing in the terminal.


Asking for Guesses Using Inquirer

Now let’s start the work on the askForLetter function and ask for some input from the user. To do this, add the following call to the function:

Copied to clipboard! Playground
const askForLetter = () => {
    inquirer.prompt([{
        name: 'letter',
        type: 'input',
        message: 'Guess a letter:',
    }]).then(input => {
        // Here we will handle the game's logic
    });
}
Asking for user input

So what is going on here exactly? If we head over to NPMJS to read about the documentation of Inquirer, we can find that there is a prompt method we can use to ask questions in the command line.

We can actually ask a number of questions, so the method expects an array, and each question can have a number of properties in the form of an object. This is why we need to pass an array with one object to this method. This object will be our question. This question has the following properties:

We can also chain a then callback from the prompt. This takes in a callback function with the input that is provided by the player. This essentially means that whenever a letter is guessed, then we should do something.

If we try this out now, Inquirer will ask us for a letter, however, we can still input numbers too. To filter those out, we can introduce a validate method to either return true (if the input is correct) or false (if the input is incorrect) based on the user’s input:

Copied to clipboard! Playground
const askForLetter = () => {
    inquirer.prompt([{
        name: 'letter',
        type: 'input',
        message: 'Guess a letter:',
        validate(value) {
            if (/^[A-Za-z]$/.test(value)) {
                return true;
            } else {
                return false;
            }
        }
    }]).then(input => {

    });
}
Add the above validation to your prompt

This method takes in the input value as an argument. Here we used a regular expression to filter out numbers and only allow letters. So what is a regular expression and what does the pattern mean exactly?

Explaining regular expressions

Regular expressions are expressions that can be used to match a pattern, provided between the slashes. It can have a bunch of special characters, each having its own meaning. Here we have the following in order:

In JavaScript, we can use the test method on regular expressions, passing a string to see if it matches the pattern. If the input is a single letter, it will match the pattern and we return true. Otherwise, for everything else (including more than one letter), we return false.

Now whenever we try to input an incorrect value, such as a number, Inquirer will reask the question, and won’t go further, unless we provide a letter.


Handling Game Logic

Now we can focus our attention on the then callback. First things first, we need to convert the input into lowercase, as all words will be lowercase inside data.js too. Try to convert input.letter to lowercase and assign it to a variable called guessedLetter.

Copied to clipboard!
const guessedLetter = input.letter.toLowerCase();
Converting the input into lowercase

We can do this by accessing the letter property on the input. As mentioned before, this is the name of the question, hence the reason we can access it here as letter. Next, we want to check whether the letter has been already guessed or not (we can follow the logic from the pseudocode):

Copied to clipboard! Playground
if (!guessedLetters.includes(guessedLetter)) {
    // This letter has not been guessed yet, so let's test it
} else {
    console.log('Already guessed, try another letter.');
    askForLetter();
}
Check if the letter has already been guessed

We can do this by using the includes method on the guessedLetters array. If this array doesn’t include the guessed letter, then we can progress further and do the necessary steps. Otherwise, we can inform the player that this letter has already been guessed, and we can recall this function to ask for another guess. As we can see, this function is a recursive function, as it is calling itself in certain cases.

In case the letter has not been guessed yet, we can add it to the guessedLetters array and also log out the word with all the letters showing which has been guessed correctly:

Copied to clipboard! Playground
if (!guessedLetters.includes(guessedLetter)) {
    guessedLetters.push(guessedLetter);

    console.log(getObscuredWord());
} else {
    ...
}
Adding the letter as a guess, and showing the obscured word

We can simply push the guessed letter into the guesses array and log out the word. As we can see, we can outsource the partly obscured word into a separate function, as we are going to need additional logic on how to display it.

Displaying the obscured word

Now that there is at least one match, it means we need to show the word, but we should not reveal all letters, just the ones that have been guessed. Let’s see what the function looks like in one piece, and then we can break it down:

Copied to clipboard! Playground
const getObscuredWord = () => {
    const obscuredWord = word.split('').map(letter => {
        if (guessedLetters.includes(letter)) {
            return letter;
        }

        return '_';
    }).join('');

    return `Your word is: ${obscuredWord}`;
}
Creating the getObscuredWord function

It is clear that we need to return a string at the end of the function. A string that contains the obscured word. So how do we put the word together? This can be done with a simple combination of split, map and join.

At the end of this function, we can return this word. Now whenever a letter is guessed, we can show it inside the word. However, as soon as we guess a letter, it exits the game because we haven’t handled what should happen on right and wrong guesses.

undefined
Guessing a letter will immediately exit the game

Right and Wrong Guesses

For this, we are going to need another branch inside the previous if statement. After we show the obscured word, we need to check whether the word includes the guessed letter or not:

Copied to clipboard! Playground
// Guessed a right letter
if (word.includes(guessedLetter)) {
    ...
// Guessed a wrong letter
} else {
    ...
}
Checking if the guess was right or wrong

Again, we can check this with another includes. Let’s see first what we need to do if the player guesses the right letter. We either want them to guess another or if they guessed all the letters correctly, we want to phase into the win state.

To achieve this, we can use the word to be guessed, and the already guessed letters, and we can find their intersection. If all letters are contained in both arrays, the player guessed everything and we win:

Copied to clipboard! Playground
// Guessed a right letter
if (word.includes(guessedLetter)) {
    const intersection = word
        .split('')
        .filter(letter => guessedLetters.includes(letter));

    // If every letter of the word is guessed already, you won!
    if (intersection.length === word.length) {
        console.log('🎉 You won!!! 🎉');
    } else {
        askForLetter();
    }
// Guessed a wrong letter
} else {
  ...
}
Handling the win state

To find the intersection, we first need to create an array from the word by using split. Next, we need to filter for those values that are already included in the guessedLetters array. If both of their length matches (meaning all guessed letters are in the word), then we can log out to the player that they won! Otherwise, we recall this function to ask for another guess.

Ending the game

There is only one last case we need to cover. If a guess was wrong, we want to decrement the remaining guesses, and if the player runs out of wrong guesses, we end the game with a game over. This can be achieved with another if-else:

Copied to clipboard! Playground
// Guessed a wrong letter
} else {
    guessesRemaining--;

    console.log(state[9 - guessesRemaining]);
    console.log(`You have ${guessesRemaining} guesses remaining.`);
    
    // If no more guesses left, it's game over.
    if (guessesRemaining === 0) {
        console.log('🚨 GAME OVER!!!');
    } else {
        askForLetter();
    }
}
Ending the game

We can decrement the number of remaining guesses by one using --. Then we can log out the ASCII art and the information about the remaining guesses. To get the correct ASCII art, we want to grab one of the indexes from the state variable. We want to start from 0, and we have 10 guesses in total (9 after the first wrong guess). This means we want to subtract the number of guessesRemaining from 9 to get the correct index for the ASCII art.

In the first wrong guess, this will be 0 (9-9), so we use the very first index, and as the number of guesses remaining decreases, the index of the state increases:

Copied to clipboard!
9-8 -> 1
9-7 -> 2
... and so on

If we reach 0, the game is over. Otherwise, we can ask for another guess. And that’s all, you have just finished your very first JavaScript game, congratulations! 🎉 There is a lot you can learn from building projects, this is why it is recommended that you start building your own projects too after finishing this course, to get more practice using JavaScript.

You can find the full source code in one piece below. In the next lesson, we are going to start working with objects.

Copied to clipboard! Playground
import inquirer from 'inquirer';
import data from './data.js';

const [words, state] = data;
const guessedLetters = [];
const word = words[Math.floor(Math.random() * words.length)];
let guessesRemaining = 10;

const askForLetter = () => {
    inquirer.prompt([{
        name: 'letter',
        type: 'input',
        message: 'Guess a letter:',
        validate(value) {
            if(/^[A-Za-z]$/.test(value)) {
                return true;
            } else {
                return false;
            }
        }
    }]).then(input => {
        const guessedLetter = input.letter.toLowerCase();

        // If this letter has not been guessed yet
        if (!guessedLetters.includes(guessedLetter)) {
            guessedLetters.push(guessedLetter);

            console.log(getObscuredWord());

            // Guessed a right letter
            if (word.includes(guessedLetter)) {
                const intersection = word
                    .split('')
                    .filter(letter => guessedLetters.includes(letter));

                // If every letter of the word is guessed already, you won!
                if (intersection.length === word.length) {
                    console.log('🎉 You won!!! 🎉');
                } else {
                    askForLetter();
                }
            // Guessed a wrong letter
            } else {
                guessesRemaining--;

                console.log(state[9 - guessesRemaining]);
                console.log(`You have ${guessesRemaining} guesses remaining.`);
                
                // If no more guesses left, it's game over.
                if (guessesRemaining === 0) {
                    console.log('🚨 GAME OVER!!!');
                } else {
                    askForLetter();
                }
            }
        } else {
            console.log('Already guessed, try another letter.');
            askForLetter();
        }
    });
}

const getObscuredWord = () => {
    const obscuredWord = word.split('').map(letter => {
        if (guessedLetters.includes(letter)) {
            return letter;
        }

        return '_';
    }).join('');

    return `Your word is: ${obscuredWord}`;
}

console.log('Welcome to Hangman!');
console.log('___________________');

askForLetter();
hangman.js
Don't forget to set the project type to "module"
  • twitter
  • facebook
JavaScript
Did you find this page helpful?
📚 More Webtips
Mentoring

Rocket Launch Your Career

Speed up your learning progress with our mentorship program. Join as a mentee to unlock the full potential of Webtips and get a personalized learning experience by experts to master the following frontend technologies:

Courses

Recommended

This site uses cookies We use cookies to understand visitors and create a better experience for you. By clicking on "Accept", you accept its use. To find out more, please see our privacy policy.