This file is part of the TADS 2 Authors Manual.
Copyright © 1987 - 2002 by
Michael J. Roberts. All rights reserved.
The manual was converted to HTML and edited by NK Guy, tela design.
So far, Ive concentrated on the basic aspects of writing a game with TADS. Once you get started on a game, youll probably have lots of ideas for plot elements in your game. Many parts of an adventure game can be implemented very easily with TADS, simply by piecing together the basic classes from adv.t and filling in descriptions, locations, and so forth. However, some of the things you will want to implement wont be that easy, and you will need to do some programming.
This chapter helps you realize your more complex ideas by showing how to implement a number of sophisticated plot elements. Ive tried to show examples of the most common difficult game features, so theres a good chance that youll find an example in this chapter thats very similar to what youre trying to do. Even if you dont find a close match to the feature you have in mind, reading this chapter will show you many of the more subtle points of TADS programming, which should give you a better idea of how to implement your ideas.
Its always best to try to limit the number of verbs a player needs to use to complete your game, because it helps prevent your adventure from becoming a game of guess the verb. Choosing the right sentence to express an idea for a command should never be a challenge - if it is, your game will quickly become uninteresting.
However, that being said, you will sometimes want to include situations in your games that call for verbs that arent in the set defined in adv.t. In addition, theres nothing wrong with making your game understand obscure verbs and phrasings that are alternatives to one of the more common verbs in adv.t. Furthermore, some games include magic words that are effectively special verbs; since the player learns these within the game, obscurity is not a problem.
The way you add a verb depends on how the verb is used. The simplest kind of verb is used by itself - the player doesnt include any objects in a command with the verb. For example, north is a verb that doesnt take any objects; magic words generally fall into this category as well. To implement a verb with no objects, all you have to do is create a deepverb object that includes a verb vocabulary property, an sdesc property, and an action( actor ) method. (The deepverb class is defined in adv.t; see Appendix A for details.) The action method is called when the verb is used with no objects. As an example, lets implement a verb for a magic word; when the magic word is used, the player will be magically transported from the cave to the shack, or the shack to the cave.
magicVerb: deepverb verb = 'xyzzy' sdesc = "xyzzy" action( actor ) = { if ( actor.location = cave ) { "Out of nowhere, a huge cloud of orange smoke fills the cave. When it clears, you suddenly realize that you're in the shack! "; actor.moveInto(shack); } else if (actor.location = shack) { "You suddenly feel very disoriented, and the room seems to be spinning all around you. As you gradually regain your balance, you realize that you're now in the cave! "; actor.moveInto( cave ); } else "Nothing happens. "; } ;For a verb that takes a direct object, the verb object itself is fairly simple; all you need to do is define verb and sdesc properties, plus the special property doAction. The doAction property defines a string that the system uses to form the names of two new properties. As an example, well implement the verb smell, which takes as its direct object the item to be smelled.
smellVerb: deepverb verb = 'smell' sdesc = "smell" doAction = 'Smell' ;When the system sees this definition, it creates two new properties: verDoSmell and doSmell. Now, for any object that the player can smell, you add the verDoSmell and doSmell methods. For example, well implement a flower (notice that we deftly bypass the opportunity for a cheap joke here at the expense of good taste by choosing an object with a non-offensive odor):
flower: item noun = 'flower' 'rose' adjective = 'red' sdesc = "red rose" ldesc = "It's a red rose. " verDoSmell( actor ) = {} doSmell( actor ) = { "A rose by any other name... "; } ;If the verDoSmell method isnt defined for an object, trying to smell it will cause the system to display the message I dont know how to smell the object. Since smell is a verb that you may want to allow for every object in your game, you may want to modify the definition of thing in adv.t to provide a better default message for this verb. You could add these lines to the definition of thing in adv.t:
verDoSmell( actor ) = {} doSmell( actor ) = { "It smells like any other <<self.sdesc>>. "; }If you do this, all you need to do is override the doSmell method for any object with a non-default odor.
Adding a verb that takes two objects (a direct object plus an indirect object) is similar. Rather than defining a doAction property, you define an ioAction(preposition) property. For example, suppose you want to implement a verb pry direct-object with indirect-object:
pryVerb: deepverb verb = 'pry' sdesc = "pry" ioAction( withPrep ) = 'PryWith' prepDefault = withPrep ;This causes the system to create the properties verIoPryWith, verDoPryWith, and ioPryWith, which the system will call in that order (verDoPryWith is called for the direct object, and the other two are called for the indirect object.) Note that weve also defined a default preposition; while this isnt required, it allows the system to be smarter by guessing what the player meant when some words are left out. For example, if the only object present that can be used as an indirect object in the pry command is a crowbar, the system can provide the with the crowbar phrase by default when the player simply types pry the door.
Whether or not you add verbs to the basic set in adv.t, its a good idea to document your verbs. In the instructions for your game, you should provide a list of all of the verbs that are needed to complete the game. If players gets stuck, this list can be helpful in convincing them that theyre not fighting the parser.
The basic travel routines used by adv.t make it easy to implement a basic map: all you have to do in your definitions is to provide properties with names like north and south, and set them to point to rooms, simply by naming the room:
kitchen: room north = hallway east = porch ;This is fine for most situations, but frequently youll want something to happen during travel thats more complicated than simply moving the player to the given room. For example, if you have a very long hallway, you may want a special message to be displayed when travelling to indicate that the walk was especially long. Or, you may want to set off a trap when the player takes a certain exit. Or, you may want to prevent travel in a particular direction until a door has been opened or some other obstacle has been removed.
The basic trick to implementing special travel effects like these is to realize that the direction properties of a room arent limited to simple room pointers - you can write a method instead. As long as you follow a few simple rules, you can do whatever you want in this method. Basically, if you want normal travel to occur after the method is finished, the method should return a room object which indicates the destination of the travel. If you dont want anything more to happen after the method is finished, the method should return nil. The following sections show how to use direction methods to implement a variety of special travel effects.
One of the simplest special effects is displaying a message during travel. For example, suppose you have a stairway leading down, but the stairs collapse while the player is descending them, dropping the player unceremoniously into the room below (with no harm done). All you really want to do here is to display a message about the stairs collapsing, then complete the travel as normal.
livingRoom: room sdesc = "Living Room" ldesc = "You're in the living room. A dark stairway leads down. " down = { "You start down the stairs, which creak and moan and wobble uncertainly beneath you. You stop for a moment to steady yourself, then continue down, when the entire stairway suddenly breaks away from the wall and crashes to the floor below. You land in a heap of splintered wood. After a few moments, everything settles, and you manage to get up and brush yourself off, apparently uninjured.\b";return( cellar ); } ;The \b is at the end of the message to separate the message about the stairways collapse from the descriptive text that accompanies normal travel into a new room. Since the method just returns a room object, travel will proceed as normal after the message has been displayed.
The collapsing stairway of the previous example will need some additional special consideration at the other end, because the player clearly cant go back up the stairs once theyve collapsed. The simplest case is to disallow travel altogether, which is simple: just make an up method that displays an appropriate message, then returns nil to indicate that no travel is possible in this direction.
cellar: room sdesc = "Cellar" ldesc = "You're in the cellar. A huge pile of broken pieces of wood that was once a stairway fills half the room. " up = { "The stairway is in no condition to be climbed. "; return( nil ); } ;Since the method returns nil, no further travel processing takes place after the message is displayed.
Note that we should add an object for the broken stairway, just for completeness, so that the player can mention the stairs in a command. Well make it a fixeditem since it cant be moved or otherwise manipulated.
brokenStairs: fixeditem location = cellar noun = 'stairs' 'stairway' 'pile' 'wood' adjective = 'broken' sdesc = "pile of wood" ldesc = "It's a huge pile of wood that used to be a stairway. You won't have much luck trying to climb it. " ;
The stairway examples shown above could be enhanced to make it possible to enter the cellar by some other passage, which would mean that the stairway could be seen before it collapses. If this were done, it would be necessary for the cellars room description, as well as the up method, to be sensitive to whether the stairway had collapsed yet.
Making a room description that adapts to the state of the room is done by turning ldesc into a method. Just as special travel effects can be achieved by making a direction property into a method, special room descriptions can be programmed by making ldesc a method.
Since the stairway can be seen before its collapsed, well introduce a new object for the working stairs. Note that this object should be climbable, so well define verDoClimb and doClimb methods for it. Note that the method actor.travelTo(location) is the exact same method that is called when the player travels normally by typing a direction command (such as up). Also note that the location property of the brokenStairs object should initially not be set (which will make it nil), since the broken stairway is not part of the game initially in the new configuration.
workingStairs: fixeditem location = cellar noun = 'stairs' 'stairway' sdesc = "stairs" ldesc = "The stairway leads up. It looks extremely rickety. " verDoClimb( actor ) = {} doClimb( actor ) = { actor.travelTo( livingRoom ); } ;Now well change the cellar objects up method, as well as its ldesc property, to reflect the current status of the stairs. Well detect whether the stairs have collapsed yet by checking the location of the workingStairs object: if its in the cellar, well know the stairs havent collapsed yet, and if its not anywhere (that is, if its location is nil), well know that the stairs have collapsed. (This object switch will be implemented below, in the livingRoom objects down method.) Well use this detection technique to make both the ldesc and the up method sensitive to the status of the stairway.
cellar: room sdesc = "Cellar" ldesc = { "You're in the cellar. "; if ( workingStairs.location = nil ) "A huge pile of broken pieces of wood that was once a stairway fills half the room. "; else "A suspicious-looking stairway leads up. "; } up = { if ( workingStairs.location = nil ) { "The stairway is in no condition to be climbed. "; return( nil ); } else { "The stairs creak and moan and sway, but you somehow manage to climb them safely.\b"; return( livingRoom ); } } ;Well also have to make a small change to the down method in the livingRoom object, to replace the workingStairs object with the brokenStairs object. Heres the new down method, which will remove the workingStairs object from the game by moving it to nil, and put the brokenStairs object in its place.
livingRoom: room sdesc = "Living Room" ldesc = "You're in the living room. A dark stairway leads down. " down = { if ( workingStairs.location = nil ) { "You quickly notice that the stairs have collapsed, so there's no way down. "; return( nil ); } else { "You start down the stairs, which creak and moan and wobble uncertainly beneath you. You stop for a moment to steady yourself, then continue down, when the entire stairway suddenly breaks away from the wall and crashes to the floor below. You land in a heap of splintered wood. After a few moments, everything settles, and you manage to get up and brush yourself off, apparently uninjured.\b"; workingStairs.moveInto( nil ); /* replace workingStairs... */ brokenStairs.moveInto( cellar ); /* ... with brokenStairs */ return( cellar ); } } ;
One common type of obstacle is a door. Doors can be implemented using exactly the same mechanisms shown above for the stairway: in the directional properties for the rooms involving the door, check the status of the door object (generally the isopen property), and either return a room object if the door is open, or display a message and return nil if the door is closed. This type of mechanism is used in DITCH.T for its doors.
However, adv.t provides a set of classes that makes it easier to implement doors. The classes are based on a general class called obstacle (which you may wish to extend into your own specialized classes to implement different types of obstacles). The basic class is doorway, and an additional class called lockableDoorway makes it easy to implement a door that can be locked and unlocked with a particular key object.
By using the doorway and lockedDoorway classes, you can implement doors with no programming, because the basic travel routines will do the necessary work based on properties defined in your doorway objects.
The first thing to note about doors is that they have an unusual property: a door exists in two rooms, because it connects the two rooms. Unfortunately, TADS doesnt have any good way of putting a single object in multiple locations. To work around this problem, you should make two objects for each actual door in your game: one for each side. Fortunately, the doorway class makes it easy to keep the states of the two objects synchronized: for example, when you open one side of the door, the other side is automatically opened.
Start by implementing the two sides of the door. Make a doorway object for each, filling in the normal properties for any object (location, sdesc, noun). Put one doorway object in each room to be connected. Now add the special properties for a doorway. Set otherside to the other door object making up the pair of doors; this is how the doors internal routines know what other door to keep synchronized. Set doordest to the room object thats reached by travelling through the door.
Then, all you need to do for the rooms containing the doors is to set the direction properties to the doors themselves, rather than the rooms reached through the doors. Heres an example, showing the two room objects and the two door objects.
porch: room sdesc = "Porch" ldesc = "You're on a porch outside a huge run-down wooden house that was probably painted white in the distant past, but which is gray and faded now. A door (which is << porchDoor.isopen ? "open" : "closed" >>) leads west. " west = porchDoor /* use door for the direction */ ; frontHall: room sdesc = "Front Hall" ldesc = "You're in the spacious front hallway of the old house. The paint on the walls is all peeling, and a thick veil of spiderwebs hangs down from the ceiling. A door (which is <<hallDoor.isopen ? "open" : "closed">>) leads east. " east = hallDoor ; porchDoor: doorway location = porch noun = 'door' sdesc = "door" otherside = hallDoor doordest = frontHall ; hallDoor: doorway location = frontHall noun = 'door' sdesc = "door" otherside = porchDoor doordest = porch ;Note that theres no need to define ldesc for a doorway, unless you want to. The default ldesc defined in the doorway object displays a message saying simply Its open or Its closed or Its closed and locked, depending on the doors state. If you want to override ldesc, you can use the isopen and islocked properties to determine the state of the door to be displayed in your custom message.
Some more properties of the doorway class are worth mentioning. If the door is not locked, the door will be opened automatically when the player tries to travel through it (such as by going west from the porch in the example above). This makes the game more convenient, because its not necessary to type open the door when thats the obvious thing to do. However, in cases where you dont want a door to be automatically opened, you can simply add noAutoOpen = true to the doorway objects properties; this requires the player to explicitly open the door, even when its not locked.
If you want to require a key to lock and unlock the door, use the lockableDoorway class instead of doorway. Then, add the mykey property, setting it to the object which serves as the key (this object should be of class keyItem). If you dont set the mykey property, the door can be locked and unlocked without any key; that is, the player simply types lock door and unlock door to lock and unlock it.
Note that theres no requirement that the two sides of a door be identical. You can take advantage of this in certain situations. For example, if you want to have a door that needs a key from the outside, but doesnt need any key from the inside (like most doors youd find in a house), make both sides lockableDoorway objects, but only set the mykey property in the object that serves as the outer side of the door.
Vehicles are rooms that move. Basically, a vehicle is an extension to the idea of complex room interconnections; rather than having a room interconnection that merely displays a message, or sometimes disallows travel, a vehicle has a room interconnection that moves around. Usually, a vehicle will also have some other behavior, such as controls that operate the vehicle, or a viewscreen that shows the area outside the vehicle.
The first type of vehicle well address is the fully-enclosed variety, such as a subway train or an elevator. Well address vehicles that are also objects in their own right, such as rubber rafts, in a later section (Nested Room Vehicles below). A fully-enclosed vehicle really just amounts to a room that has a direction property that changes, depending on where the vehicle is supposed to be. The vehicle doesnt have a location property, because its just an ordinary room - its not an object in other rooms.
As an example, lets implement an elevator that travels between two floors. The elevator will have up and down buttons to activate it, doors, and a display indicating which level its on.
To make the elevator more realistic, well arrange for it to spend a couple of turns in transit as it moves between floors. To do this, well use a notifier function thats set when a button is used to start the elevator on its way. Well also use a flag to indicate when the elevator is in transit, so that pushing the buttons while the elevator is moving will have no effect.
The doors will be simpler than normal doors, since the user cant directly manipulate them. Theyll open and close automatically.
elevDoors: fixeditem location = elevRoom noun = 'door' 'doors' adjective = 'elevator' 'sliding' sdesc = "elevator doors" ldesc = "They're a standard pair of sliding elevator doors. They're currently <<self.isopen ? "open" : "closed">>. " isopen = true verDoOpen( actor ) = { "The doors are automatic; you can't open them yourself. "; } verDoClose( actor ) = { "The doors are automatic; you can't close them yourself. "; } ;The buttons are fairly simple: they use the basic buttonitem class. Well set a property indicating the destination of the elevator when that button is pushed - the up button will take the elevator to the upper floor, and the down button will take the elevator to the lower floor. The doPush method checks to see if the elevator is already moving, and then to see if the elevator is already at the destination of this button.
To make less work for ourselves, well define a class for the two elevator buttons, then make two objects based on the class.
class elevButton: buttonitem location = elevRoom doPush( actor ) = { /* ignore if already in motion or already at destination */ if (elevRoom.isActive or elevRoom.curfloor = self.destination) "\"Click.\" "; { else { "The doors close, and the elevator starts to move. "; elevRoom.isActive := true; elevDoors.isopen := nil; elevRoom.counter := 0; elevRoom.curfloor := self.destination; notify(elevRoom, &moveDaemon, 0); } } ; elevUpButton: elevButton sdesc = "up button" adjective = 'up' destination = floor2 /* where to go when button is pushed */ ; elevDownButton: elevButton sdesc = "down button" adjective = 'down' destination = floor1 ;Youll recall that notify is a built-in function that sets up a daemon, which is a routine that you want the system to call after each turn. The call to notify in the example above tells the TADS run-time system to call the method moveDaemon in the object elevRoom after every turn (the third argument is 0, which means that the method should be called after each turn; if it had been non-zero, it would have specified a number of turns to wait before calling the method once).
The elevator object needs to be sensitive to its current location with its exit and north parameter. It also needs to be sensitive to the state of the doors. The daemon (that is started with the notify call in the buttons) controls the travel of the elevator.
elevRoom: room sdesc = "Elevator" ldesc = "You're inside a small elevator. Sliding doors (which are currently <<elevDoors.isopen ? "open" : "closed">>) lead north. " out = (self.north) north = { if ( elevDoors.isopen ) return( self.curfloor ); else { "The doors are closed. "; return( nil ); } } curfloor = floor1 /* start off on lower floor */ moveDaemon = { self.counter++; switch( self.counter ) { case 1: case 2: case 3: "\bThe elevator continues to travel slowly. "; break; case 4: "\bThe elevator stops, you hear a bell make a \"ding\" sound, and the doors slide open. "; elevDoors.isopen := true; self.isActive := nil; unnotify( elevRoom, &moveDaemon ); break; } } ;The \b sequences before the messages in the moveDaemon method are there because the routine runs as a daemon. This means its hard to predict exactly what will have been displayed just prior to the methods execution, so its safest to put in a blank line to make sure that the daemons messages are set apart from the previous message. You should generally put in a blank line before the first message displayed by any daemon.
Note that youll have to do a small amount of extra work to finish this example. First, youll have to define the rooms floor1 and floor2. These are extremely simple if the only way to reach them is through the elevator, because you dont have to worry about whether the elevator is present or not. If you want to be more sophisticated, and make the floors reachable without taking the elevator (for example, with a stairway), youll have to do two things: first, youll have to put some doors on each floor, and make sure theyre closed while the elevator isnt on that floor; and second, youll probably want to make some way to call the elevator to the current floor.
Heres a general class for making the doors on each floor. Well make the doors isopen property simply check to see if the inner doors are open, and if so, whether the elevator is on the current floor (which is the doors location). Well do the same thing for the verDoOpen and verDoClose methods that we did with the elevDoors object.
class outerElevDoor: fixeditem sdesc = "elevator door" ldesc = "They're <<self.isopen ? "open" : "closed">>. " isopen = ( elevDoors.isopen and elevRoom.curfloor = self.location ) noun = 'door' 'doors' adjective = 'elevator' verDoOpen( actor ) = { "The doors are automatic; you can't open them yourself. "; } verDoClose( actor ) = { "The doors are automatic; you can't close them yourself. "; } ;Now you just need to add an object of this class for each floor; the only thing you need to set in each object is its location property. Then, on each floor, make the south property sensitive to the state of the doors, in the same way as the north property in the elevRoom object is sensitive to the state of the inner doors.
The other addition youll want to make is to add a call button on each floor. This is probably accomplished most easily by simply using the existing daemon that runs the elevator, and adding new buttons on the floors, outside the elevator, that activate the elevator. Heres a basic class that can be used to make these buttons.
class outerButton: buttonitem sdesc = "elevator call button" adjective = 'elevator' 'call' doPush( actor ) = { /* ignore if elevator is already here */ if ( elevRoom.curfloor = self.location and elevDoors.isopen ) "Your incredible powers of observation suddenly inform you that the elevator is already here. "; else { "\"Click.\""; if ( not elevRoom.isActive ) notify( elevRoom, &moveDaemon, 0 ); elevRoom.isActive := true; elevDoors.isopen := nil; elevRoom.counter := 0; elevRoom.curfloor := self.location; } } ;Youll notice if you try to run this example that theres a minor problem: the daemon that moves the elevator around displays messages as though the player were on the elevator. By using the same daemon to move the elevator around with the call buttons, well have to change the messages so that theyre sensitive to whether the player is on the elevator or not. For motion messages, the player should only get the messages if the player is on the elevator. Messages concerning the doors opening, on the other hand, should be seen if the player is either on the elevator or at the location at which the elevator is arriving. The new moveDaemon method (for the elevRoom object) is shown below (the rest of the elevRoom object stays the same).
moveDaemon = { self.counter++; switch( self.counter ) { case 1: case 2: case 3: if ( Me.location = self ) "\bThe elevator continues to travel slowly. "; break; case 4: if ( Me.location = self ) "\bThe elevator stops, you hear a bell make a \"ding\" sound, and the doors slide open. "; else if ( Me.location = self.curfloor ) "\bYou hear a bell make a \"ding\" sound, and the elevator doors slide open. "; elevDoors.isopen := true; self.isActive := nil; unnotify( elevRoom, &moveDaemon ); break; } }There are many more enhancements that you could make to the elevator control daemon to make the elevator more realistic. First, it would be fairly easy to add more floors, simply by adding more buttons. Second, it would be nice to provide some sort of indication of where the elevator is currently. In addition, real elevators use a somewhat different control algorithm. A real elevator usually has a current direction; it travels in that direction, stopping at each floor thats been selected with a call button at the floor or with the matching button in the elevator. When it gets to the top or bottom floor, all of the selected buttons inside the elevator are cleared (i.e., de-selected). The elevator then reverses direction if necessary, servicing called floors. This type of algorithm would be a little more work to implement, and is left as an exercise to the reader.
A nested room is a room inside another room. In most cases, nested rooms are also objects in their own right. The principal feature of a nested room is that it isnt enclosed. Typical examples of nested rooms are chairs and beds: these are objects within a room, but theyre also rooms in the sense that the player can be located in them.
Implementing a nested room in TADS is fairly simple, because adv.t defines a class called nestedroom that does most of the work. Furthermore, the classes chairitem and beditem let you implement the most common nested room objects with very little work.
One feature of nested rooms that is worth mentioning is that the player can not necessarily reach all of the objects in the enclosing room from a nested room. By default, none of the objects in the enclosing room are reachable from a nested room. However, you can easily make any objects reachable from the nested room by setting its reachable property to a list containing all the reachable objects in the enclosing room. If you want to set it to everything in the enclosing room, simply set it as follows:
reachable = ( self.location.contents )This says that everything contained in the enclosing room is reachable.
Sometimes, youll want to make a nested room thats also a vehicle. For example, you might want to implement a rubber raft that the player can carry around, but also inflate and use as a vehicle. Fortunately, theres an adv.t class called vehicle that serves this function. The vehicle class is a carryable object that can be boarded as a vehicle.
Lets implement an inflatable rubber raft. The first thing we need to do is build a standard vehicle object, but well customize it so that it can only be boarded when its inflated. Well furthermore make it so that it can only be inflated when its not being carried, and well make it impossible to take when its inflated.
raft: vehicle location = startroom sdesc = "inflatable rubber raft" noun = 'raft' adjective = 'inflatable' 'rubber' isinflated = nil ldesc = "It's an inflatable rubber raft. Currently, it's <<self.isinflated ? "inflated" : "not inflated">>. " verDoTake( actor ) = { if ( self.isinflated ) "You'll have to deflate it first. "; else pass doTake; } verDoInflateWith( actor, iobj ) = { if ( self.isinflated ) "It's already inflated! "; } doInflateWith( actor, iobj ) = { if ( self.isIn( actor ) ) "You'll have to drop it first. "; else { "With some work, you manage to inflate the raft. "; self.isinflated := true; } } verDoDeflate( actor ) = { if ( not self.isinflated ) "It's as deflated as it can get. "; } doDeflate( actor ) = { "You let the air out, and the raft collapses to a compact pile of rubber. "; self.isinflated := nil; } doBoard( actor ) = { if ( self.isinflated ) pass doBoard; else "You'll have to inflate it first. "; } ;Note that well have to define a couple of verbs, and it would be convenient to add an object for the pump as well.
inflateVerb: deepverb sdesc = "inflate" verb = 'inflate' 'blow up' ioAction( withPrep ) = 'InflateWith' prepDefault = withPrep ; deflateVerb: deepverb sdesc = "deflate" verb = 'deflate' doAction = 'Deflate' ; pump: item sdesc = "pump" location = startroom noun = 'pump' verIoInflateWith( actor ) = {} ioInflateWith( actor, dobj ) = { dobj.doInflateWith( actor, self ); } ;Once all of this is done, making the raft move around is very similar to making a fully-enclosed vehicle move around. The only real difference is that the raft will always have a location. As with most vehicles of this type, youll want to make it impossible for the player to get out of the raft while its in motion. To do this, youll need to override the doUnboard and out methods. For this example, lets assume that the river (where the raft will travel) will be composed of a series of rooms, each of which is of class riverRoom. So, whenever the raft is in a riverRoom, well prevent the player from getting out. To do this, well add the methods below to the raft object. Note that these methods will inherit the corresponding vehicle methods when the raft is not floating down the river.
doUnboard( actor ) = { if ( isclass( self.location, riverRoom ) ) "Please keep your hands and arms inside the raft at all times while the raft is in motion. "; else pass doUnboard; } out = { if ( isclass( self.location, riverRoom ) ) "You can't get out until you've landed the raft. "; return( nil ); else pass out; }The only thing left is to move the raft around. For this example, well set things up as follows: each (non-river) room which borders on a river will have a property, toRiver, set to point to the bordering river room. Each river room which has a landing will have a property, toLand, set to point to the bordering non-river room. Each river room will also have a property, downRiver, set to the next river room downstream from the current room. Well introduce two new verbs: launch and land. When the player is in the raft, launch raft will set the raft in motion down the river (if its on land), and land raft will land (if theres a landing nearby). The new verbs are below.
launchVerb: deepverb sdesc = "launch" verb = 'launch' doAction = 'Launch' ; landVerb: deepverb sdesc = "land" verb = 'land' doAction = 'Land' ;Now well add some more methods to the raft object: the verb handling methods for the new verbs, and a daemon that causes the raft to drift downstream while its in the river.
verDoLaunch( actor ) = {} doLaunch( actor ) = { if (isclass( self.location, riverRoom ) ) "You're already afloat, if you didn't notice. "; else if ( self.location.toRiver = nil ) "There doesn't appear to be a suitable waterway here. "; else if ( Me.location <> self ) "You'll have to get in the raft first. "; else { "The raft drifts gently out into the river. "; notify( self, &moveDaemon, 0 ); self.counter := 0; self.moveInto( self.location.toRiver ); } } verDoLand( actor ) = {} doLand( actor ) = { if ( not isclass( self.location, riverRoom ) ) "You're already fully landed. "; else if ( self.location.toLand = nil ) "There's no suitable landing here. "; else { "You steer the raft up onto the shore. "; unnotify( self, &moveDaemon ); self.moveInto( self.location.toLand ); } } moveDaemon = { "\bThe raft continues to float downstream. "; self.counter++; if ( self.counter > 1 ) { self.counter := 0; if ( self.location.downRiver = nil ) { "The raft comes to the end of the river, and lands. "; self.moveInfo( self.location.toLand ); unnotify( self, &moveDaemon ); } else { self.moveInto( self.location.downRiver ); self.location.riverDesc; } } }Note that well expect each river room to have a property, riverDesc, which displays a message when the raft drifts into that room. The moveDaemon method will keep the raft in each river room for two turns, then move the raft to the next river room, calling riverDesc to note the entry. When the raft comes to the end of the river, the method will automatically land the raft; this means that the last river room must have a non-nil toLand property. You could alternatively put in a waterfall or other special effect when reaching the end of the river.
To build a river, all you have to do is define a series of rooms of class riverRoom, and point the downRiver property in each to the next room downriver. Landings are built by setting the toRiver and toLand properties of the landing room and corresponding river room, respectively, to point to each other.
Hiding objects is fairly simple, thanks to a set of classes defined in adv.t that make certain kinds of hiding automatic.
The adv.t classes make it possible to hide objects under or behind other objects, and to set up an object so that its contents are found only when the object is searched. The basic hider classes are underHider, behindHider, and searchHider. These classes are for the objects doing the hiding; the hidden objects are hidden inside these objects. For the objects being hidden, instead of setting their location properties, set the properties underLoc, behindLoc, or searchLoc to point to the respective classes. All hidden objects must be of class hiddenItem.
For example, to hide a key under a bed, make the bed an underHider object, and set the underLoc property of the key to point to the bed.
bed: beditem, underHider noun = 'bed' location = startroom sdesc = "bed" ; key: item, hiddenItem noun = 'key' sdesc = "key" underLoc = bed ;The behindHider and searchHider objects work the same way, but you should use the behindLoc and searchLoc properties, respectively, instead of underLoc.
Note that the initSearch function, defined in adv.t, must be called during initialization (usually, by your preinit function) to set up the hidden objects. This routine sets up special contents lists for the various hider objects. This routine only considers hidden objects when theyre of class hiddenItem, which is why you must use this class for all of your hidden objects.
Note that the ease with which you can hide objects shouldnt be taken as a license to hide objects with wild abandon. From a game design viewpoint, hidden objects are almost always poor puzzles. If you do hide an object, make sure that you provide some sort of clue that something is hidden there. Otherwise, the player must tediously look behind and under every object in the game. As with most puzzles, its easy to make a really hard game - its harder to make a fun game.
Each item that a player can manipulate in a TADS game generally corresponds to an object defined in the game program. To allow the player to refer to each object individually, your game program normally must define a unique set of vocabulary words for each object. For example, if you have two books in your game, the books must have at least one distinct adjective each, so that the player can specify in a command which book he wants to manipulate. (The one exception to this general rule is the case of indistinguishable objects, which will be discussed later.)
The style of game construction which requires that each object have a unique set of vocabulary imposes certain limitations when you design your game. Sometimes youll find that you want to have a large collection of similar objects that are not distinguished from one another. Fortunately, there are a number of ways to implement object collections; the particular mechanism you choose will vary, and depends on the type of situation you want to create. This section provides examples of several different effects you can create with collections of objects.
Suppose you are creating a bookstore. You will want to stock the store with a large number of books - but you wont want to create a huge number of individual book objects, not only because it would take too much time and effort, but also because most of the books wont be relevant to the game. There will usually be one or two important books, and many unimportant ones.
In this type of situation, one way to work within the limitations of TADS is to create a single object representing the collection of unimportant books. Since most of the books are not important, they can be grouped together into this single object, which is set up so that it tells the player about its lack of importance whenever the player tries to manipulate it. Furthermore, it can serve as a pointer to any important similar objects.
As an example, well implement a collection of books in a book store. A single object will represent the collection of unimportant books. When the player looks at the collection of books, though, it will point out the presence of a couple of important books.
manybooks: fixeditem sdesc = "books" adesc = "a number of books" noun = 'book' 'books' location = bookstore ldesc = { "The bookstore has a huge stock of books, ranging from scientific titles to the latest collection of \"Wytcome and Wyse\" cartoons. "; if ( not self.isseen ) { "One title in particular catches your attention: \"Liquid Nitrogen: Our Frigid Friend\". "; ln2book.moveInto( bookstore ); self.isseen := true; } } verDoRead( actor ) = { "Although you'd really like to sit down and start reading, you know from painful experience that the neo-fascist staff have been cracking down on browsing lately. "; } verDoTake( actor ) = { "Unfortunately, you know you could never afford all of these books. "; } ;Note that we set the adesc property to a non-default value. This is because the default adesc, which would display a followed by the objects sdesc, would result in the strange message a books in this case.
To help the player avoid wasting a lot of time trying to manipulate the books, we put in some special messages that let the player know that the books cant be manipulated. When you create a decoration object, its always a good idea to think of all of the things that the player might want to do with it, and provide messages that make it clear that the object isnt important to the game. In this case, weve made it clear that the collection of books cant be read or taken.
The most important definition, though, is the ldesc property. This property not only provides a general description of the collection of books, but also points out the single important book to the player when the collection of books is examined for the first time. (The isseen property is used to determine if the ldesc has been displayed before. The first time the ldesc property is executed, isseen is set to true.) In addition to displaying a message for the player about the special book, it moves the special book into the bookstore.
One way you may wish to extend this basic idea is to provide a collection of books from which the player can take a single book, seemingly at random. The first time a player takes a book from the collection, he gets one book; the next time, he gets another book; and so forth. This is easy to implement by overriding the doTake method of the collection of books: rather than moving the collection itself into the players inventory, the method will choose a book from a list of individual book objects, and move that book into the players inventory.
manybooks2: fixeditem sdesc = "books" adesc = "a number of books" noun = 'book' 'books' location = bookstore ldesc = { "The bookstore has a huge stock of books, ranging from scientific titles to the latest collection of \"Wytcome and Wyse\" cartoons. You may want to just try picking up a book. "; } verDoTake( actor ) = { if ( length( self.booklist ) = 0 ) "You don't see anything else you're interested in. "; } doTake( actor ) = { local selection; selection := self.booklist[ 1 ]; self.booklist := cdr( self.booklist ); selection.moveInto( actor ); "You look through the books, and choose << selection.thedesc >>. "; } booklist = [ ln2book chembook cartoonbook ] ;The property booklist contains a list of books that we have pre-defined. Each time the player takes a book, the doTake method will take the first element out of the booklist property, and act as though the player had picked up that object. This way, the player can just type take book, and the game will seem to select a book for the player. The verDoTake property makes sure theres something left in the list, and displays an appropriate message if not. Note that weve written the ldesc message so that its clear to the player that he should attempt to take a book.
Another variation on this theme involves a collection of indistinguishable objects. For example, suppose we want to implement a matchbook which contains a number of matches. The player should be able to take a match out of the matchbook in order to light it.
Since wed like to try and implement such a feature without using the indistinguishable objects functionality, well have to impose some limits on what the player can do. In particular, well only allow the player to have a single match at any given time - all of the other matches must be in the matchbook.
In terms of implementation, this means that well only need a single object to represent a match. This object will have two possible states: existent or non-existent. Well use the location property to indicate the state; if the location is nil, the object is non-existent, otherwise its in the game. When the match is non-existent, the player can take a match out of the matchbook - which brings the match into existence by moving it into the players inventory. As long as the match is in the game, the player cannot take another match out of the matchbook. Once the match is burned away, though, it will be removed from the game, and the player can take another match.
The matchbook will have a count of available matches. When the player attempts to take a match, well first check to see if there are any matches left; if so, well check to make sure that the match object doesnt already exist in the game.
matchbook: item location = bookstore noun = 'matchbook' sdesc = "matchbook" matchcount = 4 /* number of matches left in matchbook */ ldesc = { if ( self.matchcount > 0 ) "The matchbook contains << self.matchcount >> match<< self.matchcount = 1 ? "." : "es. " >>"; else "The matchbook is empty. "; } ; match: item noun = 'match' adjective = 'single' sdesc = "single match" ldesc = { if ( self.isBurning ) "It's currently lit. "; else "It's an ordinary safety match. "; } verDoLight( actor ) = { if (self.isBurning) "It's already lit!"; } burnFuse = { "\bThe match burns down. You drop it, and it disappears in a cloud of ash. "; self.isBurning := nil; self.moveInto( nil ); /* match is now non-existent */ } doLight( actor ) = { "The match starts burning. "; self.isBurning := true; notify( self, &burnFuse, 2 ); } ; fakematch: fixeditem noun = 'match' adjective = 'bound' sdesc = "bound match" location = matchbook verDoTake( actor ) = { if ( matchbook.matchcount = 0 ) "The matchbook is empty. "; else if ( match.location <> nil ) "You'll have to use the one you already took first. "; } verDoLight( actor ) = { "You'll have to remove it from the matchbook first. "; } doTake( actor ) = { "You tear a match out of the matchbook. "; match.moveInto( actor ); /* move a match into inventory */ matchbook.matchcount - ; /* one less match */ } ;The fakematch object lets the player refer to the match in a command, even when the match object doesnt exist in the game (i.e., its location is nil). The fakematch has verDoTake and doTake methods that let the player take a match out of the matchbook.
Note one flaw in this implementation: the player might not have the match, but still cant take another until the first is destroyed. This could be confusing. For example, the player may take a match, then later drop it because his hands are full, then move to a different room. As far as the player is concerned, no match is present - he left the match in another room, and may not even remember where. However, he wont be able to take another match, because the first match is still in the game. Theres no easy way to fix this. You could arbitrarily move the match into the players inventory, even though it is in some other room; this might be less confusing for some players, but it may be more confusing for others who are aware that there is a match in another room - this other match would be gone after the single match object is moved into the players inventory.
Another approach, and probably a more satisfying one, would be to use the indistinguishable objects capabilities introduced in TADS 2.2. Using this feature you can have multiple objects of the same class. You can even create objects dynamically and destroy them when theyre no longer needed. These features are discussed in Chapter 8.
Money does not fit very naturally into a TADS game for a number of reasons. If you do want to use money in your game, it can generally be implemented using techniques similar to those we used for the matches in the matchbook. Or you could implement several denominations of coinage or paper bills, and implement them as indistinguishable objects, but even that approach becomes cumbersome with large numbers of monetary units. If you go for the matchbook approach the only changes youll have to make to support money will be to add some new verbs.
The necessary new verbs will depend on your game. For example, you may want pay and buy verbs for purchasing items. The main reason youll want to use these new verbs is that theres no easy way for the player to refer to an amount of money unless you go for a very basic currency, such as the gold piece model used in a lot of fantasy games. If you want to avoid that model youll have to arrange a protocol for purchasing objects. For example, make a location that serves as a store, and make a shopkeeper actor. To purchase an object, the player tells the shopkeeper to give him an object. The shopkeeper responds by telling the player the price of the object. The player responds with pay shopkeeper; this deducts the price of the object, and gives the player the object.
If your game contains a non-obvious protocol like this, it is very important that you document it for the player. You can provide an explanation in the instructions for your game, but since most people will start playing your game immediately without reading any accompanying instructions, it would be far better to put the explanation into the game itself. There are a number of ways you could do this; you could, for example, simply display an explanation of the necessary commands when the player first enters the shop. Alternatively, you could provide instructions as error messages; when the player attempts to take an object in the shop, you could display a message: Please direct your inquiries to the shopkeeper; for example, type shopkeeper, give me the axe.
An even better approach would be to avoid situations like this altogether. If you cant make it fit easily and naturally into your game without resorting to special sequences of commands, you should probably find another way to accomplish the same thing. Remember, absolute realism is not important in a text adventure game - the only important thing is to make the game fun to play.
One of the most effective techniques for giving a game depth and making it involving is to include non-player characters. Early adventure games often seemed empty and static; the player just wandered alone through a vast maze of caves. More modern games use non-player characters (which well refer to as actors from now on), who move through the game, interacting with the player and doing things on their own. When done well, actors add a whole new dimension to a game, and make the setting seem alive and real.
Actors are one of the most complicated parts of a TADS game, because they operate on more levels than other objects. First, theres player interaction: an actor normally can interact with the player, by talking (the player can tell an actor to do things, and can ask an actor questions), and by direct action (attacking, for example, or exchanging objects). There are a great many ways a player and an actor can interact, and TADS lets you implement as much or as little interactive capability into an actor as you wish. Second, actors often do things on their own. Sometimes, this means that the actor carries out a pre-scripted set of actions. Other times, the actor just reacts to the player; for example, an actor might spend most of the game following the player around, making amusing comments from time to time. Most actors, though, are combinations of these, reacting to the player most of the time, and carrying out pre-scripted actions in response to certain events. Third, some actors have a memory, and have knowledge of various parts of the game. Certain things an actor does may depend on what a player has done previously.
Fortunately, all of these basic elements of an actor are implemented pretty easily. You can write the code for a simple actor without too much difficulty. Even better, once you have a simple actor working, fleshing out the actor is just a matter of adding to the basic framework of the actor.
To implement an actor, you need to write two main pieces of code that arent associated with most other types of objects. First is the actorAction method. This method handles most of the actors interaction with the player; it essentially defines the limits of the actors ability to interact. Second is the actors daemon. Youll recall from previous chapters that a daemon is a function or method thats called after every turn (that is, the run-time system calls a daemon each time it finishes processing a player command). The actor daemon is what allows the actor to go about its business; since it is called after each turn, the actor can perform an action of its own on each turn.
If youve played Ditch Day Drifter, youll recall a couple of actors in that game of differing degrees of interaction and activity. One fairly limited actor is the guard in front of the steam tunnels: the guard does very little apart from blocking your way into the tunnel. The guards interaction consists mostly of accepting objects; you can attempt to bribe the guard with money, for example, or give him something to drink. The guards daemon also does very little; it just displays a randomly-chosen message (from a set of several pre-determined messages) on each turn. A more versatile actor is Lloyd, the insurance robot. Lloyd can accept objects (you can buy the insurance policy), and you can also talk to him to a limited extent (you can ask about the policy). Like the guard, Lloyd displays a randomly-chosen message on most turns. In addition, Lloyd follows the player from room to room. Lloyd even reacts to certain events and places, and has a memory of sorts: once Lloyd has paid out an insurance claim, he will remember not to pay out the claim again.
Lets look at an example of implementing an actor. Well start with a simple puzzle actor, similar to the guard in Ditch. This is a very common element of adventure games: a person that is blocking the way to some goal. In this case, well implement a receptionist whos blocking the way to an office you want to enter. As long as the receptionist is awake, you wont be allowed to enter the office.
receptionist: Actor noun = 'receptionist' sdesc = "receptionist" isawake = true ldesc = { "The receptionist is a very, shall we say, sturdy-looking woman, with a tall beehive hair-do and thick glasses. She reminds you of your third-grade teacher, who was overly concerned with discipline. "; if ( self.isawake ) "She eyes you impatiently as she goes through some papers. "; else "She's slumped over the desk, fast asleep. "; } location = lobby actorDesc = { if ( self.isawake ) "A receptionist is sitting at the desk next to the door, watching you suspiciously. "; else "A receptionist is slumped over the desk next to the door. "; } actorDaemon = { if ( self.location <> Me.location or not self.isawake ) return; switch( rand( 5 ) ) { case 1: "The receptionist sharpens some pencils. "; break; case 2: "The receptionist goes through some personal mail, holding each letter up to the light and attempting to read the contents. "; break; case 3: "The receptionist looks through the personnel files. "; break; case 4: "The receptionist answers the phone and immediately puts the caller on hold, cackling to herself fiendishly. "; break; case 5: "The receptionist shuffles some papers. "; break; } } ; lobby: room enterRoom( actor ) = { if ( not self.isseen ) notify( receptionist, &actorDaemon, 0 ); pass enterRoom; } sdesc = "Lobby" ldesc = "You're in a large lobby, decorated with expensive-looking abstract pastel oil paintings and lots of gleaming chrome. A large receptionist's desk sits next to a door leading into an office to the east. The exit is to the west. " west = outsideBuilding out = outsideBuilding east = { if ( receptionist.isawake ) { "You start to stroll past the receptionist nonchalantly, trying to ignore her as though you actually belong here, but she's wise to that trick: she leaps up, and with surprising force throws you back from the doorway. Satisfied that you're not going through, she sits back down and returns to her work. "; return( nil ); } else return( office ); } ;Notice that the implementation of this puzzle looks very much like that of a door, or any other obstacle, except that the obstacle itself is somewhat more complicated. The actual work of preventing the player from moving past the receptionist, though, is exactly the same as for any other obstacle: in the direction method, we check to see if the receptionist is still awake, and if so, we refuse to let the player past by returning nil (after displaying an appropriate message, of course).
The main features that are special for an actor are the actorDesc and actorDaemon methods. The actorDesc is a special property that you should define for any actor; this method is called by the general room code that displays the long description of the room. After the description of the room and its contents, the room long description routine will call the actorDesc method of each actor in the room (except the player actor, Me). The actorDesc method should simply display an appopriate message to the effect that the actor is present; its similar to the ldesc method for the actor, but it usually contains less detail, since its more to inform the player that the actor is present than to provide a detailed desription of the actor. It should mention that the actor is present and what the actor is doing.
The actorDaemon is nothing special - you can call this method whatever you want. Most actors will have something like the actorDaemon, though. This is the daemon that makes the actor seem alive by doing something after every turn. For our simple receptionist actor, which doesnt really do anything of its own accord, this method simply displays a random message each turn. Note that the method returns immediately if the player (the Me object) is not present in the same room as the receptionist; it clearly wouldnt make any sense to display a message about what the receptionist is doing when the receptionist is not present.
Note that actorDaemon is not started automatically for an actor. Instead, you must explicitly activate it. In this case, weve chosen to activate the actorDaemon routine by calling notify() the first time the player enters the lobby. The enterRoom method of the lobby object checks to see if the room has been entered before (by checking to see if the isseen property of the current room has been set - this is always automatically set by the room code after the room has been seen for the first time); if the room has never been entered, we call notify() to start actorDaemon running. Note that enterRoom finishes by using pass to run the enterRoom method that would normally be inherited from the room class.
There are several main types of wandering actors. The first type follows the player around, like Lloyd the insurance robot in Ditch Day Drifter. This first type is fairly simple, because all you have to do is set up a daemon that checks to see if the actor and the player are in the same room, and if not, move the actor to the players location.
The big change between a stationary actor, such as the receptionist in the earlier example, and a wandering actor is in the actors daemon. Instead of just displaying a random message, as did the receptionist, the actor daemon will actually move the actor around.
Heres a basic actorDaemon method that will make the actor follow the player.
actorDaemon = { if ( self.location = Me.location ) { switch( rand( 5 ) ) { case 1: "Lloyd hums one of his favorite insurance songs. "; break; /* etc. with other random messages... */ } } else { self.moveInto( Me.location ); "Lloyd rolls into the room, and checks to make sure the area is safe. "; } }Note that this routine must be set up to run every turn with the notify() function, as explained in the receptionist example.
This new version of the method still displays a random message if the actor is in the same room with the player, but now it has some new code that is executed when the player is somewhere else. The new code moves the actor to the players location, and displays a message informing the player that the actor has entered the room.
In a real game, youll probably further extend this type of daemon to make the actor do special things in certain locations. For example, in Ditch Day Drifter, theres some extra code that displays special messages when Lloyd enters certain improbable locations, such as squeezing through the north-south crawl or climbing the rope.
The second type of wandering actor moves around on a fixed path, or track. This type of actor is not much harder to implement than an actor which follows the player; rather than using the players location in the actor motion daemon, you make a list of locations that the actor visits in sequence, and the actor motion daemon takes locations out of this list.
One new factor youll have to keep in mind in implementing this type of motion daemon is that the actor could be entering the actors location, leaving the actors location, or neither. The motion message will have to check for each of these situations.
For an example, well show how to implement a robot similar to the vacuuming robot in Deep Space Drifter that moves around between several rooms in a fixed pattern.
vacRobot: Actor sdesc = "domestic robot" noun = 'robot' adjective = 'domestic' location = stationMain tracklist = [ 'southeast' bedroomEast 'west' bathroom 'west' 2em bedroomWest 'northeast' stationMain 'north' 2em stationKitchen 'south' stationMain ] trackpos = 1 moveCounter = 0 actorDaemon = { if ( not self.isActive ) return; /* do nothing if turned off */ self.moveCounter++; if ( self.moveCounter = 3 ) /* move after 3 turns in one location */ { self.moveCounter := 0; if ( self.location = Me.location ) "The robot moves off to the <<self.tracklist[self.trackpos]>>."; self.moveInto( self.tracklist[ self.trackpos + 1 ] ); if ( self.location = Me.location ) "A domestic robot rolls into the room and starts noisily moving around the room vacuuming and dusting. "; /* move to next position on track; loop back to start if necessary */ self.trackpos += 2; if (self.trackpos > length(self.tracklist)) self.trackpos := 1; } else { /* we're not moving this turn, so display activity message */ if ( self.location = Me.location ) "The domestic robot continues darting around the room vacuuming. "; } } ;The list of rooms that the robot visits is specified in the tracklist property. This is a strange-looking list, because it contains both objects and (single-quoted) strings. The way this works is that the list elements are always inspected in pairs: the first item in a pair is a string that will be displayed to indicate the direction that the robot will leave the current room; the second item in the pair is the room to be visited next after the current room. So, the first pair contains the string southeast and the object bedroomEast: the robot will leave to the southeast, and move to the east bedroom.
The actorDaemon still does all the work, but its a little more complicated than in the previous examples. First, it checks to see if the robot is active; if not, it doesnt go any further. Next, it increments a counter that tracks how long the robot has been in the current room; when it gets to three turns, its time to move on, otherwise the robot stays where it is.
If the robot is moving, the daemon first resets the move counter to zero. Next, it checks to see if the player is in the current location; if so, the daemon displays a message telling the player that the robot is leaving, and indicates which direction (the direction is determined by looking at the current element of the tracklist list, as described above). Next, the robot is actually moved to the new location (again, determined by looking in the tracklist list). Next, if the player is in the new location, the daemon displays a message saying that the robot has entered the room. Finally, we increment the track position counter property trackpos (by 2, since the list entries are in pairs). If the position counter has moved beyond the end of the list, we reset it to 1.
If the robot is not moving during this turn, the daemon will check to see if the actor is in the same location as the player, and display a message if so. Note that the single fixed message in the example could easily be replaced by a randomly-chosen message, as we did with the earlier examples.
The third type of wandering actor moves through the game at random. This type of actor wandering is somewhat more complex than the earlier types.
The main trick in implementing a randomly-wandering actor is that we must avoid evaluating direction properties when they dont contain a fixed object. This is because we could accidentally display spurious messages, and possibly trigger side effects (such as killing the player or making other changes to the game state) if we were to evaluate a direction property that was actually a method. For example, think back to the earlier example with the stairway that collapses when used - you probably wouldnt want to display the collapse message and destroy the stairs when one of your wandering actors encountered the room.
The best way to avoid such unintended side effects is to avoid using any direction property that isnt a simple object. TADS provides a special built-in function called proptype that lets you determine the type of a property without actually evaluating it. The proptype function will tell you whether a property is a simple object or a method.
Other than the mechanism for choosing a new room, randomly-wandering actors are essentially the same as actors on a track. You generate messages in the same manner, checking to see if the actor is leaving or entering a room occupied by the player.
actorDaemon = { local dir, dirnum; local tries; local newloc; for ( tries := 1 ; tries < 50 ; tries++ ) { dirnum := rand( 6 ); dir := [ &north &south &east &west &up &down ][ dirnum ]; if (proptype(self.location, dir) = 2 /* object */) { newloc := self.location.( dir ); if ( not isclass( newloc, room ) ) continue; if (self.location = Me.location) "The robot leaves to the << [ 'north' 'south' 'east' 'west' 'up' 'down' ][ dirnum ]>>."; self.moveInto( newloc ); if ( self.location = Me.location ) "A domestic robot enters the room and starts vacuuming noisily. "; } } }The line that sets the dir local variable probably needs a little explanation, because its a tricky bit of TADS coding. The first thing is a list of property addresses - these arent properties of any object, but rather just references to properties that you can later use to get an actual property of an object. We take this list, which consists of six elements, then choose a single element at random by indexing into the list with a randomly-chosen number from 1 to 6. This leaves the dir local variable with a pointer to a property that we can use later. Note that you could include northeast, southeast, northwest, and southwest in the list if you want; we left them out to make the example a little bit easier to read.
Next, we use the proptype() built-in function to determine if the selected property (contained in dir) of the actors current location contains an object. If it does not, we continue looping.
If the selected property of the current location does in fact contain an object, we first get that location into the local variable newloc. Then, we make one more check on the new location: we ensure that newloc actually contains an object of class room. This is because it is possible for a room direction connection property to contain an object of class obstacle, such as a door (see the section on implementing doors earlier in this chapter). If its a door or other obstacle, we want to treat it the same as though the direction property didnt contain an object at all - so well just continue with the loop, ignoring this choice of a direction.
Once we have an object of class room in newloc, we proceed to move the actor the same way we moved the actor on a track. Note that we use the same indexing trick to display the direction that the actor is exiting.
In case youre wondering why the example uses a tries counter and stops after 50 times through the loop: this is simply a bit of bullet-proofing to keep the daemon from going into an infinite loop. Its quite possible, especially while youre developing your game, that the robot may encounter some rooms from which there is no escape. Remember that the robot can only use exits which are explicitly set to objects; if you create a room with nothing but exits that are methods, the robot will be unable to find a suitable exit. If we didnt include the loop counter and give up after a while, the loop would run forever in such rooms, leaving the game stuck.
In a real game, you may want to add some special-case code that allows a randomly-wandering actor to use certain exits that would normally be off-limits because theyre methods. One way of accomplishing this is to add a special check to the daemon to test if the actor is in such a special location, and if so, to make it go to a particular destination. Another way is to put additional properties in some rooms (such as robotEast) that specify directions accessible only to the robot; in the daemon, youd check for the presence of these special robot directions first, falling back on the normal direction list only if the special directions are not present.
One modification you may wish to make to this type of totally random wandering is to restrict the actor to a particular set of rooms. There are a number of ways to accomplish this. The simplest is to create a class of rooms that the actor is allowed to occupy, and always check upon choosing a new room that the room is a member of the class.
If you want to change the allowed rooms dynamically as the game runs, use a condition rather than a class. Instead of checking to see if the room is a member of a class, check to see if the condition is true. For example, if you want to create an actor that only enters dark rooms, you would check upon choosing a room to make sure the room is not lit.
More Complex Actor Scripts
For some games, you may wish your actors to exhibit even more complex autonomous behavior than weve shown so far. One type of common extension is to make the actor perform some action when in a certain room or when a certain set of conditions is met. For example, you may wish to implement a thief that steals any treasure left lying around in a maze room. To implement this, wed first need to create a class of rooms for mazes; lets call this new class mazeroom:
mazeroom: room ;Similarly, well create a class for any object that counts as a treasure:
treasure: item ;Note that the only purpose of these two classes is to serve as a flag, so we can determine if an object is considered a treasure or if a room is part of a maze. These classes dont actually implement any new behavior of their own.
Next, wed add a check in the thiefs daemon that looks for treasure objects when the thief is in a room in a maze. Well add this code to the end of the same daemon we used for the randomly wandering actor above in the previous section; we wont reproduce that entire routine here. This code is added after the for loop that comprised the bulk of that daemon.
if ( isclass( self.location, mazeroom ) ) { /* check contents of current room */ local cont := self.location.contents; local len := length( cont ); local found := nil; local i; for ( i := 1 ; i <= len ; ++i ) { if ( isclass( cont[ i ], treasure ) ) { found := true; cont[ i ].moveInto( self ); } } if ( found and Me.location = self.location ) "You notice the thief has helped himself to some items. "; }This is a fairly complicated example of making your actors do something special when certain conditions are met. A simpler example is Lloyds attempt to sell insurance to the sleeping guard in Ditch Day Drifter, which is simply a message thats displayed by Lloyds movement daemon when Lloyd enters the room with the guard.
You may want some actors to have very complicated scripts that do more than just move the actors around. You may want to implement an actor which goes about his business, doing things in some of the locations he visits. The best way to do this is probably by making your actor a state machine. One easy way to implement a state machine is to use a number to represent the current state; on each turn, you increment the state number, and use a switch statement to take the appropriate action depending on the new state.
For example, suppose you want to implement an actor that enters a building and walks to the study. If someone else is in the study, hell just wait impatiently for the other person to leave; once hes alone in the study, hell produce a key and open a safe, conveniently dropping the key near the safe. Hell then take something out of the safe and leave the building.
Well represent the set of states with numbers. State 1 will be outside the building; when in state 1, well enter the building. State 2 will be inside the front hall, and well move to the study. In state 3 well check to see if were alone; if not, well just stay in state 3 and act impatient. Once were alone, well produce the key and open the safe. In state 4, well take the object out of the safe and exit the room, moving back to the front hall. In state 5, well leave the building.
The following methods can be used to accomplish this.
actorMessage( msg ) = { if ( Me.location = self.location ) { "\b"; say( msg ); } actorMove( newloc, todir, fromdir ) = { if ( Me.location = self.location ) "<<self.thedesc>> leaves to the <<self.todir>>. "; self.moveInto( newloc ); if ( Me.location = self.location ) "<<self.thedesc>> enters the room from the <<self.fromdir>>. "; } actorState = 1 actorDaemon = { switch( self.actorState++ ) { case 1: self.actorMove( frontHall, 'north', 'south' ); break; case 2: self.actorMove( study, 'east', 'west' ); break; case 3: if ( self.location = Me.location and not Me.ishidden ) { "Jack crosses his arms and looks at his watch. "; self.actorState := 3; /* remain in state 3 */ } else { actorMessage( 'Jack looks around, and is satisfied that he is alone. He searches his pockets, and produces a key, which he inserts into the safe and opens it. He nervously removes something from the safe; he doesn\'t seem to notice when the key drops to the ground.' ); safeKey.moveInto( self.location ); } break; case 4: self.actorMove( frontHall, 'west', 'east' ); break; case 5: self.actorMove( outsideBuilding, 'south', 'north' ); break; } }Note that the actorMove and actorMessage methods that we implemented provide a convenient mechanism for displaying messages conditionally if the player is present.
Naturally, youd want to extend the end of this script to give Jack something to do (or somewhere to disappear to) at the end of his appearance. Using this basic technique, you can implement essentially arbitrarily complicated actors. Note that its easy to jump to a different state rather than progress to the next state - just assign the state variable (actorState in this case) to the new state you want to enter on the next turn. This allows your actor to behave intelligently: rather than just following a script without reference to whats going on in the game, the actor can take different actions depending on the conditions around him. In our simple example above, the only sensitivity to game conditions is that the actor waits to open the safe until hes alone (or thinks he is); you can easily test for much more complex conditions, though.
In deciding how to implement your actors, you should consider the things you actor does most frequently. If your actor mostly moves around on a track, and occasionally does something special in a particular location or when a particular set of conditions is true, then implementing your actor using the techniques for moving on a track is easiest; just check for your special conditions before or after moving. On the other hand, if your actor does something special in many locations and on most turns, using a state machine is easiest.
One of the main areas of interaction with actors in adventure games is asking the actors questions. TADS is a bit limited when it comes to artificial intelligence; unfortunately, TADS doesnt make it possible to ask actors general questions such as what do you think about the role of the media in the upcoming election? or why is the sky blue? Instead, questions addressed to actors in a TADS game are limited to asking about particular objects in the game. Furthermore, all the player can do is ask about an object in general - the player cant ask why is the fuse outside the tram?, but is limited to ask lloyd about the fuse.
Another type of interaction is to tell an actor something. As with questions, TADS is too limited to allow a player to tell an actor anything very specific. All that a player can say is something like tell lloyd about the scroll.
As a game designer, these limitations make your job fairly straightforward. Implementing the ask and tell routines is essentially the same as implementing any other verb method. Heres an example of a doAskAbout method for an actor.
verDoAskAbout( actor, iobj ) = {} doAskAbout( actor, iobj ) = { switch( iobj ) /* indirect object is what is being asked about */ { case insurancePolicy: if ( insurancePolicy.isbought ) "\"It's very complicated, but let me assure you, it's a very good policy.\""; else "\"It's a great policy for only a dollar!\""; break; default: "\"I don't know much about that.\""; } }You could add more cases as needed to add to Lloyds knowledge.
Note one strange twist here: the actor parameter to the methods is the actor that asked the question - that is, the player, or Me. The actor whos being asked is self, since the actor is the direct object of the ask verb. Note also that we had to implement an empty verDoAskAbout method so that the actor can be asked questions at all; by default, all objects inherit a generic error message for verDoAskAbout.
You can implement behavior for telling an actor about an object in much the same way, by writing verDoTellAbout and doTellAbout methods for the actor. Likewise, you can make it possible for the player to give objects to the actor by implementing verIoGiveTo and ioGiveTo methods for the actor. Note that the ioGiveTo method should move the direct object into the actors inventory (by executing dobj.moveInto(self)) if the object is accepted.
By default, actors dont let the player take objects that the actor is carrying. This is because all of the routines that try to move an object will call the verGrab( actor ) method of all of the containers of the object. By default, an actors verGrab method will display a message indicating that the actor wont give up the object so easily; since verGrab is called during the validation phase of parsing, displaying a message is sufficient to prevent the command from proceeding.
If you want to allow the player to take objects from an actor, override the actors verGrab method. You could make the method do nothing at all, in which case the actor will allow any object to be taken; or it can display a message only on certain objects, which will prevent only those objects from being taken.
The player can issue a command to an actor by typing something like lloyd, go north. When this happens, the TADS parser processes the command in almost exactly the same way as a normal command, except that the actor of the command is changed from the default Me (the players actor) to the actor indicated in the command. (In fact, it really is exactly the same processing; when no actor is typed, its as though the player had typed me, go north.)
Since most all processing occurs relative to the actor parameter to the verb-handling methods (such as verDoTake( actor ) and doDrop( actor )), you dont need to make very many special provisions for commands directed to actors. This type of processing is done almost entirely automatically by the system.
However, in most cases, you dont actually want the player to be able to boss your actors around. Most of your actors will have a mind of their own, and many will even be presenting obstacles to the player.
To make it possible for you to control whether the player can control your actors, and to what extent, the TADS parser will always call the actorAction method of the selected actor prior to allowing any further processing to proceed. If this routine executes an exit statement, the command will go no further, preventing the player from controlling your actor. Since the method receives all of the information on the current command (the verb, direct object, preposition, and indirect object) as parameters, it can choose to allow or disallow particular commands. For allowed commands, do nothing; for disallowed commands, display an appropriate message and then execute an exit statement.
At this very moment, we have the necessary techniques, both material and psychological, to create a full and satisfying life for everyone.
BURRHUS FREDERIC SKINNER, Walden Two (1948)
Chapter Nine | Table of Contents | Chapter Eleven |