PlatypusTutorial for Platypus release 4

  1. Introduction
  2. Getting Started
  3. Making Room
  4. Making Broom
  5. I Like To Say "Beaker"
  6. Down and Dirty
  7. Life's Little Conundrums
  8. Even More Useless Information
  9. I'd Like To Thank The Academy
  10. Things Are Not What They Seem
  11. Conclusion

Send questions, comments, and bug reports to Anson Turner.

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".


Platypus1. 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).


Platypus2. 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.


Platypus3. 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.


Platypus4. 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.


Platypus5. 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.


Platypus6. 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.


Platypus7. 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.


Platypus8. 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}.


Platypus9. 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.


Platypus10. 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.


Platypus11. 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. Also, make yourself a sandwich. You deserve it. While you're at it, make me one too. No meat, dairy, eggs, or honey. What? Oh, fine, I'll make it myself. Do you have any spreadable fruit around here? I don't think this bread's any good, unless there's a new green novelty bread I don't know about. Ewww, this peanut butter's all separated. Um, I don't think I need that big of a knife to cut this. And you shouldn't point knives at people, it's dangerous. Hey, what are you doing? Get away from me with that! Help! Help!

-- Anson Turner
(formatted by John G. Wood)


This page originally found at http://www.elvwood.org/InteractiveFiction/Platypus/Tutorial.html
Last updated: 14 March 2002

Notices & Disclaimers

Home
Home

Interactive Fiction
IF

Platypus Index