""" An interactive fiction game implemented as a finite state machine. Jim Mahoney | Oct 2012 | MIT License """ from random import randint # This is designed to be an illustration one of the cleaner ways to # implement an interactive fiction program, using what computerists # call a "finite state machine". # # The game itself is a simplified "dungeon crawl" with this room layout. # # North # # kitchen # | # West entrance -- parlor East # | # bedroom # # South # # You can be in any one of the four rooms. That's all that can change # in this tiny game; therefore, there are 4 states, where "a state" is # a complete description of the current situation. # # states = ('entrance', 'parlor', 'kitchen', 'bedroom', '?') # # OK, that's actually 5 states. I've added an extra to represent # any error situations that might arise. If all goes well, we'll # never get to '?'. # # The state changes in response to possible actions. There are only 5 # possible actions in this game (I said it was tiny), namely the # 4 compass directions and 'look'. # # actions = ('n', 's', 'e', 'w', 'l') # A complete specification of what can happen in this situation # (including what is told to the user) can be thought of as a big # table of (current_state, action) => (new_state, output) that defines # what change happens when. This can be encoded as a python # dictionary, as given below. # # In the dictionary I'm using a 6th action-like wildcard symbol '*' # to mean "all the other actions I haven't specified". # # In a larger game the state would include much more than just the current # room. For example, if there was a cat in the rooms, it could be a # tuple such as (person_location, cat_location). For any practical # game, the full table would be gigantic. So typically, only part of # the transition logic is hardcoded into a table, while the rest is done # with some sort of program logic, as in the transition() function # below. Nevertheless, the transition function still accomplishes # the same thing, to map current states and actions to future # states and output dialogs. # # Additional game behaviors may also be implemented outside the finite # state model. In the code below I have two such special behaviors, # namely 'quit' and 'magic'. But the essense of the engine is # still in the finite state machine. # # OK, enough talking. Here's the first of the game data, # the guts of the finite state machine transition map. transition_dict = { # prev_state action next_state output_dialog # ---------- ------ ---------- ---------------------------- ('entrance', 'e') : ('parlor', 'You walk into the parlor.'), ('entrance', 'l') : ('entrance', 'You see muddy boots in the entrance.'), ('entrance', '*') : ('entrance', 'You bump into an entrance wall.'), ('parlor', 'n') : ('kitchen', 'You walk into the kitchen.'), ('parlor', 's') : ('bedroom', 'You stroll into the bedroom.'), ('parlor', 'l') : ('parlor', 'There is a comfy chair in the parlor.'), ('parlor', '*') : ('parlor', 'You bump into a parlor wall.'), ('kitchen', 's') : ('parlor', 'You saunter into the parlor.'), ('kitchen', 'l') : ('kitchen', 'The kitchen has many dirty dishes.'), ('kitchen', '*') : ('kitchen', 'You bump into a kitchen wall.'), ('bedroom', 'n') : ('parlor', 'You jump into the parlor.'), ('bedroom', 'l') : ('bedroom', 'The bedroom ... hmmm.'), ('bedroom', '*') : ('bedroom', 'You trip over the bedpost.'), } # And here's the rest of the game data. # # Note clearly that I have *not* embedded this stuff into the game # logic. By putting it here, outside the functions, it's all in one # place, can be adapted for other games, and the variable names # document the purpose of the various strings and characters. # # It's usually good practice to separate the data from the program logic. default_action = '*' error_state = '?' error_dialog = 'An unexpected earthquake has killed you.' magic_exit_states = ['bedroom'] magic_odds = 10 # i.e. 1 chance in 10 magic_dialog = 'You find a portal under the bed \n' + \ 'that drops you into a fantasy realm \n' + \ 'where you live happily ever after.' prompt = 'Choose n,s,e,w,look,quit : ' quit_dialog = 'Quitting? So soon? Fine - be that way.' quit_action = 'q' start_state = 'entrance' start_dialog = '\nYou are standing in the entrance.' end_dialog = 'THE END' # With the game transition data defined, next we implement the mapping # from previous to new states given the user chosen action, extending # the transition_dict to cover all possible (state,action) pairs. def transition(state, action): """ Return (new_state, "dialog output") """ for key in [(state, action), (state, default_action)]: if transition_dict.has_key(key): return transition_dict[key] return (error_state, error_dialog) # All that's left is to define up the loop that changes the state, and # then to inoke it. A few utility functions help keep things tight. def interactive_loop(beginning_state, ending_state): """ Get user actions, change state, and print dialog text. """ state = beginning_state while state != ending_state: # Get the user's action choice. action = input2action(raw_input('\n' + prompt)) # End the game quickly if the user has had enough. if action == quit_action: print quit_dialog return # The heart of the finite state algorithm : (state, dialog) = transition(state, action) print dialog # And just for fun, a random 'magic' game exit if state in magic_exit_states and one_chance_in_(magic_odds): print magic_dialog return def input2action(user_input): """ Convert the user_input string to a possible game action """ # In a larger game this might take some sophisticated parsing; # here we're just looking at the first letter typed, lowercase. return user_input[0].lower() def one_chance_in_(x): """ Return True with odds 1 out of x """ return randint(1, x) == 1 def main(): print start_dialog interactive_loop(start_state, error_state) print end_dialog if __name__ == '__main__': main()