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?