Skip to content

Instantly share code, notes, and snippets.

@r3
Last active May 3, 2017 15:40
Show Gist options
  • Save r3/29f7ed8c8ebb05e97c58 to your computer and use it in GitHub Desktop.
Save r3/29f7ed8c8ebb05e97c58 to your computer and use it in GitHub Desktop.
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