Cave Commander!
ericqweinstein (200)

Hi everyone,

I made a little text-based adventure game in Python called Cave Commander based on Colossal Cave Adventure. It's pretty simple right now—only a few rooms in the cave and a handful of items—so feel free to fork the REPL and add to it!

The code is divided into three files: main.py, cave.py (where the cave map is stored), and constants.py (where we keep regular expressions that match against possible commands the player might enter, like go north or take cage). Let's start with cave.py.

Map = {
    'start': {
        'description': 'You are standing at the end of a road before ' \
                       'a small brick building. Around you is a '      \
                       'forest. A small stream flows out of the '      \
                       'building and down a gully to the SOUTH.',
        'exits': { 'South': 'grate' },
        'items': []
    },
    'grate': {
        'description': 'You reach the end of the stream, which flows ' \
                       'down through a metal grate and disappears. ',
        'exits': { 'North': 'start', 'West': 'debris' },
        'items': ['cage']
    },
    'debris': {
        'description': 'You walk along a shallow gully and duck under' \
                       ' the remains of a small bridge. You\'re in a ' \
                       'dimly lit room littered with debris.',
        'exits': { 'East': 'grate', 'West': 'bird' },
        'items': ['rod']
    },
    'bird': {
        'description': 'You follow a tunnel into a larger room where' \
                       ' you can stand upright without hunching.'     \
                       ' You can hear rustling and twittering.',
        'exits': { 'East': 'debris', 'South': 'mists' },
        'items': ['bird']
    },
    'mists': {
        'description': 'You\'re in a huge hall that extends as far ' \
                       'as you can see in an east/west direction. '  \
                       'The ceiling is shrouded in darkness. An '    \
                       'eerie mist covers the floor.',
        'exits': { 'North': 'bird', 'West': 'end' },
        'items': []
    },
    'end': {
        'description': 'You made it to the end.',
        'exits': {},
        'items': []
    }
}

Not a lot of fanciness here: just a Python dict that keeps track of the rooms in our cave. Note that each entry in the map is a key/value pair where the key is the room name and the value is another map describing that room. Each room has a 'description', a list of 'exits' where the player can go next, and a list of 'items' that are in the room.

Next up: constants.py!

import re

class Directions(object):
    NORTH = re.compile('(?:^\s*n\s*$|.*north\s*$)', re.IGNORECASE)
    SOUTH = re.compile('(?:^\s*s\s*$|.*south\s*$)', re.IGNORECASE)
    EAST = re.compile('(?:^\s*e\s*$|.*east\s*$)',   re.IGNORECASE)
    WEST = re.compile('(?:^\s*w\s*$|.*west\s*$)',   re.IGNORECASE)

class Commands(object):
    TAKE = re.compile('^\s*(?:take)\s+(.*)',      re.IGNORECASE)
    DROP = re.compile('^\s*(?:drop)\s+(.*)',      re.IGNORECASE)
    INVENTORY = re.compile('(?:^i$|.*inventory)', re.IGNORECASE)
    HELP = re.compile('(?:^h$|.*help\.*)',        re.IGNORECASE)

(It's a little overkill to put these in classes, but it will make the syntax nicer later on when we start building out main.py.) Note that there are two classes: Directions for handling directions the player might want to move in, and Commands for other actions (such as picking up and dropping items, checking the player's inventory of items, or getting help in terms of which direction to go next). If you're not used to regular expressions, these might look scary, but they're not too bad! Let's look at NORTH as an example.

When we type re.compile('(?:^\s*n\s*$|.*north\s*$)', re.IGNORECASE), we're telling Python to build the regular expression (?:^\s*n\s*$|.*north\s*$) and to match in a case-insensitive way (that is, we'll match 'north', 'North', 'NORTH', 'nOrTh', etc). The first bit, (?:^\s*n\s*$, says to match the start of the line (^) through the end of the line ($), allowing any amount of whitespace (\s*) at the beginning or end, and look for the letter n. (The ?: part just tells Python not to keep track of what got matched, since we won't be using it in our code.) The next part, .*north\s*$, tells Python to match anything at all (.*), followed by north, followed by optional whitespace (\s*) and then the end of the line ($). These two matches are separated by a pipe, |, so the entire regular expression says: "Match either the letter 'n', or the word 'north', allowing whitespace on either side, and allowing anything at all before the word 'north'." That way if the player enters 'n', or ' n ', or 'go north', or 'let's go north ', etc., our program will be able to match that input and do the right thing.

Note that regular expressions can be tricky, so be careful when adding new ones (and apologies in advance if you try to use input that my regular expressions aren't expecting!). I've found Pythex to be a really valuable tool for testing my Python regular expressions.

Okay! Now on to the main (pun intended) event: main.py. Let's start by importing our other files and creating the skeleton of our game:

import re

from constants import Directions, Commands
from cave import Map

class Game(object):
    def __init__(self):
        self.map = Map
        self.location = 'start'
        self.inventory = []

    def start(self):
        while True:
            instruction = input('> ')


if __name__ == '__main__':
    game = Game()
    game.start()

In our __init__() function, we ensure we have access to our cave map, set our location to 'start' (the first room in our map), and initialize an empty inventory. We also add a start() method, which just creates a prompt and lets the user enter input.

Next, let's add a new method, follow(), that lets the user move around the map. Add this code inside the Game class, between __init__() and start():

def follow(self, instruction):
    if re.search(Directions.NORTH, instruction):
        try:
            self.location = self.map[self.location]['exits']['North']
            print(self.map[self.location]['description'])
        except KeyError:
            print('You can\'t go that way.')
    elif re.search(Directions.SOUTH, instruction):
        try:
            self.location = self.map[self.location]['exits']['South']
            print(self.map[self.location]['description'])
        except KeyError:
            print('You can\'t go that way.')
    elif re.search(Directions.EAST, instruction):
        try:
            self.location = self.map[self.location]['exits']['East']
            print(self.map[self.location]['description'])
        except KeyError:
            print('You can\'t go that way.')
    elif re.search(Directions.WEST, instruction):
        try:
            self.location = self.map[self.location]['exits']['West']
            print(self.map[self.location]['description'])
        except KeyError:
            print('You can\'t go that way.')

    if not self.map[self.location]['exits']:
            print('You win!')

And let's update our start() method to use it:

def start(self):
    print(self.map[self.location]['description'])

    while True:
        instruction = input('> ')

        try:
            self.follow(instruction)
        except RuntimeError as err:
            print(err)

If you run the REPL, you should now be able to move around the map. From the starting position, try going south, then west. If you get stuck, take a look at cave.py to read the map and see where you can go next.

Checking out the map is fun, but it would be better if we could get hints to tell us where to go next. In order to interact with our game, we'll need to use the Commands we included in constants.py. In order to do that, we'll update our follow() method to use the commands for taking/dropping items, checking our inventory, and getting help with directions, as well as adding a little helper method, take_item(), to handle item logic (if you want to catch a bird, you're gonna need a cage, right?).

When we add those methods, we get our complete code, which looks like this:

import re

from constants import Directions, Commands
from cave import Map

class Game(object):
    def __init__(self):
        self.map = Map
        self.location = 'start'
        self.inventory = []

    def take_item(self, item):
        if item == 'bird':
            if 'cage' not in self.inventory:
                print('With what? Your bare hands? You should probably use a cage or something...')
                return
            if 'rod' in self.inventory:
                print('The bird is frightened and impossible to catch.')
                return
        try:
            self.map[self.location]['items'].remove(item)
            self.inventory.append(item)
            print('You picked up a %s.' % item)
        except ValueError:
            print('There\'s no %s for you to take.' % item)

    def follow(self, instruction):
        if re.search(Directions.NORTH, instruction):
            try:
                self.location = self.map[self.location]['exits']['North']
                print(self.map[self.location]['description'])
            except KeyError:
                print('You can\'t go that way.')
        elif re.search(Directions.SOUTH, instruction):
            try:
                self.location = self.map[self.location]['exits']['South']
                print(self.map[self.location]['description'])
            except KeyError:
                print('You can\'t go that way.')
        elif re.search(Directions.EAST, instruction):
            try:
                self.location = self.map[self.location]['exits']['East']
                print(self.map[self.location]['description'])
            except KeyError:
                print('You can\'t go that way.')
        elif re.search(Directions.WEST, instruction):
            try:
                self.location = self.map[self.location]['exits']['West']
                print(self.map[self.location]['description'])
            except KeyError:
                print('You can\'t go that way.')
        elif re.search(Commands.TAKE, instruction):
            match = re.search(Commands.TAKE, instruction).group(1).strip()
            self.take_item(match)
        elif re.search(Commands.DROP, instruction):
            match = re.search(Commands.DROP, instruction).group(1).strip()

            try:
                self.inventory.remove(match)
                self.map[self.location]['items'].append(match)
            except ValueError:
                print('You don\'t have a %s to drop.' % match)
        elif re.search(Commands.INVENTORY, instruction):
            [print('You\'ve got a %s.' % item) for item in self.inventory] if self.inventory else print('You\'re not carrying anything.')
        elif re.search(Commands.HELP, instruction):
            print('Available directions are:')
            [print('  %s' % direction) for direction in self.map[self.location]['exits']]
        else:
            print('I don\'t understand which way you want to go. ' \
                  'Please try NORTH, SOUTH, EAST, or WEST.')

        items = self.map[self.location]['items']

        if items:
            [print('There\'s a %s here.' % item) for item in items]

        if not self.map[self.location]['exits']:
            print('You win!')

    def start(self):
        print(self.map[self.location]['description'])

        while True:
            instruction = input('> ')

            try:
                self.follow(instruction)
            except RuntimeError as err:
                print(err)


if __name__ == '__main__':
    game = Game()
    game.start()

And that's it! Try moving south and west, picking up items when you find them (take bird), checking your inventory (inventory, or just i), and getting help with directions whenever you're stuck (h or help).

I hope you enjoyed this tutorial, and again, feel free to fork the REPL to add more to the game!

You are viewing a single comment. View All
jdelarios (0)

I put it into Pygame and this resulted when run:
Python3 with Pygame
nohup: redirecting stderr to stdout