Skip to content

Commit

Permalink
fixes #3, fixed max recursion issue, removed gratuitous docs -- less …
Browse files Browse the repository at this point in the history
…is more
  • Loading branch information
manuphatak committed Dec 18, 2015
1 parent 0739aca commit f4d3752
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 120 deletions.
2 changes: 1 addition & 1 deletion hangman/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def cli():
"""
Start a new game.
"""
controller.game_loop()
controller.run()


if __name__ == '__main__':
Expand Down
39 changes: 25 additions & 14 deletions hangman/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,13 @@
This module is responsible for guiding the user through the game.
"""
from __future__ import absolute_import

from hangman.utils import FlashMessage, GameOver, GameWon, GameFinished
from . import view
from .model import Hangman


# noinspection PyPep8Naming
def game_loop(game=Hangman(), flash=FlashMessage()):
"""
Main game loop.
:param hangman.model.Hangman game: Hangman game instance.
:param hangman.utils.FlashMessage flash: FlashMessage utility
:return:
"""
while True:
try:
view.draw_board(game, message=flash)
Expand All @@ -32,15 +25,33 @@ def game_loop(game=Hangman(), flash=FlashMessage()):
flash.game_won = True
except ValueError as msg:
flash(msg)
except KeyboardInterrupt:
return view.say_goodbye()
except GameFinished:
break

if view.prompt_play_again():
# reuse classes originally passed into function
GameClass, FlashClass = game.__class__, flash.__class__

return game_loop(game=GameClass(), flash=FlashClass())
def run(game=Hangman(), flash=FlashMessage()):
"""
Run ``game_loop``, handle exit.
Logic is separated from game_loop to cleanly avoid recursion limits.
:param hangman.model.Hangman game: Hangman game instance.
:param hangman.utils.FlashMessage flash: FlashMessage utility
"""

# noinspection PyPep8Naming
GameClass, FlashClass = game.__class__, flash.__class__
while True:
try:
game_loop(game=game, flash=flash)
except KeyboardInterrupt:
# Exit immediately
return view.say_goodbye()

if not view.prompt_play_again():
break

# reuse classes passed in from arguments
game, flash = GameClass(), FlashClass()

return view.say_goodbye()
112 changes: 20 additions & 92 deletions hangman/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@

class Hangman(object):
"""
The hangman game object contains the logic for managing the status of the
game and raising key game related events.
The the logic for managing the status of the game and raising key game related events.
>>> from hangman.model import Hangman
>>> game = Hangman(answer='hangman')
Expand Down Expand Up @@ -46,13 +45,9 @@ class Hangman(object):
# -------------------------------------------------------------------

def __init__(self, answer=None):
"""
Instantiate a new game. Populate answer if necessary.

:param str answer: answer to game instance
:raises: ValueError
"""
if not answer:
# Populate answer
answer = WordBank.get()

# Validate answer.
Expand All @@ -68,67 +63,31 @@ def __init__(self, answer=None):

@property
def misses(self):
"""
Get list of misses.
:rtype: [str]
"""
return sorted(list(self._misses))

@misses.setter
def misses(self, value):
"""
`self.misses` setter. Check for game over.
:param value: A single letter.
:raises: GameOver
"""

self._misses = set(value)
if self.remaining_turns <= 0:
raise GameOver
def misses(self, letters):
for letter in letters:
self._add_miss(letter)

@property
def hits(self):
"""
Get list of hits.
:rtype: [str]
"""
return sorted(list(self._hits))

@hits.setter
def hits(self, value):
"""
`self.hits` setter. Check for game won.
:param value: A single letter.
:raises: GameWon
"""

self._hits = set(value)
if self._hits == set(self.answer):
raise GameWon
def hits(self, letters):
for letter in letters:
self._add_hit(letter)

@property
def remaining_turns(self):
"""
Calculate number of turns remaining.
"""Calculate number of turns remaining."""

:return: Number of turns remaining.
:rtype: int
"""
return self.MAX_TURNS - len(self.misses)

@property
def status(self):
"""
Build a string representation of status with letters for hits and _
for unknowns.
:return: game status as string
:rtype: str
"""
"""Build a string representation of status."""
hits = self.hits

def fill_in(letter):
Expand All @@ -140,80 +99,49 @@ def fill_in(letter):
# -------------------------------------------------------------------

def guess(self, letter):
"""
Check if guess is a hit or miss.
:param str letter: Letter to check
:return: self
:rtype: Hangman
:raises: ValueError
"""
"""Add letter to hits or misses."""

# validate input
if not self.is_valid_guess(letter):
raise ValueError('Must be a letter A-Z')

# add to hits or misses
is_miss = letter.upper() not in self.answer
if is_miss:
self.add_miss(letter)
self._add_miss(letter)
else:
self.add_hit(letter)
self._add_hit(letter)

return self

# UTILITIES
# -------------------------------------------------------------------

def add_miss(self, value):
"""
Add a miss to the model. Check for game over.
def _add_miss(self, value):
"""Add a letter to misses. Check for game over."""

:param value: A single letter.
:raises: GameOver
"""
self._misses.add(value.upper())
if self.remaining_turns <= 0:
raise GameOver

def add_hit(self, value):
"""
Add a hit to the model. Check for game won.
def _add_hit(self, value):
"""Add a letter to hits. Check for game won"""

:param value: A single letter.
:raises: GameWon
"""
self._hits.add(value.upper())
if self._hits == set(self.answer):
raise GameWon

def is_valid_answer(self, word):
"""
Validate answer. Letters only. Max:16
"""Validate answer. Letters only. Max:16"""

:param str word: Word to validate.
:return:
:rtype: bool
"""
word = str(word).upper()
return not not self._re_answer_rules.search(word)

def is_valid_guess(self, letter):
"""
Validate guess. Letters only. Max:1
"""Validate guess. Letters only. Max:1"""

:param str letter: Letter to validate
:return:
:rtype: bool
"""
letter = str(letter).upper()
return not not self._re_guess_rules.search(letter)

def __repr__(self):
"""
Build a human readable representation of self.
:return: namedtuple with status, misses, and remaining_turns
:rtype: namedtuple
"""

return repr(self._repr(self.status, self.misses, self.remaining_turns))
26 changes: 13 additions & 13 deletions tests/test_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ def flash():


@pytest.fixture
def game_loop(game, flash):
from hangman.controller import game_loop
def run(game, flash):
from hangman.controller import run

return partial(game_loop, game=game, flash=flash)
return partial(run, game=game, flash=flash)


def test_setup():
Expand All @@ -50,38 +50,38 @@ def test_setup():
assert not view.prompt_play_again()


def test_game_over(game, game_loop, monkeypatch, flash):
def test_game_over(game, run, monkeypatch, flash):
monkeypatch.setattr('hangman.view.prompt_guess', lambda: 'O')
game.misses = list('BCDEFIJKL')

assert game_loop() == 'Have a nice day!'
assert run() == 'Have a nice day!'
assert flash.game_over is True
assert flash.game_answer == 'HANGMAN'


def test_game_won(game, game_loop, flash):
def test_game_won(game, run, flash):
game.hits = list('HNGMN')

assert game_loop() == 'Have a nice day!'
assert run() == 'Have a nice day!'
assert flash.game_won is True


def test_value_error(game, game_loop, monkeypatch):
def test_value_error(game, run, monkeypatch):
monkeypatch.setattr('hangman.view.prompt_guess', Mock(side_effect=['1', 'A']))
game.hits = list('HNGMN')

assert game_loop() == 'Have a nice day!'
assert run() == 'Have a nice day!'


def test_keyboard_interupt(game_loop, monkeypatch):
def test_keyboard_interupt(run, monkeypatch):
monkeypatch.setattr('hangman.view.prompt_guess', Mock(side_effect=KeyboardInterrupt))

assert game_loop() == 'Have a nice day!'
assert run() == 'Have a nice day!'


def test_game_finished(game_loop, monkeypatch):
def test_game_finished(run, monkeypatch):
monkeypatch.setattr('hangman.view.prompt_guess',
Mock(side_effect=['H', 'A', 'N', 'G', 'M', 'N', KeyboardInterrupt]))
monkeypatch.setattr('hangman.view.prompt_play_again', Mock(side_effect=[True, False]))

assert game_loop() == 'Have a nice day!'
assert run() == 'Have a nice day!'

0 comments on commit f4d3752

Please sign in to comment.