DEV Community

Cover image for Writing Hangman with Coding Nomads
Milnor
Milnor

Posted on

Writing Hangman with Coding Nomads

Intro

Recently I started the Python Bootcamp offered by Coding Nomads, despite being a professional developer already. I originally learned Python in a haphazard manner: Googling "how to X in python", then pasting together useful snippets from Stack Overflow. Now it's back to the basics as I re-learn how to use the language in a more idiomatic fashion under the guidance of a mentor. A complete code listing of my Hangman implementation is pasted at the bottom, but first I'll comment on design decisions, challenges, etc. in select snippets.

I grew up playing "Hangman" using paper-and-pencil. If the rules are unfamiliar to you, try the game's Wikipedia page. In a nutshell, you have a finite number of guesses to come up with all the letters in a mystery word. With each correct guess, you are shown all occurrences of a letter within the word. With each wrong guess, body parts are added to a simple sketch of you hanging from a noose.

Displaying the Word

Initially, I was tracking a secret_word string variable along with two lists (right_letters and wrong_letters) guessed by the player. At first I just printed the word (including only the correctly guessed letters) to the screen; e.g. _ e _ _ _. Later I decided to save this representation of the word to a variable word_in_progress, since winning the game could be detected when the string no longer contained any underscores.

secret_word = "crumpet"

right_letters = []
wrong_letters = []

    # Display the word as a sequence of blanks, e.g. "_ _ _ _ _" for "hello"
    # When they find a correct character, display the blank with the word
    #   filled in, e.g.: "_ e _ _ _" if they guessed "e" from "hello"
    word_in_progress = ""
    for each in secret_word:
        if each in right_letters:
            word_in_progress += each + " "
        else:
            word_in_progress += "_ "
    print(word_in_progress)
Enter fullscreen mode Exit fullscreen mode

Embellishment: Adding ASCII Art

What CLI game would be complete without ASCII art graphics?
I created a multi-line string variable wrong_6 to represent losing the game and being hanged by the neck until dead. Then I worked backwards with wrong_5 (one leg missing) through wrong_0 (an empty gallows).

wrong_6 = """
========
 ||  |
 ||  O
 || /|\
 || / \
 ||
===========
"""
Enter fullscreen mode Exit fullscreen mode

When I ran the game; however, something looked off. I soon remembered that \ is often an escape character in programming languages, so I corrected the limbs on the right side of the drawing to look like this:

wrong_6 = """
========
 ||  |
 ||  O
 || /|\\
 || / \\
 ||
===========
"""
Enter fullscreen mode Exit fullscreen mode

Linting Headaches

New programmers may be unfamiliar with the term linting. It refers to running your code through a static analysis tool (called a linter) that warns you when you don't conform to best practices for the language you are writing. In the case of Python, for instance, PEP-8 is the authoritative standard on best practices and pylint one of the popular linters. Black is also a great linter that should be on your radar, though I don't discuss it further here.

At my job as a developer, if I write code that doesn't pass pylint (or another language-specific linter, as appropriate), my commit will get rejected.

Naming Conventions

I ran python -m pylint hangman.py and received several warnings about naming conventions. (Note: If you are a Linux developer, assuming pylint is already installed, you should be able to run it more simply: pylint hangman.py).

hangman.py:68:0: C0103: Constant name "wrong_4" doesn't conform to UPPER_CASE naming style (invalid-name)
hangman.py:78:0: C0103: Constant name "wrong_5" doesn't conform to UPPER_CASE naming style (invalid-name)
hangman.py:88:0: C0103: Constant name "wrong_6" doesn't conform to UPPER_CASE naming style (invalid-name)
hangman.py:109:4: C0103: Constant name "word_in_progress" doesn't conform to UPPER_CASE naming style (invalid-name)
hangman.py:155:8: C0103: Constant name "your_letter" doesn't conform to UPPER_CASE naming style (invalid-name)
hangman.py:160:16: C0103: Constant name "your_letter" doesn't conform to UPPER_CASE naming style (invalid-name)
Enter fullscreen mode Exit fullscreen mode

Since I am primarily a C developer and learning to write Python like an adult is relatively new, these warnings suggest to me: "I, your great and mighty linter, believe that your global variables are all constants, kind of like a #define PI 3.14 in C, and ought to be written in ALL_CAPS rather than snake_case." However, since I'm still early on in the Coding Nomads bootcamp and don't know the best practices, I decided to kick the can down the road.

Instead of refactoring to avoid global variables or painstakingly rename them all, a quick Google search told me that I could disable the warning with # pylint: disable=invalid-name. 😎

Long Lines

There was another linting error that I decided did need fixing. One line of the code was very long:

if your_letter in right_letters or your_letter in wrong_letters or not your_letter.isalpha():
Enter fullscreen mode Exit fullscreen mode

I didn't trust the user to guess a letter that hadn't already been guessed. In fact, I don't trust the user to even guess a "letter" that is even alphanumeric, let alone alphabetic. So, I wrapped the user input in a while loop that continues ad infinitum until they get it right. (Note: If you are a new developer and haven't learned to sanitize your inputs, you really ought to read the Bobby Tables cartoon by XKCD.)

Pardon the rabbit trail about why that long line was necessary. The question was how to shorten it so that pylint would accept my code. Python is very sensitive to whitespace being just so, so when I split the long line into two, it complained about how I did it.

            if your_letter in right_letters or your_letter in wrong_letters or not
            your_letter.isalpha():
Enter fullscreen mode Exit fullscreen mode
hangman.py:158:83: E0001: Parsing failed: 'invalid syntax (<unknown>, line 158)' (syntax-error)
Enter fullscreen mode Exit fullscreen mode

A little more Googling combined with trial-and-error determined that this would work:

            if your_letter in right_letters or your_letter in wrong_letters or not \
               your_letter.isalpha():
Enter fullscreen mode Exit fullscreen mode

Randomizing the Secret Word (Potential Spoiler)

The course suggested this optional extension to make the game more interesting: "If you want to practice your online research skills, then explore some possible ways to get a word into your game that you don't know yourself. That would make it a lot more fun to play the game by yourself."

Initially, to test the script during development, I hard-coded an arbitrary "secret" word to guess:

# Hard-code a word that needs to be guessed in the script
secret_word = "crumpet"
Enter fullscreen mode Exit fullscreen mode

Where could I go to get a large pool of words? If I were working in a Linux environment, there would likely be an entire dictionary available within /usr/share/dict/. Alas, my Linux VM (virtual machine) was freezing for unknown reasons, so I began this project in the terra incognita of a Windows 11 environment.

Our good friend Google led me to a question on Stack Overflow about writing a random word generator in Python. The accepted answer was written for Python2, but just below it was a Python3 alternative using urllib from the standard library. It pointed to a website that was down at the time I tried it -- though as of writing this blog post, it has come back up -- so I swapped in the URL from the Python2 example and everything worked beautifully.

import random
import urllib.request

word_url = "https://www.mit.edu/~ecprice/wordlist.10000"
response = urllib.request.urlopen(word_url)
long_txt = response.read().decode()
words = long_txt.splitlines()
secret_word = random.choice(words)
Enter fullscreen mode Exit fullscreen mode

Code Listing

The complete code differs slightly from the snippets above due to changes under version control (the original sits within a private repo on GitHub) as well as pulling out just the parts relevant to a topic of discussion.

""" The Game of Hangman """
# pylint: disable=invalid-name

import random
import sys
import urllib.request

# adapted from https://stackoverflow.com/questions/18834636/random-word-generator-python
word_url = "https://www.mit.edu/~ecprice/wordlist.10000"
response = urllib.request.urlopen(word_url)
long_txt = response.read().decode()
words = long_txt.splitlines()
secret_word = random.choice(words)

right_letters = []
wrong_letters = []

# Create a counter for how many tries a user has
wrong = 0   # number of incorrect guesses, up to 6

wrong_0 = """
========
 ||  |
 ||  
 || 
 || 
 ||
===========
"""

wrong_1 = """
========
 ||  |
 ||  O
 || 
 || 
 ||
===========
"""

wrong_2 = """
========
 ||  |
 ||  O
 ||  |
 || 
 ||
===========
"""

wrong_3 = """
========
 ||  |
 ||  O
 || /|
 || 
 ||
===========
"""

wrong_4 = """
========
 ||  |
 ||  O
 || /|\\
 || 
 ||
===========
"""

wrong_5 = """
========
 ||  |
 ||  O
 || /|\\
 ||   \\
 ||
===========
"""

wrong_6 = """
========
 ||  |
 ||  O
 || /|\\
 || / \\
 ||
===========
"""

# Print an explanation to the user
print("  =======================")
print(" / Welcome to Hangman! /")
print("======================= ")

# Keep asking them for their guess until they won or lost
while wrong <= 6:
    # Display the word as a sequence of blanks, e.g. "_ _ _ _ _" for "hello"
    # When they find a correct character, display the blank with the word
    #   filled in, e.g.: "_ e _ _ _" if they guessed "e" from "hello"
    word_in_progress = ""
    for each in secret_word:
        if each in right_letters:
            word_in_progress += each + " "
        else:
            word_in_progress += "_ "
    print(word_in_progress)

    # My embellishments
    if wrong == 0:
        print(wrong_0)
    elif wrong == 1:
        print(wrong_2)
    elif wrong == 2:
        print(wrong_1)
    elif wrong == 3:
        print(wrong_3)
    elif wrong == 4:
        print(wrong_4)
    elif wrong == 5:
        print(wrong_5)
    elif wrong == 6:
        print(wrong_6)

    print("Correct Guesses: ", end=' ')
    for each in right_letters:
        print(each, end=' ')
    print("")

    print("Incorrect Guesses: ", end=' ')
    for each in wrong_letters:
        print(each, end=' ')
    print("")

    # Ask for user input
    if wrong == 6:
        # Display a losing message and quit the game if they don't make it
        print("Sorry, you lose.")
        sys.exit()
    elif "_" not in word_in_progress:
        # Display a winning message and the full word if they win
        print("Congrats, you win.")
        sys.exit()
    else:
        # Allow only single-character alphabetic input
        print("\n\n")
        your_letter = None
        while your_letter is None:
            your_letter = input("Guess a letter: ").lower()
            if your_letter in right_letters or your_letter in wrong_letters or not \
               your_letter.isalpha():
                your_letter = None
                continue
            if your_letter in secret_word:
                print("Good guess!")
                right_letters.append(your_letter)
            else:
                print("Nope!")
                wrong_letters.append(your_letter)
                wrong += 1
Enter fullscreen mode Exit fullscreen mode

About Me

I get paid to code, but also put much effort into drawing silly cover art in MS Paint to adorn software user manuals and the occasional blog post.

Top comments (0)