A little story engine

15 Jul 2016

# Okay, so we're trying to make something where:
# 1. the player can play it, and it's a bit like a Ren'Py game.
# 2. an author can write it, and they (mostly?) write a data structure,
#    not code.
#
# So we need a (terrible) little game to work with, as an example.
#
# Here's a description.
# On the first screen, you see some text about the game,
# and some text about two possible protagonists, Red and Blue,
# and you're offered a choice of who you want to play as.
# Regardless who you choose to play as, you go to the second screen.
# On the second screen, you see some text describing a generic crisis,
# and you're offered some choices of your response.
# There are four conceptually possible choices at this step.
# One is "clearly bad", and always available.
# One is an action that Red can do, only available if you chose to play as Red.
# One is an action that Blue can do, only available if you chose to play as
# Blue.
# One is a "kick the can down the road" option, and always available.
# If you choose the bad choice, you go to a "lose" ending screen.
# If you choose an action that only your character can do, you go to a "win"
# ending screen.
# If you choose to "kick the can down the road", you get another screen,
# and another choice.
# This time, there is only a clearly bad option and a mediocre option,
# and both lead to ending screens.
# From all the ending screens, you can start a new game.
#
# How might we express this example as a data structure?
#
# We could represent all the screens as a single dictionary.
# Then each screen has a description and some choices.
# Each choice has some consequences.
#
# When the modeling question gets tough, we "kick the can",
# by delegating to our future selves and saying that we will
# write some functions or classes that will build the
# objects that we want our author to use.
#

screens = {
  'starting': {
    'description': "This is an example game. Would you like to play as Red or as Blue?",
    'choices': [
      { 'text': 'Play as Red', 'consequences': [ Set('protagonist', 'red'), Goto('crisis') ] },
      { 'text': 'Play as Blue', 'consequences': [ Set('protagonist', 'blue'), Goto('crisis') ] },
    ]
  },
  'crisis': {
    'description': "A crisis has occurred. What do you do?",
    'choices': [
      { 'text': 'Succumb', 'consequences': [ End('You succumb ignominiously.') ] },
      { 'text': 'Use my red personality', 'requires': [ Equal('protagonist', 'red') ],
         'consequences': [ End('Your reddish personality averts the crisis!') ] },
      { 'text': 'Use my blue hair', 'requires': [ Equal('protagonist', 'blue') ],
         'consequences': [ End('Your blueish hair averts the crisis!') ] },
      { 'text': 'Dilly-dally and delay', 'consequences': [ Goto('followon') ] },
    ]
  },
  'followon': {
    'description': "The crisis has worsened. What do you do?",
    'choices': [
      { 'text': 'Succumb', 'consequences': [ End('You succumb ignominiously.') ] },
      { 'text': 'Recover', 'consequences': [ End('You avert the crisis, but some cookies crumbled.') ] },
    ]
  }
}

# Anyway, let's try writing a story engine that consumes this data structure.

def story_engine(story):
  vars = { 'current_screen': 'starting', 'done': False }
  while not vars['done']:
    screen = screens[vars['current_screen']]
    print screen['description']
    choices = screen['choices']
    for i in range(0, len(choices)):
      choice = choices[i]
      if choice_is_ok(choice, vars):
        print "%d. %s" % (i, choice['text'])
    # TODO: What if the player types a number that isn't on the list?
    # TODO: What if the player types something that isn't a number?
    # TODO: What if the list of choices is empty?
    choice = int(raw_input("? "))
    for consequence in screen['choices'][choice]['consequences']:
      consequence.Run(vars)

# This "kicks the can" by requiring a function 'choice_is_ok",
# that deals with whether or not a choice is actually allowed.
# But that's not too hard:

def choice_is_ok(choice, vars):
  if 'requires' not in choice:
    return True
  for requirement in choice['requires']:
    if requirement.Evaluate(vars) == False:
      return False
  return True

# And the other things that we postponed are not too hard:

class Set:
	def __init__(self, key, value):
		self.key = key
		self.value = value
	def Run(self, vars):
		vars[self.key] = self.value

class Goto:
	def __init__(self, next_screen):
		self.next_screen = next_screen
	def Run(self, vars):
		vars['current_screen'] = self.next_screen

class End:
	def __init__(self, endingtext):
		self.endingtext = endingtext
	def Run(self, vars):
		vars['done'] = True
		print self.endingtext

class Equal:
	def __init__(self, key, value):
		self.key = key
		self.value = value
	def Evaluate(self, vars):
		return vars[self.key] == self.value

# TODO: Add templating, so that the author can write a story that has
# lines like "%(loveinterest)s eyes you with %(emotion)s".

if __name__ == "__main__":
  story_engine(screens)