Program: Hangman

This program makes use of the many core constructs we’ve covered so far such as variables, if statements, loops, arrays, ArrayList, and methods. Although it looks long and arduous, if we break it down piece by piece, it becomes much more manageable. I recommend viewing the code and the explanations in two separate windows as this saves you from having to constantly scroll up and down.

package ch7.hangman;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Scanner;
import java.util.concurrent.ThreadLocalRandom;

public class Hangman {
    static Scanner sc = new Scanner(System.in);
    static String[] words = {"flamingo", "celestial", "machine",
            "celebrate", "spacious", "pirate", "lightning", "ceremony",
            "commencement", "parsnip", "caricature", "invalidate", "crisis",
            "indispensable", "indignation", "literature", "raven",
            "cerebellum", "elongate", "champion", "aspect", "loiter"};
    static String word = words[ThreadLocalRandom.current().nextInt(0, words.length)];
    static char[] clueChars = new char[word.length()];
    static {
        Arrays.fill(clueChars, '-');
    }

    static ArrayList<Character> wrongLetters = new ArrayList<>();
    static ArrayList<String> wrongWords = new ArrayList<>();

    public static void main(String[] args) {
        displayGameState();
        while (true) {
            getAndProcessInput();
            displayGameState();

            if (wordIsComplete()) {
                System.out.println("YOU WIN! You are freed from the noose!");
                break;
            }
            else if (wrongGuessCount() >= 6) {
                System.out.println("YOU LOSE! You are hanged!");
                break;
            }
        }
    }

    static void getAndProcessInput() {
        System.out.print("\nGuess: ");
        String guess = sc.next();
        if (guess.length() > 1) {
            if (!wrongWords.contains(guess)) {
                if (guess.equals(word)) {
                    clueChars = word.toCharArray();
                }
                else {
                    wrongWords.add(guess);
                    System.out.printf("%s is not the right word.\n", guess);
                }
            }
        }
        else {
            char letter = guess.charAt(0);
            if (!wrongLetters.contains(letter)) {
                ArrayList<Integer> letterIndexes = getIndexes(word, letter);
                if (letterIndexes.size() > 0) {
                    for (int i : letterIndexes) {
                        clueChars[i] = letter;
                    }
                }
                else {
                    wrongLetters.add(letter);
                }
            }
        }
    }

    static ArrayList<Integer> getIndexes(String str, char c) {
        char[] cArr = str.toCharArray();
        ArrayList<Integer> indexes = new ArrayList<>();
        for (int i = 0; i < cArr.length; i++) {
            if (cArr[i] == c) {
                indexes.add(i);
            }
        }
        return indexes;
    }

    static boolean wordIsComplete() {
        for (char ch : clueChars) {
            if (ch == '-') {
                return false;
            }
        }
        return true;
    }

    static int wrongGuessCount() {
        return wrongLetters.size() + wrongWords.size();
    }

    static void displayGameState() {
        displayMan();
        displayClueWord();
        displayWrongLetters();
    }

    static void displayMan() {
        System.out.println("______   ");
        System.out.println("|/   |   ");
        switch (wrongGuessCount()) {
            case 0:
                System.out.println("|        ");
                System.out.println("|        ");
                System.out.println("|        ");
                break;
            case 1:
                System.out.println("|    O  ");
                System.out.println("|       ");
                System.out.println("|       ");
                break;
            case 2:
                System.out.println("|    O  ");
                System.out.println("|    |  ");
                System.out.println("|       ");
                break;
            case 3:
                System.out.println("|    O  ");
                System.out.println("|   /|  ");
                System.out.println("|       ");
                break;
            case 4:
                System.out.println("|    O  ");
                System.out.println("|   /|\\");
                System.out.println("|       ");
                break;
            case 5:
                System.out.println("|    O   ");
                System.out.println("|   /|\\ ");
                System.out.println("|   /    ");
                break;
            case 6:
                System.out.println("|    O  ");
                System.out.println("|   /|\\");
                System.out.println("|   / \\");
                break;
        }
        System.out.println("|\\______ ");
    }

    static void displayClueWord() {
        for (char ch : clueChars) {
            System.out.print(ch);
        }
        System.out.println();
    }

    static void displayWrongLetters() {
        System.out.print("Wrong letters: ");
        for (char letter : wrongLetters) {
            System.out.printf("%s ", letter);
        }
        System.out.println();
    }
}

Variables

First, let’s go through the static variables line by line.

Line 9: Scanner

Line 9 creates a Scanner so the player can provide input as usual.

Line 10: The ‘words’ Array

Line 10 creates an array containing a bunch of words, aptly named words. It contains 22 words in total, spread over 5 lines for the sake of readability. The program will pick a word randomly from this array for the player to guess.

Line 15: Picking a random word

Line 15 is where the program picks a random word from the words array for the player to guess. It uses ThreadLocalRandom to do so. This line may look confusing because we’re used to seeing just a number between the square brackets, like 3 for example:

static String word = words[3];

In that case, word would be set to the string at index 3, i.e. “celebrate”. But, because we want the index to be random, between the brackets is instead a call to:

ThreadLocalRandom.current().nextInt(0, words.length)

This will generate a random number between 0 (inclusive) and 22 (exclusive) and return it. This number will then be used as the index number. If we imagine it returned 8, then that would result in:

static String word = words[8];

Index 8 is “commencement”, resulting in:

static String word = "commencement";

Now, the variable word contains “commencement”. The program will refer to this variable every time the player makes a guess to compare the letters.

Line 16: Array clueChars

In a typical game of hangman, dashes are used to represent each letter in the word e.g. ------------. As the player guesses letters correctly, the gaps are filled in e.g. -o--en-e-ent. This is what the array clueChars (clue characters) represents. Each character in the array will ether be a - or a letter. It will be updated and printed every time the player makes a guess so that the player is able to see their progress. And because the array uses word.length() for its size, it is guaranteed to have the same number of elements as the number of letters in the word.

Line 18: Fill clueChars

If we still imagine that word = “commencement”, then that means clueChars will have 12 elements. However, all elements will be the default value—the null character '\0'—so clueChars currently looks like this:

['\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0']

To fill clueChars with dashes instead, line 18 uses the Arrays.fill method to fill clueChars with the - character so it looks like this:

['-', '-', '-', '-', '-', '-', '-', '-', '-', '-', '-', '-']

This is performed inside something called a static initialiser (lines 17-19), which is simply the word static followed by two curly brackets for the body. Static initialisers are used to initialise (set up) static variables so they are ready for use. Code in static initialisers runs before the main method so we know the array will be ready and filled with dashes when main starts (although we could have put the Arrays.fill code at the beginning of main to the same effect).

Lines 21/22: Wrong Letters and Words

Line 21 is an ArrayList of Character called wrongLetters. This stores any wrong letters that the player guesses. Line 22 is an ArrayList of String called wrongWords. This stores any wrong words that the player guesses.

Step Through

Figure 10-1

Let’s step through the program. First, in main, line 25 calls method displayGameState. This method displays the “visuals” of the game as shown in Figure 10‑1. If we look at displayGameState on line 95, we can see it makes calls to three more methods: displayMan, displayClueWord, and displayWrongLetters. These three methods each display a different visual element of the game. displayMan is called first so let’s go to this method on line 101. This method prints the famous hangman figure; it prints a different version of the man depending on the number of wrong guesses the player has made so far. The top and bottom parts of the drawing never change, so lines 102/3 print the top part of the drawing and line 141 prints the bottom part. In between is a switch statement (lines 104-140) that prints a different drawing depending on the number of wrong guesses. As we can see, in the case of 0 wrong guesses, no part of the man is drawn (printed). In the case of 1 wrong guess, the man’s head is drawn. In the case of 2 wrong guesses, the man’s head and torso are drawn, and so on. The switch statement obtains the number of wrong guesses by calling the wrongGuessCount method on line 104. This method, on line 91, returns the total number of wrong guesses the player has made so far. It calculates this by simply adding the sizes of the two ArrayListswrongLetters and wrongWords—and returns the result. For example, if the player has wrongly guessed 2 letters and 1 word, then the method will return 3. Therefore, back at switch statement, case 3 will run and print a figure with a head, torso, and right arm (his right).

Once the man is drawn, displayMan returns back to line 96 and line 97 calls displayClueWord (line 144). This method uses a foreach loop to print out each character in the clueChars array so the player can see their progress with the word. Remember that clueChars is filled with 12 dashes (line 18) and therefore 12 dashes would be printed, as shown in Figure 10‑2. On returning, line 98 calls displayWrongLetters (line 151). This method prints all the wrong letters the player has guessed. Again, a foreach loop is all that’s needed and prints out every character in the wrongLetters ArrayList. Finally, this method returns (to line 98). Then displayGameState returns to back to main on line 25.

The program then enters a while loop (lines 26-38). This loop will keep repeating since the condition is true, so it will have to be stopped manually at some point with a break statement or some other way. Line 27 calls method getAndProcessInput (line 41). As an overview, this method obtains the player’s input, processes it, and updates various things along the way. This method gives the player the option of entering either a single letter or the whole word (if they think they know what the word is). If the player enters a word, the method will simply check if it matches the target word. If the player enters a single letter, the method will find all the places that letter exists in the word (if at all) and update the clue characters to match. Let’s now go through the method in detail. First, lines 42 prints “Guess: “ and line 43 obtain the player’s guess as a String, which is stored in variable guess. As mentioned, the player could enter either a single letter or a whole word here so line 44 checks if guess is greater than 1 character in length. If true, it is assumed the player wants to guess the whole word. In that case, line 45 checks that the player has not made this same guess before. This is because we don’t want to penalise the player if they accidentally guess the same word multiple times. We can just ignore the guess if the player guesses the same word more than once. The way it works is by checking that wrongWords does not contain guess. If true, that means the player has not guessed the word before. In that case, line 46 checks if the guess is correct. It checks if guess is equal to word. If true, the player has won, but the program doesn’t handle this directly here. Instead, line 47 fills clueChars with all the correct letters. It does this by calling word.toCharArray(), which returns a char array equivalent of the word, thus automatically completing the word for player to see. To clarify, let’s imagine that word is “commencement”. This means that word.toCharArray() returns

{'c', 'o', 'm', 'm', 'e', 'n', 'c', 'e', 'm', 'e', 'n', 't'}

which gets stored in clueChars. Therefore, when it’s time to print clueChars again, the player will see the whole word instead of dashes. On the other hand, if the player got the word wrong, the else block will run and line 50 will add the player’s guess to wrongWords. Then line 51 informs the player that their guess is incorrect.

Now let’s imagine the player guesses a letter and not a word. For example, if the player guesses “e”, then that means on line 43 that guess = “e”.  This means the if block’s condition (line 44) is false, which means the else block (lines 55-68) runs. Variable guess is a String but, in this case, it would be easier to work with a char instead. Therefore, line 56 calls guess.charAt(0). The charAt method returns a single char at the specified index, which is 0 in this case (0 is the only valid index anyway since guess only contains one character i.e. “e”). In short, guess.charAt(0) returns ‘e’, which gets stored in variable letter. Next, line 57 checks if letter has been guessed before by checking if wrongLetters does not contain letter. If false (meaning the letter has been guessed before), nothing happens as we don’t want to penalise the player. If true (meaning it’s a new letter), then the next task is to figure out all the positions (if any) where the letter exists in the word, so that the program may fill in the blanks. We can see ourselves that the letter ‘e’ appears in the word three times at indexes 4, 7, and 9 (“commencement”). This is the purpose of the getIndexes method on line 58. We pass it two arguments, word and letter. It then compiles a list of all the places that letter appears in word (that ‘e’ appears in “commencement”). It then returns this list (in the form of an ArrayList of Integer). In this case, the ArrayList it returns looks like this: {4, 7, 9}. This gets stored in variable letterIndexes. But what if the player enters a letter does not appear in the word? In that case getIndexes will return an empty list: {}. Therefore, if letterIndexes is empty, this tells us getIndexes found no occurrences of the letter. This is exactly what line 59 checks for. It checks that the size of letterIndexes is greater than 0. If true, we know there’s at least one occurrence of the letter. Therefore, lines 60-62 loops through letterIndexes and replaces the corresponding indexes in clueChars with letter. In our case, clueChars now looks like this:

{'-', '-', '-', '-', 'e', '-', '-', 'e', '-', 'e', '-', '-'}

On the other hand, if the condition is false, that means the list is empty, so the else block runs and line 69 adds the letter to the list of wrong letters. Finally, the method returns to line 27.

Line 28 calls displayGameState again. This goes through the same process we covered when it was called on line 25. This is so we can see the updated state of the game after the player’s input has been processed. Line 30 checks if the word is complete by calling the wordIsComplete method (line 82). This method checks if any characters in clueChars are dashes. To do this, it uses a foreach loop (lines 83-87) to loop over every character in clueChars. Line 84 checks if the character is a dash, and the method returns false if it is. However, if none of the characters are dashes, then the if statement won’t run a single time, which means the word is complete. Therefore, the method will instead return true on line 86.

Back at line 30, if we imagine that wordIsComplete returns true, then that means the player has correctly guessed all letters and so line 31 prints “YOU WIN!” and line 32 breaks out of the loop, ending the program. However, if wordIsComplete returns false, this means the player hasn’t guessed all the letters yet. In that case, the else if block (line 34) checks for a loss by checking if the player has had six or more wrong guesses. It obtains the number of wrong guesses by calling the wrongGuessCount method, which we covered previously. If the condition is true, that means the player has lost and line 35 prints “YOU LOSE!” and line 36 breaks out of the loop, ending the program. If it’s false, then the game continues. The while loop repeats and line 27 calls getAndProcessInput again for another guess.

Method getIndexes

static ArrayList<Integer> getIndexes(String str, char c) {
    char[] cArr = str.toCharArray();
    ArrayList<Integer> indexes = new ArrayList<>();
    for (int i = 0; i < cArr.length; i++) {
        if (cArr[i] == c) {
            indexes.add(i);
        }
    }
    return indexes;
}

The only real reason I left explaining this method until now is because it’s more generic than the rest of the code. That is, it can be used in any program that needs to know all the indexes where a character appears in some string, and isn’t specific to the program that is Hangman. Plus, it wasn’t necessary to understand how this method worked in order to understand the program. You know what it does, what arguments it requires, and what it returns, which is all that’s necessary to use it effectively. This is especially the case when working with 3rd party code/libraries.

The method is quite simple once you understand it. From line 1, we can see it takes in two arguments, a string str and a character c. Line 2 calls str.toCharArray. This method returns a char array version of str, which gets stored in cArr. For example, if

str = "elderflower"

then

cArr = {'e', 'l', 'd', 'e', 'r', 'f', 'l', 'o', 'w', 'e', 'r'}

Next, line 3 creates an ArrayList of Integer to hold all the indexes where the character is found. Currently, it is empty. Lines 4-8 loop through cArr, and every time the character c is found, the associated index is added to the indexes ArrayList. For example, let’s say that c = 'l'. In the for loop, i goes from 0 to 10. Therefore, line 5 will do cArr[0] == 'l'. This is false because index 0 is 'e', not 'l'. On the next iteration, it will do cArr[1] == 'l'. This is true, so line 6 adds i (1) to indexes. So, currently indexes = {1}. The loop will keep checking each character to see if it’s 'l'. None of the character are 'l' until i reaches 6, so cArr[6] == 'l' returns true and line 6 adds the associated index to the list, so now indexes = {1, 6}. The remaining four characters are not 'l' so no more indexes are added. When the loop ends, line 9 returns indexes. Also, don’t forget that the return type needs to match the type that is returned, which is why the method’s return type is ArrayList<Integer> (line 1).

Leave a Reply

Your email address will not be published. Required fields are marked *