We Do Tech, stuff.

Small business web and system solutions.

Free programs, software reviews, and tech, stuff.

Javascript Hangman Tutorial

by: Dan Orlovsky
Posted On: 8/31/2017 9:30:27 AM

In a javascript bootcamp, our first assignment was to make a hangman game. Here is how I arrived at my solution.


Github: https://github.com/DanOrlovsky/Hangman-Game 
Live Demo: https://danorlovsky.github.io/Hangman-Game 


The Layout

The title image is actually how our layout will look.  We were supposed to have a theme, so I chose to do a programming theme for mine.  All images/code are available through the Github link above.

First, I will display the HTML and the CSS - I am assuming knowledge of both these technologies and won't spend a lot of time on it here:

<!doctype html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Hangman!</title>
    <link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ"
        crossorigin="anonymous">
    <link rel="stylesheet" href="assets/css/style.css" />
    <script src="assets/js/game.js" type="text/javascript"></script>
</head>

<body>
    <div class="container">
        <img id="title-image" src="assets/images/hangman-title.png" alt="Hangman Title Image" />
        <div class="row">
            <div class="col-md-6 gameplay-column">
                <img id="youwin-image" src="assets/images/you-win.png" alt="Winning Image" />
                <img id="gameover-image" src="assets/images/gameover.png" alt="Game Over Image" />
                <h2 id="pressKeyTryAgain">Press Any Key to Try Again!</h2>
                <img id="hangmanImage" src="" alt="" />
            </div>
            <div class="col-md-6 gameplay-column">
                <h2>Press Any Key to Get Started!</h2>
                <h4>Wins</h4>
                <h5 id="totalWins"></h5>
                <h4>Current Word</h4>
                <h3 id="currentWord"></h3>
                <h4>Guesses Remaining</h4>
                <h6 id="remainingGuesses"></h6>
                <h4>Letters Guessed</h4>
                <h3 id="guessedLetters"></h3>
            </div>
        </div>
    </div>

    <script>
        resetGame();
        updateDisplay();
    </script>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/js/bootstrap.min.js" integrity="sha384-vBWWzlZJ8ea9aCX4pEW3rVHjgjt7zpkNpZk+02D9phzyeVkE+jo0ieGizqPLForn"
        crossorigin="anonymous"></script>
</body>

</html>

 

 What you really need to gather from the HTML is that:

  1. It's simply the display layout
  2. The IDs are important because we are going to use them in the Javascript

 In the CSS, we simply lay out a few styles - again, nothing important or spectacular happening here, so I'm just going to provide the code:

body {
    background-color: #eee;
    padding-top: 20px;
    font-family: Courier New, Courier, monospace;
    color: white;
    text-shadow: 0 0 2px black, -1px -1px 2px black, 1px 0 2px black, 1px 1px 2px black;
    background: url('../images/bgimage.jpg') no-repeat;
    background-size: cover;
}
.container { 
    text-align: center;
    
}
h1,h2,h3,
h4,h5,h6 {
    padding: 5px;
    background-color: rgba(101,155,242,.2);
}
#title-image, #gameover-image, #youwin-image{ 
    display: block;
    margin: 0 auto;
}

#currentWord {
    letter-spacing: 10px;
}

#pressKeyTryAgain, 
#gameover-image, 
#youwin-image { display: none;}
.gameplay-column {
    padding-top: 20px;
    border: 3px dashed #fff;
    text-align: center;
    margin-top: 20px;
    min-height: 500px;
    background-color: rgba(255, 255, 255, .3);
}

 

The Game Logic

The game logic exists in its own .js file.  So, the first thing we need to do is figure out exactly what data is needed at the global level.  Here is what I came up with:

var selectableWords =           // Word list
    [
        "csharp",
        "cplusplus",
        "rubyonrails",
        "python",
        "javascript",
        "ansic",
        "cobol",
        "fortran",
        "visualbasic",
        "compiler",
        "algorithm",
    ];

const maxTries = 10;            // Maximum number of tries player has

var guessedLetters = [];        // Stores the letters the user guessed
var currentWordIndex;           // Index of the current word in the array
var guessingWord = [];          // This will be the word we actually build to match the current word
var remainingGuesses = 0;       // How many tries the player has left
var gameStarted = false;        // Flag to tell if the game has started
var hasFinished = false;        // Flag for 'press any key to try again'     
var wins = 0;                   // How many wins has the player racked up

 

I stored the list of words in an array, so we can guess one at random.  maxTries is a constant that holds how many failed guesses a user gets.  It turns out, 10 is a lot!  guessedLetters holds an array of unique letters that have already been guessed.  currentWordIndex is where we will store the index of the randomly selected word.  guessingWord is an array that stores the letters that have been properly guessed.  remainingGuesses will start with the value of maxTries and decrement for every wrong guess, gameStarted was added when I first started - but its functionality has been replaced by hasFinished.  Finally, wins stores the amount of times the player successfully guessed a word.

My first order of business was creating a function that sets all of these variables up for the start of the game:  (the updateDisplay() function will be defined in the next section).

// Reset our game-level variables
function resetGame() {
    remainingGuesses = maxTries;
    gameStarted = false;

    // Use Math.floor to round the random number down to the nearest whole.
    currentWordIndex = Math.floor(Math.random() * (selectableWords.length));

    // Clear out arrays
    guessedLetters = [];
    guessingWord = [];

    // Make sure the hangman image is cleared
    document.getElementById("hangmanImage").src = "";

    // Build the guessing word and clear it out
    for (var i = 0; i < selectableWords[currentWordIndex].length; i++) {
        guessingWord.push("_");
    }
    // Hide game over and win images/text
    document.getElementById("pressKeyTryAgain").style.cssText= "display: none";
    document.getElementById("gameover-image").style.cssText = "display: none";
    document.getElementById("youwin-image").style.cssText = "display: none";

    // Show display
    updateDisplay();
};

 

First, we reset the number of remaining tries.  Then we randomly select a number between 0 and the length of our selectableWords array.  Notice we are using Math.floor()?  This is because the Math.random() function will return a float - Math.floor() will round that down to the nearest whole number.  We then initialize the guessingWord array to the length of the selectableWord, and initialize it with underscores "_" - these will get replaced as the user makes correct guesses.

function updateDisplay() does exactly that - it puts new information into our HTML placeholders for the game display:

//  Updates the display on the HTML Page
function updateDisplay() {

    document.getElementById("totalWins").innerText = wins;
    document.getElementById("currentWord").innerText = "";
    for (var i = 0; i < guessingWord.length; i++) {
        document.getElementById("currentWord").innerText += guessingWord[i];
    }
    document.getElementById("remainingGuesses").innerText = remainingGuesses;
    document.getElementById("guessedLetters").innerText = guessedLetters;
    if(remainingGuesses <= 0) {
        document.getElementById("gameover-image").style.cssText = "display: block";
        document.getElementById("pressKeyTryAgain").style.cssText = "display:block";
        hasFinished = true;
    }
};

 

 Note: if I had to write "document.getElementById" one more time, I probably would've stored all of the elements as objects (i.e.  var winImage = document.getElementById("youwin-image"); ).  I may approach future projects in that manner.

Since we're working on the display - I have a method that updates which image of the "hanging man" we should display.  If you look in the assets/images/ folder on Github, you'll see images named: 1.png, 2.png, etc.  I did it this way so I can build the image file on the fly using what try we are on, like so:

// Updates the image depending on how many guesses
function updateHangmanImage() {
    document.getElementById("hangmanImage").src = "assets/images/" + (maxTries - remainingGuesses) + ".png";
};

 

Since remainingGuesses go in descending order, we have to subtract the remaining guesses from the maximum amount.  Again, this function changes the image of the hangman and displays it on screen.

To capture keypresses on the Document, I use an onkeydown Event Listener.  Everytime a key is in the DOWN state, the event listener fires a function.  The way my code is organized, I keep this at the bottom of the source (it really doesn't matter where, this was my preference), but I feel it's relevant before we move on.

document.onkeydown = function(event) {
    // If we finished a game, dump one keystroke and reset.
    if(hasFinished) {
        resetGame();
        hasFinished = false;
    } else {
        // Check to make sure a-z was pressed.
        if(event.keyCode >= 65 && event.keyCode <= 90) {
            makeGuess(event.key.toLowerCase());
        }
    }
};

 

First, we're checking our hasFinished flag.  If it's true, we reset the game and flag it to false.  This allows the user to press a key and restart the game, rather than taking input and running the makeGuess

Notice we're checking if the keyCode is between 65 and 90?  65 is the key-code for 'A', and 90 is the key-code for 'Z' which are the only relevant keys to our game.  We do not want to fire the makeGuess function on anything other than letters.  We also call toLowerCase() so it won't matter whether the user is pressing lower or uppercase letters.  Our makeGuess function expects the actual value of the key pressed in order to compare it to the word we are trying to guess:

function makeGuess(letter) {
    if (remainingGuesses > 0) {
        if (!gameStarted) {
            gameStarted = true;
        }

        // Make sure we didn't use this letter yet
        if (guessedLetters.indexOf(letter) === -1) {
            guessedLetters.push(letter);
            evaluateGuess(letter);
        }
    }
    
    updateDisplay();
    checkWin();
};

 

Before we do anything, we make sure the user has remaining guesses.  You can ignore gameStarted, as this functionality has been replaced with hasFinished.  Then, we search our guessedLetters array for the letter we are guessing with.  If it returns -1, that means it is not in our array, so we use push() to add it.  Then we pass that letter to evaluateGuesswhere we further evaluate the guess.  Finally, we call our updateDisplay() and checkWin() functions.

// This function takes a letter and finds all instances of 
// appearance in the string and replaces them in the guess word.
function evaluateGuess(letter) {
    // Array to store positions of letters in string
    var positions = [];

    // Loop through word finding all instances of guessed letter, store the indicies in an array.
    for (var i = 0; i < selectableWords[currentWordIndex].length; i++) {
        if(selectableWords[currentWordIndex][i] === letter) {
            positions.push(i);
        }
    }

    // if there are no indicies, remove a guess and update the hangman image
    if (positions.length <= 0) {
        remainingGuesses--;
        updateHangmanImage();
    } else {
        // Loop through all the indicies and replace the '_' with a letter.
        for(var i = 0; i < positions.length; i++) {
            guessingWord[positions[i]] = letter;
        }
    }
};

 

The evaluateGuess method searches through the entire word we are guessing to find all instances.  Every instance we find we store the index in our positions array.  If no instances are found, we subtract a remainingGuess and call our updateHangmanImage() function.  If instances are found, we loop through our positions array replacing guessingWords underscores with the proper letters in the proper positions.

Finally, we check for a win:

function checkWin() {
    if(guessingWord.indexOf("_") === -1) {
        document.getElementById("youwin-image").style.cssText = "display: block";
        document.getElementById("pressKeyTryAgain").style.cssText= "display: block";
        wins++;
        hasFinished = true;
    }
};

 

To check for a win, we check if there are no more underscores in our guessingWord array.  If not, we display the winning image and our Press Any Key to Try Again element, increment the win and flag hasFinished to true!

This was a fun exercise for me, and my first attempt at designing something in javascript.  I'm sure there were a million better ways to approach this, but it was a fun learning experience for me and I look forward to writing more in javascript in the future!


Dan Orlovsky

Self-taught full-stack developer and Visual Studio junkie specializing in C#, ASP.NET (WebForms and MVC), HTML5, and CSS3.  I design custom content management solutions for small-businesses looking to take control of their website.  Each project is built with the technical aptitude of the user in mind.

I am currently studying full-stack development from Node.JS using Express, React.Js, and MongoDb, so I can expand my offerings to the tech and business communities.

 


Comments

Ad Space