5
\$\begingroup\$

This is my Python 3.6+ version of the Hangman game, with graphics generated with print and .format. I've never written code professionally so I would like to know what must be changed to reflect good practice, efficiency, clarity of comments.

from random import randint
import os
WORDS = ["word", "object","radiant","wave","stream","jazz","wisdom","flow", "california", "metal", "book", "hero", "democracy", "beet", "tissue", "shall", "imminent", "delightful", "magnificent", "awful", "switzerland", "unbelievable","none","plus","one", "crocodile", "architecture", "monotheism", "philosophy", "practical"]
LIMIT = 8
answers = [f"Find the word in less than {LIMIT} guesses. Good luck!\n", "Correct!\n", "Wrong guess.\n", "Wrong input. Type a single letter or 'exit'\n", "You already tried that letter.\n"]
word = WORDS[randint(0, len(WORDS) - 1)].upper()
wrong_guesses = []
eval_guess = 0 #index for the 'answers' list
outcome = ""
last_tried = "" #last letter tried
# build hidden word:
hidden_word = ["_" for i in range(len(word))]
# Prints the hangman progress, default value is zero
def display_hangman(counter=0):
    if counter == 1:
        print("{:^34}{}{:^31}{}{:^31}{}{:^30}".format(' _____\n ','|   |\n','   |    \n','   |  \n','   |   \n','  |\n','|\n'))
    elif counter == 2:
        print("{:^34}{}{:^31}{}{:^31}{}{:^30}".format(' _____\n ','|   |\n','   |   O\n','   |  \n','   |   \n','  |\n','|\n'))
    elif counter == 3:
        print("{:^34}{}{:^31}{}{:^31}{}{:^30}".format(' _____\n ','|   |\n','   |   O\n','   |  \\\n','   |   \n','  |\n','|\n'))
    elif counter == 4:
        print("{:^34}{}{:^31}{}{:^31}{}{:^30}".format(' _____\n ','|   |\n','   |   O\n','   |  \\|\n','   |   \n','  |\n','|\n'))
    elif counter == 5:
        print("{:^34}{}{:^31}{}{:^31}{}{:^30}".format(' _____\n ','|   |\n','   |   O\n','   |  \\|/\n','   |   \n','  |\n','|\n'))
    elif counter == 6:
        print("{:^34}{}{:^31}{}{:^31}{}{:^30}".format(' _____\n ','|   |\n','   |   O\n','   |  \\|/\n','   |   |\n','   |\n','|\n'))
    elif counter == 7:
        print("{:^34}{}{:^31}{}{:^31}{}{:^30}".format(' _____\n ','|   |\n','   |   O\n','   |  \\|/\n','   |   |\n','   |  / \n','|\n'))
    elif counter == 8:
        print("{:^34}{}{:^31}{}{:^31}{}{:^30}".format(' _____\n ','|   |\n','   |   O\n','   |  \\|/\n','   |   |\n','   |  / \\\n','|\n'))
    else:
        print("{:^34}{}{:^31}{}{:^31}{}{:^30}".format(' _____\n ','|   \n','   |    \n','   |  \n','   |   \n','  |\n','|\n'))
# clear the screen 
def clear_screen():
    os.system('cls' if os.name=='nt' else 'clear')
# display the messages of the game
def display_messages(wrong_guesses, hidden_word, eval_guess=0, LIMIT=8, outcome="", last_tried=""):
    clear_screen()
    print(f"\n{' HANGMAN ':*^36}")
    display_hangman(len(wrong_guesses))
    if outcome == 1:
        print(f"{'YOU WIN!':^36}\nYou rightly guessed: {word}")
        print("\nYou also tried: ", wrong_guesses)
    elif outcome == 0:
        print(f"{'GAME OVER.':^36}\n{'The word was:':^15}{word}")
        print("\nYou guessed:", hidden_word, "\nYou tried:", wrong_guesses)
    else:
        print(hidden_word,"\n")
        print(f"{answers[eval_guess]}")
        print(f"Wrong guesses: {wrong_guesses} Wrong guesses remaining: {LIMIT - len(wrong_guesses)}")
        print(f"Length: {len(hidden_word)} last letter tried: {last_tried}")

# core of the game
while True:
    if len(wrong_guesses) == LIMIT:
        display_messages(wrong_guesses,hidden_word,outcome=0)
        break
    if "_" not in hidden_word:
        display_messages(wrong_guesses,hidden_word,outcome=1)
        break
    # messages function
    display_messages(wrong_guesses,hidden_word, eval_guess, LIMIT, outcome, last_tried)
    # user interaction
    prompt = input("Pick a letter or type 'exit' to quit: ").upper() #make input uppercase regardless
    if prompt.lower() == "exit":
        print("Bye.")
        break
    if prompt.isalpha() and len(prompt) == 1: # check if input single letter
        if prompt in wrong_guesses or prompt in hidden_word: # If letter already tried
            eval_guess = 4 
        elif prompt in word: # correct guess
            last_tried = prompt # update last tried letter
            eval_guess = 1 # set the answer to 'Correct!'
            for i in range(len(word)):
                if prompt == word[i]:
                    hidden_word[i] = prompt # replace "_" with the letter
        elif prompt not in wrong_guesses: # Wrong guess. Add wrong letter to the wrong guesses list
            eval_guess = 2 # set the answer to 'Wrong guess!'
            wrong_guesses.append(prompt)
            last_tried = prompt
    else:
        eval_guess = 3 # wrong input'
\$\endgroup\$
3
  • 3
    \$\begingroup\$ If you're using 3.6+, any particular reason you haven't used f-strings? \$\endgroup\$
    – Energya
    Commented Nov 11, 2019 at 7:46
  • \$\begingroup\$ I couldn't quite figure out how to convert the .format spacing into f-strings.Also I was trying to store each 'print' as a string that could be called later on, but it was giving me an error saying that tuples cannot be iterated. I can expand on that in my post if need be. \$\endgroup\$
    – TeaGlass
    Commented Nov 11, 2019 at 15:12
  • \$\begingroup\$ Correct me if I'm wrong but it seems that backslash ('\') are not allowed in f-strings and I need them for new lines '\n' \$\endgroup\$
    – TeaGlass
    Commented Nov 11, 2019 at 15:22

1 Answer 1

3
\$\begingroup\$

Style

The code is what I would call "visually dense" (in lack of a better description). By that I mean, that it's hard to spot where a "block of code", e.g. a function, ends. A few well placed blank lines could work wonders here. There are well established guidelines codified in the "official" Style Guide for Python Code (aka PEP 8):

  • top-level functions or classes should be separated by two blank lines
  • inside of classes functions use single blank lines where appropriate

You will also often find that imports are also separated from the following code by two blank lines.

The style guide also has something to say about whitespace in expressions and statements. The basic recommendation here would be to have a single blank space around operators like = (you do this - good!) and also have a single blank space after ,, e.g. when calling functions (you do this sometimes - get consistent here) or when declaring lists (you don't do this, but you should).

Fortunately, you are not left alone with this task. There are a lot of tools to help you readable and consistent Python code. Some of them are listed in this answer on Code Review Meta.

Naming

answers should likely also be capitalized like words, since it's essentially a constant value you never intend to change. Apart from that the variable names follow a consistent style and have meaningful names.

Documentation

You seem to be willing to document your code. That's great! Python has a well established documentation string convention, shortly outlined in the Style Guide, and described in more detail in PEP 257. If you put the documentation immediately after the function definition and enclose the """Description in triple quotes""", Python IDEs and the built-in help(...) function can easily pick it up. This is very convenient for larger projects.

Example:

def clear_screen():
    """Clear the screen

    This function is supposed to work on Linux and Windows.
    """
    os.system('cls' if os.name == 'nt' else 'clear')

The code

Instead of answers (maybe soon ANSWERS) being a list and working with integer indices to select the appropriate answer, consider changing it to a dict. Then you could either use descriptive keys like "correct", "wrong", or "already_tried" or even an enum to make it clearer which output you are trying to print. This will help you to get rid of all the "magic numbers" in your code, which are very error prone if you ever try to change one and miss a spot.

display_messages(...) has an empty string as default argument for outcome, but then checks against integer values in the body. Expecting your input to be of a different type than the default argument is something to avoid, since it may confuse someone using that function. The general fallback in such cases is usually None. Another approach might be to make use of type hints in Python 3.6 and later. Type hints can automatically be checked by external tools like pylint or mypy to make sure the code "is safe to run" without actually running it (so called static code analysis).

display_hangman(...) is a prime example of code duplication. All the print(...)s are basically doing the same thing, only with different inputs. This means the code can easily be refactored to become more like

def display_hangman(counter=0):
    """Prints the hangman progress, default value is zero"""
    gallow = ( # this was the else case before
        ' _____\n ', '|   \n', '   |    \n', '   |  \n', '   |   \n', '  |\n', '|\n'
    )
    if counter == 1:
        gallow = (
            ' _____\n ', '|   |\n', '   |    \n', '   |  \n', '   |   \n', '  |\n', '|\n'
        )
    elif counter == 2:
        ...
    elif counter == 8:
        gallow = (
            ' _____\n ', '|   |\n', '   |   O\n', '   |  \\|/\n', '   |   |\n', '   |  / \\\n', '|\n'
        )

    print("{:^34}{}{:^31}{}{:^31}{}{:^30}".format(*gallow))

As you can see clearly, there is now only a single place where the print(...)ing happens. The elements of the gallow are stored as a tuple (a list would work too), ready to be used in .format(...). Using a tuple/list allows us to do "...".format(*gallow) which is shorthand for "...".format(gallow[0], gallow[1], ..., gallow[6]). This is called argument list unpacking in Python. The slight downside of this is that this neat trick will only work with .format(...), but not with f-strings. But that's something I can live with, you will have to decide what works best for yourself. Carefully adapting the format specifier would also likely make it possible to move each of those \n into the format string, although I did just check my hypothesis for the first gallow.

When collecting user input, you convert it to uppercase to have it in a normalized format, only to then do prompt.lower() == "exit" literally in the next line. I can see no particular reason not to use prompt == "EXIT".

You should likely wrap the code titled with # core of the game into a function as well (the generic main() comes to my mind). The games state could then be stored into the function, instead of on a global scope (I'm looking at you wrong_guesses!). That would also make it easily possible to play several rounds of your game without having to remember which global variables need to be reset in order to avoid trouble.

Once this is done, it's time to look at the infamous if __name__ == "__main__": which is very often found in Python scripts. This little line of code tells the interpreter to run the code surrounded by that if block only when called like python hangman.py. Omitting this piece of code would also start your game if you ever try to do something like from hangman import clear_screen (code reusability is cooooool).

\$\endgroup\$
4
  • \$\begingroup\$ Wow Thank you very much for the super detailed answer! I have to say that I'm not entirely familiar with dictionaries and still trying to grasp OOP but your insights make me want to refactor that program now. Having 'outcome' as 'None', of course! Actually every suggestions make sense. Going to save this post for later use, thanks again! The only thing that I still wonder is, per my comment above, if it's possible to replicate the 'display_hangman' function using Python 3.6x f-string instead of the '.format'. The problem is that I can't add backslashes '\' to f-string. \$\endgroup\$
    – TeaGlass
    Commented Nov 11, 2019 at 16:44
  • \$\begingroup\$ Thanks to your comment under your question I just now fully understood why you didn't use f-strings all the way through. I going to add a word on that in a few moments. \$\endgroup\$
    – AlexV
    Commented Nov 11, 2019 at 16:46
  • \$\begingroup\$ @TeaGlass See my newly added thoughts on display_hangman(...). Seems like "...".format(...) will stick with us, but for a good reason! :-) \$\endgroup\$
    – AlexV
    Commented Nov 11, 2019 at 17:20
  • \$\begingroup\$ Thanks a lot @AlexV! I incorporated the changes and I have more things to learn. I'll keep this thread as a reference for the future. \$\endgroup\$
    – TeaGlass
    Commented Nov 12, 2019 at 13:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.