A TADS Tutorial: Lesson Six
The Say Function, Verbs, the Manual & the Library
Review
In the last lesson, you were introduced to the concept of functions (which are called methods when they belong to an object). You learned how to "override" methods to produce specialized behavior when the player tries to do something to an item. However, the techniques from last week's lesson are only applicable if the item already defines some default behavior for the verb you want to specialize. For example, if an item is takeable, you know how to write a new doTake method to cause something unusual to happen when the item is taken.
Say
Lesson 5's examples contained many commands to print out text to the screen. As you know, the syntax for this is:These output commands are so common, that TADS provides a shortcut -- just drop the say command and use double-quotes instead of single-quotes. So the following command is equivalent:say ('This message will be printed to the screen.');We will use this abbreviated version from now on."This message will be printed to the screen";
Example: Programming a Flashlight
Let's say you want to create a flashlight. You already know how to create a portable light source (using the class lightsource). You also know that a lightsource object has a property islit, whose value (true or nil) determines whether the light is on or off.So if you wanted to make an unlit flashlight, you'd program it like this:
Notice how the ldesc takes cases on whether the flashlight is lit or not before describing the state of the flashlight. The description does seem a bit unwieldy; the two cases are exactly the same except for one word. Fortunately, there is a way to abbreviate this:flashlight : lightsource islit = nil location = startroom noun = 'flashlight' sdesc = "flashlight" ldesc = { if (self.islit) { "The flashlight has a switch; it is currently set to the on position"; } else { "The flashlight has a switch; it is currently set to the off position"; } } ;The << and >> indicate that we are temporarily going to stop outputting text in order to evaluate something. self.islit is evaluated. (When you want to access the property of the object you're describing, use self instead of the name of the object). If it is true, then the expression after the ? is evaluated, otherwise the expression after the : is evaluated.ldesc = "The flashlight has a switch; it is currently set to the << self.islit ? "on" : "off" >> position."Although TADS understands the verbs "TURN ON" and "TURN OFF," it has no standard way to apply this verb to a lightsource object.
It is necessary to write a specialized doTurnon and doTurnoff method for the flashlight object, but this is not enough. Before invoking the doTurnon method, TADS must verify that the verb is actually relevant for the object, and to do this it first invokes the method verDoTurnon.
So let's look at how you program a flashlight, and then we'll discuss it line-by-line:
Now let's take it line-by-line. We've already discussed the first part, so let's skip right to the new stuff. The first new method is verDoTurnon, which takes one argument, actor. As was mentioned, TADS invokes this method to verify whether the verb is suitable for this object. TADS knows that the verb is acceptable if no message is printed out.flashlight : lightsource islit = nil location = startroom noun = 'flashlight' sdesc = "flashlight" ldesc = "The flashlight has a switch; it is currently set to the << self.islit ? "on" : "off" >> position." verDoTurnon(actor) = { if (not actor.isCarrying(self)) { "You aren't carrying the flashlight! "; } else if (self.islit) { "The flashlight is already on! "; } } doTurnon(actor) = { "You turn on the flashlight, producing a powerful beam of light. "; self.islit = true; } verDoTurnoff(actor) = { if (not actor.isCarrying(self)) { "You aren't carrying the flashlight! "; } else if (not self.islit) { "The flashlight is already off! "; } } doTurnoff(actor) = { "You turn off the flashlight, and it goes dark. "; self.islit = nil; } ;In other words, to write a verification method, you need to think of all the possible cases when the verb isn't acceptable for the object, and print out a message to the user explaining why in each of those cases. So let's think about when it wouldn't be acceptable for the player to "TURN ON FLASHLIGHT." This is a ridiculous request if he's not carrying the flashlight, or the flashlight is already on. So:
While reading the example, don't forget that self refers to flashlight since we are defining a method of the flashlight object.verDoTurnon(actor) = { if (not actor.isCarrying(self)) { "You aren't carrying the flashlight! "; } else if (self.islit) { "The flashlight is already on! "; } }It is important to remember that a verification method should never do anything except print out error messages. If a verb is always fine for a certain object and there are no error cases to consider, it is perfectly acceptable to have an empty verification method (in other words, no instructions between the two braces).
If the verification method verDoTurnon prints a message, then TADS stops and doesn't try to actually turn on the object. But if the object passes the verification test, then doTurnon is invoked.
verDoTurnoff and doTurnoff work the same way:doTurnon(actor) = { "You turn on the flashlight, producing a powerful beam of light. "; self.islit = true; }Because we mentioned a switch on the flashlight, we should probably program the switch as a separate item just in case the player wants to look at it or manipulate it. The switch is a fixed part of the flashlight, so we make it a fixeditem, and set its location to be the flashlight.verDoTurnoff(actor) = { if (not actor.isCarrying(self)) { "You aren't carrying the flashlight! "; } else if (not self.islit) { "The flashlight is already off! "; } } doTurnoff(actor) = { "You turn off the flashlight, and it goes dark. "; self.islit = nil; }But what if the player types "TURN ON SWITCH" or "TURN OFF SWITCH." Right now, nothing will happen, and the player might get frustrated. What we need to do is redirect the action from the switch to the flashlight, by adding methods to the flashlightSwitch like this:flashlightSwitch : fixeditem location = flashlight noun = 'switch' adjective = 'flashlight' sdesc = "flashlight switch" ldesc = "The switch allows you turn the flashlight on or off. The flashlight is currently << flashlight.islit ? "on" : "off" >>." ;This is a bit tedious, so once again, TADS provides a handy abbreviation to allow you to quickly redirect the doTurnon and doTurnoff methods from the flashlightSwitch to the flashlight. Here's what the final code looks like with the abbreviation:verDoTurnon(actor) = { flashlight.verDoTurnon(actor); } doTurnon(actor) = { flashlight.doTurnon(actor); } verDoTurnoff(actor) = { flashlight.verDoTurnoff(actor); } doTurnoff(actor) = { flashlight.doTurnoff(actor); }flashlightSwitch : fixeditem location = flashlight noun = 'switch' adjective = 'flashlight' sdesc = "flashlight switch" ldesc = "The switch allows you turn the flashlight on or off. The flashlight is currently << flashlight.islit ? "on" : "off" >>." doTurnon -> flashlight doTurnoff -> flashlight ;
Verbs With Indirect Objects
So far, all the verbs we have looked like are ones that act on a single object. For example:"TAKE TREASURE" (verb is take, direct object is treasure).All these verbs cause a verification method to be invoked of the form: verDoTake, verDoDrop, verDoWear, verDoTurnon.
"DROP BOMB" (verb is drop, direct object is bomb).
"WEAR RING" (verb is ring, direct object is ring).
"TURN ON FLASHLIGHT" (verb is turnon, direct object is flashlight).Then if the object passes the verification test, the actual action methods are called: doTake, doDrop, doWear, doTurnon.
Have you figured out what the "do" stands for in each of these examples? It stands for "direct object," because you're defining what happens when the object is the direct object of the verb.
Some verbs, however, are a little more complicated than the examples we looked at. Some commands take both a direct object and an indirect object:
"BURN PAPER WITH MATCH" (verb is burnWith, direct object is paper, indirect object is match).Let's look at the last two examples in more depth. First let's set up a room for these examples:
"UNLOCK DOOR WITH KEY" (verb is unlockWith, direct object is door, indirect object is key).
"PUT TROPHY IN CASE" (verb is putIn, direct object is trophy, indirect object is case).
"DIG SAND WITH SHOVEL" (verb is dig, direct object is sand, indirect object is shovel).Next, we'll need a trophy to put in the trophy case. We don't set a location property for the trophy, because the player will need to solve a puzzle to find it (the player will have to dig in the sand):startroom : room sdesc = "Cave" ldesc = "You are standing in a cave. The ground is quite sandy. In the corner of the cave, there is a trophy case here. " ;You already know how to create a trophy case that you can put things in; just use the container class. But how do you make something special happen when you put the trophy in the trophy case? Here's how:trophy : item noun = 'trophy' sdesc = "trophy" ldesc = "It's a beautiful gold trophy. " ;The key, as you can see, is to define a function called ioPutIn. Notice that when a two-word verb takes only a direct object, the second word is lower-case (doTurnon), but when the two-word verb is one that takes both a direct and indirect object, the second word is upper-case (ioPutIn).trophyCase : container, fixeditem location = startroom noun = 'case' adjective = 'trophy' sdesc = "trophy case" ioPutIn (actor, dobj) = { if (dobj == trophy) { "As you place the trophy in the case, the case briefly glows. "; incscore(20); pass ioPutIn; } else { pass ioPutIn; } } ;ioPutIn takes two arguments, actor, and dobj (the direct object). The variable actor is filled in with whoever is doing the action, and the variable dobj is filled in with whatever the action is being done to. In other words, when the player types "PUT TROPHY IN CASE," it's like invoking the method trophyCase.ioPutIn(Me, trophy).
Although the player can put whatever he wants into the trophy case, we only want to reward the player if he puts the trophy into it. So our ioPutIn method tests whether the object being put into the case (dobj) is the trophy. If it is, then we print a message, increase the player's score by 20 points, and pass control to the default method for putting something into the case. If dobj isn't the trophy, then we don't do anything special.
Let's say we want to also allow the player to type "PUT TROPHY ON CASE." This tiny difference in syntax actually invokes a totally different verb (ioPutOn instead of ioPutIn). Fortunately, there's an easy way to let TADS know that for the trophy case, you want ioPutOn to be a synonym for ioPutIn. This shortcut makes it so that you don't have to rewrite new methods for ioPutOn that are basically just copies of ioPutIn. All you have to do is add the following line to the trophyCase object:
Of course, a similar technique exists for direct object verbs, even though we didn't discuss it. For example, if you've already defined a verDoPush and doPush method for an object, and you want the same thing to happen if the player pushes, touches or pokes the object, just add the line:ioSynonym ('PutIn') = 'PutOn'doSynonym ('Push') = 'Poke' 'Touch'
Indirect Object Verbs With Verification
The trophy/trophy case example worked because the trophy case was defined to be a container, and containers already support ioPutIn. So we just overrode the method with our desired specialized behavior; no verification methods were required.But our next example, digging in sand with a shovel, is a bit more tricky. There is no class of object that supports digging by default. So just like with the flashlight, we'll need to provide verification methods. But since digWith is a verb that involves two objects (a direct object and an indirect object), there are two verification methods to be defined.
When the player types "DIG SAND WITH SHOVEL," the following methods are invoked by TADS:
TADS stops if either of the verification methods prints out a message. Let's program the sand first. The sand is the direct object of the command "DIG SAND WITH SHOVEL." As always, the verification method does nothing except print out a message if the verb is invalid. We'll print an error message if the player tries to dig with anything except the shovel:shovel.verIoDigWith(Me) sand.verDoDigWith(Me, shovel) shovel.ioDigWith(Me, sand)There are a few things to notice here. verDoDigWith takes two arguments, actor and iobj (the indirect object). We print out an error message if the indirect object is not the shovel (using the symbol != which means "is not equal to"). Finally, notice that we once again use the << >> symbols to evaluate an expression in the midst of outputting. In this case, we evaluate the indirect object's thedesc property, which prints out the sdesc prepended with the word "the." The \^ symbol is a special outputting command that capitalizes the next letter.sand : fixeditem location = startroom noun = 'sand' sdesc = "sand" ldesc = "The sand looks fairly soft. You could probably dig in it if you had the right tool. " verDoDigWith(actor, iobj) = { if (iobj != shovel) { "\^<<iobj.thedesc>> isn't really the appropriate tool for the job. "; } } ;So if, for example, the player tried to dig the sand with the trophy, this verification method, if invoked, would print the error message "The trophy isn't really the appropriate tool for the job. "
Now we can program the shovel, which is the indirect object of the command "DIG SAND WITH SHOVEL."
Notice that the verification method verIoDigWith only takes one argument, actor. In the verification method, we reject the attempt to dig with the shovel if the player isn't carrying the shovel. Note the use of the word not, which turns true into nil and nil into true.shovel : item location = startroom noun = 'shovel' sdesc = "shovel" ldesc = "It looks like a strong, sturdy shovel. " verIoDigWith(actor) = { if (not actor.isCarrying(self)) { "You're not carrying the shovel! "; } } ioDigWith(actor, dobj) = { if (dobj == sand) { "You dig in the sand and discover a trophy. "; trophy.moveInto(actor.location); } else { "You use the shovel to dig for a while in <<dobj.thedesc>>, but discover nothing. "; } } ;The actual action method, ioDigWith, takes two arguments: actor and dobj (the direct object). In the method, we test what the player tried to dig with the shovel. If the player tried to dig the sand, then the trophy is uncovered. Although there are currently no objects other than the sand that will pass the dig verification tests, we should allow for the possibility that we might create another diggable object later. The else clause handles this possibility, by saying that nothing much happens if we try to dig something other than the sand.
Limitations
These techniques will work for any of the verbs that TADS understands. In other words, TADS needs to know how to recognize a verb in a player command, and then figure out which verification and action methods to invoke. TADS already understands many verbs, and in the next lesson you'll learn how to add even more verbs to that list. Here is a list of some of the verbs that TADS understands. The root word in parentheses must be prefaced with verDo, do, verIo, or io as appropriate.ASK DIRECT-OBJECT ABOUT INDIRECT-OBJECT (AskAbout)
ATTACH DIRECT-OBJECT TO INDIRECT-OBJECT (AttachTo)
ATTACK/KILL/HIT DIRECT-OBJECT WITH INDIRECT-OBJECT (AttackWith)
GET IN/GET INTO/BOARD DIRECT-OBJECT (Board)
CENTER DIRECT-OBJECT (Center)
CLEAN DIRECT-OBJECT (Clean)
CLEAN DIRECT-OBJECT WITH INDIRECT-OBJECT (CleanWith)
CLIMB DIRECT-OBJECT (Climb)
CLOSE DIRECT-OBJECT (Close)
DIG/DIG IN DIRECT-OBJECT WITH INDIRECT-OBJECT (DigWith)
DRINK DIRECT-OBJECT (Drink)
DROP/PUT DOWN DIRECT-OBJECT (Drop)
DROP/PUT DOWN DIRECT-OBJECT ON INDIRECT-OBJECT (PutOn)
EAT DIRECT-OBJECT (Eat)
FASTEN DIRECT-OBJECT (Fasten)
GET OUT/GET OUT OF/GET OFF/GET OFF OF DIRECT-OBJECT (Unboard)
GIVE/OFFER DIRECT-OBJECT TO INDIRECT-OBJECT (GiveTo)
INSPECT/EXAMINE/LOOK AT/X DIRECT-OBJECT (Inspect)
JUMP DIRECT-OBJECT (Jump)
LIE/LIE ON/LIE IN/LIE DOWN/LIE DOWN ON/LIE DOWN IN DIRECT-OBJECT (Lieon)
LOCK DIRECT-OBJECT (Lock)
LOCK DIRECT-OBJECT WITH INDIRECT-OBJECT (LockWith)
LOOK BEHIND DIRECT-OBJECT (Lookbehind)
LOOK IN DIRECT-OBJECT (Lookin)
LOOK THROUGH/LOOK THRU DIRECT-OBJECT (Lookthru)
LOOK UNDER/LOOK BENEATH DIRECT-OBJECT (Lookunder)
MOVE NORTH/MOVE N/PUSH NORTH/PUSH N DIRECT-OBJECT (MoveN)
Note: moveSVerb, moveEVerb, moveWVerb, moveNEVerb, moveNWVerb, moveSEVerb, and moveSWVerb are defined similarly.
MOVE DIRECT-OBJECT (Move)
MOVE DIRECT-OBJECT TO INDIRECT-OBJECT (MoveTo)
MOVE DIRECT-OBJECT WITH INDIRECT-OBJECT (MoveWith)
OPEN DIRECT-OBJECT (Open)
PLUG DIRECT-OBJECT INTO INDIRECT-OBJECT (PlugIn)
POKE DIRECT-OBJECT (Poke)
PULL DIRECT-OBJECT (Pull)
PUSH DIRECT-OBJECT (Push)
PUT/PLACE DIRECT-OBJECT IN INDIRECT-OBJECT (PutIn)
PUT/PLACE DIRECT-OBJECT ON INDIRECT-OBJECT (PutOn)
READ DIRECT-OBJECT (Read)
TAKE OFF DIRECT-OBJECT (Unwear)
STAND ON INDIRECT-OBJECT (Standon)
SAY DIRECT-OBJECT (Say)
SCREW DIRECT-OBJECT (Screw)
SCREW DIRECT-OBJECT WITH INDIRECT-OBJECT (ScrewWith)
SHOW DIRECT-OBJECT TO INDIRECT-OBJECT (ShowTo)
SIT ON/SIT IN/SIT/SIT DOWN/SIT DOWN IN/SIT DOWN ON DIRECT-OBJECT (Siton)
TAKE/PICK UP/GET/REMOVE DIRECT-OBJECT (Take)
TAKE/PICK UP/GET/REMOVE DIRECT-OBJECT OUT OF INDIRECT-OBJECT (TakeOut)
TAKE/PICK UP/GET/REMOVE DIRECT-OBJECT OFF/OFF OF INDIRECT-OBJECT (TakeOff)
TELL DIRECT-OBJECT ABOUT INDIRECT-OBJECT (TellAbout)
THROW/TOSS DIRECT-OBJECT AT INDIRECT-OBJECT (ThrowAt)
TOUCH DIRECT-OBJECT (Touch)
TURN OFF/DEACTIVATE/SWITCH OFF DIRECT-OBJECT (Turnoff)
ACTIVATE/TURN ON/SWITCH ON DIRECT-OBJECT (Turnon)
TURN DIRECT-OBJECT (Turn)
TURN DIRECT-OBJECT WITH INDIRECT-OBJECT (TurnWith)
TURN DIRECT-OBJECT TO INDIRECT-OBJECT (TurnTo)
TYPE DIRECT-OBJECT ON INDIRECT-OBJECT (TypeOn)
UNFASTEN/UNBUCKLE DIRECT-OBJECT (Unfasten)
UNLOCK DIRECT-OBJECT (Unlock)
UNLOCK DIRECT-OBJECT WITH INDIRECT-OBJECT (UnlockWith)
UNPLUG DIRECT-OBJECT (Unplug)
UNPLUG DIRECT-OBJECT FROM INDIRECT-OBJECT (UnplugFrom)
UNSCREW DIRECT-OBJECT (Unscrew)
UNSCREW DIRECT-OBJECT WITH INDIRECT-OBJECT (UnscrewWith)
WEAR DIRECT-OBJECT (Wear)
The Manual
About 70% of a typical adventure game can be programmed using the techniques discussed in these six lessons. At this point, you should be well on your way to creating your own interesting work of interactive fiction. Remember, like any story, spelling and grammar counts. Although upcoming lessons will continue to introduce advanced techniques, I will be slowing down the presentation of new concepts in order to give you more time to work on your own project. Spend some time playing other TADS games, plan out your own story, and most of all, spend plenty of time programming your ideas.There are a couple other great resources that will help you learn more advanced TADS techniques. The programming/tads/ directory on the IF-Archive contains a manual subdirectory. Within that subdirectory, download the TADS Author's Manual.
Chapters 1, 9, and 10 of the Author's Manual contain more programming examples, much like the ones contained in these lessons. Most of the examples will look familiar to you, although there are a few topics in Chapter 10 that we have not yet covered (like how to create other characters and vehicles).
Chapters 2 and 3 are mostly introductory material, giving an overview of how the language works. These chapters also have much overlap with the lessons you've already completed.
Chapters 4 through 7 contain a very in-depth explanation of how the TADS parser works. It goes into more detail than you're likely to need unless you decide to become very advanced at TADS programming.
Appendix A describes all the subtleties of the built-in TADS classes. Many of these classes were discussed in Lessons 3 and 4 (readable, container, etc.) but there are many more that you can use (doorways, obstacles, dialItems, etc.). They are all described in Appendix A.
Chapter 8 contains the most new material. TADS is a full-fledged programming language, and it comes complete with all the standard ways to manage flow control and manipulate data, including lists of items. If you've ever used another programming language, you'll find that TADS can probably do all the same things. So far, we have only talked about the if-else construct, because just this one programming construct allows you to handle most interactive fiction programming issues. However, there are certain cases where other programming constructs (like switch-case, for loops, while loops, etc.) are useful. Chapter 8 details all these capabilities of TADS as a programming language.
You may have found some revision notes with your compiler which contain a description of all the features that were added to TADS after version 2 of the system came out. Sometimes these notes describe things in more detail, and with more examples, than the manual.
In summary, Chapters 1, 2, 3, 9, and part of 10 provide a nice review of the material you've already learned. Some of you may be ready for the advanced material covered in Chapter 10, Chapter 8, Appendix A and Chapters 4 through 7.
Adv.t
Another excellent resource is the source file adv.t, that comes with the TADS compiler. This file contains the source code for all the default TADS classes. Although Appendix A gives a pretty good overview of what's in adv.t, this is the place to look if you want the final say on how the default verbs and classes work.Speaking of source code, programs are available for several other TADS games. If you would like to see more programming examples from actual games, look in the games/tads/source directory of the IF-Archive.
Debugging
Errors in TADS fall into a couple broad categories. You've already encountered compiler errors. Compiler errors occur when TADS can't make sense out of your program when you go to compile it. Usually this is because of a missing semicolon, or a misspelled name. Many of you have become quite adept at tracking down these errors and fixing them.But there are some errors that the compiler can't catch, because sometimes your program looks right, but is actually broken. These errors aren't encountered until the player actually tries to run the game, so they are called run-time errors. Run-time errors are much more difficult to debug, because the errors are even less descriptive than compiler errors. If you haven't encountered any run-time errors, you probably will soon.
The best way to fix run-time errors is with a specialized tool called a debugger. Before you can use the debugger, you must compile your program with another option -- the debugging switch. In other words, on a command prompt system, change the compile command to be:
Or look for the option in one of the menus of your compiler. This option adds extra debugging information to the program.tc -C -ds exercise6.tThen, launch the TADS Debugger. Once the debugger has started, open your .gam file.
Initially, your game will be paused. To start it, select Go from the Debug menu. You can play your game as usual, but if your program creates a run-time error, you should receive a more descriptive message.
The TADS debugger has many useful abilities; too many to go into here. Many of its capabilities are discussed in Appendix G of the manual.
Some things you can do include:
Place a breakpoint in your program so that the debugger will pause your game when it executes a certain line of your program.Step through your program line-by-line, and evaulate expressions along the way to make sure all the variables contain what you think they do.
Use the stack and the call trace to help you determine how the program got to wherever it is.
See the Sample Source Code
Go on to Lesson Seven
Go Back to Lesson Five
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