ConSpace
a TADS 3 library extension
version 2
2004-2005 Eric Eve and Steve Breslin

We are very pleased to announce a breakthrough in the way space is represented in interactive fiction.

Consider a large forest, with a number of paths and roads running through it. When we attempt to represent this in a piece of traditional IF, we select various key points in the forest, draw a box around each of them, and make a room containing everything in that box. These boxes cannot overlap, and everything outside each box is generally too far away to be even noticed, let alone interacted with. This already shatters any hope of spacial continuity, but to make matters worse, everything inside the box shares the same immediate location, so we don't have any sense of space within the room either.

This produces a clumsy disjoint between the map and the world we're attempting to represent. We would like to create the illusion of a natural, open outdoors area, but the limitations of the IF spacial-model force us into a catacomb of monadic chambers, of tiny islands connected only by magical portals. This doesn't at all feel like being outdoors.

For a long time, the limitations of the IF world-model has been a central concern for many IF developers, but the problem has not been adequately resolved -- until now.

While it is built upon earlier developments (specifically nested rooms and sense connectors), ConSpace goes significantly further. This cutting-edge (and easy-to-use) extension presents a world model that actually feels proximity-based from the player's point of view, but which remains conveniently room-based from the author's. In our forest example, we would select key points in the forest to use as rooms, but the circles would become larger and overlap with nearby circles, so that the 'rooms' would feel like waypoints within an open space, rather than enclosures. (With the granularity of the space entirely at the game designer's descretion.) Finally we can present continuous, connected space in interactive fiction.

Getting Started

This extension requires TADS 3.0.9 or later.

To use this extension in your project, simply include ConSpace.tl in the list of source files used by your project (after the standard library).

There is a test-game (Find the Ring) included in the ConSpace.zip archive. This is a good introduction to ConSpace, though it only serves a taste of the extension's potential. We recommend that you play the test-game and take a look at the included sourcecode for the game. The present document will refer to a simplified version of the test-game.

You are welcome to contact the authors directly, but questions and problems are best addressed to the newsgroup: rec.arts.int-fiction

Connected Space (SpaceConnectors and connectionGroups)

ConSpace allows you to create a group of rooms which behave as areas within a larger space.

To create such a spacially connected group of rooms, you can simply assign to each room the same connectionGroup property. This property should refer to the SpaceConnector object that links the rooms together.
northHall: Room 'Hall (northern end)'
    south = southHall
    connectionGroup = hallGroup
;

southHall: Room 'Hall (southern end)'
    north = northHall
    connectionGroup = hallGroup
;

hallGroup: SpaceConnector
;
Note that there is no need explicitly to provide a list of linked rooms in the hallGroup object, since all rooms in the connectionGroup will be automatically added to their group's SpaceConnector. Alternatively, you can define the locationList of the SpaceConnector, and that connector will be added to the connectionGroup of each of its locations.

Note also that a single room can participate in multiple connection groups. You can declare this by setting the room's connectionGroup property to a list of SpaceConnectors, or alternatively by including the room in the locationList of multiple SpaceConnectors.

The capability to declare connection groups on a per-room, per-room-class, and per-SpaceConnector basis can be confusing, especially if you are allowing multiple SpaceConnectors per room (as when you want spaces to partially overlap). So it's a good idea to pick and stick with one technique for declaring spacial relationships.

Joining rooms together in a connectionGroup has three main effects:

> PUT BALL ON TABLE
(first moving over to the ball, then taking it, then moving over to the table)
Done.

The player will then end up once again in northHall at the end of the turn.

Connections between Rooms in the same connectionGroup

There are two main "feels" of spacially connected rooms, determined basically by how compass-moves are handled. We would say that the hallway is not "closely connected": the two rooms act like normal IF rooms insofar as they feel separated and discrete. This is because the rooms are connected compass-wise: moving north and south means moving within the space.

But we can also connect rooms "closely" -- so that moving within the space is always performed implicitly. In this case we reserve the standard compass connections for leaving the closely connected multi-room space altogether. The kitchen is an example of such a closely-connected space.

Loosely connected Hallway Closely connected Kitchen
Hall (southern end)
The front door is to the south, and the hallway continues northward.
There's a table here.
In the northern end of the hall, you see a ball.
> EXITS
Obvious exits lead north, to the northern end of the hall; and south.
> TAKE BALL
(first going over to the ball)
Taken.
> SOUTH
You walk into the southern end of the hall.
Kitchen
The entrance to this large square kitchen is through the white door immediately to the south. A large wooden table stands near the west wall, opposite the stove.
> EXITS
Obvious exits lead south, back to the north end of the hall; and east.
> STAND NEAR STOVE
Okay, you are now standing near the stove.
> EXITS
Obvious exits lead south and east.
> SOUTH
(first going over to the hall door)
You walk into the northern end of the hall.

As you can see, implicit movements are automatically performed as appropriate in either case, but with the closely-connected kitchen, explicit compass-wise movement takes the player out of the space, while in the hallway compass-wise movement is one mechanism for moving within the space.

If you wish to create the sense of a "close space" in which rooms are connected by close proximity rather than compass-wise, movement between the linked rooms in the space will be effected only by spacial commands such as MOVE NEAR so-and-so, or as implicit commands executed as the actor performs other actions (e.g., taking an object located in another room in the connectionGroup).

If you wish to leave out all the internal connections, you will find it convenient to define the external ones on a class, and use that class to create all the rooms within the same connectionGroup.

class KitchenRoom: Room
    east = gardenByBackDoor
    south = northHall
    connectionGroup = KitchenGroup
;

With this definition, moving EAST from any KitchenRoom will take us into the garden, while moving SOUTH from any KitchenRoom will send us back into the hallway.

The advantage of doing it this way, without implementing internal compass connections between rooms of the same connectionGroup, is that the rooms so connected will feel more like a single room.

Note that when a room is not connected compass-wise to other rooms in its connectionGroup, we assume that these unconnected rooms are contiguous. Thus, when we need to perform an implicit move to an unconnected room in the group, we simply move the player directly from his current location into the new location.

By contrast, when a room is connected compass-wise to other rooms in its connectionGroup, we implicitly move the player through each of the intervening rooms between his current room and the final location, as appropriate. The player will move along the shortest path, which we discover from an internal path-finding algorithm.

If for whatever reason you wish to disable intermediary moves, so that all implicit travel is direct, you can set the flag SpaceConnector.allowIntermediaryMoves = nil. You can switch this on a per-connector basis. This flag is on (intermediary moves are enabled) by default.

It is permissible to mix the two types of connection groups, and indeed we only distinguish them for aesthetic reasons. Let us take three examples:

(In this and the following diagrams, round shapes represent connection groups, and boxes represent rooms; the lines connecting boxes represent compass-wise room connections.)

In the left side case, Room 1 is implicitly connected to all the other rooms in the connection group. If the player is in Room 2, and implicitly moves to Room 3, he will make an intervening implicit move through Room 4, because he will follow the compass-wise connection path if such a path is present. But if a player is in Room 1, any implicit move he makes to any room in the map will move him directly to the target room.

In the center case, Room 1 and Room 3 are in the same connection group, and they are indirectly connected (compass-wise) via Room 2, which itself is outside the connection group. The player may implicitly move between Room 1 and Room 3, without taking an intermediary move through Room 2. This is because Room 2 is outside of the connection group, and therefore does not allow implicit movement; because Room 1 and Room 3 are in the same connection group, we assume that implicit movement is permissible between them.

By way of review, we consider the the right side case: rooms 1 and 2 are connected compass-wise, and rooms 3, 4, and 5 are connected compass-wise. An implicit move from Room 3 to Room 5 will make an intermediary move through Room 4. But an implicit move from Room 1 to Room 5 (for example) will not require any intermediary move: the two rooms are not connected compass-wise, so we assume they are contiguous.

If there is no compass-wise connection (either direct or indirect) between two rooms within a single connection-group, it is assumed that the rooms are contiguous, and any implicit move between them is therefore a direct move from the one to the other -- with no intermediary moves necessary.

So, to model rooms that cannot be reached from each other, but which can be seen from each other, you must join them by a DistanceConnector rather than by a SpaceConnector: the SpaceConnector always allows implicit travel between the rooms it connects.

Extra Approach Rooms

Normally the approach verbs ("WALK OVER TO", "STAND NEAR", "SIT NEAR" etc.) are only allowed for travel between locations within the same connectionGroup. Exceptionally, however, you may have a landmark item such as a large tree, visible through a SenseConnector, but not within the same connectionGroup, that it would be convenient to allow approach to using these verbs.

The extraApproachRooms property (defined on the Thing which permits actors to approach it) allows you to set this up; it should contain a list of the rooms (outside the connectionGroup, if any) from which the object can be approached using the approach verbs.

Note that you would not not normally want to include a room in this list that was not adjacent to the room containing this landmark object, unless you were willing to allow the travel to bypass the intervening rooms.

Note also that adding a Room to the list of extraApproachRooms will not cause an implicit approach action from one of those rooms; approach must be explicit. If you want to enable implicit travel, you should instead use the normal connection group technique.

Overlapping Connection Groups

ConSpace allows a room to participate in multiple connected spaces. This can be useful for either partially or fully overlapping spaces, such as where a closely-connected space exists within a larger space connected compass-wise, or where a "border location" between two connected spaces may participate in each.

Unfortunately this can be a bit confusing for the author, so we urge you to keep in mind what SpaceConnector (as a subclass of DistanceConnector) actually does, so you don't accidentally connect rooms that aren't supposed to be connected. For example, perhaps we might want to spacially connect the hallway and the kitchen, and the garden and the kitchen. The correct way to do this is to add gardenGroup and hallGroup to the KitchenRoom.connectionGroup list. In this case one might accidentally make the error of adding kitchenGroup to GardenRoom.connectionGroup and to HallRoom.connectionGroup. But in this case, we would have made all three groups interconnected, since the kitchenGroup SpaceConnector (which is a subclass of DistanceConnector) would be present in all three groups of rooms.

The rule is simple enough: any room which maintains a SpaceConnector will be spacially connected to all other rooms which maintain that SpaceConnector.

Where a room participates in multiple connection groups (that is, when there's more than one SpaceConnector located in the room), the player may move implicitly (or by STAND NEAR, etc.) to any other room which participates in any of those connection groups. Note, however, that ConSpace-enabled travel does not chain through connection groups, but only allows implicit travel so long as the starting and target location share some connection group.

So, for example we have three rooms, Room1, Room2, and Room3, and two connection groups, GroupA and GroupB. Room1 participates in GroupA; Room3 participates in GroupB; and Room2 participates in both groups (so the two groups partly overlap).

When the player is in Room2, the player can "STAND NEAR" (or implicitly interact with) any object located in either group. But if the player is in Room1, he can only "STAND NEAR" an object located in Room1 or Room2. Room3, and all objects therein, are outside of the player's current space.

There follows a review and extrapolation from what we already know. We will introduce a more complicated situation, to ensure that the system's implications are clear.

First, we recall that whenever two rooms share a connection group, implicit moves between those rooms are permitted. If there is a compass-wise path that connects the two rooms, and which remains within the connection group, then this path is followed. If there is no path between the rooms which remains within any of the rooms' shared connection groups, then we assume the rooms are contiguous, and we allow direct implicit movement between the rooms.

So let us turn to our next example, on which we make a number of observations:

Room 1, while it is connected compass-wise to Rooms 6 and 7, does not share a connection group with either of those rooms, so implicit movement from Room 1 to (or through) Rooms 6 and 7 is not permitted. However, Room 1 is implicitly connected to all the rooms in Group A: Rooms 2, 3, and 5. So we allow (direct) implicit movement from Room 1 to any of the other rooms in Group A.

Room 2 and Room 1 are indirectly connected compass-wise, via Room 7. But if we take the path via Room 7, we travel outside of the connection group common to Room 2 and Room 1. Therefore, when the player moves implicitly between Room 2 and Room 1, this move is direct. Similarly, when the player moves implicitly from Room 2 to Room 9, the move is direct, for there is no compass-wise path which connects 2 and 9, and which remains in a connection group common to both rooms. However, when we have an implicit move from Room 2 to Room 5 or 8, there will be an intermiedary move via Room 7, because in both cases the compass-wise path remains within Group B.

Room 4 is not connected compass-wise to any other room, so Room 4 is implicitly connected to all the rooms in Group B: Rooms 2, 5, 7, and 8.

Room 5 is connected to all the other rooms. The player would move from Room 5 to Rooms 1, 3, 4 and 6 directly, because in each case, there is no compass-wise path which remains in a common group (or, in the case of Room 4, no compass-wise path at all). Implicit travel from Room 5 to Room 2 would mean taking an intermediary move via Room 7; similarly, implicit travel to Room 9 would take an intermediary move via Room 8. The direct compass-wise route would be followed for traveling to Rooms 7 or 8.

Side-Effects of Direct and Intermediary Travel

ConSpace is perfectly consistent with the framework which the main library provides for travel. ConSpace does, however, make some assumptions about spacial contiguity and accessibility, as we have remarked: if rooms are connected by a single SpaceConnector, we assume that they are accessible to each other, for the purposes of travel. So, implementing travel barriers or doors within (as opposed to between) connected spaces -- this calls for particular care, and is probably not a good idea in general. Again, if you wish to produce a sense of spacial connection between rooms, but wish to disallow movement between those rooms, DistanceConnector is required, and SpaceConnector is probably unsuitable.

Whenever an actor enters or leaves a room, the associated notifications (as determined by the main library) are performed. Naturally, this applies the same for ConSpace-enabled moves, including implicit and intermediary moves.

ConSpace provides two additional notifications for travel, when that travel involves the actor entering or leaving a connected space. Note that these notifications are not called when the actor is merely moving within the space. These methods are invoked in the relevant SpaceConnector(s):

SpaceConnector also defines two properties that control whether or not a lookAround is performed when moving to and between connected spaces:

SpaceConnectorDoor

As a convenience for the game author, the class SpaceConnectorDoor is provided. In a game that makes wide use of ConSpace, a door will often wish to behave by spacially connecting the locations on either side of the door when the door is open, and close this spacial connection when the door is closed. This functionality is provided by SpaceConnectorDoor. Simply set the (master-object) door to inherit from SpaceConnectorDoor, and everything is taken care of automatically.

Some care is needed with the use of SpaceConnectorDoor, since opening a SpaceConnectorDoor is effectively like removing a partition between two rooms. If you have a room divided by a partition, like the ballroom in the sample game, this may be just what you want. On the hand, making the front door in the sample game a SpaceConnectorDoor would have been disastrous, because it would make it feel like the front wall of the house had just collapsed. The SpaceConnectorDoor between the kitchen and the hall works okay because the location it connects to on the kitchen side is explicitly just the part of the kitchen immediately adjacent to the door. This is probably the best way to set up a SpaceConnector door between discrete rooms.

SpaceConnectorDoor defines the property lookAroundOnTraverse, which is true by default. Normally we want to perform a lookAround when the PC moves through this door when the door is opened. If you don't the lookAround, set lookAroundOnTraverse to nil

SpaceConnectorDoor inherits from Door.

Dynamically Altering Connection Groups

ConSpace provides a suite of methods for runtime modification of spacial connection groups. These methods are defined by the SpaceConnector class:

Defining Proximity

The ConSpace extension defines four additional properties on Thing:

These point to the locations that are considered to be near, under, behind and in front of the Thing. By default, nearLoc is defined as getOutermostRoom and the other three are nil. When these properties refer to Rooms they are only really useful when the Thing is situated in a room that's in a connectionGroup. For example, the two-room hall example would allow a command like GO NEAR RED BALL to take the player character into whichever of the two rooms the red ball was currently in.

The default definition of nearLoc is what you want for most Things, whether the Thing is supposed to be reachable from its nearLoc or not. The other three properties are useful for objects that represent landmarks in a particular room. For example, if there is a clock fixed to the wall of northHall, you might set the clock's underLoc property to northHall so that STAND UNDER CLOCK would take the player character to northHall.

Note that we do not have an "inLoc" or "onLoc": such locations are modeled by ECC, as explained a couple sections below.

Nested Space

Sometimes it is most useful to have NEAR, UNDER, BEHIND or IN FRONT OF refer to a NestedRoom rather than a Room within the connected space.

For example, you might want to set things up so that the player has to put a box near the gate before standing on the box and then climbing over the gate. You could always define a nearGate room linked to the larger connectionGroup, but it might be easier and neater simply to define a NestedSpace object to represent the space near the gate.

You can set this up using an anonymous NestedSpace object.

gate: Door ...
    nearLoc: NestedSpace { }
;

If you use a NestedSpace with underLoc, behindLoc or frontLoc, you can either set its objInPrep to something appropriate, or use one of the NestedSpaceUnder, NestedSpaceBehind or NestedSpaceFront classes, which do this for you. Either way, it's very simple: the following two properties are identical in practice:

underLoc: NestedSpaceUnder { }
underLoc: NestedSpace { objInPrep = 'under' }

Or if you want to get a little fancy:

underLoc: NestedSpace { objInPrep = 'beneath' }

Please note especially: using a NestedSpace with a Thing, or with a Thing's underLoc, behindLoc and frontLoc properties, is probably only meaningful if the Thing is fixed in place (that is, if it inherits from NonPortable). If you wish to enable things to be under or behind other things that can move around, you're probably better off using a ComplexContainer or ECContainer.

A final issue is how commands like OUT and GET OUT should be handled when the player character is in a NestedSpace. Depending on context you may want such commands to remove the PC from the NestedSpace (thus being taken equivalent to GO AWAY FROM), or else to be treated as a command to leave the NestedSpace's container. By default OUT and GET OUT will take the PC out of the NestedSpace. If you want the other behaviour (leaving the room or whatever), just set the NestedSpace's out property to nil.

ECContainer (Enterable Complex Container)

The library defines a ComplexContainer class that allows an object that things can be put in, on, behind or under. The library also defines NestedRoom, so actors can enter sub-locations within a room. Enterable Complex Container (ECContainer) melds these two ideas.

Actors can enter an ECContainer in the same way that objects can be placed in a ComplexContainer: actors can get in, on, under, or behind the ECContainer object.

The ECContainer works much like a ComplexContainer, and is fully compatible with the normal ComplexContainer. You can use normal ComplexComponents for any of its sub-locations.

The difference is that its subXxx components can be:

With normal complex components, it's necessary to use mixed inheritance in the definition. But these four enterable complex component need not be mixed with any parent classes (ComplexComponent or NestedRoom or whatever). Each of these four classes defines everything needed, so no mix-in classes are necessary.

For example, to make a desk you can stand on, but cannot stand in, you could write:

desk: ECContainer 'desk' 'desk'
subContainer: ComplexComponent, Container { /* etc. */ }
subSurface: ESubSurface { etc. }
;

Again, note that the "normal" complex component, the subContainer, requires mixed inheritance, while the enterable complex component inherits singly from, in this case, ESubSurface.

For another example, to make an object you can stand in, on, under, and behind, you could do this:

gym: ECContainer 'gym' 'gym'
    subRear: ESubRear {}
    subUnderside: ESubUnderside {}
    subSurface: ESubSurface {}
    subContainer: ESubContainer {}
;

ECContainer works just like ComplexContainer, but allows actors to enter the subcomponents of the container.

When to use ECC rather than ConSpace/NestedSpace

The space under or behind the same object cannot be modeled by both a ConSpace/NestedSpace and a complex container. Its subXxx properties would interfere with its xxxLoc properties.

Whether to use ConSpace or ECC depends on the nature of the spatial relationship, and in particular whether its spaces are intrinsic or merely incidental. If the object can be moved, you definitely want to use ECC. If it's immobile, you can use either but not both.

For example, a bed might want a surface and an underside, so that you could lie on and climb under the bed. If you want the underside and topside of the bed to be separate rooms in the connection group, that will work fine. But in this case you might want the bed to be an ECContainer instead.

It is intrinsic to the IN and ON relationships that the space so designated relates to a particular object: ON THE TABLE or IN THE OVEN always refer to spaces defined in close relation to the objects named. In such cases the spaces need to be contained by the object (as a ComplexContainer), and the ECC is needed if an actor can enter one of them.

On the other hand, if the red ball happens to be located at the north end of the hall (as in the test game), the fact that STAND NEAR RED BALL takes you to the north end of the hall is incidental to the object. Any other object located at the north end of the hall would have served equally well as a reference point, and at any point the red ball could be moved so that NEAR RED BALL no longer referred to the same location. In this situation there's no point at all having a 'near red ball' NestedSpace attached to the red ball: the situation is modelled far better with the ConSpace approach.

Some cases can be handled by either approach, and which one uses may be partly a matter of taste, and partly depend on the exact effect desired.

The rule of thumb is:

Sense Connector Filter (SCfilter)

The ConSpace extension includes the file SCfilter.t, a patch for the main Tads-3 library. This patch is entirely optional.

The patch eliminates some time wasted in low-level library processes. The main library is normally somewhat inefficient in its handling of complex spacial arrangements in large maps. With this patch the library will serve huge and elaborate maps exactly the same as usual, but with far less computation overhead.

This patch will not effect your game's behavior in any way whatsoever. ConSpace works the same whether you're using this patch or not, and you can even use this patch if you are not using ConSpace.

If the map of your game becomes very spacially complex, and especially if you therefore encounter any noticable lagtime between game prompts, you may wish to use this patch. Simply include the SCfilter.t file anywhere in your list of source files (after the main library).

Sample Game (ConSpaceTest: Find the Ring)

The sample game included with this distribution is utterly trivial as a piece of IF, but you may like to play it to get a feel for what ConSpace and ECContainer do. Try commands like PUT RED BALL IN BOX from the southern end of the hall; or GET UNDER BED upstairs in the bedroom. Experiment with moving around within the kitchen, the hall, and the garden. You can also experiment with giving orders to Bob.

Please also examine the source code of the sample game to see some examples of how the extension features described above can be used in practice.


Eric Eve & Steve Breslin
2004-2005