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:
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.if (condition) { statement1; statement2; } else { statement1; statement2; }
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:
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.notify(object, &method, howFarInFuture);If you want to cancel this action that is scheduled to occur in the future, you just use the unnotify function like this:
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.unnotify(object, &method);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:
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.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 ;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:
Next, we should program a pile of ash that has no starting location, but will replace the bomb when the bomb is detonated.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. "; } ;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.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, 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.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); } ;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).
So that's all there is to it! notify to schedule something, and unnotify to cancel.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; } } ;Here's what the end result looks like:
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.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!
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.
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.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; } ;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.
Here's what it looks like in the game: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); } } ;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.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.So the fisherman script method can be rewritten as the following equivalent program:
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.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; } }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:
Here's what it looks like in the game: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; } } ;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:
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.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; } ;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).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 ;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:"
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: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; } } } ;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.if (not (radio.location == Me.location or Me.isCarrying(radio))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.
Either programming method results in the same behavior. Here's how it looks when you play it: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)]); } ". "; } ;>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 ContentsThe Text Adventure Development System
Version 2.2
This page is part of Mark Engelberg's TADS Tutorial
Copyright © 1999 Mark Engelberg