[I6] Scope addition from off-stage or a different room -- why the difference in takeability?

The following code

Hats and Wormholes
Constant Story "Hats and Wormholes";
Constant Headline "^(a study)^";


Include "Parser";
Include "VerbLib";
Include "Grammar";

Class Room
    has light;

Room Start "Starting Point"
    with    description
                "An uninteresting room.",
            e_to End;

Object red_wormhole "red wormhole" Start
    with    name 'red' 'wormhole',
            initial
                "A red-tinted wormhole hangs in the air here.",
            description
                [;
                    if (parent(orange_hat) == nothing)
                        print_ret (A) orange_hat, " lies temptingly on the other side.";
                    else
                        "There's nothing but some kind of void beyond.";
                ],
            add_to_scope
                [;
                    if (orange_hat hasnt moved)
                        AddToScope(orange_hat);
                ]
    has     static;

Room End "Ending Point"
    with    description
                "Another uninteresting room.",
            w_to Start;

Object blue_wormhole "blue wormhole" end
    with    name 'blue' 'wormhole',
            initial
                "A blue-tinted wormhole hangs in the air here.",
            description
                [;
                    if (parent(green_hat) == Elsewhere)
                        print_ret (A) green_hat, " lies temptingly on the other side.";
                    else
                        "There's nothing but some kind of room beyond.";
                ],
            add_to_scope
                [;
                    if (green_hat hasnt moved)
                        AddToScope(green_hat);
                ]
    has     static;

Room Elsewhere "Elsewhere"
    with    description
                "Someplace else entirely.";

Object green_hat "green hat" Elsewhere ! placed in a separate room
    with    name 'green' 'hat'
    has     clothing;

Object orange_hat "orange hat" ! placed off-stage
    with    name 'orange' 'hat'
    has     clothing;



[ Initialise ;

    location = Start;

];

generates the following behavior:

Hats and Wormholes
(a study)
Release 1 / Serial number 201125 / Inform v6.31 Library 6/11 SX

Starting Point
An uninteresting room.

A red-tinted wormhole hangs in the air here.

>X RED
An orange hat lies temptingly on the other side.

>GET ORANGE
Taken.

>E

Ending Point
Another uninteresting room.

A blue-tinted wormhole hangs in the air here.

>X BLUE
A green hat lies temptingly on the other side.

>GET GREEN
That isn't available.

Note that, although the transcript is from Inform 6.31 with StdLib 6/11, the behavior is essentially identical using Inform 6.34 with StdLib 6.12.4. (The only difference is that the final message is “The green hat isn't available.”)

The only functional difference between the orange hat and the green hat is that the orange hat is off-stage while the green hat is located in a different room. Should there be a difference in takeability between these two cases? If so, what is the rationale?

The reason in the code for the difference can be found in the routine ObjectIsUntouchable():

annotated Standard Library 6.12.4 version of ObjectIsUntouchable()
[ ObjectIsUntouchable item flag1 flag2 ancestor i; ! StdLib 6.12.4 version
    ! Determine if there's any barrier preventing the actor from moving
    ! things to "item".  Return false if no barrier; otherwise print a
    ! suitable message and return true.
    ! If flag1 is set, do not print any message.
    ! If flag2 is set, also apply Take/Remove restrictions.

    ! If the item has been added to scope by something, it's first necessary
    ! for that something to be touchable.

    ancestor = CommonAncestor(actor, item);
    if (ancestor == 0) {
        ancestor = item;
        while (ancestor && (i = ObjectScopedBySomething(ancestor)) == 0)
            ancestor = parent(ancestor);
        if (i) {
            if (ObjectIsUntouchable(i, flag1, flag2)) return;
            ! An item immediately added to scope
        }
    }
    else

    ! First, a barrier between the actor and the ancestor.  The actor
    ! can only be in a sequence of enterable objects, and only closed
    ! containers form a barrier.

    if (actor ~= ancestor) {
        i = parent(actor);
        while (i ~= ancestor) {
            if (i has container && i hasnt open) {
                if (flag1) rtrue;
                return L__M(##Take, 9, i, noun);
            }
            i = parent(i);
        }
    }

    ! Second, a barrier between the item and the ancestor.  The item can
    ! be carried by someone, part of a piece of machinery, in or on top
    ! of something and so on.

    i = parent(item);                         ! ** This is where two cases begin to differ. For both, ancestor == nothing.
    if (item ~= ancestor && i ~= player) {    ! ** orange_hat i == nothing, green_hat i == Elsewhere
        while (i ~= ancestor) {               ! ** HERE orange_hat FALSE and skips while loop, green_hat TRUE and enters while loop
            if (flag2 && i hasnt container && i hasnt supporter) {     ! ** flag2 is set TRUE by AttemptToTakeObject()
                if (i has animate) {          ! ** Elsewhere is not animate, skips
                    if (flag1) rtrue;
                    return L__M(##Take, 6, i, noun);
                }
                if (i has transparent) {      ! ** Elsewhere is not transparent, skips
                    if (flag1) rtrue;
                    return L__M(##Take, 7, i, noun);
                }
                ! Now at "default" case where parent hasnt container or supporter or animate or transparent, e.g. a room like Elsewhere
                if (flag1) rtrue;             ! ** flag1 is set FALSE by AttemptToTakeObject(), so messages allowed
                return L__M(##Take, 8, item, noun); ! ** "That isn't available." or equivalent
            }
            if (i has container && i hasnt open) {
                if (flag1) rtrue;
                return L__M(##Take, 9, i, noun);
            }
            i = parent(i);                    ! ** if all checks pass, check parent(i); this will eventually hit a room or nothing
        }
    }
    rfalse;
];

Note that, by the library logic, the status of flag2 (which signals “apply Take/Remove restrictions”) makes no functional difference for the orange hat. By law of code, things “directly” off-stage can’t have the “Take/Remove restrictions” applied, because the check for whether or not flag2 is set won’t be reached in such cases. This could be just a matter of efficiency, as nothing (the parent for directly off-stage things) can’t have any of the attributes that will be checked when flag2 is set. However, also note that things “indirectly” off-stage (i.e. part of the object tree of another off-stage object) might still fail these checks, if, for example, they belong to an off-stage person, are in a closed off-stage container, etc.

As far as I can tell in the documentation provided by DM4 and the comments in the routine itself, the reason for the behavior in the case of green_hat (parent is a room) is not covered. There is effectively an on-stage/off-stage check hidden in the structure. Note that things deemed “takeable” by the explicit logic will always end up affected by the implicit logic here, as the chain of checked parent objects will always terminate in either a room or nothing. This could be a simple case of omission in the documentation.

Throughout the following, consideration is restricted to objects for which the take/remove restrictions that are explicitly laid out in Standard Library comments (and code) do not apply.

Some questions:

  1. DM4 p. 102 (tail end of section 7.6, online at https://inform-fiction.org/manual/html/s7.html) has an example of the "That isn't available." message in action in the exact circumstances of the green_hat case (i.e. attempting to take a non-local on-stage object), so there is evidence that the behavior in this case is intentional. Given that, would it be an improvement to change the library message from the current "That isn't available." (or analog) to something like "That isn't here." so that there is more informative feedback (to both author and player) about the reason for the failure of the takeability check? And would it be an improvement to the Standard Library to update the comments in the routine to make this intentional logic explicit and explain its rationale?
  2. If the behavior in the orange_hat case is not intentional (and I have so far found no evidence that it is), would it be desirable to produce a consistent result for non-local takeability in both cases? And, if so, would it be better to attain consistency by forbidding the taking of things from off-stage or by allowing the taking of non-local on-stage things?

For the purpose of discussion, here are some relevant points from the formal definition of the world model described in DM4 section 24 [pages 176-187 or online at https://inform-fiction.org/manual/html/s24.html]:

1.2.2. A room represents some region of space, not necessarily with walls or indoors.
1.2.7. Objects out of play represent nothing in the model world and the protagonist does not interact with them. Out of play objects are mostly things which once existed within the model world but which were destroyed, or which have not yet been brought into being.
2.2.6. An object out of play is either contained in another object out of play, or else not contained at all.
3.1. Spatial arrangement on the small scale (at ranges of a few moments’ walking distance or less) is modelled by considering some objects to lie close together and others to lie far apart.
3.1.1.2. All objects with the same parent are considered to be equidistant from, and to have equal access to, each other.
3.1.2. Objects ultimately contained in different rooms are considered to be so far apart that they will not ordinarily interact. Because of this the model takes no account of one being further away than another.
4.1. The senses are used in the world model primarily to determine whether the player can, or cannot, interact with a nearby object. Three different kinds of accessibility are modelled: touch, sight and awareness.
4.2. Awareness = sight + touch, that is, the player is aware of something if it can be seen or touched.
4.2.1. Awareness represents the scope of the player’s ability to interact with the world in a single action…
4.4.1. In the light, the player can touch anything close to the player provided that (a) every object between them is see-through and (b) none of the objects between them is a closed container.
6.2. An action can involve no objects other than the player, or else one other object of which the player is aware, or else two other objects of which the player is aware.
6.2.1. The following actions fail if the player cannot touch the object(s) acted on: taking, dropping, removing from a container, putting something on or inside something, entering something, passing through a door, locking and unlocking, switching on or off, opening, closing, wearing or removing clothing, eating, touching, waving something, pulling, pushing or turning, squeezing, throwing, attacking or kissing, searching something.

There don’t seem to be any specific formal world model rules concerning the effects of deliberately changing scope. The effects of PlaceInScope() and ScopeWithin() are limited to changes in visibility only. Changes in touchability are definitely supplied through the use of found_in, which formally places the item in question within the room via the object tree. AddToScope() seems (to me) to want to offer found_in-like changes in touchability; the fact that objects brought to scope via AddToScope() cast light in the current room – something that PlaceInScope() and ScopeWithin() do not do – is support for the idea that objects so added are intended to be considered as being within the space modeled by the room.

My own preferred answer to question #2 above would be to make non-local on-stage things takeable by default. This is informed by two factors: First, I’m guessing that most authors are writing some sort of code to check for non-locality in cases where they want things to be in scope for visibility but not takeability, anyway – if only to prevent the "That isn't available." message. Second, zarf made a convincing argument in another thread that an author might be altering scope on purpose to allow the player character to reach a hat through a wormhole, and I can think of other scenarios (e.g. a pie on a kitchen counter takeable from outside of the kitchen through an open window) where this kind of behavior would be useful. Also, the logic of a shared locality check for the actor and an object is pretty simple, so it seems like less of a burden to put on hypothetical authors to halt an unwanted ##Take than the alternative, which is to require them to force the allowance of a ##Take when the library says no. The latter adds the greater burden of having to replicate in their exception code any ##Take-specific logic found elsewhere (e.g. carrying capacity).

This approach does not strictly line up with the formal world model as defined in DM4, but then neither does its current function (see rule 1.2.7). As mentioned, the world model rules don’t seem to address what happens when one deliberately breaks the rules via scope modification, so perhaps some new rules are needed, and this could be one of them.

1 Like