# Cactus (Prototype) - A game engine for text-based adventure games

@Annonomus Penguin, @QPaysTaxes, and I are building an engine for easy creation of text-based adventure games, like Zork. The engine is still in its early stages of development, and the following code is my prototype. Here's the library code.

cactus.py

class MapPosition(object):
"""
Describes data about a position on in the world
map. Contains the following data attributes.

name     - The name of the choice.
desc     - A description of the choice.
choices  - A dictionary of possible choices.
function - An (optional) function to be run.
"""
def __init__(self, name, desc, choices, function=None):
self.name     = name
self.desc     = desc
self.choices  = choices
self.function = function

def print_choice(self):
"""
Outputs certain data about the choice, e.g, description,
it's name, possible choices, etc.
"""
if self.choices != {}:
print "{0}: {1} Choices: {2}".format(
self.name,
self.desc,
', '.join([key for key, value in self.choices.iteritems()])
)
else:
print "{0}: {1}".format(
self.name,
self.desc
)

def run_function(self):
"""
Run the attached function when the choice is chosen
by the user.
"""
if self.function is not None:
self.function()

class GameMap(object):
"""
Describes data about a world map. Contains the
following data attributes.

map_data - A list containing MapPosition's
"""
def __init__(self, map_data):
self.map_data = map_data

def find_start(self):
"""
This iterates over the self.map_data list and finds
the starting choice.
"""
for index, choice in enumerate(self.map_data):
if choice.name.lower() == "start":
return index
else:
continue

def return_map(self):
"""
Returns the map_data as a list.
"""
return self.map_data

class MainGame(object):
"""
Describes data about the game itself. Contains
the following data attributes.

name        - The name of the game.
desc        - The intro text to be printed.
prompt      - The prompt to use during gameplay.
game_map    - A GameMap instance containing map data.
"""
def __init__(self, name, desc, prompt, game_map):
self.name         = name
self.desc         = desc
self.prompt       = prompt
self.game_map     = game_map
self.map_data     = game_map.return_map()
self.start_index  = game_map.find_start()
self.map_position = self.start_index

def play_game(self):
"""
Start playing the game. This function will find the
MapPosition element that has the name "start". Do note,
user input is lowered, but a check is also done on non-
lowered input.
"""
print self.name
print self.desc

while True:
current_position = self.map_data[self.map_position]
possible_choices = current_position.choices

current_position.print_choice()
current_position.run_function()

user_input = raw_input(self.prompt)

if user_input.lower() in possible_choices:
self.map_position = possible_choices[user_input.lower()]
elif user_input in possible_choices:
self.map_position = possible_choices[user_input]


Before I show the example file, here's a small "tutorial" on how to create a small game.

• First off, create a GameMap instance. This instance will hold MapPosition instances stored in an list.

GAME_MAP = cactus.GameMap([
# ...
])


• Next, fill the list inside GameMap with MapPosition instances. A MapPosition instance contains the following data.
• A name for the position. (Note, any MapPosition with the name "start" will be the starting position for the game).
• A description of the position.
• Other possible MapPosition instances that the player can choose.
• An optional function to be run.

cactus.MapPosition(
"position name",
"position description",
{
"a choice": 1 # <-- References the index of another position.
}
function=a_func # <-- If no function is referenced, default is None.
)


• Finally, an instance of MainGame is created with the following data.
• The name of the game.
• A description of the game.
• The prompt to be used in the game.
• The game map, and instance of GameMap.

GAME = cactus.MainGame(
"game name",
"game description",
"game prompt",
GAME_MAP
)


• Finally, after all that has been done, call GAME.play_game(), and play your game.

For those who also want a concrete example of a simple game, the following program demonstrates that.

import cactus
from sys import exit

GAME_MAP = cactus.GameMap([
cactus.MapPosition(
"Start",
"Welcome to the test!",
{
"left":  1,
"right": 2,
}
),
cactus.MapPosition(
"Left",
"You took the left path and lived!",
{},
function=exit
),
cactus.MapPosition(
"Right",
"You took the right path and died!",
{},
function=exit
)
])

GAME = cactus.MainGame(
"Test Game",
"This is a simple test game! Yay!",
"> ",
GAME_MAP
)

GAME.play_game()


Finally, for those who are interested, here's the Official Cactus Discussion Chat Room, and here's the link to the official project on Github.

• It looks good! Not as good as mine will, but good! (Rampant narcissism aside, I like it) – Nic Hartley May 31 '15 at 21:36
• @QPaysTaxes Thanks, I appreciate the feedback! :) – Ethan Bierlein May 31 '15 at 21:37
• So if you wanted a thorough game, you'd have a multiple nests inside each MapPosition call, correct? It seems as though you'll end up with one huge file in the end with tens or hundreds of layers nested deep! Am I wrong? – Alex L Jun 1 '15 at 3:23
• @AlexL No... Not neccisarily. Each MapPosition contains a dictionary of user choices which reference the index of another MapPosition in the GameMap array. – Ethan Bierlein Jun 1 '15 at 11:40
• It seems that MainGame does not need to be a class, but could rather be implemented as a function. – jme Jun 1 '15 at 18:49

else:
continue


Does nothing: remove it.

lower or not lower: you must decide

I suggest you decide: either all input is lowercased or it is kept as the original, the following is not a clean solution (you can understand it is not clean because you spelled it out in the docs).

if user_input.lower() in possible_choices:
self.map_position = possible_choices[user_input.lower()]
elif user_input in possible_choices:
self.map_position = possible_choices[user_input]

• Hmm, could I specify an option where the user can choose to have text lowered? Would that be cleaner? – Ethan Bierlein Jun 1 '15 at 11:47
• @EthanBierlein in fact I think you should use a keyword argument: this way beginners will have the text automatically lowercased and experts would be able to differentiate desert and DESERT and Desert may they decide to. – Caridorc Jun 1 '15 at 16:06