Last active
March 13, 2018 00:42
-
-
Save nathggns/b08094d78a497cdc1da2 to your computer and use it in GitHub Desktop.
Python hangman game
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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