Stage 2 - Movement

Introduction

In Stage 1 you made three rooms, connected them, and got the program to describe each one. That was a good start, but it’s not much of a game yet. In Stage 2 you’ll write code that lets the player move between rooms, which means changing the game’s state (for example, which room you are in), and you’ll build the main loop.

State machines

A state machine is a way of thinking about how a program changes as things happen. At any moment, the program is in one state (like being in a certain room in your game). When an event happens, such as the player typing a command, the program follows a rule that decides what the next state should be.

For example, typing “east” might move you from the Armoury to the Lab. Each state has certain things you can do, and each action can move you to a new state. It’s like following a map where every choice leads to a different place, and the program always knows exactly where it is and what it should do next.

The main loop is a key part of event-driven programming. Your main.py file will set up the game and create all the objects it needs. Then it will enter the main loop, where the program waits for the player to type something and then reacts to that input.

Event-driven programming

Event-driven programming is when a program doesn’t just run straight from top to bottom, but instead waits for things to happen and reacts to them. These things are called events, like the user typing a command, clicking a button, or a sensor sending data.

The program sits in a loop, listening for these events, and when one occurs, it runs the code that matches that event. This makes programs more flexible because they only do something when there’s a reason to, just like you don’t answer someone until they speak to you first.

To achieve this we will need to complete the following steps:

Pseudocode

  • Create the move method

  • Initialize the starting room

  • Create the main loop which:

    • describes current room

    • accepts user input

    • responds to user input

Class Diagram

We have updated the Room class diagram to reflect the Stage 2 work.

lesson 2 class diagram

Notice we have a new method move(direction):room

  • accepts one argument (direction)

  • returns a Room object

Create the move method

Open the room.py file and add the code highlighted below:

 1# room.py
 2
 3class Room():
 4    
 5    def __init__(self,room_name):
 6        # initialises the room object
 7        self.name = room_name.lower()
 8        self.description = None
 9        self.linked_rooms = {}
10        
11    def describe(self):
12        # sends a description of the room to the terminal
13        print(f"\nYou are in the {self.name}")
14        print(self.description)
15        for direction in self.linked_rooms.keys():
16            print(f"To the {direction} is the {self.linked_rooms[direction].name}")
17    
18    def link_rooms(self, room_to_link, direction):
19        # links the provided room, in the provided direction
20        self.linked_rooms[direction.lower()] = room_to_link
21        
22    def move(self, direction):
23        # returns the room linked in the given direction
24        if direction in self.linked_rooms.keys():
25            return self.linked_rooms[direction]
26        else:
27            print("You can't go that way")
28            return self

We need to create the main loop before we call this code, but let’s investigate our new code anyway.

Code Explaination

  • def move(self, direction): → defines the move function and takes one input: the direction the player wants to go.

  • # returns the room linked in the given direction → a comment explaining what the function does.

  • if direction in self.linked_rooms.keys(): → checks whether the direction the player typed is actually one of the directions this room allows.

    • self.linked_rooms.keys() → gets all the possible directions you can go from this room.

    • if direction in → checks if the player’s direction is one of those options.

  • return self.linked_rooms[direction] → returns the room in that direction if it’s valid.

    • self.linked_rooms[direction] → retrieves the room object linked to that direction from the dictionary.

  • else: → runs if the direction isn’t allowed.

    • print("You can't go that way") → tells the player they tried an invalid direction.

    • return self → keeps the player in the same room because the move didn’t work.

Initialize starting room

Now go to the main.py file and make the highlighted changes below

 1# main.py
 2
 3from room import Room
 4
 5# create rooms
 6cavern = Room("Cavern")
 7cavern.description = ("A room so big that the light of your torch doesn’t reach the walls.")
 8
 9armoury = Room("Armoury")
10armoury.description = ("The walls are lined with racks that once held weapons and armour.")
11
12lab = Room("Laboratory")
13lab.description = ("A strange odour hangs in a room filled with unknownable contraptions.")
14
15# link rooms
16cavern.link_rooms(armoury,"south")
17armoury.link_rooms(cavern,"north")
18armoury.link_rooms(lab,"east")
19lab.link_rooms(armoury,"west")
20
21'''
22# describe the rooms
23cavern.describe()
24armoury.describe()
25lab.describe()
26'''
27
28# initialise variables
29current_room = cavern

Let’s investigate the new code

Code Explaination

  • The ''' on lines 21 and 26 → turns the room descriptions into a big comment, so Python ignores that code.

    • You could delete it, but leaving it commented out means you can bring it back later if you need it for debugging.

  • # initialise variables → a comment to explain what the next lines of code are doing.

  • current_room = cavern

    • makes a variable that tracks which room the player is currently in.

    • starts the player in the cavern room.

Create main loop

Still working in the main.py file, we will now make the main loop.

Add the highlighted code below so you can see the main loop for the first time.

 1# main.py
 2
 3from room import Room
 4
 5# create rooms
 6cavern = Room("Cavern")
 7cavern.description = ("A room so big that the light of your torch doesn’t reach the walls.")
 8
 9armoury = Room("Armoury")
10armoury.description = ("The walls are lined with racks that once held weapons and armour.")
11
12lab = Room("Laboratory")
13lab.description = ("A strange odour hangs in a room filled with unknownable contraptions.")
14
15# link rooms
16cavern.link_rooms(armoury,"south")
17armoury.link_rooms(cavern,"north")
18armoury.link_rooms(lab,"east")
19lab.link_rooms(armoury,"west")
20
21'''
22# describe the rooms
23cavern.describe()
24armoury.describe()
25lab.describe()
26'''
27
28# initialise variables
29current_room = cavern
30running = True
31
32# ----- MAIN LOOP -----
33while running:
34    current_room.describe()
35    
36    command = input("> ").lower()

Finally we can run our code, but don’t forget PRIMM. Predict you think the program will do, then run the program.

Escaping an infinite loop

If your Python program gets stuck in an infinite loop, you can stop it by pressing Ctrl + C on Windows or Control + C on a Mac.

If you’re using Thonny, you can also click the stop button to end the program.

Let’s investigate the new code line-by-line.

Code Explaination

  • running = True → used to keep the main loop going until the player decides to quit.

    • This is called a flag variable—it starts as True, and when the player wants to exit, it gets changed to False.

  • # ----- MAIN LOOP ----- → a comment showing where the main loop begins.

  • while running: → starts the main loop.

    • The loop keeps repeating as long as running is True.

  • current_room.describe() → runs the describe function for whatever room is stored in the current_room variable.

    • At the start, this is the cavern.

  • command = input("> ").lower() → reads what the player types.

    • input("> ") → shows "> " on the screen and waits for the player to type something.

    • .lower() → turns the input into lowercase so the program can read it more easily.

    • command = → stores the final text in the command variable.

Notice that no matter what the player types, the same thing keeps happening. That’s because we’ve built the main loop, which is waiting for events (the player’s input), but we haven’t written any code to react to those events yet.

In a state machine, the game should change state when something happens—like moving to a new room—but right now there are no rules telling the program how to change state when an event occurs. So the loop just repeats without doing anything new.

Responding to commands

In Event Driven Programming the entering of user’s commands is called an event. Now we have to create code that responds to those events. This kind of code is called an event handler.

Back in our main.py we’re going to create an event handler to deal with the entry of a direction ("north", "south", "east" or "west"). Add the highlighted code.

 1# main.py
 2
 3from room import Room
 4
 5# create rooms
 6cavern = Room("Cavern")
 7cavern.description = ("A room so big that the light of your torch doesn’t reach the walls.")
 8
 9armoury = Room("Armoury")
10armoury.description = ("The walls are lined with racks that once held weapons and armour.")
11
12lab = Room("Laboratory")
13lab.description = ("A strange odour hangs in a room filled with unknownable contraptions.")
14
15# link rooms
16cavern.link_rooms(armoury,"south")
17armoury.link_rooms(cavern,"north")
18armoury.link_rooms(lab,"east")
19lab.link_rooms(armoury,"west")
20
21'''
22# describe the rooms
23cavern.describe()
24armoury.describe()
25lab.describe()
26'''
27
28# initialise variables
29running = True
30current_room = cavern
31
32# ----- MAIN LOOP -----
33while running:
34    current_room.describe()
35    
36    command = input("> ").lower()
37    
38    if command in ["north", "south", "east", "west"]:
39        current_room = current_room.move(command)

Predict you think the program will do, then run the program.

Let’s investigate that code.

Code Explaination

  • if command in ["north", "south", "east", "west"]: → runs this block only if the player typed a direction.

    • ["north", "south", "east", "west"] → the list of directions the game will accept.

    • if command in → checks whether what the player typed is in that list.

  • current_room = current_room.move(command) → works out which room to go to next.

    • current_room.move(command) → calls the move function, using the player’s direction to find the next room.

    • current_room = → updates current_room so the game’s state now matches the new room.

Testing

Testing branching code

Whenever you test branching code, it is important to ensure you methodically test all possible branches.

To do this:

  • create a table which lists every possible branch

  • for each branch, list the expected results

  • record the actual results

  • idenfiy any discrepancies

Now that we can move between all our rooms, we can test that our code is working correctly. Draw up a table to test each option. Below is an example of my table.

Current Room

Command

Expected Result

Actual Result

cavern

north

“You can’t go that way”

“You can’t go that way”

cavern

south

moved to armoury

moved to armoury

cavern

east

“You can’t go that way”

“You can’t go that way”

cavern

west

“You can’t go that way”

“You can’t go that way”

armoury

north

moved to cavern

moved to cavern

armoury

south

“You can’t go that way”

“You can’t go that way”

armoury

east

moved to lab

moved to lab

armoury

west

“You can’t go that way”

“You can’t go that way”

lab

north

“You can’t go that way”

“You can’t go that way”

lab

south

“You can’t go that way”

“You can’t go that way”

lab

east

“You can’t go that way”

“You can’t go that way”

lab

west

moved to armoury

moved to armoury

Notice that I tested each of the four directions in each of the three rooms in my dungeon.

Exiting

Although the user can now move around our dungeon, they cannot exit the game. Now we need to make an event handler to deal with the user wanting to quit the game.

 1# main.py
 2
 3from room import Room
 4
 5# create rooms
 6cavern = Room("Cavern")
 7cavern.description = ("A room so big that the light of your torch doesn’t reach the walls.")
 8
 9armoury = Room("Armoury")
10armoury.description = ("The walls are lined with racks that once held weapons and armour.")
11
12lab = Room("Laboratory")
13lab.description = ("A strange odour hangs in a room filled with unknownable contraptions.")
14
15# link rooms
16cavern.link_rooms(armoury,"south")
17armoury.link_rooms(cavern,"north")
18armoury.link_rooms(lab,"east")
19lab.link_rooms(armoury,"west")
20
21'''
22# describe the rooms
23cavern.describe()
24armoury.describe()
25lab.describe()
26'''
27
28# initialise variables
29running = True
30current_room = cavern
31
32# ----- MAIN LOOP -----
33while running:
34    current_room.describe()
35    
36    command = input("> ").lower()
37    
38    if command in ["north", "south", "east", "west"]:
39        current_room = current_room.move(command)
40    elif command == "quit":
41        running = False

Predict you think the program will do, then run the program.

Make sure you test the quit option

Let’s investigate that code

Code Explaination

  • elif command == "quit": → if the command is not an acceptable direction, then check if it is quit

  • running = False change our flag variable to False

    • this means that when the loops returns to the top, where running will be False and the loop will exit.

Capture incorrect commands

The code now understands the movement commands and the quit command, but what if the player types something completely different? The loop just keeps going and shows the same room again. That’s not very helpful.

We should tell the player when their command doesn’t make sense, so they know they need to try something else. Let’s fix that.

Change main.py to include the highlighted code below.

 1# main.py
 2
 3from room import Room
 4
 5# create rooms
 6cavern = Room("Cavern")
 7cavern.description = ("A room so big that the light of your torch doesn’t reach the walls.")
 8
 9armoury = Room("Armoury")
10armoury.description = ("The walls are lined with racks that once held weapons and armour.")
11
12lab = Room("Laboratory")
13lab.description = ("A strange odour hangs in a room filled with unknownable contraptions.")
14
15# link rooms
16cavern.link_rooms(armoury,"south")
17armoury.link_rooms(cavern,"north")
18armoury.link_rooms(lab,"east")
19lab.link_rooms(armoury,"west")
20
21'''
22# describe the rooms
23cavern.describe()
24armoury.describe()
25lab.describe()
26'''
27
28# initialise variables
29running = True
30current_room = cavern
31
32# ----- MAIN LOOP -----
33while running:
34    current_room.describe()
35    
36    command = input("> ").lower()
37    
38    if command in ["north", "south", "east", "west"]:
39        current_room = current_room.move(command)
40    elif command == "quit":
41        running = False
42    else:
43        print("I don't understand.")

Predict you think the program will do, then run the program.

Make sure you test our error capturing by entering some incorrect commands.

Let’s investigate the new code:

Information

  • else: → a catch-all option for any input which is not a recognised command.

  • print("I don't understand.") → lets the user know their command doesn’t make sense.

Testing

Now we need to test those two additional features. Draw up a table to test each option. Below is an example of my table.

Command

Expected Result

Actual Result

south

moved to armoury

moved to armoury

dog

“I don’t understand.”

“I don’t understand.”

quit

program exits

program exits


Stage 2 task

There is not much to do for our Make phase of this stage, but you do need to test that you can navigate to and from your stage 1 task additional room.

Take the table you used to test navigating the rooms and expand it to also test navigating to your stage 1 task room.