___________________________________

A TADS Tutorial: Lesson Five

Methods

___________________________________

Review

In the first four lessons, you learned how to use the default TADS classes to connect a series of rooms, and then populate the rooms with simple items like notes, lights, chairs, and containers. It's great that TADS provides so many useful classes to handle most of the standard types of items you may want to include in your game, but what if you want to create something really unusual? This is what we'll be looking at in this lesson. The key to creating objects with specialized behavior is to understand the concept of a function. It will take several lessons for you to master all the techniques for writing functions, and then you'll be able to create just about anything you can imagine!

___________________________________

Compiling

From now on, add the C option to your compile command. In other words, if you're using a command-prompt system, then type:
tc -C lab5.t

If you're using a GUI, then one of the menus on your compiler should give you the option of using C-style operators.

___________________________________

Comments

First, a word about comments. As your program grows in length, it can get harder and harder to remember the purpose of each object, and why you put it there. In order to make your program easier to read, it is often useful to include English comments that help you jog your memory later. Of course, if you just type the comments in directly, the compiler will assume the comments are part of the program, and it will give you lots of errors, so you have to indicate to the compiler which parts are your comments. There are two ways to do this:

Single-line comments. Anything to the right of two forward-slashes (//), on the same line as the slashes, is considered to be a comment. Example:

// This line is a comment.
goldSkull : item       // This is also a comment
   sdesc = "gold skull"
   noun = 'skull' 'head'
   adjective = 'gold'
   location = pedestal
;

Multi-line comments. To mark several lines as comments, mark the beginning of the commented section with /* and the end with */. Example:
/*
This is a comment
Which goes across
Several lines
*/

You can also use multi-line comment markers to make the compiler ignore sections of your program. Just put a /* at the beginning of the section that you want the compiler to ignore, and a */ at the end. This can be a handy technique for tracking down compiler errors. If you get an error that doesn't make sense, you can try "commenting out" sections of your program until the error goes away. That should help you get an idea which section is causing the error.

___________________________________

Functions

Before getting into the specifics of how functions are written in TADS, it is important to understand the general concept of a function. Function is a mathematical term for something that takes zero, one, or more inputs (which programmers call arguments) and generates an output (which programmers call a return value). Some people like to think of functions as machines that process the inputs into an output.

As a simple example, imagine a mathematical function called Add2. It has one input, which is expected to be a number. It outputs the number plus 2. In the language of mathematics, we would write this as:

Add2(x) = x + 2

The way we would write the description of this function in TADS is:
Add2 : function(x)
{
  return(x + 2);
}

Once this function is defined, then you can use it anywhere in your TADS program to add two to a number. For example,
goldSkull : item       
  weight = Add2(5)  // I am setting the weight to be 5 + 2 = 7.
;

___________________________________

Methods

As we've already discussed, TADS is an object-oriented programming language. That means that we write TADS programs as a series of object descriptions. Objects contain many properties, and we can set the values of those properties. But objects can also contain functions! Any function that belongs to an object is called a method.

We'll be looking at many examples of functions and methods.

___________________________________

Special Travel Behavior

It is possible to create special travel effects, like displaying a message when the player moves between two rooms, or setting off a trap, or preventing travel until an obstacle has been removed.

The trick to implementing all these travel effects is that instead of assigning a simple value to a property, you can write a method. In this case, the method has no inputs, but the method can issue a series of programming commands or statements before returning a value. This may not make a lot of sense yet, so let's look at an example:

livingRoom : room
  sdesc = "Living Room"
  ldesc = "You're in the living room.  A dark stairway leads down. "
  down = cellar
;

The above example is what you already know how to do. The following example demonstrates a new technique for adding travel messages.
livingRoom : room
  sdesc = "Living Room"
  ldesc = "You're in the living room.  A dark stairway leads down."
  down = 
  {
    say('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 thing to notice here is that the value of down was replaced with a method. A function or method is always comprised of a sequence of statements surrounded by braces { }. Each statement must end with a semicolon. The last statement will typically be a return statement which determines the output of the method. In this case, since down expects a room name as its value, we need to make sure that a room name is returned from the method. Also note that the first statement invokes the function say, which prints the subsequent string (indicated by single quotes) to the screen.

Another thing you may have noticed is the \b at the end of the travel message. \b is one of several sequences that the say function recognizes as a special print command. Specifically, \b is a command to end the current line and then print a blank line. Another useful print command is \n, which ends the current line, and starts a new line without printing a blank line. Another one is \t, which indents the output, just like pressing the tab key.

If you don't want the player to go anywhere after the travel message is displayed, that's easy too. Just return nil and the player won't go anywhere. This is useful when you want to display a more specific message than "You can't go that way" for an illegal direction. For example,

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 =
  {
    say('The stairway is in no condition to be climbed. ');
    return(nil);
  }
;

___________________________________

Example: Taking a Snake

Let's create a puzzle in which the player must use special gloves in order to pick up a deadly snake.

First, we need to create the gloves for the player to wear.

gloves : clothingItem
  location = startroom
  noun = 'gloves'
  adjective = 'rubber'
  sdesc = "rubber gloves"
  ldesc = "Upon close examination, you see that the gloves are
  reinforced with some sort of metallic fiber. "
;

Next we need to program the snake. We make the snake an item so that it is possible to pick it up. But the default "TAKE" behavior isn't going to work for us. The default behavior for taking the snake is handled in a method called doTake. The method takes one argument (actor), which is typically going to be the player's character (once you start adding other characters to the game, it becomes possible for one of the non-player characters to try to pick up the snake, so the actor argument would be replaced by whoever is trying to pick up the snake).

We need to replace this doTake method with one that will do what we want. This is called "overriding the method."

snake : item
  location = cellar
  noun = 'snake' 'cobra'
  adjective = 'poisonous' 'venomous'
  sdesc = "poisonous snake"
  ldesc = "The venemous cobra flares his hood and bares his fangs. "
  
  doTake(actor) = 
  {
    if (gloves.isworn)
    {
      say('As you pick up the snake, it tries to bite you, but the
      gloves protect your hands. ');
      pass doTake;
    }
    else
    {
      say ('As you attempt to take the snake, it sinks its fangs into your
      bare hands and injects you with a paralyzing venom.  In a matter of
      minutes, you are dead. ');
      die();
    }
  }
;

Let's look at the new doTake method more carefully:
doTake(actor) = 
  {
    if (gloves.isworn)

Whenever you want to access an object's property, you use the syntax: object.property

All clothingItem objects have a property isworn that is set to true when it is being worn, and nil if it is false. Note that the above line doesn't have a semicolon at the end of it. That's because it isn't a function, it's just a keyword that tests whether the expression in parentheses is true or nil. If the expression is true it executes the code that immediately follows in braces, otherwise it skips down to the code in braces after the else.

   {
      say('As you pick up the snake, it tries to bite you, but the
      gloves protect your hands. ');
      pass doTake;
    }

So this code is going to be executed if the gloves are being worn (i.e., gloves.isworn is true). The last line, pass doTake; is a command to go ahead and perform the standard doTake method here. So what this code says is that if the gloves are being worn, the message is printed out that the gloves protect your hands, and then you go ahead and successfully take the snake.
   else
    {
      say ('As you attempt to take the snake, it sinks its fangs into your
      bare hands and injects you with a paralyzing venom.  In a matter of
      minutes, you are dead. ');
      die();
    }
  }

This is the code that is going to be executed if the gloves aren't being worn (i.e., when gloves.isworn is nil). The last line, die();, invokes a function that kills the player. The function takes no arguments, so that's why there's nothing inside of the parentheses.

Here's what the final program looks like when you run it:

>GET SNAKE
As you attempt to take the snake, it sinks its fangs into your bare hands and
injects you with a paralyzing venom.  In a matter of minutes, you are dead.

*** You have died ***

In a total of 2 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:  >UNDO
(Undoing one command)

Cellar
   You're in the cellar.  A huge pile of broken pieces of wood that was once a
stairway fills half the room.
   You see a poisonous snake here.

>WEAR GLOVES
Okay, you're now wearing the rubber gloves.

>GET SNAKE
As you pick up the snake, it tries to bite you, but the gloves protect your
hands.  Taken.

___________________________________

Example: Lifting a Boulder

Let's create a puzzle in which the player has to eat spinach in order to become strong enough to lift an enormous gold boulder (like Popeye!).

First, we need to create the can of spinach.

can : openable
  location = startroom
  noun = 'can'
  adjective = 'spinach'
  sdesc = "can of spinach"
  ldesc = 
  {
    say('This can of spinach has an easy-open top. ');
    pass ldesc;
  }
  isopen = nil
;

The can is an openable object, which you've seen before. But if you recall, I warned you before that you need to be careful not to set the ldesc property because the ldesc property automatically describes whether the object is open or closed, and you don't want to replace this message. But now you know how to print your own description of the object, and then invoke the default ldesc which describes whether the object is open or closed. This is what the above example demonstrates. Here's what it will look like when you play the game:
>EXAMINE CAN
This can of spinach has an easy-open top.  The can of spinach is closed.

Now we need to program the spinach that is inside of the can. Let's print out a message to let the player know that eating the spinach made him strong, and then pass control to the default doEat method.
spinach : fooditem
  location = can
  noun = 'spinach'
  sdesc = "spinach"
  adesc = "some spinach"
  ldesc = "It looks quite tasty. "
  doEat(actor) = 
  {
    say ('You feel imbued with an incredible sense of strength. ');
    actor.strong = true;
    pass doEat;
  }
;

You probably noticed another interesting line in the spinach program:
actor.strong = true;

This sets the strong property in the actor object to true (don't forget -- actor is an input, a variable that will be set to whoever tries to eat the spinach). Interestingly, the strong property is not a standard property of actors -- we created the strong property just by using it!

Next we program the heavy gold boulder, that can only be picked up if the player is strong from eating the spinach:

goldBoulder : item
  location = cellar
  noun = 'boulder'
  adjective = 'gold'
  sdesc = "gold boulder"
  ldesc = "This enormous boulder is made of pure gold.  If only you
  could take it, you'd be rich!"
  
  doTake(actor) = 
  {
    if (actor.strong)
    {
      say('You hoist the boulder as easily as a pebble. ');
      pass doTake;
    }
    else
    {
      say('You struggle to lift the boulder, but fail.  You are too
      weak.');
    }
  }
  
  doDrop(actor) = 
  {
    say ('The boulder crashes to the ground with a mighty thud. ');
    pass doDrop;
  }
;

In addition to overriding the doTake method, we've also added a special message to the doDrop method to give the boulder a sense of weight.

Here's what the player sees:

>X BOULDER
This enormous boulder is made of pure gold.  If only you could take it, you'd
be rich!

>GET BOULDER
You struggle to lift the boulder, but fail.  You are too weak.

>OPEN CAN
Opening the can of spinach reveals some spinach.

>EAT SPINACH
You feel imbued with an incredible sense of strength.  That was delicious!

>GET BOULDER
You hoist the boulder as easily as a pebble.  Taken.

>DROP BOULDER
The boulder crashes to the ground with a mighty thud.  Dropped.

___________________________________

Example: Traveling to Dreamland

Let's create a puzzle in which the player needs to sleep on a bed in order to travel to a surreal place called Dreamland.
bed : beditem
  location = bedroom
  noun = 'bed'
  sdesc = "bed"
  ldesc = "The bed looks so comfy, you'd really like to lie down and 
  take a nap. "
  
  doLieon(actor) = 
  {
    say ('You lie down on the bed, snuggle under the covers, and drift
    off to sleep ...\b');
    incscore(10);
    actor.travelTo(dreamland);
  }
;

beditem objects have a method called doLieon, which is invoked whenever a character lies down on the bed. In this example, we override the method so that instead of actually lying on the bed, the player gets a message that he has fallen asleep. Then we increase the player's score by 10 points with the command incscore(10);.

Just as properties belong to objects, so do methods, and we can access them using the same syntax. All characters contain a method called travelTo which is used to move them from place to place. In fact, when the player types directional commands, such as "EAST" and "NORTH," the player's travelTo method is being invoked. So as our last step, we use the travelTo method to transport the actor (the player) to the room dreamland.

Here's what the player sees:

Bedroom
   There is a bed here.  The exit is to the west.

>EXAMINE BED
The bed looks so comfy, you'd really like to lie down and take a nap.
 
>LIE ON BED
You lie down on the bed, snuggle under the covers, and drift off to sleep...

Dreamland
   You are standing on a rocky crag, surrounded on all sides by a great abyss. 
There is a throne here.

___________________________________

Example: Magically Summoning a Bridge

Let's create a puzzle in which the player must sit in a magic throne to cause a bridge to appear.

First, let's program the bridge:

bridge : fixeditem
  noun = 'bridge'
  sdesc = "bridge"
  ldesc = "The bridge stretches into the distance as far as you can see.
  You don't know where it goes, but at least it's a way off this lonely crag. "
;

Notice that the bridge does not have an assigned location. That's because the bridge doesn't exist anywhere in the game until the player summons it.

Now let's create dreamland:

dreamland : room
  sdesc = "Dreamland"
  ldesc = 
  {
    say ('You are standing on a rocky crag, surrounded on all 
    sides by a great abyss. There is a throne here. ');
    if (bridge.location == dreamland)
    {
      say('A glowing bridge leads north into the distance. ');
    }
  }
  north =
  {
    if (bridge.location == dreamland)
    {
      say('You walk along the bridge for what seems like an
      eternity ...\b');
      return(clouds);
    }
    else
    {
      say('You can\'t go that way. ');
      return(nil);
    }
  }
;

There are a few things worth mentioning about this example. First, ldesc has been modified so that it prints out the description of the room, and if the bridge is present, then it also describes the bridge.

Second, look at how we actually test whether the bridge is present. In particular, notice the two equal signs! You use two equal signs whenever you want to test whether two things are equal. You use only one equal sign when you want to set the property on the left side of the equal sign to be equal to the value of the expression on the right side of the equal sign. So, as you can see, two equal signs mean something very different than one equal sign. It is very important to keep this distinction in mind.

Third, north has been modified to test for whether the bridge exists before allowing the player to walk in that direction.

Fourth, did you notice the \' in the line:

say('You can\'t go that way. ');

The reason for this is that the ' in can't will confuse the compiler into thinking that the string has ended. The \' tells the compiler that you really just mean the character ' and you don't mean to end the string yet. Similarly, you can use \" to put double-quote characters in a doubly-quoted string. (We'll look at an example of that in a bit.)

Finally, we need to create the throne that allows the player to summon the bridge by sitting in it:

throne : chairitem
  location = dreamland
  noun = 'throne'
  sdesc = "throne"
  ldesc = "The throne is located at the highest point of the crag, facing  
  north. "
  doSiton(actor) =
  {
    if (bridge.location == dreamland)
    {
      pass doSiton;
    }
    else
    {
      say ('As you sit in the throne, a magical, glowing bridge appears
      before you, spanning the great abyss. ');
      bridge.moveInto(dreamland);
      incscore(20);
      pass doSiton;
    }
  }
;

To do this, we override the chair's doSiton method, which is invoked when the player sits in the chair. First, we test whether the bridge is already in dreamland. If it is, then we just do the default sit action and nothing special happens.

However, if the bridge is not in dreamland, then it is summoned. We print out a message to this effect, move the bridge to dreamland, increase the player's score by 20, and then finish by letting him actually sit in the chair.

Note that the bridge was moved to dreamland using the bridge's moveInto method. All items are moved around with moveInto, just like all actors travel around using the travelTo method.

Here's what the player will see:

Dreamland
   You are standing on a rocky crag, surrounded on all sides by a great abyss. 
There is a throne here.

>EXAMINE BRIDGE
I don't see any bridge here.

>EXAMINE THRONE
The throne is located at the highest point of the crag, facing north.

>SIT ON THRONE
As you sit in the throne, a magical, glowing bridge appears before you,
spanning the great abyss.  Okay, you're now sitting on the throne.

>STAND
Okay, you're no longer in the throne.

>LOOK
Dreamland
   You are standing on a rocky crag, surrounded on all sides by a great abyss. 
There is a throne here.  A glowing bridge leads north into the distance.

>EXAMINE BRIDGE
The bridge stretches into the distance as far as you can see.  You don't know
where it goes, but at least it's a way off this lonely crag.

>NORTH
You walk along the bridge for what seems like an eternity...

Clouds
   You're standing on fluffy clouds.  The bridge from where you came is to the
south.  A set of pearly gates lie to the east.  A sign is posted on the gate.
   You see a harp here.

___________________________________

Example: Getting Into Heaven

The idea behind this puzzle is that the player is standing in front of the pearly gates of heaven. A sign on the gates says that only angels are allowed in. The player can't get into heaven unless he is holding a harp so that he looks like an angel to the invisible (but stupid) guards.

Let's decorate the room with the gate and the sign:

gates : decoration
  location = clouds
  noun = 'gates' 'gate'
  adjective = 'pearly'
  sdesc = "pearly gates"
  adesc = "a set of pearly gates"
  ldesc = "The pearly gates look exactly as you always imagined the
  entrance to heaven would look.  The gates are wide open, but there is
  a sign hanging from one of the gates. "
;

sign : readable, fixeditem
  location = clouds
  noun = 'sign'
  sdesc = "sign"
  ldesc = "The sign says, \"ANGELS ONLY.\" "
;

Notice that the sign's ldesc is an example of using \" to put double-quotes in a doubly-quoted string.

When the player picks up the harp, we want one of the harp strings to break off and become a separate object. The player will need this string later to solve another puzzle.

First, let's create the string, but we won't assign it a location, because it doesn't exist in the game until the player picks up the harp:

string : item
  noun = 'string'
  adjective = 'harp'
  sdesc = "harp string"
  ldesc = "The harp string is a long, thin metallic wire. "
  found = nil;
;

We've created a new property found that will be set to true once the harp string has been discovered by the player. This will help us keep track of whether the puzzle has been solved.

Now we can create the harp:

harp : item
  location = clouds
  noun = 'harp'
  sdesc = "harp"
  ldesc = "The harp is big and shiny. "
  doTake(actor) = 
  {
    if (string.found)
    {
      pass doTake;
    }
    else
    {
      say('As you pick up the harp, a string comes loose and falls to
      the ground. ');
      string.found = true;
      string.moveInto(actor.location);
      incscore(10);
      pass doTake;
    } 
  }
;

So as you can see, if the string has already been found, we don't need to do anything special. But if it hasn't been found, then we tell the player that a string has come loose. Next we set the found property of string to be true. Notice that there is only one equal sign in the statement:
string.found = true;

because we are setting the property, not testing it.

Next, we move the string to wherever the actor happens to be located (which is stored in the actor's location property).

Then we increase the player's score by 10 points, and finish up by letting him take the harp normally.

Finally, we're ready to program the room:

clouds : room
  sdesc = "Clouds"
  ldesc = "You're standing on fluffy clouds.  The bridge from where you
  came is to the south.  A set of pearly gates lie to the east.  
  A sign is posted on the gate. "
  south =
  {
    say ('You\'d rather not walk back across that long bridge again. ');
    return (nil);
  }
  east = 
  {
    if (Me.isCarrying(harp))
    {
      say('As you begin to walk toward the gate, you hear two 
      disembodied voices talking about you.
      \n\t"Do you think he looks like an angel?"
      \n\t"Well, he\'s got a harp, doesn\'t he?"\b ');

      return(heaven);
    }
    else
    {
      say('As you begin to walk toward the gate, you hear two 
      disembodied voices talking about you.
      \n\t"Do you think he looks like an angel?"
      \n\t"Nah, I don\'t think so.  We better stop him."
      \n\t A jolt of energy zaps your body and prevents you from passing
      through the gate.  Stunned, you walk backward a few steps. ');
      
      return(nil);
    }
  }
;

Look at the condition we test for in the east method. We test for whether the player is holding the harp to see whether he is allowed to walk through the gates. The way this is tested is with:
if (Me.isCarrying(harp))

Me is the name of the object representing the player. (We could have used Me in many of the other examples, but by using the variable actor instead, the overridden methods apply to any character in the game, not just the player.) So Me.isCarrying(harp) returns true if harp is in the player's inventory, and nil if it isn't.

Here's what the player will see:

Clouds
   You're standing on fluffy clouds.  The bridge from where you came is to the
south.  A set of pearly gates lie to the east.  A sign is posted on the gate.
   You see a harp here.

>X GATES
The pearly gates look exactly as you always imagined the entrance to heaven
would look.  The gates are wide open, but there is a sign hanging from one of
the gates.

>READ SIGN
The sign says, "ANGELS ONLY."

>E
As you begin to walk toward the gate, you hear two disembodied voices talking
about you.
   "Do you think he looks like an angel?"
   "Nah, I don't think so.  We better stop him."
   A jolt of energy zaps your body and prevents you from passing through the
gate.  Stunned, you walk backward a few steps.

>GET HARP
As you pick up the harp, a string comes loose and falls to the ground.  Taken.

>L
Clouds
   You're standing on fluffy clouds.  The bridge from where you came is to the
south.  A set of pearly gates lie to the east.  A sign is posted on the gate.
   You see a harp string here.

>GET STRING
Taken.

>E
As you begin to walk toward the gate, you hear two disembodied voices talking
about you.
   "Do you think he looks like an angel?"
   "Well, he's got a harp, doesn't he?"

Heaven

___________________________________

Limitations

The techniques described in this lesson only work for modifying methods that the object already supports by default. For example, all items support doTake, doDrop, and doInspect (invoked when the player looks at the item). clothingItem objects also support doWear and doUnwear.

All the various container classes support doOpen, doClose, and doLookin.

beditem objects support doLieon ("LIE ON BED"), doStand ("STAND"), doBoard ("GET ON BED"), and doUnboard ("GET OFF OF BED").

chairitem objects support doSiton ("SIT ON CHAIR"), doStand ("STAND"), doBoard ("GET ON CHAIR"), and doUnboard ("GET OFF OF CHAIR").

fooditem objects support doEat.

seethruItem objects support doLookthru.

underHider objects support doLookunder.

behindHider objects support doLookbehind.

searchHider objects support doSearch.

In the next lesson we'll look at how to create totally new verbs and write methods for objects that don't ordinarily support them. In the meantime, this should give you plenty of possibilities to play with.

___________________________________

Exercise

Experiment with these techniques. Try implementing the following puzzles, or create your own:

An incredibly sticky item that can't be dropped!

A treasure chest contains a bomb, and will explode when the chest is opened. The explosion will kill the player unless the player is wearing armor.

A book is on a bookcase in a mansion library. When you try to take the book, instead of actually getting the book, you end up activating a hidden mechanism which rotates the bookcase and puts you in a secret room.

A ring that when worn, lets you see paranormal activities.

See the Sample Source Code
Go on to Lesson Six
Go Back to Lesson Four
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