home *** CD-ROM | disk | FTP | other *** search
- 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.
-
-
-
-
-
-
-
-
-
-
-
-
- - o -
- ə