patternpythonModerate
Python Hangman program
Viewed 0 times
programpythonhangman
Problem
To learn Python, I've been working through Learn Python the Hard Way, and to exercise my Python skills I wrote a little Python Hangman game (PyHangman - creative, right?):
```
#! /usr/bin/env python2.7
import sys, os, random
if sys.version_info.major != 2:
raw_input('This program requires Python 2.x to run.')
sys.exit(0)
class Gallows(object):
def __init__(self):
'''Visual of the game.'''
self.state = [
[
'\t _______ ',
'\t | | ',
'\t | ',
'\t | ',
'\t | ',
'\t | ',
'\t________|_',
],
[
'\t _______ ',
'\t | | ',
'\t O | ',
'\t | ',
'\t | ',
'\t | ',
'\t________|_',
],
[
'\t _______ ',
'\t | | ',
'\t O | ',
'\t | | ',
'\t | | ',
'\t | ',
'\t________|_',
],
[
'\t _______ ',
'\t | | ',
'\t O | ',
'\t \| | ',
'\t | | ',
'\t | ',
'\t________|_',
],
[
'\t _______ ',
'\t | | ',
'\t O | ',
'\t \|/ | ',
'\t | | ',
'\t | ',
'\t________|_',
],
[
'\t _______ ',
'\t | | ',
'\t O | ',
'\t \|/ | ',
'\t | | ',
'\t / | ',
'\t________|_',
],
[
'\t _______
```
#! /usr/bin/env python2.7
import sys, os, random
if sys.version_info.major != 2:
raw_input('This program requires Python 2.x to run.')
sys.exit(0)
class Gallows(object):
def __init__(self):
'''Visual of the game.'''
self.state = [
[
'\t _______ ',
'\t | | ',
'\t | ',
'\t | ',
'\t | ',
'\t | ',
'\t________|_',
],
[
'\t _______ ',
'\t | | ',
'\t O | ',
'\t | ',
'\t | ',
'\t | ',
'\t________|_',
],
[
'\t _______ ',
'\t | | ',
'\t O | ',
'\t | | ',
'\t | | ',
'\t | ',
'\t________|_',
],
[
'\t _______ ',
'\t | | ',
'\t O | ',
'\t \| | ',
'\t | | ',
'\t | ',
'\t________|_',
],
[
'\t _______ ',
'\t | | ',
'\t O | ',
'\t \|/ | ',
'\t | | ',
'\t | ',
'\t________|_',
],
[
'\t _______ ',
'\t | | ',
'\t O | ',
'\t \|/ | ',
'\t | | ',
'\t / | ',
'\t________|_',
],
[
'\t _______
Solution
I'll get a few trivial comments out of the way before addressing some serious concerns about the flow of control in your program.
User experience
Overall, the application feels like it's nicely put together. I enjoyed playing it while writing this review.
Python 3 compatibility
You explicitly check that the game is running on Python 2. With a few changes, all related to
-
Put the following shim at the top, then replace all
-
Replace all
-
However, the following printing code, which uses a trailing comma to suppress the newline in Python 2, requires special treatment:
I suggest the following replacement code, which takes a better approach even if you weren't interested in Python 3:
Wordlist
Gallows
Interaction between
-
The way
Flow of control
All the critiques above are minor details. The most important problem with the code, in my opinion, is that you are misusing recursive function calls as a kind of goto.
To see a symptom of the problem, play a few rounds, then hit CtrlC. Notice that the stack trace is very deep — it contains one call to
The remedy is to use loops for looping.
Here's how I would write it (without making too many of the recommended changes listed above).
```
def play(word):
""" Play one game of Hangman for the given word. Returns True if the
player wins, False if the player loses. """
gallows = Gallows()
wrong_guesses = 0 # number of incorrect guesses
blanks = set_blanks(word) # blanks which hide each letter of the word until guessed
used = [] # list of used letters
while True:
new_page()
print(gallows.get_image(wrong_guesses))
print(' '.join(blanks))
print(' '.join(used))
guess = input("Guess a letter: ")
blanks, used, missed = check_letter(word, guess.lower(), blanks, used)
if blanks == list(word):
return endgame(True, word)
elif missed and wrong_guesses >= 6:
return endgame(False, word)
elif missed:
wrong_guesses += 1
def endgame(won, word):
print('')
if won:
print("Congratulations, you win!")
print("You correctly guessed the word '%s'!" % word)
else:
print("Nice try! Your word was '%s'." % word)
return won
def play_again():
while True:
play_again = input("Play again? [y/n] ")
if 'y' in play_again.lower():
User experience
Overall, the application feels like it's nicely put together. I enjoyed playing it while writing this review.
- The past participle of the verb "to hang" is "hung", but when talking about executions, it should be "hanged". ("Hung" isn't completely unacceptable usage, though.)
endgame()should not clear the screen. For me, part of the fun of playing Hangman is to see the final state of the game, and clearing the screen denies me that pleasure.
Python 3 compatibility
You explicitly check that the game is running on Python 2. With a few changes, all related to
input() and print(), you can also get it to run on Python 3.-
Put the following shim at the top, then replace all
raw_input() calls with input().if sys.version_info.major < 3:
# Compatibility shim
input = raw_input-
Replace all
print "string" with print("string")-
However, the following printing code, which uses a trailing comma to suppress the newline in Python 2, requires special treatment:
for x in blanks:
print x,
print '\n'
for x in used:
print x,
print '\n'I suggest the following replacement code, which takes a better approach even if you weren't interested in Python 3:
print(' '.join(blanks))
print(' '.join(used))Wordlist
- Buried in the middle of a long program, you hard-coded
"test.txt". That's bad for maintainability; hard-coding the same filename twice is even worse. Instead, theWordListconstructor should take a filename parameter.
- You might have a file descriptor leak in the constructor, since you
open()the file not using awithblock or callingclose(). Therefore, you rely on the garbage collector to trigger the closing, which won't necessarily happen in all Python interpreters.
- No need to assign the variable
word. Justreturn line.lower().strip().
Gallows
Gallows.set_state()is a misnomer. TheGallowsobject doesn't actually keep any state! A more accurate name would beGallows.get_image().
- I would prefer it if
Gallowsdid keep track of the number of wrong guesses, and had methods.increment_count()and.is_hanged(). Currently, you hard-code 6 as a limit inplay(), a number that you hope is consistent with the number of images available inGallows(minus one for the initial image).
Interaction between
reset(), check_letter(), and play()- You pass clusters of variables around a lot. Those are actually state variables, so these three functions would be better as members of a class.
check_letter()should not takemissedas a parameter. It's an output, not an input.
-
The way
check_letter() handles correct guesses is clumsy. I would write# replace the corresponding blank for each instance of guess in the word
elif guess in word:
for index, char in enumerate(word):
if char == guess:
blanks[index] = guess
used += guess # add the guess to the used letter listFlow of control
All the critiques above are minor details. The most important problem with the code, in my opinion, is that you are misusing recursive function calls as a kind of goto.
To see a symptom of the problem, play a few rounds, then hit CtrlC. Notice that the stack trace is very deep — it contains one call to
play() for every guess ever made in the history of the program, including previous rounds. It is inappropriate to keep such state around in the call stack.The remedy is to use loops for looping.
Here's how I would write it (without making too many of the recommended changes listed above).
```
def play(word):
""" Play one game of Hangman for the given word. Returns True if the
player wins, False if the player loses. """
gallows = Gallows()
wrong_guesses = 0 # number of incorrect guesses
blanks = set_blanks(word) # blanks which hide each letter of the word until guessed
used = [] # list of used letters
while True:
new_page()
print(gallows.get_image(wrong_guesses))
print(' '.join(blanks))
print(' '.join(used))
guess = input("Guess a letter: ")
blanks, used, missed = check_letter(word, guess.lower(), blanks, used)
if blanks == list(word):
return endgame(True, word)
elif missed and wrong_guesses >= 6:
return endgame(False, word)
elif missed:
wrong_guesses += 1
def endgame(won, word):
print('')
if won:
print("Congratulations, you win!")
print("You correctly guessed the word '%s'!" % word)
else:
print("Nice try! Your word was '%s'." % word)
return won
def play_again():
while True:
play_again = input("Play again? [y/n] ")
if 'y' in play_again.lower():
Code Snippets
if sys.version_info.major < 3:
# Compatibility shim
input = raw_inputfor x in blanks:
print x,
print '\n'
for x in used:
print x,
print '\n'print(' '.join(blanks))
print(' '.join(used))# replace the corresponding blank for each instance of guess in the word
elif guess in word:
for index, char in enumerate(word):
if char == guess:
blanks[index] = guess
used += guess # add the guess to the used letter listdef play(word):
""" Play one game of Hangman for the given word. Returns True if the
player wins, False if the player loses. """
gallows = Gallows()
wrong_guesses = 0 # number of incorrect guesses
blanks = set_blanks(word) # blanks which hide each letter of the word until guessed
used = [] # list of used letters
while True:
new_page()
print(gallows.get_image(wrong_guesses))
print(' '.join(blanks))
print(' '.join(used))
guess = input("Guess a letter: ")
blanks, used, missed = check_letter(word, guess.lower(), blanks, used)
if blanks == list(word):
return endgame(True, word)
elif missed and wrong_guesses >= 6:
return endgame(False, word)
elif missed:
wrong_guesses += 1
def endgame(won, word):
print('')
if won:
print("Congratulations, you win!")
print("You correctly guessed the word '%s'!" % word)
else:
print("Nice try! Your word was '%s'." % word)
return won
def play_again():
while True:
play_again = input("Play again? [y/n] ")
if 'y' in play_again.lower():
return True
elif 'n' in play_again.lower():
return False
else:
print("Huh?")
def main(words_file='test.txt'):
wordlist = Wordlist(words_file)
new_page()
print("\nWelcome to Hangman!")
print("Guess the word before the man is hanged and you win!")
input("\n\t---Enter to Continue---\n")
new_page()
while True:
play(wordlist.new_word())
if not play_again():
breakContext
StackExchange Code Review Q#43318, answer score: 10
Revisions (0)
No revisions yet.