This file is part of the TADS 2 Author’s Manual.
Copyright © 1987 - 2002 by Michael J. Roberts. All rights reserved.
The manual was converted to HTML and edited by NK Guy, tela design.


Chapter Ten


Advanced TADS Techniques

So far, I’ve concentrated on the basic aspects of writing a game with TADS. Once you get started on a game, you’ll probably have lots of ideas for plot elements in your game. Many parts of an adventure game can be implemented very easily with TADS, simply by piecing together the basic classes from adv.t and filling in descriptions, locations, and so forth. However, some of the things you will want to implement won’t be that easy, and you will need to do some programming.

This chapter helps you realize your more complex ideas by showing how to implement a number of sophisticated plot elements. I’ve tried to show examples of the most common difficult game features, so there’s a good chance that you’ll find an example in this chapter that’s very similar to what you’re trying to do. Even if you don’t find a close match to the feature you have in mind, reading this chapter will show you many of the more subtle points of TADS programming, which should give you a better idea of how to implement your ideas.


Creating your own Verbs

It’s always best to try to limit the number of verbs a player needs to use to complete your game, because it helps prevent your adventure from becoming a game of “guess the verb.” Choosing the right sentence to express an idea for a command should never be a challenge - if it is, your game will quickly become uninteresting.

However, that being said, you will sometimes want to include situations in your games that call for verbs that aren’t in the set defined in adv.t. In addition, there’s nothing wrong with making your game understand obscure verbs and phrasings that are alternatives to one of the more common verbs in adv.t. Furthermore, some games include “magic words” that are effectively special verbs; since the player learns these within the game, obscurity is not a problem.

The way you add a verb depends on how the verb is used. The simplest kind of verb is used by itself - the player doesn’t include any objects in a command with the verb. For example, “north” is a verb that doesn’t take any objects; magic words generally fall into this category as well. To implement a verb with no objects, all you have to do is create a deepverb object that includes a verb vocabulary property, an sdesc property, and an action( actor ) method. (The deepverb class is defined in adv.t; see Appendix A for details.) The action method is called when the verb is used with no objects. As an example, let’s implement a verb for a magic word; when the magic word is used, the player will be magically transported from the cave to the shack, or the shack to the cave.

  magicVerb: deepverb
    verb = 'xyzzy'
    sdesc = "xyzzy"
    action( actor ) =
    {
        if ( actor.location = cave )
        {
          "Out of nowhere, a huge cloud of orange smoke
          fills the cave.  When it clears, you suddenly
          realize that you're in the shack! ";
          actor.moveInto(shack);
        }
        else if (actor.location = shack)
        {
          "You suddenly feel very disoriented, and the
          room seems to be spinning all around you.  As you
          gradually regain your balance, you realize that
          you're now in the cave! ";
          actor.moveInto( cave );
        }
        else
          "Nothing happens. ";
    }
  ;

For a verb that takes a direct object, the verb object itself is fairly simple; all you need to do is define verb and sdesc properties, plus the special property doAction. The doAction property defines a string that the system uses to form the names of two new properties. As an example, we’ll implement the verb “smell,” which takes as its direct object the item to be smelled.

  smellVerb: deepverb
    verb = 'smell'
    sdesc = "smell"
    doAction = 'Smell'
  ;

When the system sees this definition, it creates two new properties: verDoSmell and doSmell. Now, for any object that the player can smell, you add the verDoSmell and doSmell methods. For example, we’ll implement a flower (notice that we deftly bypass the opportunity for a cheap joke here at the expense of good taste by choosing an object with a non-offensive odor):

  flower: item
    noun = 'flower' 'rose'
    adjective = 'red'
    sdesc = "red rose"
    ldesc = "It's a red rose. "
    verDoSmell( actor ) = {}
    doSmell( actor ) =
    {
      "A rose by any other name... ";
    }
  ;

If the verDoSmell method isn’t defined for an object, trying to smell it will cause the system to display the message “I don’t know how to smell the object.” Since “smell” is a verb that you may want to allow for every object in your game, you may want to modify the definition of thing in adv.t to provide a better default message for this verb. You could add these lines to the definition of thing in adv.t:

  verDoSmell( actor ) = {}
  doSmell( actor ) =
  {
    "It smells like any other <<self.sdesc>>. ";
  }

If you do this, all you need to do is override the doSmell method for any object with a non-default odor.

Adding a verb that takes two objects (a direct object plus an indirect object) is similar. Rather than defining a doAction property, you define an ioAction(preposition) property. For example, suppose you want to implement a verb “pry direct-object with indirect-object”:

  pryVerb: deepverb
    verb = 'pry'
    sdesc = "pry"
    ioAction( withPrep ) = 'PryWith'
    prepDefault = withPrep
  ;

This causes the system to create the properties verIoPryWith, verDoPryWith, and ioPryWith, which the system will call in that order (verDoPryWith is called for the direct object, and the other two are called for the indirect object.) Note that we’ve also defined a default preposition; while this isn’t required, it allows the system to be smarter by guessing what the player meant when some words are left out. For example, if the only object present that can be used as an indirect object in the “pry” command is a crowbar, the system can provide the “with the crowbar” phrase by default when the player simply types “pry the door.”

Whether or not you add verbs to the basic set in adv.t, it’s a good idea to document your verbs. In the instructions for your game, you should provide a list of all of the verbs that are needed to complete the game. If players gets stuck, this list can be helpful in convincing them that they’re not fighting the parser.


Complex Room Interconnections

The basic travel routines used by adv.t make it easy to implement a basic map: all you have to do in your definitions is to provide properties with names like north and south, and set them to point to rooms, simply by naming the room:

  kitchen: room
    north = hallway
    east = porch
  ;

This is fine for most situations, but frequently you’ll want something to happen during travel that’s more complicated than simply moving the player to the given room. For example, if you have a very long hallway, you may want a special message to be displayed when travelling to indicate that the walk was especially long. Or, you may want to set off a trap when the player takes a certain exit. Or, you may want to prevent travel in a particular direction until a door has been opened or some other obstacle has been removed.

The basic trick to implementing special travel effects like these is to realize that the direction properties of a room aren’t limited to simple room pointers - you can write a method instead. As long as you follow a few simple rules, you can do whatever you want in this method. Basically, if you want normal travel to occur after the method is finished, the method should return a room object which indicates the destination of the travel. If you don’t want anything more to happen after the method is finished, the method should return nil. The following sections show how to use direction methods to implement a variety of special travel effects.


Display a Message During Travel

One of the simplest special effects is displaying a message during travel. For example, suppose you have a stairway leading down, but the stairs collapse while the player is descending them, dropping the player unceremoniously into the room below (with no harm done). All you really want to do here is to display a message about the stairs collapsing, then complete the travel as normal.

  livingRoom: room
    sdesc = "Living Room"
    ldesc = "You're in the living room.  A dark stairway leads down. "
    down =
    {
        "You start down the stairs, which creak and moan and
        wobble uncertainly beneath you.  You stop for a moment
        to steady yourself, then continue down, when the entire
        stairway suddenly breaks away from the wall and crashes
        to the floor below.  You land in a heap of splintered
        wood.  After a few moments, everything settles, and you
        manage to get up and brush yourself off, apparently
        uninjured.\b";
        return( cellar );
    }
  ;

The “\b” is at the end of the message to separate the message about the stairway’s collapse from the descriptive text that accompanies normal travel into a new room. Since the method just returns a room object, travel will proceed as normal after the message has been displayed.


Simple Obstacles

The collapsing stairway of the previous example will need some additional special consideration at the other end, because the player clearly can’t go back up the stairs once they’ve collapsed. The simplest case is to disallow travel altogether, which is simple: just make an up method that displays an appropriate message, then returns nil to indicate that no travel is possible in this direction.

  cellar: room
    sdesc = "Cellar"
    ldesc = "You're in the cellar.  A huge pile of broken pieces of
            wood that was once a stairway fills half the room. "
    up =
    {
        "The stairway is in no condition to be climbed. ";
        return( nil );
    }
  ;

Since the method returns nil, no further travel processing takes place after the message is displayed.

Note that we should add an object for the broken stairway, just for completeness, so that the player can mention the stairs in a command. We’ll make it a fixeditem since it can’t be moved or otherwise manipulated.

  brokenStairs: fixeditem
    location = cellar
    noun = 'stairs' 'stairway' 'pile' 'wood'
    adjective = 'broken'
    sdesc = "pile of wood"
    ldesc = "It's a huge pile of wood that used to be a stairway.
            You won't have much luck trying to climb it. "
  ;


Adaptive Room Descriptions

The stairway examples shown above could be enhanced to make it possible to enter the cellar by some other passage, which would mean that the stairway could be seen before it collapses. If this were done, it would be necessary for the cellar’s room description, as well as the up method, to be sensitive to whether the stairway had collapsed yet.

Making a room description that adapts to the state of the room is done by turning ldesc into a method. Just as special travel effects can be achieved by making a direction property into a method, special room descriptions can be programmed by making ldesc a method.

Since the stairway can be seen before it’s collapsed, we’ll introduce a new object for the working stairs. Note that this object should be climbable, so we’ll define verDoClimb and doClimb methods for it. Note that the method actor.travelTo(location) is the exact same method that is called when the player travels normally by typing a direction command (such as “up”). Also note that the location property of the brokenStairs object should initially not be set (which will make it nil), since the broken stairway is not part of the game initially in the new configuration.

  workingStairs: fixeditem
    location = cellar
    noun = 'stairs' 'stairway'
    sdesc = "stairs"
    ldesc = "The stairway leads up.  It looks extremely
            rickety. "
    verDoClimb( actor ) = {}
    doClimb( actor ) =
    {
      actor.travelTo( livingRoom );
    }
  ;

Now we’ll change the cellar object’s up method, as well as its ldesc property, to reflect the current status of the stairs. We’ll detect whether the stairs have collapsed yet by checking the location of the workingStairs object: if it’s in the cellar, we’ll know the stairs haven’t collapsed yet, and if it’s not anywhere (that is, if it’s location is nil), we’ll know that the stairs have collapsed. (This object switch will be implemented below, in the livingRoom object’s down method.) We’ll use this detection technique to make both the ldesc and the up method sensitive to the status of the stairway.

 cellar: room
    sdesc = "Cellar"
    ldesc =
    {
        "You're in the cellar. ";
        if ( workingStairs.location = nil )
            "A huge pile of broken pieces of wood that was
            once a stairway fills half the room. ";
        else
            "A suspicious-looking stairway leads up. ";
    }
    up =
    {
        if ( workingStairs.location = nil )
        {
            "The stairway is in no condition to be climbed. ";
            return( nil );
        }
        else
        {
            "The stairs creak and moan and sway, but you
            somehow manage to climb them safely.\b";
            return( livingRoom );
         }
     }
  ;

We’ll also have to make a small change to the down method in the livingRoom object, to replace the workingStairs object with the brokenStairs object. Here’s the new down method, which will remove the workingStairs object from the game by moving it to nil, and put the brokenStairs object in its place.

  livingRoom: room
    sdesc = "Living Room"
    ldesc = "You're in the living room.  A dark stairway leads
             down. "
    down =
    {
        if ( workingStairs.location = nil )
        {
            "You quickly notice that the stairs have
            collapsed, so there's no way down. ";
            return( nil );
        }
        else
        {
            "You start down the stairs, which creak and moan and
            wobble uncertainly beneath you.  You stop for a moment
            to steady yourself, then continue down, when the entire
            stairway suddenly breaks away from the wall and crashes
            to the floor below.  You land in a heap of splintered
            wood.  After a few moments, everything settles, and you
            manage to get up and brush yourself off, apparently
            uninjured.\b";
            workingStairs.moveInto( nil );      /* replace workingStairs... */
            brokenStairs.moveInto( cellar );       /* ... with brokenStairs */
            return( cellar );
         }
     }
  ;


Doors

One common type of obstacle is a door. Doors can be implemented using exactly the same mechanisms shown above for the stairway: in the directional properties for the rooms involving the door, check the status of the door object (generally the isopen property), and either return a room object if the door is open, or display a message and return nil if the door is closed. This type of mechanism is used in DITCH.T for its doors.

However, adv.t provides a set of classes that makes it easier to implement doors. The classes are based on a general class called obstacle (which you may wish to extend into your own specialized classes to implement different types of obstacles). The basic class is doorway, and an additional class called lockableDoorway makes it easy to implement a door that can be locked and unlocked with a particular key object.

By using the doorway and lockedDoorway classes, you can implement doors with no programming, because the basic travel routines will do the necessary work based on properties defined in your doorway objects.

The first thing to note about doors is that they have an unusual property: a door exists in two rooms, because it connects the two rooms. Unfortunately, TADS doesn’t have any good way of putting a single object in multiple locations. To work around this problem, you should make two objects for each actual door in your game: one for each side. Fortunately, the doorway class makes it easy to keep the states of the two objects synchronized: for example, when you open one side of the door, the other side is automatically opened.

Start by implementing the two sides of the door. Make a doorway object for each, filling in the normal properties for any object (location, sdesc, noun). Put one doorway object in each room to be connected. Now add the special properties for a doorway. Set otherside to the other door object making up the pair of doors; this is how the door’s internal routines know what other door to keep synchronized. Set doordest to the room object that’s reached by travelling through the door.

Then, all you need to do for the rooms containing the doors is to set the direction properties to the doors themselves, rather than the rooms reached through the doors. Here’s an example, showing the two room objects and the two door objects.

  porch: room
    sdesc = "Porch"
    ldesc = "You're on a porch outside a huge run-down wooden house
            that was probably painted white in the distant past,
            but which is gray and faded now.  A door (which is
            << porchDoor.isopen ? "open" : "closed" >>) leads west. "
    west = porchDoor                   /* use door for the direction */
  ;

  frontHall: room
    sdesc = "Front Hall"
    ldesc = "You're in the spacious front hallway of the old house.
            The paint on the walls is all peeling, and a thick veil
            of spiderwebs hangs down from the ceiling.  A door
            (which is <<hallDoor.isopen ? "open" : "closed">>)
            leads east. "
    east = hallDoor
  ;

  porchDoor: doorway
    location = porch
    noun = 'door'
    sdesc = "door"
    otherside = hallDoor
    doordest = frontHall
  ;

  hallDoor: doorway
    location = frontHall
    noun = 'door'
    sdesc = "door"
    otherside = porchDoor
    doordest = porch
  ;

Note that there’s no need to define ldesc for a doorway, unless you want to. The default ldesc defined in the doorway object displays a message saying simply “It’s open” or “It’s closed” or “It’s closed and locked,” depending on the door’s state. If you want to override ldesc, you can use the isopen and islocked properties to determine the state of the door to be displayed in your custom message.

Some more properties of the doorway class are worth mentioning. If the door is not locked, the door will be opened automatically when the player tries to travel through it (such as by going west from the porch in the example above). This makes the game more convenient, because it’s not necessary to type “open the door” when that’s the obvious thing to do. However, in cases where you don’t want a door to be automatically opened, you can simply add noAutoOpen = true to the doorway object’s properties; this requires the player to explicitly open the door, even when it’s not locked.

If you want to require a key to lock and unlock the door, use the lockableDoorway class instead of doorway. Then, add the mykey property, setting it to the object which serves as the key (this object should be of class keyItem). If you don’t set the mykey property, the door can be locked and unlocked without any key; that is, the player simply types “lock door” and “unlock door” to lock and unlock it.

Note that there’s no requirement that the two sides of a door be identical. You can take advantage of this in certain situations. For example, if you want to have a door that needs a key from the outside, but doesn’t need any key from the inside (like most doors you’d find in a house), make both sides lockableDoorway objects, but only set the mykey property in the object that serves as the outer side of the door.


Vehicles

Vehicles are rooms that move. Basically, a vehicle is an extension to the idea of complex room interconnections; rather than having a room interconnection that merely displays a message, or sometimes disallows travel, a vehicle has a room interconnection that moves around. Usually, a vehicle will also have some other behavior, such as controls that operate the vehicle, or a viewscreen that shows the area outside the vehicle.

The first type of vehicle we’ll address is the fully-enclosed variety, such as a subway train or an elevator. We’ll address vehicles that are also objects in their own right, such as rubber rafts, in a later section (“Nested Room Vehicles” below). A fully-enclosed vehicle really just amounts to a room that has a direction property that changes, depending on where the vehicle is supposed to be. The vehicle doesn’t have a location property, because it’s just an ordinary room - it’s not an object in other rooms.

As an example, let’s implement an elevator that travels between two floors. The elevator will have “up” and “down” buttons to activate it, doors, and a display indicating which level it’s on.

To make the elevator more realistic, we’ll arrange for it to spend a couple of turns in transit as it moves between floors. To do this, we’ll use a “notifier” function that’s set when a button is used to start the elevator on its way. We’ll also use a flag to indicate when the elevator is in transit, so that pushing the buttons while the elevator is moving will have no effect.

The doors will be simpler than normal doors, since the user can’t directly manipulate them. They’ll open and close automatically.

  elevDoors: fixeditem
    location = elevRoom
    noun = 'door' 'doors'
    adjective = 'elevator' 'sliding'
    sdesc = "elevator doors"
    ldesc = "They're a standard pair of sliding elevator doors.
       They're currently <<self.isopen ? "open" : "closed">>. "
    isopen = true
    verDoOpen( actor ) =
    {
        "The doors are automatic; you can't open them yourself. ";
    }
    verDoClose( actor ) =
    {
        "The doors are automatic; you can't close them yourself. ";
    }
  ;

The buttons are fairly simple: they use the basic buttonitem class. We’ll set a property indicating the destination of the elevator when that button is pushed - the “up” button will take the elevator to the upper floor, and the “down” button will take the elevator to the lower floor. The doPush method checks to see if the elevator is already moving, and then to see if the elevator is already at the destination of this button.

To make less work for ourselves, we’ll define a class for the two elevator buttons, then make two objects based on the class.

  class elevButton: buttonitem
    location = elevRoom
    doPush( actor ) =
    {
        /* ignore if already in motion or already at destination */
        if (elevRoom.isActive or elevRoom.curfloor = self.destination)
            "\"Click.\" ";
        {
        else
        {
            "The doors close, and the elevator starts to move. ";
            elevRoom.isActive := true;
            elevDoors.isopen := nil;
            elevRoom.counter := 0;
            elevRoom.curfloor := self.destination;
            notify(elevRoom, &moveDaemon, 0);
         }
      }
   ;

  elevUpButton: elevButton
    sdesc = "up button"
    adjective = 'up'
    destination = floor2                     /* where to go when button is pushed */
  ;

  elevDownButton: elevButton
    sdesc = "down button"
    adjective = 'down'
    destination = floor1
  ;

You“ll recall that notify is a built-in function that sets up a “daemon,” which is a routine that you want the system to call after each turn. The call to notify in the example above tells the TADS run-time system to call the method moveDaemon in the object elevRoom after every turn (the third argument is 0, which means that the method should be called after each turn; if it had been non-zero, it would have specified a number of turns to wait before calling the method once).

The elevator object needs to be sensitive to its current location with its exit and north parameter. It also needs to be sensitive to the state of the doors. The daemon (that is started with the notify call in the buttons) controls the travel of the elevator.

  elevRoom: room
    sdesc = "Elevator"
    ldesc = "You're inside a small elevator.  Sliding doors (which are
       currently <<elevDoors.isopen ? "open" : "closed">>) lead north. "
    out = (self.north)
    north =
    {
        if ( elevDoors.isopen ) return( self.curfloor );
        else
        {
            "The doors are closed. ";
            return( nil );
         }
     }
    curfloor = floor1          /* start off on lower floor */
    moveDaemon =
    {
        self.counter++;
        switch( self.counter )
        {
            case 1:
            case 2:
            case 3:
              "\bThe elevator continues to travel slowly. ";
              break;

            case 4:
          "\bThe elevator stops, you hear a bell make a
              \"ding\" sound, and the doors slide open. ";
          elevDoors.isopen := true;
              self.isActive := nil;
              unnotify( elevRoom, &moveDaemon );
              break;
       }
        }
     ;

The “\b” sequences before the messages in the moveDaemon method are there because the routine runs as a daemon. This means it’s hard to predict exactly what will have been displayed just prior to the method’s execution, so it’s safest to put in a blank line to make sure that the daemon’s messages are set apart from the previous message. You should generally put in a blank line before the first message displayed by any daemon.

Note that you’ll have to do a small amount of extra work to finish this example. First, you’ll have to define the rooms floor1 and floor2. These are extremely simple if the only way to reach them is through the elevator, because you don’t have to worry about whether the elevator is present or not. If you want to be more sophisticated, and make the floors reachable without taking the elevator (for example, with a stairway), you’ll have to do two things: first, you’ll have to put some doors on each floor, and make sure they’re closed while the elevator isn’t on that floor; and second, you’ll probably want to make some way to call the elevator to the current floor.

Here’s a general class for making the doors on each floor. We’ll make the door’s isopen property simply check to see if the inner doors are open, and if so, whether the elevator is on the current floor (which is the door’s location). We’ll do the same thing for the verDoOpen and verDoClose methods that we did with the elevDoors object.

  class outerElevDoor: fixeditem
    sdesc = "elevator door"
    ldesc = "They're <<self.isopen ? "open" : "closed">>. "
    isopen = ( elevDoors.isopen and elevRoom.curfloor = self.location )
    noun = 'door' 'doors'
    adjective = 'elevator'
    verDoOpen( actor ) =
    {
        "The doors are automatic; you can't open them yourself. ";
    }
    verDoClose( actor ) =
    {
        "The doors are automatic; you can't close them yourself. ";
    }
  ;

Now you just need to add an object of this class for each floor; the only thing you need to set in each object is its location property. Then, on each floor, make the south property sensitive to the state of the doors, in the same way as the north property in the elevRoom object is sensitive to the state of the inner doors.

The other addition you’ll want to make is to add a call button on each floor. This is probably accomplished most easily by simply using the existing daemon that runs the elevator, and adding new buttons on the floors, outside the elevator, that activate the elevator. Here’s a basic class that can be used to make these buttons.

  class outerButton: buttonitem
    sdesc = "elevator call button"
    adjective = 'elevator' 'call'
    doPush( actor ) =
    {
        /* ignore if elevator is already here */
        if ( elevRoom.curfloor = self.location and elevDoors.isopen )
            "Your incredible powers of observation suddenly inform
            you that the elevator is already here. ";
        else
        {
            "\"Click.\"";
            if ( not elevRoom.isActive ) notify( elevRoom, &moveDaemon, 0 );
            elevRoom.isActive := true;
            elevDoors.isopen := nil;
            elevRoom.counter := 0;
            elevRoom.curfloor := self.location;
        }
     }
  ;

You’ll notice if you try to run this example that there’s a minor problem: the daemon that moves the elevator around displays messages as though the player were on the elevator. By using the same daemon to move the elevator around with the call buttons, we’ll have to change the messages so that they’re sensitive to whether the player is on the elevator or not. For motion messages, the player should only get the messages if the player is on the elevator. Messages concerning the doors opening, on the other hand, should be seen if the player is either on the elevator or at the location at which the elevator is arriving. The new moveDaemon method (for the elevRoom object) is shown below (the rest of the elevRoom object stays the same).

  moveDaemon =
  {
        self.counter++;
        switch( self.counter )
        {
          case 1:
          case 2:
          case 3:
            if ( Me.location = self )
                "\bThe elevator continues to travel slowly. ";
            break;
          case 4:
            if ( Me.location = self )
                "\bThe elevator stops, you hear a bell make a
                \"ding\" sound, and the doors slide open. ";
            else if ( Me.location = self.curfloor )
                "\bYou hear a bell make a \"ding\" sound,
                and the elevator doors slide open. ";
            elevDoors.isopen := true;
            self.isActive := nil;
            unnotify( elevRoom, &moveDaemon );
            break;
         }
    }

There are many more enhancements that you could make to the elevator control daemon to make the elevator more realistic. First, it would be fairly easy to add more floors, simply by adding more buttons. Second, it would be nice to provide some sort of indication of where the elevator is currently. In addition, real elevators use a somewhat different control algorithm. A real elevator usually has a current direction; it travels in that direction, stopping at each floor that’s been selected with a call button at the floor or with the matching button in the elevator. When it gets to the top or bottom floor, all of the selected buttons inside the elevator are cleared (i.e., de-selected). The elevator then reverses direction if necessary, servicing called floors. This type of algorithm would be a little more work to implement, and is left as an exercise to the reader.


Chairs and other Nested Rooms

A “nested room” is a room inside another room. In most cases, nested rooms are also objects in their own right. The principal feature of a nested room is that it isn’t enclosed. Typical examples of nested rooms are chairs and beds: these are objects within a room, but they’re also “rooms” in the sense that the player can be located in them.

Implementing a nested room in TADS is fairly simple, because adv.t defines a class called nestedroom that does most of the work. Furthermore, the classes chairitem and beditem let you implement the most common nested room objects with very little work.

One feature of nested rooms that is worth mentioning is that the player can not necessarily reach all of the objects in the enclosing room from a nested room. By default, none of the objects in the enclosing room are reachable from a nested room. However, you can easily make any objects reachable from the nested room by setting its reachable property to a list containing all the reachable objects in the enclosing room. If you want to set it to everything in the enclosing room, simply set it as follows:

  reachable = ( self.location.contents )

This says that everything contained in the enclosing room is reachable.


Nested Room Vehicles

Sometimes, you’ll want to make a nested room that’s also a vehicle. For example, you might want to implement a rubber raft that the player can carry around, but also inflate and use as a vehicle. Fortunately, there’s an adv.t class called vehicle that serves this function. The vehicle class is a carryable object that can be boarded as a vehicle.

Let’s implement an inflatable rubber raft. The first thing we need to do is build a standard vehicle object, but we’ll customize it so that it can only be boarded when it’s inflated. We’ll furthermore make it so that it can only be inflated when it’s not being carried, and we’ll make it impossible to take when it’s inflated.

  raft: vehicle
    location = startroom
    sdesc = "inflatable rubber raft"
    noun = 'raft'
    adjective = 'inflatable' 'rubber'
    isinflated = nil
    ldesc = "It's an inflatable rubber raft.  Currently,
             it's <<self.isinflated ? "inflated" : "not inflated">>. "
    verDoTake( actor ) =
    {
        if ( self.isinflated )
           "You'll have to deflate it first. ";
        else
           pass doTake;
    }
    verDoInflateWith( actor, iobj ) =
    {
        if ( self.isinflated ) "It's already inflated! ";
    }
    doInflateWith( actor, iobj ) =
    {
        if ( self.isIn( actor ) )
           "You'll have to drop it first. ";
        else
        {
            "With some work, you manage to inflate the raft. ";
            self.isinflated := true;
        }
    }
    verDoDeflate( actor ) =
    {
        if ( not self.isinflated ) "It's as deflated as it can get. ";
    }
    doDeflate( actor ) =
    {
        "You let the air out, and the raft collapses to a
        compact pile of rubber. ";
        self.isinflated := nil;
    }
    doBoard( actor ) =
    {
        if ( self.isinflated ) pass doBoard;
        else "You'll have to inflate it first. ";
    }
  ;

Note that we’ll have to define a couple of verbs, and it would be convenient to add an object for the pump as well.

  inflateVerb: deepverb
    sdesc = "inflate" 
    verb = 'inflate' 'blow up'
    ioAction( withPrep ) = 'InflateWith'
    prepDefault = withPrep
  ;

  deflateVerb: deepverb
    sdesc = "deflate"
    verb = 'deflate'
    doAction = 'Deflate'
  ;

  pump: item
    sdesc = "pump"
    location = startroom
    noun = 'pump'
    verIoInflateWith( actor ) = {}
    ioInflateWith( actor, dobj ) =
    {
       dobj.doInflateWith( actor, self );
    }
  ;

Once all of this is done, making the raft move around is very similar to making a fully-enclosed vehicle move around. The only real difference is that the raft will always have a location. As with most vehicles of this type, you’ll want to make it impossible for the player to get out of the raft while it’s in motion. To do this, you’ll need to override the doUnboard and out methods. For this example, let’s assume that the river (where the raft will travel) will be composed of a series of rooms, each of which is of class riverRoom. So, whenever the raft is in a riverRoom, we’ll prevent the player from getting out. To do this, we’ll add the methods below to the raft object. Note that these methods will inherit the corresponding vehicle methods when the raft is not floating down the river.

  doUnboard( actor ) =
  {
    if ( isclass( self.location, riverRoom ) )
        "Please keep your hands and arms inside the raft
         at all times while the raft is in motion. ";
    else
      pass doUnboard;
  }
  out =
  {
    if ( isclass( self.location, riverRoom ) )
        "You can't get out until you've landed the raft. ";
        return( nil );
    else
      pass out;
  }

The only thing left is to move the raft around. For this example, we’ll set things up as follows: each (non-river) room which borders on a river will have a property, toRiver, set to point to the bordering river room. Each river room which has a landing will have a property, toLand, set to point to the bordering non-river room. Each river room will also have a property, downRiver, set to the next river room downstream from the current room. We’ll introduce two new verbs: “launch” and “land.” When the player is in the raft, “launch raft” will set the raft in motion down the river (if it’s on land), and “land raft” will land (if there’s a landing nearby). The new verbs are below.

  launchVerb: deepverb
    sdesc = "launch"
    verb = 'launch'
    doAction = 'Launch'
  ;

  landVerb: deepverb
    sdesc = "land"
    verb = 'land'
    doAction = 'Land'
  ; 

Now we’ll add some more methods to the raft object: the verb handling methods for the new verbs, and a daemon that causes the raft to drift downstream while it’s in the river.

  verDoLaunch( actor ) = {}
  doLaunch( actor ) =
  {
    if (isclass( self.location, riverRoom ) )
        "You're already afloat, if you didn't notice. ";
    else if ( self.location.toRiver = nil )
        "There doesn't appear to be a suitable waterway here. ";
    else if ( Me.location <> self )
        "You'll have to get in the raft first. ";
    else
    {
        "The raft drifts gently out into the river. ";
        notify( self, &moveDaemon, 0 );
        self.counter := 0;
        self.moveInto( self.location.toRiver );
    }
  }
  verDoLand( actor ) = {}
  doLand( actor ) =
  {
    if ( not isclass( self.location, riverRoom ) )
        "You're already fully landed. ";
    else if ( self.location.toLand = nil )
        "There's no suitable landing here. ";
    else
    {
        "You steer the raft up onto the shore. ";
        unnotify( self, &moveDaemon );
        self.moveInto( self.location.toLand );
    }
  }
  moveDaemon =
  {
    "\bThe raft continues to float downstream. ";
    self.counter++;
    if ( self.counter > 1 )
    {
        self.counter := 0;
        if ( self.location.downRiver = nil )
        {
             "The raft comes to the end of the river, and lands. ";
             self.moveInfo( self.location.toLand );
             unnotify( self, &moveDaemon );
         }
        else
        {
            self.moveInto( self.location.downRiver );
            self.location.riverDesc;
        }
     }
  }

Note that we’ll expect each river room to have a property, riverDesc, which displays a message when the raft drifts into that room. The moveDaemon method will keep the raft in each river room for two turns, then move the raft to the next river room, calling riverDesc to note the entry. When the raft comes to the end of the river, the method will automatically land the raft; this means that the last river room must have a non-nil toLand property. You could alternatively put in a waterfall or other special effect when reaching the end of the river.

To build a river, all you have to do is define a series of rooms of class riverRoom, and point the downRiver property in each to the next room downriver. Landings are built by setting the toRiver and toLand properties of the landing room and corresponding river room, respectively, to point to each other.


Hiding Objects

Hiding objects is fairly simple, thanks to a set of classes defined in adv.t that make certain kinds of hiding automatic.

The adv.t classes make it possible to hide objects under or behind other objects, and to set up an object so that its contents are found only when the object is searched. The basic hider classes are underHider, behindHider, and searchHider. These classes are for the objects doing the hiding; the hidden objects are hidden inside these objects. For the objects being hidden, instead of setting their location properties, set the properties underLoc, behindLoc, or searchLoc to point to the respective classes. All hidden objects must be of class hiddenItem.

For example, to hide a key under a bed, make the bed an underHider object, and set the underLoc property of the key to point to the bed.

  bed: beditem, underHider
    noun = 'bed'
    location = startroom
    sdesc = "bed"
  ;

  key: item, hiddenItem
    noun = 'key'
    sdesc = "key"
    underLoc = bed
  ; 

The behindHider and searchHider objects work the same way, but you should use the behindLoc and searchLoc properties, respectively, instead of underLoc.

Note that the initSearch function, defined in adv.t, must be called during initialization (usually, by your preinit function) to set up the hidden objects. This routine sets up special contents lists for the various hider objects. This routine only considers hidden objects when they’re of class hiddenItem, which is why you must use this class for all of your hidden objects.

Note that the ease with which you can hide objects shouldn’t be taken as a license to hide objects with wild abandon. From a game design viewpoint, hidden objects are almost always poor puzzles. If you do hide an object, make sure that you provide some sort of clue that something is hidden there. Otherwise, the player must tediously look behind and under every object in the game. As with most puzzles, it’s easy to make a really hard game - it’s harder to make a fun game.


Collections of Objects

Each item that a player can manipulate in a TADS game generally corresponds to an object defined in the game program. To allow the player to refer to each object individually, your game program normally must define a unique set of vocabulary words for each object. For example, if you have two books in your game, the books must have at least one distinct adjective each, so that the player can specify in a command which book he wants to manipulate. (The one exception to this general rule is the case of indistinguishable objects, which will be discussed later.)

The style of game construction which requires that each object have a unique set of vocabulary imposes certain limitations when you design your game. Sometimes you’ll find that you want to have a large collection of similar objects that are not distinguished from one another. Fortunately, there are a number of ways to implement object collections; the particular mechanism you choose will vary, and depends on the type of situation you want to create. This section provides examples of several different effects you can create with collections of objects.


Object Collections as Decorations

Suppose you are creating a bookstore. You will want to stock the store with a large number of books - but you won’t want to create a huge number of individual book objects, not only because it would take too much time and effort, but also because most of the books won’t be relevant to the game. There will usually be one or two important books, and many unimportant ones.

In this type of situation, one way to work within the limitations of TADS is to create a single object representing the collection of unimportant books. Since most of the books are not important, they can be grouped together into this single object, which is set up so that it tells the player about its lack of importance whenever the player tries to manipulate it. Furthermore, it can serve as a pointer to any important similar objects.

As an example, we’ll implement a collection of books in a book store. A single object will represent the collection of unimportant books. When the player looks at the collection of books, though, it will point out the presence of a couple of important books.

  manybooks: fixeditem
    sdesc = "books"
    adesc = "a number of books"
    noun = 'book' 'books'
    location = bookstore
    ldesc =
    {
        "The bookstore has a huge stock of books, ranging from
        scientific titles to the latest collection of
        \"Wytcome and Wyse\" cartoons. ";
        if ( not self.isseen )
        {
            "One title in particular catches your attention:
            \"Liquid Nitrogen:  Our Frigid Friend\". ";
            ln2book.moveInto( bookstore );
            self.isseen := true;
        }
    }
    verDoRead( actor ) =
    {
        "Although you'd really like to sit down and start reading,
        you know from painful experience that the neo-fascist staff
        have been cracking down on browsing lately. ";
    }
    verDoTake( actor ) =
    {
        "Unfortunately, you know you could never afford all of these
        books. ";
    }
  ;

Note that we set the adesc property to a non-default value. This is because the default adesc, which would display “a” followed by the object’s sdesc, would result in the strange message “a books” in this case.

To help the player avoid wasting a lot of time trying to manipulate the books, we put in some special messages that let the player know that the books can’t be manipulated. When you create a decoration object, it’s always a good idea to think of all of the things that the player might want to do with it, and provide messages that make it clear that the object isn’t important to the game. In this case, we’ve made it clear that the collection of books can’t be read or taken.

The most important definition, though, is the ldesc property. This property not only provides a general description of the collection of books, but also points out the single important book to the player when the collection of books is examined for the first time. (The isseen property is used to determine if the ldesc has been displayed before. The first time the ldesc property is executed, isseen is set to true.) In addition to displaying a message for the player about the special book, it moves the special book into the bookstore.


Selecting from an Object Collection

One way you may wish to extend this basic idea is to provide a collection of books from which the player can take a single book, seemingly at random. The first time a player takes a book from the collection, he gets one book; the next time, he gets another book; and so forth. This is easy to implement by overriding the doTake method of the collection of books: rather than moving the collection itself into the player’s inventory, the method will choose a book from a list of individual book objects, and move that book into the player’s inventory.

 manybooks2: fixeditem
    sdesc = "books"
    adesc = "a number of books"
    noun = 'book' 'books'
    location = bookstore
    ldesc =
    {
        "The bookstore has a huge stock of books, ranging from
        scientific titles to the latest collection of
        \"Wytcome and Wyse\" cartoons.  You
        may want to just try picking up a book. ";
    }
    verDoTake( actor ) =
    {
        if ( length( self.booklist ) = 0 )
            "You don't see anything else you're interested in. ";
    }
    doTake( actor ) =
    {
        local selection;
        selection := self.booklist[ 1 ];
        self.booklist := cdr( self.booklist );
        selection.moveInto( actor );
        "You look through the books, and choose << selection.thedesc >>. ";
    }
    booklist = [ ln2book chembook cartoonbook ]
  ;

The property booklist contains a list of books that we have pre-defined. Each time the player takes a book, the doTake method will take the first element out of the booklist property, and act as though the player had picked up that object. This way, the player can just type “take book,” and the game will seem to select a book for the player. The verDoTake property makes sure there’s something left in the list, and displays an appropriate message if not. Note that we’ve written the ldesc message so that it’s clear to the player that he should attempt to take a book.


Selecting from Indistinguishable Objects

Another variation on this theme involves a collection of indistinguishable objects. For example, suppose we want to implement a matchbook which contains a number of matches. The player should be able to take a match out of the matchbook in order to light it.

Since we’d like to try and implement such a feature without using the indistinguishable objects functionality, we’ll have to impose some limits on what the player can do. In particular, we’ll only allow the player to have a single match at any given time - all of the other matches must be in the matchbook.

In terms of implementation, this means that we’ll only need a single object to represent a match. This object will have two possible states: existent or non-existent. We’ll use the location property to indicate the state; if the location is nil, the object is non-existent, otherwise it’s in the game. When the match is non-existent, the player can take a match out of the matchbook - which brings the match into existence by moving it into the player’s inventory. As long as the match is in the game, the player cannot take another match out of the matchbook. Once the match is burned away, though, it will be removed from the game, and the player can take another match.

The matchbook will have a count of available matches. When the player attempts to take a match, we’ll first check to see if there are any matches left; if so, we’ll check to make sure that the match object doesn“t already exist in the game.

  matchbook: item
    location = bookstore
    noun = 'matchbook'
    sdesc = "matchbook"
    matchcount = 4  /* number of matches left in matchbook */
    ldesc =
    {
        if ( self.matchcount > 0 )
            "The matchbook contains << self.matchcount >> match<<
             self.matchcount = 1 ? "." : "es. " >>";
        else
            "The matchbook is empty. ";
    }
  ;

  match: item
    noun = 'match'
    adjective = 'single'
    sdesc = "single match"
    ldesc =
    {
      if ( self.isBurning )
        "It's currently lit. ";
      else
        "It's an ordinary safety match. ";
    }
    verDoLight( actor ) =
    {
      if (self.isBurning)
        "It's already lit!";
    }
    burnFuse =
    {
      "\bThe match burns down. You drop it,
      and it disappears in a cloud of ash. ";
      self.isBurning := nil;
      self.moveInto( nil );             /* match is now non-existent */
    }
    doLight( actor ) =
    {
      "The match starts burning. ";
      self.isBurning := true;
      notify( self, &burnFuse, 2 );
    }
 ;

  fakematch: fixeditem
    noun = 'match'
    adjective = 'bound'
    sdesc = "bound match"
    location = matchbook
    verDoTake( actor ) =
    {
      if ( matchbook.matchcount = 0 )
        "The matchbook is empty. ";
      else if ( match.location <> nil )
        "You'll have to use the one you already took first. ";
    }
    verDoLight( actor ) =
    {
      "You'll have to remove it from the matchbook first. ";
    }
    doTake( actor ) =
    {
      "You tear a match out of the matchbook. ";
      match.moveInto( actor );      /* move a match into inventory */
      matchbook.matchcount - ;     /* one less match */
    }
  ;

The fakematch object lets the player refer to the match in a command, even when the match object doesn’t exist in the game (i.e., its location is nil). The fakematch has verDoTake and doTake methods that let the player take a match out of the matchbook.

Note one flaw in this implementation: the player might not have the match, but still can’t take another until the first is destroyed. This could be confusing. For example, the player may take a match, then later drop it because his hands are full, then move to a different room. As far as the player is concerned, no match is present - he left the match in another room, and may not even remember where. However, he won’t be able to take another match, because the first match is still in the game. There’s no easy way to fix this. You could arbitrarily move the match into the player’s inventory, even though it is in some other room; this might be less confusing for some players, but it may be more confusing for others who are aware that there is a match in another room - this “other” match would be gone after the single match object is moved into the player’s inventory.

Another approach, and probably a more satisfying one, would be to use the indistinguishable objects capabilities introduced in TADS 2.2. Using this feature you can have multiple objects of the same class. You can even create objects dynamically and destroy them when they’re no longer needed. These features are discussed in Chapter 8.


Money

Money does not fit very naturally into a TADS game for a number of reasons. If you do want to use money in your game, it can generally be implemented using techniques similar to those we used for the matches in the matchbook. Or you could implement several denominations of coinage or paper bills, and implement them as indistinguishable objects, but even that approach becomes cumbersome with large numbers of monetary units. If you go for the matchbook approach the only changes you’ll have to make to support money will be to add some new verbs.

The necessary new verbs will depend on your game. For example, you may want “pay” and “buy” verbs for purchasing items. The main reason you’ll want to use these new verbs is that there’s no easy way for the player to refer to an amount of money unless you go for a very basic currency, such as the “gold piece” model used in a lot of fantasy games. If you want to avoid that model you’ll have to arrange a protocol for purchasing objects. For example, make a location that serves as a store, and make a shopkeeper actor. To purchase an object, the player tells the shopkeeper to give him an object. The shopkeeper responds by telling the player the price of the object. The player responds with “pay shopkeeper”; this deducts the price of the object, and gives the player the object.

If your game contains a non-obvious protocol like this, it is very important that you document it for the player. You can provide an explanation in the instructions for your game, but since most people will start playing your game immediately without reading any accompanying instructions, it would be far better to put the explanation into the game itself. There are a number of ways you could do this; you could, for example, simply display an explanation of the necessary commands when the player first enters the shop. Alternatively, you could provide instructions as error messages; when the player attempts to take an object in the shop, you could display a message: “Please direct your inquiries to the shopkeeper; for example, type ‘shopkeeper, give me the axe.”’

An even better approach would be to avoid situations like this altogether. If you can’t make it fit easily and naturally into your game without resorting to special sequences of commands, you should probably find another way to accomplish the same thing. Remember, absolute realism is not important in a text adventure game - the only important thing is to make the game fun to play.


Non-Player Characters

One of the most effective techniques for giving a game depth and making it involving is to include non-player characters. Early adventure games often seemed empty and static; the player just wandered alone through a vast maze of caves. More modern games use non-player characters (which we’ll refer to as “actors” from now on), who move through the game, interacting with the player and doing things on their own. When done well, actors add a whole new dimension to a game, and make the setting seem alive and real.

Actors are one of the most complicated parts of a TADS game, because they operate on more levels than other objects. First, there’s player interaction: an actor normally can interact with the player, by talking (the player can tell an actor to do things, and can ask an actor questions), and by direct action (attacking, for example, or exchanging objects). There are a great many ways a player and an actor can interact, and TADS lets you implement as much or as little interactive capability into an actor as you wish. Second, actors often do things on their own. Sometimes, this means that the actor carries out a pre-scripted set of actions. Other times, the actor just reacts to the player; for example, an actor might spend most of the game following the player around, making amusing comments from time to time. Most actors, though, are combinations of these, reacting to the player most of the time, and carrying out pre-scripted actions in response to certain events. Third, some actors have a “memory,” and have knowledge of various parts of the game. Certain things an actor does may depend on what a player has done previously.

Fortunately, all of these basic elements of an actor are implemented pretty easily. You can write the code for a simple actor without too much difficulty. Even better, once you have a simple actor working, fleshing out the actor is just a matter of adding to the basic framework of the actor.

To implement an actor, you need to write two main pieces of code that aren’t associated with most other types of objects. First is the actorAction method. This method handles most of the actor’s interaction with the player; it essentially defines the limits of the actor’s ability to interact. Second is the actor’s “daemon.” You’ll recall from previous chapters that a daemon is a function or method that’s called after every turn (that is, the run-time system calls a daemon each time it finishes processing a player command). The actor daemon is what allows the actor to go about its business; since it is called after each turn, the actor can perform an action of its own on each turn.

If you’ve played Ditch Day Drifter, you’ll recall a couple of actors in that game of differing degrees of interaction and activity. One fairly limited actor is the guard in front of the steam tunnels: the guard does very little apart from blocking your way into the tunnel. The guard’s interaction consists mostly of accepting objects; you can attempt to bribe the guard with money, for example, or give him something to drink. The guard’s daemon also does very little; it just displays a randomly-chosen message (from a set of several pre-determined messages) on each turn. A more versatile actor is Lloyd, the insurance robot. Lloyd can accept objects (you can buy the insurance policy), and you can also talk to him to a limited extent (you can ask about the policy). Like the guard, Lloyd displays a randomly-chosen message on most turns. In addition, Lloyd follows the player from room to room. Lloyd even reacts to certain events and places, and has a memory of sorts: once Lloyd has paid out an insurance claim, he will remember not to pay out the claim again.

Let’s look at an example of implementing an actor. We’ll start with a simple “puzzle actor,” similar to the guard in Ditch. This is a very common element of adventure games: a person that is blocking the way to some goal. In this case, we’ll implement a receptionist who’s blocking the way to an office you want to enter. As long as the receptionist is awake, you won’t be allowed to enter the office.

  receptionist: Actor
    noun = 'receptionist'
    sdesc = "receptionist"
    isawake = true
    ldesc =
    {
        "The receptionist is a very, shall we say, sturdy-looking
        woman, with a tall beehive hair-do and thick glasses.  She
        reminds you of your third-grade teacher, who was overly
        concerned with discipline. ";
        if ( self.isawake )
            "She eyes you impatiently as she goes through some papers. ";
        else
            "She's slumped over the desk, fast asleep. ";
    }
    location = lobby
    actorDesc =
    {
        if ( self.isawake )
            "A receptionist is sitting at the desk next to the
            door, watching you suspiciously. ";
        else
            "A receptionist is slumped over the desk next to
            the door. ";
    }
    actorDaemon =
    {
        if ( self.location <> Me.location or not self.isawake ) return;
        switch( rand( 5 ) )
        {
         case 1:
            "The receptionist sharpens some pencils. ";
            break;
         case 2:
            "The receptionist goes through some personal mail, holding
            each letter up to the light and attempting to read the
            contents. ";
            break;
         case 3:
            "The receptionist looks through the personnel files. ";
            break;
         case 4:
            "The receptionist answers the phone and immediately
            puts the caller on hold, cackling to herself fiendishly. ";
        break;
         case 5:
            "The receptionist shuffles some papers. ";
            break;
         }
     }
   ;

  lobby: room
    enterRoom( actor ) =
    {
      if ( not self.isseen )
        notify( receptionist, &actorDaemon, 0 );
      pass enterRoom;
    }
    sdesc = "Lobby"
    ldesc = "You're in a large lobby, decorated with expensive-looking
    abstract pastel oil paintings and lots of gleaming chrome.
    A large receptionist's desk sits next to a door leading
    into an office to the east. The exit is to the west. "
    west = outsideBuilding
    out = outsideBuilding
    east =
    {
      if ( receptionist.isawake )
      {
        "You start to stroll past the receptionist nonchalantly,
        trying to ignore her as though you actually belong here,
        but she's wise to that trick: she leaps up, and with
        surprising force throws you back from the doorway.
        Satisfied that you're not going through, she sits back
        down and returns to her work. ";
        return( nil );
      }
      else
        return( office );
    }
  ;

Notice that the implementation of this puzzle looks very much like that of a door, or any other obstacle, except that the obstacle itself is somewhat more complicated. The actual work of preventing the player from moving past the receptionist, though, is exactly the same as for any other obstacle: in the direction method, we check to see if the receptionist is still awake, and if so, we refuse to let the player past by returning nil (after displaying an appropriate message, of course).

The main features that are special for an actor are the actorDesc and actorDaemon methods. The actorDesc is a special property that you should define for any actor; this method is called by the general room code that displays the long description of the room. After the description of the room and its contents, the room long description routine will call the actorDesc method of each actor in the room (except the player actor, Me). The actorDesc method should simply display an appopriate message to the effect that the actor is present; it’s similar to the ldesc method for the actor, but it usually contains less detail, since it’s more to inform the player that the actor is present than to provide a detailed desription of the actor. It should mention that the actor is present and what the actor is doing.

The actorDaemon is nothing special - you can call this method whatever you want. Most actors will have something like the actorDaemon, though. This is the daemon that makes the actor seem alive by doing something after every turn. For our simple receptionist actor, which doesn’t really do anything of its own accord, this method simply displays a random message each turn. Note that the method returns immediately if the player (the Me object) is not present in the same room as the receptionist; it clearly wouldn’t make any sense to display a message about what the receptionist is doing when the receptionist is not present.

Note that actorDaemon is not started automatically for an actor. Instead, you must explicitly activate it. In this case, we’ve chosen to activate the actorDaemon routine by calling notify() the first time the player enters the lobby. The enterRoom method of the lobby object checks to see if the room has been entered before (by checking to see if the isseen property of the current room has been set - this is always automatically set by the room code after the room has been seen for the first time); if the room has never been entered, we call notify() to start actorDaemon running. Note that enterRoom finishes by using pass to run the enterRoom method that would normally be inherited from the room class.


Following the Player

There are several main types of wandering actors. The first type follows the player around, like Lloyd the insurance robot in Ditch Day Drifter. This first type is fairly simple, because all you have to do is set up a daemon that checks to see if the actor and the player are in the same room, and if not, move the actor to the player’s location.

The big change between a stationary actor, such as the receptionist in the earlier example, and a wandering actor is in the actor’s daemon. Instead of just displaying a random message, as did the receptionist, the actor daemon will actually move the actor around.

Here’s a basic actorDaemon method that will make the actor follow the player.

  actorDaemon =
  {
    if ( self.location = Me.location )
    {
        switch( rand( 5 ) )
        {
         case 1:
             "Lloyd hums one of his favorite insurance songs. ";
             break;
         /* etc. with other random messages... */
         }
    }
    else
    {
        self.moveInto( Me.location );
        "Lloyd rolls into the room, and checks to make sure
        the area is safe. ";
    }
  }

Note that this routine must be set up to run every turn with the notify() function, as explained in the receptionist example.

This new version of the method still displays a random message if the actor is in the same room with the player, but now it has some new code that is executed when the player is somewhere else. The new code moves the actor to the player’s location, and displays a message informing the player that the actor has entered the room.

In a real game, you’ll probably further extend this type of daemon to make the actor do special things in certain locations. For example, in Ditch Day Drifter, there’s some extra code that displays special messages when Lloyd enters certain improbable locations, such as squeezing through the north-south crawl or climbing the rope.


Moving on a “Track”

The second type of wandering actor moves around on a fixed path, or “track.” This type of actor is not much harder to implement than an actor which follows the player; rather than using the player’s location in the actor motion daemon, you make a list of locations that the actor visits in sequence, and the actor motion daemon takes locations out of this list.

One new factor you’ll have to keep in mind in implementing this type of motion daemon is that the actor could be entering the actor’s location, leaving the actor’s location, or neither. The motion message will have to check for each of these situations.

For an example, we’ll show how to implement a robot similar to the vacuuming robot in Deep Space Drifter that moves around between several rooms in a fixed pattern.

  vacRobot: Actor
    sdesc = "domestic robot"
    noun = 'robot'
    adjective = 'domestic'
    location = stationMain
    tracklist = [ 'southeast' bedroomEast 'west' bathroom 'west'
               2em bedroomWest 'northeast' stationMain 'north'
               2em stationKitchen 'south' stationMain ]
    trackpos = 1
    moveCounter = 0
    actorDaemon =
    {
        if ( not self.isActive ) return;          /* do nothing if turned off */
        self.moveCounter++;
        if ( self.moveCounter = 3 )          /* move after 3 turns in one location */
        {
            self.moveCounter := 0;
            if ( self.location = Me.location )
                "The robot moves off to the <<self.tracklist[self.trackpos]>>.";
            self.moveInto( self.tracklist[ self.trackpos + 1 ] );
            if ( self.location = Me.location )
                "A domestic robot rolls into the room and starts noisily
                 moving around the room vacuuming and dusting. ";

                 /* move to next position on track; loop back to start if necessary */
                 self.trackpos += 2;
                 if (self.trackpos > length(self.tracklist)) self.trackpos := 1;
         }
         else
         {
             /* we're not moving this turn, so display activity message */
             if ( self.location = Me.location )
               "The domestic robot continues darting around the room
               vacuuming. ";
         }
     }
   ;

The list of rooms that the robot visits is specified in the tracklist property. This is a strange-looking list, because it contains both objects and (single-quoted) strings. The way this works is that the list elements are always inspected in pairs: the first item in a pair is a string that will be displayed to indicate the direction that the robot will leave the current room; the second item in the pair is the room to be visited next after the current room. So, the first pair contains the string ‘southeast’ and the object bedroomEast: the robot will leave to the southeast, and move to the east bedroom.

The actorDaemon still does all the work, but it’s a little more complicated than in the previous examples. First, it checks to see if the robot is active; if not, it doesn’t go any further. Next, it increments a counter that tracks how long the robot has been in the current room; when it gets to three turns, it’s time to move on, otherwise the robot stays where it is.

If the robot is moving, the daemon first resets the move counter to zero. Next, it checks to see if the player is in the current location; if so, the daemon displays a message telling the player that the robot is leaving, and indicates which direction (the direction is determined by looking at the current element of the tracklist list, as described above). Next, the robot is actually moved to the new location (again, determined by looking in the tracklist list). Next, if the player is in the new location, the daemon displays a message saying that the robot has entered the room. Finally, we increment the track position counter property trackpos (by 2, since the list entries are in pairs). If the position counter has moved beyond the end of the list, we reset it to 1.

If the robot is not moving during this turn, the daemon will check to see if the actor is in the same location as the player, and display a message if so. Note that the single fixed message in the example could easily be replaced by a randomly-chosen message, as we did with the earlier examples.


Random Wandering

The third type of wandering actor moves through the game at random. This type of actor wandering is somewhat more complex than the earlier types.

The main trick in implementing a randomly-wandering actor is that we must avoid evaluating direction properties when they don’t contain a fixed object. This is because we could accidentally display spurious messages, and possibly trigger side effects (such as killing the player or making other changes to the game state) if we were to evaluate a direction property that was actually a method. For example, think back to the earlier example with the stairway that collapses when used - you probably wouldn’t want to display the collapse message and destroy the stairs when one of your wandering actors encountered the room.

The best way to avoid such unintended side effects is to avoid using any direction property that isn’t a simple object. TADS provides a special built-in function called proptype that lets you determine the type of a property without actually evaluating it. The proptype function will tell you whether a property is a simple object or a method.

Other than the mechanism for choosing a new room, randomly-wandering actors are essentially the same as actors on a track. You generate messages in the same manner, checking to see if the actor is leaving or entering a room occupied by the player.

  actorDaemon =
  {
    local dir, dirnum;
    local tries;
    local newloc;
    for ( tries := 1 ; tries < 50 ; tries++ )
    {
        dirnum := rand( 6 );
        dir := [ &north &south &east &west &up &down ][ dirnum ];
        if (proptype(self.location, dir) = 2 /* object */)
        {
            newloc := self.location.( dir );
            if ( not isclass( newloc, room ) ) continue;
            if (self.location = Me.location)
                "The robot leaves to the <<
                 [ 'north' 'south' 'east' 'west' 'up' 'down' ][ dirnum ]>>.";
            self.moveInto( newloc );
            if ( self.location = Me.location )
                 "A domestic robot enters the room and starts vacuuming
                  noisily. ";
         }
      }
   }

The line that sets the dir local variable probably needs a little explanation, because it’s a tricky bit of TADS coding. The first thing is a list of property addresses - these aren’t properties of any object, but rather just references to properties that you can later use to get an actual property of an object. We take this list, which consists of six elements, then choose a single element at random by indexing into the list with a randomly-chosen number from 1 to 6. This leaves the dir local variable with a pointer to a property that we can use later. Note that you could include northeast, southeast, northwest, and southwest in the list if you want; we left them out to make the example a little bit easier to read.

Next, we use the proptype() built-in function to determine if the selected property (contained in dir) of the actor’s current location contains an object. If it does not, we continue looping.

If the selected property of the current location does in fact contain an object, we first get that location into the local variable newloc. Then, we make one more check on the new location: we ensure that newloc actually contains an object of class room. This is because it is possible for a room direction connection property to contain an object of class obstacle, such as a door (see the section on implementing doors earlier in this chapter). If it’s a door or other obstacle, we want to treat it the same as though the direction property didn’t contain an object at all - so we’ll just continue with the loop, ignoring this choice of a direction.

Once we have an object of class room in newloc, we proceed to move the actor the same way we moved the actor on a track. Note that we use the same indexing trick to display the direction that the actor is exiting.

In case you’re wondering why the example uses a “tries” counter and stops after 50 times through the loop: this is simply a bit of bullet-proofing to keep the daemon from going into an infinite loop. It’s quite possible, especially while you’re developing your game, that the robot may encounter some rooms from which there is no escape. Remember that the robot can only use exits which are explicitly set to objects; if you create a room with nothing but exits that are methods, the robot will be unable to find a suitable exit. If we didn’t include the loop counter and give up after a while, the loop would run forever in such rooms, leaving the game stuck.

In a real game, you may want to add some special-case code that allows a randomly-wandering actor to use certain exits that would normally be off-limits because they’re methods. One way of accomplishing this is to add a special check to the daemon to test if the actor is in such a special location, and if so, to make it go to a particular destination. Another way is to put additional properties in some rooms (such as robotEast) that specify directions accessible only to the robot; in the daemon, you’d check for the presence of these special robot directions first, falling back on the normal direction list only if the special directions are not present.

One modification you may wish to make to this type of totally random wandering is to restrict the actor to a particular set of rooms. There are a number of ways to accomplish this. The simplest is to create a class of rooms that the actor is allowed to occupy, and always check upon choosing a new room that the room is a member of the class.

If you want to change the allowed rooms dynamically as the game runs, use a condition rather than a class. Instead of checking to see if the room is a member of a class, check to see if the condition is true. For example, if you want to create an actor that only enters dark rooms, you would check upon choosing a room to make sure the room is not lit.

More Complex Actor Scripts

For some games, you may wish your actors to exhibit even more complex autonomous behavior than we’ve shown so far. One type of common extension is to make the actor perform some action when in a certain room or when a certain set of conditions is met. For example, you may wish to implement a thief that steals any treasure left lying around in a maze room. To implement this, we’d first need to create a class of rooms for mazes; let’s call this new class mazeroom:

  mazeroom: room
  ;

Similarly, we’ll create a class for any object that counts as a treasure:

  treasure: item
  ;

Note that the only purpose of these two classes is to serve as a flag, so we can determine if an object is considered a treasure or if a room is part of a maze. These classes don’t actually implement any new behavior of their own.

Next, we’d add a check in the thief’s daemon that looks for treasure objects when the thief is in a room in a maze. We’ll add this code to the end of the same daemon we used for the randomly wandering actor above in the previous section; we won’t reproduce that entire routine here. This code is added after the for loop that comprised the bulk of that daemon.

  if ( isclass( self.location, mazeroom ) )
  {
        /* check contents of current room */
        local cont := self.location.contents;
        local len  := length( cont );
        local found := nil;
        local i;
        for ( i := 1 ; i <= len ; ++i )
        {
            if ( isclass( cont[ i ], treasure ) )
            {
              found := true;
              cont[ i ].moveInto( self );
            }
        }
        if ( found and Me.location = self.location )
            "You notice the thief has helped himself to some items. ";
    }

This is a fairly complicated example of making your actors do something special when certain conditions are met. A simpler example is Lloyd’s attempt to sell insurance to the sleeping guard in Ditch Day Drifter, which is simply a message that’s displayed by Lloyd’s movement daemon when Lloyd enters the room with the guard.

You may want some actors to have very complicated scripts that do more than just move the actors around. You may want to implement an actor which goes about his business, doing things in some of the locations he visits. The best way to do this is probably by making your actor a “state machine.” One easy way to implement a state machine is to use a number to represent the current state; on each turn, you increment the state number, and use a switch statement to take the appropriate action depending on the new state.

For example, suppose you want to implement an actor that enters a building and walks to the study. If someone else is in the study, he’ll just wait impatiently for the other person to leave; once he’s alone in the study, he’ll produce a key and open a safe, conveniently dropping the key near the safe. He’ll then take something out of the safe and leave the building.

We’ll represent the set of states with numbers. State 1 will be outside the building; when in state 1, we’ll enter the building. State 2 will be inside the front hall, and we’ll move to the study. In state 3 we’ll check to see if we’re alone; if not, we’ll just stay in state 3 and act impatient. Once we’re alone, we’ll produce the key and open the safe. In state 4, we’ll take the object out of the safe and exit the room, moving back to the front hall. In state 5, we’ll leave the building.

The following methods can be used to accomplish this.

  actorMessage( msg ) =
  {
    if ( Me.location = self.location )
    {
        "\b";
        say( msg );
    }
    actorMove( newloc, todir, fromdir ) =
    {
      if ( Me.location = self.location )
        "<<self.thedesc>> leaves to the <<self.todir>>. ";
      self.moveInto( newloc );
      if ( Me.location = self.location )
        "<<self.thedesc>> enters the room from the <<self.fromdir>>. ";
    }
    actorState = 1
    actorDaemon =
    {
      switch( self.actorState++ )
      {
         case 1:
            self.actorMove( frontHall, 'north', 'south' );
            break;
         case 2:
            self.actorMove( study, 'east', 'west' );
            break;
         case 3:
            if ( self.location = Me.location and not Me.ishidden )
            {
                "Jack crosses his arms and looks at his watch. ";
                self.actorState := 3;         /* remain in state 3 */
            }
            else
            {
                 actorMessage( 'Jack looks around, and is satisfied that
                  he is alone.  He searches his pockets, and produces a
                  key, which he inserts into the safe and opens it.  He
                  nervously removes something from the safe; he doesn\'t
                 seem to notice when the key drops to the ground.' );
                 safeKey.moveInto( self.location );
            }
            break;
         case 4:
            self.actorMove( frontHall, 'west', 'east' );
            break;
         case 5:
            self.actorMove( outsideBuilding, 'south', 'north' );
            break;
        }
    }

Note that the actorMove and actorMessage methods that we implemented provide a convenient mechanism for displaying messages conditionally if the player is present.

Naturally, you’d want to extend the end of this script to give Jack something to do (or somewhere to disappear to) at the end of his appearance. Using this basic technique, you can implement essentially arbitrarily complicated actors. Note that it’s easy to jump to a different state rather than progress to the next state - just assign the state variable (actorState in this case) to the new state you want to enter on the next turn. This allows your actor to behave intelligently: rather than just following a script without reference to what’s going on in the game, the actor can take different actions depending on the conditions around him. In our simple example above, the only sensitivity to game conditions is that the actor waits to open the safe until he’s alone (or thinks he is); you can easily test for much more complex conditions, though.

In deciding how to implement your actors, you should consider the things you actor does most frequently. If your actor mostly moves around on a track, and occasionally does something special in a particular location or when a particular set of conditions is true, then implementing your actor using the techniques for moving on a track is easiest; just check for your special conditions before or after moving. On the other hand, if your actor does something special in many locations and on most turns, using a state machine is easiest.


Talking to Actors

One of the main areas of interaction with actors in adventure games is asking the actors questions. TADS is a bit limited when it comes to artificial intelligence; unfortunately, TADS doesn’t make it possible to ask actors general questions such as “what do you think about the role of the media in the upcoming election?” or “why is the sky blue?” Instead, questions addressed to actors in a TADS game are limited to asking about particular objects in the game. Furthermore, all the player can do is ask about an object in general - the player can’t ask “why is the fuse outside the tram?”, but is limited to “ask lloyd about the fuse.”

Another type of interaction is to tell an actor something. As with questions, TADS is too limited to allow a player to tell an actor anything very specific. All that a player can say is something like “tell lloyd about the scroll.”

As a game designer, these limitations make your job fairly straightforward. Implementing the “ask” and “tell” routines is essentially the same as implementing any other verb method. Here’s an example of a doAskAbout method for an actor.

  verDoAskAbout( actor, iobj ) = {}
  doAskAbout( actor, iobj ) =
  {
    switch( iobj ) /* indirect object is what is being asked about */
    {
     case insurancePolicy:
        if ( insurancePolicy.isbought )
            "\"It's very complicated, but let me assure you,
            it's a very good policy.\"";
        else
            "\"It's a great policy for only a dollar!\"";
        break;
      default:
        "\"I don't know much about that.\"";
      }
    }

You could add more cases as needed to add to Lloyd’s knowledge.

Note one strange twist here: the actor parameter to the methods is the actor that asked the question - that is, the player, or Me. The actor who’s being asked is self, since the actor is the direct object of the “ask” verb. Note also that we had to implement an empty verDoAskAbout method so that the actor can be asked questions at all; by default, all objects inherit a generic error message for verDoAskAbout.

You can implement behavior for telling an actor about an object in much the same way, by writing verDoTellAbout and doTellAbout methods for the actor. Likewise, you can make it possible for the player to give objects to the actor by implementing verIoGiveTo and ioGiveTo methods for the actor. Note that the ioGiveTo method should move the direct object into the actor’s inventory (by executing dobj.moveInto(self)) if the object is accepted.


Taking Objects from Actors

By default, actors don’t let the player take objects that the actor is carrying. This is because all of the routines that try to move an object will call the verGrab( actor ) method of all of the containers of the object. By default, an actor’s verGrab method will display a message indicating that the actor won’t give up the object so easily; since verGrab is called during the validation phase of parsing, displaying a message is sufficient to prevent the command from proceeding.

If you want to allow the player to take objects from an actor, override the actor’s verGrab method. You could make the method do nothing at all, in which case the actor will allow any object to be taken; or it can display a message only on certain objects, which will prevent only those objects from being taken.


Commanding an Actor

The player can issue a command to an actor by typing something like “lloyd, go north.” When this happens, the TADS parser processes the command in almost exactly the same way as a normal command, except that the actor of the command is changed from the default Me (the player’s actor) to the actor indicated in the command. (In fact, it really is exactly the same processing; when no actor is typed, it’s as though the player had typed “me, go north.”)

Since most all processing occurs relative to the actor parameter to the verb-handling methods (such as verDoTake( actor ) and doDrop( actor )), you don’t need to make very many special provisions for commands directed to actors. This type of processing is done almost entirely automatically by the system.

However, in most cases, you don’t actually want the player to be able to boss your actors around. Most of your actors will have a mind of their own, and many will even be presenting obstacles to the player.

To make it possible for you to control whether the player can control your actors, and to what extent, the TADS parser will always call the actorAction method of the selected actor prior to allowing any further processing to proceed. If this routine executes an exit statement, the command will go no further, preventing the player from controlling your actor. Since the method receives all of the information on the current command (the verb, direct object, preposition, and indirect object) as parameters, it can choose to allow or disallow particular commands. For allowed commands, do nothing; for disallowed commands, display an appropriate message and then execute an exit statement.




At this very moment, we have the necessary techniques, both material and psychological, to create a full and satisfying life for everyone.
BURRHUS FREDERIC SKINNER, Walden Two (1948)


Chapter Nine Table of Contents Chapter Eleven

楽天モバイル[UNLIMITが今なら1円] ECナビでポインと Yahoo 楽天 LINEがデータ消費ゼロで月額500円〜!


無料ホームページ 無料のクレジットカード 海外格安航空券 解約手数料0円【あしたでんき】 海外旅行保険が無料! 海外ホテル