metanohi/site/projects/zita/index.org

19 KiB

Zita

#&summary A simple but cumbersome text adventure game engine #&

#&toc

#&img;url=img/zita-logo.png, center, alt='Logo of Zita'

Zita

Zita is a text adventure engine written in Python. Text adventures, also known as interactive fiction in some cases, challenge the minds of people, using no graphics at all. With Zita, it is now possible to write a text adventure in normal Python syntax.

Installation

To run Zita, Python must be installed. No other depencies are needed.

Download Zita.

To run Zita, run zita.py. If a game directory is specified, Zita will attempt to execute the game files in that directory. If no game directory is specified, Zita will attempt to run the game Zeqy. Besides zita.py, there is also a file named defs.py. This file is executed by zita.py on startup and should not be used on its own. It contains functions crucial to Zita.

Hacking

Zita is released under the GPLv3+. Feel free to improve it.

Zeqy

Zeqy is a very short text adventure developed for Zita and is shipped together with the engine. It features a short "story" and proves Zita's simplicity. Zeqy is released under the Creative Commons Zero 1.0 Universal license. This means that you're free to do whatever you want to do with Zeqy. There is no owner of Zeqy.

Loading data

Games to be run in Zita's engine are saved in text files in a directory. In the case of Zeqy, several files reside in the 'zeqy' directory. The files of a game directory are loaded when Zita runs. In this section, Zeqy will be used as an example of how Zita works.

A look at the 'zeqy' directory reveals the following files: #&pre commands.zt functions.zt LICENSE rooms.zt defaults.zt items.zt main.zt vars.zt #&

When Zita starts, it first looks for a file named 'prerun' inside the game directory. If a file named that is found, Zita will execute any Python code inside the file. This allows programmers to change various default values. After looking for 'prerun', Zita attempts to find a main file. By default, the main file must be named something that starts with 'main', though this can be changed in the 'prerun' file. If several files that have names starting with 'main' exist, only one of them will be used. If, for example, a directory has both a file named 'main', a file named 'main.py' and a file named 'main.zt', the latter will be loaded (except if the 'prerun' file states otherwise). This is because the '.zt' ending is the default suffix.

Looking at the file list earlier, it is now possible to deduct that the file 'main.zt' must be the main file for Zeqy. Here it is:

# Print welcome message
p('Welcome to Zeqy, a simple (and short) text adventure demonstrating the \
possibilities of Zita, an even simpler Python text adventure engine. If you\'re \
completely lost, try asking the engine for help.\n')

# Include files
include(['functions', 'rooms', 'items', 'vars', 'commands', 'defaults'])

# Starting position
goto('grass1')

Python code in the main file is executed before Zita starts a game. This means that the main file is suitable to use to print a welcome message, though this is of course also possible to do in the 'prerun' file.<br /> What should be done in the main file and not in the 'prerun' file is including files. The include function loads and executes both files and files in subdirectories. In the above example, only files are imported, as no subdirectories exist. If, however, such directories did exist, the include command would look for those too.

An example

We have a game directory with a file named 'stuff' and a directory named 'stuff' that holds several files. In the main file we write this:

#++python2

include(['stuff'])

This makes Zita load and execute both the 'stuff' file and the files in the 'stuff' folder.

It is possible to make the include function accept only either one file, all files or files in subdirectories. Refer to the include and getfilenames functions found in defs.py.

When Zita has succesfully loaded all data that it needs to run a game succesfully, it is important to use the goto function to create a starting position. A starting position can also be defined in another way, but using goto ensures that a message describing the current location is shown.

Data structures

When the main file has been loaded and executed, an infinite loop is started. It runs until the variable COMPLETELY_DONE is true. When that happens, Zita exits.

The code inside the loop asks the user for input, which it the processes. Input is split with spaces, and the first word is always the command. A command cannot exist of more than one word. All characters that come after the command are considered part of an object. After the user has pressed Enter, Zita searches through several global variables to see if what the user has typed matches a stored command and/or an object.

The 6 important global variables in Zita are:

  • room
  • item
  • var
  • command
  • default
  • extra

All except extra are dictionaries. The extra variable is a string that can contain Python code.

>room

This variable keeps track of the "rooms" in a game. Rooms are the locations in which a player can be. Rooms can hold items and point to other rooms. See the commented example below containing the room (taken from Zeqy's 'rooms.zt'):

room = {
'grass1': { # We start defining a room with the grass1 id
    'name': 'grassy field', # Its name (names are currently not used by Zita)
    'desc': 'You are standing on a grassy field covered with flowers. The wind \
almost blows you away.', # This is the description. The description of this
                         # room is shown when the user enters it.
    
    # Items are included in a list containing dictionaries. The 'id' property
    # should point to the id of an item, while the 'desc' property is optional
    # and can be used to describe the relationship between an item and the room
    # it is in.
    'items': [{
        'id': 'redflower',
        'desc': "'To the right is a ' + item[citem]['name'] + '.'"
        }, {
        'id': 'multic_flower',
        'desc': "'To the left is a ' + item[citem]['name'] + '.'"
    }],
    
    # Rooms includes directions in the 'dir' part. From the code below Zita
    # understands that walking north will take the user to the room with an
    # id of 'grass2', walking west will take the user to a swamp, going up
    # will result in a message telling the user going up is impossible, and
    # trying everything else will result in Zita giving the user a message.
    'dir': {
        'n': 'grass2',
        'w': 'swamp',
        'u': "!p('You can\\'t fly yet!')",
        '&rest': "!p('Going north or west should be possible.')"
    }
}
}

Note that in the above code, the variable citem pops up. This is a special variable holding the current item id. In the items list above, in the case of 'redflower', citem would be short for 'redflower', and in the case of 'multic_flower', citem would be short for 'multic_flower'. This may seem useless, but in some cases it's handy. Read on.

>item

This variable keeps information about items. Items can be carried around in an inventory. An item has a name and a series of commands associated with it. An item can have two states. Either it's in or it's out, i.e. it's either in the inventory or in the current room. See below for a commented example (taken from Zeqy's 'items.zt'):

item = {
'redflower': { # This is the id. Rooms use ids like this.
    'name': 'red flower', # The name
    
    # Commands associated with the item and the code to execute on activation.
    # Apart from 'cmd', which reacts to an item no matter what state it is in,
    # there is also 'in' and 'out'. These are simply not needed in this case.
    
    # ┏━┓┏┓╻   ┏━╸╻ ╻┏━┓┏┳┓┏━┓╻  ┏━╸ 
    # ┣━┫┃┗┫   ┣╸ ┏╋┛┣━┫┃┃┃┣━┛┃  ┣╸ ╹
    # ╹ ╹╹ ╹   ┗━╸╹ ╹╹ ╹╹ ╹╹  ┗━╸┗━╸╹
    # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    # A user types:
    # > eat red flower
    # Zita finds this item and executes the value of the 'eat' property in the
    # 'cmd' property. If there were no 'cmd' commands that matched the command,
    # Zita would move on to see if any 'in' or 'out' commands, depending on the
    # state of the item, matching the command existed.
    
    'cmd': {
        'eat': "p('It doesn\\'t seem edible.')",
        'look': "p('It\\'s pretty.')",
        'get': "p('The flower is stuck.')"
    }
}
}

The above example presents only a simple item. It is possible to create much more complex ones. A slightly more complex item can be seen below (this one is also taken from 'items.zt'):

item = {
'multic_flower': {
    'name': 'yellow flower',
    'cmd': {
        'smell':
# Using Python's multiline indicator (three quotes), one can write code that
# takes up more than one line. The code below prints a message and changes the
# color (in the name) of the current item. This happens when the flower is
# smelled to.
"""
flcolor = randelm(var['colors']) # Gets a random color. The 'var' variable is
                                 # explained later on.
p('You breathe in the fumes of the ' + item[citem]['name'] + '. In a matter of
picoseconds the flower changes its color to ' + flcolor + '.')
item[citem]['name'] = flcolor + ' flower'
""",
        'look': "p('It\\'s like the sun.')"
    }    
}
}

Last, but not least, Zita understands the 'pref' (prefix) property:

item = {
'wood': {
    'name': 'dry wood',
    'pref': 'some'
    # If 'some' wasn't specified as a prefix in this case, opening one's
    # inventory would read this:
    
    # You currently have:
    #   A dry wood
    
    # In this case, however, 'a wood' is not what we want to write. 'some wood'
    # is better. Setting 'pref' to 'some' solves this problem. Now it reads:
    
    # You currently have:
    #   Some dry wood

}
}

If Zita detects more than one item matching the user's input, the user is prompted to choose between the available items. This would be the case if a room had to books, a red one and a green one, and the user merely states the lust to read a book.

Tip: If you get tired of typing long item names, you can choose to only type the last part of an item. Instead of typing 'capture magenta-coloured space creep', you can choose to simply type 'capture creep' or even 'capture p' (as the last letter in 'capture magenta-coloured space creep' is a p).

>var

This variable merely holds variables and their respective values. var variables can be used to store data. Looking at 'vars.zt' from Zeqy, we see, among other stuff, this (commented version):

var = {
    # 'imposs' does not have to exist, though Zeqy needs it in the default
    # variable.
    'imposs': ['That\'s not an option.', 'I am unable to accomplish that \
particular feat.', 'That seems out of the question.', 'Are you stupid?', 'I \
could try, but I don\'t want to.', 'I\'m not doing that!', 'No way.'],
    
    # 'listoptions' MUST exist. If Zita prompts a user to choose between
    # several items, a list is created using characters from 'listoptions'.
    'listoptions': 'abcdefghijklmnopqrstuvwxyz',
    # Using these list letters would result in multiple-choice scenarios
    # looking like this:
    
    # > eat python
    # Which python?
    # a. dangerous python
    # b. Monty Python
    
    # 'which' MUST exist. It holds what to output when multiple items are
    # available. The variable what holds the current
    # object specified by the user. In the above example, what would be python.
    'which': "'Which ' + what + '?'",
    
    # 'invalidwhich' MUST exist. The value of this variable is output when
    # multiple items are available and the user attempts to get an item that
    # doesn't exist.
    'invalidwhich': "'Not a valid ' + what + '. Try again.'",
    #An example:
    
    # > c
    # Not a valid python. Try again.
    
    # 'steps' does not have to exist. It is self-explanatory.
    'steps': 0,
    
    # 'inventory' MUST exist. Normally it should be an empty list, but it is of
    # course also possible to have the user start with one or more items.
    'inventory': [],
    
    # 'location' MUST exist, though it isn't completely necessary to have it
    # defined here. Mostly, writing a goto function in the main file is better
    # than defining the room here. Defining the room here means that the user
    # will not see a startup message describing the current location.
    'location': None,
    
    # 'points' does not necessarily have to exist. Zita doesn't depend on it.
    'points': 0
}

Variables are useful.

>command

If Zita is unable to match a user-typed command with a user-typed object, it checks if a command independent of items matches. A command can be short ones such as:

command = {
    'steps': "p('You have taken ' + str(var['steps']) + ' steps.')"
}

..Though it can also include more complex commands, such as:

command = {
    'go': # It's the common 'go' command!
"""
if len(cmd) == 1: # cmd is a list containing user input split with spaces.
    p('You can\\'t go nowhere.')
else:
    ltd = long2dir(cmd[1]) # Converts 'north' to 'n', etc.
    if not 'dir' in room[var['location']]:
        p('You are trapped!')
    elif cmd[1] != '&rest' and ltd in room[var['location']]['dir']:
        rname = room[var['location']]['dir'][ltd]
        if rname[0] == '!':
            exec(rname[1:]) # Only execute what's after the exclamation mark.
        else:
            goto(rname) # Use the goto function
    elif '&rest' in room[var['location']]['dir']: # Default action
        rname = room[var['location']]['dir']['&rest']
        if rname[0] == '!':
            exec(rname[1:])
        else:
            goto(rname)
    else:                               # Converts 'w' to 'west', etc.
        p('It is not possible to go ' + short2dir(txt[len(cmd[0])+1:]) + '.')
"""
}

If no available item has a command named 'go', the above code will be executed in the case of a 'go' request. Typing 'go s', 'go west' and other variations can, however, end up being annoying (if you're lazy), so adding 's', 'west' and similar shortcut commands would easen playing a game. This can be done in the following way:

command = {
    'n': # 'go n' shortcut
"""
txt = 'go ' + cmd[0] # Not really needed
cmd = ['go', cmd[0]] # Fools the 'go' command
exec(command['go']) # Acts as if the program was the user
""",
    # Applying more shortcuts are even easier:
    'ne': "exec(command['n'])"
}

Item-independent commands can also be used to make people laugh.

>default

The default variable contains info on what to do when commands don't seem to exist. For example, it's tiresome to insert a 'get' commands in ever item we create. By using the default variable, we can create an item definition which acts as a shortcut. See below:

default = {
'item': { # Default values for items
    'cmd': {                   # Either 'a', 'an', 'some', etc.
        'look': "p('It\\'s ' + getprefix(citem) + ' ' + item[citem]['name'] +
'.')",  # The exec_proper_command function will find the correct place to look
        # for a command. A hierachy is defined in Zeqy's defs.py.
        'lookat': "exec_proper_command('look', i, i_or_o, citem)",
        
        # The rest is simple shortcut-shortcuts.
        'examine': "exec(default['item']['cmd']['lookat'])",
        'e': "exec(default['item']['cmd']['lookat'])",
        'l': "exec(default['item']['cmd']['lookat'])"
    },
    'out': {
        'get': # All items should be gettable by default.
"""
p('You take the ' + item[citem]['name'] + '.')
inv(citem) # Adds item to inventory
""",
        'take': "exec_proper_command('get', i, i_or_o, citem)",
        'g': "exec(default['item']['out']['take'])",
        't': "exec(default['item']['out']['take'])"
    }
},

# 'item-substitutes' MUST exist. It is used as a reference to a previous item.
# This specific property could just as well have been placed in the var
# variable, but for now it must be in default.
'item-substitutes': ['it'],

# Zita will print a random messages of the list when there is no command match.
'command': "p(randelm(var['imposs']))",

# Code to execute when a load has been succesfully completed. Not strictly
# necessary, but quite useful.
'loaded': "p('Data succesfully loaded.\\n\\n')",

'room': { # Rooms can have default values too.
    # Will be shown if no room description exists.
    'desc': "'You have reached a ' + croom + '.'",
    
    # Will be shown if no item description exists.
    'item': "'You see ' + getprefix(citem) + ' ' + item[citem]['name'] + '.'"
}
}

Using defaults thereby make programming text adventures in Zita much easier.

>extra

Let's have another look at the zeqy directory:

#&pre commands.zt&del functions.zt LICENSE rooms.zt&del defaults.zt&del items.zt&del main.zt&del vars.zt&del #&

The only file we haven't had a look in is the 'functions.zt' file (LICENSE does not include Python code). The functions file contains.. functions. And global ones at that.

def point(p):
    # Adds points
    global var
    var['points'] += p

There is no 'extra' file. The job of the extra variable is to hold information on dynamically created global content. Using the extra_add function, one can save and execute e.g. a function at the same time. While it would be easier to simply define a global function directly, problems would arise when saving and loading the progress in a game, as eventual global functions would not be saved/loaded. By storing them in the extra variable, this obstacle is overcome. The problem is that it gets a little messy in that a lot of backslashes may be needed to escape quote characters.</p>

But because functions in 'functions.zt' are defined when Zita loads a game and not dynamically, those functions do not have to reside inside extra.

Saving and loading progress

Using the built-in save and =code functions, it's quite easy to create a command that keeps track of a player's progress. Saving and loading take place with the Python pickle module.

Footnote

This short manual might not have covered everything there is to say about Zita. The best way to get an impression of Zita is to try it out. It's recommended to use a pre-existing Zita adventure (like Zeqy) as a base.

Prominent languages/engines

While I haven't actually used it, the Adventure Definition Language (ADL) seems to be pretty good. At least it's extensively documented. Other engines naturally exist too.