TADS Programming (4) - Fuses and Daemons Contributed by Michael J Roberts One of the more powerful but obscure features in TADS is the ability to schedule future operations using "fuses" and "daemons". This scheduling feature can be used for many purposes, from animating characters to setting off traps. What are fuses and daemons? The term "fuse" refers to the bit of string that you'd attach to a stick of dynamite in order to produce a delay before setting off the explosive. Just as with a fuse made of string, a TADS fuse lets you set up an event to happen after a delay. The term "daemon" is borrowed from its usage in Unix and other operating systems to refer to programs that run in the background, performing system maintenance functions automatically without user intervention. TADS daemons are functions that are executed after each turn, without the game program needing to call them explicitly. In TADS, the unit of time is a turn. Both fuses and daemons are scheduled based on turns. When you set a fuse, you specify the number of turns until the fuse "burns down", at which point the fuse function will be called. Daemons are called after each command the player enters. As an example, let's set a simple fuse that displays a message after three turns. First, we need to define the function that the fuse will call when it burns down. myFuse: function(parm) { "\bHello from myFuse!!!"; } Second, we need to set the fuse. To do this, simply call setfuse(), the built-in function that schedules a new fuse. We could define a new verb whose only purpose is to set this fuse to be called three turns from now: fuseVerb: deepverb verb = 'setfuse' action(actor) = { setfuse(myFuse, 3, nil); } ; You may be curious about two features of the myFuse function. First, what's that argument "parm"? Second, why is that "\b" sequence there? The argument "parm" is provided because TADS always passes one argument to a fuse or a daemon when it's called. The value of this argument is simply the value that you passed to the setfuse() or setdaemon() built-in function in the first place. This value isn't used by the system at all -- it's entirely for your use. The reason it's provided is so that you can use the same fuse function in several different ways if you want to; the function can figure out what it's supposed to do based on the value of the argument. In practice, most fuse and daemon functions have only one use, so the parameter is ignored, and you can just pass "nil" as the parameter value in setfuse() or setdaemon(). The "\b" sequence is included because you can't easily predict what will be displayed immediately before a fuse or daemon is invoked. Since these functions will be called by TADS itself between turns, the messages they print need to be set off from the adjacent text. The best way to do this is to display a blank line before a fuse's messages. Some people may prefer to simply print a newline and a tab; to do this, substitute "\n\t" for "\b". Daemons are very similar to fuses. The difference, of course, is that a daemon is called after every turn; a fuse is only called once, after a specified number of turns has elapsed. You should also be aware of "notifiers", which are similar to fuses and daemons, but invoke a method of an object, rather than a function. These are sometimes more convenient to code, but are otherwise the same as fuses and daemons. We could rewrite the fuse above using a notifier. notifyVerb: deepverb myNotifier = { "\bHello from notifyVerb.myNotifier!!!"; } verb = 'notify' action(actor) = { notify(self, &myNotifier, 3); } ; That ampersand, "&", in the call to notify() is quite important. It tells TADS that you're only referring to the property myNotifier for future reference, and you don't want to evaluate it immediately. Always remember to include the ampersand when calling notify(). Note that the third argument to notify() is the number of turns to wait before calling the property; if the number of turns is zero, it means that the property should be called after every turn -- which means that it acts like a daemon. Note that the new built-in function rundaemons() calls both kinds of daemons: those set with setdaemon(), and those set with notify() used with 0 as the third argument. However, a daemon started with notify() is removed with unnotify(), not with remdaemon(). Let's look at some uses for fuses and daemons. A fuse would be useful if you wanted to create a door on springs, which automatically closes a few turns after it's opened. Here's a doorway object that would behave this way. screenDoor: doorway sdesc = "screen door" noun = 'door' adjective = 'screen' location = porch springClose = { if (self.isopen) { if (Me.location = self.location) "\bThe screen door swings shut."; self.isopen := nil; } } doOpen(actor) = { notify(self, &springClose, 3); pass doOpen; } ; The springClose method demonstrates another couple of important things you should keep in mind when writing fuses and daemons. First, note that the method checks self.isopen before doing anything; this is because the player could have manually closed the door before the fuse is fired. This is often true of fuses and daemons -- because they happen after some number of player moves, the player could do something that changes the state of the game between the time the fuse is scheduled and the time it is fired. So, you should always check the current conditions at the time the fuse is fired to make sure everything is as you expect. Second, note that the method checks the player's location (Me.location) prior to displaying a message; this is because the player could have left the room, in which case the message about the door closing would be out of place. Regardless of the player's location, though, the door is closed. Daemons have many uses. One of the most obvious is for animating characters. For an example of this, you can look at lloyd.follow in Ditch Day Drifter; this is a daemon that causes Lloyd (the insurance robot) to follow the player, or to display a wacky message any time the player and Lloyd are in the same room. This daemon makes Lloyd do things on his own, which makes the game feel more alive. A less obvious use for daemons is to take some special action when a set of conditions in the game has been met. Using a daemon to check conditions can often make your coding job a lot easier, because you only have to figure out what the conditions are -- you don't have to figure out all the different ways they can be satisfied. For example, suppose that you want to design a trap similar to the venerable puzzle involving the pedestal and gold skull in the TADS Author's Manual, only you wanted to generalize it. The big opportunity for improvement is to make the trap go off whenever the pedestal is down to too little weight, regardless of how the weight got removed. One way to do this would be using the pedestal's Grab method, which is called whenever anything is removed from the pedestal. But suppose that you implemented an object that involved an evaporating liquid. Using a daemon, naturally, you could implement a flask that lost a unit of weight each turn the flask was open. Now, if you put the open flask on the pedestal, eventually enough liquid could evaporate that the pedestal trap should fire. This wouldn't be detected with Grab, because the flask isn't removed from the pedestal -- it simply gets lighter. The solution, of course, is to use a daemon. You could design a simple daemon that checks the weight of the objects on the pedestal on each turn, and sets off the trap if the weight is too low. pedestal: fixeditem, surface noun = 'pedestal' sdesc = "pedestal" location = altarRoom checkWeight = { if (addweight(self.contents) < 5) { if (Me.location = self.location) { "\bA volley of poisonous arrows shoots from the walls! You try to avoid them, but you cannot...\b"; die(); } else { "\bYou hear a loud noise somewhere nearby."; unnotify(self, &checkWeight); arrows.moveInto(self.location); } } } ; Now, we have to start the daemon somewhere. This could be done in the enterRoom(actor) method in the altarRoom the first time the player enters the room: enterRoom(actor) = { if (not self.isseen) notify(self, &checkWeight, 0); pass enterRoom; } The checkWeight daemon runs after every turn, and checks the contents of the pedestal to see if they provide enough weight to keep the trap from going off. When the weight becomes too low -- for whatever reason -- the trap goes off. Note the unnotify() call in the checkWeight daemon. This call stops the daemon. This is necessary, because the trap can only spring once; after that, you never want it activated again. If you left the daemon running, it would set off the trap on every subsequent turn, which isn't exactly what we had in mind. Fuses and daemons have many other uses. After you've experimented with these features a little bit, you'll probably find that they aren't too difficult to use, once you know the basic tricks: always pay attention to message formatting, and always check conditions at the time of the fuse's or daemon's invocation to make sure they're what you expect.