11
\$\begingroup\$

@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.

\$\endgroup\$
  • 2
    \$\begingroup\$ It looks good! Not as good as mine will, but good! (Rampant narcissism aside, I like it) \$\endgroup\$ – Nic Hartley May 31 '15 at 21:36
  • 2
    \$\begingroup\$ @QPaysTaxes Thanks, I appreciate the feedback! :) \$\endgroup\$ – Ethan Bierlein May 31 '15 at 21:37
  • 2
    \$\begingroup\$ 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? \$\endgroup\$ – Alex L Jun 1 '15 at 3:23
  • 1
    \$\begingroup\$ @AlexL No... Not neccisarily. Each MapPosition contains a dictionary of user choices which reference the index of another MapPosition in the GameMap array. \$\endgroup\$ – Ethan Bierlein Jun 1 '15 at 11:40
  • \$\begingroup\$ It seems that MainGame does not need to be a class, but could rather be implemented as a function. \$\endgroup\$ – jme Jun 1 '15 at 18:49
5
\$\begingroup\$
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]
\$\endgroup\$
  • \$\begingroup\$ Hmm, could I specify an option where the user can choose to have text lowered? Would that be cleaner? \$\endgroup\$ – Ethan Bierlein Jun 1 '15 at 11:47
  • 2
    \$\begingroup\$ @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. \$\endgroup\$ – Caridorc Jun 1 '15 at 16:06

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.