A fun little post came across my feed today thanks to a re-blog by @mproxima. The post was by @steampunkkaja and discussed a bit of Python trouble they were having with a Hangman game they're making. You can read the original post here, and I encourage you to do so!
I will forward this by saying: I don't typically program in Python. It's not a language I use very often, and it's not one I would say I "know"... but at this point, I can look at most languages and more-or-less understand what's going on.
SO! Let's jump in and fix this program with that out of the way. First, let's list the two problems @steampunkkaja is encountering so we can keep them in our mind.
- If you correctly guess a word, the game never ends.
- If you don't correctly guess a word and use up all of your available guesses, the game still never ends.
Reading this, you may think that these are the same problem, and you'd be right... but I'm getting ahead of myself. Let's step back and look at the code in a couple of chunks:
And the code block, for anyone wanting to try this themselves so you can copy/paste:
import random
libraryA = ['act','bat','carp','day']
libraryB = ['butte','donkey','effective','metre']
libraryG = ['bog','fog','frog','grog','trogdor']
libraryX = ['sesquipedalian','varyag']
library = [libraryA,libraryB,libraryG,libraryX]
def num_to_blanks(num):
return '_' * num
wordlist = random.choice(library)
word = random.choice(wordlist)
break_word = list(word)
display = list(num_to_blanks(len(word)))
guesses = 6
Here we have the first section - where we're doing our basic setup of default variables. So far, this is all pretty standard, and nothing immediately jumps out as being a problem. There are a few minor things I'd change, but that's likely due to my background in Java/'C' languages which avoids the use of underscores in variables, and prefers cammelCase for combined words. I'd personally change wordlist
to wordList
and break_word
to breakWord
but that's not a big deal at all.
That said, let's move on now to our function definitions:
def hang(letter,guesses):
if letter not in word:
guesses = guesses -1
print('That letter is not in the word, you now have ', guesses, ' guesses remaining.')
return guesses
First up is the hang()
function, which is a simple abstraction where we're evaluating if a guessed letter was correct or not. And, it's the first section with an issue we can fix!
Python has these little... "quirks" lets call them... that make it not super intuitive as a beginner language, and here we're running into one right off the bat.
Remember how we set up guesses
as a default value of 6? This, in Python, is considered a Global Variable. Remember that, because it's about to become important for two reasons.
Reason 1: When you are interacting with a global variable, you need to specify that the variable you're talking about is in-fact the Global one... because Python assumes by default that any variable created within a function only has scope within the function it's created in. That's a problem when you want to pass data back and forth at a high level like this. There's a few ways you could fix it, but we'll focus on the simple one in a minute.
Reason 2: Global Variables cannot be Parameters. So, in hang(word,guesses)
we've explicitly set that as a passed-in parameter/argument. In most languages this is the proper way to do things but, with Python... not so much.
So, how do we fix it?
Well, we could either remove all references to guesses
as a parameter we pass and explicitly target the global variable or we could create a new variable to pass around but that's not really worth the added complexity here. Which leaves us with the first option, and that ends up looking like this:
def hang(letter):
if letter not in word:
global guesses
guesses = guesses - 1
print('That letter is not in the word, you now have ', guesses, ' guesses remaining.')
And there we go! We're getting a countdown as expected! Woo-hoo!
Now, if you're paying close attention here you'll note that I also removed an extra line in this function - the Return statement.
I think that the idea of that return
was to set the global variable, but that's not actually how return
works in Python. In a function, return
tells the program 'if I call this function by itself, spit out the value of guesses
'. There's no point where we'd ever say print(hang(word))
and expect a value, so we can remove it entirely.
So, that solves problem 1 already, but let's still look at the guess_letter()
function, as there's some stuff here we can improve as well.
def guess_letter(word,guesses):
letter = input('Choose a letter: ')
if letter not in word:
hang(letter,guesses)
return guesses
if letter in word:
if word.count(letter) == 1:
print('That letter is in the word once')
display[word.find(letter)] = letter
return display
if word.count(letter) > 1:
print('That letter is in the word', word.count(letter), 'times')
for i in range(len(word)):
if break_word[i] == letter:
display[i] = letter
return display
if guesses == 0:
print('You Lose. The word was:', word)
As we just discussed, we can immediately rip guesses
out of everything here, and set a single reference to the Global variable at the start of the function. We can also remove the 'return' for guesses
as we never need to spit that out. Similarly, we can remove the return display
inside this function. We will never be calling guess_letter
alone and expecting a value.
At this point, we should have a function looking like this:
def guess_letter(word):
global guesses
letter = input('Choose a letter: ')
if letter not in word:
hang(letter)
if guesses == 0:
print('You Lose. The word was:', word)
if letter in word:
if word.count(letter) == 1:
print('That letter is in the word once')
display[word.find(letter)] = letter
if word.count(letter) > 1:
print('That letter is in the word', word.count(letter), 'times')
for i in range(len(word)):
if break_word[i] == letter:
display[i] = letter
We also have moved the last IF statement up further in the function. Why? Because at the heart of it, IF statements are evaluating and qualifying data. Here, it doesn't matter as much because we haven't nested this inside of a 'while' loop nor are these Else-If statements, so it's still going to evaluate the if letter in word
statement no matter what. Ideally, we'd like to set this up in a way that we don't need to evaluate the truth of this statement if we already know it's not valid.
Particularly in larger programs, you want to make sure you're only executing code when it's needed, as it'll help speed up the overall runtime by skipping anything it doesn't need. You'd best be able to see this when running through a debugger, and going line by line. Even after you printed 'you lost' you'd still see the program hit line 32 and do a boolean check on if the letter was in the word.
We can solve that here by first nesting the guess evaluation since it'll only ever change if a value is NOT in a word, and we can then further set the letter in word
statement to be an else-if, which will only evaluate if the first if statement isn't true
.
def guess_letter(word):
global guesses
letter = input('Choose a letter: ')
if letter not in word:
hang(letter)
if guesses == 0:
print('You Lose. The word was:', word)
elif letter in word:
if word.count(letter) == 1:
print('That letter is in the word once')
display[word.find(letter)] = letter
if word.count(letter) > 1:
print('That letter is in the word', word.count(letter), 'times')
for i in range(len(word)):
if break_word[i] == letter:
display[i] = letter
There are other improvements we could make here, such as using 'else' in place of if word.count(letter) > 1:
since it's not 0 times (we disqualified that first), and it's not exactly equal to 1 time... so therefore it could only be more than one... but that's not strictly necessary. The code executes fine either way.
Now, finally, let's look at the smallest section of change - the guess_word()
function:
def guess_word(word,guesses):
attempt = input('Guess the word: ')
if attempt == word:
print('You win!')
guesses = 0
if attempt != word:
guesses -= 1
print('Sorry, that is not the word. You have:',guesses,'guesses remaining.')
return guesses
if guesses == 0 and attempt != word:
print('You Lose. The word was:',word)
Because we already did the heavy lifting earlier in this code review, we can basically just apply the relevant portions to this chunk and leave it as is. AKA: let's remove the 'return' and cut 'guesses' out of the arguments we're passing in.
Resulting in:
def guess_word(word):
global guesses
attempt = input('Guess the word: ')
if attempt == word:
print('You win!')
guesses = 0
if attempt != word:
guesses -= 1
print('Sorry, that is not the word. You have:',guesses,'guesses remaining.')
if guesses == 0 and attempt != word:
print('You Lose. The word was:',word)
This leaves us with just the core runnable part of the program to look at:
print('Welcome to Hangman!')
print('The word to guess is ', display)
while guesses != 0:
option = input('Enter "1" to guess a letter or "2" to guess the word: ')
if option == '1':
guess_letter(word,guesses)
print(display)
if option == '2':
guess_word(word,guesses)
if guesses == 0:
break
Here again, we're just going to remove the guesses
arguments. We don't need them since now we're working directly with the Global variable, and that brings us to:
print('Welcome to Hangman!')
print('The word to guess is ', display)
while guesses != 0:
option = input('Enter "1" to guess a letter or "2" to guess the word: ')
if option == '1':
guess_letter(word)
print(display)
if option == '2':
guess_word(word)
if guesses == 0:
break
A fully fixed game that you now absolutely can lose!
And, hopefully, a deeper understanding of how if statements and global variables work!
Python can be a tricky language, it's not as strongly typed as something like Java, and behaves a little differently under the hood. It is a fairly simple language though with a ton of power. Definitely one worth learning!