home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
rtsi.com
/
2014.01.www.rtsi.com.tar
/
www.rtsi.com
/
OS9
/
OSK
/
GAMES
/
informosk.lha
/
manual.txt
< prev
next >
Wrap
Text File
|
1993-11-27
|
148KB
|
3,602 lines
---------------------------------------------------------------------------
Inform: A Compiler of Infocom-Format Games
(third edition)
---------------------------------------------------------------------------
"I will build myself a copper tower
With four ways out and no way in
But mine the glory, mine the power..."
(Louis MacNeice, "Flight of the Heart")
Hello, Informer!
This manual contains four elements. Firstly, it documents Inform, a
program to manufacture Infocom version-3 format story files, which can then
be played on any of the interpreters now widely available on most machines.
Inform has now been fairly heavily re-written, and is quite dependable (at
least on my machine) even on very large game files.
Secondly, it attempts to fully specify the version-3 "Z-machine". Some of
this information is already circulating in other files, but uncollated. The
rest seems only to be available in as much as it is implicit in the
interpreter sources. There seems to be some demand for this, if only among
those tinkering with the interpreters.
Thirdly, it contains articles on how to design games in general, which is
not specific to this format at all. Admittedly I do not always follow my
own dictats, but I think that game implementation bears about the same
relation to design as typing does to writing poetry, and I didn't want
only to talk about the typing.
Fourthly, it documents a suite of standard game routines provided with
Inform to allow designers to begin coding at once. In effect, this means
that an Inform source file need not contain any of the parser code, or the
running of the "game universe" - the library consists roughly of a full
implementation of Zork without any actual puzzles. It manages rooms,
objects, containers, things on top of other things, light, scoring,
switching things on and off, opening, closing and locking things, entering
things, travelling about in them and so forth: it implements about 80 verbs.
The parser it uses (which can be entirely invisible to the designer, or can
be altered if necessary) is about as good as anything Infocom ever had.
(And its source is very heavily commented, so the algorithm may be of
interest even to non-Informers.)
To get a quick look at the language, read parts (1) to (12) and Appendix
C.
---------------------------------------------------------------------------
Prefaces to editions of this manual
---------------------------------------------------------------------------
Historically, Inform was not a wonderfully well-written program: it must
be admitted that I treated it more as an easel than a painting.
But it works, and it runs in only two passes. (This may sound easy but is
not, because the story file format requires all manner of tricky operations
to be done: for example, the dictionary must be alphabetically sorted, and
the code must know absolute addresses of its entries... and the address of
the start of the dictionary depends on many other things not known during
pass 1... and so on.)
It seems also, for what it's worth, to be more efficient than Infocom's
own compiler - perhaps because it compiles from a low C-like rather than
high LISP-like source.
Inform is not public domain in the proper legal sense of the term. The
copyright is retained by the author, Graham Nelson. I am perfectly happy
for Inform to be used by anybody for any recreational purpose. It may be
freely distributed provided no profit is involved, and provided the
copyright message is retained. Please do not circulate heavily modified
versions, and please comment any private changes of your own at the top of
the source code. Story files produced by Inform belong to whoever wrote the
source for them; I think, however, it is fair to ask that game-writers put
some message into their credits saying that Inform was used, and giving the
version number used to compile it.
Notes on the second release:
Since the first release, much improvement has been made in Inform's memory
management which is now quite efficient: it allocates between 50 and 75K of
memory, as opposed to 800K in the first edition. The code is in ANSI C, is
contained in a single file (without needing non-standard headers) and some
effort has been made to improve its portability. Hopefully it doesn't
assume an ASCII character set, or 32-bit integers, or any particular
byte-orientation within integers. PC versions now ought to be feasible.
The code has been annotated to some extent, and contains notes which
should be useful to anyone trying to port the code to a new machine.
This documentation has changed only in the new "objectloop" construction,
and in Appendix C (sample output for the given programs). The language
which Inform compiles has not changed (except that two defunct features,
which had not in any case been documented, have been withdrawn). Details of
changes to the source code of Inform may be found in detailed comments at
its head.
GAN
June 1993
And the third:
The third edition of this manual contains a fair amount of new material.
Most of the changes in Inform are improvements which do not affect the
language (better error reporting, bug fixes, speed improvements, new
compile options, much greater portability across different C compilers).
However, various new features have been added, mainly to provide for needs
which cropped up for the author:
new command line switches; see (1)
new directives "statusline", "release", "include",
"default", "stub"; (2), (18)
string indirection via the synonyms table; (5), (16)
multiple object names; (11), (16)
new constant forms #n$word and #r$routine; (5)
new object alteration commands "write" and "give"; (8)
new command "string"; (8), (16)
new command "font"; (8), (18)
abbreviations, properly using the synonyms table. (17)
Making use of abbreviations slows compilation down, but (when switched on)
can make reasonable memory savings (about 8%).
Inform also now correctly works out the checksum and length fields for the
story file header, which (a) makes a "verify" command easy to implement, and
(b) may make story files work on older interpreters.
One change has been made which is incompatible with earlier editions:
the names of some of the more esoteric debugging directives have been
homogenised.
The text has been revised and clarified in several other places.
GAN
November 1993
The author's email address may be found at the bottom of this file.
Comments and bug reports (by email) are welcomed with whatever degree of
enthusiasm he can muster.
---------------------------------------------------------------------------
Contents
---------------------------------------------------------------------------
Inform
1. Command lines and errors
2. Source file format
3. Compiler directives
4. Variables
5. Constants
6. Routines
7. Expressions
8. Commands
9. Conditions
10. Built-in functions
11. Objects
12. Verbs and grammar
13. Exactly what Inform does with words
14. Indirect function calls
15. Text spacing
16. Drastic alteration of objects
17. Abbreviations
18. The status line
The Z-machine
A1. The Z-machine
A2. How text is encoded
A3. How Z-code is encoded
A4. Using Inform as an assembler
Designing games
B1. A Bill of Player's Rights
B2. What makes a good game?
B3. Writing a Parser
Example Inform programs
C1. A Hello Cruel World program
C2. "Deja Vu": a toy game
C3. The library routines
C4. A shell game to build on
---------------------------------------------------------------------------
1. Command lines and errors
---------------------------------------------------------------------------
If Inform is run without any parameters given, it prints out something like
the following information:
Archimedes Inform 1.0 (v794/at)
Release 3 (November 6th 1993)
(allocating memory for arrays) (temporary files)
This program is a compiler to version-3 Infocom format story files.
It is copyright (C) Graham Nelson, 1993.
Its syntax is "inform [-list] <file1> [<file2>]"
<file1> is the name of the Inform source file; Inform translates this into
"Zcode.<file1>"
(unless <file1> contains a '.' or '/', in which case it is left alone).
<file2> may optionally be given as the name of the story file to make.
If it isn't given, Inform writes to
"Zgames.<file1>"
but if it is, then Inform takes <file2> as the full filename.
-list is an optional list of switch letters following the initial hyphen:
a list assembly-level instructions compiled
b give statistics after both passes
c more concise error messages
d contract double spaces after full stops in text
e economy mode (slower): make use of declared abbreviations
f frequencies mode: show how useful abbreviations are
h print this information
i ignore default switches set within the file
l list all assembly lines
m say how much memory has been allocated
o print offset addresses
p give percentage breakdown of story file
s give statistics
t trace Z-code assembly
w disable warning messages
x print # for every 100 lines compiled (in both passes)
For example: "inform -dex curses ram:curses".
(Don't worry if the version numbers differ slightly on your copy.)
Samples of -s and -p output can be found in Appendix C. For -d, see
section (15). For -e and -f, see (17). -x is useful if running Inform on a
slow machine, since it offers some signs of life. -a, -p and -l are largely
useful to assembly-language programmers and the poor unfortunate obliged to
maintain Inform.
-i overrides any "SWITCHES" directive in the body of the code, so that the
only switches applying are those in the command line.
-m reveals how many bytes were malloc'ed. The program can be compiled in
several different versions, with varying memory needs, and porters might
need to use this.
The filenames Inform uses for input and output will obviously depend on what
machine it runs on. The Archimedes conventions are those above, but, for
example, the Unix version compiles files like "dejavu.inf" with headers
like "parser.h" to "dejavu.z3".
As Inform runs, it may come up with warnings, errors or fatal errors.
Here is a typical sample:
Archimedes Inform 1.0 (v794/at)
line 10: Error: Symbol name expected
> global
line 12: Error: The "Main" routine is not allowed to have local variables
> [ Main i
line 12: Warning: Local variable unused: "i"
line 23: Warning: Local variable unused: "j"
Compiled with 2 errors and 2 warnings (no output)
(Infocom interpreters can crash horribly when given incorrect files, so
Inform never writes a file which caused an error, though it will allow
files causing warnings only.)
-c (concise) mode doesn't quote from the source files; -w turns warnings
off.
The exchange rate is 100 errors to the fatal error: i.e., after 100 errors,
Inform gives up altogether.
---------------------------------------------------------------------------
2. Source file format
---------------------------------------------------------------------------
Lines in an Inform file are terminated by semicolons. Exclamation marks
! thus...
denote that the rest of that physical line is a comment. Backslashes "fold"
strings up, so that for example
initpos "A hinged trapdoor in the floor stands open, and light streams in \
from below."
is treated as if the "f" in "from below." follows directly from where the
backslash \ is; i.e., the carriage return and leading spaces are removed.
Inform command names are not case sensitive, and nor are variable names.
However, reserved words after the initial command (such as the "to" in a
"for" construction) must be in lower case.
There are seven kinds of line:
Examples
labels .Label;
routines start and stop [ NewRoutine i j;
directives #Release 4;
assignments fred = parent(lamp);
function calls Verify();
compiled commands for i 1 to 10 { Step(i); }
assembly language @prop_len_addr lamp lv;
In practice, you don't need to know assembly language, but it's there.
---------------------------------------------------------------------------
3. Compiler directives
---------------------------------------------------------------------------
Directives are instructions to Inform which do not themselves make code.
They can be prefaced by a # character, as in C, but need not be.
ABBREVIATE <string> Declare abbreviation (see (17))
ATTRIBUTE <name> Make new attribute flag
CONSTANT <name> <value> Declare a constant
DICTIONARY <name> <text> Enter <text> in dictionary, and make
a new constant for its address
END End compilation here (this is optional)
GLOBAL <name> [ = <a> ] Make a new global variable;
[give it the initial value a]
[ string <a> ] [make it point to an (a+1)-byte array,
which has <a> as first byte, and is
otherwise zeros]
[ data <a> ] [make it point to an a-byte array,
which is all zeros]
[ initial <i1> ... ] [make it point to an array, the bytes
of which are as given]
[ initstr "text" ] [make it point to an array, the bytes
of which are the ASCII values of the
characters in the string]
INCLUDE <filename> Include a file in the source
OBJECT ... Make an object (see below)
PROPERTY ... Make a new property (see below)
RELEASE <a> Set the release number to <a>
SERIAL <string> Set the serial number to <string>
(it must be a six-digit number, and
defaults to the date in the form
YYMMDD if Inform has access to
today's date, or to 930000 if it
hasn't)
STATUSLINE score Make the status line show score/turns
time ...show hours/minutes
SWITCHES <switches> Declare default switch settings
(eg: SWITCHES dexs
causes "inform filename" to be read as
"inform -dexs filename")
VERB ... Enter a line of grammar (see below)
The following are mainly for debugging the compiler (should anyone ever
get around to doing this) but might sometimes be amusing or helpful:
LISTSYMBOLS List the symbol table
LISTDICT List the dictionary
LISTOBJECTS List the object tree
LISTVERBS List the verb table
(the names of which have changed from earlier releases),
TRACE Trace assembler
LTRACE List the lines of input
ETRACE Trace expression evaluator
BTRACE Trace assembler on both passes
NOTRACE, NOLTRACE, etc Turn off appropriate tracing
And there are two more rather technical directives:
DEFAULT <cname> <value> If the constant has not yet been
defined, define it with this value
STUB <rname> <n> If the routine has not yet been
defined, define one which has n local
variables and always returns false
---------------------------------------------------------------------------
4. Variables
---------------------------------------------------------------------------
There are two kinds of variable, global and local (plus one special one).
Variables are all two-byte integers, which are treated as signed when it
makes sense to do so (eg in asking whether one is positive or not) but
not when it isn't (eg when it is used as an address).
There can be up to 240 global variables; as indicated in (3), these can be
initialised to point to dynamic workspace, so as to achieve the effect of
strings and arrays. They have to be declared before use. For instance:
Global turns = 1;
Global buffer string 120; ! Buffer holding 120 characters
Global task_scores initial 4 5 9 1 2 3 0;
In any routine, there can be up to 15 local variables. These are declared
when the routine begins. (There is one exception: the special Main routine
may not have local variables.)
There is also a stack, but it should be tampered with only with care. Never
call a variable "sp", as this is the stack pointer variable which you might
occasionally need to use.
The observant reader will have noticed that 240+15+1 = 256. This is of
course no coincidence.
---------------------------------------------------------------------------
5. Constants
---------------------------------------------------------------------------
Constants may be prefixed with a # character if desired. This can be useful
if they are alphabetical and might otherwise be confused with something else.
A constant in "double quotes" assembles the given text at a suitable (even)
address, and gives half this address as the integer value. Inside this text
the character ^ is replaced by a newline character, and the character ~ by
a double-quote mark. In practice you seldom need to worry where the text
is stored, or how.
(New in Release 3) Inside a string, @dd (an @ sign followed by two decimal
digits) is compiled to the synonym of that number. When the Z-machine
finds this, it prints the string pointed to by that entry in the synonym
table. (This is useful in altering object short names - see section (16).)
A character in single quotes, such as 'e', means the ASCII value of that
character. (This is true even on machines not using ASCII, of course.)
A dollar $ indicates that a hexadecimal constant follows; $$ indicates that
binary follows.
Any constant declared in a directive can be quoted, and so can the special
constants
adjectives_table
preactions_table
actions_table
(set up by Inform) which give the code address of these tables.
A constant beginning a$, followed by the name of a routine which is an
action routine, will have as value the number of the action.
A constant beginning w$, followed by a word of text, has as value the
address of the given word in the dictionary (Inform will give an error
at compile time if no such word is there).
(New in Release 3) A constant beginning n$, followed by a word of text,
has as value the address of the given word in the dictionary (Inform
adds it to the dictionary as a new word if it is not already there).
(New in Release 3) A constant beginning r$, followed by a routine name,
gives (half) the address of the given routine. (This is very helpful
when altering properties of objects which are routine addresses.)
Thus, for instance, the following are legal constants:
31415
$ff
$$1001001
#adjectives_table
#a$LookSub
#w$invent
'X'
"an emerald the size of a plover's egg"
"~Hello,~ said Peter.^~Hello, Peter,~ said Jane.^"
#r$FireRodRoutine
#n$amazon
Unfortunately, at present negative constants are not allowed, and Inform
will reject, say, #-50. In practice they are only very occasionally
needed, and can of course always be got by, say, 0-50.
---------------------------------------------------------------------------
6. Routines
---------------------------------------------------------------------------
The syntax to begin a routine is
[ RoutineName <l1> ... <ln>;
and to end it, is
];
l1 to ln are the names of local variables, which are also the call
parameters. For example, if you have a routine
[ Look i j k;
...some code...
];
and it is called by
Look(attic);
then i will initially have the value "attic" when this is executed.
Any local variables not specified (in this case, j and k) are initially
zero. It should be emphasized that it is legal to call Look with 0, 1,
2 or 3 arguments. (Three is the maximum number of arguments any routine
can have.)
Every routine returns a value to the caller; if no such value is
explicitly given, this value is the integer 1 ("true"). In a line like
Banner();
the return value is thrown away.
Inside a routine, labels may be declared with a line of their own:
.labelname;
but note that whereas local variables have names which only mean anything
locally, labels have names which are global. In other words, you can't
have a label called "loop" more than once in the file. (It is legal to
jump from one routine to a label inside another one, but extremely
dangerous.)
There is one special routine, which you must define, called Main. This is
where execution of the game will begin, and it _must_ be the first one
defined. Returning from Main will cause the interpreter to crash: you
should explicitly use the "QUIT" instruction instead. Also, uniquely and
for peculiar reasons, Main is _not_ permitted to have any local variables
of its own. This means it is usually only used as an outer shell.
(Inform issues a warning if the earliest defined routine is not called
"Main".)
---------------------------------------------------------------------------
7. Expressions
---------------------------------------------------------------------------
The usual arithmetic expressions are allowed, including the operators:
= set variable (only) on left equal to value on right
+ - plus, minus
* / % & | times, divide, remainder, bitwise and, bitwise or
-> --> byte, word array entry
(eg: buffer->4 gives contents of the byte with address
buffer+4, while table-->3 gives the word at table+6)
In addition one may call a function, either a built-in function or a
routine.
For example:
4*(x+3/y)
i=j-->1
Fish(x)+Fowl(y)
[Note: in earlier releases, Inform used to be unable to cope with
many complicated expressions used in the same command, such as
put buffer+6 byte i+j+1 56*prime(4);
...but now it can.]
---------------------------------------------------------------------------
8. Commands
---------------------------------------------------------------------------
The "high level" commands in Inform are as follows:
NEW_LINE Print a carriage return
SHOW_SCORE Redisplay the score bar immediately, without
waiting for the next keyboard input
PRINT "text" Print text
PRINT_RET "text" Print text, print a newline and return true (1)
PRINT_NUM <a> Print a as a (signed) decimal number
PRINT_CHAR <a> Print the character whose ASCII value is a
PRINT_ADDR <a> Print the string whose address is a
PRINT_PADDR <a> Print the string whose address is 2*a
PRINT_OBJ <a> Print the short name of object a
REMOVE <a> Remove object a from the tree of objects
(it may certainly be later put back)
MOVE <a> TO <b> Add object a to the things possessed by b
WRITE <object> <p1> <v1> [<p2> <v2>...] Set properties of the given
object to the given values. (This is intended
to replace the more primitive PUT_PROP: it can
accept more complicated expressions, and handle
more than one property at a time.)
GIVE <object> <a1> [<a2>...] Give the object the quotes attributes. An
attribute beginning with ~ is cleared instead.
(This is intended to replace SET_ATTR and
CLEAR_ATTR.) Thus, for instance,
give lamp light ~open container scored;
PUT <addr> BYTE <index> <v> Write byte value v into index'th byte after addr
PUT <addr> WORD <index> <v> ...and similarly for words
INC <var> Increment variable
DEC <var> Decrement
RETURN Return (actually, return true, i.e. 1)
RETURN <a> Return the value a
RTRUE Return true, i.e. the value 1
RFALSE Return false, i.e. the value 0
(These used to, and still can, be called
"ret#true" and "ret#false".)
INVERSION Prints (in the game, not at compile time)
the version number of Inform used to
compile the story file
IF <condition> If the condition is true, execute the code
{ ... code ... } (braces are _compulsory_) [else execute the
[ ELSE { ... other ...} ] other code instead]
WHILE <condition> While loop
{ ... code ... }
FOR <var> <init> TO <final> For loop: the final value must be a constant
{ ... code ... } or another variable. If the range is empty, it
does not execute even once.
DO Until loop
{ ... code ... }
UNTIL <condition>
OBJECTLOOP <var> FROM/IN <obj> A form of while loop. The var first holds
either the obj value (if it is FROM) or its
child (if IN), and runs through the sibling
objects. So, for instance,
objectloop x in lamp { print_obj x; new_line; }
is equivalent to
x=child(lamp); while x~=0 { print_obj x; new_line;
x=sibling(x); }
BREAK Break out of current loop (not block)
JUMP <label> Jump to label (warning: exercise caution in
jumping out of one routine into another)
STRING <n> <text> Set the nth indirect string to text (see (16))
FONT on Turn proportional fonts on/off (see (18))
off
The remaining commands need not be used if the library is being included:
QUIT Quit the game (at once, with no confirmatory
question to the user)
RESTART Restart the game from its initial state (ditto)
SAVE <label> Try to save the game (asking the user for a file
to put it in): if successful, jump to the label,
otherwise carry on
RESTORE <label> Ditto, but restore
VERIFY <label> Ditto, but verify that the game is intact
READ <a> <b> Reads keyboard into buffer a and decomposes it
to the buffer b:
on entry, a[0] = buffer size, b[0] similarly
on exit a[1] = no chars typed,
2 to a[1]+1 are the chars (unterminated)
From byte 2, b contains 4-byte chunks, one for
each word of input:
address of dictionary entry if recognised,
0000 otherwise
number of letters in word
first char of word in a
This command automatically redisplays the status
(score) line.
If a command matches none of these, or if it began with an @ character, the
line is sent to the assembler instead. Some of the assembler opcodes are
fairly usable, but the essential features of the Z-machine can be got at
with just the high-level commands and functions.
---------------------------------------------------------------------------
9. Conditions
---------------------------------------------------------------------------
These take the form
<a> <relation> <b>
where the relation is one of
== a equals b
~= a doesn't equal b
< > >= <= comparisons
has object a has attribute b at the moment
hasnt ...hasnt...
near objects a and b have the same parent
far ...haven't...
These may _not_ be used in expressions (as if the language were C) and
there is no AND/OR construction. There is a reason for this, but not a
very good one (unless you count laziness). However, one concession
towards such a feature is provided, viz. the useful construction
<something> == <v1> [or <v2> [or <v3>]]
which is true if the first something is any of the values given.
---------------------------------------------------------------------------
10. Built-in functions
---------------------------------------------------------------------------
The built in functions are
PARENT(obj) SIBLING(obj) CHILD(obj)
for reading the object tree (see (11) below), together with
RANDOM(x)
which returns a uniformly random number between 1 and x, and
PROP_LEN(addr) PROP_ADDR(o,p) PROP(o,p)
for which see (11) below.
Warning: some interpreters set up their random number generator with poor
choices of seed value, which means that the first few random numbers may be
rather peculiarly distributed. After a time, it settles down. To get
around this, "Curses" (for example) takes and throws away 100 random numbers
when it begins.
---------------------------------------------------------------------------
11. Objects
---------------------------------------------------------------------------
The object hierarchy is a tree of up to 255 "objects", which you might use
for many different game elements: rooms, compass points, scenery, things
which can be picked up, and so on.
They are numbered from 1 to 255, and the number 0 by convention means
"nothing". Attempting to print_obj object 0 will produce a string full of
peculiar letters and (if you are very unlucky indeed) even random ASCII
values followed by an interpreter crash.
In the tree, each object has a parent, a sibling, and a child. Thus, for
instance, a portion may resemble
Meadow
|
Mailbox -> Player
| |
Note Sceptre -> Cucumber -> Torch -> Magic Rod
|
Battery
in which -> shows siblings, and | parents and children. In this case, the
Meadow has nothing as its parent. Anything with no possessions, such as the
note, has nothing as its child, and so on.
When an object is moved, its possessions move with it, of course.
In practice an object needs rather more data than just a position in a tree.
It also has a collection of variables attached to it.
Firstly, there are 32 flags, called "attributes", which can be either set or
clear. These might be such conditions as "giving light", "currently worn"
or "is one of the featureless white cubes". All 32 are free for the
programmer to use (though the Library routines, if in use, consume many of
them). They must be declared before use, by directives like
ATTRIBUTE locked;
which will allocate a new attribute and make a constant "locked" to have the
value of its number. You never then need to know about these numbers,
because you can use commands like
IF obj has locked { print_ret "But it's locked!"; }
GIVE obj locked;
Warning: 32 sounds like plenty, but the limit can quite easily be hit. The
author has found it useful to declare one as "general", to be used for
different things for different objects. (And this is done for you in the
library.)
Secondly, there are 30 "properties". These are far more elaborate. For one
thing, not every object has every property. The following all declare new
properties:
PROPERTY door_to;
PROPERTY article "a";
PROPERTY blorpleroutine $ffff;
The value given, in the case of article and blorpleroutine, is the default
value: that is, the value of the property which an object will have if it
doesn't explicitly have some other value. If you don't define a default
value, it will by default be 0.
So, for instance,
PROP(frog,timeleft);
will return 0 if "frog" has no timeleft entry.
The data for a given property can be a number, or up to four numbers in a
row, or up to eight bytes of data. The simplest way to get at the current
value is something like
i=PROP(location,door_to);
which will get the first number in the property door_to of object location.
Similarly, it can be written to with
WRITE location door_to hall_of_mists;
A subtle point is that numbers smaller than 256 are stored differently from
larger ones. In order to decide whether the property is one byte's worth or
two, the Z-machine looks at the number of bytes which the property has in
all, and sees whether it is odd or even; if even, it presumes the number is
a 2-byte word; if odd, it presumes it is just one byte.
This is seldom something you need to know about, but occasionally you will
want a property which will, later in the game, need to hold a value of, say,
1000, but which initially will be zero. This is particularly the case with
timing mechanisms, for instance. The command
PROPERTY LONG timeleft;
declares the property "timeleft" and requires Inform to make sure that all
"timeleft" fields are 2 bytes wide, even if they have small initial values.
More elaborate manipulation has to be done by hand.
k=PROP_ADDR(o,weird);
sets k to the address of the "weird" data of object o. To find out how many
bytes there are, apply PROP_LEN to this address.
l=PROP_LEN(k);
Once you have the address you can read and write to it directly. Be careful
not to overrun the length, which may not be changed.
Warning: the Z-machine crashes if you attempt to write to a property field
which an object hasn't got. So although you can read an undeclared property
(you just get the default values), you can't write to one.
An object is declared (before the body of the code) by something like:
OBJECT trapdoor "hinged trapdoor" attic
with name "hinged" "trap" "door" "trapdoor",
initpos "A hinged trapdoor in the floor stands open, and light \
streams in from below.",
closedpos "There is a closed trapdoor in the middle of the floor.",
portalto house,
postroutine TrapdoorPost,
dirprop d_to
has portal static open light openable;
trapdoor is a constant which is set to its object number; "hinged trapdoor"
is its attached short name; attic is the object which initially possesses
it. If it was to be initially unowned, this would be "nothing" instead of
"attic".
After "with" is a list of property definitions, in the form
<property> <data1> ...
[[, <property> <data1> ...]]
Warning: an excellent source of mysterious errors is missing off the commas
between these, since property names are themselves legal constants.
There is one special property, called "name". Its data must be (up to four
at most) words, as above, and these are entered into the dictionary as nouns
(if they aren't already present): the data actually stored is the dictionary
addresses.
Note that the dictionary itself does _not_ know that "door" refers to this
object: there might be any number of objects which could be called "door".
After "has" is a list of attributes which the object initially has.
(New feature in Release 3) You may give an object more than one internal
name, thus:
OBJECT frog tree brick "frog" attic
with ...;
after which the same object can be called frog, tree or brick within the
source code: in other words, several constants are created with the same
value. Why on earth should you want this? - See section (16).
---------------------------------------------------------------------------
12. Verbs and grammar
---------------------------------------------------------------------------
Whereas objects should be declared at the start of the file, the grammar
to be allowed by the game should be declared at the end. This is done with
the VERB command. VERB does something very complicated, but probably not
what you think. A typical VERB command would be:
VERB "take" "get" "pick" "lift" * "out" -> ExitSub
* multi -> TakeSub
* multiinside "from" noun -> RemoveSub
* "in" noun -> EnterSub
* "off" held -> DisrobeSub;
This declares a verb, for which "take", "get" etc are synonyms, and which
can take five different courses. In the first, it must be followed by the
word "out". In the last, it must be followed by "off" and then an item
which is currently held by the player. In the second, it can be followed by
one object, or a list, perhaps specified as "everything", for instance.
There can be no grammar at all, for example
VERB "invent" "i" * -> InvSub;
After the "->" is the name of a routine which is to be called when this is
matched.
For traditional reasons unclear to the author, previous Infocom hackers have
called words such as "out" and "off", adjectives. This is monstrously
illiterate since they are of course prepositions. We shall wearily follow
convention anyway.
Remember that the Z-machine does _not_ contain the bulk of a game parser,
only the computationally expensive and low-level part which works out what
the words are. So this command only sets up a table with some numbers in.
If you want a parser, you have to write code to deal with the table again.
If you're using the library routines, the parser is all done for you and
the possible tokens are:
Word What the library parser uses it for
==== ===================================
noun any visible object
held object held
multi one or more visible objects
multiheld one or more held objects
multiexcept one or more objects, except the other object
multiinside one or more objects, inside the other object
creature an animate creature
special any single word or number
Look through the library's grammar table for examples.
(New in Release 3) If a verb is declared as a meta-verb, e.g. via
VERB meta "score"
* -> ScoreSub;
then the parser will treat it as outside the game - taking no time up, and
possible at any moment.
---------------------------------------------------------------------------
13. Exactly what Inform does with words
---------------------------------------------------------------------------
This is a very technical section about exactly how Inform deals with the
grammar table and the dictionary. It can safely be ignored by anyone
using the library routines supplied, and in fact since the remaining
sections of the manual proper are quite specialised, the next part to
read is probably Appendix C.
By convention, adjectives are numbered downwards from $ff. Thus, if
the above were the opening lines of grammar, "from" would be $fe, and so on.
As they are created, they are entered into the dictionary, and also into the
adjective table, which has four-byte entries
<dictionary address of word> 00 <adjective number>
----2 bytes----------------- ----2 bytes-----------
In order to make life more interesting, these entries are stored in reverse
order (i.e., lowest adjective number first). The address of this table is
rather difficult to deduce from the file header information, so the constant
#adjectives_table is set up by Inform to refer to it. In any event, the
table isn't very useful and is created only for the sake of conforming to
Infocom internal conventions.
The important tables are the grammar and action tables.
The grammar table address is stored in word 7 (ie bytes 14 and 15) of the
header. The table consists of a list of two-byte addresses to the entries
for each word. This list is immediately followed by these entries, one
after another.
An entry consists of one byte giving the number of lines (eg, 5 for the
"take" definition above) and then that many 8-byte lines. These lines
have the form
<objects> <sequence of words> <action number>
--1 byte- ----6 bytes-------- --1 byte-------
<objects> is the number of objects which need to be supplied: eg, 0 for
"inventory", 1 for "take frog", 2 for "tie rope to dog". The sequence
of words gives up to 6 blocks of syntax to follow the verb, which must
be matched in order. Large numbers such as $ff mean that the appropriate
adjective must appear; small numbers are inserted by special words such as
"held" or "noun" in the VERB command:
Word Byte
==== ====
noun 0
held 1
multi 2
multiheld 3
multiexcept 4
multiinside 5
creature 6
special 7
The sequence is padded out to 6 bytes with zeros.
The action numbers begin at 0. The first routine mentioned as an action (in
the above example, ExitSub) is assigned action number 0; the next (TakeSub)
is given 1, and so on. The appropriate number is stored in the last byte of
the line.
Thus, a little later on in the grammar, the line
VERB "exit" "leave" * -> ExitSub;
might well appear, and ExitSub will mean "action 0" as before.
So this table does not store the address of the action routine, as one might
expect. Instead the addresses corresponding to the action numbers are
stored in the actions table. Once again, Inform puts this table in its
conventional place, but this address being difficult to work out, the
constant #actions_table is set up to hold it. The actions table is simply
a list of 2-byte entries giving the routine addresses (divided by 2).
There is also a preactions table, with another constant #preactions_table,
created only to conform to Infocom conventions; it is set up containing 0000
for each action. ("Curses", for instance, makes no use of this.)
In the mean time, what has happened to the actual words, "take", "get",
"pick" and "lift"? Note that these do not appear in the grammar table at
all. Instead they are entered into the dictionary, along with the verb
number. As a final baroque twist, these numbers also count down from $ff.
Any number of words can be given, all referring to the same verb number;
"Curses" has 11 synonyms for "attack", for instance.
Of course, Inform does not know or care what is done with any of these
tables. For instance, the "take" verb has the entry
005
000 255 000 000 000 000 000 000
001 002 000 000 000 000 000 001
002 005 254 000 000 000 000 002
001 253 000 000 000 000 000 003
001 252 001 000 000 000 000 004
but it is up to the code you write to deal with this. (The VERBS command
will print out the full verb table in a similar format.)
This section describes what Inform does with the dictionary. Again, if
you use the parser supplied, you needn't know this.
The fourth word of the file header (bytes 8 and 9) contain the dictionary
table's address.
The table begins with a 7-byte header:
03 '.' ',' '"'
meaning there are three characters used to separate words in typed input,
full stops, commas and quotation marks. (The Z-machine will allow any list
to be given here but Inform decides on this for you.)
07 <number_of_entries>
----2 bytes--------
meaning there are that many entries in the dictionary, all 7 bytes long.
(This could again be in principle varied, but allows for six significant
letters in words, while still enabling the text of the word to occupy a
4-byte integer - which is convenient and fast when the compiler is
alphabetically sorting.)
The seven-byte entries are in alphabetical order, and look like:
<the text of the word> <flags> <verb number> <adjective number>
----4 bytes----------- --1 b-- ----1 byte--- ----1 byte--------
The text is stored in the usual text format, thus allowing up to 6
characters. The flags (chosen once again to conform loosely to Infocom
conventions, not for any sensible reason) have the eight bits
7 6 5 4 3 2 1 0
<noun> .. .. .. <adj> <spec> .. <verb>
<verb>, <noun> and <adj> mean the word can be a verb, noun or adjective; the
<spec> bit means the word was inserted by a DICTIONARY command in the
program, except that <verb> words also have the <spec> bit set (ours not to
wonder why).
(Release 3 of Inform also makes use of bit 1 above to indicate which verbs
were declared as "meta": the parser can use this to see how to treat a
verb.)
Note that a word can be any combination of these at once. It can even be
simultaneously a verb, adjective and noun.
Typically a full game contains about 600 dictionary entries - about ten
times the number of portable objects. Even so it only consumes about 4K, or
1/64th of the available memory. It's never worth economising on dictionary
entries; nothing else a designer can do with 4K will be as goodáto the user.
---------------------------------------------------------------------------
14. Indirect function calls
---------------------------------------------------------------------------
Occasionally one needs to call a function whose address is in a variable:
for example, if the routine address has been looked up from a table, or an
object's property list.
For this, the function "indirect" is provided:
a=indirect(b);
sets a to the return value of calling the function whose address is in b.
If you want to pass arguments as well, you should use the assembler-level
@icall. But do so with care: it is dangerously easy to leave values lying
about on the stack, which will overflow causing a mysterious crash
hundreds of turns later.
---------------------------------------------------------------------------
15. Text spacing
---------------------------------------------------------------------------
Typewritten English, like this file, normally puts a double space after a
full stop. This is much easier to read. Unfortunately Infocom-standard
interpreters do not usually understand that. When they fold text across
lines, they can easily turn
...and a pomegranate. After all, you always hated fruit.
into something which looks like
|You decline the offer of a banana, an apple and a pomegranate. |
| After all, you always hated fruit. |
| |
|> |
which looks awful. It would be easy to fix the interpreter not to do this;
but nobody does. In case (like the author's) your typing is habitually
double-spaced, Inform provides a command line option -d to change it back
again. It does this only by replacing the string ". " by ". " in text
conversion.
---------------------------------------------------------------------------
16. Drastic object alteration
---------------------------------------------------------------------------
In earlier versions of Inform, there were some aspects of an object
difficult to change, once set. Firstly, the "short name". If you
declared an object as, say,
OBJECT frog "little green frog" attic
WITH ...
then the game would always refer to it as "little green frog": this would
be impossible to alter if, for instance, it should in some magical way
become an enormous green frog.
A sneaky way around this is to use string indirection. Declare it as
OBJECT frog "@00" attic
WITH ...
so that, when the Z-machine does a print_obj on it, it prints out the
string entered 0th in the synonyms table. In your initialisation code,
write your own string here, by:
STRING 0 "little green frog";
(which actually compiles simply to:
PUT $0042 WORD 0 "little green frog";
$0042 is the address of the synonyms table; setting word n changes the
string printed in place of @n.)
Then at any time you can amend the name by
STRING 0 "enormous, slavering green frog";
@00 to @25 are available. (Counting in decimal, not hex.)
(This system also provides an elegant way of dealing with bottles and
containers of water in general, say: "full beer bottle" can become
"half-empty beer bottle" and then "empty beer bottle". (But if so,
remember also to change its indefinite article from "a" to "an".))
Secondly, properties which pointed to game routines were tricky to
set for very complicated reasons to do with how constants are translated.
Suffice to say that one can now do this by, e.g.
WRITE frog preroutine #r$EnormousFrogPre;
or
WRITE frog preroutine #r$LittleFrogPre;
Thirdly, it was difficult to change the dictionary entries recognised
as referring to an object. Well, it still is, but here's how it's done:
Create your object with the name field containing dummy entries, e.g.
OBJECT frog "@00" attic
WITH name "zzzzzz" "zzzzzz" "zzzzzz",
...;
and then initially set these by
x=prop_addr(frog, name);
put x word 0 #n$little;
put x word 1 #n$green;
put x word 2 #n$frog;
then alter them by
x=prop_addr(frog, name);
put x word 0 #n$enormo;
(Warning: if there are only three entries in the name property list, as
here, then the Z-machine will crash if you try to write to the fourth: so
make sure there are enough dummy entries when you create the object.)
It is thus possible to change absolutely every aspect of an object. The
virtue of this is that, as Richard Tucker pointed out to the author, the
limit of 256 objects ceases to be a limitation if you can recycle them.
It takes careful coding, but these methods allow that recycling to take
place.
This is why multiple internal names are now allowed for an object:
OBJECT frog brick herring "@00" attic
WITH name "zzzzzz" "zzzzzz" "zzzzzz",
...;
will allow the same object number to be called frog, brick or herring by
different routines which deal with different incarnations of the same
object.
---------------------------------------------------------------------------
17. Abbreviations
---------------------------------------------------------------------------
When the game becomes full, about 10K out of the 128K length can be saved by
making use of text abbreviations: a method under which up to 64 commonly
occurring phrases can be abbreviated whenever they occur.
This makes no difference to the text as seen by the player.
Because checking for these causes a speed overhead, and isn't worthwhile
until the game is about 95% full, Inform does not do so except in economy
mode (compiling with the switch -e on).
An abbreviation must be declared explicitly, before any other text appears,
by a directive such as:
ABBREVIATE "the ";
Only 64 may be declared (note for experts: the remaining 32 slots in the
synonyms table are allocated for variable strings, see (16)).
(This causes "the " to be stored internally as only 2 text chunks, rather
than 4, whenever it occurs: which is very often.)
Obviously, to get maximum gain one must make sensible choices. A reasonable
start is:
"they " "the " "The " "You " "you " "you're " "and " "but "
"is " "There " "can't " "of " "to " "with " "are " "that "
"have " "which " "hrough " "in " "here " "into " "It " "east"
"west" "north" "south" "not " "an " ", " "thing" "door"
"she " "he " "above " "below " ".~" ",~ " "***" "if "
"If " "it " "might " "could " "up " "down " "back " "out"
"What " "Why "
plus a few specific to the game. To see how good these abbreviations are,
try compiling with the -f (frequencies) option set, which will count the
number of times each abbreviation is used, and work out how many bytes it
saved. For instance, "the " occurs some 1200 times in "Curses". One soon
sees that parts of speech and words like "there" make big savings, but that
almost any proper noun makes hardly any difference.
---------------------------------------------------------------------------
18. The Status Line
---------------------------------------------------------------------------
One of the most distinctive features of Infocom games in play is the status
line, the (usually highlighted) bar across the top of the screen. The
interpreter automatically prints the current game location, and either the
time or the score and number of turns taken.
The status line is redisplayed at least (a) on a SHOW_SCORE command and
(b) each time the game asks the player to type something. It may be
displayed even more often, at the whim of the interpreter.
The place name is the short name of the object whose number is held in
global 0 - the earliest global declared in the file. If ever this global
holds the value 0, the name displayed will become corrupted and the game
may crash.
The next two globals are also used.
By default, these show the score and number of turns taken so far, usually
in the form "4/87". (Interpreters vary in how they print these.)
However, if the file contains the directive
Statusline time;
then they are treated as the time in hours and minutes. For instance,
1 and 32 would come out as "1:32 am".
It is up to the program to adjust these variables as time passes, score
is gathered, location changes, etc. (Though as usual, the library will
take care of some of this work.)
Another miscellaneous point: on some machines, text will by default
be displayed in a proportional font (i.e. one in which the width of a
letter depends on what it is, so for example an i will be narrower than
an m). If you want to display a diagram made up of letters, you will have
to turn this off, for which the "font" command is provided:
font off;
print " +---+^ | A |^ +---+^";
font on;
for example. (Remember to turn the font back on afterwards!)
On a machine not using proportional fonts, these have no effect.
---------------------------------------------------------------------------
A1. The Z-machine
---------------------------------------------------------------------------
The so-called Z-machine (the imaginary machine for which story files are
programs) is quite well-adapted to its task. It maintains a hierarchy of
objects and possessions, and does the computationally-intensive part of
parsing input itself. That said, it does not contain the bulk of the
parser. The parsing tables which some investigators think are part of the
Z-machine format, are in fact the same across different Infocom games only
because they all contain essentially the same parser code. Thus, Inform is
in principle free not to compile such tables, but it does so in order to
placate other people's programs (such as INFODUMP). Some tables are put to
subtly different uses, however.
The following description is fairly complete, but only covers version 3.
It would be helpful if someone public-spirited would write an account of the
differences in later versions.
The version 3 Z-machine is 128K long at most. Addresses within it are
nonetheless held in 2-byte words, which is why some addresses are stored as
half their actual values, and why some items (routines and static strings)
are always stored at even addresses.
The first 64 bytes contain a header. The first 4 bytes are:
03 <Flags> <Release Number>
----2 bytes-----
3 indicates version 3; the release number is as set in the program; the
flags byte contains bits:
1 Status line type (clear for Score/Turns, set for Hours:Mins)
3 Censorship bit (used by some games, but not by the Z-machine)
4 Alternative prompts - sometimes used by primitive interpreters
5 Status window support - used only by "Seastalker"
Next come seven word addresses, at words 2 to 8:
2 <Start of Routines> Where routines begin, in bytes
3 <Main Routine> Address of main routine, in bytes, +1
(This +1 is why Main cannot have local variables - it is a peculiarity
of the standard. Note also that this is uniquely a routine address in
bytes and not words: Main must occur in the lower 64K of the file. Inform
always sets word 3 to be word 2, plus 1.)
4 <Dictionary> The dictionary table address, in bytes
5 <Object tree> Object table address, in bytes
6 <Variables> Global variables address, in bytes
7 <Save area size> The total number of bytes in a saved game
(Saving the game is done by saving this many bytes from the beginning of
the machine. (Saved games also contain the current state of the Z-machine
stack; the stack is _not_ stored anywhere in the Z-machine's memory.))
8 <More flags>
This word of flags has bits:
0 Scripting on: send output to printer
1 Disable proportional fonts while this is set
4 Something mysterious to do with sound effects in The Lurking Horror
This is followed by the six bytes from byte 18 to 23, which are the version
number string. (Inform sets these to the current date, in the form YYMMDD.)
Then more words:
12 <Synonyms table> Synonym table address in bytes
13 <Length> Length of file, in words
14 <Checksum> Sum of bytes from 64 upwards, mod $10000
(The length and checksum are not actually used at all by many interpreters,
but are set by Inform anyway. Inform also pads out the file to an exact
number of 512-byte blocks with zeros, since some interpreters still make
use of swapping blocks in and out, virtual-memory style.)
The remaining bytes in the header are used by the interpreter and should be
left alone by the game code.
We are now at $0040 and by convention we reach the synonyms. Usually, the
actual strings (the expansions of the synonyms) are stored here, one after
another, making up 96 strings. When that is out of the way, the actual
table begins (and this is what the synonyms address points to). The table
contains 96 two-byte entries, giving the word addresses of the strings
before it.
(Since Inform only uses synonyms in an unorthodox way, it actually puts
a single dull string " " (three spaces) at $0040, and then makes a table
of 96 pointers to it, starting at $0042: finally it fills in those
abbreviations declared explicitly by the user, into the latter 64 slots,
reserving the first 32 for variable usage. Note that Inform does not
write the synonym expansions declared by the user at $0040: but then there
is no requirement to.)
Next is the object table. In fact it begins with what is sometimes called
the "global properties table", though it is actually a table of default
values of properties. This is a list of 31 2-byte words. There is no
property 0, so the first word is always 0000.
(Inform also sets the default for property 1 - the special "name" property -
to 0000; the remainder are set in property definitions.)
After these 62 bytes, the objects begin, beginning from object 1. An object
entry consists of 9 bytes, looking like:
<the 32 attribute flags> <parent> <sibling> <child> <properties>
---32 bits in 4 bytes--- ---3 bytes------------------ ---2 bytes--
The last three bytes are 00 when the object pointed to is "nothing". The
<properties> is an address (in bytes) of the properties attached to the
given object.
When all these 9-byte entries are out of the way, the properties tables
begin. (Inform keeps these in the same order as the objects they are
attached to.) An individual property table has the brief header
03 <text of short name of object>
--some even number of bytes---
and then lists the properties held, in descending numerical order. (This
order is essential.) A property is stored as
<size byte> <the actual property data>
---between 1 and 8 bytes--
The size byte is arranged as 32*the number of data bytes, plus the property
number.
Each list of properties is ended by a 00 size byte. This is why there is no
property 0.
When all the property tables are done, we come to the global variable table.
Global variables are numbered from 0 to 239, and this table begins with 240
initial 2-byte values for them. After this is conventially left space for
all the arrays, dynamic strings and so on which they point to.
We have now reached the top of the save area. Everything above here is
never altered.
Next is the table of grammar, which is described as above. It is
immediately followed by the actions table, the preactions table and then the
adjectives table, also described above.
And next the dictionary table, described above.
Next is the code area. Not all Infocom games begin with Main, but all
Informed ones do. The code area simply contains a list of routines.
All routines (and static strings) must occur at even addresses, so as to
enable them to have word addresses instead. (Inform occasionally inserts
00 bytes between routines to ensure this.)
A routine begins with one byte indicating the number of local variables the
routine has (from 0 to 15), and then with that many 2-byte words giving
their initial values, if not supplied by the call to the routine. (Inform
never makes use of this initialisation, and simply stores 0000's here.)
Unlike global variables, these bytes are _not_ used for the current values
of the variables: they are kept on the stack.
Executable code follows this header. There is no special marker for the end
of a routine; it is simply expected that in every case a legal return
instruction will be hit.
Finally, from the end of the code to the top of memory are the static
strings. These are put up here to be out of the way, where they won't clog
up the bottom 64K of memory. There's no table of their addresses, or pointer
to where they begin; each is referred to by an address in the code or data
given earlier.
---------------------------------------------------------------------------
A2. How text is encoded
---------------------------------------------------------------------------
Text is stored as a sequence of 2-byte words. Each of these is divided into
three 5-bit pieces, plus 1 bit left over, arranged as
--first byte------- --second byte---
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
bit --first-- --second--- --third--
The bit is set only on the last 2-byte word of the text, and so marks the
end.
The pieces are then characters, with values in the range 0 to 31.
There are three alphabets, in which the numbers 6 to 31 mean:
A0 abcdefghijklmnopqrstuvwxyz
A1 ABCDEFGHIJKLMNOPQRSTUVWXYZ
A2 ^0123456789.,!?_#'~/\-:()
('^' being actually the new-line character.)
Character 0 is a space in all alphabets. Characters 1, 2 and 3 are used for
abbreviations: thus, 1 followed by 14 means "print entry 14 in the synonym
table"; 2 followed by 5 means "print entry 32+5=37..."; 3 followed by 20
means "print entry 64+20=84..." etc.
The Z-machine provides these for commonly occurring strings to be printed
out as if they were characters, thus saving memory. Being plainly
abbreviations, these are for some reason called "synonyms". (In practice
this can save about 10K in a 128K file.)
(Inform makes use of these only when instructed to (e.g. @12 is compiled to
1, 12, meaning print the 12th entry in the synonym table), or when
abbreviations have been declared (see section (17)).)
By default, a character is presumed to be in A0, i.e. to be a lower-case
English letter. However, the character 4 means that the next one (only) is
in A1; and 5 means the next is in A2.
Notice that character 6 in A2 is blank. It isn't a space: it simply isn't
there. The sequence 5 followed by 6 indicates that the next two characters
define an ASCII value. This is the way to get at the characters not in any
of the three alphabets. For example, the familiar message
*** You are dead ***
takes four "characters" to produce each of the *'s.
Finally, note that the end-bit only comes up once every three characters,
so that a way is needed to safely use up any spare characters in the last
2-byte block. This is done by padding out with 5's. (5 followed by 5 does
nothing.)
This is especially the case with dictionary entries. Some dictionary
entries, like "i", ought only to take one 2-byte block, but in order to make
all entries two 2-byte blocks and alphabetically sortable by number, they
are padded out by up to five 5's in a row.
(Note that care must be taken to avoid dictionary entries ever containing
use of synonyms.)
In practice the text compression factor is not really very good: "Curses"
contains about 155000 characters of text, stored in 99000 bytes. (Text
usually accounts for about 75% of a story file.) But the encoding does at
least encrypt the text so that casual browsers can't read it.
---------------------------------------------------------------------------
A3. How Z-code is encoded
---------------------------------------------------------------------------
The encoding of version 3 Z-code is to say the least complicated. The
reader is warned that it is also different to that in all other versions.
There are all kinds of exceptions intended either to make small economies of
code size (these are very seldom worth the effort, in fact) or to provide
new features tacked on at the last minute.
Experimenting with Inform as an assembler, while tracing is turned on, may
be helpful.
Z-code understands four kinds of operand, and describes these in 2-bit
fields:
$$00 Large constant (>=256) 2 bytes
$$01 Small constant (0 to 255) 1 byte
$$10 Variable 1 byte
$$11 Omitted altogether 0 bytes
Variables are described in one byte. 00 means the top of the stack, 01 to
$0f are the local variables of the current routine and $10 to $ff are the
global variables, 0 to 239. Writing to 00 pushes something onto the stack
and reading from it pulls it off. The stack can also be manipulated (with
care) using the PUSH, PULL and POP instructions. The stack is guaranteed to
be at least 512 bytes long, and some interpreters are more generous. There
isn't any way to check stack overflowing, so be careful with recursion.
(One of the trickiest problems in compiling Z-code is throwing away unwanted
return values of routines which are left on the stack... it can take
hundreds of turns before a game crashes if this is got wrong.)
Z-code opcodes are 1 byte only. To begin with, look at the top two bits.
If these are $$11, we shall call it "variable"; if $$10, "short"; and
otherwise "long".
In this description, we shall adopt the opcode names used by the existing
Infocom disassembler "TXD".
For short opcodes, look at the next two bits (4 and 5). These give the kind
of operand which the code has. If this is $11, there isn't an operand and
the opcode has no argument at all. In this event, the remaining part of the
opcode gives what it is:
$00 RET#TRUE (1) The opcode is followed by text
$01 RET#FALSE in 2-byte chunks as usual
$02 PRINT (1)
$03 PRINT_RET (1) (2) Opcode followed by a branch
$05 SAVE (2)
$06 RESTORE (2) (3) This is an abbreviation for
$07 RESTARE RET SP, to save one byte
$08 RET(SP)+ (3)
$09 POP
$0A QUIT
$0B NEW_LINE
$0C SHOW_SCORE
$0D VERIFY (2)
If the type wasn't $11, then an operand follows, and moreover the "code"
part of the opcode means something different:
$00 JZ (2) (4) Followed by a store opcode
$01 GET_SIBLING (2) (4) (before the branch, if there
$02 GET_CHILD (2) (4) is also a branch)
$03 GET_PARENT (4)
$04 GET_PROP_LEN (4) (5) Refers indirectly to variables
$05 INC (5) by their number (Inform
$06 DEC (5) suppresses this feature, so
$07 PRINT_ADDR "@inc sp" produces the constant
0 instead of variable no. 0 as
$09 REMOVE_OBJ operand)
$0A PRINT_OBJ
$0B RET
$0C JUMP
$0D PRINT_PADDR
$0E LOAD (4) (5)
$0F NOT (4)
"Long" opcodes have two operands. The bottom 5 bits of the opcode say what
it is:
$01 JE (2) (6) (6) If this is encoded as
$02 JLE (2) "variable", then operands 3 and
$03 JGE (2) 4 (if present) are used as a
$04 DEC_CHK (2) (5) kind of OR command: eg,
$05 INC_CHK (2) (5) branch if o1 = o2, o3 or o4
$06 COMPARE_POBJ (2)
$07 TEST (2)
$08 OR (4)
$09 AND (4)
$0A TEST_ATTR (2)
$0B SET_ATTR
$0C CLEAR_ATTR
$0D STORE (5)
$0E INSERT_OBJ
$0F LOADW (4)
$10 LOADB (4)
$11 GET_PROP (4)
$12 GET_PROP_ADDR (4)
$13 GET_NEXT_PROP (4)
$14 ADD (4)
$15 SUB (4)
$16 MUL (4)
$17 DIV (4)
$18 MOD (4)
The alert reader will notice that bits 5 and 6 are left spare to be used.
Now there are two operands to specify, which ought to take up 4 bits, which
obviously won't fit. So a more economical form is used instead. Bit 6
refers to the first operand, and bit 5 to the second. A value of 0 means a
small constant and 1 means a variable. Now, type $11 (not really there)
operands can't happen, so that's no problem, but there might well be type
$00 (large constant) operands, for example in "@mul x #666 sp". In this
event, the opcode is instead programmed as a "variable" opcode.
So we must now describe the "variable" opcode form. In addition to the
possible opcodes which can arise from overflowing "long" opcodes, there are
others which can only be "variable". Here all of the bottom 6 bits are
available to describe the opcode, and this either holds the above numbers
$00 to $18 or else:
$20 CALL (4) (7) These codes are somewhat
$21 STOREW conjectural and only apply
$22 STOREB to a few Infocom games; Inform
$23 PUT_PROP never uses them unless told to
$24 READ explicitly
$25 PRINT_CHAR
$26 PRINT_NUM
$27 RANDOM (4)
$28 PUSH
$29 PULL (5)
$2A STATUS_SIZE (7)
$2B SET_WINDOW (7)
$33 SET_PRINT (7)
$34 #RECORD_MODE (7)
$35 SOUND (7)
Some of these are only of "variable" type because the available codes for
the other types had run out - PRINT_CHAR, for instance. Others, especially
CALL, need the flexibility to have between 1 and 4 operands.
In the "variable" type opcode, all eight bits of the opcode have been used
up, so we have to add another byte describing the operands. This is divided
into four 2-bit fields. For example, $$00101111 means large constant
followed by variable (and no third or fourth opcode).
Once the opcode is out of the way, the operands are simply stored in one or
two-byte form as appropriate.
PRINT and PRINT_RET are followed by text: this is assembled in the usual way
immediately after the opcode (which may well be at an odd address, but this
doesn't matter) and execution resumes after the last 2-byte chunk of text
(the one with top bit set).
Opcodes marked as "store" in the above tables, return a value: for example,
MUL multiplies its two arguments together, and CALL calls a routine which
must return a value. Such instructions are followed by a single byte giving
the variable (stack pointer, local or global as usual) to put it in. This
may look like an extra operand but is not: there is no need to tell the
Z-machine what type it has, since it must be a variable.
Finally, there are instructions which test a condition. Apart from the
obvious branch instructions (JE and so on), SAVE does this, for example, the
test in question being whether or not the save was successful. Branches are
stored in two different ways for economy reasons: nearby ones in a single
byte at the end of the instruction, farther ones in two bytes.
The top bit of the first byte of a branch is the "flag". If this is clear,
then a branch occurs when the condition came out false. If it is set, then
the branch occurs when it was true.
If the next bit (bit 6) is set, then the branch is in abbreviated 1-byte
format and the offset is in the bottom 6 bits (0 to 5). If not, the offset
is in the bottom 15 bits (0 to 6 of the first byte, and all of the second).
This offset can be positive or negative. (Eg., all 1's means -1 in the
usual way.)
In the abbreviated form, an offset of 1 in fact means "return true from the
current routine" and an offset of $20 (i.e., -31) means "return false". An
offset of 1 is never useful but -31 might arise, and so it is essential to
use the long form for such branches.
Working out what the offset ought to be is more complicated than it appears
because the PC has already moved on from the start of the instruction when
it reaches the branch. The bizarre formula in question is
Offset = Destination address - Address of this instruction - Length + B
where
Length = number of bytes in instruction (not counting the branch)
and B is 1 for short branches, 0 for long ones.
In practice Inform compiles branches in the long form, considering the
economy to be not worth the nightmarish computation needed to make the
long/short decision. (One problem is that the number of bytes in each
instruction _must_ be the same in both passes, so that the decision needs to
be made before the value of the offset is known... in a 2-pass compiler this
is insoluble. Another is that the offsets are affected by the size of the
branch, confusing things considerably on forward branches.) However, its
assembler mode allows you to make an explicit choice.
JUMP instructions similarly encode their address operand as an offset, but
always as a two-byte (signed) constant. In this respect they differ from
CALL instructions. In a CALL, the address is half the absolute routine
address.
---------------------------------------------------------------------------
A4. Using Inform as an assembler
---------------------------------------------------------------------------
Inform can also act as an assembler. A line beginning with an @ character
is sent straight to the assembly routines. Constants and variable names
can be given as operands but not compound expressions. The following are
supported:
jump <label> go to (local) label
jz <a> [~]<label> If a==0 go to label
(or return if "rtrue" or "rfalse")
je <a> <b> [~]<label> a=b
jge <a> <b> [~]<label> a>b (note: not >=)
jle <a> <b> [~]<label> a<b
test_attr <a> <b> [~]<label> object a has attribute b
test <a> <b> [~]<label> a&b != 0
compare_pobj <a> <b> [~]<label> objects a, b have same parent
In the above, if the ~ is set,
the condition is negated
ret#true return true
ret#false return false
ret
retsp return sp
ret <a> return a
save [~]<label> save; go to label if successful
restore [~]<label> restore; ...
verify [~]<label> verify file integrity; ...
restart reset Z-machine
quit exit Z-machine
show_score redisplay status line immediately
store <v> <a> v=a
loadw <a> <b> <v> v=word at (word address) a+b
loadb <a> <b> <v> v=byte at (byte address) a+b
storew <a> <b> <c> word at (word address) a+b=c
storeb <a> <b> <c> byte at (byte address) a+b=c
get_prop <a> <b> <v> v=property b of object a
get_prop_addr <a> <b> <v> v=address of...
get_next_prop <a> <b> <v> ? - seldom used
put_prop <a> <b> <c> property b of obj a is c
get_parent <a> <v> v=parent of a
get_prop_len <a> <v> v=property length of a
get_sibling <a> <v> [~]<label> v=sibling of a, branch if this =0
get_child <a> <v> [~]<label> ...child
inc_chk <v> <a> [~]<label> if v++=a then label
dec_chk <v> <a> [~]<label> if v--=a then label
load <v1> <v2> ?
random <a> <v> v=random number up to a
inc <v> v=v+1
dec <v> v=v-1
or <a> <b> <v> v=a | b
and <a> <b> <v> v=a & b
add <a> <b> <v> v=a + b
sub <a> <b> <v> v=a - b
div <a> <b> <v> v=a / b
mod <a> <b> <v> v=a % b
set_attr <a> <b> set attribute bit b on object a
clear_attr <a> <b> clear...
push <a1> [... <a4>] push a1 to a4 onto the stack
pull <a1> [... <a4>] pull a1 to a4 from it
pop throw away top of stack
insert_obj <a> <b> give object a to b
remove_obj <a> remove a from hierarchy
call <rname> [ <a1>... ] <v> v=rname(a1,...)
icall indirect call: sp=(sp)()
read <a> <b> see above
print_num <v> print v in decimal
print "<text>" print text
print_ret "<text>" print text, newline and return 1
new_line print newline
print_addr <a> print string at address a
print_paddr <a> print string at address 2*a
print_obj <a> print name of object a
print_char <a> print ASCII char <a>
Branch statements use the long form of the branch code if the label (or
tilde) is prefaced with a question mark '?', and otherwise use the short
form.
---------------------------------------------------------------------------
B1. A Bill of Player's Rights
---------------------------------------------------------------------------
W. H. Auden once observed that poetry makes nothing happen. Adventure
games are much the same, except that they make the player annoyed most of
the time. There's a fine line between a challenge and a nuisance: and
perhaps the most important point about designing a game is to think like a
player, not a programmer or even an author.
With that in mind, I hold the following rights to be self-evident:
1. Not to be killed without warning
At its most basic level, this means that a room with three exits, two of
which lead to instant death and the third to treasure, is unreasonable
without some hint. Mention of which brings us to:
2. Not to be given horribly unclear hints
Many years ago, I played a game in which going north from a cave led to a
lethal pit. The hint was: there was a pride of lions carved above the
doorway. Good hints can be skilfully hidden, or very brief (I think, for
example, the hint in the moving-rocks plain problem in "Spellbreaker" is a
masterpiece) but should not need explaining even after the event.
A more sophisticated version of (1) leads us to:
3. To be able to win without experience of past lives
Suppose, for instance, there is a nuclear bomb buried under some anonymous
floor somewhere, which must be disarmed. It is unreasonable to expect a
player to dig up this floor purely because in previous games, the bomb blew
up there. To take a more concrete example, in "The Lurking Horror" there is
something which needs cooking for the right length of time. As far as I can
tell, the only way to find out the right time is by trial and error. But
you only get one trial per game. In principle a good player should be able
to play the entire game out without doing anything illogical. In similar
vein:
4. To be able to win without knowledge of future events
For example, the game opens near a shop. You have one coin and can buy a
lamp, a magic carpet or a periscope. Five minutes later you are transported
away without warning to a submarine, whereupon you need a periscope. If you
bought the carpet, bad luck.
5. Not to have the game closed off without warning
Closed off meaning that it would become impossible to proceed at some
later date. If there is a papier-mache wall which you can walk through at
the very beginning of the game, it is extremely annoying to find that a
puzzle at the very end requires it to still be intact, because every one of
your saved games will be useless. Similarly it is quite common to have a
room which can only be visited once per game. If there are two different
things to be accomplished there, this should be hinted at.
6. Not to need to do unlikely things
For example, a game which depends on asking a policeman about something he
could not reasonably know about. (Less extremely, the problem of the
hacker's keys in "The Lurking Horror".) Another unlikely thing is waiting
in uninteresting places. If you have a junction such that after five turns
an elf turns up and gives you a magic ring, a player may well never spend
five turns there and never solve what you intended to be straightforward.
On the other hand, if you were to put something which demanded investigation
in the junction, it might be fair enough. ("Zork III" is especially poor in
this respect.)
7. Not to need to do boring things for the sake of it
In the bad old days many games would make life difficult by putting
objects needed to solve a problem miles away from where the problem was,
despite all logic - say, putting a boat in the middle of a desert. Or, for
example, it might be fun to have a four-discs tower of Hanoi puzzle in a
game. But not an eight-discs one.
8. Not to have to type exactly the right verb
For instance, looking inside a box finds nothing, but searching it does.
Or consider the following dialogue (amazingly, from "Sorcerer"):
>unlock journal
(with the small key)
No spell would help with that!
>open journal
(with the small key)
The journal springs open.
This is so misleading as to constitute a bug. But it's an easy design fault
to fall into. (Similarly, the wording needed to use the brick in Zork II
strikes me as quite unfair. Or perhaps I missed something obvious.)
9. To be allowed reasonable synonyms
In the same room in "Sorcerer" is a "woven wall hanging" which can instead
be called "tapestry" (though not "curtain"). This is not a luxury, it's an
essential.
10. To have a decent parser
This goes without saying. At the very least it should provide for taking
and dropping multiple objects. Some suggestions for a decent specification
are given in Appendix B3.
The last few are more a matter of taste, but I believe in them:
11. To have reasonable freedom of action
Being locked up in a long sequence of prisons, with only brief escapes
between them, is not all that entertaining. After a while the player begins
to feel that the designer has tied him to a chair in order to shout the plot
at him.
12. Not to depend much on luck
Small chance variations add to the fun, but only small ones. The thief in
"Zork I" seems to me to be just about right in this respect, and similarly
the spinning room in "Zork II". But a ten-ton weight which fell down and
killed you at a certain point in half of all games is just annoying.
13. To be able to understand a problem once it is solved
This may sound odd, but many problems are solved by accident or trial and
error. A guard-post which can be passed only if you are carrying a spear,
for instance, ought to have some indication that this is why you're allowed
past. (The most extreme example must be the notorious Bank of Zork.)
14. Not to be given too many red herrings
A few red herrings make a game more interesting. A very nice feature of
"Zork I", "II" and "III" is that they each contain red herrings explained in
the others (in one case, explained in "Sorcerer"). But difficult puzzles
tend to be solved last, and the main technique players use is to look at
their maps and see what's left that they don't understand. This is
frustrated when there are many insoluble puzzles and useless objects. So
you can expect players to lose interest if you aren't careful. My personal
view is that red herrings ought to have some clue provided (even only much
later): for instance, if there is a useless coconut near the beginning, then
perhaps much later an absent-minded botanist could be found who wandered
about dropping them. The coconut should at least have some rationale.
The very worst game I've played for red herrings is "Sorcerer", which by
my reckoning has 10.
15. To have a good reason why something is impossible
Unless it's also funny, a very contrived reason why something is
impossible just irritates. (The reason one can't walk on the grass in
"Trinity" is only just funny enough, I think.)
16. Not to need to be American to understand hints (*)
The diamond maze in "Zork II" being a case in point. Similarly, it's
polite to allow the player to type English or American spellings or idiom.
For instance "Trinity" endears itself to English players in that the soccer
ball can be called "football" - soccer is a word almost never used in
England.
17. To know how the game is getting on
In other words, when the end is approaching, or how the plot is
developing. Once upon a time, score was the only measure of this, but
hopefully not any more.
(*) Several people have politely pointed out to me that my own game
"Curses" is, shall we say, slightly English. But then, like any
good dictator, I'm better at drafting constitutions than abiding
by them.
---------------------------------------------------------------------------
B2. What makes a good game?
---------------------------------------------------------------------------
1. The Plot
The days of games which consisted of wandering around doing unrelated things
to get treasures, are long passed: the original Adventure was fun, and so
was Zork, but two such games are enough. There should be some overall task
to be achieved, and it ought to be apparent to the player in advance.
This isn't to say that it should be apparent at once. Instead, one can
begin with just an atmosphere or mood. But if so, there must be a
consistent style throughout and this isn't easy to keep up. "The Lurking
Horror" is an excellent example of a successful genre style; so is "Leather
Goddesses of Phobos".
At its most basic, this means there should be no electric drills lying about
in a medieval-style fantasy. The original Adventure was very clean in this
respect, whereas Zork was less so: I think this is why Adventure remains the
better game even though virtually everything in Zork was individually
better.
If the chosen genre isn't fresh and relatively new, then the game had better
be very good.
Plot begins with the opening message, rather the way an episode of Star Trek
begins before the credits come up. It ought to be striking and concise (not
an effort to sit through, like the title page of "Beyond Zork"). By and
large Infocom were good at this. A fine example is the overture to
"Trinity" (by Brian Moriarty):
Sharp words between the superpowers. Tanks in East Berlin. And now,
reports the BBC, rumors of a satellite blackout. It's enough to spoil your
continental breakfast.
But the world will have to wait. This is the last day of your $599 London
Getaway Package, and you're determined to soak up as much of that
authentic English ambience as you can. So you've left the tour bus behind,
ditched the camera and escaped to Hyde Park for a contemplative stroll
through the Kensington Gardens.
Already you know: who you are (an unadventurous American tourist, of no
significance in the world); exactly where you are (Kensington Gardens, Hyde
Park, London, England); and what is going on (World War III is about to
break out). Notice the careful details: mention of the BBC, of continental
breakfasts, of the camera and the tour bus. More subtly, "Trinity" is a
game which starts as a kind of escapism from a disastrous world out of
control: notice the way the first paragraph is in tense, blunt,
headline-like sentences, whereas the second is much more relaxed. So a lot
has been achieved by these two opening paragraphs.
The most common plots boil down to saving the world, by exploring until
eventually you vanquish something ("Lurking Horror" again, for instance) or
collecting some number of objects hidden in awkward places ("Leather
Goddesses" again, say). The latter can get very hackneyed (got to find the
nine magic spoons of Zenda to reunite the Kingdom...), so much so that it
becomes a bit of a joke ("Hollywood Hijinx") but still it isn't a bad idea,
because it enables many different problems to be open at once.
Most games have a prologue, a middle game and an end game, which are usually
quite closed off from each other. Usually once one of these phases has been
left, it cannot be returned to.
2. The Prologue
In establishing an atmosphere, the prologue gives a good head start. In the
original mainframe Adventure, this was the above-ground landscape; the fact
that it was there gave a much greater sense of claustrophobia and depth to
the underground bulk of the game.
Sometimes a dream-sequence is used (for instance, in "Lurking Horror"), or
sometimes simply a more mundane region of game (for instance, the
guild-house in "Sorcerer"). It should not be too large or too hard.
As well as establishing the mood of the game, and giving out some background
information, the prologue has to attract a player enough to make him carry
on playing. It's worth imagining that the player is only toying with the
game at this stage, and isn't drawing a map or being at all careful. If the
prologue is big, the player will quickly get lost and give up. If it is too
hard, then many players simply won't reach the middle game.
Perhaps eight to ten rooms is the largest a prologue ought to be, and even
then it should have a simple (easily remembered) map layout.
3. The Middle Game
A useful exercise is to draw out a tree (or more accurately a lattice) of
all the puzzles in a game. At the top is a node representing the start of
the game, and then lower nodes represent solved puzzles. An arrow is drawn
between two puzzles if one has to be solved before the other can be. For
instance, a simple portion might look like:
Start
/ \
/ \
Find key Find car
\ |
\ |
Start car
|
|
Reach motorway
This is useful because it checks that the game is soluble (for example, if
the ignition key had been kept in a phone box on the motorway, it wouldn't
have been) but also because it shows the overall structure of the game.
The questions to ask are:
How much is visible at once?
Do large parts of the game depend on one difficult puzzle?
How many steps does a typical problem need?
Some games, such as the original Adventure, are very wide: there are thirty or
so puzzles, all easily available, none leading to each other. Others, such as
"Spellbreaker", are very narrow: a long sequence of puzzles, each of which
leads only to a chance to solve the next.
A compromise is probably best. Wide games are not very interesting, while
narrow ones can in a way be easy: if only one puzzle is available at a time,
the player will just concentrate on it, and will not be held up by trying to
use objects which are provided for different puzzles.
Bottlenecks should be avoided unless they are reasonably guessable:
otherwise many players will simply get no further.
Puzzles ought not to be simply a matter of typing in one well-chosen line.
One hallmark of a good game is not to get any points for picking up an
easily-available key and unlocking a door with it. This sort of low-level
achievement - like wearing an overcoat found lying around, for instance -
should not be enough. A memorable puzzle will need several different ideas
to solve (the Babel fish dispenser in "Hitch-hikers", for instance).
4. Size and Density
Once upon a time, the sole measure of quality in advertisements for
adventure games was the number of rooms. Even quite small programs would
have 200 rooms, which meant only minimal room descriptions and simple
puzzles which were scattered thinly over the map.
Nowadays a healthier principle has been adopted: that (barring a few
junctions and corridors) there should be something out of the ordinary about
every room.
One reason for the quality of the "Infocom" games is that the version 3
system has an absolute maximum of 255 objects, which needs to cover rooms,
objects and many other things (eg, compass directions, or the spells in
"Enchanter" et al). Many "objects" are not portable anyway: walls,
tapestries, thrones, control panels, coal-grinding machines and so on.
As a rule of thumb, four objects to one room is about right: this means
there will be, say, 50-60 rooms. Of the remaining 200 objects, one can
expect 15-20 to be used up by the game's administration (eg, a "darkness"
room, 10 compass directions, a player and so on). Another 50-75 or so
objects will be portable but the largest number, at least 100, will be
furniture.
So an object limit can be a blessing as well as a curse: it forces the
designer to make the game dense. Rooms are too precious to be wasted.
In the Infocom format, there's room for 150K of text at the most. This is
the equivalent of about a quarter of a modern novel, or perhaps the analogy
is that there are enough bytes to store a very substantial book of poetry.
Roughly, then, one can afford to spend 2K of text in each room - about ten
times the level of detail of the original mainframe Adventure.
5. Rewards
There are two kinds of reward which need to be given to a player in return
for solving a puzzle. One is obvious: that the game should advance a
little. But the player at the keyboard needs a reward as well, that the
game should offer something new to look at. In the old days, when a puzzle
was solved, the player simply got a bar of gold and had one less puzzle to
solve.
Much better is to offer the player some new rooms and objects to play
with, as this is a real incentive. If no new rooms are on offer, at least
the "treasure" objects can be made interesting, like the spells in the
"Enchanter" trilogy or the cubes in "Spellbreaker".
6. Mazes
Almost every game contains a maze. Nothing nowadays will ever equal the
immortal
You are in a maze of twisty little passages, all alike.
But now we are all jaded. A maze should offer some twist which hasn't been
done before (the ones in "Enchanter" and "Sorcerer" being fine examples).
The point is not to make it hard and boring. The standard maze solution is
to litter the rooms with objects in order to make the rooms distinguishable.
It's easy enough to obstruct this, the thief in "Zork I" being about the
wittiest way of doing so. But that only makes a maze tediously difficult.
Instead there should be an elegant quick solution: for instance a guide who
needs to be bribed, or fluorescent arrows painted on the floor which can
only be seen in darkness (plus a hint about darkness, of course).
Above all, don't design a maze which appears to be a standard impossibly
hard one: even if it isn't, a player may lose heart and give up rather than
go to the trouble of mapping it.
7. Wrong guesses
For some puzzles, a perfectly good alternative solution will occur to
players. It's good style to code two or more solutions to the same puzzle,
if that doesn't upset the rest of the game. But even if it does, at least
a game should say something when a good guess is made. (Trying to cross the
volcano on the magic carpet in "Spellbreaker" is a case in point.)
One reason why "Zork" held the player's attention so firmly (and why it took
about ten times the code size, despite being slightly smaller than the
original mainframe Adventure) was that it had a huge stock of usually funny
responses to reasonable things which might be tried.
My favourite funny response, which I can't resist reprinting here, is:
You are falling towards the ground, wind whipping around you.
>east
Down seems more likely. ["Spellbreaker"]
(Though I also recommend trying to take the sea serpent in "Zork II".) This
is a good example because it's exactly the sort of boring rule (can't move
from the midair position) which most designers usually want to code as fast
as possible, and don't write with any imagination.
Just as some puzzles should have more than one solution, some objects should
have more than one purpose. In bad old games, players automatically threw
away everything as soon as they'd used them. In better designed games,
obviously useful things (like the crowbar and the gloves in "Lurking
Horror") should be hung on to by the player throughout.
8. The Map
To maintain an atmosphere throughout it's vital that the map should be
continuous. Adventure games used to have maps like
Glacier
|
Oriental Room -- Fire Station
(megaphone) (pot plant)
|
Cheese Room
in which the rooms bore no relation to each other, so that the game had no
overall geography at all, and objects were unrelated to the rooms they were
in. Much more believable is something like
Snowy Mountainside
\
Carved Tunnel
|
Oriental Room -- Jade Passage -- Fire Dragon
(buddha) (bonsai tree) Room
|
Blossom Room
Try to have some large-scale geography too: the mountainside should extend
across the map in both directions. If there is a stream passing through a
given location, what happens to it? And so on.
In designing a map, it adds to the interest to make a few connections in the
rarer compass directions (NE, NW, SE, SW) to prevent the player from a
feeling that the game has a square grid. Also, it's nice to have a few
(possibly long) loops which can be walked around, to prevent endless
retracing of steps.
If the map is very large, or if a good deal of to-and-froing is called for,
there should be some rapid means of moving across it, such as the magic
words in Adventure, or the cubes in "Spellbreaker".
9. The End Game
Some end games are small ("Lurking Horror", or "Sorcerer" for instance),
others large (the master game of the mainframe Adventure). Nonetheless
almost all games have one.
End games serve two purposes. Firstly they give the player a sense of being
near to success, and can be used to culminate the plot, to reveal the game's
secrets. This is obvious enough. But secondly they also serve to stop the
final stage of the game from being too hard.
As a designer, you don't usually want the last step to be too difficult; you
want to give the player the satisfaction of finishing, as a reward for
having got through the game. (But of course you want to make him work for
it.) An end game helps, because it narrows the game, so that only a few
rooms and objects are accessible.
The most annoying thing is requiring the player to have brought a few
otherwise useless objects with him. The player should not be thinking that
the reason for being stuck on the master game is that something very obscure
should have been done 500 turns before.
10. And Finally...
Finally, the winner gets some last message (which, like the opening message,
should have something amusing in it and should not be too long). That
needn't quite be all, though. In its final incarnations (alas, not the one
included in Lost Treasures), "Zork I" offered winners access to the hints
system at the RESTART, RESTORE or QUIT prompt.
11. It's Never Finished
Games are never finished. There's always one more bug, or one more message
which could be improved, or one more little cute reply to put in. Debugging
is a creative process and adds to the life of the game. My game "Curses"
was finished after about 80K of coding, but it's now 128K long. In other
words, over a third of Curses is devoted to "irrelevant" features, blind
alleys and the like.
A first stage in debugging is to produce what you think is a perfectly
working game and hand it over to some players (preferably several) for a
week or two. Expect this to produce a hundred or so awesomely trivial
bugs and one or two hugely obvious ones. But don't expect that that's the
only hundred. I think altogether 200 bugs in "Curses" have been spotted
since my original declared-finished version - about 50 of those in the
seven months since its public release. I also think that the many people
who wrote to me to suggest little extensions and repairs have greatly
contributed to the game, and that's why there are so many names in the
credits.
12. Last Word
Inform makes it easy to knock up games which look a little bit like the
classics, until you play them. Infocom, Inc, acquired a certain mystique in
their time, but a game is not good merely because it runs under one of their
interpreters. Still, I hope a few people may put in the effort to turn out
a finished game or two, and add to the canon.
--------------------------------------------------------------------------
B3. Writing a Parser
---------------------------------------------------------------------------
This lengthy dialogue is to meant to demonstrate what a player can expect
from a parser, and some of the pitfalls in designing one.
At each stage, lines 2a, 2b, ... etc are alternative inputs. Comments are in
square brackets. It is taken as read that the parser can cope with words
(such as "plant") being nouns, verbs or prepositions depending on context.
Parser Demo
Welcome to the demonstration cave.
You can see an Elvish-made sword and a Dutch-made sword.
[To begin with, an easy ambiguity. There may be many other swords
elsewhere, but these should not obstruct the parser.]
You can see an Elvish-made sword and a Dutch-made sword.
1a. > take the dutch sword
Taken.
1b. > remove a sword
(the Elvish-made sword)
Taken.
1c. > get sword
Which do you mean, the Elvish-made sword or the Dutch-made sword?
[Suppose you tried 1b. There are four possible responses: you
don't care which, you want both, you say exactly which, or you give
a command again in full.]
2a. > either
(the Elvish-made sword)
Taken.
2b. > both
Elvish-made sword: Taken.
Dutch-made sword: Taken.
2c. > elvish
Taken.
2d. > pick up the dutch sword
Taken.
[2a may seem unnecessary - but what if the two objects were
absolutely indistinguishable from each other? At least this would
allow the player to pick up one of them.]
[Now the player holds the Dutch sword, say, but the Elvish one is on
the floor. The parser should therefore be able to decide which you
mean when you want to take or drop.]
3a. > drop sword
(the Dutch-made sword)
Dropped.
> drop the dutch sword
It is already on the ground.
3b. > take sword
(the Elvish-made sword)
Taken.
[Note that the parser accepts "drop the dutch sword", even when the
Dutch sword is already on the ground, exactly so as to allow the
production of the helpful message. This shows that requests can be
contextually allowable, even when the parser knows they are illegal.]
A dwarf comes in and deposits a Roman sword on the floor.
[Next, plurals. The three swords are now on the floor, suppose, and
so are a Jersey cow, a red herring and a postage stamp.]
4a. > take all the swords
Roman sword: Taken.
Dutch-made sword: Taken.
Elvish-made sword: Taken.
4b. > take every sword except the dutch
Roman sword: Taken.
Elvish-made sword: Taken.
4c. > take the roman and dutch swords
Roman sword: Taken.
Dutch-made sword: Taken.
4d. > take all swords but roman, elvish and dutch
Nothing to do!
4e. > get everything except the swords and the fish
Jersey cow: Too large.
postage stamp: Taken.
[Some verbs ought not to allow multiple objects.]
5a. > examine everything
That verb can't take multiple objects.
[Others can be abbreviated. Common abbreviations are "x" for "examine",
"i" for "inventory", "l" for "look", "z" for "wait", "g" for "again".]
6a. > x roman sword
A gladius, or one-handed short sword.
[Words like "it" or "them" ought to refer to the last-mentioned item.]
7a. > take it
Taken.
[But beware: this is dangerous if the object is no longer in context.]
8a. > drop it into chasm
The Roman sword vanishes into darkness.
> x it
The Roman sword is no longer available.
[Also, to some extent "it" should follow the game. It would be usual to
code the following by removing the sword object and replacing it with an
Eagle one - but what happens to "it"?]
9a. > wave magic wand at it
The Roman sword transmogrifies into the Eagle of the Ninth Legion.
> x it
The Eagle is the standard of the legion...
[Vagueness: and more on resolving ambiguity. To begin with, you should
have the opportunity to specify what you want more fully - but only if
it is not obvious:]
10a. > take
What do you want to take?
> the postage stamp
Taken.
10b. > drop
(the Eagle of the Ninth)
Dropped.
[One can take this dangerously far. Should the parser be allowed to
implicitly solve a problem for the player? Suppose you dropped
a sandwich by mistake a while back, and now intend to eat it...]
11a. > eat
(the plant root)
Mmm, the plant root isn't bad, but what strange things are
happening to your vision...
[The parser should be aware, when resolving ambiguities and handling
plurals, of the distinction between objects being worn and merely being
carried.]
12a. > inventory
You're wearing a taffeta dress and carrying a cotton blouse.
> drop all
cotton blouse: Dropped.
12b. > drop
(the cotton blouse)
Dropped.
12c. > drop taffeta
It's suddenly rather cold in here.
[Similarly, there is a distinction between living and dead objects:]
13a. > say hello
(to the dwarf)
The dwarf grunts with irritation.
> look
A dwarf guards a cotton blouse and a postage stamp.
> take all
cotton blouse: The dwarf bars you.
postage stamp: The dwarf bars you.
[Which brings us to conversation. By convention, the standard format
is:]
14a. > dwarf, hello
The dwarf grunts with irritation.
14b. > dwarf, go east
The dwarf trundles off east.
14c. > dwarf, take
What do you want the dwarf to take?
> postage stamp
The dwarf angrily refuses.
14d. > dwarf, drop everything
dwarvish shield: The dwarf drops the shield.
mithril armour: The dwarf refuses.
[Note that in the latter case, context for the dwarf is not the same as
context for the player, and "everything" must be read differently.]
[A particularly nasty, if unlikely case to resolve is:]
15a. > dwarf, examine
What do you want the dwarf to examine?
> dwarf, stamp
[Is the correct idea that the dwarf should look at himself and the
postage stamp? Or that he should stamp his feet?]
[Another problem with conversation is that the parser should accept
it even when it doesn't make sense in game terms. After all, you might
really mean this...]
16a. > dwarf, get your lousy beard out of here
The dwarf spits.
[A parser may well have to accept arbitrary numbers or words at some
point. For instance:]
17a. > write earth on cube
You write "earth" on the featureless white cube.
17b. > enter 132
You type "132" at the keyboard, and the safe opens.
[But care is sometimes needed, unless the parser insists on the player
using double-quotes:]
18a. > enter safe
You type "safe" at the keyboard.
18b. > enter the safe
You enter the walk-in safe.
[In a complicated command, information should gradually accumulate:]
19a. > transfer sword
Which do you mean, the Roman sword or the Dutch-made sword?
> roman
What do you want to transfer the Roman sword to?
> filing
Which do you mean, the green filing cabinet or the blue filing
cabinet?
> blue
Transferred.
[However if, say, "transfer" is a verb which always takes the
preposition "to" - ie, which must have the form "transfer x to y" - the
parser ought to be able to manage if there's only one container handy:]
20a. > transfer a sword
(the Roman sword to the green filing cabinet)
Transferred.
[The fact that a single verb may have many possible forms (as many as
a dozen, for instance) means that a parser may have to make many
different guesses to resolve ambiguities. These may clash. At the
very least, the parser shouldn't ask the player for a ruling more than
once: and if it changes its mind, it should only print up
(what it inferred you meant)
once, for the pattern which got nearest to acceptance. Another pitfall
is:]
21a. The dwarf takes everything away, then returns with the Sword of
Damocles and the Sword of Alexander.
> take sword of damocels
You can't see any such thing.
[This appears obvious. The trap is that the parser may have recognised
that "sword of" might refer to one of two things, and ask the player which
is intended, and only afterwards reach the inevitable conclusion that the
command doesn't make sense, because "damocels" isn't a preposition which
"take" can have.]
[Containers complicate the issue of context a little. Consider these
alternative lines:]
22a. > look
There are a red apple and two boxes here, a glass box and a steel box:
both open.
> i
You carry a rucksack (which is empty), a torch, a banana and an
apple. You are wearing a patterned dress.
> put everything in the rucksack
torch: Transferred.
banana: Transferred.
green apple: Transferred.
> empty rucksack into glass
torch: Transferred.
banana: Transferred.
green apple: Transferred.
> get apple from glass box (*)
Taken.
> drop it in glass box
Done.
> close glass
You close the glass box.
> x apple
Which apple do you mean, the red apple or the green apple?
> green
The apple is not yet ripe.
> take it
But the glass box is closed.
22b. > drop all except torch into the steel box
rucksack: Dropped.
banana: Dropped.
green apple: Dropped.
> put dress in steel box
(taking it off first)
Done.
> x dress
It has pictures of tulips on.
> close steel box
You close the steel box.
> x dress
You can't see any such thing.
[Note the way no attempt is made to put the rucksack inside itself.
Also, note that in the former line, objects can still be seen (if not
manipulated) because the glass box has transparent walls. In the
latter, they become invisible when the box closes.
At (*), "get apple" would have been ambiguous. In most games, so
would "get apple from box", though a really astute parser could deduce
that the box must be the glass one, because the other does not contain
an apple, and that the apple must be the green one, because the red one
isn't in anything.]
[Containment complicates the rules governing darkness considerably -
what happens after...]
22c. > put torch in steel box
Done.
> close steel box
It is now pitch dark!
> open steel box
[Is the steel box now in context? Most parsers would insist that objects
on the floor of a dark room are not in context. But that punishes the
player rather severely for her mistake, and not very realistically. On
the other hand...]
23a. > east
It's pitch dark in here.
> get gelignite
Taken.
[...in circumstances where the gelignite has never been seen before, is
regrettable.]
[To what extent should a parser issue implicit commands on behalf of the
player? In some cases, this is clearly good practice. In others, it is
not so clear. For instance:]
24a. > drop dress
(taking off the patterned dress first)
Dropped.
24b. > read newspaper
(taking the copy of the Daily Telegraph)
"The Prime Minister today proved his brilliant grasp of..."
24c. > drop newspaper
(taking the copy of the Daily Telegraph)
Dropped.
24d. > examine delicate bomb label
(taking the delicate bomb)
The bomb explodes at your first touch!
[The first pair are clearly right, the second pair wrong. This illustrates
also that the parser cannot handle "implicit taking" and the like in a
naive way: it must use the same game code as if the player had asked
explicitly. Otherwise, the player could sneakily use, say, "drop chains"
to get the parser to remove her chains, even though the game itself would
not have allowed this. Similarly, which of these will a player be more
impressed by?]
25a. > drop all
newspaper: Dropped.
delicate bomb: You don't really mean that!
25b. > drop all
newspaper: Dropped.
delicate bomb: The bomb explodes cataclysmically, killing you!
[A degree of customisation also makes for a happier player. For instance,
in the author's game "Curses", the tedious rule is maintained that a player
can't hold more than five things at once, to which there is the tedious
solution of carrying a rucksack around to hold miscellaneous plunder. It
becomes very boring to type all the "put x in rucksack" commands by hand, so
the parser infers these:]
25a. > get red herring
(putting the cypher into the rucksack to make room)
Taken.
[But a parser has to know a certain amount to do this sensibly: not to try
putting the rucksack into itself, not to put the light source into the
rucksack, and to try disposing of the item longest ignored.]
["Again" is surprisingly hard to code in a context-sensitive way, but
players want it:]
26a. > throw herring into chasm
It disappears into the darkness.
> again
I can't see what you're referring to.
[And another is-it-fair? darkness problem...]
27a. > press switch
It is now pitch dark in here!
> again
I can't see what you're referring to.
[These are at least correct behaviour. The disaster would be to continue
to allow the player to refer to the herring after it had fallen into the
chasm. So the obvious solution is simply to parse the player's original
words all over again in the situation as it now is. But then...]
28a. > press button
Which button do you mean, the red button or the blue button?
> blue
Click!
> again
Click!
[...would not have worked - the question would have been asked all over
again. So the player's clarifications have to be kept as well.]
[Another tricky bit of coding is for corrections, i.e. "oops".]
29a. > throw swrod at the dwarf
You can't see any such thing.
> oops sword
The dwarf expires untidily.
[In practice it probably isn't worth coding this in an unduly clever way -
it only needs to work in most easy cases. Even so it can be difficult to
point to the "obvious" wrong word, when the user's input as failed to
match against many different patterns.]
The conclusion of all this is that:
(i) a parser is a major part of the game, and implicitly contains many
of its rules and assumptions;
(ii) while it's obviously desirable to separate it as much as possible
from the game proper, it must be customised to some degree and so
cannot be separated altogether.
Thus, although it means very much more work, there is something to be said
for a game-design system which obliges the designer to provide a parser
himself. Inform adopts the compromise of providing one which is as general
as possible, and held at arm's length, but which can be modified when
needed.
---------------------------------------------------------------------------
C1. A Hello Cruel World program
---------------------------------------------------------------------------
> !
> ! A great step backward in interactive fiction...
> !
>
> Object hillside "Bare hillside" nothing;
> global place = hillside;
> global score = 0;
> global turns = 1;
>
> [ Main;
>
> print "^^^^^^^^^^^You wake up, shivering to see that Morgoth \
> the Flatulent Devil is towering over you...^^";
> Message();
> print "^^\
> ...and he squashes you effortlessly.^^ *** You have died ***^^^^^";
>
> quit;
> ];
>
> [ Message i;
> print "HELLO CRUEL WORLD^";
> print "A Non-Interactive Demonstration^\
> Copyright (c) 1993 by Graham Nelson. All rights reserved.^";
> print "Release ";
> print_num 0-->1;
> print " / Serial number ";
>
> for i 18 to 23 { print_char 0->i; } new_line;
> ];
>
Note that the familiar banner has to be produced by your code. By
convention, the first word (at bytes 2 and 3) of the file is the release
number, and this is what is set by the RELEASE command. In this file there
isn't a RELEASE command, so it comes out as 1. Bytes 18 to 23 contain the
serial number, or in fact the serial string of ASCII characters. By custom
and tradition, these are the date of compilation arranged YYMMDD, and Inform
sets these automatically.
Note also that Message had to be a separate routine since we needed a local
variable, and Main is not permitted to have local variables of its own.
(The above source has changed a little since the first release: if the
object is not included, then some interpreters (not the InfoTaskForce one)
which voluntarily display the status line (when not asked to do so), get in
a quandary printing a location, time and score. So for their benefit, here
are all three.)
On my machine (an Acorn Archimedes A5000), compiling with statistics
produces something like:
*inform -s hellow
Archimedes Inform 1.0 (v794/at)
Input 31 lines (41 statements, 690 chars)
1 objects (maximum 255) 0 dictionary entries (maximum 750)
0 attributes (maximum 32) 2 properties (maximum 30)
0 adjectives (maximum 240) 0 verbs (maximum 110)
0 actions (maximum 125) 0 abbreviations (maximum 64)
3 globals (maximum 240) 480 variable space (maximum 1500)
141 symbols (maximum 3500) 2 routines (maximum 400)
314 characters of text (compressed to 276 bytes, rate 0.878)
Output story file is 1.5K long (maximum 128K)
Essential size 1152 bytes: 129920 remaining
Completed in 1 seconds.
(The second being mostly consumed in printing out the statistics. In
practice the compilation time is roughly proportional to the output length,
and typically takes 0.5 seconds per K of story file on my machine.)
The "essential size" is the number of bytes actually used: the story file
is rounded up in size to an exact number of 512-byte blocks. The number
remaining is the actual number of bytes free in the Z-machine.
Note: if you try compiling the example games, and get different
statistics outputs, do not worry; it probably means you're using a later
version of Inform than the one that printed the above. Similarly,
do not worry if your compiled story file shows differences with the
object code in the archive.
If the "Deja Vu" example (see below) compiles without Inform errors and
plays properly, then Inform is probably working OK.
As an extreme example, statistics for "Curses" look something like:
*inform -sdep curses
Archimedes Inform 1.0 (v794/at)
Input 8532 lines (20095 statements, 348700 chars)
255 objects (maximum 255) 732 dictionary entries (maximum 750)
32 attributes (maximum 32) 29 properties (maximum 30)
19 adjectives (maximum 240) 103 verbs (maximum 110)
115 actions (maximum 125) 64 abbreviations (maximum 64)
84 globals (maximum 240) 1340 variable space (maximum 1500)
3496 symbols (maximum 3500) 307 routines (maximum 400)
144284 characters of text (compressed to 91814 bytes, rate 0.636)
Output story file is 128K long (maximum 128K)
Approximate percentage breakdown of story file:
Z-code 62.2%
Static strings 24.1%
Dictionary 3.9%
Objects 6.7%
Globals 1.0%
Parsing tables 1.7%
Header and synonyms 0.1%
Total of save area 7.9%
Total of text 70.2%
Essential size 130780 bytes: 292 remaining
Completed in 59 seconds.
---------------------------------------------------------------------------
C2. "Deja Vu": a toy game
---------------------------------------------------------------------------
The "Hello World" program above is enticingly short and easy, but only
because it doesn't contain a parser. A fully functioning parser is hard
work to write, and occupies a good deal of "Inform" code. Besides this, the
everyday mechanics of an adventure game involve more coding than most
designers want to go into, at least at the outset.
"Deja Vu" is a toy game in terms of how large it is to play, but
demonstrates the use of a large supplied library of Inform code.
Compiling "Deja Vu" with statistics should produce something like:
*inform -s dejavu
Archimedes Inform 1.0 (v794/at)
Input 3837 lines (7529 statements, 126447 chars) from 4 files
48 objects (maximum 255) 282 dictionary entries (maximum 750)
30 attributes (maximum 32) 25 properties (maximum 30)
19 adjectives (maximum 240) 77 verbs (maximum 110)
86 actions (maximum 125) 0 abbreviations (maximum 64)
77 globals (maximum 240) 1125 variable space (maximum 1500)
1508 symbols (maximum 3500) 162 routines (maximum 400)
16143 characters of text (compressed to 12078 bytes, rate 0.748)
Output story file is 27K long (maximum 128K)
Essential size 27430 bytes: 103642 remaining
Completed in 19 seconds.
Again, don't worry if the details are slightly different.
---------------------------------------------------------------------------
C3. The library routines
---------------------------------------------------------------------------
There are three header files provided to make up the kernel of a game,
which need to be #include'd at different points. Together they amount
to a kind of operating system for the Z-machine.
Since they are themselves written in Inform, they can be altered if
necessary, but the idea is that they shouldn't need to be. The shell game
in (C4) shows the minimum code needed to use the library, and DejaVu is a
more substantial example.
Your file _must_ provide one routine:
Initialise() is called before the banner is printed up, and one thing it
_must_ do is to set "location" to the initial game location.
It must also define two string constants, Story and Headline, e.g.:
> Constant Story "ZORK II";
> Constant Headline "^An Interactive Plagiarism^\
> Copyright (c) 1993 by Ivan O. Ideas.^";
In addition, it may (but need not) provide the following routines:
TimePasses() is called after every turn (but not, for instance, after a
command like "score" or "save").
PrintRank() completes the printing of the score. (You might want to change
this, so as to make the ranks something like "junior astronaut" or
"master catburglar" or whatever suits your game.)
DarkToDark() is called when a player goes from one dark room into another
one. If you want, you can take the opportunity to kill the player off
for doing this.
DeathMessage() is to print up "You have died" style messages. To kill a
player, set "deadflag" to 1. To make a player win, set "deadflag" to 2.
If you set "deadflag" to any other value (3, 4, ...) this routine is
called to say what happened, e.g. "You have changed".
LookRoutine() is called at the end of every location description.
NewRoom() is called when the room changes, before any description of it
is printed. (This happens no matter how the change of room occurred.)
PrintTaskName() prints the name of a game task (such as "driving the
car").
Amusing() is called to print some amusing afterword when the game is won
(for instance, it might advertise some features which a successful
player might never have noticed).
GamePreRoutine() and GamePostRoutine() - see below.
Apart from these, the only routines of yours which are ever called are those
attached to your objects.
Three of the properties objects can have are:
preroutine whose values are routine names
postroutine
liferoutine
Suppose the player is in the Bedquilt Room, and types "drop oyster". Once
it has checked that this is a reasonable command, the parser does the
following:
1. Call GamePreRoutine (if there is one).
If this returns true, stop here.
2. Call the preroutine of Bedquilt Room.
If this returns true, stop here.
3. Then the preroutine of the oyster.
If this returns true, stop here.
4. Actually drop the object.
5. Call the postroutine of Bedquilt.
If this returns true, stop here.
6. Then the postroutine of the oyster.
If this returns true, stop here.
7. Call GamePostRoutine (if there is one).
If this returns true, stop here.
8. Print "Dropped."
The point is that the preroutines can change the rules (e.g., to implement
"you can't drop things in the Bedquilt room", or "the toffee apple sticks
to your fingers") and postroutines can change what is printed (e.g., one
might print "Sinks into the bedquilt without trace" and remove whatever
was dropped).
Anything not mentioned in a preroutine or postroutine is dealt with in
the standard way. In a postroutine the following variables are set up:
action The action, in the form #a$DropSub
inp1 First parameter | if given
inp2 Second |
(A complicated exception handles objects you can travel inside or push
from place to place - see the DejaVu code for the red car.)
To get your code to run whenever the room description is printed for a
particular place, put it under #a$LookSub in the room's postroutine.
Similarly, #a$GoSub in a room's postroutine applies when it has just been
walked into, whereas in a preroutine it refers to an intention to leave.
Here is a typical example:
> [ CloverPost;
> if action~=#a$TakeSub { rfalse; }
> if player~=parent(sword) { rfalse; }
> print_ret "As you take the clover, your sword shines \
> a sudden burst of light!";
> ];
Thus, when the clover is picked up, it makes the sword shine - as a hint,
say, that it's lucky. Note that print_ret returns the value true (as well
as printing a new-line automatically), indicating intervention. (And
"Taken.", which would be the usual response, is not printed.)
Warning: remember that in a postroutine, the action in question has already
taken place. One might want the message to only appear the first time the
clover is taken, and naively code this as:
> if clover has moved { rfalse; }
...but that wouldn't work, because when the postroutine is called, the
"moved" attribute has already been set by TakeSub.
There is nothing to stop more than one object having the same pre- or
postroutine, and nothing to stop you changing them half-way through the
game after something happens. (But be warned: the right command would be
something like
> write clover postroutine #r$NewCloverPostroutine;
and remember to use the #r$ construction.)
GamePreRoutine and GamePostRoutine are extreme measures, changing the
rules across the entire game. Remember that they're run every turn:
don't put anything too slow in them.
The actions implemented by the library are in three groups:
1. Quit, Restart, Restore, Verify, Save, ScriptOn, ScriptOff,
Brief, Normal, Verbose;
2. Inv, Take, Drop, Remove, PutOn, Insert, Transfer, Empty,
Enter, Exit, Go, Look, Examine, Give,
Unlock, Lock, SwitchOn, SwitchOff, Open, Close, Disrobe, Wear, Eat;
3. Rhet [rhetorical answer], Burn, Pray, Wake, WakeOther [person],
Kiss, Think, Smell, Listen, Taste, Touch, TouchThing, Dig,
Cut, Jump [jump on the spot], JumpOver, Tie, Drink,
Fill, Sorry, Strong [swear word], Mild [swear word], Attack, Swim,
Swing [something], Blow, Rub, Set, WaveHands [ie, just "wave"],
Wave [something], Pull, Push, PushDir [push something in a direction],
Turn, Squeeze, LookUnder [look underneath something], Search,
ThrowAt, Answer, Buy, Ask, Sing, Climb, Wait, Sleep
Note that the player can type all manner of things to get these. For
instance, "take off shirt" and "remove shirt" both cause the Disrobe
action. Your code can ignore this complication.
Group 1 actions are "meta" - they are outside the game proper, and your
code is unable to stop them. (If you want a room where the game can't be
saved, you'll have to tamper with SaveSub directly.)
Group 2 actions, by default, actually do something; group 3 actions,
by default, just reply with a message.
The routine names are these with "Sub" on the end, such as "SleepSub".
The action numbers corresponding to them are #a$SleepSub, and so on.
Although all actions go through a preroutine, not all of them bother
to check postroutines. For instance, since the built-in "smell" routine
just says "You smell nothing out of the ordinary", there would be no
point calling a postroutine - nothing, after all, has been done. (These
are the group 3 actions above.)
The ones which actually do something, and use postroutines, are:
Inv, Take, Drop, Remove, PutOn, Insert, Exit, Go, Look, Examine, Unlock,
Lock, SwitchOn, SwitchOff, Open, Close, Disrobe, Wear, Eat, Search.
(Some other group 2 actions use these postroutines indirectly - if the
player empties a sack out onto the floor, this is deemed to be dropping
the objects within, for instance, and if a sack is emptied into a packing
case this is considered a multiple insertion.)
Animate objects (such as sea monsters, mad aunts or nasty little dwarves)
have a "liferoutine". When this is called, the variable "reason_code"
holds the reason it was. This happens for the following reasons:
0 The player has asked the creature to do something,
e.g., by "troll, go south". action, inp1 and inp2 are
set up as usual: e.g. action=#a$GoSub and inp1=s_obj.
#a$AttackSub Player tried to attack creature
#a$KissSub Tried to kiss
#a$AnswerSub The player tried either "answer <word> to troll", or else
"troll, <something not understood>". In either case
special_word is set to the dictionary entry of the first
word, or 0 if it isn't in the dictionary, and
special_number is set to an attempt to read it as a
number. (For instance, "computer, 143" will cause
special_number to be set to 143.)
#a$AskSub "ask troll about ..." - similarly special_word and
special_number are set up
#a$GiveSub inp1 holds the object number of what the player has tried
to give (or feed) the creature. (Unless code is written
the creature will "seem uninterested" and nothing will
happen.)
#a$ThrowAtSub inp1 holds whatever the player tried to throw.
If the routine doesn't exist, or returns false, events will take their
usual course.
Quite often you want to simulate the effect of a player typing something.
For instance, in the Pepper Room the air is full of pepper and every turn
you sneeze and drop something at random. If the code to do this simply
removes an object and puts it on the floor, then it might accidentally
provide a solution to the problem "the toffee apple sticks to the hands".
So a routine Process is provided, as in:
print "You sneeze convulsively, and lose your grip on ";
DefArt(obj); print "...^"; Process(obj, 0, #a$DropSub);
Formally, Process executes the third-named action, with arguments
(inp1 and inp2) being the first and second. 0 is given for inp2 here
because Drop doesn't have a second argument.
DefArt(obj) is a useful little routine which prints the name of the
object, with its definite article attached. This causes, for example,
the platinum bar
Aunt Jemima
Elbereth
(notice that proper names do not have "the" printed in front of them).
Similarly, CDefArt(obj) prints the same but with a capital letter at
the front, e.g.
The platinum bar
The marbles
and IndefArt(obj) with the indefinite article:
a platinum bar
an orange balloon
your Aunt Jemima
some bundles of reeds
far too many marbles
Another useful routine is DescribeObj(obj), which produces a more complete
description:
an electric torch (providing light)
a rucksack (which is open and contains a scarlet fish)
Time now to detail how to make objects and places in this universe. The
library sets up (and uses) the following attributes:
light gives off light
concealed invisible, but touchable
worn currently worn clothing
clothing can be clothing
animate is a creature of some kind
female gender female (otherwise male) - used for eg "her"
proper short name is proper noun
moved has been picked up at some time
portal is a doorway of some kind
container can contain things (eg, a bottle)
supporter can support things (eg, a table)
interior container with interior invisible when closed
open container or doorway which is open
openable container or doorway open/closeable to player
workflag (flag used by parser)
enterable can be entered (eg, car or fireplace)
scenery not described by looks, or moveable (eg, rock face)
static immovably fixed in place (eg, Y2 rock)
direction (is one of the compass directions)
lockable can be locked or unlocked (with suitable key)
locked is presently locked
switchable can be switched on and off
on is presently switched on
edible can be eaten
autosearch examining this object causes it also to be searched
scored award points for first taking
talkable is not an animate creature, but can nevertheless
be talked to (eg, a microphone)
general general-purpose flag
"general" is free for the user (as are the remaining spare attributes) and
usually means "the puzzle associated with this object has been solved".
Most of the above attributes make no sense for locations. The attributes
applying to them are:
light there's ambient light
visited place has been visited at some time
scored award points for first visit
(If you simply never want to have darkness, a sneaky way of doing it is to
give the player object the light attribute, by tampering with its definition
in the Parser header file.)
Here are the properties defined and used by the library:
Default value Use
if not 0
name (*) dictionary words referring to an
object, or dictionary words which
refer to "irrelevant" things in a room
longdesc long description of a room, or the
examine description of an object
article "a" indefinite article for object
initpos initial description of an object not yet
picked up (eg, "A flower grows here.")
preroutine $ffff see above
postroutine $ffff
liferoutine $ffff (only relevant for animate objects)
n_to s_to (**) map connections from one room to another
e_to w_to
ne_to se_to
nw_to sw_to
u_to d_to
portalto place that a "portal" connects to
dirprop direction which goes through it
closedpos description of a portal which is closed
(the initpos is used when it is open),
or description of something switchable
which is off (initpos used when it's on)
timeleft useful for timed events
with_key key object which must be held for a
lockable object to be locked or unlocked
cantgo "You can't go that way."
printed when player tries to go an
impossible way from a location
capacity 100 maximum number of things a container
or supporter can contain or support
(*) name is a property actually defined in the syntax of Inform itself.
But this is the use to which it is put by the library routines.
(**) warning: do not confuse these with the direction objects n_obj,
s_obj... which are what inp1, inp2 etc would be set to in a command
mentioning a direction (such as "push the car north").
You cannot be both a container and a supporter.
For example, let us look at the object definitions needed to make a
locked door, which needs a key.
> Object broken "Broken Passage" nothing
> with longdesc "You are in a dirty broken passage...",
> e_to cave, n_to door, s_to yriver, u_to view;
> Object door "iron door" broken
> with name "iron" "door",
> initpos "In the north wall is an open iron door.",
> closedpos "In the north wall is a shut iron door.",
> portalto ledge,
> dirprop n_to,
> with_key key
> has portal static openable lockable locked;
> Object ledge "Canyon Ledge" nothing
> with longdesc "The door opened out onto a tiny ledge...",
> s_to broken
> has light scored;
Note that the map connection north from the broken passage is to the door,
not to a location. The door has two different descriptions according to
whether it is opened or closed. It is a portal to the ledge, and the
direction which it goes in is north. (This is set so that "enter door"
can be translated into "go north".) It needs the key to be unlocked,
but can then freely be opened. Finally, it's static - you can't walk off
with it.
You get 5 points for getting through. Once there, the ledge goes south
straight back to the broken passage. (Doors which can be locked, opened
etc. on both sides have to be implemented with two objects, one each side.)
With these definitions, no actual code is needed at all.
Another brief example:
> Object searchlight "Gotham City searchlight" cave
> with name "search" "light" "template", article "the",
> longdesc "It has some kind of template on it.",
> initpos "The old city searchlight shines out a bat against \
> the feather-clouds of the darkening sky.",
> closedpos "The old city searchlight, neglected but still \
> functional, sits here."
> has switchable static;
The library won't allow the player to carry an indefinite number of
objects: the limit allowed is the constant MAX_CARRIED, which you
may define if you wish. (If you don't define it, it's 100, which
roughly removes the rule.)
If you define SACK_OBJECT to be some container, then the player will
automatically put old, least-used objects away in it as the game
progresses. (If not, nothing happens.)
The other constants you are allowed to define help the score routines
along. These are:
MAX_SCORE The maximum game score (default 0)
NUMBER_TASKS Number of individual "tasks" to perform (0)
OBJECT_SCORE Bonus for picking up a "scored" object (4)
ROOM_SCORE Bonus for entering a "scored" room (5)
and then the individual tasks have scores, as follows:
Global task_scores initial t1 t2 ... tn;
Within your code, when a player achieves something, call Achieved(task)
to mark that the task has been completed. It will only award points if
this task has not been completed before.
The library maintains light by itself. It will cope, for instance, with
what happens if a dwarf picks up the light source and walks away, or if
the light source is shut up in an opaque box. The program can give
or take away the light attribute to anything, or move anything anywhere,
without having to think about whether or not this makes the room dark.
If you're writing a game with time instead of score/turns on the
status line, you can set the time by
SetTime( hours*60+minutes, rate);
where rate=0 means the time is left alone, and if rate>0 that many
minutes pass every action, whereas if rate<0 that many actions pass
for every minute.
Setting the_time tells the library that the game is a "time" rather than
"score" game. Remember to call this in Initialise().
Finally, you might want the parser to know about plural nouns. Many
games involve collecting a number of similar items, say a set of nine
crowns in different colours. Then you'd want the parser to recognise
things like
> drop all of the crowns except green
for instance. It can do this, provided it's told that "crowns" is the
plural of crown, and provided it's told how to recognise a crown when
it sees one.
You need not declare any, but are allowed up to three such plurals, declared
in your Initialise() routine by lines like:
> plural_word1 = #n$crowns; plural_filter1 = is_crown;
(and so on, for 2 and 3). You must already have declared is_crown as
an Attribute, and flagged all the crowns with this attribute. (#n$crowns
is a construction which puts the word "crowns" into the dictionary.)
Eventually, you will want to tamper with the library routines. But they're
more flexible than they look, and you can put the evil day off for a very
long time.
---------------------------------------------------------------------------
C4. A shell game to build on
---------------------------------------------------------------------------
The shell below is the minimum you need to write to use the library
routines. Note that the order in which things occur is quite important.
Nothing should appear above the #include "Parser" line except possibly
more defined constants and any abbreviations. Task_scores must be defined
in between the two inclusions; nothing else should be. The grammar table
should be last.
It's good form to put the objects all together, before the code, and then
to define all your own global variables, and then to reach the routines.
> !
> ! Shell of a game
> !
>
> Constant Story "SHELL";
> Constant Headline "^An Interactive Skeleton^\
> Copyright (c) 1993 by (your name here).^";
>
> Release 1;
> Statusline score;
>
> #include "Parser";
>
> Global task_scores initial 0;
>
> #include "VerbLib";
>
> Object room "Blank Room" nothing
> with longdesc "An empty room."
> has light;
>
> [ Initialise;
> location=room;
> print "^^^^^Welcome to the shell...^^";
> ];
>
> [ PrintRank;
> print ", earning you the rank of ";
> print_ret "vacancy.";
> ];
>
> #include "Grammar";
> end;
(Actually, the Release and Statusline directives were redundant (1 and
score being the default settings anyway) but it's good form to include
them.)
---------------------------------------------------------------------------
Graham Nelson, gan10@uk.ac.cam.phx or phx.cam.ac.uk
Magdalen College, nelson@uk.ac.ox.vax or vax.ox.ac.uk
Oxford OX1 4AU,
UK. 18th April 1993
and 16th May 1993
and 14th November 1993
---------------------------------------------------------------------------