If a function just calls another function, delete one of them. You
don't need both main()
and testing()
, since the former does nothing
other than call the latter.
Global variables are almost never needed. Don't organize programs around
the need to modify global variables. Instead, organize your programs around
functions that rely only on their local variables (or methods that follow
similar principles). If some functions need access to the same information,
pass that data back and forth using function arguments and return values.
Don't put logic in top-level code. At the top level of a program, you can
perform imports or define constants, functions, or classes. Put all other code
inside of functions or methods. If we apply all of the preceding ideas to your
current code, we end up with the following structure:
def main():
lives = 6
chosen_word = random.choice(WORD_LIST)
...
while lives > 0:
guess = ask_input()
blanks, lives = replace_blank(guess, chosen_word, blanks, lives)
...
def ask_input():
...
def replace_blank(guess, chosen_word, blanks, lives):
...
return (blanks, lives)
Try to design functions so that they perform a single task. This
reorganization of the code highlights some awkwardness in the implementation.
The main()
function initializes lives
and blanks
but then delegates
responsibility for updating them to the replace_blank()
function. Meanwhile,
that function has various other responsibilities, like printing messages to
the user and clearing the terminal.
Missing features. Many hangman games tell the user what letters have
already been guessed and I've never seen one that penalizes a player for
guessing an already-guessed letter. I mention this missing behavior because it
points the way toward a less awkward code design. The first step is to select a
data structure to hold prior guesses. A set is a natural fit. In fact, if we
know the initial number of lives, the secret word, and the prior guesses, all
other information about game state can be derived (remaining lives, whether
the game is finished, how the secret word should be displayed, etc).
Try to separate logic/algorithm from interactivity and printing. Code that
interacts with users is a hassle to test and debug. Ideally, you want most of
your program to operate purely in the data realm: for functions that means
receiving data and returning data; for methods it might mean receiving data and
then returning data or modifying internal data. However, many programs require
interactivity and related printing. Programs like this are often good fits for
a design that used a main function to handle interactions and an immutable
object that knows hold to hold the current state and then how to compute the
next state. Let's start with a sketch of the interactive layer. This
is where we perform all printing and collecting of user input.
Notice especially that this part of the program contains very little algorithmic logic.
import random
import os
WORD_LIST = [...]
HANGMANPICS = [...]
def main():
# Setup.
gs = GameState(random.choice(WORD_LIST))
# Play until done.
while not gs.is_finished:
# Printing.
os.system('clear')
print(gs.graphic)
print('Secret word:', gs.display_word)
print('Prior guesses:', gs.display_guesses)
if gs.message:
print(gs.message)
# Derive the next state based on user input.
gs = gs.next_state(ask_input())
# Wrap up.
print(gs.outcome)
def ask_input():
while True:
guess = input("Guess a letter? ").lower()
if guess.isalpha() and len(guess) == 1:
return guess
if __name__ == '__main__':
main()
The rest of the code just requires implementing the needed data-retrieval
behaviors in GameState. As noted, the initial state will hold an empty set
for prior guesses. A message
attribute will be used to store information
needed by our interactive layer when printing game status.
from dataclasses import dataclass, field
@dataclass(frozen = True)
class GameState:
word : str
prior_guesses : set = field(default_factory = set)
message : str = ''
INITIAL_LIVES = len(HANGMANPICS) - 1
BLANK = '_'
Inside the GameState class, we need to implement various read-only properties to get
information derived from our core pieces of data (the word and the prior
guesses):
class GameState:
@property
def lives(self):
return self.INITIAL_LIVES - sum(
char not in self.word
for char in self.prior_guesses
)
@property
def display_word(self):
return ''.join(
char if char in self.prior_guesses else self.BLANK
for char in self.word
)
@property
def display_guesses(self):
return ' '.join(sorted(self.prior_guesses))
@property
def is_finished(self):
return self.lives <= 0 or self.word == self.display_word
@property
def graphic(self):
return HANGMANPICS[self.INITIAL_LIVES - self.lives]
@property
def outcome(self):
return (
None if not self.is_finished else
'You won' if self.display_word == self.word else
'You lost'
)
Finally, the class needs a method to generate a new GameState instance
based on the current state and the user's guess:
class GameState:
def next_state(self, guess):
w = self.word
pg = self.prior_guesses
message = (
f'Already guessed: {guess}' if guess in pg else
f'Correct: {guess}' if guess in w else
f'Not in word: {guess}. You lose a life'
)
return GameState(w, pg | {guess}, message)
With a design like this, the vast majority of your functionality
could be tested and debugged purely by working with the data-oriented
GameState class.