19 KiB
Zita
#&summary A simple but cumbersome text adventure game #&
#&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.
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 <span class='code'>include</span> 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 <span class='code'>include</span> 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 <span class='code'>default</span>
# 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?
# <span class='b'>a</span>. dangerous python
# <span class='b'>b</span>. Monty Python
# 'which' MUST exist. It holds what to output when multiple items are
# available. The variable <span class='code'>what</span> holds the current
# object specified by the user. In the above example, <span class='code'>what</span> 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 <span class='code'>goto</span> 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 <span class='code'>goto</span> 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 <span class='code'>exec_proper_command</span> 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 <span class='code'>var</span>
# variable, but for now it must be in <span class='code'>default</span>.
'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.