Addendum- understanding the behaviour of Inform’s spatial relations.
This is an addendum to my earlier post above regarding Inform 7’s spatial relations.
That has also been edited to:
(i) clarify and expand some passages
(ii) correct a number of inaccuracies, particularly with regard to regions
(iii) add some further useful undocumented information
Although there are a number of quirks to the implementation of Inform’s many spatial relations, in large part these are explicable by an understanding of the underlying implementatation model.
Objects in the Inform World Model are arranged into an Object Family Tree, within which any object can only be directly connected to one parent, sibling and/or child. This simplified description does not take account of incorporation, which uses similar ideas but is differently implemented and will be discussed later. Furthermore, in practice the Object Tree is typically broken up into a number of separate trees, usually with a room as the ‘top’ parent in each, and sometimes even with isolated objects that have no parents, siblings or children.
Library Nebulous Thingumajig
Bookcase----->Writing Desk----->Sherlock Holmes----->Green Door
| | |
| <-contains | <-supports |<-carries
v v v
Dictionary Lamp Pipe----->Deerstalker(worn)
In the Object Tree, objects are connected only in these three basic correspondences- parent, sibling and child. Although formally an object only has one child and/or sibling by direct connection in the Object Tree, they are also talked about as groups of siblings and children, each in a group of siblings having the same parent, of which they are the children. In the above diagram, the child of the Library is the Bookcase and the children of the Library are the Bookcase, Writing Desk, Sherlock Holmes and the Green Door. The sibling of Sherlock Holmes is the Green Door and the siblings of Sherlock Holmes are the Bookcase, Writing Desk and Green Door. The parent of Sherlock Holmes is the Library. Although hidden deep within Inform 7, the structure of the Object Tree occasionally reveals itself. For example, when listing miscellaneous items in a room description (
'You can also see...'), these are generally listed with the ‘eldest’ child of the room location first, then in sibling order. The ‘eldest’ sibling is the one most recently moved to its parent. When an object is moved to a parent, it therefore becomes the child of that parent, and the previous eldest child becomes its sibling. Knowing this allows the order of printing of miscellaneous items to be controlled- even if an object is already in the location, moving it to the location moves it to the top of the list for printing. You will notice that taking then dropping an object in the location has the same effect, and for the same reason.
Note that rooms do not have siblings or a parent. They are connected to other rooms by mapping relations and to regions via their ‘map region’ property, neither of which are part of the Object Tree.
Considering how parent, child and sibling connections are manifest in Inform’s basic spatial relations, the latter are determined by the kind of the parent object. A room or container’s children are contained by it. A supporter’s children are supported by it. A person’s children are carried by it, unless they have the I6 ‘worn’ attribute (in I7 terms, somebody wears them), in which case they are worn rather than carried. Put the other way round, something is contained if it is one of the children of a room or container, supported if it is one of the children of a supporter and worn or carried if it is one of the children of a person. An object such as the Nebulous Thingumajig in the above diagram is said to be ‘off-stage’ or ‘nowhere’.
It should now be clear why the basic spatial relations covered so far are mutually exclusive. Since the only way to know how a child relates to its parent is the kind of its parent, the parent cannot be allowed to be more than one of a room, a supporter, a container or a person. If an object were to be at the same time a supporter, a container and a person, Inform has no way of knowing if its children are being supported, contained or carried. Furthermore, an object cannot be both worn and carried, because the state of the either-or attribute ‘worn’ determines which it is. Also, since an object can have only one parent, it cannot be contained/supported/carried or worn by more than one object.
Small print Note that while in theory canonical, the restriction that the a parent must contain, support, wear or carry one of its (unincorporated) children by being one of the relevant kinds is currently only rigorously enforced when the world is being initially set up, or when objects are placed or moved through actions- such as typing ‘Put the cat in the aspidistra’ or code like ‘try putting the cat in the aspidistra’. Writing ‘The cat is in the aspidistra’ for example automatically creates the aspidistra as a container, even if elsewhere it is defined as a thing.
However, even if the aspidistra is none of container, supporter, room or person, placing or moving things directly through code once play is underway- through phrases such as ‘now the cat is in the aspidistra’ or ‘move the cat into the aspidistra’- is allowed (when it probably shouldn’t be), and this case results in the poor beast becoming trapped inside the herbaceous perennial. The cat ends up held and enclosed by the aspidistra- but not contained, supported, carried, worn or incorporated (since its parent is none of the relevant kinds and no incorporation relation has been created). In fact, code similar or equivalent to ‘Now the aspidistra carries…, Now the aspidistra wears…, Now the aspidistra contains…, Now the aspidistra supports…, Now the aspidistra has…, Now the aspidistra holds…’ are all interchangeable and simply insert the cat as a child object of the aspidistra without checking which of Inform’s spatial relationships are being created!
Given that an object that is not a container cannot be made transparent, the unfortunate feline remains forever unreachable and invisible- like a fossil concealed inside a rock- unless the deus ex machina of further code releases her. The game effect is similar to sealing the animal inside a closed, opaque, unopenable container. Quite apart from the interest the RSPCA might take in the situation, it should go without saying that this kind of behaviour is not encouraged.
<-->Estate Nebulous Thingumajig
r . | <-holds, contains & encloses-++++++++++
e . v |
g <->Mansion-------------------->Gardens encloses
i . | v
o . | <-holds, contains & encloses-++++++++++
n . v
a <->East Wing------------------>West Wing
l . .
l . . (via 'map region')
y . . <-contains (**but does not hold**)
o . |
n . | <-contains & holds
t . v
a .->Bookcase----->Writing Desk----->Sherlock Holmes---->Green Door
i . | | |
n . | <-contains | <-supports |<-carries/wears
s . | & holds | & holds | & holds & has
. v v v
.->Dictionary Lamp Pipe---->Deerstalker(worn)
Above is the previous object tree extended to show some regions. Regions exist in one or more object trees of their own, separate from other objects. In Inform, without exception every parent holds each of its children. Therefore rooms, containers, supporters and people hold their children as well as containing/supporting/carrying or wearing them. It also follows that regions hold their child regions, but do not support/carry or wear them. Regions also therefore enclose (directly or indirectly hold) their child regions and their grandchild regions etc. but never rooms, since regions can never hold a room.§ Although regions do contain any regions they hold, testing for this in Inform is awkward and often confusing due the the ambiguity (between containment and regional-containment) of the terms ‘in’ and ‘contains’ when talking about regions (see post 7 above). Generally it’s best to use the holding relation for regions.
§Since Ver.10 of Inform 7, regions also directly or indirectly regionally-contain any regions they enclose, and (somewhat unexpectedly) themselves- a fact now reflected by the two-way arrows in the left margin adjacent to the regions in the above diagram.
Object trees of regions are connected to rooms and rooms’ object trees not by parent/child connections but through a property provided by every room- its map region. A room’s map region may be nothing, in which case it is not in any region, but if the property is a region, that establishes a connection which by a special rule means the said region contains that room. This is the only situation in which an object contains something it does not hold. It also means that the same region, and that region’s parent (if any), and grandparent, and great-grandparent etc. regionally-contain the room and the room’s entire object tree. It does not mean that the region holds the room- it does not- holding applies only to parent/child connections.
: | <-supports
: | & holds
: <-incorporates & holds
Left Drawer==========>Right Drawer====>Inkwell
: | |
: | <-contains & holds | <-contains & holds
: v v
: Pen Ink
: <-incorporates & holds
| <-contains & holds
It remains to describe the implementation of incorporation. Above is an expansion of the Writing Desk encountered in the preceding example object tree. As well as supporting the Lamp, it incorporates two drawers and an Inkwell, containing some Ink. The left drawer contains a Pen but also itself incorporates a Secret Compartment, which contains a Key. Thus an extensive additional object tree is hung off the Writing Desk through the top-level incorporation relation. This does not break the object tree rule that an object has at most one child by direct connection, because the incorporation relation is not implemented within the main object tree model but through three I6 properties provided by things that either incorporate something or are part of something else: component_parent, component_sibling and component_child. These properties do exactly what their names suggest- they hold a reference to the thing (if any) that is the property-providing thing’s parent, sibling or child by incorporation. These relationships are shown by double-dotted lines in the above object tree- so the Left Drawer has component sibling of the Right Drawer, component parent of the Writing Desk and component child of the Secret Compartment. Although incorporation is implemented slightly differently and separately to the main object tree, it remains the case that a component parent holds its component children (as well as incorporating them) and that an object can have only one parent, which holds it, either through one of the four types of relation represented by the main object tree (containment/support/carrying or wearing) or through incorporation. Similarly, objects enclose all objects within an object tree they incorporate, so in the above example the Writing Desk encloses the Ink. Indeed, so does the Library- the ‘cascade of enclosure’ is not broken by incorporation.
It should now be evident how Inform’s holding and possession relations work: holding exactly represents any parent/child relationship (including being a parent/child through incorporation), and enclosure any unbroken cascade of parent/child relationships (including through incorporation). Possession represents the parent/child relationship (not including incorporation) between a person and its children.
Furthermore, the Object Tree sheds light on the somewhat arcane I7 phrases ‘the first thing held by…’, which equates to ‘the child of…’ and ‘the next thing held after…’,which equates to ‘the sibling of…’. In particular, it explains why, unlike other forms of ‘holding’, ‘held’ in these phrases does not include incorporated things.
Understanding the Object Tree also provides some insight into the quirky spatial relationships of doors and backdrops. An object can only ever have one parent and therefore can only occupy one place in the object tree. How then can doors and backdrops appear in more than one room? The answer is ‘by sleight of hand’. Inform keeps a record of (or knows how to work out) which rooms a door or a backdrop are found in, and if the player enters one of those rooms, quickly moves all relevant doors and/or backdrops to the player’s location before anything is printed, creating the illusion that they had been there all along. This quick-change-act is ordinarily only triggered when the player moves to a new location, which for doors (being always static) is fine, but it can be caught out if between moves of the player some condition changes that means a backdrop should now suddenly be present in or absent from the player’s current location (e.g. the moon coming out from behind a cloud). This is the reason that Inform provides the phrase ‘update backdrop positions’ so that backdrops can be moved around the object tree between turns or even mid-turn if necessary. All this explains why despite being found in more than one room, a door or a backdrop has, at any given time no more than one room which holds and contains it. That will usually be the most recent location the door/backdrop was moved to- being generally the last room occupied by the player in which the door/backdrop is to be found. As noted previously, a backdrop is the only kind of object not to be enclosed by an object it is held by.
Very small print- location of backdrops With the location of two-sided doors and backdrops, especially backdrops, things get really weird. For small print on the location of doors, see the previous post. The I7 documentation states that backdrops have no location, but that isn’t true in the sense that ‘the location of a-backdrop’ is rarely nothing. To understand what’s going on, it needs to be appreciated that which rooms a backdrop (or two-sided door) is found in is information stored in an I6 property array called found_in. A two-sided door has an array with two entries, corresponding exactly to the front side and the back side of the door. For a backdrop, in the simplest case the array comprises a list of named rooms derived from assertions in the source such as ‘The moon is in the Skylight Room and the Carpark’. → I6 ‘with found_in skylight_room car_park,’. However, instead of being just one room, an entry in the array can instead reference an I6 routine that when called returns true if the player’s location is among one or more different rooms- similar to e.g. ‘with found_in [; if (location == skylight_room or car_park) rtrue; rfalse;]’. Such a routine will usually be the first element of the array, and in deciding during play whether to move a backdrop to the current location in the object tree, if Inform finds a routine as the first element of the array it stops there and doesn’t consider further entries. Conversely, when determining ‘the location of’ a backdrop, if the backdrop hasn’t already been moved to the player’s current location Inform will cycle through all the entries of the found_in array until it finds either (i) a routine that returns true for the player’s current location, or (ii) any simple named room.
During setup, if at least one statement defining where a backdrop is found is implemented as a routine (i.e. the backdrop is found in at least one region) the compiler rolls all such statements together into one routine and found_in only ever has one entry with one routine (the initial one or a replacement during play). Note that all statements redefining in play where a backdrop is found are implemented as a routine: (‘move the (a-backdrop) backdrop to all (description-of-rooms)’, ‘move the (a-backdrop) to (a-region)’, ‘now the (a-backdrop) is everywhere’)- a reference to this routine replacing the first entry in the found_in array. The exception is ‘now the (a-backdrop) is nowhere’, which moves the backdrop out of the object tree and sets its I6 ‘absent’ attribute to true, leaving found_in unchanged.
This means something odd happens if during setup found_in is created with multiple entries as a list of room names,e.g. (with found_in skylight_room car_park,) but then the backdrop is moved with e.g.‘move the moon backdrop to all windowed rooms’: now found_in still has two entries and looks something like ‘found_in <routine returning true when player’s location is a windowed room> car_park’. This leads to the following anomaly: when the player is not in a windowed room (e.g. in the Cellar, or the Car Park) the backdrop is not present, as the backdrop position updating routine bails after failing to find a match to the player’s current location from the routine referenced by the first entry, but ‘the location of’ the backdrop is the Car Park, because in determining location, having failed to match a room corresponding to the player’s location in the routine referenced by the first entry, Inform moves on to found_in’s next entry and, finding a simple room name, returns that- whether or not the player is actually in the car park.
The rules determining the location of a backdrop can be summarised thus:
- if absent → nowhere
- if currently in the room enclosing the player → that room
- otherwise → the default room found in
- if no I6 found_in property → nowhere
- if no room matched by found_in → nowhere
- 1st room matched by I6 found_in property
- LocationOf() iterates through all entries of the found_in property until (i) a routine matches with any room or (ii) it’s a simple room name: this can lead to unexpected results when the first entry has been changed to a routine that matches no room, as location may be returned from the second or subsequent entries as a room the backdrop is no longer found in
- this outcome is made more likely by a bug with how ‘move a-backdrop backdrop to all a-description-of-rooms’ routines are implemented. found_in routines are supposed to consult the I6 global variable ‘location’ to match with, but I7 currently implements these particular routines such that they need to be called with the room to match with as a parameter. LocationOf() depends on the former mechanism when evoking found_in routines, consequently in I7 these ‘a-description-of-rooms’ found_in routines always return false to LocationOf() because it calls them with no parameters.
- in the case where found_in has the value FoundEverywhere- a routine that simply returns true- the room enclosing the player will be returned under the previous rule
- otherwise → nowhere
NB the showme command doesn’t use LocationOf() but iteratively prints the object tree and/or component tree enclosing the object- or if the object is at the top of an object tree, prints “out of play”. This means that in the case of a non-absent backdrop not yet moved to a location, showme indicates its location as ‘out of play’ while LocationOf() returns the default location.
Further small-print - off-stage and nowhere for backdrops It might be supposed that ‘is off-stage’ and ‘is nowhere’ are exact equivalents, but in the case of backdrops that’s not the case. ‘some-object is nowhere’ is exactly equivalent to ‘the location of some-object is nothing’ whereas on-stage is defined by these rules:
- if not a thing → no
- if a door → yes
- if a backdrop:
- if absent → no
- otherwise → yes
- if enclosed by a room → yes
- otherwise → no
In the case of an non-absent backdrop, it will be on-stage but may still have no location e.g if found_in comprises solely a routine that doesn’t match the player’s current location (see above)- in which case it will be on-stage but nowhere. Furthermore, the meaning of ‘out of play’ when location is shown by the ‘showme a-backdrop’ command has a different meaning again- as indicated above it means ‘the ultimate (in)direct holder is not a room’ which in the case of a backdrop means it is removed from the object tree. In summary, for a backdrop
- off-stage == ‘absent’ attribute set (in I7 this results from ‘(now) a-backdrop is nowhere’)
- nowhere == ‘location of a-backdrop is nothing’
- ‘showme’ out of play == ‘a-backdrop is not (in)directly held by a room’
use of ‘location of a-backdrop’ and ‘a-backdrop is nowhere’ It’s probably advisable to avoid both these usages, since the behaviour of the former is difficult to predict and the latter depends on the former.
Addendum to the addendum: some useful undocumented phrases involving spatial relations:
the component parts core of (an object) is the enclosing object (thing, container, supporter or person) most closely related to it solely through direct or indirect incorporation- in other words, the object it is ultimately directly or indirectly a part of. For example, if a button is part of a control panel, which is part of a remote control, which is in a drawer, which is itself part of a cabinet, which is part of a bookcase in the Library, the component parts core of the button would be the remote control. The component parts core of the drawer is the bookcase. The idea here is that parts of things are considered to be represented by the whole and here the button is ultimately a part of the remote control. The component parts core of an object which is not a part of anything is itself. The component parts core of nothing is nothing.
the not-counting-parts holder of (a thing) (note the hyphens) is the object (thing, container, supporter, person or room) most closely related to it through direct or indirect holding, but skipping over any intervening things related by incorporation. In other words, the not-counting-parts holder of a thing is the component parts core of the holder of its component parts core. Again, the idea is that parts of things are considered to be represented by the whole. In the above example, the not-counting-parts holder of the button is the bookcase. The button is ultimately a part of the remote control, which is held by the drawer which is ultimately part of the bookcase. If nothing holds the component parts core of something, the not-counting-parts holder of it is nothing.
the common ancestor of (one object) with (another object) (note ‘with’ not ‘and’) is the object (room or thing or region) most closely related through holding to both items. For example, if a hat is worn by a cat, which is in a cage, which is part of a display case, which is on the bookcase described in the previous paragraph, which is in the Library, the common ancestor of the hat with the button will be the bookcase. If a display is part of the remote control, the common ancestor of the button with the display will be the remote control. The common ancestor of the hat with the cage is the cage. If there is no common ancestor, because for example they are things in different rooms, or one or the other is off-stage, the common ancestor is nothing. The common ancestor of something with nothing is nothing. The common ancestor of something with itself is itself.
As there is no holding relation between rooms and regions, a region cannot be the common ancestor of a room or anything enclosed by a room- a region can only be the common ancestor of two regions.
The visibility-holder of (an object) If the object is a closed, opaque container then its visibility-holder is nothing. Otherwise, it is the not-counting-parts holder of the object. The idea is that starting from the standpoint of a given object, one can move up through the hierarchy of visibility-holders until reaching one whose visibility-holder is nothing. That final visibility-holder provides the outer limit of visibility (called the visibility ceiling) from the standpoint of the original given object, which will be either the location or a closed, opaque container enclosing the original given object. The visibility-holder of nothing is nothing.
This idea is used by the Standard Rules when determining what of her surroundings the player can see- usually in preparation for printing locale descriptions- using the phrase ‘Calculate visibility ceiling at low level’ in which case the ‘original given object’ is the player and the results are stored in a variable called ‘visibility ceiling calculated’. Also, the number of steps taken to get from the player to the ‘visibility ceiling calculated’ is stored in the variable ‘visibility ceiling count calculated’. In the case of the player being directly held by a lighted location, the visibility ceiling calculated is the location and the visibility ceiling count calculated will be 1. If the player is on a table inside an open cage in the location, visibility ceiling calculated is the location and the visibility ceiling count calculated will be 3. If the player is standing holding a light source on a chair inside a closed, opaque cupboard in the location, visibility ceiling calculated is the cupboard and the visibility ceiling count calculated will be 2. If the player gets down off the chair, visibility ceiling calculated is the cupboard and the visibility ceiling count calculated will be 1. If the player then opens the cupboard, the visibility ceiling calculated is the location and the visibility ceiling count calculated will be 2. In the special case of the player being in darkness, the visibility ceiling calculated is the darkness object and the visibility ceiling count calculated is 0.
The touchability ceiling of (an object) is the enclosing object (room, supporter, container or person) most distantly related through touchability. This is calculated in a similar way to the visibility ceiling (see previous paragraph) except via a hierarchy of not-counting-parts holders that the ‘reaching outside’ rulebook decides can be reached out through. By default, only reaching out of closed containers is disallowed by this rulebook, so the touchability ceiling will usually be either the location or the first closed container encountered in moving up through the hierarchy. The touchability ceiling of something whose component parts core (see above) is held by nothing (i.e. is off-stage) is its component parts core. Similarly, if the last object encountered in the hierarchy is not a room but nevertheless is held by nothing (i.e. it is off-stage) that object is the touchability ceiling. The touchability ceiling of nothing is nothing.