home *** CD-ROM | disk | FTP | other *** search
/ No Fragments Archive 10: Diskmags / nf_archive_10.iso / MAGS / SYNTAX / SPECIALPD1.MSA / TADS4.ART < prev    next >
Text File  |  1994-10-25  |  10KB  |  241 lines

  1.          TADS Programming (4) - Fuses and Daemons
  2.  
  3.             Contributed by Michael J Roberts
  4.  
  5. One of the more powerful but obscure features in TADS is the
  6. ability to schedule future operations using "fuses" and "daemons".
  7. This scheduling feature can be used for many purposes, from
  8. animating characters to setting off traps.
  9.  
  10. What are fuses and daemons?  The term "fuse" refers to the bit of
  11. string that you'd attach to a stick of dynamite in order to
  12. produce a delay before setting off the explosive.  Just as with a
  13. fuse made of string, a TADS fuse lets you set up an event to
  14. happen after a delay.  The term "daemon" is borrowed from its
  15. usage in Unix and other operating systems to refer to programs
  16. that run in the background, performing system maintenance
  17. functions automatically without user intervention.  TADS daemons
  18. are functions that are executed after each turn, without the game
  19. program needing to call them explicitly.
  20.  
  21. In TADS, the unit of time is a turn.  Both fuses and daemons are
  22. scheduled based on turns.  When you set a fuse, you specify the
  23. number of turns until the fuse "burns down", at which point the
  24. fuse function will be called.  Daemons are called after each
  25. command the player enters.
  26.  
  27. As an example, let's set a simple fuse that displays a message
  28. after three turns.  First, we need to define the function that the
  29. fuse will call when it burns down.
  30.  
  31.    myFuse: function(parm)
  32.    {
  33.      "\bHello from myFuse!!!";
  34.    }
  35.  
  36. Second, we need to set the fuse.  To do this, simply call
  37. setfuse(), the built-in function that schedules a new fuse.  We
  38. could define a new verb whose only purpose is to set this fuse to
  39. be called three turns from now:
  40.  
  41.    fuseVerb: deepverb
  42.       verb = 'setfuse'
  43.       action(actor) = { setfuse(myFuse, 3, nil); }
  44.    ;
  45.  
  46. You may be curious about two features of the myFuse function.
  47. First, what's that argument "parm"?  Second, why is that "\b"
  48. sequence there?
  49.  
  50. The argument "parm" is provided because TADS always passes one
  51. argument to a fuse or a daemon when it's called.  The value of
  52. this argument is simply the value that you passed to the setfuse()
  53. or setdaemon() built-in function in the first place.  This value
  54. isn't used by the system at all -- it's entirely for your use.
  55. The reason it's provided is so that you can use the same fuse
  56. function in several different ways if you want to; the function
  57. can figure out what it's supposed to do based on the value of the
  58. argument.  In practice, most fuse and daemon functions have only
  59. one use, so the parameter is ignored, and you can just pass "nil"
  60. as the parameter value in setfuse() or setdaemon().
  61.  
  62. The "\b" sequence is included because you can't easily predict
  63. what will be displayed immediately before a fuse or daemon is
  64. invoked.  Since these functions will be called by TADS itself
  65. between turns, the messages they print need to be set off from the
  66. adjacent text.  The best way to do this is to display a blank line
  67. before a fuse's messages.  Some people may prefer to simply print
  68. a newline and a tab; to do this, substitute "\n\t" for "\b".
  69.  
  70. Daemons are very similar to fuses.  The difference, of course, is
  71. that a daemon is called after every turn; a fuse is only called
  72. once, after a specified number of turns has elapsed.
  73.  
  74. You should also be aware of "notifiers", which are similar to
  75. fuses and daemons, but invoke a method of an object, rather than a
  76. function.  These are sometimes more convenient to code, but are
  77. otherwise the same as fuses and daemons.  We could rewrite the
  78. fuse above using a notifier.
  79.  
  80.  
  81.    notifyVerb: deepverb
  82.      myNotifier = { "\bHello from notifyVerb.myNotifier!!!"; }
  83.      verb = 'notify'
  84.      action(actor) = { notify(self, &myNotifier, 3); }
  85.    ;
  86.  
  87. That ampersand, "&", in the call to notify() is quite important.
  88. It tells TADS that you're only referring to the property
  89. myNotifier for future reference, and you don't want to evaluate it
  90. immediately.  Always remember to include the ampersand when
  91. calling notify().  Note that the third argument to notify() is the
  92. number of turns to wait before calling the property; if the number
  93. of turns is zero, it means that the property should be called
  94. after every turn -- which means that it acts like a daemon.  Note
  95. that the new built-in function rundaemons() calls both kinds of
  96. daemons:  those set with setdaemon(), and those set with notify()
  97. used with 0 as the third argument.  However, a daemon started with
  98. notify() is removed with unnotify(), not with remdaemon().
  99.  
  100. Let's look at some uses for fuses and daemons.
  101. A fuse would be useful if you wanted to create a door on springs,
  102. which automatically closes a few turns after it's opened.  Here's
  103. a doorway object that would behave this way.
  104.  
  105.    screenDoor: doorway
  106.       sdesc = "screen door"
  107.       noun = 'door'
  108.       adjective = 'screen'
  109.       location = porch
  110.       springClose =
  111.       {
  112.           if (self.isopen)
  113.    {
  114.        if (Me.location = self.location)
  115.            "\bThe screen door swings shut.";
  116.        self.isopen := nil;
  117.    }
  118.       }
  119.       doOpen(actor) =
  120.       {
  121.           notify(self, &springClose, 3);
  122.           pass doOpen;
  123.       }
  124. ;
  125.  
  126. The springClose method demonstrates another couple of important
  127. things you should keep in mind when writing fuses and daemons.
  128. First, note that the method checks self.isopen before doing
  129. anything; this is because the player could have manually closed
  130. the door before the fuse is fired.  This is often true of fuses
  131. and daemons -- because they happen after some number of player
  132. moves, the player could do something that changes the state of the
  133. game between the time the fuse is scheduled and the time it is
  134. fired.  So, you should always check the current conditions at the
  135. time the fuse is fired to make sure everything is as you expect.
  136. Second, note that the method checks the player's location
  137. (Me.location) prior to displaying a message; this is because the
  138. player could have left the room, in which case the message about
  139. the door closing would be out of place.  Regardless of the
  140. player's location, though, the door is closed.
  141.  
  142. Daemons have many uses.  One of the most obvious is for animating
  143. characters.  For an example of this, you can look at lloyd.follow
  144. in Ditch Day Drifter; this is a daemon that causes Lloyd (the
  145. insurance robot) to follow the player, or to display a wacky
  146. message any time the player and Lloyd are in the same room.  This
  147. daemon makes Lloyd do things on his own, which makes the game feel
  148. more alive.
  149.  
  150. A less obvious use for daemons is to take some special action when
  151. a set of conditions in the game has been met.  Using a daemon to
  152. check conditions can often make your coding job a lot easier,
  153. because you only have to figure out what the conditions are -- you
  154. don't have to figure out all the different ways they can be
  155. satisfied.
  156.  
  157. For example, suppose that you want to design a trap similar to the
  158. venerable puzzle involving the pedestal and gold skull in the TADS
  159. Author's Manual, only you wanted to generalize it.  The big
  160. opportunity for improvement is to make the trap go off whenever
  161. the pedestal is down to too little weight, regardless of how the
  162. weight got removed.  One way to do this would be using the
  163. pedestal's Grab method, which is called whenever anything is
  164. removed from the pedestal.
  165.  
  166. But suppose that you implemented an object that involved an
  167. evaporating liquid.  Using a daemon, naturally, you could
  168. implement a flask that lost a unit of weight each turn the flask
  169. was open.  Now, if you put the open flask on the pedestal,
  170. eventually enough liquid could evaporate that the pedestal trap
  171. should fire.  This wouldn't be detected with Grab, because the
  172. flask isn't removed from the pedestal -- it simply gets lighter.
  173. The solution, of course, is to use a daemon.  You could design a
  174. simple daemon that checks the weight of the objects on the
  175. pedestal on each turn, and sets off the trap if the weight is too
  176. low.
  177.  
  178.     pedestal: fixeditem, surface
  179.        noun = 'pedestal'
  180.        sdesc = "pedestal"
  181.        location = altarRoom
  182.        checkWeight =
  183.        {
  184.           if (addweight(self.contents) < 5)
  185.    {
  186.        if (Me.location = self.location)
  187.        {
  188.            "\bA volley of poisonous arrows shoots from the
  189.     walls!  You try to avoid them, but you cannot...\b";
  190.     die();
  191.        }
  192.        else
  193.        {
  194.            "\bYou hear a loud noise somewhere nearby.";
  195.     unnotify(self, &checkWeight);
  196.     arrows.moveInto(self.location);
  197.        }
  198.    }
  199.        }
  200.     ;
  201.  
  202. Now, we have to start the daemon somewhere.  This could be done in
  203. the enterRoom(actor) method in the altarRoom the first time the
  204. player enters the room:
  205.  
  206.     enterRoom(actor) =
  207.     {
  208.        if (not self.isseen) notify(self, &checkWeight, 0);
  209.        pass enterRoom;
  210.     }
  211.  
  212. The checkWeight daemon runs after every turn, and checks the
  213. contents of the pedestal to see if they provide enough weight to
  214. keep the trap from going off.  When the weight becomes too low --
  215. for whatever reason -- the trap goes off.  Note the unnotify()
  216. call in the checkWeight daemon.  This call stops the daemon.  This
  217. is necessary, because the trap can only spring once; after that,
  218. you never want it activated again.  If you left the daemon
  219. running, it would set off the trap on every subsequent turn, which
  220. isn't exactly what we had in mind.
  221.  
  222. Fuses and daemons have many other uses.  After you've experimented
  223. with these features a little bit, you'll probably find that they
  224. aren't too difficult to use, once you know the basic tricks:
  225. always pay attention to message formatting, and always check
  226. conditions at the time of the fuse's or daemon's invocation to
  227. make sure they're what you expect.
  228.  
  229.  
  230.  
  231.  
  232.  
  233.  
  234.  
  235.  
  236.  
  237.  
  238.  
  239.  
  240.                               - o -
  241. ə