Chapter 2. Hunt the Wumpus

 

This chapter covers

  • Writing your first real program
  • How programs work
  • Some easy ways to organize programs

 

Now that you have Python set up and installed and know how to enter and run a test program, let’s get started with writing a real one. I’ll begin by explaining a few of Python’s basic features, and then you’ll create a simple text-based adventure game called Hunt the Wumpus.

As you progress through the chapter, you’ll add features to your game, building on the initial version. This is how most programmers (including the author) learned to program: learn just enough about the language to be able to write a simple program, and then build up from there. In order to do that, you need more knowledge—but you only need to learn a little bit more to be able to make small additions to your program. Repeat the process of adding small features a few more times, and you’ll have a program that you couldn’t have created in one sitting. Along the way, you’ll have learned a lot about the programming language.

In this chapter, you’ll experience the early days of programming first hand, as you write your own version of Hunt the Wumpus. The text-based interface is ideal for your first program because you only need to know two simple statements to handle all of your input and output. Because all of your input will be strings, the logic of your program is straightforward and you won’t need to learn a lot to start being productive.

 

A brief history of Hunt the Wumpus

Hunt the Wumpus was a popular early computer game written by Gregory Yob in 1976. It puts you in the shoes of an intrepid explorer, delving into a network of caves in search of the hairy, smelly, mysterious beast known only as the wumpus. Many hazards faced the player, including bats, bottomless pits, and, of course, the wumpus. Because the original game was released with source code, it allowed users to create their own versions of Hunt the Wumpus with different caves and hazards. Ultimately, reinterpretations of Wumpus led to the development of an entire genre of first-person adventure games, such as Adventure and Zork.

 

By the end of this chapter, you’ll know how to add features to your fully functioning version of Hunt the Wumpus, and you’ll even be able to tweak it to create your own version.

Before we get to the cave adventures, let’s figure out the basics.

What’s a program?

As you learned in chapter 1, a program consists of statements that tell the computer how to do something. Programs can execute simple tasks, such as printing a string to the screen, and can be combined to execute complex tasks, like balancing accounts or editing a document.

 

Program

A series of instructions, usually called statements, that tell your computer how to do certain things.

 

The basic mechanics of a program are straightforward: Python starts at the first line and does what it says, then moves to the next and does what that says, and so on. For example, enter this simple Python program:

print "Hello world!"
print "This is the next line of my program"

The code outputs output the following text to the screen:

Hello world!
This is the next line of my program

Python can do many different types of things. So that you can get started on your program as soon as possible, this chapter will give you a brief idea of the statements you can use to tell Python what to do. We won’t go into extensive detail, but you’ll learn everything you need so that you can follow what’s going on.

There’s a lot to take in, so don’t worry too much if you don’t understand it all at once. You can think of programming like this as painting a picture; you’ll begin with a light pencil sketch before you get started properly. Some parts will be hazy at first, but it’s important to get a sense of the whole before you try to make sense of the details.

You might also want to read this chapter at your computer, so that you can experiment with different statements to see what works and try out your own ideas.

We’ll start by investigating that print statement you just tried out.

Writing to the screen

The print statement is used to tell the player what’s happening in your game, such as which cave the player is in or whether there’s a wumpus nearby. You’ve already seen the print statement in the Hello World program, but there are some extra things that it can do, too. You’re not limited to printing out words; pretty much anything in Python can be printed:

print "Hello world!"
print 42
print 3.141592

You can print out lots of things at once by putting a comma between them, like this:

print "Hello", "world!"
print "The answer to life, the universe and everything is", 42
print 3.141592, "is pi!"

But printing statements wouldn’t make for an interactive game. Let’s see how you can add options.

Remembering things with variables

Python also needs some way to know what’s happening. In the Hunt the Wumpus game, for example, Python needs to be able to tell which cave the wumpus is hiding in, so it will know when the player has found the wumpus. In programming, we call this memory data, and it’s stored using a type of object called a variable. Variables have names so they can be referred to later in the program.

To tell Python to set a variable, you choose a name for the variable and then use the equals sign to tell Python what the variable should be. Variables can be letters, numbers, words, or sentences, as well as some other things that we’ll cover later. Here’s how to set a variable:

variable = 42
x = 123.2
abc_123 = "A string!"

In practice, your program can get quite complex, so it helps if you choose a name that tells you what the variable means or how it’s supposed to be used. In the Hunt the Wumpus program, you’ll use variable names like this:

player_name = "Bob"
wumpus_location = 2

 

Note

There are some restrictions on what your variable names can be; they can’t start with a number, have spaces in them, or conflict with some of the names which Python uses for its own purposes. In practice, you won’t run into these limitations if you’re using meaningful names.

 

Table 2.1 gives you an overview of the variable types you’ll be using in your Hunt the Wumpus program.

Table 2.1. Types of variable used in Hunt the Wumpus

Type

Overview

Numbers Whole numbers like 3 or 527, or floating-point numbers like 2.0 or 3.14159. Python won’t switch between them, so you’ll need to be careful in some cases; for instance, 3 / 2 is 1 instead of 1.5. 3.0 / 2 will give the right answer.
Strings A sequence of characters, including a–z, numbers, and punctuation. They can be used for storing words and sentences. Python has a few different ways of representing strings: you can use both single or double quotes— 'foo' or "foo"—as well as special versions with triple quotes that can run over multiple lines.
Lists A collection of other variables, which can include other lists. Lists begin and end with a square bracket, and the items inside are separated with commas: ["foo", "bar", 1, 2, [3, 4, 5]].

Now that you have variables working, how do you get the player involved?

Asking the player what to do

The program also needs some way of asking the player what to do in certain situations. For Hunt the Wumpus, you’ll use the raw_input command. When Python runs that command, it will prompt the player to type something in, and then whatever was typed can be stored in a variable:

player_input = raw_input(">")

Next, you need to figure out what to do with user input.

Making decisions

If that was all there was to programming, it would be kind of boring. All of the interesting stuff happens when the player has to make a choice in the game. Will they pick cave 2 or cave 8? Is the wumpus hiding in there? Will the player be eaten? To tell Python what you want to happen in certain situations, you use the if statement, which takes a condition, such as two variables being equal or a variable being equal to something else, and something to do if the condition is met:

if x == y:
    print "x is equal to y!"
if a_variable > 2:
    print "The variable is greater than 2"
if player_name == "Bob":
    print "Hello Bob!"

You can also use an else command, which tells Python what to do if the condition doesn’t match, like this:

if player_name == "Bob":
    print "Hello Bob!"
else:
    print "Hey! You're not Bob!"

So that Python can tell the body of the if statement from the rest of your program, the lines which are part of it are indented. If you put an if statement within another if statement—usually referred to as nesting—then you need to indent again, for a total of eight spaces. Normally, you’ll use four spaces for each level of indentation.

Some common conditions are listed in table 2.2.

Table 2.2. Common conditions

Condition

Overview

name == "bob" True if the variable name stores the string “bob”. Python uses two equal signs to distinguish it from assignment: name = "bob" means something completely different.
name != "bob" True if the variable name is something other than the string “bob”. != is generally read as “not equals.”
a > 0 True if the variable a stores a number that is greater than 0.
0 <= a <= 10 True if a is a number between 0 and 10, inclusive.
"ab" in "abcde" You can also tell whether a string is part of another string by using in.
not "bob" in "ab" "bob" not in "ab" Python also has the not and not in commands, which reverse the sense of an expression.

Now that you have a handle on decision-making statements, let’s see what you can do to keep the program going.

Loops

One of the great things about computers is not that they can do things, but that they can do things over and over and over and not get bored. Big lists of numbers to add? No problem. Hundreds of lines of files? Ditto. The program only needs to know what it’s going to be repeating and when it should stop. In the Hunt the Wumpus program, you’ll be using a structure called a while loop, which loops as long as a condition that you specify is true, and a break statement, which allows you to control when it stops. Here’s an example:

while True:
    print "What word am I thinking of?"
    answer = raw_input(">")
    if answer == "cheese":
        print "You guessed it!"
        break
    else:
        print "No, not that word..."

We’re almost to the end of the tour of Python’s basic features; our last one is functions.

Functions

There are also a few statements called functions in the Wumpus program. They usually tell you useful things about your program, the player, or the variables, and they look like this:

range(1,21)
len(cave_numbers)

Normally, functions will tell you things by returning a value, which you can store in a different variable or use directly:

cave_numbers = range(1,21)
print "You can see", len(cave_numbers), "caves

Now that we’ve covered some of the basics, let’s see how you can use them to build a simple program. This doesn’t do everything that the original Hunt the Wumpus program did, but for now we want to get something off the ground to see how it all fits together.

 

Incremental programming

In later sections of this chapter, you’ll build on this program by adding features or refining ones that are already there, and tidy up as you go. This is how most programmers tend to work: start simply and build as you go. You can download this program from www.manning.com/hellopython, but I’d suggest following along and typing it in as you read it. That’ll help you remember the individual statements more easily, but you’ll also be establishing a key habit which will help you as you write larger programs—start with a small program and grow from there.

 

Table 2.3 lists the basic features that you’ll learn in this chapter.

Table 2.3. Basic Python features

Feature

Overview

Statements Usually one line in a program (but can be more) that tells Python to do something.
Variables Used to refer to information so that a program can use it later. There are many different types of information that Python can refer to.
if-then-else This is how you tell Python to make a decision. An if statement consists of at least a condition such as x == 2 or some_function() < 42 and something for Python to do if that expression is true. You can also include an else clause, which tells Python what to do if the expression is false.
Loops Used to repeat certain statements multiple times. They can be either while loops, which are based on a condition like an if statement, or for loops, which run once for each element of a list. From within a loop, you can use the continue statement, which jumps to the next iteration of the loop, or a break statement, which breaks out of the loop entirely.
Functions A series of statements that can be run to return a value to a separate part of your program. They can take input if necessary, or they can read (and sometimes write) other variables in your program.
Indenting Because you can nest functions, loops, and if statements within each other, Python uses white space (normally four spaces per level) at the start of a line to tell which statements belong where.
Comments Whenever Python encounters a # character at the start of a line, it will ignore that line and not run it. Additionally, if there’s a # character that’s not inside a string, it will ignore the rest of the line. Comments are used to explain parts of your program, either to other programmers or to yourself in a few weeks—when you’ve forgotten most of the details of what you were doing. You won’t see too many in the book, because we use numbered comments for code listings.

You’ve learned a lot in this section, but in the next section you’ll put this knowledge to good use and write your first program.

Your first program

Now that you have an understanding of the basics of Python, let’s take a look at the program. It’s difficult to see how a program works just by reading about individual features, because, in a working program, they all depend on each other. In this section, we’ll explore the first version of Hunt the Wumpus and solve the first problem that comes up.

 

Note

Experimentation is critical to developing an intuition for how Python works, and how all of the parts fit together. Without it, you’ll be stuck cut and pasting other people’s programs, and when you have a bug, it’ll be impossible to fix.

 

The first version of Hunt the Wumpus

If you don’t understand the next listing right away, don’t worry. A good way to figure out what a program does is to experiment with it—change a few statements, run it again, and see what the differences are. Or, copy a few statements into another file so you can run them in isolation.

Listing 2.1. Your first version of Hunt the Wumpus

Let’s start with the “setup” part of the program . You’re storing a list of numbers in the program, each of which represents a cave. Don’t worry too much about the first line—you’ll learn more about the import statement in chapter 3. The choice function will return one of the caves, picked at random, and you use it to place the wumpus and the player in their starting positions. Note the loop at the end that you use to tell if the player and the wumpus are in the same spot—it wouldn’t be a fun game if the player got eaten right away!

The introductory text tells the player how the game works. You use the len() function to tell how many caves there are. This is useful because you may want to change the number of caves at a later point, and using a function like this means you only have to change things in one place when you define the list of caves.

Your main game loop is where the game starts. When playing the game, the program gives the player details of what the player can see, asks the player to enter a cave, checks to see whether the player has been eaten, and then starts over at the beginning. while loops will loop as long as their condition is true, so while True: means “loop over and over again without stopping” (you’ll handle the stopping part in a minute).

The first if statement tells the player where the player is and prints a warning if the wumpus is only one room away (“I smell a wumpus!”). Note how you’re using the player_location and wumpus_location variables. Because they’re numbers, you can add to and subtract from them. If the player is in cave 3, and the wumpus is in cave 4, then the player_location == wumpus_location - 1 condition will be true, and Python will display the message.

You then ask the player which cave the player wants next . You do some checking to see that the player has put in the right sort of input. It has to be a number, and it has to be one of the caves. Note also that the input will be a string, not a number, so you have to convert it using the int() function. If it doesn’t match what you need, you display a message to the player.

If the input does match a cave number, it will trigger this else clause . It updates the player_location variable with the new value and then checks to see if the player’s location is the same as the wumpus’s. If it is ... “Aargh! You got eaten by a wumpus!” Once the player has been eaten, the game should stop, so you use the break command to stop your main loop. Python has no more statements to execute, and so the game ends.

Debugging

If you’ve typed in listing 2.1 exactly as written and run it, you’ll notice that it doesn’t quite work as planned. In fact, it refuses to run at all. The exact results will depend on your computer’s operating system and how you’re running your Python program, but you should see something similar to what is shown in the following listing. If you don’t, try running your program from the command line by typing python wumpus-1.py.

Listing 2.2. BANG! Your program explodes
Welcome to Hunt the Wumpus!
You can see
Traceback (most recent call last):
  File "wumpus-1.py", line 10, in ?
    print "You can see", len(caves), "caves"
NameError: name 'caves' is not defined

What’s happened is that there’s a bug in the program. There’s a statement in listing 2.1 that Python doesn’t know how to run. Rather than guess what you meant, it will stop and refuse to go any further until you’ve fixed it.

Luckily, the problem is easy to fix: Python tells you what line is at fault and the type of error that’s been triggered, and it provides a rough description of the problem. In this case, it’s line 10, and the error is NameError: name 'caves' is not defined. Oops—the program tried to access the variable caves instead of cave_numbers. If you change line 10 so that it reads

print "You can see", len(cave_numbers), "caves"

then the program should run.

Congratulations—your first real Python program! Next, let’s see what else you can do to improve Hunt the Wumpus.

Experimenting with your program

Experimenting with programs is the most common way that most programmers learn how to deal with new programming problems and find solutions. You, too, can experiment with your new program and see what else you can get it to do. You’re the one typing it in, so the wumpus program is yours. You can make it do whatever you want it to. If you’re feeling brave, try the following ideas.

More (or fewer) caves

You might find 20 caves to be too many—or too few. Luckily, it’s your program now, so you can change the line where you define cave_numbers to be smaller or larger. Question: what happens if you have only one cave?

A nicer wumpus

You haven’t put a bow and arrow into the game yet, so all the player can do is wander aimlessly around the caves until the player bumps into the wumpus and gets eaten. Not a very fun game. How about if you change the line where the player finds the wumpus to read:

print "You got hugged by a wumpus!"

Aww, what a nice wumpus! (The author and publisher disclaim any and all responsibility for dry-cleaning your clothes to get out the wumpus smell should you choose this option.)

More than one wumpus

The wumpus must be awfully lonely down in the caves. How about giving it a friend? This is a bit trickier; but you already have the existing wumpus code to work from. Add a wumpus_friend_location variable, and check that wherever you check the first wumpus_location as shown here.

Listing 2.3. Adding a friend for the wumpus
wumpus_location = choice(cave_numbers)
wumpus_friend_location = choice(cave_numbers)
player_location = choice(cave_numbers)
while (player_location == wumpus_location or
       player_location == wumpus_friend_location):
       player_location = choice(cave_numbers)
...

if (player_location == wumpus_location - 1 or
    player_location == wumpus_location + 1):
    print "I smell a wumpus!"
if (player_location == wumpus_friend_location - 1 or
    player_location == wumpus_friend_location + 1):
    print "I smell an even stinkier wumpus!"
...

    if player_location == wumpus_location:
         print "Aargh! You got eaten by a wumpus!"
         break
     if player_location == wumpus_friend_location:
        print "Aargh! You got eaten by the wumpus' friend!"
         break

Now that’s a more interesting game!

There’s still more you can do to improve the Hunt the Wumpus game, starting with the cave structure.

Making the caves

The first thing that you might have noticed about listing 2.1 is that the “maze of caves” isn’t a maze. It’s more like a corridor, with the caves neatly placed in a line, one after the other. It’s easy to figure out where the Wumpus is—move into the next cave in sequence until you smell it. Because figuring out the location of the wumpus is such an integral part of the game, this is the first thing to fix. While addressing this, you’ll learn a bit more about Python’s lists and forloops.

Lists

Assume for a second that you wanted to write a program to help you do your shopping. The first thing that you’d need is some way to keep track of what you wanted to buy. Python has a built in mechanism for exactly this sort of thing, called a list. You can create and use it like any other variable:

shopping_list = ['Milk', 'Bread', 'Cheese', 'Bow and Arrow']

If you want to find out what’s on your shopping list, you can print it out or you can use an index to find out what’s in a specific place. Lists will keep everything in the order in which you defined it. The only catch is that the index of an array starts at 0, rather than 1:

>>> print shopping_list
['Milk', 'Bread', 'Cheese', 'Bow and Arrow']
>>> print shopping_list[0]
Milk

A clever trick if you need it, is that an index of -1 gets the last item in your array:

>>> print shopping_list[-1]
Bow and Arrow

You can also check whether a particular thing is in your list:

if 'Milk' in shopping_list:
    print "Oh good, you remembered the milk!"

The other cool thing about lists is that they fulfill many purposes. You’re not limited to strings or numbers—you can put anything at all in there, including other lists. If you had lists for two stores (say, the supermarket and Wumpus ‘R’ Us (“for all your Wumpus-hunting needs!”), you could store them in their own lists and then store those lists in one big list:

>>> supermarket_list = ['Milk', 'Bread', 
     'Cheese']
>>> wumpus_r_us_list = ['Bow and Arrow',
     'Lantern', 'Wumpus B Gone']
>>> my_shopping_lists = [supermarket_list,
     wumpus_r_us_list]

You can also put things into a list and take them out again. If you forget to put rope on your list, that’s easily fixed:

>>> wumpus_r_us_list.append('Rope')
>>> print wumpus_r_us_list
['Bow and Arrow', 'Lantern', 'Wumpus B Gone', 'Rope']

You want to catch a Wumpus instead of scaring it away, so perhaps the “Wumpus B Gone” isn’t such a good idea:

>>> wumpus_r_us_list.remove('Wumpus B Gone')
>>> print wumpus_r_us_list
['Bow and Arrow', 'Lantern', 'Rope']

You can also cut out parts of a list if you need to, by giving two values separated with a colon. This is called slicing a list. Python will return another list starting at the first index, up to but not including the second index. Remember that list indexes start at zero:

first_three = wumpus_r_us_list[0:3]

If you give a negative value, then Python will measure from the end instead of the front:

last_three = wumpus_r_us_list[-3:]

Notice that that last example left out the last index. If you leave a value out of a slice like that, Python will use the start or end of the list. These two slices are exactly the same as the previous two:

first_three = wumpus_r_us_list[:3]
last_three = wumpus_r_us_list[1:]

Finally, once you’ve taken everything out of a list, you’ll end up with an empty list, which is represented with two square brackets by themselves: [].

 

Note

One difference between Python and some other programs, such as C, is that Python’s variables aren’t variables in the classic sense. For the most part, they behave as if they are, but they’re more like a label or a pointer to an object in memory. When you issue a command like a = [], Python creates a new list object and makes the a variable point to it. If you then issue a command like b = a, b will point to the same list object, and anything that you do via a will also appear to happen to b.

 

Now that you know about lists, let’s tackle for loops.

For loops

Once you have all of your things in a list, a common way to use the list is to do something to each item in it. The easiest way to do this is to use a type of loop called a forloop. A for loop works by repeating some statements for every item in a list, and assigns that item to a variable so that you can do something with it:

print "Wumpus hunting checklist:"
for each_item in wumpus_r_us_list:
    print each_item
    if each_item == "Lantern":
        print "Don't forget to light your lantern"
        print "once you're down there."

Except for the variable, for loops are much the same as while loops. The break statement which you used in the while loop in listing 2.1 will also work in for loops.

 

Note

This is a common pattern in programming—get a bunch of stuff, and do something to everything in your bunch.

 

Coding your caves

In Hunt the Wumpus, each cave is only supposed to connect to a small number of other caves. For example, cave 1 might only have tunnels to caves 5, 7, and 12, and then cave 5 has tunnels to 10, 14, and 17. This limits the number of caves the player can visit at once, and navigating their way through the cave system to try and find the wumpus becomes the central challenge of the game.

In your first version of Hunt the Wumpus, you were already using a list of cave numbers to tell Python where the wumpus and player were. In your new version, you’ll use a similar sort of list, but changed so that it can tell you which caves can be visited from a particular place. For each cave, you’ll need a list of other caves, so what you’re after is a list of lists. In Python, it looks like this:

caves = [ [2, 3, 7],
          [5, 6, 12],
          ...
        ]

What this tells you is that cave 0 (don’t forget that lists start with their index at 0) links to caves 2, 3, and 7; cave 1 links to caves 5, 6, and 12; and so on. Because the caves are generated randomly, your numbers will be different, but the overall structure will be the same. The number of the cave is the same as its index in the list so that Python can easily find the exits later. Let’s replace section 1 of listing 2.1 with the following listing so that it sets up your new and improved cave system.

Listing 2.4. Setting up your caves
from random import choice

cave_numbers = range(0,20)                 
caves = []                                 
for i in cave_numbers:                     
    caves.append([])                       

for i in cave_numbers:                     
    for j in range(3):                      
        passage_to = choice(cave_numbers)   
        caves[i].append(passage_to)         
print caves                                 

You’re still using a range function to generate the list of caves, but you’ve changed the range so that it starts at 0 instead of 1, to match the indexes of your list. Then you make an empty list for each of the caves that you’re supposed to have. At this point, it’s a list of unconnected caves.

For each unconnected cave in your list, you pick three other caves at random and append them onto this cave’s list of tunnels. To make things easier, you use another for loop inside the first one, so that if you need to change the number of tunnels later, you only need to change the number 3 to whatever you’d like.

When you’re picking a cave to link to, you use a temporary variable to store it. The main advantage of this is that you can use a meaningful name to make the code much easier to read, because you know what that variable does. Note that you could have joined these two lines together by writing caves[i].append(choice(cave_numbers)) instead (using the choice(cave_numbers) function directly), but it’s much harder to read.

So that you can check the program is working properly, you print out the list of caves. This is usually referred to as a debug string, because it’s a handy technique when you’re trying to debug a program. You can remove this line once the program is running properly, because the player shouldn’t know the caves ahead of time.

Now, when you run your program, it should print out a list of caves, like this:

[[8, 7, 14], [1, 18, 4], [4, 8, 15], [6, 6, 0], 
 [5, 3, 6], [15, 9, 10], [2, 13, 5], [17, 18, 3], 
 [4, 8, 15], [18, 17, 2], [1, 9, 15], [11, 4, 16], 
 [16, 10, 6], [2, 10, 5], [13, 4, 6], [8, 14, 11], 
 [16, 4, 10], [3, 12, 17], [18, 18, 0], [2, 8, 5]]

This is exactly what to expect. In this one, cave 0 links to caves 8, 7, and 14; cave 1 links to caves 1, 18, and 4; and so on. Now that you have the list, all that you have to do is alter the rest of your program to use it. Sections 4 and 5 of listing 2.1 should be replaced with the following listing.

Listing 2.5. Altering your program to use the new cave system

You’re only using the cave list to find out which caves the player can enter next, so the changes to the code are pretty straightforward. Instead of checking whether the player’s input is within the list of cave numbers, you check the list for the specific cave you’re in.

There’s a bug in the code you used to set up your caves. You may not believe me, especially if you’ve played a few games already, but there is. Let’s get back into debugging mode.

Fixing a more subtle bug

What makes the bug hard to spot is that the code runs properly, but sometimes the game is impossible to win. In this section, we’ll look at why the game can be unwinnable and how to fix it.

 

Note

These are the worst kind of bugs to hunt down—your program doesn’t crash or spit out any obvious errors, but it’s definitely wrong.

 

We’ll start by examining how the caves are linked.

The problem

The trick is that all the cave tunnels are generated randomly, so they can be linked in any possible way. Let’s think about an easier case, with a small cave system. Suppose the tunnels happened to link like they do in figure 2.1.

Figure 2.1. This isn’t a very fun game.

The player wouldn’t ever be able to catch the wumpus.

With lots of caves, it’s less likely that you’ll strand the player in an isolated corner of the map; but, ideally, you’d like the program to be as bulletproof as you can make it, so that it’s impossible, rather than unlikely.

The solution

You need to make two changes to the map generation to solve the problem. The first is to make the tunnels two-way. If you can go from cave 1 to cave 2, then you should be able to move back from cave 2 to cave 1.

The second is to make sure that every cave is linked together and that there are no isolated caves (or networks of caves). This is called a connected structure. That way, no matter how you join up the rest of the passages, you can be sure players can reach every cave, because players can go back the way they came and choose a different passage. If players forget which way they came then they can still get lost, but that’s their fault rather than yours.

Now, how do you use Python to link tunnels?

Coding connected caves

Connecting caves is straightforward—when you create a one-way tunnel, you add another one way tunnel back the way you came. Every time you say caves[a].append[b], you also say caves[b].append[a]. The program looks something like the following listing.

Listing 2.6. Creating a linked cave network

First, create a list of caves that you haven’t visited, and visit cave 0 . You loop until unvisited_caves is empty ; that is, there are no unvisited caves left. You pick one that has fewer than three tunnels to other caves . If you link 1 cave to 10 others, the game will be too hard, because it will be difficult or impossible to work out which tunnel leads to the wumpus.

is where you’re building the cave. You pick a random unvisited cave, put a tunnel in the old cave to the new one, and then link from the new one back to the old one. This way you know that players can find their way back. In figure 2.2, you’re adding cave 3 to your structure— it will get linked to one of either cave 0, 1, or 2.

Figure 2.2. Adding cave 3 to your network

Once you’re done with the cave, you can move it from the unvisited list to the visited list . Steps , , and get repeated until you run out of caves (unvisited caves == []). Your cave structure will start to look like figure 2.3.

Figure 2.3. That’s much better!

The progress report lines are optional, but if you include them you’ll be able to see your caves in the process of being built, because every time Python goes through the loop it will print out the current cave structure. It also looks a bit nicer than print caves.

Now that all the caves are linked, the rest of the job requires adding some more one-way tunnels . It’s exactly the same as the previous example, except that you’ll already have at least one tunnel in each cave. So that you don’t add more than three tunnels, you change your for loop into a while loop.

With your cave problem solved, let’s see how functions can improve the readability of your code.

Clean up your code with functions!

If you’ve been following along with the examples (you should!), you’ll notice that your program is growing longer and longer. It’s a relatively short example, but, even so, it’s becoming hard to understand what’s happening in the program. If you wanted to give a copy of your program to a friend for them to use, they might have a hard time figuring out what all the pieces do.

 

Note

Remember how we were talking about hiding complexity in chapter 1? Functions are one of the critical ways that Python can hide the complex parts of your program.

 

It’s time for a spring-cleaning, and you’re going to do that by designing your program to use some functions. You’ve been using a few functions so far; they’re the choice(), len(), raw_input() parts of your code—so you have a rough idea of how they work. What you don’t know (yet) is what they really are or how to create your own.

Function basics

Functions are a way of making a section of your program self contained, often referred to as encapsulation. It’s an important way of breaking down a program into easily understood parts. A good rule of thumb is that each function “should do one thing and do it well.” There should be as little overlap between your functions as possible. This is similar to the way the parts of a car engine work; if a fan belt breaks, you should replace the fan belt—it wouldn’t make much sense to have to change your tires or spark plugs as well.

There are several advantages to using functions in your program:

  • You only have to write that part of the program once, and then you can use it wherever you like. Later, if you don’t like the way your program works or you find a bug, you only have to change your code in one place.
  • In much the same way you can choose nice variable names that tell you what’s going on in your program, you can also choose nice function names that describe what the function does.
  • One of the reasons your code is hard to understand now is that it’s all in one big piece and it’s difficult to tell where parts begin and end. If it were broken into smaller parts, with a part for setting up the caves, a part for making a tunnel, a part for moving the player, and so on, you would only need to read (and understand) one small piece of the program instead of a large chunk.

Functions are one of the main units of encapsulation in Python. Even advanced structures such as classes, which we cover in chapter 6, are composed of functions. Python also has what are called first-class functions, which means that you can assign functions to variables and pass them to other functions. You’ll learn more about how to use functions like this in chapter 7.

Functions have input and output, which you’ve seen already—when you use a function, you send it some data and then get back some more data as an answer. Some functions will do things themselves, but other functions will return a value after performing some calculations. Here’s a simple function that will add two numbers together:

def add_two_numbers(a, b):                     
    """ This function adds two numbers """     
    return a + b                               

Let’s look at the initial line of the function declaration. It starts with the reserved word def, followed by a name for your function, and then the parameters that the function will expect within brackets. When you call the function later in your program, you specify what these parameters are—they can be explicit values or variables.

The second line is called a docstring, and it’s another useful way of making your programs easier to read when combined with good variable and function names. It should be a short description of the function and what it does—anything that someone might need to know in order to use the function properly. You’ve also used a special version of a Python string with three quotes, so that you can extend the docstring over more than one line if you need to.

The third line is where the function does its work. In this case it’s easy—add a and b together. The return statement tells Python that the function has finished and to send the result of a + b back to whoever called it.

Variable scope

Python places some limits on functions so they can only affect a small part of your program, normally the function itself. Most variables that are set inside your functions are known as local variables, and you won’t be able to use them outside of the function:

def create_a():
    a = 42

create_a()
print a

When you try and run this program, you’ll get an error like this one:

Traceback (most recent call last):
  File "<stdin>", line 5, in test.py
NameError: name 'a' is not defined

What happened? You set the a variable inside the create_a() function, didn’t you? Actually, it was only created inside the function. You can think of it as “belonging” to create_a. As soon as Python has finished with a variable, it gets thrown away—in this case, as soon as the function exits.

Additionally, you won’t be able to change most variables that have been defined outside the function. Instead, when you create a variable, you’ll be creating a new one. The following code won’t work:

a = 42
def add_to_a(b):
    a = a + b
add_to_a(42)

Unless you tell it otherwise, Python assumes that the a variable is supposed to be within the add_one_to_a function. Trying to access a variable inside of a function produces an error like this:

Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "<stdin>", line 2, in add_to_a
UnboundLocalError: local variable 'a' referenced before assignment 

The rule of thumb to remember is that the variables used in functions and the variables used in the rest of your program are different. Within a function, you should only use the variables that are passed into it as parameters, and, once back in the main part of your program, you should only use the variables that are returned from the function.

But, like most rules of thumb, there are exceptions. In your program, you’re making one exception when you’re modifying the list of caves. In Python, the lists of caves and cave networks are a special type of variable called an object, and behind the scenes you’re sending messages to these objects instead of modifying them directly. You’ll learn more about how that works in chapter 6. But, for now, think of lists as being a special exception to the rule that you can’t modify external variables.

Shared state

When functions (or objects) work on a single copy of something, it’s referred to as shared state. You can use shared state by making functions work on a list of caves, but, generally, shared state is a bad thing to have in your programs. If you have a bug in one of your functions, Python may corrupt your data (perhaps truncate it, or replace it with something odd). You won’t notice this until a completely separate part of your program tries to read the garbled data and displays odd results. When that happens, your program will become much harder to fix, depending on the number of functions that access your shared state.

 

Note

Shared data is a double-edged sword. You need to have some, but it’s also a source of bugs—particularly if a lot of functions share the data.

 

In chapter 6, you’ll learn how to limit the number of functions that have access to shared state by using another Python structure called a class. For now, though, you’ll have to be careful; you’ll only modify your caves when you set them up, and you’ll leave them alone once you’re playing the game.

 

Data and operations on data

Most programs can be thought of as a collection of information or data that also features rules about ways to interact with that data. The Hunt the Wumpus program is no exception. You have a cave structure and locations for the wumpus and the player, functions that make changes to that data, and then a main program that ties it all together using the functions.

Designing your programs this way makes them much easier to write and debug and gives you more opportunities to reuse your code than if you had thrown everything into one big program or function.

If you have a data structure that fits everything your program needs and makes it easy to retrieve the data you need, that’s usually half the battle when it comes to writing your program.

 

Now that you know what functions are and why you’d want to use them, let’s go ahead and see how to break up your wumpus game into individual functions.

Fixing the wumpus

In principle, encapsulating a program into functions isn’t too hard: look for parts of your program that fit some of the following criteria, and try to pull them out into functions where they

  • Do one particular thing (self contained)
  • Are repeated several times
  • Are hard to understand

When considering the Hunt the Wumpus game, you should be able to see that it has three main sections. You’ll start with the simplest functions first and then use them to build the rest of your program.

Interacting with the caves

When dealing with cave-related tasks, there are several simple actions that you perform quite often:

  • Create a tunnel from one cave to another.
  • Mark a cave as visited.
  • Pick a cave at random, preferably one that is ok to dig a tunnel to.

To make your life easier when working with the list of caves, you can create what are known as convenience functions. These are functions that perform a (potentially complicated) series of actions but hide that complexity when you’re using the function in your program. The benefit is that you can perform the actions in one step in your main program, and you don’t have to worry about the details once you’ve created the function. That makes your program easier to understand and helps to reduce bugs in your programs. The next listing introduces some convenience functions that you can use to make Hunt the Wumpus clearer and more comprehensible.

Listing 2.7. Adding convenience functions

Creating tunnels and visiting caves are both obvious candidates for functions . It’s easy to make an error by using the wrong variable to refer to a cave, and using code like create_tunnel(cave1, cave2) makes your program much easier to read.

In the choose_cave function , you can hide even more detail. When you choose a cave, you’re normally only interested in caves that have fewer than three tunnels. Adding that check into the function will remove a lot of duplicated code from your main program. Note also that choose_cave accepts a list of caves as input so you can use it to pick a cave from either the visited or unvisited cave list.

It’s not only the “final” versions of your code that can have convenience functions. You can also create convenience functions to help you while programming. If you want to debug your code at a later point, a function to print all of your caves comes in handy..

Next let’s turn our attention to how to create your caves.

Creating the caves

We’ve already talked about the data that a program uses. One good rule of thumb is to create functions that do particular things to your data or that tell you about your data, and then use only those functions to “talk” to your data. In programming terminology, this is normally referred to as an interface. With an interface to guide you, it’s much harder to make a mistake or get confused about what the data means. To some extent, you’ve already started that process.

In Hunt the Wumpus, there are three tasks that you need to perform when creating caves that are ideal candidates for functions:

  • Set up the cave list.
  • Make sure all of the caves are linked.
  • Make sure there are three tunnels per cave.

In listing 2.8, three functions do exactly that. These functions are the essential core of your program, so it will pay off to try to get them right. There are no hard and fast rules, but some signs that your program is well written include the following:

  • It’s easy to read and understand.
  • It’s easy to find and fix bugs.
  • You only have to change limited parts of your program when you add new features.
  • You can reuse some of your functions when modifying the program.

Ultimately, though, what “right” means will vary from program to program depending on the design and what that design is trying to achieve.

Listing 2.8. Cave-creation functions

Creating the list of caves hasn’t changed much from the previous listing, but it’s still a good idea to put well-defined sections of code in their own functions for readability.

All the hard work of connecting the caves and tunneling is done in link_caves . Did you notice how the convenience functions that you defined in the previous listing help to tidy things up even further? Even if you didn’t know what this function was doing, it’d be pretty easy to guess.

With finish_caves, you haven’t created a convenience function . It’s the only section of code where you create a one-way tunnel, so the benefit is a bit more limited than in the other cases. Whether you create a function in cases like this might depend on whether you were planning on adding more functionality later. Decisions like this can be something of a stylistic issue, so pick the option that feels best for you. You can always change it later if you need to repeat some code.

Finally, let’s bring functions to how Hunt the Wumpus interacts with the player.

Interacting with the player

When running the program, there are two tasks that you perform regularly to find out what the player wants to do next:

  • Tell the player about where they are.
  • Get some input from the player.

Because the appearance of a program is likely to change substantially, either due to the feedback of the people using it or from adding new features, it often makes sense to keep the interface separated from the rest of the program and interact with the player through well-defined mechanisms. The next listing defines two functions you’ll use for these two tasks in your user interface.

Listing 2.9. Player-interaction functions

Here’s the mechanism that I was talking about. It doesn’t matter what the player enters; this function will always return either a special value of None (Python’s version of null) if the input wasn’t right, or the number of the cave that the player wants to enter. You can check this easily in the main part of your program.

The rest of the program

Once you have all of these functions, it doesn’t leave much of your program that isn’t a function. But this is a good thing, as you’ll see shortly.

Listing 2.10 shows the final installment of the updated Hunt the Wumpus game. It behaves exactly the same way as the program in listing 2.6 as far as the player is concerned, but the structure has completely changed. All of your tasks are now stored within functions, and the main program uses those functions to do everything in the game—display the current cave, get input, move the player, and so on.

Listing 2.10. The refactored wumpus game
from random import choice

...function definitions...

cave_numbers = range(0,20)
unvisited_caves = range(0,20)
visited_caves = []
caves = setup_caves(cave_numbers)

visit_cave(0)
print_caves()
link_caves()
print_caves()
finish_caves()

wumpus_location = choice(cave_numbers)
player_location = choice(cave_numbers)
while player_location == wumpus_location:
     player_location = choice(cave_numbers)
    
while True:
    print_location(player_location)
    new_location = get_next_location()
    if new_location isn't None:
        player_location = new_location
    if player_location == wumpus_location:
        print "Aargh! You got eaten by a wumpus!"
        break

Notice how short and easy to follow the main part of the program is now. It’s only 20 lines, and, because you’ve chosen useful function names, you could probably figure out what it does even if you didn’t know anything about Python. That’s the ideal that you should be aiming for. Clear, easy-to-understand code will save you a lot of time when reading and modifying it later on.

 

Simplify

You’ve seen how you refined and simplified the program as you went along, including going back and changing parts completely when necessary. If you can simplify your code, there’s normally no reason not to. The simpler a program is, the easier it is to write, understand, debug, and modify. The refining process is typically along the lines that you’ve seen so far in this chapter:

  • Use meaningful names for both variables and functions.
  • Use white space to separate different sections of program.
  • Store values in intermediate variables.
  • Break up functions so that they do one thing well.
  • Limit the amount of shared state that functions use, and be clear about what that shared state is.

Perfection is achieved not when there is nothing left to add, but when there is nothing left to take away.

Antoine de Saint-Exupéry

 

Caves ... check. Wumpus ... check. Running around in the caves ... check. A way to win the game... Hmm. There’s no way to win the game. Better do something about that.

Bows and arrows

In the traditional wumpus game, you had a bow and one arrow, and when you thought that you knew which cave the wumpus was in, you could choose to fire an arrow into that cave. If you guessed wrong, too bad!

 

Note

One of the golden rules of game design is that the player has to be able to enjoy your game. Without a bow and arrow, you can still explore and have fun, but firing the bow and arrow is how you find out whether your exploration and understanding of the cave system is correct.

 

It should be easy to see how to add this sort of feature by now, because it’s similar in style to the get_next_location() function. You’ll add a total of three more functions:

  • Ask whether the player wants to move or shoot.
  • Find out where to move.
  • Find out where to fire an arrow.

You’ll also modify the get_next_location() function into a general function ask_for_cave(). That’s what it is already, and you can call it from both your movement and firing functions. By writing it this way, your two input functions will be short, which helps keep your program manageable. If you add another feature later that needs to ask for a cave, then you’ll already have a useful function to call on, which makes programming easier and faster.

Listing 2.11. Adding arrows

You don’t need to make too many changes to your earlier get_next_location function; you just need a name change to make its intention clear and some cosmetic changes to how the program asks for input . The fact that you don’t need to make extensive changes is normally a good sign that a function is designed properly. If you had to significantly modify your function, it could be a sign that the original was trying to do too much at once.

The function get_action() is similar to the ask_for_cave() function, except that the valid input differs. Hmm ... perhaps there’s the possibility that you can create a clearer function, one that both of these can call. In chapter 6, you’ll learn about a good way to do that.

It’s not just input that can be made into its own function. Actions within the game can be functions too . Perhaps actions is too strong a word—notice how the action functions don’t do anything (that is, set any variables); they only return what should happen, and then the main program takes action based on what the functions tell it to do.

The main part of your program is still as clear as it was previously , even though you’ve added a major new piece of functionality. If it’s much more complicated, that’s usually a sign that you might need to create a new function for some parts of your program and simplify the core of what you’re doing.

More atmosphere

Congratulations! You now have a fully functional Hunt the Wumpus program, which you can play over and over again and use to impress your friends. Well, sort of. It works, but a number for each cave isn’t atmospheric or impressive. It makes your program easier to think about, but it needs that extra bit of polish. How about changing the program so that instead of numbers, it uses descriptive names for each cave?

 

Note

The core game mechanics are what make Hunt the Wumpus fun, but the final bits of polish like this are what distinguish good games from great games.

 

One way to do that is to reference a list of cave names stored in your program based on the cave number. Instead of displaying the raw cave number, display cave_names[cave_number]. When you ask the player for a cave, they should instead pick a number from 1 to 3, with the name of the cave after the number. You’re aiming for something similar to what’s shown in the following listing.

Listing 2.12. An interface for Hunt the Wumpus
Black pit
From here, you can see:
    1 - Winding steps
    2 - Old firepit
    3 - Icy underground river
I smell a wumpus!

What do you do next?
   m) move
   a) fire an arrow
> 

The list of cave names is relatively easy. You can borrow mine or create your own. Notice that, in the following listing of cave names, you can break a list over multiple lines at the commas between items. This is to make the program easier to read and modify.

Listing 2.13. A list of cave names
cave_names = [
    "Arched cavern",
    "Twisty passages",
    "Dripping cave",
    "Dusty crawlspace",
    "Underground lake",
    "Black pit",
    "Fallen cave",
    "Shallow pool",
    "Icy underground river",
    "Sandy hollow",
    "Old firepit",
    "Tree root cave",
    "Narrow ledge",
    "Winding steps",
    "Echoing chamber",
    "Musty cave",
    "Gloomy cave",
    "Low ceilinged cave",
    "Wumpus lair",
    "Spooky Chasm",
]

The only other changes that you need to make are to what you’re displaying, and what input you’ll accept, as shown in the following listing.

Listing 2.14. Hunt the Wumpus—now with 40% more atmosphere!

Here’s where you print out the current caves and the list of caves the player can see . They’re all using the printable cave name from your list of cave names, rather than the number. Instead of printing the cave list, you’re using a for loop, with tunnel as an index into the list of tunnels. You’re also adding one to it to get 1, 2, or 3 rather than the 0, 1, or 2 indexes, to make it extra friendly.

Now that you know there are only three valid choices, you can check directly for those rather than needing the user to enter the number of the cave. You’re also subtracting one from the result, because you need 0, 1, or 2 for your list index, rather than 1, 2, or 3.

Even though you’re using 1, 2, and 3 as choices, you still return the cave number as an index. All of your changes are contained within the print_location and ask_for_cave functions and use the interface that we talked about earlier, so nothing else in your program needs to be changed at all .

Where to from here?

You don’t have to stop with the program as listed. There are a number of features you can add, including some that were in the original version of Hunt the Wumpus. Feel free to invent your own—this is your program now, and you can make it do whatever you like.

Bats and pits

In the original Hunt the Wumpus, there were other hazards: bats, which carried the player to another cave, and pits, which worked in a similar way to the wumpus (“I feel a draft!”).

Making the wumpus move

One wumpus variant made the wumpus move to a different, random cave if the player missed with their arrow—instead of causing the player to lose the game.

Different cave structures

The original Hunt the Wumpus had a static cave structure, in which the caves were vertices of a dodecahedron. You don’t necessarily have to follow this format, but experimenting with different cave structures could make for a more fun game. For example, perhaps you don’t like one-way tunnels; that should be easy to fix. Also, in the current version, caves can tunnel to themselves. I happen to like that sort of layout, but you may not. Being able to write your own programs means that you’re not stuck with my design choices; you’re free to make your own.

Summary

This chapter covered a lot of material. Not only did you learn the basics of Python and how to fit them together to make a program, but we also covered possible ways to design your programs and took a look at why certain design choices might be better than others.

The best way to start writing a program is to choose something simple that either does part of what you need or describes the core of your program; then, build it from there. In Hunt the Wumpus, the first step was to create the initial game loop of choosing a cave and allowing the player to move to a different one. From there, you were able to develop a proper cave system; after making sure that the caves were connected properly, your program became a fully fledged game that can be played and won (or lost).

The best way to continue to develop your program is to refine it as you go, by breaking commonly used parts into functions and trying to develop an interface between different sections of your program. Because it’s easy to lose track of the overall structure in low-level details, such as adding items to lists or making sure that caves have three tunnels, a large part of your interfaces will often entail hiding superfluous details or making sections of your program easier to work with.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.247.196