# Implementing Hangman using a State Pattern

I decided to implement a Hangman game using an object oriented State Pattern design. This is one of my first actual OOP as I normally write functional code.

Code use:

if __name__ == '__main__':
game = Context('represents', lives=5) # hangman word is represents
game.startGame()


I'm mostly happy with this but wanted to check:

• is there a nicer way of having DeadState / WonState where I don't have to use pass for the process
• I've copied the style of docstrings from Flask Restless Github. Is this correct use of docstrings?
• is there better way of combining user input and the Context class than the .startGame() method? I've seen it mentioned on Stack Overflow you shouldn't have input statements inside classes.
class Context(object):
"""A class that keeps track of the hangman game context and current state.
This follows a State Design Pattern.

word is string. This is the word to guess.
guessed_letters is a list containing letters guessed
lives is a number that is the number of incorrect guesses allowed.
state is class representing the state of the game.
message is a string message from processing the letter guess.

The :meth:.process method processes guessed letters, updating the
lives, guessed_letters and message for the gameplay.
"""
def __init__(self, word, lives=10):
self.word = word
self.guessed_letters = []
self.lives = lives
self.state = HoldingState()
self.message = ''

def won(self):
"""Tests if all letters of in word are in guessed_letters"""
return all(l in self.guessed_letters for l in self.word)

def lost(self):
"""Tests if we have run out of lives"""
return self.lives <= 0

def hangman_word(self):
"""Returns a string in hangman form where letters are hyphens
or guessed letters. Letters or hyphens are space separated.

word is a string. This is the word to be printed in hangman form.

guessed_letters is a list of the letters guessed that should be shown.

For example::

>>> hangman_word('bordereau', ['o', 'd', 'e'])
'_ o _ d e _ e _ _'

"""
return ' '.join(l if l in self.guessed_letters else '_' for l in self.word)

def process(self, letter):
"""Requests the state to process the guessed letter.

The State takes care of updating the lives, guessed_letters and providing a
message for the gameplay"""
self.state.process(letter, self)

def startGame(self):
"""Runs the game.

Checks for final states (WonState / DeadState) else shows the hangman
word, letters guessed and takes as input the letter guessed and
processes this.

"""
while True:
if self.state == WonState():
print(f'You win! The word was indeed {self.word}')
return
print(f'You lose! The word was {self.word}')
return
print('Hangman Word:', self.hangman_word() + '\n')
print('Letters guessed: ' + ' '.join(sorted(self.guessed_letters)) + '\n')
letter = input('Guess a letter: ')
self.process(letter)
print('\n' + self.message + '\n')

class BaseState(object):
"""Represents a state of the context.

This is an abstract base class. Subclasses must override and
implement the :meth:.process method.

The :meth:.process method updates the context variables and changes the
context state.

"""
def process(self, letter, context):
"""Updates the context variables and changes the context state..

**This method is not implemented in this base class; subclasses
must override this method.**

"""
raise NotImplementedError

def __eq__(self, other):
"""Overrides the default implementation

Tests equality of states by testing if there are the same class

"""
if isinstance(other, self.__class__):
return True
return False

class HoldingState(BaseState):
"""The default state."""
def process(self, letter, context):
if letter in context.guessed_letters:
context.state = RepeatedGuessState()
elif letter in context.word:
context.state = CorrectGuessState()
else:
context.state = IncorrectGuessState()
context.state.process(letter, context)

class RepeatedGuessState(BaseState):
"""An intermediate state."""
def process(self, letter, context):
context.message = "You've repeated a guess!"
context.state = HoldingState()

class CorrectGuessState(BaseState):
"""An intermediate state."""
def process(self, letter, context):
context.message = "Correct guess!"
context.guessed_letters.append(letter)
if context.won():
context.state = WonState()
else:
context.state = HoldingState()

class IncorrectGuessState(BaseState):
"""An intermediate state."""
def process(self, letter, context):
context.lives -= 1
context.message = f"Incorrect guess! You've lost a life. {context.lives} lives remaining"
context.guessed_letters.append(letter)
if context.lost():
else:
context.state = HoldingState()

"""A final state representing a lost game."""
def process(self, letter, context):
pass

class WonState(BaseState):
"""A final state representing a game won."""
def process(self, letter, context):
pass

if __name__ == '__main__':
game = Context('represents')
game.startGame()


Neither BaseState nor any of its derived classes contain any data. You don’t need to keep creating instances of these state objects. You could create one of each as global constants. Then, you wouldn’t need the special __eq__ method, since you could simply test identity equality.

You could make BaseState and its derived classes callable objects, by defining a __call__(self, letter, context) method, instead of a process(self, letter, context) method. You could then invoke the state processing by calling self.state(letter, self).

Of course, an class with no data and only a single function is effectively just a function, and functions are first class objects in Python, so you can ditch the classes all together, and store the function as the “state”. Eg)

def repeated_guess_state(letter, context):
"""An intermediate state"""
context.message = "You’ve repeated a guess!"
context.state = holding_state


But wait! Why are these outside of the Context class? We have to pass context to each state handler. If these were methods defined on the Context class, they’d automatically get self, which is the context, passed to them effectively for free:

class Context:

# ... __init__, won, lost, hangman_word methods omitted

def process(self, letter):
self.state(letter)

def repeated_guess_state(self, letter):
"""An intermediate state"""
self.message = "You’ve repeated a guess!"
self.state = self.holding_state


Now these state processors (methods) actually part of the Context class, so that don’t have to reach into another class and manipulate the private internals. They have legitimate access to the internal details. So these data members should be declared private ... which in Python is little more than the convention of a leading underscore (eg, self._lives, self._state, and so on).

is there a nicer way of having DeadState / WonState where I don't have to use pass for the process

Nothing wrong with using pass. You have a function the does nothing.

You could make the states actually print the win/loss messages, and then transition to a “finished” state. The finished state would likely have the pass statement, though. Alternately, you could use None as the finish state, and loop while self.state: instead of while True:

I've copied the style of docstrings from Flask Restless Github. Is this correct use of docstrings?

The exact format of the docstrings is dependant on the tool you use to automatically generate documentation with. Codes like:

:meth:.process


are used by “Sphinx” documentation generator. See it for details.

is there better way of combining user input and the Context class than the .startGame() method? I've seen it mentioned on Stack Overflow you shouldn't have input statements inside classes.

There is nothing wrong with asking for input inside of a class. But ...

Separating I/O from game logic allows you to ...

• create a text-based console game,
• create a Graphical UI based game, using the same game logic
• test the game code using automated test software

In the first case, the input comes from input() and goes to print() statements, in the second it comes from & goes to (say) TkInter, and the third it comes from and goes to test code.

You’ve mostly separated the I/O already. Wherever the input comes from, you need to pass it to self.process(...), and the output (mostly) goes to self.message. You just need to factor out the console I/O, and maybe places that into class ConsoleHangman(Context):.

class TkInterHangman(Context): left as exercise to student.