MusicWords Logo

Techniques And Design Strategies

Learning any new programming language requires some effort. A language optimized for interactive fiction poses some special challenges, simply because it includes more preconfigured widgets than a general-purpose language. In the process of learning TADS 3 (and I've barely begun), I've been able to take advantage of the generous and often inspired guidance provided by the TADS power users on rec.arts.int-fiction. After a month or so of asking dumb questions and getting smart answers, it occurred to me that maybe other newcomers to TADS would like to have access to a repository where the stuff I've learned is made available with a minimum of searching.

That's what this page is for. I'm sure I'll add to it as time goes on. Thanks to Eric Eve, Khelwood, Nikos Chantziaras, Krister Fundin, and others for their help. Needless to say, all of the goofy shit that remains in the tips below is due strictly to my own ineptitude.

As of April 2008, the new release of TADS 3 (3.0.016) comes with a whole new book by Eric Eve called Learning T3. This is a phenomenal resource. Study it! (I plan to.)

Distant Characters

It sometimes happens that you want the player to be able to see a character who is too far away to talk to. One standard way to do this (there are others, using DistanceConnectors) is to create a basic Distant object. You might call it viewOfOtherCharacter. You might also want to print out a non-default message when the player tries to talk to this distant character. You can do it this way, in the Distant object:

dobjFor(TalkTo) {
    verify() {}
    check() {
        failCheck('It might be better if you didn\'t do anything to attract his attention. ');
    }
}

If you do this, however, the library will spit out a run-time error (a bug) if the player types simply 'hello' when in the location where the Distant object is residing. The reason is because your dobjFor(TalkTo) code has rendered conversation logical.

The fix is to add the following code (courtesy of Khelwood) to the player character object:

canTalkTo(actor) {
    if (actor.communicationSenses == nil) return nil;
    return inherited(actor);
}

takeTurn

ActorStates include a handy takeTurn method, which can be used to cause the actor to do whatever is needed. But if you're an emigree from the Inform 6 community, you may assume (wrongly) that takeTurn is like Inform's each_turn routine. each_turn only runs when the player is in the room, but takeTurn always runs. (It could be used to cause an NPC to wander around at random, for instance.)

If you want to subvert this behavior, there's an easy way to do it:

if (myPlayerCharacter.getOutermostRoom != roomWhereNpcIs) return;

Instead of putting this directly in takeTurn, you may want to put it in whatever custom method the takeTurn is invoking.

No Exit

Normally, when the player asks to go in a direction where there's no location from the current room, the TADS interpreter helpfully lists the exits that do exist. But what if you've tossed the player into a deep pit from which only a magical escape is possible? You might think to define a cannotGoThatWayMsg for the pit (perhaps 'The sides of the pit are distressingly solid, and far too steep to climb'), but when the player tries to leave, this will be the result:

>u
The sides of the pit are distressingly solid, and far too steep to climb. There are no obvious exits.

In the circumstances, that second sentence is unnecessary and ugly. Here's how to get rid of it:

cannotGoThatWayMsg = 'The sides of the pit are distressingly solid, and far too steep to climb. '
cannotGoThatWay() {
    reportFailure(cannotGoThatWayMsg);
    }

What we're doing here is overriding the cannotGoThatWay method of BasicLocation (found on line 4202 of travel.t) in order to get rid of its second line, which is a call to cannotGoShowExits(gActor).

Teleportation

A classic event in IF is that some magical action transports the player to an entirely different location. Here's one good way to do it in TADS 3. Let's suppose your player object is named Henry. In the block of code for 'wave magic wand' or whatever causes the teleportation, include this:

Henry.moveIntoForTravel(thePlaceHenryIsMovingTo);
Henry.lookAround(true);

The lookAround command is what will cause the room name and description to show up in the transcript, just as if the player had typed 'look'. But you'll find that the line spacing before the room name is a bit too narrow. To fix this, include the new-paragraph tag <.p> at the end of the last sentence of printout before the teleportation is triggered.

Fix for the 'remove me' Bug

This library bug (in 3.0.12) causes TADS to freak out in response to the command 'remove me'. The fix is simple. In actor.t, find the tryMakingRoomToHold(obj, allowImplicit) method. It should be somewhere around line 6600. There's a line in this method that looks like this:

if (obj.whatIfHeldBy({: getWeightHeld()}, self) > weightCapacity)

Directly above this line, add a new line:

if (obj==self || isIn(obj)) return nil;

If the line is already there, you're using a more recent version of the library in which the bug has been fixed.

Fix for the 'set to' Bug

In TADS 3.0.12, trying to use the verb 'set to' to set an object that isn't settable will cause a crash. Stephen Gorrell suggested this fix:

modify playerActionMessages
    cannotSetToMsg = '{You/he} {cannot} set {that dobj/him} to anything. '
;

Detecting Travel

Once in a while you may encounter a game design situation in which, when the player arrives in a particular location, it's useful to know where she's coming from. There are two ways to do this in TADS (if not more). Both of these will work whether the player character used a direct connection between rooms or passed through a door or other TravelConnector.

First, here's how to find out which direction the most recent travel command utilized:

if (gAction.originalAction.getDirection == northDirection)

Replace "northDirection," obviously, with whatever direction you're concerned with. This appears to work even if the actual command the player gave was 'cross bridge' or whatever.

The second method is to intercept travelerArriving in the new room:

travelerArriving(traveler, origin, connector, backConnector) }
    if (origin == withinTheCrypt) doSomethingScary(); 
    inherited(traveler, origin, connector, backConnector);
}

This will cause your doSomethingScary method to run whenever the player arrives from withinTheCrypt. Note that your new method will run (and print out its result, if any) before the inherited version of travelerArriving runs. To be specific, travelerArriving adjusts the traveler's posture if necessary, runs enteringRoom(traveler), and finally calls the traveler's describeArrival(origin, backConnector) routine.

This order of events could cause the printout to read clumsily. describeArrival, for instance, is where the lookAround action for the new room is invoked. If you want your customized message to be printed after the room description rather than before it (for instance, if you want to add, "Behind you, the vault door slams shut!"), you could reverse the order of events in travelerArriving so that inherited is called first, followed by your new line of code, or you might simply set a flag in travelerArriving and react to the flag in the room's description property. (Note that the latter technique will produce an output only if the player has chosen verbose mode or if you cleverly set the new room's seen property to nil before describeArrival is called.)

Looking in an Empty Container

By default, if the player enters 'look in the chest' when the chest is open but empty, the TADS library will respond, "You see nothing unusual in the chest." I find this unacceptably vague. To customize it, you need to understand that the Thing class has a property called lookInLister. Listers assemble lists of objects for printout. The value of this property is thingLookInLister, which is an object in msg_neu.t (one of the two language-specific files responsible for understanding and generating actual English).

Rather than modify thingLookInLister globally (though that's a good option too), we could customize our chest object by giving it its own thingLookInLister, like this:

chest : OpenableContainer 'dusty old chest' 'dusty old chest'
     lookInLister : thingLookInLister {
          showListEmpty(pov, parent) {
               gMessageParams(parent);
               defaultDescReport('There's nothing in the dusty old chest but cobwebs. ');
          }
     }
;

The effect of this is that all of the methods of thingLookInLister are inherited except showListEmpty, which we've overridden to produce the desired output. I'm not sure the call to gMessageParams is needed, since we're not using a {the dobj/him} parameter substitution macro. But leaving it in won't hurt anything.

Changing an Object's Vocabulary During Gameplay

It often happens that the words used to describe an object change during the course of a game. For instance, a bottle full of milk might later become empty. If you're feeling lazy, you can just include 'empty full bottle/milk' in the object's vocabWords property and trust that the player will use the correct term without thinking about it. But there are situations where that's not such a good idea. For instance, the player might have poured the milk into a glass, and it might now be desirable that 'milk' refer to the glass but not to the bottle.

This can be handled using ThingStates. ThingStates can be useful for other reasons. But in the current version of the library, the vocabulary assigned to each ThingState is assumed to be unique, so if you have two states of a bottle to which the player should be able to apply the word 'empty', using ThingStates won't work.

The solution is to use the methods cmdDict.addWord and cmdDict.removeWord. These methods are explained in the "Dictionary" page of the System Manual. Their syntax is:

addWord(obj, str, vocabProp)
removeWord(obj, str, vocabProp)

the cmdDict object is not mentioned in the Library Reference Manual; it's declared near the beginning of adv3.h with this line:

dictionary cmdDict;

All that line does is create an object of the dictionary class. This object will then be filled with your game's vocabulary during the initialization phase. To continue our example, when the milk bottle is emptied (however that happens), these lines should be added to the code:

cmdDict.addWord(milkBottle, 'empty', &adjective);
cmdDict.removeWord(milkBottle, 'full', &adjective);
cmdDict.removeWord(milkBottle, 'milk', &noun);
cmdDict.removeWord(milkBottle, 'milk', &adjective);

(I'm assuming you've included 'milk' as both an adjective and a noun in the original vocabWords in order to allow the user to refer to the object either as 'milk bottle' or as 'bottle of milk'.)

These methods are "safe" in the sense that if you try to remove a word that isn't there, nothing bad will happen. Likewise, if your code loops around and tries to add a word five times, it will only be added once.

Creating an Object That Resists Being Used

For puzzly reasons, you might find yourself wanting to create an object that rejects almost all commands other than 'examine'. Since the library defines dozens of verbs and your game will probably require adding a few new ones, intercepting them all one by one would be tedious and error-prone. Here's the solution:

dobjFor(Examine) {
     verify () {}
}
dobjFor(Default) {
     verify () {illogical(cannotUseItMsg));
     }
}
iobjFor(Default) {
     verify () {illogical(cannotUseItMsg));
     }
}
cannotUseItMsg = 'There\'s just no way you\'re going to be able to do that. '

The dobjFor(Default) and iobjFor(Default) blocks will trap any action that you don't specifically allow. In the code above, we've allowed Examine by overriding the verify test for Default. If your object may become more cooperative later on, you'd handle it this way:

dobjFor(Default) {
     verify () {
          if (!cooperative) illogical(cannotUseItMsg));
     }
}
cooperative = nil

A Container with a Removable Lid

By default, most interactive fiction languages model "containment" in a fairly simple way. If object A is "contained in" object B, and if object B is a container that happens to be closed, then object A is invisible because it's "inside" a closed container. This can be inconvenient. For instance, you might want to create a paint can that has a lid that can be pried up. Obviously the lid object needs to be "contained in" the can object in some sense, so that if the player carriers the paint can to a new room the lid object won't be left behind. But unless some special technique is employed, the lid object will end up "inside" the can, which means 'pry open the lid' will never work because the lid will be invisible until the can is opened.

TADS provides a ComplexContainer class, which gets us close to a solution -- but the components of a complex container are assumed to be permanent parts of the container object, not removable objects. The solution is to include a sneaky subsurface component as part of the paint can, and to allow only the lid to be put on that surface:

paintCan: ComplexContainer 'paint can/paint' 'paint can'
     "It's a can of Sherwin/Williams mustard yellow. "
     subSurface: ComplexComponent, RestrictedSurface {
          validContents = [canLid]
          contentsListedInExamine { return nil; }
          notifyInsert (obj, newCont) {
               "You fit the lid over the top of the can and bang it down with the heel of your
               hand. It fits snugly. ";
          }
     }
     subContainer: ComplexComponent, Container {
          isOpen = (!canLid.isIn(lexicalParent.subSurface))
          notifyInsert(obj, newCont) {
               if (obj == canLid) failCheck('The lid won\'t fit inside the can. ');
          }
          iobjFor(PutIn) {
               verify () {
                    if (!lexicalParent.isOpen) illogicalNow ('In order to put anything in the paint can, you\'ll need
                    to take off the lid. ');
               }
          }
     }
     // etc.
;

canLid: Thing '(paint) (can) lid/top' 'paint can lid'
     "The lid is round and shiny. "
     location = paintCan.subSurface
     // etc.
;

The key points here are the definition of validContents for the subsurface and the definition of the lid's location.

playerActionMessages

Many of the default messages that TADS produces are contained in the playerActionMessages object in msg_neu.t. You can easily modify this object if you don't like the defaults. There may be times, though, when you'd like one object to produce a "default" message that's different from those produced by other objects. This can entail a little more work.

For instance, I changed a door so that going east wouldn't cause TADS to try to open the door automatically. To do this, I added the nonObvious macro to the dobjFor(Open) verify block:

dobjFor(Open) {
     verify () { nonObvious; inherited; }

At this point, instead of trying to open the door automatically in response to 'e', the software replied, "You must open the door first." Not much of an improvement. This message, it turns out, is called mustOpenDoorMsg. It's queued up by the doorOpen object in precond.t. Since hacking a preCond object is a little deep for me, I inserted two new lines into a modification of playerActionMessages. If the player is getting this message in a particular room (called the alcove), and if the passage to the east has never been opened, a substitute string will be returned. It looks like this:

modify playerActionMessages
     mustOpenDoorMsg(obj) {
          gMessageParams(obj);
          if ((Diane.location == alcove) && (!stoneSlab.everOpened))
               return 'If there\'s a way to go east, it\'s blocked by the stone slab. ';
          return '{You/he} {must} open {the obj/him} first. ';
     }
;

This is a bit inelegant, but it does work.

Overriding Parts of an Action Response

One of my first questions, when I started learning TADS, was what exactly happened when I created some new dobjFor code to handle a particular action. For instance, consider this:

dobjFor(Lock) { 
     verify() { 
          if (isLocked) illogicalAlready ('The bolt has been slid into the frame; 
          the door is already locked. '); 
     } 
}

What's happening here is that I've overridden everything that was originally (in the class to which my door object belongs) in the verify block of dobjFor(Lock). If I want to include any other verify tests that may have been part of the class definition, I need to add the inherited keyword:

     verify() { 
          if (isLocked) illogicalAlready ('The bolt has been slid into the frame; 
          the door is already locked. '); 
          inherited;
     } 

However, I haven't done anything to check() or action(), so those automatically inherit the class behavior. When creating a dobjFor, I don't need to do this:

     check () { inherited; }
     action () { inherited; }

The preCond and remap aspects of dobjFor(Lock) are also inherited intact, because I haven't mentioned them.

Sneaky Elevator Tricks

This isn't about the TADS library, it's about one way to handle an odd situation. I created a room that's an elevator. It only ever visits the first and second floors. I wanted the player to be able to type 'u' when on the lower floor as a convenient shortcut for 'press the up button', and likewise 'd' when on the upper floor. But usually these commands will be routed to TravelConnectors, and the player isn't actually travelling -- she's still in the same room! What to do?

First I defined a pair of fake travel connectors:

elevatorUpConnector: FakeConnector
	"<<elevator.moveUp>>"
;
elevatorDownConnector: FakeConnector
	"<<elevator.moveDown>>"
;

Normally a FakeConnector prints out a message explaining why travel in that direction is impossible or uninteresting. In this case there's no message at all, only a call to my own method in the elevator object. My new methods handle the actual movement of the elevator -- which includes adjusting the elevator room's up and down properties. Among other things, moveUp does this:

up = nil;
down = elevatorDownConnector;

The other thing moveUp and moveDown do is swap the doors. The interior elevator door is actually two separate door objects -- an elevatorBottomDoorInside and an elevatorTopDoorInside. (These doors are linked to exterior doors that are in different rooms.) The moveUp method does this:

elevatorBottomDoorInside.moveInto(nil);
elevatorTopDoorInside.moveInto(self);

As far as the player is concerned, it's the same door as before. (Of course, if the game allowed the player to write graffiti on the inside of the door, I'd have to do it a different way.)

Using Scripted Messages

I wanted to create a ThroughPassage (a stairway) that would print out a varying travelDesc. So I tried to do it this way:

+ stairs: TravelWithMessage, ThroughPassage 'stairs' 'stairs' 
     "The staircase is narrow. " 
     travelDesc : ShuffledEventList {[ 
          'The stairs creak a bit as you climb. ', 
          'The stairs creak loudly. ', 
          'The stairs squeak unsteadily, a sound that sets your teeth on edge. ' 
     ]} 
     destination = upstairsHall 
; 

But that didn't work. This worked:

     travelDesc { travelDescScript.doScript(); } 
     travelDescScript : ShuffledEventList {[
     //etc.

The only difference is that now travelDescScript is a separate embedded object rather than being an anonymous embedded object that replaced the travelDesc itself.


Except where noted, all contents of MusicWords.net are (c) 2006 Jim Aikin.
All rights reserved, including reprint and electronic distribution rights.