3: Handling Multiple TurnsThis is Part 3 of an ongoing series about writing interactive fiction games in Python. By the end of Part 2 we had created a text-based user interface and explored one way of storing multiple scenes. This part will finally bring the needed glue for the player to move between all of the scenes in the story. In other words, we'll have a game! You're going to be amazed at how simple this is to do. We'll start with a simple, clumsy approach.
# ifiction.py
# - An interactive fiction game
import textwrap # For nice formatting of the description
import sys # For exiting the game
scenes = {
"field": {
"description": "You are standing in a field. To the north of you are some mountains, " \
"to the east of you is a forest, to the west of you is a cave, and to " \
"the south of you is a valley.",
"paths": [
{ "go_to": "mountains", "phrase": "Go to the mountains" },
{ "go_to": "forest", "phrase": "Go to the forest" },
{ "go_to": "cave", "phrase": "Go into the cave" },
{ "go_to": "valley", "phrase": "Go to the valley" }
]
},
"mountains": {
"description": "You are standing at the foot of a mountain range. Huge impassable peaks " \
"loom over you. There is a cave to the east, and a field south of you " \
"leading into a valley.",
"paths": [
{ "go_to": "cave", "phrase": "Go into the cave" },
{ "go_to": "field", "phrase": "Go south into the field" }
]
},
"forest": {
"description": "A giant confused bear mistakes your for one of her cubs and takes you " \
"away with her. Although you eventually learn to love your new bear " \
"family, your adventuring days are over.",
"paths": [ ]
},
"cave": {
"description": "You are in a long dark cave. You see points of daylight at either end of " \
"the cave, one to the northeast and one to the southwest.",
"paths": [
{ "go_to": "mountains", "phrase": "Go northwest" },
{ "go_to": "field", "phrase": "Go southwest" }
]
},
"valley": {
"description": "You are standing in the middle of a huge, beautiful valley. Standing right " \
"before you is ... whatever it was you were looking for. Success!",
"paths": [ ]
}
}
scene = scenes["field"]
while 1 == 1: # Watch out, could be an infinite loop!
next_step = None
description = scene["description"]
paths = scene["paths"]
print textwrap.fill(description)
# Show the menu for this scene.
for i in range(0, len(paths)):
path = paths[i]
menu_item = i + 1
print "\t", menu_item, path["phrase"]
print "\t(0 Quit)"
# Get the user selection from the menu.
prompt = "Make a selection (0 - %i): " % len(paths)
while next_step == None:
try:
choice = raw_input(prompt)
menu_selection = int(choice)
if menu_selection == 0:
next_step = "quit"
else:
index = menu_selection - 1
next_step = paths[ index ]
except (IndexError, ValueError):
print choice, "is not a valid selection!"
if next_step == "quit":
print "Good bye!"
sys.exit()
else:
scene = scenes[ next_step["go_to"] ]
print "You decided to:", next_step["phrase"]
The changes really are simple. I decided to put the
whole process of describing the scene and getting user
input into a Let's take a look at running the game.
You are standing in a field. To the north of you are some mountains,
to the east of you is a forest, to the west of you is a cave, and to
the south of you is a valley.
1 Go to the mountains
2 Go to the forest
3 Go into the cave
4 Go to the valley
(0 Quit)
Make a selection (0 - 4): 1
You decided to: Go to the mountains
You are standing at the foot of a mountain range. Huge impassable
peaks loom over you. There is a cave to the east, and a field south of
you leading into a valley.
1 Go into the cave
2 Go south into the field
(0 Quit)
Make a selection (0 - 2): 2
You decided to: Go south into the field
You are standing in a field. To the north of you are some mountains,
to the east of you is a forest, to the west of you is a cave, and to
the south of you is a valley.
1 Go to the mountains
2 Go to the forest
3 Go into the cave
4 Go to the valley
(0 Quit)
Make a selection (0 - 4): 4
You decided to: Go to the valley
You are standing in the middle of a huge, beautiful valley. Standing
right before you is ... whatever it was you were looking for. Success!
(0 Quit)
Make a selection (0 - 0): 0
Good bye!
Congratulations, it's a game! You can stop at this point. The game is complete, and there is nothing more that needs to be done. There are some more things I would like to do with the game before I move on. I invite you to follow me in the process of making our code more pleasant to read. I will spend time wandering from thought to thought. You will probably learn less about programming, but quite a bit about how I look at programs. Cleaning upThe game works, but it could stand to be cleaned up. Refactoring is the practice of examining your application code and deciding what changes would make the code easier to read, faster, or just plain better in some way, but without changing what the program does. That's the hard part. It is so tempting to add new features as soon as you think of them. That leads to a pile of unreadable code, sooner or later. That pile usually shows up sooner if you don't refactor often enough. Trust me. I am speaking from years of experience creating huge piles of unreadable code. Some developers may argue that this program is too small
for refactoring to be much use. After all, my copy is only
87 lines including It isn't difficult, either. We can start by searching for clumsy-looking blocks of code which make it harder to figure out what's going on. This looks like a good candidate right here.
# Get the user selection from the menu.
prompt = "Make a selection (0 - %i): " % len(paths)
while next_step == None:
try:
choice = raw_input(prompt)
menu_selection = int(choice)
if menu_selection == 0:
next_step = "quit"
else:
index = menu_selection - 1
next_step = paths[ index ]
except (IndexError, ValueError):
print choice, "is not a valid selection!"
What do we want? We want the user to tell us what she wants to do next. The user picks a number which could lead to another scene or quitting. Let us define it in a function.
# Scene definitions
# ...
# Function definitions
def select_path(paths):
next_step = None
# Show the menu for this scene.
for i in range(0, len(paths)):
path = paths[i]
menu_item = i + 1
print "\t", menu_item, path["phrase"]
print "\t(0 Quit)"
# Get the user selection from the menu.
prompt = "Make a selection (0 - %i): " % len(paths)
while next_step == None:
try:
choice = raw_input(prompt)
menu_selection = int(choice)
if menu_selection == 0:
next_step = "quit"
else:
index = menu_selection - 1
next_step = paths[ index ]
except (IndexError, ValueError):
print choice, "is not a valid selection!"
return next_step
We just moved the code into a function The rest of the function block looks like the original
chunk of code, until it reaches the end. Instead of doing
something with the selected As I was saying - if
# Game starts here.
scene = scenes["field"]
while 1 == 1: # Watch out, could be an infinite loop!
description = scene["description"]
paths = scene["paths"]
print textwrap.fill(description)
next_step = select_path(paths)
if next_step == "quit":
print "Good bye!"
sys.exit()
else:
scene = scenes[ next_step["go_to"] ]
print "You decided to:", next_step["phrase"]
The original chunk has been replaced by a single line of code. This has made things a little more readable, but I still see a lot of changes we could make. Yes, I really do program like this. It is a faster process than you think, especially if you're not narrating as you write code. What would I like to do next? Well, I don't like the
way
That is more or less how this reads to me in English. The second part is okay, but the first part is nonsensical. Let's roll up our sleeves and make some sense out of this. First off: we know we are going to be working with the
user input portion of this function, and maybe in a big
way. Let's protect Here is a first version of
# Function definitions
def menu_input(paths):
next_step = None
prompt = "Make a selection (0 - %i): " % len(paths)
while next_step == None:
try:
choice = raw_input(prompt)
menu_selection = int(choice)
if menu_selection == 0:
next_step = "quit"
else:
index = menu_selection - 1
next_step = paths[ index ]
except (IndexError, ValueError):
print choice, "is not a valid selection!"
return next_step
def select_path(paths):
# Show the menu for this scene.
for i in range(0, len(paths)):
path = paths[i]
menu_item = i + 1
print "\t", menu_item, path["phrase"]
print "\t(0 Quit)"
# Get the user selection from the menu.
next_step = menu_input(paths)
return next_step
The current phrasing of
How do you say that in Python? You say it with recursion.
def menu_input(paths):
prompt = "Make a selection (0 - %i): " % len(paths)
try:
choice = raw_input(prompt)
menu_selection = int(choice)
if menu_selection == 0:
next_step = "quit"
else:
index = menu_selection - 1
next_step = paths[ index ]
except (IndexError, ValueError):
print choice, "is not a valid selection!"
# Try again!
next_step = menu_input(paths)
return next_step
I have managed to clean up the code merely by changing
the way I phrased the task. What does
There is one more change I would like to make to
def menu_input(paths):
prompt = "Make a selection (0 - %i): " % len(paths)
try:
choice = raw_input(prompt)
menu_selection = int(choice)
if menu_selection == 0:
selected_path = "quit"
else:
index = menu_selection - 1
selected_path = paths[ index ]
except (IndexError, ValueError):
print choice, "is not a valid selection!"
# Try again!
selected_path = menu_input(paths)
return selected_path
Choosing a variable name can be a tricky business. It doesn't have much effect on how the program runs, but it can have a huge impact on how easy it is for you to read the code. I discuss code reading a lot, and there is a good reason for that. You will ultimately be spending more time reading code than writing it. Even if you only work on your own projects, you will have to review the code multiple times. And program code is not written for the computer. It's written for the programmer. All the computer needs are the specific machine instructions to perform a task. The reason we don't write much in machine language these days is the fact that we don't have to. Computers are powerful enough to provide layers between us and the machine language. So, if you are writing code, write for people. You can think of it as a story if you want to. Try to make it like a story by Ernest Hemingway, a man who was famous for writing simply and clearly.
My writing is a long way from his, but I keep this goal in my head while I write code. I am nearly done with refactoring this code. The
while 1 == 1: # Watch out, could be an infinite loop!
description = scene["description"]
paths = scene["paths"]
print textwrap.fill(description)
next_step = select_path(paths)
if next_step == "quit":
print "Good bye!"
sys.exit()
else:
scene = scenes[ next_step["go_to"] ]
print "You decided to:", next_step["phrase"]
It is very tempting to rewrite this as a
recursive function, since it worked so well for menu input.
Unfortunately, that may not work for a game loop. Python
has built-in recursion limits, which you can find from the
library function Oh well, I guess I could put this block into its own function.
def play_game(start_scene):
scene = start_scene
while 1 == 1: # Watch out, could be an infinite loop!
description = scene["description"]
paths = scene["paths"]
print textwrap.fill(description)
next_step = select_path(paths)
if next_step == "quit":
print "Good bye!"
sys.exit()
else:
scene = scenes[ next_step["go_to"] ]
print "You decided to:", next_step["phrase"]
# Game starts here.
play_game(scenes["field"])
This does make the application code simple. It's just a function call to play the game, starting with the "field" scene. And really, this is as far as I feel like refactoring the game code. Here is the final form of our simple interactive fiction game.
# ifiction.py
# - An interactive fiction game
import textwrap # For nice formatting of the description
import sys # For exiting the game
scenes = {
"field": {
"description": "You are standing in a field. To the north of you are some mountains, " \
"to the east of you is a forest, to the west of you is a cave, and to " \
"the south of you is a valley.",
"paths": [
{ "go_to": "mountains", "phrase": "Go to the mountains" },
{ "go_to": "forest", "phrase": "Go to the forest" },
{ "go_to": "cave", "phrase": "Go into the cave" },
{ "go_to": "valley", "phrase": "Go to the valley" }
]
},
"mountains": {
"description": "You are standing at the foot of a mountain range. Huge impassable peaks " \
"loom over you. There is a cave to the east, and a field south of you " \
"leading into a valley.",
"paths": [
{ "go_to": "cave", "phrase": "Go into the cave" },
{ "go_to": "field", "phrase": "Go south into the field" }
]
},
"forest": {
"description": "A giant confused bear mistakes your for one of her cubs and takes you " \
"away with her. Although you eventually learn to love your new bear " \
"family, your adventuring days are over.",
"paths": [ ]
},
"cave": {
"description": "You are in a long dark cave. You see points of daylight at either end of " \
"the cave, one to the northeast and one to the southwest.",
"paths": [
{ "go_to": "mountains", "phrase": "Go northwest" },
{ "go_to": "field", "phrase": "Go southwest" }
]
},
"valley": {
"description": "You are standing in the middle of a huge, beautiful valley. Standing right " \
"before you is ... whatever it was you were looking for. Success!",
"paths": [ ]
}
}
# Function definitions
def menu_input(paths):
prompt = "Make a selection (0 - %i): " % len(paths)
try:
choice = raw_input(prompt)
menu_selection = int(choice)
if menu_selection == 0:
selected_path = "quit"
else:
index = menu_selection - 1
selected_path = paths[ index ]
except (IndexError, ValueError):
print choice, "is not a valid selection!"
# Try again!
selected_path = menu_input(paths)
return selected_path
def select_path(paths):
# Show the menu for this scene.
for i in range(0, len(paths)):
path = paths[i]
menu_item = i + 1
print "\t", menu_item, path["phrase"]
print "\t(0 Quit)"
# Get the user selection from the menu.
next_step = menu_input(paths)
return next_step
def play_game(start_scene):
scene = start_scene
while 1 == 1: # Watch out, could be an infinite loop!
description = scene["description"]
paths = scene["paths"]
print textwrap.fill(description)
next_step = select_path(paths)
if next_step == "quit":
print "Good bye!"
sys.exit()
else:
scene = scenes[ next_step["go_to"] ]
print "You decided to:", next_step["phrase"]
# Game starts here.
play_game(scenes["field"])
We are more or less done with this train of thought. I have introduced you to many topics, but I have taken my own strange path through them. Your next step should be to reexamine the official Python tutorial and see if it makes any more sense than the first time you read it. More ideasNow that you have a complete game, what else can you do? There are many ideas. I may even tackle a few of them in future installments. You don't have to wait for me, though.
A Bonus Diversion: ScopeThis was originally part of the main text, but it didn't really belong anywhere once I had finished writing. I decided to leave it in as one more bout of insane rambling instead of deleting it and probably forever forgetting it. At least this way I have something to start from when I *do* feel like talking about scope. Hey, you may be wondering how I could get away with
using the variable Another thing I would like to point out is that they are each completely different variables from Python's perspective. We use the term scope to describe where a particular variable can be seen. First, if a variable is first defined inside of a function, it is only visible within that function. As soon as the function returns, its "local" variables usually cease to exist. (There are special situations where this is not true, but I am not going to look at them yet. Look up "closure" on the Internet for more information). Variables that are defined outside of a function are visible from the point where they are defined until the end of the file. If you define a value for a local variable with the same name as a global variable, though, the local variable "masks" the global variable until the end of the function. It's all fairly confusing, and easiest to demonstrate with another trip to the Python console. >>> x = "waffle" >>> def foo(): ... print x ... >>> foo() waffle >>> def bar(): ... x = "angry bears!" ... print x ... >>> bar() angry bears! >>> print x waffle >>> Are you feeling a little lost? It's okay, variable scope confuses many developers. The scope rule to remember is that a local definition trumps a global definition. The style rule to remember? Don't use global variables and you only need to remember about local variables and variables handed to a function as part of its arguments. |
![]() |
|
|
Copyright 1999 - 2009 Brian Wisti
|
||