Seminar 4 Hunt the Wumpus, part 1

We will program text adventure computer game Hunt the Wumpus: “In the game, the player moves through a series of connected caves, arranged in a dodecahedron, as they hunt a monster named the Wumpus. The turn-based game has the player trying to avoid fatal bottomless pits and”super bats" that will move them around the cave system; the goal is to fire one of their “crooked arrows” through the caves to kill the Wumpus…"

As before, we will start with a very basic program and will build it step-by-step towards the final version. Don’t forget to download the exercise notebook.

4.1 Lists

So far, we were using variables to store single values: computer’s pick, player’s guess, number of attempts, etc. However, you can store multiple values in a variable using lists. The idea is fairly straightforward, a variable is not a simple box but a box with slots for values numbered from 0 to len(variable)-1.

The list is defined via square brackets <variable> = [<value1>, <value2>, ... <valueN>] and an individual value can be accessed also via square brackets <variable>[<index>] where index goes from 0 to len(<variable>)-1 (len(<object>) function returns number of items in an object, in our case, it would be a list). Thus, if you have five values in the list, the index of the first one is 0 (not 1) and the index of the last one is 4 (not 5)!

Do exercise #1 see how lists are defined and indexed.

You can also get many values from the list via so called slicing when you specify index of many elements via <start>:<stop>. There is a catch though and, as this is a recurrent theme in Python, pay close attention: The index slicing builds goes from start up to but not including stop, in mathematical notation \([start, stop)\). So, if you have a list my_pretty_numbers that holds five values and you want to get values from second (index 1) till fourth (index 3) you need to write the slice as 1:4 (not 1:3!). This including the start but excluding the stop is both fairly counterintuitive (I still have to consciously remind myself about this) and widely used in Python.

Do exercise #2 to build the intuition.

You can also omit either start or stop. In this case, Python will assume that a missing start means 0 (the index of the first element) and missing stop means len(<list>). If you omit both, e.g., my_pretty_numbers[:] it will return all values, as this is equivalent to my_pretty_numbers[0:len(my_pretty_numbers)].1

Do exercise #3.

You can also use negative indexes that are relative to length of the list. Thus, if you want to get the last element of the list, you can say my_pretty_numbers[len(my_pretty_numbers)-1] or just my_pretty_numbers[-1]. The last-but-one element would be my_pretty_numbers[-2], etc. You can use negative indexes for slicing but keep in mind including the start but excluding the stop: my_pretty_numbers[:-1] will return all but last element of the list not the entire list.

Do exercise #4.

The slicing can be extended by specifying a step, so that stop:start:step. This can be combined with omitted and negative indexes. To get every odd element of the list, you write my_pretty_numbers[::2]:

my_pretty_numbers = [1, 2, 3, 4, 5, 6, 7]
my_pretty_numbers[::2]
## [1, 3, 5, 7]

Do exercise #5.

Finally, for those who are familiar with R, the good news is that Python does not allow you to use indexes outside of the range, so trying to get 6th element (index 5) of a five-element-long list will generate a simple and straightforward error (a so-called fail-fast principle). The bad news is that if your slice is larger than the range, it will truncated to the range without an extra warning or an error. So, for a five-element list my_pretty_numbers[:6] will return all numbers of to the maximal possible index (thus, effectively, this is equivalent to my_pretty_numbers[:]). Moreover, if the slice is empty (2:2, cannot include 2, even though it starts from it) or the entire slice is outside of the range, Python will return an empty list, again, neither warning or error is generated.

Do exercise #6.

4.2 Caves

In our game, the player will wander through a systems of caves with cave being connected to three other caves. The cave layout will be CONSTANT, so we will define at the beginning of the program as follows.

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

Let us decipher this. You have a list of twenty elements (caves). Inside each element is a list of connecting caves. This means, that if you are in cave #1 (index 0), it is connected to CAVES[0][1, 4, 5] (note that these numbers inside are zero-based indexes as well!). So, to see what is the index of the second cave connected to the first one you would write CAVES[0][1] (you get first element of the list and, then, the second element of the list from inside).

Do exercise #7 to get comfortable with indexing list of lists.

To allow the player to wander, we need to know where they are to begin with. Let us define a new variable called, simply, player and assign a random integer between 0 and 19 to it, thus putting the player into a random cave. For this, you will need a randomint function from the random library. Look at our previous seminar, if you forgot how to use it.

Our player needs to know where they can go, so on each turn we will need to print out the information about which cave the player is in and about the connecting caves (use string formatting to make this look nice). Let this be our first code snippet for the game. The code should look like this

# import randint function from the random library

# define CAVES (simply copy-paste the definition)

# create `player` variable and set it to a random number between 0 and 19, 
# putting player into a random cave

# print out the list of the connecting caves. Use string formatting.

Put your code into exercise #8.

4.3 Wandering around

Now that the player can “see” where they are, let them wander! Use input() function to ask for the index of the cave the player wants to go to and store the value in a new variable move_to. Remember that input() returns a string, so you will need to explicitly convert it to an integer (see Guess-the-Number game, if you forgot how to do it). Now “move” the player to that by assigning the move_to value to the player. For now, enter only valid numbers, as we will add checks later. To make wandering continuous, put it inside the while loop, so that player wanders until they get to the cave #5 (index 4). We will have more sensible game-over conditions later on but this will allow you to exit the game without interrupting it from outside. The code should look like this (remember to watch your indentations!).

# import randint function from the random library

# define CAVES (simply copy-paste the definition)

# create `player` variable and set it to a random number between 0 and 19, 
# putting player into a random cave

# while player is not in the cave #5 (index 4):
    # print out the list of the connecting caves. Use string formatting.
    # get input about the cave the player want to move to, store it in a variable `move_to`
    # "move" the `player` to the cave they wanted to `move_to`
    
# print a nice game-over message

Put your code into exercise #9.

4.4 Checking whether a value is in the list

Right now we trust the player (well, you) to enter the correct index for the cave. Thus, the program will move a player to a new cave even if you enter an index of the cave that is not connected to the current one. Even worse, it will try to move the player to an undefined cave, if you enter an index larger than 19. To check whether an entered index matches one of the connected cave, you need to use in conditional statement. The idea is straightforward, if the value is in the list, the statement is True, if not, it is False.

x = [1, 2, 3]
print(1 in x)
## True
print(4 in x)
## False

Note that you can check one value/object at a time. Because a list is also a single object, you will be checking whether it is an element of the other list, not whether all or some of it elements are in it.

x = [1, 2, [3, 4]]
# This is False because x has no element [1, 2], only 1, and 2 (separately)
print([1, 2] in x)

# This is True because x has [3, 4] element
## False
print([3, 4] in x)
## True

Do exercise #10.

4.5 Checking valid cave index

Now that you know how to check whether a value is in the list, let’s use it to validate cave index. Before moving the player, you now need to check whether the entered index is in the list of the connected caves. If this is True, you move the player as before. Otherwise, print out an error message, e.g. “Wrong cave index!” without moving a player. Loop ensure that the player will be prompted for the input again.

# import randint function from the random library

# define CAVES (simply copy-paste the definition)

# create `player` variable and set it to a random number between 0 and 19, 
# putting player into a random cave

# while player is not in the cave #5 (index 4):
    # print out the list of the connecting caves. Use string formatting.
    # get input about the cave the player want to move to, store it in a variable `move_to`
    # if `move_to` matches on of the connected caves:
      # "move" the `player` to the cave they wanted to `move_to`
    # else:
      # print out an error message
    
# print a nice game-over message

Put your code into exercise #11.

4.6 Checking that string can be converted to an integer

There is another danger with out input: The player is not guaranteed to enter a valid integer! So far we relied on you to behave but in real life, even when people do not deliberately try to break your program, they will occasionally press the wrong button. Thus, we need to check that the string that they entered can be converted to an integer.

Python string is an object (more on that in a few seminars) with different methods that allow to perform various operations on them. On subset of methods allows you to make a rough check of its content. The one we are interested is str.isdigit() that checks whether all symbols are digits and that the string is not empty (it has at least one symbol). You can follow the link above to check other alternatives such as str.islower(), str.isalpha(), etc.

Do exercise #12.

4.7 Checking valid integer input

Modify the code that gets the input from the user. First, store the raw string (not converted to an integer) into an intermediate variable, e.g. move_to_str. Then, if move_to_str is all digits, convert it to an integer, and do the check that it is a valid connected cave index (moving player or printing an error message). However, if move_to_str is not all digits, only print the error message. This means you need to have an if-statement inside the if-statement. The outline is below, watch you indentations!

# import randint function from the random library

# define CAVES (simply copy-paste the definition)

# create `player` variable and set it to a random number between 0 and len(CAVES)-1, 
# putting player into a random cave

# while player is not in the cave #5 (index 4):
    # print out the list of the connecting caves. Use string formatting.
    # get input into a variable `move_to_str`
    # if move_to_str can be converted to an integer:
        # convert move_to_str to integer and store value in move_to
        # if `move_to` matches one of the connected caves:
            # "move" the `player` to the cave they wanted to `move_to`
        # else:
            # print out an error message
    # else:
        # print error message that user must enter a number
    
# print a nice game-over message

Put your code into exercise #13.

4.8 Wrap up

You now have a player in a system of the caves and they can navigate around. Next time, you will learn how to make your code modular by using functions and the game will have more thrills to it once we add bottomless pits and excitable bats.


  1. Note, that this is almost but not quite the same thing as just writing my_pretty_numbers, the difference is subtle but important and we will look into it later when talking about mutable versus immutable types.↩︎