"""
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()
syntax highlighted by Code2HTML, v. 0.93pm6