[net.micro.amiga] AmigaVenture Programmer's Guide

hadeishi@husc4.harvard.edu (mitsuharu hadeishi) (10/19/86)

	The following is a introductory guide to help you get
started writing AmigaVentures with the AmigaVenture 1.17 kernal
posted a few days ago.  This is only a beginning; to actually
learn the details, you must peruse the actual source code and
read the voluminous comments which document the program's operation.
However, you should find the following an invaluable guide to the
murky depths of AmigaVenture.

	Enjoy!				-Mitsu

---------------------------------------------------------------------------

  Guide to Writing Amiga Ventures

---------------------------------------------------------------------------

  By Mitsu Hadeishi 10/18/86

  Current mailing address is hadeishi%husc4@harvard.edu

---------------------------------------------------------------------------


   Q. What is AmigaVenture?

   A. AmigaVenture is a program skeleton that allows you to easily
write your own custom text adventures.


   Q. How flexible is it?

   A. AmigaVenture is completely flexible, since it is written in
AmigaBasic.  Most people already know a flavor of Basic, and AmigaBasic
is a natural extension of this.  Becuase AmigaBasic uses alphanumeric
labels instead of line numbers, because it has structured programming
constructs such as IF-THEN-ELSE and WHILE-WEND, and because it allows the
definition of subprograms with local variables, AmigaBasic is very well
suited for the writing of even very sophisticated programs.  And because it
is interpreted rather than compiled (although a compiler is available),
you can test programs immediately.  AmigaBasic is also fast; much faster
than other microcomputer Basics by a large factor.


   Q. How extensive is the program?

   A. AmigaVenture supplies you with all the tools you need to write
a modern text adventure with relative ease.  The program is complicated,
however, and though I've tried to make it as simple as possible, mastering
all of the tools provided may take some time.  


   Q. What is an adventure?

   A. An adventure consists of a set of locations and descriptions,
a set of objects and their properties, and a set of actions the player
can perform within the adventure world.  You can also define characters
that act within the adventure, doing playful things or interacting with
the player.  In addition, you may have events which occur at various
times or places or under certain conditions which change the adventure
significantly.  Within this structure you are free to dream up anything you
like.


   Q. How does the player interact with the adventure?

   A. The player enters a series of commands in an English-like command
language.  An effort has been made to allow the definition of a very
natural command language; the command interpreter is very powerful,
and is capable of understanding something like "Put everything that's in
the bag underneath the table." or "Get everything but the red book and
go north."  This interpreter was developed in stages from a much less
sophisticated interpreter; this was possible because AmigaVenture was
developed in an interpreted language environment.  The current state
of the interpreter is MORE sophisticated than the interpreter found in
many commercial products.

   The interpreter is also capable of distinguishing between different
objects through the use of adjectives.  So, for example:

  | > get the earring
  |
  | Which earring do you mean:
  | The diamond earring, or the pearl earring?
  |
  | > the pearl
  |
  | Taken.

   The interpreter is smart; if the diamond earring is not visible,
then it is assumed the player is not referring to it.

   The way this is done is the following: adjectives and nouns are
not distinguished internally.  Following each word in the noun list
are the list of all possible objects or abstract ideas the word could
be referring to.  For example "earring" can refer to, say, two objects,
so the listing might be

   DATA earring,6,8,0

   This means "earring" could refer to objects 6 or 8.  The 0 is simply
a marker which means "no more entries in the list".  Now "pearl"
only refers to object 6.  When the player enters "pearl" the program
checks to see that the ambiguity has been resolved and continues the
command.

   This system works, for the most part, but there are occasional
difficulties.  Usually any problems are easily worked around.
This system has the advantage of being simple and fast, and handles
most things you might want to do.


   Q. How do you begin to define an adventure?

   A. First, draw a map.  The text adventure structure is rather rigid;
it assumes the player is going to be doing a lot of exploring of a
somewhat fixed terrain.  Of course you can define several terrains
disconnected from each other; this is simply a matter of drawing a map
with several disconnected regions.  One region might be the seashore
and another might be an island; or they might be two cities connected
by teleportation boxes; or spaceports connected by a space trip.
Anything goes.

   Q. How does the adventure move along?

   A. I personally feel that if the player does nothing, the adventure
should move along without him.  That is, there should be things happening
all the time in various parts of the adventure, and if the player doesn't
participate in those events, too bad.  If you want to design a static
universe that doesn't change even though the player just wanders around
in circles, that is easy to do.  I think it is a good idea to throw in
a little spice (like a character who pops up and takes you on an
involuntary tour of the city, or something like this).  The player can
get easily bored if s/he gets "stuck" with no obvious way out.

   However, the player is still the prime mover, and the player
interacts with the adventure world through VERBS, or COMMANDS.
(I know, this is a very I-It conception, but that's the nature of this
beast.)  Each verb can have its own properties and can do anything
the designer wishes.  It can print out "Goob." and do nothing.  It can
unlock a magic door.  It can turn on a machine, teleport the player to
another location, tell the player his physical condition, anything.


   Q. Show me.

   A. The designer can write anything s/he wants, but there is a general
protocol that all commands should follow.

   In general, there are three places in AmigaVenture that you need to
modify when you implement a new command.  These areas begin with the
labels Verbs:, Commands:, and DoCommand:.

   Verbs:

   The parser needs a list of all the words which could refer to that
verb.  For example, for the verb Drop: has the following synonyms:
"let go of", "get rid of", "put down", "drop", and "release".  The
parser is capable of handling one, two, and three-word verbs.  The number
you associate with the verb should be the NEXT HIGHER NUMBER from the
highest verb there before.

   Commands:

   This is where you go next, to put the actual verb definition.
This list is IN ORDER and MUST be IN ORDER.  To add a new command
you simply add a label to the end of the list of commands.  This
label can be anything you want except a reserved AmigaBasic word,
and it should be mnemonic (something you can easily remember).

   After your new command label, you put a series of DATA statements
characterizing the grammatical properties of the verb.  For example,
the verb might take only direct objects, and if no direct object
is specified the program might be asked to search the objects not
on the player's person for a possible choice, etc.  A lot of flexibility
is put in these DATA statements which allows the parser to do a lot
of pre-processing even before the verb gets the information.  More on this
later.

   DoCommand:

   Here you modify the highest verb allowed (change the IF statement),
and add a verb to the list of IF...THEN ON...GOSUB commands.  This
should be self-explanatory.  Make sure to keep the same pattern which
is there already.

   HOW TO WRITE THE COMMAND ITSELF
   --- -- ----- --- ------- ------

   Let's take a look at a typical command.

TurnOn:
DATA 2,0,0
DATA 0,0
DATA 0,0
DATA 2,0

IF n < 0 THEN GOSUB Absurd:RETURN
IF n <> lamp THEN GOSUB Cannot:RETURN

IF flag(lampon) THEN PRINT FNcap$(nn$(0))" is already on.":RETURN
flag(lampon) = 1
PRINT FNcap$(nn$(0))" is now on.

RETURN

   Okay.  The first four data statements define grammatical properties
of the verb.  For a full explanation, see the in-code documentation.
This verb says the object must be physically accessible, the verb
takes no indirect objects, it doesn't matter if the object is on the
player or in the room, there is no default search path for an
ambiguity, and you can have as many objects as you want: as in "turn on the
lamp, the machine, and the staircase."

   IF n < 0 THEN GOSUB Absurd:RETURN

   The first IF statement is saying that the object must be a physical
object, non an abstract noun.  All abstract nouns (i.e. "north", "game",
etc., words that do not refer to verbs or physical objects) have NEGATIVE
numbers to avoid confusion.  This line is typical of many verbs, and
perhaps should be added to the list of default grammatical paramters.
For now, each verb having this restriction (which is most of them)
have this line of code.

   IF n <> lamp THEN GOSUB Cannot:RETURN

   Before the command is called, some global variables are set first.
The important ones are: n, the direct object code, o, the indirect
object code, and p, the preposition code.  Important string variables
are: n$(0) and nn$(0), strings describing the direct object (n),
n$(1) and nn$(1), strings describing the indirect object (o), and
p$, the preposition string.  n$() typically is just a single word
describing the noun, and nn$() is typically "the "+n$().  You should
use nn$() instead of explicitly writing "the" because some words
(typically abstract nouns, i.e., you don't want to say "you can't pick up
the north!", rather you want to say "you can't pick up north!").
   "lamp" is a variable defined in the Initialize: section of the program,
which is called when the program is started.  Any object which is referred
to specifically should be referred to through a variable so that if
the object number changes this variable can be changed and all the code
will still work.  For example, in your adventure you might have a
different object be the "lamp", and in that case you'd change this
variable.

   IF flag(lampon) THEN PRINT FNcap$(nn$(0))" is already on.":RETURN

   flag() is an array which is used very heavily in AmigaVenture.
ALL ADVENTURE STATUS NUMBERS should be stored in this array.  This
includes, for example, the positions of various characters you might
program, the time of day, whether or not a magic door is open or not,
etc.  This is so that the LoadGame: and SaveGame: commands do not
need to be modified for every new adventure, and also so that the
status of the adventure is available to the conditional description
processor (see documentation under Map:).  Rather than referring to
the flag directly, it is indexed through the variable "lampon" which
refers to a flag number.  This is done for reasons similar to the above.
   FNcap$() is a string function that returns the same string back,
with the initial letter capitalized.

   flag(lampon) = 1

   Turn on the lamp.  (Set the flag).

   PRINT FNcap$(nn$(0))" is now on.

   Prints "The lamp in now on." unless you've changed the word
describing the lamp object.

   RETURN

   Returns control to the caller.  This is usually the DoCommand:
routine, which branches to PostProcess:.  However, a command may call
another command (you must use the Alias() subprogram to set up the
command stack first; more on this later) so this may be returning control
to another command instead.


   Q. When would a command call another command?

   A. Well, when a command determines that some condition has not been
met for the completion of the command, the command can call another
command in an attempt to rectify this problem.  For example, the player
may want to open a container, but if it is locked, then the program will
automatically try to unlock it first:

OpenIt:
DATA 2,0,0
DATA 0,0
DATA 0,0
DATA 2,0

IF n < 0 THEN GOSUB Absurd:RETURN
IF folded(n) THEN GOTO UnWrap
IF openable(n) = 0 THEN GOSUB Cannot:RETURN
IF locked(n) THEN
   PRINT"(trying to unlock "nn$(0)" first)
   CALL Alias("unlock",11,(n(0)),0,0):GOSUB Unlock
   GOSUB RestoreCommand
   IF locked(n) THEN RETURN
   PRINT"(then, proceeding . . .)
END IF

   Let's stop here.  Now, this code is relatively straightforward to
read.  If the object n happens to be locked, the command prints a message
to the player, and then calls the Unlock command.  After the command
returns, it OpenIt checks to see if the thing is locked (if so, it
just RETURNs), otherwise, it continues and opens the object.

   But what is this "Alias" command doing here?  Well, before a command
is called, several variables must be set up (i.e. n, p, o, n$(), nn$(),
and p$, not to mention v$ and v which contain information related to
the verb.)  You call the Alias command with a list of the verb string,
the verb number, the direct object, the preposition code, and the
indirect object code.  That is, CALL Alias("VERB",verb,n,p,o) will set
up the variables you want.  THE EXTRA PARENTHESES AROUND n(0) ARE
NECESSARY.  WHEN YOU DO THIS, YOU PASS THE VALUE OF THE VARIABLE AND NOT
THE VARIABLE ITSELF TO A SUBPROGRAM.  This should be noted carefully
for calling other subprograms as well; sometimes you want the subprogram
to change your variable (to return a value, for example), sometimes you
do not.

   What Alias() does is to set up all the appropriate variables so you
can call the command just as though it were being called from DoCommand:
after a command has been interpreted by the parser.  Alias() "fools" the
command into thinking it has been called just like an ordinary command.
The command then does its stuff, printing out whatever messages it
prints out (such as "You idiot! You can't unlock that!") and then
returns.
   But now all of your nice variables have been changed.  You want them
back the way they were before you called the other command.
   The routine RestoreCommand does exactly that.  It restores all of
the variables that you just changed with Alias().  Since a command
can never be sure that another command won't call yet another command,
these values are stored on a "command stack" that keeps track of various
levels of command calling.  This allows up to ten levels of this kind
of tom foolery.  In practice you usually never get above three or four.

   It's kind of fun to watch Alias() at work:

  | >put the purse in the backpack
  |
  | (taking the purse first): Taken.
  | (opening the backpack first):
  | The backpack is now open.
  | (then, putting the purse in the backpack):
  | Done.

   The first Alias() was done by the parser, because it knows the verb
Put: requires that the direct object be in the hands of the player.
The next was done by the Put: command, because it knows to put something
inside something else you have to open it first.

   You needn't worry about the internal details of Alias() and
RestoreCommand.  If you use them as in the example above and the examples
in the source code you have, you should have no problems.


   Q. Could you say something about the properties of objects?

   A. Well, let me say something about nouns.  There are basically two
kinds of nouns, the abstract nouns and the concrete nouns.  The concrete
nouns category paradoxically includes adjectives (they are treated just
like regular nouns).  This may sound strange, but it works in practice.

   To add a new abstract noun to the vocabulary, you must change the
lists in two places: under Nouns: and under Abstract:.

   To add a new concrete object to the vocabulary, you must change
the lists in two places: under Nouns: and under Objects:.

   Nouns: contains a list of all nouns and adjectives in the adventure.
Every noun or adjective has a list of numbers after it, terminated by a
zero, which lists all the possible noun codes associated with the noun.
Positive numbers refer to concrete objects in the object list, negative
numbers refer to abstract nouns in the abstract list.

   A noun word may refer to both concrete objects and abstract codes.

   In the Nouns: list, to add a new word, simply append the word to the
list of nouns and list the objects or abstract nouns this word could
refer to.  This goes for both adjectives and nouns.

   If you are adding a new abstract noun, you must also add an entry
to the Abstract: list.

   If you are adding a new concete object, you must first specify
all of its properties and its initial location and relation to other
objects in the Objects: list.  Some of the properties you can define
are whether it has a surface, whether you can put things under it
(like a table), how heavy it is, how bulky it is, how much capacity
it has, how much you can fit through its opening (a bottle has a
narrow opening, for example), where it starts off to begin with
and whether it is inside something else to begin with (or on top
of something else, etc.), whether you can roll it up, whether you can
wrap things with it, whether or not it can contain water (if you add
such a thing the NEXT object MUST be WATER, and this object will be
the WATER that is INSIDE the previous one when it is full of water.
For more information about water and its properties, see documentation
under Objects:, WaterLists:, and Fill: and Pour:) and other properties.
You are free to add your own properties, but you must change Initialize:
to read in the new properties correctly and also change LoadGame: and
SaveGame: to load and save these properties in the middle of a game.
How to define these properties is documented under Objects:.  You must
also list some adjectives that the program can use to distinguish it
from other objects when it asks the question: "Which do you mean:
the brown bag or the red bag?".  HOWEVER, WHATEVER YOU SPECIFY HERE WILL
NOT BE USED BY THE PARSER UNLESS YOU ALSO MODIFY THE Nouns: LIST.
(Note: properties of objects are typically stored in arrays.  The current
list of properties is documented in the Quick Reference Guide.)

   After you have added the object to the Objects: list, then you should
add all the words and adjectives the player might use to refer to the
object in the Nouns: list.  Of course, if the word is already in the
Nouns: list, then you simply add the code to the list of codes after
the word.  If the word is not there, make up a new code for the word
and add it to the list.  The Nouns: list does not need to be in any
particular order.

   For example, suppose I have added the following object to the
Objects list:

DATA 21,a small,backpack,canvas,"The label says 'COOP.'"
DATA 3,0,0, 7,7, 7,0,5,0, 15,0,5,0, 1,0,0,0, 1,1,0,0,0,0, 0,4,1,0,0

   Now I want to add "small", "backpack", "canvas", and
"pack" to the Nouns: list.

   I notice "small", "backpack", and "pack" are already in the list, so
I make the following changes:

DATA small,5,7,21,0,satin, . . .
DATA . . . backpack,14,21,0,pack,14,21,0

   I then add "canvas" to the list:

DATA canvas,21,0

   And now I am done.  The new object will appear in my adventure,
and I will be able to manipulate it and do various things with it
(like wear it, open and close it, etc.)


   Q. Is this all I need to know to write an adventure?

   A. You also need to know how to write location descriptions and
how to set up the map.  The descriptions can be made to be conditional;
i.e., certain descriptions only printed when a certain flag is a certain
value, or only during the day, etc.  The map can also be conditional;
you can have a location that just prints a message then forces you to
another location (this is handy for making teleportation doors and
the like, so you get a description like "Your vision blurs, and suddenly .
. ." and then you are put in a new location.)  You can also have a
direction which goes one place if a flag is set, and prints a
message or goes somewhere else if the flag is clear.  See Map: for
a thorough discussion of this (in the program).


   Q. Anything else?

   A. Finally, when you start to write sophisticated code, you will
want to use the various subprograms made available for your use.
Each subprogram is documented in the code itself, but I will list some
of the most noteworthy here.  The labels are used so the programmer
can list them easily; they have no function in the program itself
other than this.

Calc:

   Visible() allows you to check whether an object is visible to the
player, and you can also add the condition of the object being in
the player's hands or in the room (not in the player's hands).

   Avail() is the same, but checks further that the object is physically
available to the player to reach out and grab.  If you don't want this
to be able to happen (force field or something) you must alter the
Take: command to check for force fields, etc.

   CheckLight() allows you to check the current lighting situation in
the player's location.

   NameNoun() returns n$ and nn$ strings to the caller when they
give it a noun code.

Calc2:

   ListSib() lists the siblings of an object.  This is used by the
parser and is of little use to a regular adventure programmer.

   Inside() determines whether one object is inside another, and
also in what relation (perhaps rather than inside it is on top of
or underneath, or wrapped by that object.)

   EvalCond() evaluates a map conditional (used by Look: to determine
whether or not to print conditional descriptions) This is used by
Look: and is of little use to a regular adventure programmer.

   RollDice simply puts a random number between 0 and 99 in flag(random).
This is called every move so a map can have random move locations.

   ListBottles() lists all the water container's in a player's
possession.  Also of little use to the average adventure programmer.

Lists:

   Contents() prints a list of the object, all its siblings, and everything
inside of, on top of, wrapped by, and underneath an object.  (Note
that "underneath" refers to something not touching the object, like
something under a table.  If a book is on the table, then the table is
NOT "underneath" the book.  Sounds funny, but its true.)  You can
also specify that it only print what's inside the object and not its
siblings as well.

   (A sibling is an object "next" to the object.  I.e., several
books on a table are all siblings, several objects lying free in a
room are siblings.)

   Remove() removes an object from relation with everything and
places it in limbo (location 0).  This is how you make something
disappear.  Combined with Insert(), this is how to move something.

   Insert() inserts an object into relation with something else.
This might be a room (inside a room) or it might be another object
(in which case it inherits the location of its "parent.")  For
performance reasons this routine ASSUMES YOU HAVE JUST CALLED REMOVE().
You MUST call Remove() prior to calling Insert(), or the list structure
will be badly mangled.

   Setloc() allows you to set the location of a particular object and all
of its descendants.

   RemList() removes an entire list of objects but does NOT place them
in limbo.  It simply makes them "invisible"; they will not appear in
a Contents() list.  However, if you ask whether they are "there" or
not the Visible() and Avail() routines will still say the same thing
(as before you called RemList()).  This routine is primarily used to
move whole lists of objects around quickly (which is why the objects
are not Setloc()'d) in conjunction with Concat().  The head of the
list is returned.

   Concat() is used to concatenate an entire list of objects (the type
removed by RemList()) to another list.  The routine is typically called
after RemList to concatenate a list to another.  In fact, you MUST
call RemList() before calling Concat() or the pointers in the lists
will get mixed up (and you will have STRANGE results, like an object
"appearing" to be in two places at once, etc.)

WaterLists:

   Fill() fills a water container with the specified amount.  If there
is too much, it stops and returns the actual amount filled.

   Empty() empties a water container.

   Tumble() takes all objects that are stacked on top of the object
and makes them siblings of the object (and they all fall down).
Used primarily to make things accurate; if you have a stack of
something and throw it in your backpack, the stack will just fall
apart and you'll have a jumble of stuff in your pack.  Not used by
the typical adventure programmer (used in the Place: command).

   There are also a bunch of interpreter subprograms which you do
NOT need to fool with unless you want to modify the parser or fix
a bug (heaven forbid) in the parser.  If you want to modify these,
please email me and I'll try to explain how the parser works.
Otherwise please treat it as a black box which takes command lines
and gives you a nice series of verb, direct object, preposition,
indirect object combinations.

   If you want to move some of this information to disk, the easiest
way would be to move some or all of the DATA sections and the Initialize:
routine to a separate program, run the program and store the resulting
arrays on disk in random access files.  This would save memory and
allow the creation of larger adventures; however it would slow game
play and slow the development of the adventure.  The advantage of
having everything in DATA statements is that you can easily modify
the adventure and try it out immediately.  This allows a great deal
of creativity and adventure in the writing of the game.  When you are
done fleshing out the structure (perhaps with short descriptions) you
can later put long descriptions, etc. onto disk and thereby enrich the
play environment.  After deleting the documentation comments it should be
possible to write rather large adventures using this system, at least
as large as commericially available adventures and possibly much
larger with careful disk use.  Use your imagination and have fun!

                                -Mitsu