Rooms and Regions

In addition to Thing, the other absolutely essential class you need to write a TADS game with the adv3Lite library is Room. Every game must have at least one Room in which the action takes place. Your game may have several Rooms (depending on the size of the game world you want to implement). Note that in Interactive Fiction in general and the adv3Lite in particular a Room isn't necessarily a room in a house (such as a kitchen or study) but any area the contents of which are considered accessible to the player character; so, while a Room could be a conventional room in a house, it could also be one corner of a city square, or a section of a riverbank, or a woodland path, or a meadow, part of the deck of a ship, or any number of other such things.

A Room is a kind of Thing (or, to put it more technically, Room is a subclass of Thing) so Room inherits all the properties and methods of Thing, but in practice you won't use many of them, and in the main you'll be using the methods and properties specific to Room. (Possible exceptions include the vocab property and the isLit property, which determines if the Room is lit or dark; by default it's lit. Another clear exception is the desc property, which contains the description of the Room).

Defining a Room

The basic properties to define on a Room are its roomTitle (the name that's displayed in bold at the start of a room description), its desc (the body of the room description) and, optionally, its vocab. The normal way to define these properties is through the Room template. Without the vocab property this looks like this:

kitchen: Room 'Kitchen' 
    "This kitchen is equipped much as you'd expect, with, for example, a sink
    over by the window, a large table in the middle of the room, and an oven
    over by the back door to the east, not far from the fridge. The other exits
    are west to the hall, north to the dining-room and down to the cellar. "
;

If the vocab property is defined, it is given in a second single-quoted string, thus:

kitchen: Room 'Kitchen' 'kitchen' 
    "This kitchen is equipped much as you'd expect, with, for example, a sink
    over by the window, a large table in the middle of the room, and an oven
    over by the back door to the east, not far from the fridge. The other exits
    are west to the hall, north to the dining-room and down to the cellar. "
;

There are a couple of advantages to defining the vocab property on a Room:

  1. If you want the player to be able to use the GO TO command (implemented via pathfind.t) rooms have to have vocab words for the player to be able to refer to them in a command like GO TO KITCHEN.
  2. By defining the vocab property you also automatically define the Room's name property (and hence it's theName property), which can be useful if you want the game to display a message that includes the name of the Room (e.g. 'You wander into the kitchen', perhaps generated from "You wander into <<getOutermostRoom.theName>>. "). In the kitchen example above this may look a little redundant since the name is the same as the roomTitle, but this need not always be the case. For example you might have a room whose roomTitle is 'Portland Square (east side)', which wouldn't work too well as a name (you might want to name it 'east side of Portland Square', for example).

That said, in many cases you can leave the vocab property to be defined implicitly on many rooms, since the default behaviour of the English-language-specific part of the library is to derive the vocab property from the roomTitle property according to the following rules:

  1. The vocab property won't be derived from the roomTitle if the vocab property has already been explicitly defined.
  2. The vocab property won't be derived from the roomTitle is the Room's autoName property is set to nil (it's true by default).
  3. The vocab property will be a lower-case version of the roomTitle (e.g. 'Kitchen' will become 'kitchen') unless the Room's proper property is set to true (indicating that the Room has a proper name like 'Market Street').

This saves the need to type a name like 'kitchen' twice in the common case, while allowing the vocab property to be somewhat different from the roomTitle property in cases like the Portland Square (east side) example.

By default the roomTitle is displayed in bold at the start of a room description. This normally works well, but if for any reason you want to change this format you can do so by modifying the roomnameStyleTag. For example, if you wanted the roomTitle (the name of the room) to be displayed in bold italics each time, you could override roomnameStyleTag thus:

modify roomnameStyleTag
    openText = '\n<b><i>'
    closeText = '</i></b>\n'
;

You could also use <FONT> tags here for other effects, for example specifying a particular colour, but this needs to be done with caution, since a colour that looks good in your interpreter may not work so well for players who have chosen a different colour scheme, such as white text on a blue background. On the other hand, there's no reason why you shouldn't use a <FONT> tag to change the size of the room name if you so wished, for example:

modify roomnameStyleTag
    openText = '\n<b><FONT SIZE=+2>'
    closeText = '</FONT></b>\n'
;

Incidentally, there's also a roomdescStyleTag that can be used in a similar way to format the long description of a room (but not the listing of its contents), a roomcontentsStyleTag that can be used to format the display of a room's contents, and a statusroomStyleTag for formatting the room name as it is displayed in the status line, as well as a number of other StyleTags you can look up in the Library Reference Manual.

Direction Properties

Unless your game only has a single room, you will generally need to provide some means of travelling from one location to another, and the normal way of doing that in an IF game is by defining direction properties on a Room. These correspond to the command a player would type to move in the corresponding direction (e.g. if the player typed NORTH or GO NORTH the game would attempt to move the player character according to the value of the current room's north property). The adv3Lite library provides 16 such properties:

You do not have to define all of these directions on every Room (indeed, you'll probably never do so); if any direction property is left at nil that simply means that travel is not possible in that direction. But if you do define any of these properties they can be defined as one of:

If you define a direction property as a Room, Door, TravelConnector or method, then the corresponding direction will be shown in the list of exits (assuming that the module exits.t is present in your game). You can change this behaviour on a TravelConnector (and hence also on a Room or Door) by setting its isConnectorListed property to nil. A method will (virtually) always be listed as a possible exit (since presumably the point of defining it as a method is that something happens if the player character attempts to move in that direction). An exit defined as a single- or double-quoted string will never be listed as an available exit.

It follows from this that defining a direction property as a string is equivalent to using a NoTravelMessage in the adv3 library (for which reason NoTravelMsg is not defined in the adv3Lite library). Likewise, a method that simply displays a string is equivalent to an adv3 FakeConnector (which is likewise not defined in the adv3Lite library). For example:

kitchen: Room 'Kitchen' 'kitchen' 
    "This kitchen is equipped much as you'd expect, with, for example, a sink
    over by the window, a large table in the middle of the room, and an oven
    over by the back door to the east, not far from the fridge. The other exits
    are west to the hall, north to the dining-room and down to the cellar. "
	
    north = diningRoom
    east = backDoor
    west = hall
    down = cellar
    south = "Unfortunately, you can't simply walk through the window, and you have no means of opening it. "
    southwest { "You have no reason to visit the pantry. "; }
;

This illustrates what is probably likely to be the most common use of a method defined on a direction property, but in principle such a method can do anything you like, including killing the player character, ending the game in victory, or moving the player character to another location. If a direction-property method does result in the player character being moved to another location, the library keeps track of it in a LookupTable for use by the pathfinder, however for anything other than displaying a message that doesn't result in travel or ending the game, it's probably better and cleaner to use a TravelConnector to carry out the side-effects of travel (rather than defining them in a method). The possibility of doing whatever you like in a method is nevertheless there if you want it.

The shipboard directions port, starboard, fore and aft will generally be rather meaningless except for rooms that are meant to be aboard a vessel of some sort. Conversely, it's possible, though by no means certain, that a game may want to prevent the use of compass directions when the location is meant to be aboard a ship. To make it easier to deal with such situations Room defines the two properties/methods allowShipboardDirections and allowCompassDirections. If either of these is nil, the corresponding set of directions is disabled for all relevant commands (e.g. GO PORT, THROW BALL PORT or PUSH TROLLEY PORT) carried out in the room in question. Attempts to use the disallowed directions in any command will then be blocked with message stating "Shipboard/compass directions have no meaning here." By default allowCompassDirections is true for all rooms, but game code can override this on rooms that are meant to be aboard ship (or, indeed, in any game that wants to abolish compass directions altogether). On the other hand the default behaviour of allowShipboardDirections() is to return true if and only if one or more of the shipboard direction exits (port, starboard, fore or aft) is non-nil on the room in question. Normally, this means that allowShipboardDirections() can be left to take care of itself, but occasionally you may have rooms, such as a the hold of the ship, where none of the shipboard directions is actually defined (because the only way out is UP, say) but the shipboard directions are still notionally meaningful; on such a room you could simply define allowShipboardDirections = true. (The enforcement of these conditions is carried out by the Doer method checkDirection() which is called from Doer.exec() for any command involving a direction).

Finally, note that defining anything on a direction property of a room establishes a connection in one direction only. For example, in the sample code shown above, defining north = diningRoom on the kitchen establishes a connection to the dining from the kitchen going north, but it doesn't also establish a connection south from the dining room to the kitchen; that would need to be defined explicitly on the diningRoom object. This may be an advantage (a) because it may help to make your code clearer and (b) you may not always want a connection back in the reverse direction, or you may want it to behave differently. On the other hand, if you would prefer the reverse connections to be automatically set up for you, you could try using the symconn extension, which does just that.


Directions

A direction in the adv3Lite object is generally represented by an object of type Direction, usually named with the name of the direction plus 'Dir', e.g. northDir, eastDir, downDir, southwestDir. For the most part you won't need to worry about direction objects since they generally take care of themselves, but occasionally you may want to refer to the name of a direction object when it's used as a parameter for some method or an element of a list (e.g. in the route finder) to indicate the direction taken.

Otherwise, the only time you might want to worry about Direction objects is if you want to define a custom direction. This is a relatively straightforward process, best explained by means of an example. Suppose, for example, you wanted to implement a nornoreast direction which caused the player character to travel via the nornoreast property of the current room when the player entered the command NORNOREAST or NNE. You would just need to do this:

nornoreastDir: CompassDirection
    name = 'nornoreast'
    dirProp = &nornoreast
    sortingOrder = 1450
    opposite = sousouwest //assuming you were also defining a sousouwest direction
;

grammar directionName(nornoreast): 'nornoreast' | 'nne' : Production
    dir = nornoreastDir
;

With these two definitions, you could then used nornoreast just like any of the built-in directions. The sortingOrder property on the Direction object defines the order in which this direction will appear in any list of exits; I chose 1450 here since this would make nornoreast come just after northeast. The grammar declaration that follows enables the parser to recognize 'nornoreast' and 'nne' as referring to the nornoreast direction.


Other Room Properties and Methods

Some other properties and methods of Room you may find useful include:

Note that some of these explanations involve concepts we haven't come to yet. Don't worry; they will be explained fully in their place when we come to them.


The Floor of a Room

The ground (or floor) is present virtually everywhere (except for rooms representing odd locations like the tops of trees or masts). The library defines a Floor class, and one instance of it, defaultGround, to represent the presence of the floor/ground in every room. A Floor is a combination of MultiLoc and Decoration that (by default) is added into every Room. Its main purpose is to facilitate the parser's ability to disambiguate items by their locations. Without it, if, say, there were two identical coins, one on a table and one directly in the room, the parser would have to ask "Which do you mean, the coin on the table or the coin?", which is unclear and fails to give the player an easy way of selecting the latter. Thanks to the presence of a defaultGround in every Room the parser can ask "Which do you mean, the coin on the table or the coin on the ground?" and the player can refer to "the coin on the ground" to disambiguate.

The defaultGround object present in every room also performs the secondary purpose of allowing players to refer to 'the ground' or 'the floor' which must be implicitly present in nearly every room, but its implementation is deliberately minimalistic to discouraage players from trying to interact with it. The library will translate PUT SOMETHING ON FLOOR to DROP SOMETHING, and X GROUND/FLOOR will of course work, but that's about it (everything else gets the standard decoration response 'The ground is not important')

If you want to define a custom Floor object for a particular location, or omit it altogether (e.g. for a room at the top of a tree), you can do so by overring the floorObj property, either to point to your custom Floor object, or to nil (in the case of a room without a floor). You should do this even if you implement your custom floor object as a Fixture in a single location rather than using the custom Floor class, but you might find it better to use the Floor class even for a custom floor that appears only in one room, since it's designed to facilitate the parser disambiguation just described. If you do decide to define your own Fixture, you'll need to copy most of the methods and properties of the Floor class onto it to make it work properly as a floor.


Closed Containers as Quasi-Rooms

Although all the above properties and methods have been described as belonging to Room, several of them are in fact defined on Thing, to allow for the possibility that the player character may at some point be inside a closed container and look around from inside it. In that case you can use the roomTitle, darkName and darkDesc properties on the containing Thing to determine what it should be called and how it should be described from the inside. These work in just the same way as they do for Room, as does the isIlluminated() method. You can also override the interiorDesc property to describe how the closed container looks from the inside.


Regions

Regions in adv3Lite are simply a means of grouping Rooms together in any way you find useful (e.g. all downstairs rooms, all indoor rooms, all forest rooms, all riverside rooms, all outdoor rooms). You don't have to use Regions if you don't want to, but they are straightforward to use if you do. To include a Room in a Region, simply list that Region in the Room's region property and create a corresponding Region object, for example:

kitchen: Room 'Kitchen' 'kitchen' 
    "This kitchen is equipped much as you'd expect, with, for example, a sink
    over by the window, a large table in the middle of the room, and an oven
    over by the back door to the east, not far from the fridge. The other exits
    are west to the hall, north to the dining-room and down to the cellar. "
	
    regions = [downstairs]
;

downstairs: Region
;

Regions can themselves be included within other regions by setting their regions property. For example, to place the downstairs Region entirely within the indoors Region we could write:

downstairs: Region
  regions = [indoors]
;

indoors: Region
;

It is also perfectly legal to define the regions property of Rooms in such a way that Regions end up overlapping.

The properties and methods of Region you may find useful include:

Some of the uses of Regions depend on features of the library we have not yet covered, and will need to be mentioned again when we come to them, but in summary the main uses to which Regions can be put include:

A further word of explanation may be in order about travelerLeaving() and travelerEntering(). The first is called on all the regions the traveler is about to leave, and the second on all the regions the traveler is about to enter. Leaving a region means travelling from a room that is an that region to a room that is not. Conversely entering a region means traveling from a room that is not in that region to one that is. So, for example, if throneRoom is in regions A, B, C and D and corridor is in regions C, D, E and F, traveling from throneRoom to corridor would cause travelerLeaving() to be invoked on regions A and B (as well as throneRoom) and travelerEntering() to be invoked on Regions E and F (as well as corridor).

Note that all these notifications take place just before the travel is actually executed. If you want something to take place immediately after the traveler enters a region one way to do it would be to set a zero-length fuse in the travelerEntering() method, e.g.:

planeRegion: Region
    travelerLeaving(traveler, dest) { "You're about to leave the plane. "; }
    travelerEntering(traveler, origin) 
    { 
        "You're about to enter the plane. ";
        new Fuse(self, &edesc, 0);
    }
    edesc = "You've just boarded the plane. "
;

One further point: several of the properties on which the Region mechanisms depend are set up by the library at the preinitialization stage. In particular the roomList property of a Region is built at PreInit stage. This means that the layout of regions cannot be changed during the course of a game. If you need a Region that changes during the course of your game you could try the DynamicRegion extension.

If a Region is defined with a rooms property containing a list of rooms, each of these rooms will have that Region added to its regions list, but the building of the Region's roomList will still proceed as before (removing duplicate entries in any case). This makes it safe to define the Rooms that go into a Region either by listing the Regions in a Room's regions property, or by listing the rooms if a Region's rooms property, or a mixture of both.

There is absolutely no need to define both the regions property of a Rooms and the rooms property of a Region to associate Rooms with Regions, but no harm will be done if you do. The purpose is simply to allow both methods of associating Rooms with Regions so that game authors can use whichever method they find most congenial, including a mixture of the two.

The rooms property of a Region can be specified via a template, thus:

 downstairsRegion: Region
   [hall, kitchen, study, lounge]
   regions = [indoorRegion]
;
 

In this example, note how putting one Region inside another must still be done via the enclosed Region's regions property. If you have a larger Region that encloses smaller Regions you could also use the rooms property on the larger Region to list the smaller Regions that go to make it up. The point to bear in mind is that the regions property of X can be used to define the regions that X is in, while the rooms property can be used to define the Rooms (or Regions) that are in X, and that these provide alternative means for defining the same relationship.