6
\$\begingroup\$

I've created a little hangman game. It's pretty simple, and not to exciting but I want to know if there's a way I could turn this little game into something more extravagant and more exciting. How can I take this delicate piece of crap machinery into something a little more fun and a bit more complicated? For example:

  • Can I turn the list that is shown into a single string?
  • Is there a better way to use a random word?
  • Am I using the for and while loop in the correct syntax?

Source:

from random import randint


WORD_LIST = {
    'rand_word_1': 'sergeanty',
    'rand_word_2': 'amphipneustic',
    'rand_word_3': 'hypochil',
    'rand_word_4': 'grus',
    'rand_word_5': 'depressed',
    'rand_word_6': 'distortedly',
    'rand_word_7': 'unselected',
    'rand_word_8': 'crakow',
    'rand_word_9': 'preendorsing',
    'rand_word_10': 'convenient'
}


HANGMAN = (
    """
    x-------x
    """,
    """
    x-------x
    |
    |
    |
    |
    |
    """,
    """
    x-------x
    |       |
    |       0
    |
    |
    |
    """,
    """
    x-------x
    |       |
    |       0
    |       |
    |
    |
    """,
    """
    x-------x
    |       |
    |       0
    |      /|\\
    |
    |
    """,
    """
    x-------x
    |       |
    |       0
    |      /|\\
    |      /
    |
    """,
    """
    x-------x
    |       |
    |       0
    |      /|\\
    |      / \\
    |
    GAME OVER
    """
)


MAX = len(HANGMAN) - 1
num = randint(1, 10)
num_string = str(num)
words = 'rand_word_{}'.format(num_string)
WORD_TO_GUESS = WORD_LIST[words]
HIDDEN = ['_'] * len(WORD_TO_GUESS)
LETTERS_GUESSED = []


def begin_game():
    hang_size = 0
    print "\tHANGMAN!"
    word_arr = list(WORD_TO_GUESS)

    while hang_size < MAX:
        print str(HIDDEN)
        user_guess = raw_input('Guess a letter dude: ')

        if user_guess in LETTERS_GUESSED:
            print 'You already guessed that.. PAY ATTENTION!'
            user_guess = raw_input('Guess a letter dude: ')

        if user_guess in word_arr:
            print "Yeah yeah.. We're all impressed.. It's in the word woohoo.."

            for num in range(len(word_arr)):
                if user_guess == word_arr[num]:
                    HIDDEN[num] = user_guess
                    if HIDDEN.count('_') == 0:
                        print 'You win! Finally you did something right!'
                        quit()

        else:
            print "{}.. Really? That's the best you can do.. Not in my word..".format(user_guess)
            hang_size += 1
            print HANGMAN[hang_size]


begin_game()
\$\endgroup\$
11
\$\begingroup\$

Overview

I can see that you are a beginner and that is totally fine! Some of the points I will be making might, sound a bit harsh and go over your head. If they do try to improve the parts of the code you do understand, and instead post a follow up question.


LMGTFY / RTFM

Asking a question on a SE, site is a great way to learn. However another is searching for answers beforehand. In fact all three questions in your bullet list could be googled / found in the manual.

As you know CodeReview has it's own tag -- since you tagged your own question with the tag. This means people have asked hangman related questions earlier, and you could have browsed these questions before posting your own. As an example, even I have asked a question about hangman. Here is another very similar question.

This is not to discredit you for asking, however drawing suggestions and inspiration from similar problems is a fantastic way to learn.


PEP 8 - Style Guide for Python Code

As to pick the lowest hanging fruits. Consistency is a key part of programming, as it greatly increases readability. It also tells a bit about the coders eye for detail. Lets' look at how you have named your global constants:

MAX
num 
num_string 
words 
WORD_TO_GUESS 
HIDDEN 
LETTERS_GUESSED 

Do you see the inconsistency? Pep 8 recommends the following naming-conventions:

  • CAPITALIZED_WITH_UNDERSCORES for constants
  • UpperCamelCase for class names
  • lowercase_separated_by_underscores for other names

General structure

A very rough sketch of how a standard python program should look like is as follows

import

global CONSTANTS
global VARIABLES

functions

classes

if __name__ == '__main__':

    main()

All of your code should be split into short simple functions, which each serve a single purpose. You should in general have as little code as possible floating around that is not part of a function. This includes minimizing the need for global variables and constants. There are some exceptions to this rule, however your code is not one of them.


Some more pointers

As earlier answers stated you can change your dictionary word list into

WORD_LIST = [
    'sergeanty',
    'amphipneustic',
    'hypochil',
    'grus',
    'depressed',
    'distortedly',
    'unselected',
    'crakow',
    'preendorsing',
    'convenient'
]

As mentioned earlier you use too many global variables. This

MAX = len(HANGMAN) - 1
num = randint(1, 10)
num_string = str(num)
words = 'rand_word_{}'.format(num_string)
WORD_TO_GUESS = WORD_LIST[words]
HIDDEN = ['_'] * len(WORD_TO_GUESS)
LETTERS_GUESSED = []

can be shortened into

MAX = len(HANGMAN) - 1
word_to_guess = random.choice(WORD_LIST)
hidden = ['_'] * len(word_to_guess)
letters_guessed = set()

Where I have used the choice function from the random module. For an example of use see below

   >>> import random
   >>> a = ["Stack", "Overflow", "rocks"]
   >>> print(random.choice(a))
   rocks

for a more detailed explanation see the second bullet point at the start.

You might also have noticed that some of the variables are not CAPITALIZED that is because they do not need to be global variables, and can be moved into the main function.

def play_hangman():
    hang_size = 0
    word_to_guess = random.choice(WORD_LIST)

    hidden = ['_'] * len(word_to_guess)
    letters_guessed = set()

Note how I also conveniently changed the name of your main() function. Naming variables, functions and classes in programming is one of the more difficult things to do. A general rule of thumb is that the names should be clear, distinct and succinct in that order. As others have pointed out main() only achieves the third, which should be the last thing to worry about.


Handling user input

https://imgur.com/X17puIB

Jokes aside as a rule you should never except the user to use the correct input syntax. Instead you should build in checks, exceptions to handle these errors.

For an example your code only checks once if I have tried the letter before.

if user_guess in LETTERS_GUESSED:
    print 'You already guessed that.. PAY ATTENTION!'
    user_guess = raw_input('Guess a letter dude: ')

One way to fix this is using a while loop instead

while user_guess in LETTERS_GUESSED:
    print 'You already guessed that.. PAY ATTENTION!'
    user_guess = raw_input('Guess a letter dude: ') 

What happens if i decide to try the letter banana or the letter ? Your function does nothing to handle it. What If I try the more common mistake to try A then a? All of this can be fixed by putting the get_user_input into it's own function

ALPHABETH = 'abcdefghijklmnopqrstuvwxyz'

def guess_letter(letters_guessed):
    while True:
        guess = raw_input('Guess a letter dude: ').lower()
        if guess not in ALPHABETH:
            print guess, 'is like totally not in the alphabet dude, try again!'
        elif guess in letters_guessed:
            print 'You already guessed {}.. PAY ATTENTION!'.format(guess)
        else:
            return guess

Do note that I did not have to convert ALPHABETH into a list. Python can compare strings just as well as lists. Also notice how I used Python string lower() method to ensure that the user input was all lowercase.


Continuing on

The next bit of your code could then be written as

while hang_size < MAX:
    print str(hidden)
    user_guess = guess_letter(letters_guessed)
    letters_guessed.add(user_guess)
    if user_guess in word_to_guess:

Where we again used that Python can compare strings just fine. The add is used instead of append since letters_guessed is now a set and not a list. Why am I using sets, you ask?

When you want to store some values which you'll be iterating over, Python's list constructs are slightly faster. However, if you'll be storing (unique) values in order to check for their existence, then sets are significantly faster.

For words as short as your the difference is negligible, however it is a good practice nevertheless.

for num in range(len(word_arr)):

This can be improved using Pythons enumerate() function that adds a counter to an iterable.

    for i, letter in enumerate(word_to_guess):
        print i, letter
        if user_guess == letter:
            hidden[i] = letter
    if hidden.count('_') == 0:
        print 'You win! Finally you did something right!'
        return 

I really see no point in turning the for-loop into a zip list. Yes, it is shorter. However for a novice it is much harder to understand what is going on.


Winning condition and return

The statement

if HIDDEN.count('_') == 0:
    print 'You win! Finally you did something right!'
    quit()

Is weird. The if HIDDEN.count('_') == 0: is cryptic at best. A good practice is to check what you really want to check.. You are not interested in how many _'s are left, but rather if the user has guessed the correct word. This can be checked as follows

user_guessed_word_spaced = ' '.join(hidden)
word_to_guess_spaced = ' '.join(word_to_guess)
if user_guessed_word_spaced == word_to_guess_spaced:

At a first glance this might seem convoluted. However in the actual code word_to_guess_spaced = ' '.join(hidden) is only declared once. Secondly using a spaced string ' '.join() instead of ''.join() allows us to directly print the output at the start of the while-loop.

Your way to exit the function is very intuitive for a beginner, however it should be avoided like the plague for a beginner. A more natural way is to use return instead.

quit raises the SystemExit exception behind the scenes.

Furthermore, if you print it, it will give a message:

>>> print (quit)
Use quit() or Ctrl-Z plus Return to exit
>>>

This functionality was included to help people who do not know Python. After all, one of the most likely things a newbie will try to exit Python is typing in quit.

Nevertheless, quit should not be used in production code. This is because it only works if the site module is loaded. Instead, this function should only be used in the interpreter.

I have made some adjustments to accommodate for this. This includes having only a single return statement, as well as some better handling of printing the winning conditions.


There are more things to nitpick, which I did not try to fix/improve

  • You should always comment your code. You can never be sure you will understand your code 6months down the road. A common way to do this is with PEP 257 -- Docstring Conventions.
  • Your creation of strings could be improved.
  • Your list of words is short, and a simple fix is to use a larger list from the internet.
  • You could also make a short menu to congratulate the user, or ask if one wants to play again.

Code

Using all of these small improvements leads to the following code.

import random


WORD_LIST = [
    'sergeanty',
    'amphipneustic',
    'hypochil',
    'grus',
    'depressed',
    'distortedly',
    'unselected',
    'crakow',
    'preendorsing',
    'convenient'
]


HANGMAN = (
    """
    x-------x
    """,
    """
    x-------x
    |
    |
    |
    |
    |
    """,
    """
    x-------x
    |       |
    |       0
    |
    |
    |
    """,
    """
    x-------x
    |       |
    |       0
    |       |
    |
    |
    """,
    """
    x-------x
    |       |
    |       0
    |      /|\\
    |
    |
    """,
    """
    x-------x
    |       |
    |       0
    |      /|\\
    |      /
    |
    """,
    """
    x-------x
    |       |
    |       0
    |      /|\\
    |      / \\
    |
    """
)


MAX = len(HANGMAN) - 1
ALPHABETH = 'abcdefghijklmnopqrstuvwxyz'


def guess_letter(letters_guessed):
    while True:
        guess = raw_input('Guess a letter dude: ').lower()
        if guess not in ALPHABETH:
            print guess, 'is like totally not in the alphabet dude, try again!'
        elif guess in letters_guessed:
            print 'You already guessed {}.. PAY ATTENTION!'.format(guess)
        else:
            return guess


def play_hangman():
    hang_size = 0
    word_to_guess = random.choice(WORD_LIST)
    word_to_guess_spaced = ' '.join(word_to_guess)

    hidden = ['_']*len(word_to_guess)

    letters_guessed = set()
    user_guessed_word_spaced = ' '.join(hidden)

    print "HANGMAN!"

    while hang_size < MAX:

        print
        print user_guessed_word_spaced

        user_guess = guess_letter(letters_guessed)
        letters_guessed.add(user_guess)

        if user_guess in word_to_guess:

            print "\nYeah yeah.. We're all impressed.. {} is in the word woohoo..".format(user_guess)

            for i, letter in enumerate(word_to_guess):
                if user_guess == letter:
                    hidden[i] = letter

            user_guessed_word_spaced = ' '.join(hidden)
            if user_guessed_word_spaced == word_to_guess_spaced:
                print
                print word_to_guess
                break

        else:
            print "{}.. Really? That's the best you can do.. Not in my word..".format(user_guess)
            hang_size += 1
            print HANGMAN[hang_size]

    return user_guessed_word_spaced == word_to_guess_spaced


if __name__ == '__main__':

    is_winner = play_hangman()
    if is_winner:
        print '\nYou win! Finally you did something right!'
    else:
        print '\nGAME OVER'
\$\endgroup\$
  • 3
    \$\begingroup\$ Should ascii_lowercase be used instead for the ALPHABET? Less likely to miss a letter or mistype it (e.g. adding by accident greek "o" when ALPHABET is created manually would create a hard to spot bug). \$\endgroup\$ – Fermi paradox Jul 19 '16 at 12:06
  • 1
    \$\begingroup\$ Probably, and I have done so in my own question about hangman. However as the poster is a beginner (judging by the code), and the review was already long enough I opted for the clearer less ideal solution. Good catch though! \$\endgroup\$ – N3buchadnezzar Jul 19 '16 at 12:10
  • \$\begingroup\$ I love that picture. I come from a Ruby background so I understand about 90% of what you're saying, this was my first attempt at Python program, which isn't to shabby if I do say so myself. But you make some very valid points and I really appreciate you taking the time to go through my code. \$\endgroup\$ – YoYoYo I'm Awesome Jul 19 '16 at 15:20
6
\$\begingroup\$

Your WORD_LIST doesn't really make sense. It isn't a list, but it should be. Your rand_words are exactly what lists were made for. A dictionary is better only when you need keys. When your keys are numerical, a list makes a lot more sense. Of course, hard-coding the words isn't a very good idea. Once the game is played five times, words are fairly likely to re-appear. If you had the words in a separate file (or pulled them off the internet), you could have a lot more words, and you could theoretically add more later.


I wouldn't put GAME OVER directly into the hang man picture. Whatever uses the picture should do whatever it wants at the end.


Your name capitalization is inconsistent. Constants should be all-caps; variables should be lower case. It makes sense for the word list and the hangmen to be all-caps, but HIDDEN, for example, changes in your function. LETTERS_GUESSED should change, but it doesn't. Your if user_guess in letters_guessed: block should have a continue in it instead of asking for its own input. That way, the user can make the same mistake as many times as he wants. You can then add a line after it (not in the block) that adds to the list of guessed letters. I believe that WORD_TO_GUESS should be lowercase because, although it stays the same within one game, it is not the same across different games. It can change. If you ever create a program that runs the game more than once, it becomes clear that it is not really a constant. Since it changes per game, I would add it to the function itself.


Your function is called begin_game. What does it do? It does not just begin the game; it runs the whole thing. It might be a good idea to have a begin_game function that prints all of that stuff that shows up at the beginning, but this function does not deserve the name.


I have already discussed your second point. Now for the third (the first will come later). Your syntax is of course correct. (There would be an error if it weren't). As for the advisability, your while loop makes sense. The for loop could be simplified to:

hidden = [w if user_guess == w else h for w, h in zip(word_arr, hidden)]
if '_' not in hidden:
    print 'You win! ...'
    return

Notice that I changed your if hidden.count... line. All you want to do is check if _ appears. Python already invented a simpler way. I also changed quit() to return so that the function can be re-used. You might consider having the function return True when the player wins and False when the player loses. That way, whatever calls the function can decide what happens with a win or loss.


Now for your first point. To print a list of strings as one, use .join():

print ' '.join(hidden)

The space is used because that is what we want to show up between the letters. If you would rather use something else, change it.

\$\endgroup\$
5
\$\begingroup\$

Can I turn the list that is shown into a single string?

Sure!

print ','.join(HIDDEN)

str.join(iterable):

Return a string which is the concatenation of the strings in the iterable iterable. The separator between elements is the string providing this method.

Is there a better way to use a random word?

There's no reason that you need to use a dictionary here. A list subscript implementation:

WORD_LIST = [
    'sergeanty',
    'amphipneustic'
]

WORD_TO_GUESS = WORD_LIST[randint(0, len(WORD_LIST)-1]

Am I using the for and while loop in the correct syntax?

You don't need to check HIDDEN.count('_') more than once per guess. Take the conditional out of your for loop to avoid the redundant checks:

for num in range(len(word_arr)):
    if user_guess == word_arr[num]:
        HIDDEN[num] = user_guess

if HIDDEN.count('_') == 0:
    print 'You win! Finally you did something right!'
    quit()

In terms of syntax, they are indeed correct, otherwise the interpreter would have thrown a SyntaxError.

Check out the style guide for best practices.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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