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