Writing Hangman with Coding Nomads
Tags: python, beginners
Published: 2023-05-15 21:27:24

## Intro
Recently I started the Python Bootcamp offered by [Coding Nomads](https://codingnomads.co/), 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](https://en.wikipedia.org/wiki/Hangman_(game)). 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)
```

## Embellishment: Adding ASCII Art
What CLI game would be complete without [ASCII art](https://en.wikipedia.org/wiki/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
 || /|\
 || / \
 ||
===========
"""
```
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
 || /|\\
 || / \\
 ||
===========
"""
```

## 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](https://peps.python.org/pep-0008/) is the authoritative standard on best practices and [pylint](https://pypi.org/project/pylint/) one of the popular linters. [Black](https://pypi.org/project/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)
```

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`. :sunglasses:

### 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():
```
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 `continue`s *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](https://imgs.xkcd.com/comics/exploits_of_a_mom.png) 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():
``` 
```
hangman.py:158:83: E0001: Parsing failed: 'invalid syntax (<unknown>, line 158)' (syntax-error)
```

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():
```

## 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"
``` 
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](https://stackoverflow.com/questions/18834636/random-word-generator-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](http://svnweb.freebsd.org/csrg/share/dict/words?view=co&content-type=text/plain) 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](https://www.mit.edu/~ecprice/wordlist.10000) 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)
```

## 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
```

## 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.