TADS 3 is the latest version of “The Adventure Development System.” It is a powerful programming language for the development of text adventures. Out of the box, TADS comes with a base programming language (which is geared towards text adventures but could well be used for other purposes) and an adventure library. It is the library that provides lots of classes that make text adventure development possible. There are classes for rooms, items, non-player characters, buttons, levers, even smells and sounds.
The TADS Adv3 library, as it is called, is quite big and provides numerous classes for almost every conceivable adventure implementation detail. Because of this level of detail, it can be difficult to get started with TADS 3, and this is one of the reasons that there is also a “light” version of adv3 available (called Adv3Lite) – though this doesn’t mean that it’s that much smaller, but the authors have removed some of the less used classes.
While spending time learning to use Adv3, it occurred to me that I would like to focus on developing complex environment components and try to isolate them from the rest of the code. This way, I would be able to encapsulate code, removing it from the main body of code to a separate file, and initialize my components with a few parameters. This way, I would be able to reuse polished components in a game (or various games). Still, it’s unlikely that I would want to reuse a component over and over, because every game is and should be different. Nevertheless, encapsulating code improves code readability and is an interesting exercise it itself.
This article will go on to describe the development of a sample complex component: an elevator that moves between floors (rooms) and is equipped with buttons, doors and floor indicators.
The elevator is added declaratively to the game by saying:
Here, the elevator stops at
startRoom (floor 0) and
topRoom (floor 1). More rooms can be added to the list. It is not necessary to make changes to startRoom or topRoom other than adding text to the rooms describing the elevator doors, call button and floor indicator. These objects will be added automatically to the rooms.
exitDir indicates in what direction the exit of the elevator lies. In this example, the elevator exit is to the south, thus the exits from the rooms to the elevator lie to the north.
Other design requirements are:
- The elevator and all its related objects are generated from the declaration above.
- A call button summons the elevator from outside (if the elevator is not present).
- The elevator doors open automatically.
- The elevator contains buttons for all floors.
- The player can press the buttons to make the elevator move, or go UP or DOWN to keep interaction less wordy.
- The floor indicator shows the floor the elevator is on, both inside and outside of the elevator.
Elevator base class
The first code for the elevator base class is this:
This defines the elevator room name, destination name and overrides default walls, ceiling and floor for a better look. It also implements the properties
floors (the list of floors that the elevator can visit, in order from bottom to top) and
exitDir (the direction that the elevator opens into). Finally, it keeps track of where the elevator currently is. Note that we ask the Adv3 library to spell out a digit for us (e.g. 1 becomes ‘one’, 2 becomes ‘two’ etc.) using the
spellInt function. There are also functions for spelling out ordinals, which we’ll need later.
Connecting the exits
exitDir is actually a reference to a property, which is how we store our exit direction. TADS allows us to use an ampersand (&) to get a reference to a property. We’ll still need to actually set a TravelConnector in that direction. We’ll add a method to the class for this:
When you call
setExit(0), then the property that
exitDir refers to is set to point to the room stored in
floors. For the sample code at the top of this article,
exitDir = &south, so that means that
elevator.south is set to
startRoom (which is the first floor in the floors list). We use a reference to a property to make sure that the code also works if the elevator exit direction were to lie to the north, or to the southeast, or even up, although that would make no sense.
We’ll need a place to actually call the
setExit method, which we’ll get to shortly.
Describing the exits
It would be nice if we were able to say which direction the doors lead:
A set of open double doors leads north out of the elevator.
We do have this information (it’s in
exitDir), but we need to find a way to convert that direction to a string. I found that this was possible by adding a method to the Elevator class:
Here, we query the Adv3 library for all existing directions (north, south, etc. as well as up, down, port…). For each direction, we then ask the Elevator for the TravelConnector that exists for this direction (in fact, it’s the one that is created by the
setExit method). For the TravelConnector that matches
self.(exitDir), we return the direction’s name. This is an interesting bit:
self.(exitDir) actually dereferences our reference to a property against the Elevator instance (
self), thus returning the actual property value. It turns out that property references are valuable tools. It’s actually possible to dereference this reference against a different object, as well.
Initializing the elevator
So far, we only have the elevator base class and a couple of methods. Where and when should these methods be called? How can we create additional objects, like the elevator doors, buttons and floor indicator? We’ll need some sort of entry point to the elevator code that gets called when the elevator is first constructed.
It turns out there is a special method that gets called by Adv3 at precisely this time:
initializeThing. But where does it come from? The Elevator class is derived from Adv3’s Room class, which is in turn derived from the Thing class. It is the Thing class that provides the
initializeThing method, which is intended to be overridden. We can thus initialize the Elevator like so:
Whenever a new Elevator is instantiated, the
initializeThing method calls its base code using inherited (the Thing class does other initialization work that we must allow to happen). After that, we are free to call our own methods, so we call
setExit to point the exit to the first room in the floors list.
The initialization object is also a good place to create other objects associated with the elevator.
Creating associated objects
Our elevator still needs buttons, a panel to group them on, a set of doors, and a floor indicator. And that’s just inside. On each floor that the elevator visits, we’ll also need to automatically create a set of doors, a call button, and a floor indicator.
Since our Elevator is a class and not an object, we cannot just say:
Suppose we had more than one elevator in our game, which elevator would our floorIndicator belong to? To get this to work, we need to create classes for our other objects as well. Here, for instance, is the elevator floor indicator:
Some of this code is standard Adv3 fare: defining the readout’s name, vocabulary words, and long description. It’s a
CustomFixture, so we can provide a custom
cannotTakeMsg as well. However, there are some new things here as well.
The readout class has a
construct method. This method gets called when we create an instance of the indicator using the
new keyword. This allows us to pass in some parameters as well: the elevator that the indicator belongs to, and the location it’s in (we will have an indicator inside the elevator, as well as indicators outside, one on each floor). We’re storing a reference to the owning Elevator instance in order to be able to store the floor number the elevator’s on in the indicator’s description.
Creating an instance of the indicator class is done so:
This will create an instance connected to
self (assuming this code lives in our Elevator class), and places it in the startRoom (floor 0).
We can now add this to our Elevator initialization method to create a readout inside the elevator:
We’ll need to add a button inside the elevator for each floor that it can visit. We’ll make a button class first:
This class has a
construct method, just like the ElevatorReadout we’ve seen before. It stores a reference to the owning Elevator instance, as well as the 0-based index of the floor that the elevator goes to when the button is pressed. We also use the index to add the floor number to the button description, so that we don’t need to create many different button classes. Finally, we’ll use the index to add vocabulary words to the button. That way, it’s possible to refer to them as “button one” or “button marked two”.
We’ll need to add a button for each floor, so we add a loop to the Elevator initialization code (note index – 1, since TADS lists indices start at 1).
When a button is pressed, we call the method moveToFloor on the elevator to actually move the elevator:
This method causes the elevator to move to the floor indicated by
index. If the elevator is already on the destination floor, nothing happens. This is a good place to add some flavor text describing the elevator’s movement.
Adding floor indicators and a call button on each floor is done in a similar way.
It remains to add doors to the elevator. It’s currently possible to leave the elevator and arrive on the correct floor, but it’s not possible to enter it. We could use
exitDir to create the right connectors, but we’d like to go one step further and add doors to each floor. The doors are open when the elevator is present, and closed when it’s on a different floor.
First, we’ll add a base class for the doors. It’s not actually possible to open or close the doors (they are automated) so we’ll use a ThroughPassage. More importantly, our doors are special in the sense that while doors leading into the elevator have only once associated facet object, which is the internal doors. However, the internal doors have multiple associated facts, i.e. all external doors. The Adv3 Door implementation can’t handle this. The following code implements some basic door behaviour:
The internal doors are simple to implement. From inside the elevator, they are always open. They merely set the Elevator instance as the passage’s destination:
The external doors are different. These doors are open or closed depending on whether the elevator is present. We implement this behaviour by overriding the
explainTravelBarrier methods which our class derives from ThroughPassage:
This class’s construction method takes a
room and a
connection argument. The room is the location that the external doors are placed in, while the connection is the direction that they open into. This must be the direction that is opposite the exitDir, so we’ll need a method to get opposite directions:
Supporting UP and DOWN
It would be nice to be able to move the elevator by simply typing UP or DOWN, rather than PRESS BUTTON X every time. What I’d like to do is override the UP and DOWN commands to actually remap to pressing a button. The way I’ve found to do this is:
The Elevator room’s Up connector is now a
NoTravelMessage . The connector’s
travelDesc method is overridden to instruct the elevator to move up.
Please note that I am sure a more elegant method to do this must exist, but I haven’t found it yet.
Getting the code
The complete code for the Elevator component discussed in this article is available for download here.