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.tIf 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:
Multi-line comments. To mark several lines as comments, mark the beginning of the commented section with /* and the end with */. Example:// This line is a comment. goldSkull : item // This is also a comment sdesc = "gold skull" noun = 'skull' 'head' adjective = 'gold' location = pedestal ;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./* This is a comment Which goes across Several lines */
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:
The way we would write the description of this function in TADS is:Add2(x) = x + 2Once this function is defined, then you can use it anywhere in your TADS program to add two to a number. For example,Add2 : function(x) { return(x + 2); }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:
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 = 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.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); } ;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.
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).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. " ;We need to replace this doTake method with one that will do what we want. This is called "overriding the method."
Let's look at the new doTake method more carefully: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(); } } ;Whenever you want to access an object's property, you use the syntax: object.propertydoTake(actor) = { if (gloves.isworn)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.
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.{ say('As you pick up the snake, it tries to bite you, but the gloves protect your hands. '); pass doTake; }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.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(); } }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.
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: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 ;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.>EXAMINE CAN This can of spinach has an easy-open top. The can of spinach is closed.You probably noticed another interesting line in the spinach program: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; } ;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!actor.strong = true;Next we program the heavy gold boulder, that can only be picked up if the player is strong from eating the spinach:
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.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; } ;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.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);.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); } ;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:
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.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. " ;Now let's create dreamland:
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.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); } } ;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:
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.)say('You can\'t go that way. ');Finally, we need to create the throne that allows the player to summon the bridge by sitting in it:
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.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; } } ;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:
Notice that the sign's ldesc is an example of using \" to put double-quotes in a doubly-quoted string.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.\" " ;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:
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.string : item noun = 'string' adjective = 'harp' sdesc = "harp string" ldesc = "The harp string is a long, thin metallic wire. " found = nil; ;Now we can create the harp:
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: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; } } ;because we are setting the property, not testing it.string.found = true;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:
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: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); } } ;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.if (Me.isCarrying(harp))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 ContentsThe Text Adventure Development System
Version 2.2
This page is part of Mark Engelberg's TADS Tutorial
Copyright © 1999 Mark Engelberg