Movement in a text-based game using room class (Python)

I'm a newbie to game design and coding in general, and I've started working on a text-based adventure game project (like Zork) to work on my narrative writing skills while teaching myself Python. Any tips/pointing me in the right direction would be greatly appreciated.

Currently, I've established an inventory system, a room class, a player class, and an items class with a subclass for weapons.

The current thing that I'm stuck on is getting my player character to be able to move through the rooms in a consistent manner, and whether or not the code is actually reading the player character as being "in" certain rooms based on their position.

inventory = []

currentroom = 0

inventory.append(item)

class player:
def __init__(self, name, health, bag, position):
self.name = name
self.health = health
self.bag = bag
self.position = position

player.position = currentroom

player.bag = inventory

class Items:
def __init__(self, name, info, weight, position):
self.name = name
self.position = position
self.info = info
self.weight = weight

class Weapon(Items):
def __init__(self, name, info, damage, speed, weight, position):
super().__init__(name, info, weight, position)
self.damage = damage
self.speed = speed

class Room:

def __init__(self, description, exits, actions, roominv, position):  # Runs every time a new room is created
self.description = description
self.exits = exits
self.actions = actions
self.roominv = roominv
self.position = position

sword = Weapon("Sword", "A sharp looking sword. Good for fighting goblins!", 7, 5, 5)
knife = Weapon("Knife", "A wicked looking knife, seems sharp!", 5, 7, 3)
stick = Weapon("Stick", "You could probably hit someone with this stick if you needed to", 2, 3, 3)
shackkey = Items("Shack Key", "A key! I wonder what it opens.", .01)
cottagekey = Items("Cottage Key", "An ornate key with an engraving of a small cottage on one side", .01)
Moonstone = Items("Moonstone", "A smooth white stone that seems to radiate soft white light", .05)
flower = Items("Flower", "A beautiful wildflower", .001)

introd = Room("You are in a forest, you can hear wildlife all around you. Your sword lies at your feet. There seems to be a clearing in the distance.", 'N' "clearing", {"Search the ground", "Go North"}, [sword], 0)

clearing = Room("You are in a clearing surrounded by forest. Sunlight is streaming in, illuminating a bright white flower in the center of the clearing. To the South is the way you entered the forest. A well worn path goes to the East. In the distance a harp can be heard.", {'S': "introd", 'E': "forest path"}, {"Take flower", "Go south", "Go East"}, [flower], 1)

forest_path = Room("You begin walking down a well beaten path. The sounds of the forest surround you. Ahead you can see a fork in the road branching to the South and East. You can smell smoke coming from the South, and can hear a stream to the East", {'S': "cottage", 'E': "stream"}, {"Go South", "Go East"}, [stick], 2)

stream = Room("You come upon a relaxing stream at the edge of the woods. To your South is a rickety looking shack, to your West is the forest path you came down", {'S': "shack", 'W': "forest path"}, {"Go South", "Go West"}, [shackkey], 3)

shack = Room("In front of you is a shack, possibly used as an outpost for hunting. It looks dilapidated.", {'S': "inside shack", 'N': "stream"}, {"Go South", "Go North"}, None, 4)

insideshack = Room("The inside of the shack is dirty. Bits of ragged fur are scattered about the floor and on a table against the back wall. A sharp looking knife is on the table. There is a key hanging on the wall by a string.", {'N': "shack"}, {"Go North", "Take Knife", "Take Key"}, [knife, cottagekey], 5)

cottage = Room("A quaint cottage sits in the middle of a small clearing, smoke drifting lazily from the chimney.", {'N': "forest path", 'S': "inside cottage"}, {"Go north", "Go South"}, [Moonstone], 6)


I figured that I could use this to determine what room the player is in, however I'm not sure if this will do what I want it to do. For example, if player.position = 0 then according to my code they are in the introd room

How can I make it so the player only has access to their current room's attributes and not the attributes of the other Room instances?

Am I heading in the right direction with this or should I be using different structure?

Any tips would be much appreciated, currently browsing forums and youtube but still struggling with the logic.

I'm sorry if this is worded poorly/I'm being unclear here.

• Instead of the player class knowing its Room via a number, the position variable could just store the Room itself. E.g, if the player is in the cottage, then you might say player.position = cottage. Then, you can use the actions by doing say, player.position.actions. This way, it would be overall simpler, and you can get rid of the position variable in the Room class as well. Win-win! – Demandooda Jul 7 '17 at 18:49
• Thanks! How would I change the assigned room in player.position as the player moves around though? – Schrodinger'sStat Jul 7 '17 at 18:57
• I'm currently trying out this method you suggested and I'm trying to get movement working with this player.position = cottage command = input(">>> ") def movement(): if player.position == Room(introd) and command == 'N': set(player.position) = Room(clearing) However I'm getting an error telling me that I have invalid syntax (in sublime text it says "[E] Can't assign to function call") I guess I can't use set() on player.position? – Schrodinger'sStat Jul 7 '17 at 19:21
• There's a number of issues that suggest you should brush up on your Python. player should be an instance of a class Player, position should be a link to a room, rooms should either link directly to each other or else you need a global registry of room name string to Room instance, etc. – Jimmy Jul 7 '17 at 19:25

The player and Basic Python Literacy

This is something you'll get better with experience, but you don't seem to have a good grasp on how classes work yet. For example, you're setting variables directly on the player class.

# YOUR CODE
inventory = []
currentroom = 0

inventory.append(item)

class player:
def __init__(self, name, health, bag, position):
...

player.position = currentroom
player.bag = inventory


How I'd write this. Note that world[room_name] part. I'll get to it in a second:

class Player:
def __init__(self, name, health, bag, room_name):
self.name = name
self.room = world[room_name])
...

player = Player("Bob", 100, [], "introd")


Rooms and links between Rooms

I think you need to have a global registry of room names, either as a dict or wrapped up in a class itself.

class Room:
def __init__(name, description, links):
...

world = {}
world['introd'] = Room(
"introd",
"You are in a forest",
{'N': 'clearing'}
)
world['clearing'] = Room(
'clearing',
'You are in a clearing surrounded by forest',
{'S': 'introd'}
)


Then your player move function can:

class Player:
...
def move(self, direction):
if direction not in self.room.exits:
print("Cannot move in that direction!")
return
new_room_name = self.room.exits['room']
print('moving to', new_room_name)
self.room = world[new_room_name]

1. take a direction (N)
2. look it up in the current room's exits. (new_room_name becomes "clearing")
3. set the players new room

Now your core look can be something like

while True:
command = raw_input('>>')
if command in {'N', 'E', 'S', 'W'}:
player.move(command)
elif command == 'look':
print(player.room.description)
print('Exits', player.room.exits.keys())
else:
print('Invalid command')


An aside on room links by name versus links by reference An alternate design would be to not have the world dict at all, and just link rooms directly to each other:

forest = Room("a forest")
clearing = Room("a clearing")
forest.exits['S'] = clearing
clearing.exits['N'] = forest


However, this requires adding exits after defining the rooms (since otherwise you will need to refer to rooms before they are defined). Also you may want to eventually put the room data in a data file (like a database, CSV, or a JSON file) and having names or ids is important to link rooms together then.

• Good answer. However, I might add that you can also do something like a 2D list storing the rooms. Since the player can move North, South, East, and West, and each index of the array is in the form of list[i][j], you simply increment i to move East, decrement i to move West, increment j to move North and decrement j to move South. You may run into problems if your entire list isn't populated with rooms but that can easily be solved by checking if a given index isn't empty before allowing the user to go there. – Orren Ravid Jul 7 '17 at 21:38
• @orrendavid I considered that but most of the MUD's i've played have had highly irregular map layouts (rooms with interiors larger than exteriors, vertical portals, passages with less cells on the way down than on the way up, etc) that would be very hard to shoehorn into a 2D array. I find the exit-links strategy much more flexible. – Jimmy Jul 7 '17 at 21:57
• that's fair. Your approach does provide a greater flexibility. Since OP didn't specify what sort of navigation he was looking for, I suggested the 2D array as a possibility, which I believe OP as a beginner should consider while also considering its shortcomings as you mentioned. – Orren Ravid Jul 7 '17 at 22:08

Yes, your question is (a little) unclear. I thought about commenting to seek some clarity first, but maybe it's more useful to just explore the implications of a movement system? @Jimmy's already covered some of this; such is the nature of picking at an answer between other tasks :)

A very simple system

The basic questions you need to be able to answer with a movement system are where is the player? and where can the player move to? Let's just try to write a movement command, and see what feels like a natural (well--IMO) way to express these questions in Python (I'm just freewriting pseudo-code and omitting things that aren't in focus; don't expect this to execute).

class Player(object):
...

def parse_command(self, command, *args):
"""
I wouldn't actually check commands in this order;
it just makes for a better progression
"""

# is this a global command (like "help")?
if command in command_list:
return commands[command](args)

# is this something the user can do with an item they're holding?
for item in self.bag:
if command in item.actions:
return item.actions[command](args)

# Is this something the user can do in the room they occupy?
if command in self.location.actions:
return self.location.actions[command](args)

# Is this a valid exit?
if command in self.location.exits:
return self.move(self.location, self.location.exits[command], direction)

def move(self, start, end, direction):

# maybe ask the present room if we can go, in case there's a guard, or a booby-trap
if start.can_exit(self, end, direction):

# maybe we won't ask the destination, but we might want to give everyone a chance to react to our move
start.exit(self, end, direction)
self.location = end
end.enter(self, start, direction)
return True


This fairly simple expression works if player.location is literally a reference to the room object, and not just an identifier (string, number) we can use to look it up. Instead of starting with player.location = 0, you can start with player.location = introd. It also answers your next question (How can I make it so the player only has access to their current room's attributes and not the attributes of the other Room instances?), because you can just do something like print(player.location.description).

You've probably already realized that this really simple model gets tricky when it's time to define exits, because all of the room objects have to be loaded before they can link to each other. One way around this is to create all of your room objects at once, and then configure them:

introd = Room()
clearing = Room()
forest_path = Room()

introd.setup("You are in a forest, you can hear wildlife all around you. Your sword lies at your feet. There seems to be a clearing in the distance.", {'N': clearing}, {"Search the ground", "Go North"}, [sword], 0)

clearing.setup("You are in a clearing surrounded by forest. Sunlight is streaming in, illuminating a bright white flower in the center of the clearing. To the South is the way you entered the forest. A well worn path goes to the East. In the distance a harp can be heard.", {'S': introd, 'E': forest_path}, {"Take flower", "Go south", "Go East"}, [flower], 1)

...


Increasing complexity

This isn't to say that using a position number is wrong--just that there's a simpler way to write it if you don't need something more complex. If your text game was so large that you couldn't load all of the rooms at once, you'd need a stable string or number that you could use to look up rooms as-needed. Likewise, if you want to have a "save game" file, you'll probably need to save a string or number indicating where the user was.

Unless your game is very small, I would recommend using a string (perhaps even a path) to identify the rooms; by the time you've got 20 rooms, it's going to get hard to remember which exit/id points where, and you'll have to do lots of cross-referencing to untangle them if you mess up. You'd need to change up some code:

# you could make a rooms dict keyed by room ID, AND pass the ID to each room
rooms = {key: Room(key) for key in ["introd", "clearing", "forest_path"]} # this a "dict comprehension"
rooms["introd"].setup(... {'N': "clearing"}...)

# without further changes, moving would look a little tedious...
self.move(rooms[self.location], rooms[rooms[self.location].exits[command]], command)

# You could have your cake and eat it too by using a *property decorator* on the player object (property decorators enable a method to behave like a regular object property):
class Player(object):
...
@property
def location(self):
return rooms[self._location_id]

@location.setter
def location(self, value):
self._location_id = value.room_id

# then moving would simplify to:
self.move(self.location, rooms[self.location.exits[command]], command)


More complex or special-purpose options

One of the nice things about the simple system is that you can make non-traditional exits (like "ladder" or "swim"), but it does have some downsides when compared to, for example, a coordinate-based system:

• It's harder to draw maps (where do you draw an exit to "swim"?)
• Locational messages are harder to do cleanly ("You hear a loud scream to the east" vs. "You hear a loud scream from... swim?")
• It's harder (but not impossible) to craft certain kinds of free-form/unbounded/random exploration experiences, such as wandering off into a wild forest where you can walk any direction, but have a random chance of stumbling on the entrance to a cave system.
• Certain kinds of directional commands or abilities can be awkward (that darn swim exit would once again break a command like "throw torch east" that in a coordinate system might just yield "You toss a torch east into the darkness and it lights up {description}.")