Tutorial for Platypus release 4+ ================================ Send questions, comments, and bug reports to platypushome@yahoo.com. Conventions followed in this document: Attribute: -light- Class, or member: [Rooms] Global variables: =actor= Obsolete: lockable* Property: +description+ Property routine: +parse_name()+ Routine: InDark() Googly eyes: (@)(@) Summary section references are enclosed in {curly braces}. For example, {4d} means "see section 4d of the Summary". 1. Introduction This document provides an introduction to developing a simple work of interactive fiction using the Platypus library for Inform. It is intended for those who are already familiar with the standard library. It will probably be inadequate for someone who is altogether new to Inform. Use of the compiler itself will not be covered at all. It is assumed that you will configure it appropriately so that the Platypus library files can be found (e.g. through an ICL file). Note that although Infix can be used with Platypus, it is not included. You should copy "infix.h" from the set of standard library files into your Platypus directory. Infix does not, as of this writing, work with Glulx. Note also that both Platypus and the standard library have files called "English.h" and they are NOT identical. Thus, you should keep the two sets of files separate (not in the same directory). 2. Getting Started (Note: A completed "tutorial.inf" file is included with the Platypus distribution.) We start with "skeleton.inf", which supplies the essentials: Constant Story "INSERT TITLE HERE"; Constant Headline "INSERT DESCRIPTION/COPYRIGHT HERE"; Include "First"; Include "Middle"; ! OPTIONAL COMPONENTS: ! Include "footnote"; ! Include "scenery"; ! Include "nameable"; ! MAIN CODE GOES HERE Include "Last"; ! NEW GRAMMAR GOES HERE No Initialise() routine is provided by the skeleton, because it is supported, but not required, by the library. 3. Making Room Let's get started by adding a room to the "main code" section: Rooms Laboratory "Laboratory" has light, with name 'laboratory' 'lab', description "This room is remarkable for its non-descriptness. There is a single exit to the south.", dirs sdir Closet, startup [; move player to self; ]; The first thing to notice is that the room is of the [Rooms] class {3}. The [Rooms] class provides the +fpsa+ property which is used by FindPath() {6a}, a routine used to find a path between two rooms. The class also provides some simple default behavior for a room, such as converting LISTEN TO LAB into an ordinary LISTEN command. (You can override this in the individual room's +respond()+ property.) That brings us to our next point: the +name+ property now contains the actual names of the room rather than "scenery" words. This is particularly important for the GO TO command {14a}, which allows the player to go back to an already-visited room. The GO TO command finds a path for the player using FindPath(). The next change to notice is the +dirs+ property {3c}. This replaces all of the direction properties (n_to*, sw_to*, etc.) from the standard library. There are two forms the +dirs+ property can take, array and routine. In the array form, it contains a list of one or more direction objects (ndir, swdir, outdir, etc.), after each set of which is a room object, a door object, or a string to print. We'll see the routine form in a moment. Finally, we come to the +startup()+ property. This property routine is called for any object that provides it when the program first starts. We've used the Laboratory's +startup()+ property to move the =player= there, as that is where we want to begin the story. Another way to do this would be to provide an Initialise() routine. We could also have specified the player's starting location by setting player.location directly. Both methods are acceptable ONLY in Initialise() or a +startup()+ routine. Once the game is underway, move [Actors] by calling MoveTo() {11b} (or using ##Go). As it stands, the program is not compilable, because we haven't supplied the Closet object yet. After the code for Laboratory, let's add: Rooms Closet "Broom Closet" has light, with name 'closet', adjective 'broom', description "This is just a small, plain closet.", dirs [ d; if (d == ndir or outdir) return Laboratory; ], points 2; ...which introduces two new properties, +adjective+ and +points+. The words in +adjective+ can be used to refer to the object, but the parser will give priority to the +name+ property. This becomes important when we add a broom, as we'll do in the next section. When a room has a +points+ property, the =player= receives the +points+ upon first entering the room. We'll look at other uses for this property later. If you type SCORE, you will see that the library has automatically added the +points+ of the Closet to the maximum score. The maximum score is held in a global variable, =maximum_score=, so it can be set manually if desired. +points+ can be negative, and negative +points+ do not affect the default maximum score. As promised, we also see the routine form of +dirs+ (or +dirs()+). It takes a direction object as a parameter and returns the room object (or door, or string) that the direction leads to, or 0 if there is no exit in that direction. 4. Making Broom Beneath the code for Broom Closet, we add a rather poorly-implemented broom: Object -> broom "broom" with name 'broom', description "It's broom-like."; Now, compile and run the program. GO SOUTH into Broom Closet and enter EXAMINE BROOM. The parser automatically matches the command against the broom and not Broom Closet, because 'broom' is the +name+ of the broom, but only an +adjective+ for the closet. (As it happens, if you change 'broom' from an +adjective+ to a +name+ for Broom Closet, the parser would still assume that EXAMINE BROOM referred to the broom. That is because the player is considered less likely to be referring to the name of a room, except in a GO TO command. However, the parser would then have printed "(the broom)" to indicate its assumption.) PICK UP THE BROOM, GO NORTH back into Laboratory, and DROP IT. Then, GO TO CLOSET and enter EXAMINE BROOM. Now the parser assumes you mean the Broom Closet, because it is the only object in sight that matches the word 'broom'. The +respond()+ routine for the [Rooms] class converts an ##Examine action on the room into a ##Look command. (We'll cover +respond()+ later.) If you provide the constant WEAK_ADJECTIVES, the above experiment does not work. With WEAK_ADJECTIVES, the words in an object's +adjective+ property can only supplement names, not replace them. In other words, the player must supply at least one +name+ in order to refer to an object. (This is the way adjectives work in TADS, for example.) In the above example, the word 'broom' would never match the Closet unless the word 'closet' were also entered. 5. I Like To Say "Beaker" Going back a bit, let's add the following items under the code for Laboratory: Object -> workbench "workbench" has supporter hider static, with name 'workbench' 'bench' 'table', description "A sturdy table.", allow_entry [ a; if (a == upon) rtrue; ]; Object -> -> beaker "beaker" has container transparent open, with name 'beaker' 'bottle', adjective 'glass', description "A glass bottle with a wide mouth.", points 5; Now recompile and run the program. You will see the workbench is now in the Laboratory. But where is the beaker? To answer that, let's first look at the workbench code, starting with the "has" line. The workbench is both a -supporter- and a -hider-. A -supporter- can have objects placed upon them, while a hider can have objects placed under it. In the standard library, an object can be a -supporter- or a -container-, but not both. In Platypus, an object can be a -supporter-, a -container-, or a -hider-, or any combination of the three {5}. The library needs to know whether the object's children are on top of it, inside of it, or underneath it. This is handled via three attributes: -upon-, -inside-, and -under- {5a}. We did not give the beaker any of those attributes. For that reason, it is considered "buried" inside the workbench and inaccessible {5b}. Children of a "holder", that is, a -supporter-, -container-, or -hider-, must have one (and only one!) of the three position attributes in order to exist in the game environment (i.e., to be in scope). (Exception: if an object is -transparent-, children with no position attribute are in scope, but are treated as attached to the parent and can't be taken.) Of course, the position attribute given must match one of the parent's holder attributes. Since the workbench is both a -supporter- and a -hider-, we can give the beaker either the -upon- or the -under- attribute in order to bring it into play. Let's put it on top of the workbench. Add -upon- to the list of attributes provided in the "has" line for the beaker and recompile. Now you should see the beaker. To make things a bit less tedious, there is a shortcut to setting up the positions of objects. Call SetDefaultObjectPositions() {11a} in your Initialise() routine or a +startup()+ property. It will automatically set an appropriate position attribute for any object that is in need of one. Of course, if more than one position is possible, the routine cannot know your intentions. A strict order of priority is followed: -upon- is preferred to -inside-, which is preferred to -under-. The MoveTo() routine {11b} can be called in order to move objects once the game is underway. If we wanted to "teleport" the broom onto the workbench, we would call MoveTo(broom, workbench, upon). The first parameter is the object to move. The second is the object to move it to (that is, its new parent). The third parameter, which is optional, sets the position attribute. If it is not provided, it will be set by default as described in the preceding paragraph. MoveTo() is also used for moving [Actors], including the =player=. The beaker has been given the +points+ property. Because it is a takeable object, picking up the beaker gives the player 5 points (only the first time!). Once again, the +points+ are automatically figured into the =maximum_score= (now at 7). Let's go back to the workbench code, and give it the -transparent- attribute. In addition to its effect on containers, -transparent- causes the -under- contents of a -hider- to be listed in room descriptions. (Of course, this makes "hider" a misnomer.) We want the player to readily see anything that has been placed under the workbench. Without -transparent-, the player would have to LOOK UNDER THE WORKBENCH in order to see what's there, although the objects would still be in scope. At this point, you may want to try putting the broom under the bench and typing LOOK, LOOK UNDER THE BENCH, LOOK ON THE BENCH, TREE BENCH. You could also try ;GIVE BENCH ~TRANSPARENT, assuming you have compiled with Infix. The broom will no longer be shown in the room description, though it will still be present, hidden under the bench. We also gave the workbench the +allow_entry()+ property routine {5c}, which replaces the enterable* attribute. It takes one parameter, a position attribute, and returns true if the object can be entered by [Actors] in that way. The +allow_entry()+ routine for the workbench returns true for -upon-, but not for -under-, so [Actors] can climb onto the workbench but cannot crawl beneath it. 6. Down and Dirty To illustrate some other points, we'll make some changes to the workbench. Delete -transparent- from the "has" line, and change the +allow_entry()+ routine to always return true. Then we add an +inside_description()+: Object -> workbench "workbench" has supporter hider static, with name 'workbench' 'bench' 'table', description "A sturdy table.", inside_description [; if (actor has under) "It's dusty down here."; ], allow_entry [; rtrue; ]; It is now possible to CRAWL UNDER THE BENCH. Because it is not -transparent-, the normal room description is not shown while the player is beneath it, nor are the items -upon- the bench shown. However, LOOK ON BENCH still works, and scope is unaffected. Notice that +inside_description()+ checks to see that the =actor= is -under- the bench, as the text would be incongruous if the actor were on top of it. A more proper way of writing the condition would be: if (IndirectlyContains(self, actor) == under) By using IndirectlyContains() {11f}, we would ensure that the "dusty" message would be printed even if the player were sitting -upon- a rug which was itself -under- the bench. Let's modify the workbench so that the player cannot see or reach the items on top of it while beneath it. This will require the use of +respond() and +meddle()+. Under the standard library, there are five "reaction" properties: three individual (before*, after*, and life*), and two of general scope (react_before* and react_after*). In Platypus, there are -- count them -- ten reaction properties {2c}: six individual (+respond_early()+, +respond_early_indirect()+, +respond()+, +respond_indirect(), +respond_late()+, and +respond_late_indirect()+) and four of general scope (+meddle_early()+, +meddle()+, +meddle_late()+, and +meddle_late_late()+). +respond_early()+ and +respond_late()+ work just like before* and after*. The only difference arises with respect to rooms, where they only react to actions directed at the room itself, not to all actions taking place within it. +repond_early_indirect()+ and +respond_late_indirect()+ are much the same, except that they are called for the indirect object (if any). (Fake actions for indirect objects, such as ##LetGo* and ##ThrownAt*, are not used by Platypus.) +meddle_early()+ and +meddle_late()+ work like react_before* and react_after*. Again, they are different only when provided by rooms, in which case they act like before* and after*, reacting to actions which take place in the room. Unlike any other object, an actor's +location+ can react to actions that take place out of scope (i.e., inside an opaque container). +respond()+ and +meddle()+ are called immediately before an action takes place, but after the library has determined that the action is possible. +respond()+ is called for the direct (=noun=) and +respond_indirect()+ for the indirect object (=second=) of the action, and +meddle()+ is called for every object in scope. To give an example, if the =actor= tries to ##Take something which is locked in a -transparent- box, +respond()+ and +meddle()+ won't be called at all, because the action is impossible and does not reach the "about to happen" stage. There is no equivalent to life*, as there is no need for one. In most cases, +respond()+ or +respond_indirect()+ would be used instead. First, we add a +respond()+ routine to the workbench to prevent the player from using ##LookOn on it while under it. Then we add a +meddle()+ routine to prevent the player from doing anything with the items on the workbench while under it: respond [; LookOn: if (IndirectlyContains(self, actor) == under) "You'll have to get out from under the workbench first."; ], meddle [; if (noun == 0 || IndirectlyContains(self, actor) ~= under) rfalse; if (IndirectlyContains(self, noun) == upon || (second && IndirectlyContains(self, second) == upon)) "You'll have to get out from under the workbench first."; ]; We could have used =player= in place of =actor= in our conditions, since there are no other [Actors] in the program so far, but using =actor= is a good habit. Of course, if we do introduce any [Actors] who might trigger these reactions, we will need to modify them anyway, to prevent incongruous "You'll have to get out..." messages from appearing. The simplest way to do so would be to preface them with: if (actor ~= player) rfalse; which would prevent them from applying to anyone other than the =player=. 7. Life's Little Conundrums So far, the player can score 2 points by entering the Closet, and 5 points for picking up the beaker, but those points aren't very satisfying. Let's add a more ambitious goal, like putting the broom into the beaker. First, we create an object to represent the task {7}: Object silly "finding a silly bug" with points 10; ... being careful not to put the object before anything using the -> syntax. Then we give the beaker a +respond_late_indirect()+ routine: respond_late_indirect [; Insert: if (noun == broom) Achieved(silly); ]; since the broom is the indirect object (that is, the =second=) of ##Insert in this case. Now, compile and run the program, TAKE THE BEAKER AND GO SOUTH. Then, TAKE THE BROOM, AND PUT IT IN THE BEAKER. Then get your FULL SCORE. Tasks are listed in the order in which they are achieved. You can, however, change this by giving them +number+ properties. {7b} By default, all tasks have a +number+ of 0, and negative numbers are allowed (and cause tasks to appear earlier in the list, of course). You can give the same number to as many tasks as you like; they will be subsorted in order of achievement. The "finding sundry items" and "visiting various places" lines (which appear if the player receives +points+ from a taking an item or entering a room with +points+) are treated as tasks numbered 30000 and 30001, respectively. Thus, they will generally appear at the end of the list. You are free to change the +number+ properties of the finding_items and visiting_places objects (e.g., during startup) to relocate them in the list of achievements. 8. Even More Useless Information For no reason at all, let's attach a footnote to the broom's +description+. First we have to include the footnote code {7d}, so we delete the ! before the line: Include "footnote"; Then we create our footnote: Footnotes broomnote "Like a broom, in other words."; And change the +description+ of the broom: description [; "It's broom-like.",(note) broomnote; ]; And that's it. Use the NOTE command {7g} to view the footnote (once you've examined the broom), in this case, NOTE 1. Use the NOTES command {7h} to review all of the footnotes that you have viewed with the NOTE command. We could also give the broomnote a +number+ property if we wanted the note to have a specific number (anything from 1 to 32767) {7e}. You should not give the same number to more than one note. If you want the note references to stop appearing after the player has read the associated notes, put: give FootnoteGizmo general; in your startup code {7f}. 9. I'd Like To Thank The Academy Now we add a more complicated object: Actors scientist "scientist" has activedaemon female, with location Laboratory, name 'scientist' 'woman', description "She looks like an ordinary scientist.", daemon [; if (random(2) == 1) { switch(random(2)) { 1: if (self in Laboratory) self.perform(##Go, sdir); else self.perform(##Go, ndir); 2: if (beaker in self) { if (self in Laboratory) self.perform(##PutOn, beaker, workbench); } else if (TestScope(scientist, beaker)) { if (IndirectlyContains(workbench, beaker) ~= under) { if (beaker in player) { MoveTo(beaker, scientist); "~What are you doing with that?~ the scientist exclaims, taking the beaker away."; } else self.perform(##Take, beaker); } else if (TestScope(scientist)) "The scientist looks around, scratching her head."; } } rtrue; } if (TestScope(self) == 0) rfalse; switch(random(5)) { 1: "^The scientist says, ~Hmmm.~"; 3: "^The scientist says, ~Aha!~"; } ]; Object -> coat "white coat" has clothing worn, with name 'coat', adjective 'white' 'lab', description "Very official."; Let's begin at the beginning. The scientist is of the [Actors] class {4b}. This will allow us to invoke the +perform()+ property {4c} for her, as we'll see shortly. We've given her the -activedaemon- attribute so that her +daemon()+ will be running as soon as the game starts. (We could also call StartDaemon(scientist), which would do the same thing.) Her +location+ property {4e} has been given the initial value of Laboratory. This will cause the library to automatically place her there at startup. The +name+ and +description+ lines should be self-explanatory, so let's skip ahead to her +daemon()+. The first switch statement has 2 possible outcomes. The first simply causes the scientist to walk from one of the two rooms to the other by invoking +perform()+ with a ##Go action, with the appropriate direction object. The library will display the arrival or departure message to the player, as appropriate. The second possibility deals with the beaker. If the scientist is carrying the beaker, she will place it on the workbench (again using +perform()+, this time with ##PutOn) if she is in the Laboratory. Otherwise, she will take the beaker if it is in scope to her, unless it is hidden under the workbench. If the =player= is holding the beaker, she will still take it, but we cannot use +perform()+ for this, because ##Take does not allow objects to be stolen from other [Actors]. Instead, we call MoveTo() and print an appropriate message. Note that because we use IndirectlyContains() to check whether the beaker is under the workbench, the player can hide the beaker from her either by putting it under the workbench, or by crawling under there while holding it. If the beaker is not in scope to the scientist, we simply print a message expressing her confusion, if she is in scope to the =player=. There is a 1-in-2 chance of bypassing the first switch statement altogether, in which case we just print a random message (sometimes). But first we call TestScope() to prevent the message from being printed if the player isn't around. The scientist's coat will be in scope to the player whenever the scientist is, because it is -worn-. However, anything the scientist is carrying, such as the beaker, will not be. In other words, once the scientist picks up the beaker, it disappears as far as the player is concerned until she puts it down again. (It remains in scope for her, of course.) We can change this by giving her -transparent-. The player will then be able to see (and refer to) what she is carrying. 10. Things Are Not What They Seem Now, to illustrate a few final points, we modify the broom object, and add another actor: Object -> broom "broom" with short_name [; give self activedaemon; ], parse_name [ wd c fl; while ((wd = NextWord()) ~= 0) { if (wd == 'broom') { c++; fl = 1; } else if (wd == 'alien' && fl) { c++; give self general; } else break; } return c; ], description [; "It's broom-like.",(note) broomnote; ], meddle_early [; if (self has general) { Transmogrify(broom, broom_alien); give self ~activedaemon; give broom_alien activedaemon; print "(Lucky guess.)^"; } ], daemon [; if (TestScope(self) == 0) { Transmogrify(broom, broom_alien); give self ~activedaemon; give broom_alien activedaemon; } ]; Actors broom_alien "broom alien" has neuter, with name 'broom' 'alien', description "Weird.", allow_take [; rtrue; ], daemon [; if (self has general) { give self ~general; return; } give self general; if (self in workbench) self.perform(##Exit); if (self.location == player.location) return; if (FindPath(self.location, player.location, self)) self.perform(##Go, self.&path_moves-->0); ], messages [; ExitFromUpon: "^#A# hops off #o#."; ExitFromUnder: if (lm_o has transparent || IndirectlyContains(lm_o, player) == under) "^#A# skitters out from under #o#."; "^#A# appears from under #o#."; Go: if (lm_n == 5002) "^#A# hops into the room."; ]; Yikes! Well, the broom's not quite as boring now. It is in fact a broom alien. There are two ways the player can discover that. The first is by calling it "broom alien". The second is by leaving it behind, in which case it will follow the player around (which an ordinary broom would be unlikely to do). However, the alien is slow and only gets to move every other turn. Let's go over the code for the broom object. The +short_name()+ routine does not print its name, but instead activates its +daemon()+. This is a sneaky way of causing the +daemon()+ to activate the first time the player sees the broom. Once started, the +daemon()+ causes the broom to change into the broom alien as soon as the player is out of scope. This is done with a call to Transmogrify(), a routine which can be used whenever something in the game is changed such that it must be represented by a different object from that point. We also start the alien object's +daemon()+ and stop that of the broom. A +parse_name()+ routine takes the place of +name+ and +adjective+. In this case, we use it to determine whether the player has referred to the broom as a "broom alien". If so, the broom is swapped immediately with the alien, and the player's deduction is acknowledged. This is handled in the broom's +meddle_early()+ routine, since by that time parsing is finished. Turning to the broom_alien object, we have given it an +allow_take()+ property {4j} which always returns true. This allows any other actor to pick up the alien. The +daemon()+ for the alien toggles its -general- attribute, and returns immediately if it was set. This effectively causes the +daemon()+ to only execute every other turn. It always leaves the workbench if it has been placed on or under it, via an ##Exit action. If the alien is not near (in scope of) the =player=, then FindPath() is called to find a path from the alien's +location+ to the player's. Because the =player= is a moving target, we call FindPath() again for each move, rather than calling it once and following the complete path. (Ignore for the moment the fact that there are only two rooms in the game.) If FindPath() returns true, indicating that a path was found, we execute a ##Go action for the alien, with the first direction object in the alien's +path_moves+ property. The [Actors] class provides the three properties that are set by FindPath(): +path_length+, +path_moves+, and +path_rooms+. +path_length+ indicates the number of moves in the path, +path_moves+ contains the directions to ##Go in, and +path_rooms+ contains a list of the [Rooms] that the directions lead to. So, after executing a ##Go action with the first direction on the list, the =actor= should be in the room indicated by the first entry in +path_rooms+. If not, something has gone wrong with the path: either the =actor= was unable to go in the given direction due to some obstacle, or the room that the direction leads to has changed. And so on for each move in the path. For the simple movement of the broom alien, we never look at anything except the first entry of its +path_moves+ property. Note that although the alien will exit from the workbench, it cannot escape if put into the beaker. Also, the library takes care of updating an actor's +location+ even when the actor is carried around by someone else. We have provided a +messages()+ property (making use of print codes {4i}) for the alien to replace the generic messages associated with its movements. The library will, of course, only print these messages when the alien is in scope to the player. Notice that the message for ##ExitFromUnder changes depending on whether the alien is appearing from beneath an opaque hider. 11. Conclusion That concludes the tutorial. If you haven't already, now might be a good time to look through the summary and reference files for the things that weren't covered here. Unless the room is on fire. Then it might be a good time to leave.