___________________________________

A TADS Tutorial: Lesson Nine

Timing

___________________________________

A Word About Braces

I've seen some confusion about where braces and semicolons go. The thing to remember is that braces are used to group statements (i.e., instructions) together. Each statement within that group must have a semicolon at the end.

So, every method is surrounded by braces because a method is a series of statements that must be grouped together.

Similarly, the set of statements after an if must be grouped together with braces like this:

if (condition)
{
  statement1;
  statement2;
}
else
{
  statement1;
  statement2;
}

Notice that the statements must end in semicolons, but the if and else lines don't, because they really aren't statements by themselves, they are just keywords that direct the flow of program execution to the appropriate block of statements. Also, there is no need to put a semicolon after a brace.

___________________________________

Delayed Reactions

Let's say you want to program a bomb that detonates when you push a button on the bomb. You already know how to do this. But what if you want the bomb to detonate three turns after the button has been pushed? This requires the use of a TADS function called notify, which schedules a method to be called at some point in the future.

The syntax for notify is:

notify(object, &method, howFarInFuture);

In other words, the first argument (i.e., input to the function) is the object whose method you want to invoke. The second argument is the name of the method (the method name must be preceded by an ampersand symbol). The third argument is a number which tells TADS how many turns from now to call the method.

If you want to cancel this action that is scheduled to occur in the future, you just use the unnotify function like this:

unnotify(object, &method);

This should be a lot clearer after you see an example, so let's go ahead and look out how to program a delayed-reaction bomb.

But first, we need to program a suitable setting for our bomb. How about a mailbox out on a sidewalk? We'll allow the player to detonate the bomb anywhere, but we'll give a special message if he tries to blow up the mailbox.

Let's start with the location:

startroom : room
  sdesc = "Sidewalk"
  ldesc = "You are standing out on a sidewalk.
  There is a mailbox here.  The sidewalk leads east toward a lake. "
  east = lake
;

Now, we need to program the mailbox. None of the default container classes are quite suitable, because you can put things in a mailbox, but you can't take them out. Also, you can't see the things that are inside of a mailbox. Fortunately, all items have the properties contentsVisible and contentsReachable, which determine whether the things they contain are visible or accessible, respectively.

You could discover these properties for yourself by looking in adv.t to see how the container classes you've worked with (openable, transparentItem) were implemented. If you looked, you'd find that the container classes use the properties contentsVisible and contentsReachable to control visibility and accessibility.

With these properties in mind, we can program the mailbox like this:

mailbox : fixeditem
  location = startroom
  noun = 'mailbox'
  sdesc = "mailbox"
  ldesc = "It is a standard, United States mailbox. The penalty for
  tampering with mail is quite severe. "
  contentsVisible = nil
  contentsReachable = nil
  verIoPutIn(actor) = {}
  ioPutIn(actor, dobj) = 
  {
    "You open the mailbox door, put <<dobj.thedesc>> inside, and   
    close the door.  You hear a thunk as <<dobj.thedesc>> lands 
    inside the mailbox. ";
    dobj.moveInto(mailbox);
  }
  verOpen(actor) =
  {
    "You open the mailbox door, but of course, you can't actually see
    inside the mailbox while the door is open, so you let go and the
    door slams shut. ";
  }
  verClose(actor) =
  {
    "The mailbox is already closed. ";
  }
;

Next, we should program a pile of ash that has no starting location, but will replace the bomb when the bomb is detonated.
ash : item
  noun = 'pile'
  adjective = 'ash'
  sdesc = "pile of ash"
  ldesc = "The pile of ash is probably the charred remains of the bomb
  you detonated earlier. "
;

Now it's time to program the bomb. We'll use a property called isActive to store a true or nil value as to whether the bomb is currently counting down. We'll also create a method named explode, which will contain all the statements that are to be executed when the bomb detonates. The explode method should consider whether the bomb is in the mailbox, and whether the player is around to witness the explosion.
bomb : item
  location = startroom
  noun = 'bomb'
  sdesc = "bomb"
  isActive = nil 
  ldesc = 
  {
    "The tiny bomb has a small button. ";
    if (self.isActive)
    {
      "It's ticking loudly. ";
    }
  }
  explode =
  {
    "\b";   // Print a blank line before the explosion message
    
    // Special message if bomb explodes inside the mailbox
    if ( self.location == mailbox )
    {
      // Is the player where he can witness the explosion?
      if ( Me.location == mailbox.location )
      {
        "There is a huge explosion from within the mailbox.  Smoke
        comes pouring out for a moment, but surprisingly, the mailbox
        has suffered no visible damage.  These items must be well-
        built.  Of course, the mail inside has probably been completely
        obliterated.  You're in big trouble now! ";
      }
      else  // The player can't actually see the mailbox
      {
        "You hear a distant, muffled explosion. ";
      }
    }
    else      // bomb not in mailbox
    {
      // Is the player where he can witness the explosion?
      if( self.isIn(Me.location) )
      {
        "The bomb's explosion is the last thing you ever see. ";
        die();
      }
      else // bomb not nearby
      {
        "You hear an explosion off in the distance. ";
      }
    }
    
    // Message has been printed, now replace bomb with ash
    ash.moveInto(self.location);
    self.moveInto(nil);
  }
;

Now, we need to create the detonator button. Let's make it a toggle button. In other words, the first time the player presses the button, the bomb starts ticking down. But if the player presses the button again before the bomb blows up, the countdown is cancelled.

To commence the countdown, we'll notify the bomb's explode method to trigger in 3 turns (counting the end of the turn in which the button was pressed as the first turn).

bombButton : fixeditem
  location = bomb
  noun = 'button'
  adjective = 'bomb'
  sdesc = "bomb button"
  ldesc = "A foreboding button protrudes from the bomb.  I wonder what
  it does ... "
  verDoPush(actor) = {}
  doPush(actor) =
  {
    if (not bomb.isActive)
    {
      "The bomb starts ticking. ";
      notify(bomb, &explode, 3);
      bomb.isActive = true;
    }
    else
    {
      "The bomb goes quiet.  Whew! ";
      unnotify(bomb, &explode);
      bomb.isActive = nil;
    }
  }
;

So that's all there is to it! notify to schedule something, and unnotify to cancel.

Here's what the end result looks like:

Sidewalk
   You are standing out on a sidewalk.  There is a mailbox here.  The sidewalk
leads east toward a lake.
   You see a bomb here.

>X BOMB
The tiny bomb has a small button.

>PUSH BUTTON
The bomb starts ticking.

>X BOMB
The tiny bomb has a small button.  It's ticking loudly.

>Z
Time passes ...

The bomb's explosion is the last thing you ever see.

*** You have died ***

In a total of 4 turns, you have achieved a score of 0 points out of a possible
100.

You may restore a saved game, start over, quit, or undo the current command.
Please enter RESTORE, RESTART, QUIT, or UNDO:  >RESTART

A TADS Adventure
Developed with TADS, the Text Adventure Development System.

Sidewalk
   You are standing out on a sidewalk.  There is a mailbox here.  The sidewalk
leads east toward a lake.
   You see a bomb here.

>PUSH BUTTON
The bomb starts ticking.

>PUT BOMB IN MAILBOX
You open the mailbox door, put the bomb inside, and close the door.  You hear a
thunk as the bomb lands inside the mailbox.

>Z
Time passes ...

There is a huge explosion from within the mailbox.  Smoke comes pouring out for a
moment, but surprisingly, the mailbox has suffered no visible damage.  These
items must be well-built.  Of course, the mail inside has probably been
completely obliterated.  You're in big trouble now!

The scenarios in which the bomb explodes with the player out of sight work similarly, but are not included here in order to save space.

___________________________________

Executing a Script

One useful thing about the notify function is that if you pass the number 0 as the third argument, the object's method will be called every turn until cancelled. This has tremendous utility, particularly with respect to scripts and "character fidgets."

The drama of interactive fiction can be greatly enhanced by the skillful use of scripted scenes. For example, you may remember that in the game "The Plant" there was a scene that unfolded over several turns while you were on the hill, watching the trucks get hijacked.

Let's program a lake at which a scene reminiscent of "Jaws" plays out. Since we want the player to witness the full scene, we'll temporarily pause the script when the player leaves the room, and resume when he returns. (An alternative would be to prevent the player from leaving during the script, as in "The Plant").

The script will be a method of the fisherman object (the guy out on the lake). Fortunately, all room objects have an enterRoom and leaveRoom method that are invoked when the player enters and leaves the room, respectively. We can take advantage of these methods to trigger and cancel the script.

lake : room
  sdesc = "Lake"
  ldesc = 
  {
    "It's a beautiful day out.  You are standing on a grassy lawn
    beside a blue lake. To the north lies a garden. ";
    if (fisherman.location == self)
    {
      "A fisherman is peacefully fishing from his boat in the center of
      the lake. ";
    }
  }
  west = startroom
  north = garden
  
  enterRoom( actor ) =
  {
    if (fisherman.location == self)
    {
      notify(fisherman, &script, 0);
    }
    pass enterRoom;
  }
  leaveRoom( actor) =
  {
    unnotify (fisherman, &script);
    pass leaveRoom;
  }
;

Now we can program the fisherman, and the script associated with him. The fisherman uses a property called scriptNum to keep track of how far into the script we are. Each time the script method is called, it adds 1 to scriptNum, and then takes cases on scriptNum to determine which part of the script to perform.

In other words, since scriptNum starts at 0, the first time the script method is called, scriptNum is incremented to 1, and the first part of the script is performed. The next turn, scriptNum is incremented to 2, and part two of the script is performed, and so on.

fisherman : fixeditem
  location = lake
  noun = 'fisherman'
  sdesc = "fisherman"
  ldesc = "The fisherman sees you looking at him, and he waves back at
  you with a smile. "
  
  scriptNum = 0
  script =
  {
    self.scriptNum = self.scriptNum + 1;
    if (self.scriptNum == 1)
    {
      "\bThe fisherman fiddles with some bait. ";
    }
    if (self.scriptNum == 2)
    {
      "\bYou notice some bubbles rising up around the fisherman's boat. ";
    }
    if (self.scriptNum == 3)
    {
      "\bA fin appears in the lake, heading in the general direction of
      the boat, and then disappears. ";
    }
    if (self.scriptNum == 4)
    {
      "\bThe boat is rocked violently by some unseen force,
      tipping the fisherman out of his boat.  He flails in the
      water for a moment, screaming, before vanishing beneath
      the surface of the lake. ";
      fisherman.location = nil;
    }
    if (self.scriptNum == 5)
    {
      "\bYou think you see a pool of red in the center of the lake, but
      the color quickly dissipates. ";
      unnotify(self, &script);
    }
  }
;

Here's what it looks like in the game:
Lake
   It's a beautiful day out.  You are standing on a grassy lawn beside a blue
lake.  To the north lies a garden.  A fisherman is peacefully fishing from his
boat in the center of the lake.

The fisherman fiddles with some bait.

>Z
Time passes ...

You notice some bubbles rising up around the fisherman's boat.

>Z
Time passes ...

A fin appears in the lake, heading in the general direction of the boat, and then
disappears.

>X FISHERMAN
The fisherman sees you looking at him, and he waves back at you with a smile.

The boat is rocked violently by some unseen force, tipping the fisherman out of
his boat.  He flails in the water for a moment, screaming, before vanishing
beneath the surface of the lake.

>L
Lake
   It's a beautiful day out.  You are standing on a grassy lawn beside a blue
lake.  To the north lies a garden.

You think you see a pool of red in the center of the lake, but the color quickly
dissipates.

As you can see, it is entirely possible to program scripts using chains of if blocks. However, there is a somewhat more concise way to write a script, using a control flow construct called switch. switch evaluates the expression in parentheses after the switch keyword, and then jumps to the corresponding case. It breaks out of the switch block when it hits a break command.

So the fisherman script method can be rewritten as the following equivalent program:

script =
  {
    self.scriptNum = self.scriptNum + 1;
    switch (self.scriptNum)
    {
      case 1:
        "\bThe fisherman fiddles with some bait. ";
        break;
      case 2:
        "\bYou notice some bubbles rising up around the fisherman's 
        boat. ";
        break;
      case 3:
        "\bA fin appears in the lake, heading in the general direction of
        the boat, and then disappears. ";
        break;
      case 4:
        "\bThe boat is rocked violently by some unseen force,
        tipping the fisherman out of his boat.  He flails in the
        water for a moment, screaming, before vanishing beneath
        the surface of the lake. ";
        fisherman.location = nil;
        break;
      case 5:
        "\bYou think you see a pool of red in the center of the lake, but
        the color quickly dissipates. ";
        unnotify(self, &script);
        break;
    }
  }

It's a little more concise, but if you don't feel comfortable using switch, don't worry about it -- you can get by without it.

If you do choose to use switch, don't forget to put a break command at the end of every case. If you don't, it will keep executing statements in the next case which is probably not what you want. (Sometimes this feature of switch can be put to good use though, as you'll see in the next example.)

As a side note, a script method does not need to be called script. You can call it whatever you want.

___________________________________

Scripts that Repeat

What if you want to make a script repeat over and over again? It actually requires a rather simple modification to the technique already discussed. Instead of calling unnotify in the last part of the script, just reset the scriptNum to 0.

Also, unlike the fisherman script, which paused when we left the room and resumed when we returned, let's experiment in this example with making the script reset to the beginning every time you enter the room. Again, this is a simple modification, we just reset the script by explicitly setting the scriptNum to 0 when the player enters.

As an example, let's consider a squirrel that repeatedly runs up a tree, gets an acorn, and buries it. For the purposes of this example, we won't bother to program the squirrel as a separate object that you can interact with, we'll just program the script as part of the room. For steps two and three of the script, the squirrel just rummages around for acorns, so we can print exactly the same message for both of those steps. This allows us to make use of the unusual behavior that happens in a case that isn't terminated by a break command:

garden : room
  sdesc = "Garden"
  ldesc = "You are in a beautiful outdoor garden with many flowers 
  and trees.  The lake is to the south. "
  south = lake
  enterRoom(actor) =
  {
    self.scriptNum = 0;
    notify (self, &script, 0);
    pass enterRoom;
  }
  leaveRoom(actor) =
  {
    unnotify(self, &script);
    pass leaveRoom;
  }
  scriptNum = 0
  script = 
  {
    self.scriptNum++;  
    // Above line is shorthand for self.scriptNum = self.scriptNum + 1
    switch(self.scriptNum)
    {
      case 1:
        "\bA squirrel scampers up a nearby oak tree. ";
        break;
      case 2:
      case 3:
        "\bThe squirrel pulls acorns off the tree. ";
        break;
      case 4:
        "\bThe squirrel scampers down the tree with one of the acorns and
        buries it in a little hole. ";
        // Reached end of script, so reset
        self.scriptNum = 0;
        break;
    }
  }
;

Here's what it looks like in the game:
Garden
   You are in a beautiful outdoor garden with many flowers and trees.  The lake
is to the south.

A squirrel scampers up a nearby oak tree.

>WAIT
Time passes ...

The squirrel pulls acorns off the tree.

>WAIT
Time passes ...

The squirrel pulls acorns off the tree.

>WAIT
Time passes ...

The squirrel scampers down the tree with one of the acorns and buries it in a
little hole.

>WAIT
Time passes ...

A squirrel scampers up a nearby oak tree.

___________________________________

Randomized Scripts

Sometimes, you want to run a script indefinitely, but if it just repeated over and over, it would get extremely dull. You want your script to choose a random selection from a large list of possibilities.

For example, let's say we want to have a radio in our game. When the radio is on, it plays music. Of course, the player only hears the music if he is near the radio. To further complicate the problem, let's say that we want the radio to have two stations, a classical station and a rock station, and the player can tune to either station using a dial on the radio.

Although you have all the skills necessary to program your own radio switch from scratch, the dial is a bit more tricky. Fortunately, it turns out that there are already default classes for both switches and dials in the TADS library. This simplifies the task of programming these parts of the radio, so let's go ahead and do them first using the TADS classes.

The switchItem class provides reasonable defaults for the verbs "switch" (which toggles the state of the switch), "turn on," and "turn off." All we have to do is fill in the part that activates or cancels the script before passing off to the default methods:

radioSwitch : switchItem
  location = radio
  noun = 'switch'
  adjective = 'radio'
  sdesc = "radio switch"
  ldesc = "The radio is <<self.isActive ? "on" : "off">>. "
  doTurnon(actor) =
  {
    notify(radio, &script, 0);
    pass doTurnon;
  }
  doTurnoff(actor) =
  {
    unnotify(radio, &script);
    pass doTurnoff;
  }
  doSwitch(actor) =
  {
    if (self.isActive)
    {
      unnotify(radio, &script);
    }
    else
    {
      notify(radio, &script, 0);
    }
    pass doSwitch;
  }
;

The dialItem class allows the player to "TURN DIAL TO 2," for example. You need to set the properties minsetting and maxsetting to determine the acceptable range of values, and then the property setting stores the current setting of the dial.
tunerDial : dialItem
  location = radio
  noun = 'dial'
  adjective = 'radio' 'tuner'
  sdesc = "tuner dial"
  // ldesc already defined by dialItem class, says setting
  minsetting = 1
  maxsetting = 2
  setting = 1
;

Now we're ready to program the radio. Instead of applying the switch command to scriptNum as in the previous examples, we'll just switch based on a random number. The function rand generates a random number between 1 and whatever number we pass as an argument. In this case, since we only have three possibilities, we do rand(3).

Also, as discussed in Lesson 6, we can easily redirect player commands from the radio to its component. In other words, it's easy to make "TURN ON RADIO" function just like "TURN ON RADIO SWITCH:"

radio : item
  location = garden
  noun = 'radio'
  sdesc = "radio"
  ldesc = "The radio has a tuner dial and an on/off switch. "
  doTurnon -> radioSwitch
  doTurnoff -> radioSwitch
  doSwitch -> radioSwitch
  doTurnTo -> tunerDial

  script =
  {
    // Exit out of the script without doing anything if radio
    // isn't within reach, and therefore not audible
    if (not radio.isReachable(Me))
      return;
      
    if (tunerDial.setting == 1)
    {
      // Classical station
      switch(rand(3))
      {
        case 1: 
          "\bThe radio is playing a selection by Brahms. ";
          break;
        case 2:
          "\bThe radio is playing a selection by Beethoven. ";
          break;
        case 3:
          "\bThe radio is playing a selection by Grieg. ";
          break;
      }
    }
    
    if (tunerDial.setting == 2)
    {
      // Rock station
      switch(rand(3))
      {
        case 1: 
          "\bThe radio is playing a selection by Alanis Morissette. ";
          break;
        case 2:
          "\bThe radio is playing a selection by the Beatles. ";
          break;
        case 3:
          "\bThe radio is playing a selection by Phil Collins. ";
          break;
      }
    }
  }  
;

The above example introduced the concept of using the return command to exit out of a method prematurely. Also, it introduced the isReachable method which determines whether one object is reachable from another. A similar method isVisible also exists. You may wonder why I didn't just use a test like:
if (not (radio.location == Me.location or Me.isCarrying(radio))

But such a test would not have covered all the possibilities of the radio being stored in a container, etc. isReachable is a more thorough test that will cover more of these possibilities.

Incidentally, there's an even more compact way to program the radio. But doing so requires a concept we haven't discussed yet, called lists. Just to give you a taste of this now, let's finish off with an example of how this program would look using lists to simplify it.

The thing to keep in mind while reading this example is that a list is defined by writing a group of objects (numbers, strings, etc.) surrounded by square brackets. Once you've defined a list, you can access the elements of the list by number. So list[1] gives you the first object of the list, and so on.

radio : item
  location = garden
  noun = 'radio'
  sdesc = "radio"
  ldesc = "The radio has a tuner dial and an on/off switch. "
  doTurnon -> radioSwitch
  doTurnoff -> radioSwitch
  doSwitch -> radioSwitch
  doTurnTo -> tunerDial

  classicalSelections = ['Brahms' 'Beethoven' 'Grieg']
  rockSelections = ['Alanis Morissette' 'the Beatles' 'Phil Collins']
  script =
  {
    // Exit out of the script without doing anything if radio
    // isn't within reach, and therefore not audible
    if (not radio.isReachable(Me))
      return;

    "\bThe radio is playing a selection by ";
    if (tunerDial.setting == 1)
    {
      say(self.classicalSelections[rand(3)]);
    }
    if (tunerDial.setting == 2)
    {
      say(self.rockSelections[rand(3)]);
    }
    ". ";
  } 
;

Either programming method results in the same behavior. Here's how it looks when you play it:
>X RADIO
The radio has a tuner dial and an on/off switch.

>TURN ON RADIO
Okay, it's now turned on.

The radio is playing a selection by Grieg.

>WAIT
Time passes ...

The radio is playing a selection by Beethoven.

>WAIT
Time passes ...

The radio is playing a selection by Grieg.

>TURN DIAL TO 2
Okay, it's now turned to 2.

The radio is playing a selection by Alanis Morissette.

>WAIT
Time passes ...

The radio is playing a selection by Phil Collins.
See the Sample Source Code
Go on to Lesson Ten
Go Back to Lesson Eight
See the Table of Contents

The Text Adventure Development System
Version 2.2
This page is part of Mark Engelberg's TADS Tutorial
Copyright © 1999 Mark Engelberg