Last active
May 3, 2017 15:40
-
-
Save r3/29f7ed8c8ebb05e97c58 to your computer and use it in GitHub Desktop.
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
import random | |
import json | |
CONTENT_PATH = './content.json' | |
DEBUG = True | |
#raw_input = input # Hack for Python 3 | |
class Scene(object): | |
# The scene class will keep track of a few variables that are | |
# reused throughout the script. | |
# The `name` var will designate which content is loaded, where you | |
# converted names to file pathes and opened a TXT file, I loaded | |
# a single JSON file as a `dict` and assign it to `Scene.scenes` | |
# Also a prompt, because I got tired of typing ">>> " (DRY) | |
name = None | |
prompt = ">>> " | |
scenes = None | |
# In Python, we can create line-wrapped strings using parens, just | |
# like a tuple, but without any commas: ("foo" "bar") == "foobar" | |
# I feel it's more important for the text to be readable than for the | |
# spaces prior to the value be equal. | |
deaths = {'giant': ("The giant picks you up by your feet and bites " | |
"off your head."), | |
'starve': ("You continue on. Darkness falls on the forest " | |
"around you. You are not seen again."), | |
'disappear': ("You wander into the passage alone and wander " | |
"in the darkness for the rest of your days."), | |
'pit': ("Wrong key! The trapdoor swings open beneath " | |
"you. You enter a free fall and land on the " | |
"wooden spikes."), | |
'beheading': ("Wrong. In a single blow, the skeleton slices " | |
"off your head. Later.")} | |
# I just want to re-iterate that the above are class attributes which | |
# are accessible from even subclass instances due to Python's MRO | |
def __init__(self): | |
# Because of Python's MRO, the `self.scenes` reference will resolve to | |
# `Scene.scenes`. It starts with the sentinel value of `None`. when the | |
# first `Scene` (this includes subclasses) to be instantiated will see | |
# that `Scene.scenes` is `None` and call the `_load_scenes` method. | |
if self.scenes is None: | |
self._load_scenes() | |
self.play() | |
# Decorators are awesome, but I recognize that you may not have been | |
# introduced. For now, simply recognize that this class will have a | |
# reference to the class passed implicitly as the first argument, | |
# whereas a normal method would be passed a reference to the instance | |
# The implication of this method being a class method is that, when | |
# run, it will assign the instantiated JSON content to `Scene.scenes` | |
# and since that's accessible to all instances of `Scene` children, | |
# it will only be executed once. Once run a single time, the | |
# `Scene.__init__` method will see that the object exists and skips | |
# executing the method. Were it a instance method, it would assign | |
# the instantiated JSON to the invoking instance, and would therefore | |
# be executed once for each instance of each `Scene` object | |
@classmethod | |
def _load_scenes(cls): # <= note the `cls` instead of `self` | |
# It doesn't matter what we call the first positional arg; | |
# there's nothing special about "self" or "cls" and you could | |
# just as easily use "foo" but don't. It's a strong convention. | |
# The following is a context manager (`with`). it ensures that | |
# the file handler (`raw_json`) is closed when execution leaves | |
# the scope. | |
with open(CONTENT_PATH) as json_stream: | |
cls.scenes = json.loads(json_stream.read()) | |
# Since each `Scene` object (or subclass) has a `name` attribute, we | |
# can generalize the `play_scene` method to simply retrieve the | |
# appropriate string and print it based on the calling instance's name | |
def play(self): | |
# In the parent `Scene` class, there is no proper name, so we raise | |
# a `NotImplementedError` to inform the user that they did something | |
# that was not intended to happen (attempt to play the `Scene` class) | |
if self.name is None: | |
# Note the means I used to turn this long expression into two | |
# lines. Not everyone will find this to be worthwhile, but I try | |
# to adhere to PEP8's 80 characters per line restriction as it | |
# makes it easier for me to view two columns of code with my | |
# screen split. It's a conventional nice number. | |
raise NotImplementedError( | |
# The `format` method from the `str` builtin class is a | |
# replacement for the "old %s formatting" % "string" | |
# which I find to be ugly and less capable | |
"Scene {} has no name".format(type(self))) | |
print(self.scenes[self.name]) | |
# Again, you'll notice the way I turned a long expression into a | |
# multi-line expression. This can also be used to improve readability | |
# if you feel the need to comment on a single line... | |
def user_choice(self, | |
live_answers, | |
death_answers, | |
print_possibilities=True): # ...like this default arg | |
possibile_answers = live_answers + death_answers | |
if print_possibilities: | |
random.shuffle(list(possibile_answers)) | |
print(possibile_answers) | |
# You'll notice `None` used frequently as a sentinel value in Python | |
# and it's become a convention. Here we use it to allow the user to | |
# have as many attempts as necessary to type things in properly | |
# I changed the name to `answer` because you had `live_answers` | |
# already defined. Might as well keep to your established style to | |
# avoid surprising readers (principle of least astonishment) | |
answer = None | |
while answer not in possibile_answers: | |
answer = raw_input(self.prompt) | |
# You may be tempted to write out an if/then statement for a bool | |
# return value: | |
# | |
# if answer in live_answers: | |
# return True | |
# else: | |
# return False | |
# | |
# But that's an anti-pattern (anti-idiom?) when you can do this: | |
return answer in live_answers | |
# All that's left of the `Death` class. | |
def die(self, death_name): | |
print(self.deaths[death_name]) | |
# Again, the `Scene` isn't meant to be instantiated and used, but to be | |
# sub-classed, but it's good to have a default fall back for if someone | |
# forgets to implement the method, or attempts to use a `Scene` directly | |
def enter(self): | |
raise NotImplementedError( | |
"Scene {} has no interaction".format(type(self))) | |
class RoomOne(Scene): | |
# Here's where we see that `name` class attribute | |
name = 'room_one' | |
# This `enter` method overrides the one in `Scene` that raises the | |
# exception. I won't mention uses of overriding again, but it's | |
# worth pointing out this least once. | |
def enter(self): | |
user_lives = self.user_choice( | |
# Note the use of tuples here. The objects we use can say a | |
# great deal about our intent. A tuple is obviously not meant | |
# to be mutated, so one can expect that the `Scene.user_choice` | |
# method to be pure (not have side effects). It's a minor thing | |
# here, but the concept is useful. | |
live_answers=('go into the house', 'walk toward the smoke'), | |
# It's also worth noting that I'm passing these args with | |
# keywords. While it isn't strictly necessary as I pass the | |
# collections in the proper order, it does add to the | |
# readability of the code. It's nice to not just pass around | |
# ugly collections of clutter without some indication as | |
# to what it is. | |
death_answers=('keep going', 'turn around', 'pass', 'ignore')) | |
# Note how this reads like English. It's a ternary statement, but | |
# what's important is that I've set it up to read naturally. I follow | |
# this pattern in the remaining `Scene` children classes: | |
# | |
# 1. Get user info using `Scene.user_choice` | |
# 2. Pass `Scene.user_choice` our acceptable and unacceptable values | |
# and get a bool | |
# 3. Ternary instantiates and enters the next `Scene` or dies. | |
Cottage().enter() if user_lives else self.die('starve') | |
class Cottage(Scene): | |
name = 'cottage' | |
def enter(self): | |
user_lives = self.user_choice( | |
live_answers=('hide',), | |
death_answers=('fight the giant', 'run away', 'do nothing')) | |
Dungeon().enter() if user_lives else self.die('giant') | |
class Dungeon(Scene): | |
name = 'dungeon' | |
def enter(self): | |
# You had called this `input`, but `input` is a builtin function. | |
# It's best not to shadow variables, and never good to shadow | |
# builtins. Attempting to shadow keywords will result in errors. | |
target = random.randint(1, 3) | |
if DEBUG: | |
print(target) | |
user_lives = self.user_choice( | |
live_answers=(target,), | |
death_answers=('none', 'leave'), | |
# I choose not to print the options here because mixed up | |
# `int`s between 1 and 3 (inclusive) is useless: | |
# Choose one: 3, 1, 2 # <= Ugly! | |
print_possibilities=False) | |
Tunnels().enter() if user_lives else self.die('disappear') | |
class Tunnels(Scene): | |
name = 'tunnels' | |
# Okay, I lied. I'm going to mention overriding again. | |
# This one is entirely a convenience method, or more accurately: | |
# Useless. I added it to throw you a curveball with `super`. | |
# It will call the parent's implementation of `user_choice` and | |
# pass the `correct_answer` as a tuple (to be iterable as | |
# `Scene.user_choice` requires iterables), an empty tuple to | |
# serve as the `death_answers`, though any iterable would suffice. | |
# This works because of the implementation of `Scene.user_choice` | |
# Only `live_answers` is checked for membership of the user's input. | |
# Any value that is not in `live_answers` is implicitly a wrong answer | |
def user_choice(self, correct_answer): | |
return super(Tunnels, self).user_choice( | |
live_answers=(correct_answer,), | |
death_answers=(), | |
# This time we surpress printing the possibilities as there is | |
# only one possibility: the correct one. | |
print_possibilities=False) | |
def enter(self): | |
# Your method worked, but look back and see how it marches | |
# off to the right of the page. This is nicer looking, and | |
# makes it easier for me to adhere to my 80 char convention | |
# We iterate over the three correct answers and essentially use | |
# the same pattern we've used for every `enter` method above. | |
# If the user enters an incorrect answer, it jumps out of the | |
# loop and dies. | |
# If the user gets all three answers correct, the for-loop | |
# will have finished without `break`ing, which is the condition | |
# for the `else` clause to be executed. Again, the `else` block | |
# will only execute if the for-loop is terminated with a | |
# StopIteration exception (minor simplification). | |
for correct_answer in ("Dancing in the Dark", "1972", "20"): | |
user_answers_correctly = self.user_choice(correct_answer) | |
if not user_answers_correctly: | |
self.die('beheading') | |
break # Causes `else` clause to not execute | |
else: | |
TreasureRoom() | |
class TreasureRoom(Scene): | |
name = 'treasure_room' | |
if __name__ == '__main__': | |
RoomOne().enter() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment