4
\$\begingroup\$

I wanted to create some kind of generic text-based adventure-game engine to see if I could build an interesting puzzle out of it.

The idea was to define a mini-language to be able to define the adventure in a file and provide sessions of play like:

You are at the entrance of a temple. Type temple to go in.
> temple
A strange statue is in the middle of the room. 4 doorways goes out.
The south one goes back to the outside. The north, east and west ones
goes to dark rooms.
> south
You are back at the entrance of the temple.
> temple
A strange statue is in the middle of the room. 4 doorways goes out.
The south one goes back to the outside. The north, east and west ones
goes to dark rooms.
> statue
The statue is held with heavy chains and seems to be the only thing of
interest in this temple. Appart from the doorways to the north, south,
east, and west.
> north
You found a green gem in this room. Yay! The only option here is to go
back to the main room.
> temple
> main
> room
> main room
You are back in the main room and the green gem starts glowing near
the statue. The doors to the north, south, east, and west are still
opened.
> east
The only remarkable thing here, beside the door to the main room is a
tiny fountain in the wall.
> fountain
You found a blue gem in the fountain. You can head back to the main
room.
> main room
You are back in the main room and your 2 gems are brighter near the
statue. The doors to the north, south, east, and west are still
opened.
> west
An empty room with 3 walls and a door to the main room.
> wal
> wall
Upon closer inspection, one of the wall revealed an opening containing
a red gem. Quick! To the main room.
> main room
You are back in the main room and you feel all your gems pulsating
together near the statue. The doors to the north, south, east, and
west are still opened.
> statue
You approach the statue with the 3 gems and the chain entangling it
start removing themselves. You got everything you needed to extract
the statue out of the temple. Congratulations.
You can't go anywhere else. This is the end!

This "puzzle" was defined by the following file:

The lines at the top of the file before any
[place] tag are comments. You can put general
informations such as contributors and version
here.


[start]
You are at the entrance of a [temple].
Type temple to go in.

This is also a comment as it is placed after
a blank line and before the next [place] tag.

[temple]
A strange [statue] is in the middle of the
room. 4 doorways goes out. The [south] one
goes back to the outside. The [north], [east]
and [west] ones goes to dark rooms.

[statue]
The statue is held with heavy chains and
seems to be the only thing of interest in
this [temple]. Appart from the doorways to
the [north], [south], [east], and [west].

[statue:blue gem:green gem:red gem]
You approach the statue with the 3 gems and
the chain entangling it start removing themselves.
You got everything you needed to extract the
statue out of the temple. Congratulations.

[south]
You are back at the entrance of the [temple].

[north]
You found a <green gem> in this room. Yay! The only
option here is to go back to the [main room].

[north:green gem]
Nothing left in this room except the door to
the [main room] in your back.

[east]
The only remarkable thing here, beside the
door to the [main room] is a tiny [fountain]
in the wall.

[fountain]
You found a <blue gem> in the fountain. You can head
back to the [main room].

[fountain:blue gem]
Nothing more in there. Go back to the [main
room].

[west]
An empty room with 3 [wall]s and a door to
the [main room].

[wall]
Upon closer inspection, one of the wall
revealed an opening containing a <red gem>.
Quick! To the [main room].

[wall:red gem]
You find no other opening in the walls.
Except the obvious door to the [main room].

[main room]
You are back in the main room with its 4
doors to the [north], [south], [east], and
[west]. And the [statue] at the middle.

[main room:blue gem]
You are back in the main room and the blue
gem starts glowing near the [statue]. The
doors to the [north], [south], [east], and
[west] are still opened.

[main room:red gem]
You are back in the main room and the red
gem starts glowing near the [statue]. The
doors to the [north], [south], [east], and
[west] are still opened.

[main room:green gem]
You are back in the main room and the green
gem starts glowing near the [statue]. The
doors to the [north], [south], [east], and
[west] are still opened.

[main room:blue gem:red gem]
You are back in the main room and your 2
gems are brighter near the [statue]. The
doors to the [north], [south], [east], and
[west] are still opened.

[main room:blue gem:green gem]
You are back in the main room and your 2
gems are brighter near the [statue]. The
doors to the [north], [south], [east], and
[west] are still opened.

[main room:green gem:red gem]
You are back in the main room and your 2
gems are brighter near the [statue]. The
doors to the [north], [south], [east], and
[west] are still opened.

[main room:blue gem:green gem:red gem]
You are back in the main room and you feel
all your gems pulsating together near the
[statue]. The doors to the [north], [south],
[east], and [west] are still opened.

As you can probably guess from this file, you define several places in this file and you link them together. Each place is defined by its name enclosed in brackets on its own line, followed by a paragraph of text and terminated by an empty line or the end of file. Anything before the first place or between the blank line and the next place name is treated as a comment.

A place name can be followed by : and an item name. The player won't be able to reach this place if they don't have found said item. Add : and item names as necessary if the player need more than one item to enter the place. You can also combine the definition of a place without items and the definition of the same place with an item to provide different descriptions baed on the player inventory and allow them to reach further places once they found the item.

Items are defined in the text description of a place. They should be enclosed in less-than/greater-than delimiters (<...>). As soon as the player enter a place containing an item, it is added to its inventory and never removed.

Lastly, destinations reachable from the current place must be enclosed in brackets ([...]) within the text description of the place. The content within brackets must match an existing definition for a place. If the target place have some item restrictions, the current inventory of the player will be analyzed before allowing them to travel to this destination. You can also define hidden places by using an exclamation mark within the brackets ([..!..]). The part before the exclamation mark (which can be empty) will be displayed and the part after the exclamation mark will define a destination from the current place (and thus must match an actual place definition).

Here is the code for that:

from collections import namedtuple, defaultdict
from textwrap import wrap
import re
import argparse


HIDDEN_PLACES_PATTERN = re.compile(r'\[([^\[!]*)!([^\]]*)\]')
PLACES_PATTERN = re.compile(r'\[([^\]!]*)\]')
ITEMS_PATTERN = re.compile(r'<([^>]*)>')
Place = namedtuple('Place', 'description directions items')


def parse_places(filename):
    story_map = defaultdict(dict)

    with open(filename) as stream:

        def read_description():
            for line in stream:
                line = line.strip()
                if not line:
                    return
                yield line

        for line in stream:
            line = line.strip()
            if line.startswith('[') and line.endswith(']'):
                place, *items = line.strip('[]').split(':')
                description = ' '.join(read_description())
                story_map[place][tuple(items)] = parse_actions(description)

    return story_map


def parse_actions(text):
    hidden_places = [m.group(2) for m in HIDDEN_PLACES_PATTERN.finditer(text)]
    text = HIDDEN_PLACES_PATTERN.sub(r'\1', text)
    reachable_places = PLACES_PATTERN.findall(text)
    text = PLACES_PATTERN.sub(r'\1', text)
    available_items = ITEMS_PATTERN.findall(text)
    text = '\n'.join(wrap(ITEMS_PATTERN.sub(r'\1', text)))
    return Place(text, reachable_places + hidden_places, available_items)


def can_reach(directions, items, story_map):
    available = {}
    for location in directions:
        choices = story_map[location]
        for required_items in sorted(choices, key=len):
            if all(item in items for item in required_items):
                available[location] = choices[required_items]
    return available


def explore(story_map, entry_point='start'):
    place = Place('', [entry_point], [])
    current_items = set()

    while True:
        reachable_places = can_reach(place.directions, current_items, story_map)
        if not reachable_places:
            print('You can\'t go anywhere else. This is the end!')
            return

        while entry_point not in reachable_places:
            entry_point = input('> ')

        place = reachable_places[entry_point]
        print(place.description)
        current_items.update(place.items)


if __name__ == '__main__':
    parser = argparse.ArgumentParser(
            description='Automatic Story Teller will guide you through a '
            'journey. You just need to input the name of your destination '
            'whenever the `>` prompt appears. If the story does not progress '
            'and the prompt keeps appearing it means the destination you '
            'entered is not a valid one; try an other one.')
    parser.add_argument(
            'filepath', help='path to the file containing '
            'the journey you want to play.')
    parser.add_argument(
            '-s', '--start', default='start',
            help='place where you want to start your journey.')
    args = parser.parse_args()
    story = parse_places(args.filepath)
    explore(story, args.start)

What can I do better?

\$\endgroup\$
3
\$\begingroup\$

Don't conflate links and rooms

The biggest flaw I see is that room names and the links between them use the same identifiers. This is particularly problematic with directional indicators, such as north, south, etc.

Let's say we have the following room:

[Throne Room]
An elaborate description of a throne room.
There are passages going [north], [south], [east], and [west].

We have a room to the north, so let's add that. Since we named it north above, we have to call it north here:

[north]
You are in a long hallway.
The hallway continues to the [north].
To the south is the [Throne Room]

This seems simple enough -- you can go back to the throne room or keep going north. But north here links back to itself. So we have to use a different name. So we do this:

[north]
You are in a long hallway.
To the north is the [north hallway].
To the south is the [Throne Room].

Ok, we've solved that problem. Now we add the north hallway:

[north hallway]
You are in a long hallway.
There are rooms to the east and west, but they are locked.
To the north is the [north north hallway].
To the south is ... [north]?!?

As you can see, there are two problems here:

  • We have the rather silly name of north north hallway.
  • We have the ridiculous situation where north is south.

Now, one could argue that this problem is easily solved by using "better" names. But I would argue that "good" names are difficult to come up with -- especially with a large map with many generic areas. Even in this small example, how would you come up with "good" names for the various hallway segments? You could number them (hallway1, hallway2, etc.), but I would not consider those "good" names from the player's perspective.

The solution is to separate the name used to activate a link from the name of the room it links to. For example, you could use the form [name|room] to indicate that the user can type "name" to go to "room". Here's how we might write up our simple example:

[Throne Room]
An elaborate description of a throne room.
There are passages going [north|Hallway1], [south|...], [east|...], and [west|...].

[Hallway1]
You are in a long hallway.
The hallway continues to the [north|Hallway2].
To the [south|Throne Room] is the [Throne Room]

[Hallway2]
You are in a long hallway.
There are rooms to the east and west, but they are locked.
The hallways continues to the [north|Hallway1] and [south|Hallway3].

Other Features to Consider

Here are some other features you might want to consider. Even if you don't add them now, start thinking about how you would extend the engine to support them in the future.

  • Object descriptions. It's a lot more interesting if you can not only find an object, but if you can "examine" it as well. Good descriptions can add a lot of depth to your game.
  • Object interactions. Let the player "do something" with an object. This opens the door for interesting puzzles. For example:

    • Get the bucket
    • Fill it with water
    • Use it to douse the fire blocking your path

    You could have these actions happen automatically. I.e.,

    • Player stumbles into the room with the bucket => they automatically pick it up
    • Player stumbles into the room with the water => if they have the bucket, it is filled with water
    • Player stumbles into the room with the fire => if they have a bucket of water, it is doused

    The player is very passive in all this, though, so it isn't very interesting. Now consider the following interaction:

    • Player enters room with bucket; ignores it.
    • Player enters room with water; ignores it.
    • Player enter room with fire; gears start turning
    • Goes back to water; hmm, if only there was a way to carry this ... aha! the bucket!
    • Player goes, picks up the bucket, fills it with water, and douses the fire!

    This type of interaction keeps the player involved in the game. Rather than passively "stumbling into" the solution for a puzzle, they take an active role in making key observations and applying them to form a solution.

Consider using an existing parser

As you add more features, you might find that maintaining your own language and parser for it takes of time that could be spent actually implementing the features. You might want to consider using an extensible format for which a parser already exists. XML would be a good candidate here, although there are other formats as well.

\$\endgroup\$
  • \$\begingroup\$ Good thoughts overall. For the interactive part with the objects, I initially thought that something like [A] A room with a <bucket> [B] A room with a [fountain] [fountain:bucket] bucket + water = <bucket of water> would be enough interaction needed. But thinking back to it, having the ability to pick objects can lead to elaborate descriptions where the player can not be aware that there are some collectible items. Not sure if forcing to pick every item manually is a good thing or if letting room for automatic actions can benefit the user experience. \$\endgroup\$ – Mathias Ettinger Nov 19 '16 at 19:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.