Skip to content

Instantly share code, notes, and snippets.

@nathggns
Last active March 13, 2018 00:42
Show Gist options
  • Save nathggns/b08094d78a497cdc1da2 to your computer and use it in GitHub Desktop.
Save nathggns/b08094d78a497cdc1da2 to your computer and use it in GitHub Desktop.
Python hangman game
# All this code is in one file for simplicity. It wouldn't normally be.
#
# This code makes advanced use of classes and regular expressions.
# Good luck with it.
# Let me know if there's anything you can't understand.
#
# Oh it also used list comprehensions. Enjoy those.
import re
# Handles all of the logic for the game.
# @note Does not take any input or give any output from the user
# @see {IO} Look here for user io handling
class Game:
# Init a game.
#
# @param {number} maxTries The maximum number of tries this game has
def __init__(self, maxTries):
self.maxTries = maxTries
self.tries = 0;
# Set the word we're testing against.
# @param {string} word The word to test against
def setWord(self, word):
self.letters = list(word)
self.matches = map(lambda item: None, self.letters)
# Try a letter in the game.
#
# @param {string} letter The letter to test
# @return {bool} If the letter matched or not
def tryLetter(self, letter):
if self.isLost():
raise Exception('Cannot try again. Game has been lost')
if letter not in self.letters:
self.tries = self.tries + 1
return False
newMatches = [
match if match is not None else
letter if self.letters[idx] == letter else
match
for idx, match in enumerate(self.matches)
]
self.matches = newMatches
return True
# Check if the game is won or not
# Uses the matches list to do so
# @return {bool} is the game finished yet
def isWon(self):
return len(
# Takes out any matches letters
filter(
# A mini function to check if a letter
# has been matches yet
lambda item: item is not None,
self.matches
)
) == len(self.letters)
# Check if game is lost or not
def isLost(self):
return self.tries >= self.maxTries
# Get the stats for the game that is outputted
# @note Maybe this shouldn't be here? Not sure. Hmm...
def getStats(self):
return {
'letters' : ['-' if match is None else match for match in self.matches],
'tries' : self.tries,
'maxTries' : self.maxTries
};
# A simple class to test a string against a compiled-ahead-of-time regex
class RegexTester:
# @param {string} regex the regex to use to test
def __init__(self, regex):
self.compiled = re.compile(regex)
# @param {string} word the word to test against our compiled regex
def test(self, word):
return self.compiled.match(word)
# Handles the input for getting a word from a user
class WordInputter:
def __init__(self, wordTester):
self.wordTester = wordTester
def input(self, onNotValid):
result = raw_input('Please choose a word: ')
if not self.wordTester.test(result):
return onNotValid(self)
return result
# Handles the input for getting a letter from a user
# @todo Maybe reduce code repetition between this class and {WordInputter}
class LetterInputter:
def __init__(self, letterTester):
self.letterTester = letterTester
def input(self, onNotValid):
result = raw_input('Please choose a letter: ')
if not self.letterTester.test(result):
return onNotValid(self)
return result
# Handles outputting the game stats on every try
class GameStatsOutputter:
def __init__(self, game):
self.game = game
def output(self):
stats = self.game.getStats()
print('Current number of tries: %d/%d' % (stats['tries'], stats['maxTries']))
print(stats['letters'])
# Handles all of the IO of the game
class IO:
def __init__(self, game, inputters, outputters):
self.game = game
self.inputters = inputters
self.outputters = outputters
def start(self):
self.outputters['heading'].output()
self.game.setWord(self.askForWord())
self.letterLoop()
# This is the loop in which a user is asked for a word, it is validated, and is asked again if it fails
def askForWord(self):
# This is called when a word is not valid
def onInvalid(inputter):
self.outputters['wordInvalid'].output()
return self.askForWord()
return self.inputters['word'].input(onInvalid)
# This is the letter handling loop
def letterLoop(self):
self.outputters['gameStats'].output()
# If the game is lost, we need to tell the user and stop here
if self.game.isLost():
self.outputters['gameIsLost'].output()
return
# If the game is won, let's tell the user and stop here
if self.game.isWon():
self.outputters['gameIsWon'].output()
return
# We'll ask for a letter, falling back to None if it isn't valid
letter = self.inputters['letter'].input(lambda inputter: None)
# If the letter wasn't valid, we want to tell the user that
# and start again
# @note Maybe this should be done if the notValid callback above. Not sure.
if not letter:
self.outputters['letterIsInvalid'].output()
return self.letterLoop()
# If the letter isn't in the word, we want to tell the user that and ask for another
if not self.game.tryLetter(letter):
self.outputters['letterIsNotInWord'].output()
return self.letterLoop()
# At this point, the letter is in the word
self.outputters['letterIsInWord'].output()
# Start the process again.
return self.letterLoop()
# Turns a string into a outputter that IO can use
# @note Probably wouldn't use something like this in production code. I just cba typing
# @note How this works is really advanced. Just ignore it.
def s2o(s):
def printFromLambda(child):
print(s)
return type('fromLambda', (), { 'output' : printFromLambda })()
# From here is procedural code. This would normally be in a separate file from
# This creates our instance of our game, with a maximum number of tries
game = Game(9)
# This creates an instance of our IO handler with all the correct inputters and outputters
io = IO(
game,
{
# Words can only consist of one or more a-z characters
'word' : WordInputter(RegexTester('^[a-z]+$')),
# A letter is only one of an a-z character
'letter' : LetterInputter(RegexTester('^[a-z]$'))
},
{
'heading' : s2o('Welcome to my hangman game'),
'wordInvalid' : s2o('Your word is not valid. Please only choose words with the letters a-z'),
'letterIsInvalid' : s2o('You did not input a valid a-z letter'),
'letterIsInWord' : s2o('Found a letter!'),
'letterIsNotInWord' : s2o('Letter is not in word. Try again.'),
'gameIsLost' : s2o('You lost!'),
'gameIsWon' : s2o('You won!'),
# This one isn't just outputting a string, it's outputting the stats of the game
'gameStats' : GameStatsOutputter(game)
}
)
io.start()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment