metanohi/site/projects/zita/index.org

512 lines
19 KiB
Org Mode

#+title: Zita
#&summary
A simple but cumbersome text adventure game
#&
#+license: bysa, page
#+license: gpl 3+, program
#+license: cc0, Zeqy
#&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 [[http://en.wikipedia.org/wiki/Interactive_fiction][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.
[[zita.tar.gz][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 [[http://creativecommons.org/publicdomain/zero/1.0/][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:
#+BEGIN_SRC python2
# 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')
#+END_SRC
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'):
#+BEGIN_SRC python2
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.')"
}
}
}
#+END_SRC
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'):
#+BEGIN_SRC python2
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.')"
}
}
}
#+END_SRC
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'):
#+BEGIN_SRC python2
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.')"
}
}
}
#+END_SRC
Last, but not least, Zita understands the 'pref' (prefix) property:
#+BEGIN_SRC python2
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
}
}
#+END_SRC
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):
#+BEGIN_SRC python2
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
}
#+END_SRC
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:
#+BEGIN_SRC python2
command = {
'steps': "p('You have taken ' + str(var['steps']) + ' steps.')"
}
#+END_SRC
..Though it can also include more complex commands, such as:
#+BEGIN_SRC python2
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:]) + '.')
"""
}
#+END_SRC
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:
#+BEGIN_SRC python2
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'])"
}
#+END_SRC
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:
#+BEGIN_SRC python2
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'] + '.'"
}
}
#+END_SRC
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.
#+BEGIN_SRC python2
def point(p):
# Adds points
global var
var['points'] += p
#+END_SRC</pre>
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 [[http://adl.sourceforge.net/][Adventure Definition Language]] (ADL) seems
to be pretty good. At least it's extensively documented. Other engines
naturally exist too.