Thoughts on room connections?

Got another design question. Probably some rubber-ducking here, but maybe someone has some ideas.

Wondering about connections between rooms this time – “doors” to Inform7, “connectors” to TADS. Initially I thought these didn’t need to be formalized in a standard library, and could be done in an ad-hoc way:

office is north of lobby.
office door is jammed.

player tries to move north, player is in lobby?
office door is jammed?
    say "The door to the office is jammed."

But these connections seem to be pretty common, and can do some interesting things:

  • Prevent travel.
  • Narrate something during travel.
  • Be acted on: entered, examined, locked.

So, maybe it’s better to have some canonical connectors.

My initial attempt at these allows defining a connection name and the rooms it connects – "office door" connects lobby to office – and when the player wants to “enter office door,” that gets redirected to “move north,” based on the fact that “office is north of lobby.”

Thinking about it more, this seems backwards, since “move north” should check for a connector in order to deny travel or narrate something, but the connector already knows where it leads, so the fact that “office is north of lobby” seems irrelevant when entering the door. Instead, it seems like “move north” should defer to a connector if it’s there, while “enter office door” should move the entrant to the destination, regardless of what direction it’s associated with. This also has the benefit of connectors working between non-adjacent locations, like teleporters or transports.

But then I wonder, is it too restrictive to indiscriminately check for a connector when traveling? Maybe there’s a connector that’s not associated with the normal path of travel, something non-obvious like a duct. Say the office door connector is blocked, but there’s also a duct connector, and both lead into the office. We never want to take the duct when traveling, only the door, unless the player explicitly enters the duct.

That suggests there should be two ways of defining connectors. One is implicitly entered during directional travel (when applicable), and the other is only entered explicitly.

"office door" connects lobby to office.
duct explicitly connects lobby to office.

Maybe something like this? Seems like the implicit one is more common, so it should be less verbose, but the “explicitly” wording feels a little misplaced. Any suggestions here?

What’s the precedent for this in existing engines? I guess Inform7’s doors are more like rooms where you automatically pop out the other side, but would the duct really be an enterable container with some special behavior when you enter it? Or is there a cleaner way? What about TADS?

As somebody who recently spent a lot of time thinking about this, may I humbly recommend Chapter 5: Moving around?

The complications with doors arise as you add other “standard” behaviors to the library and need them to work consistently with doors and door-like things.

For example, Inform has phrases like “room north of R1” and “best route from R1 to R2 (using doors)”.

Also consider the ability of the author to write a rule like “before going north from R1: …” You’d like that to work consistently whether the player types NORTH or ENTER DOOR. (In both the locked and unlocked cases, of course.)

Really they’re more like backdrops than anything else.

See, the other complication is that a door is contained in two rooms, which breaks a naive IF world model. Inform’s solution is not entirely smooth.

In my game Escape! there is a door between the kitchen and the garden. From kitchen north to garden and from garden south back into kitchen.

The door is modeled as an openable lockable object. The modeling language has no “in-between-rooms” concept, so the door is either in the kitchen or in the garden. When the player enters the kitchen, the door moves to the kitchen, when the player enters the garden, the door moves to the garden. The door knows where it is, so it will change its description according to the location.
The door intercepts player commands for open and close and changes game state as necessary. E.g. when the door is opened, it will create passages from the kitchen to the garden and back. When it is closed, it will remove the passages.

The door also has a keyhole, which is an object contained in the door. When the key is in the keyhole, it remembers on which side of the door it is. So, when the player is in the kitchen and the key is in the hole on the garden side, “turn key’ or “lock door” will say that the key is in the other side of the door. Completely useless and none of the testers found it, but I wanted to see if it would work.

Commands for moving north from the kitchen or south from the garden are caught by the kitchen and garden locations who will check if the door object has state open. If not, they will trigger an “open door” command and if that succeeds they will return control to the interpreter to do the actual move.

This is what it looks like in code:

Some code from the kitchen location:

  t_entrance  # executed when entering the kitchen
    move(o_kitchen_door, %this)  # must be able to refer to the door
    l_json.r_maptext = o_player.d_maptext_house
    nomatch()

   t_north     # executed when going north
     if not(testflag(o_kitchen_door.f_open)) then
       printcr("(opening the kitchen door first).\n")
       if not(try(l_location, 0, 1, "open [o_kitchen_door]")) then
         disagree()  # stop the command
       else
         nomatch()  # may continue with the move

I have no code for “enter door”, but it would just execute t_north.

The complete door object:

$OBJECT o_kitchen_door
 DESCRIPTIONS
   d_sys       "the door", "the kitchen door"

   d_exa       "The door is made of wood; it gives access to the garden."

   d_exa1      "The door is made of wood; it leads back into the kitchen."

   d_entr      "\nTo the north is a door that leads to the garden."

   d_entr1     "\nTo the south is a door that leads to the kitchen."

   d_no_window " In the upper half of the door is an opening where /
                a window used to be."

 CONTAINED in l_kitchen

 FLAGS
   f_openable = 1
   f_lockable = 1
   f_locked   = 1

 ATTRIBUTES
   r_key = o_rusty_key

 TRIGGERS
   "examine [o_kitchen_door]"                 -> t_exa
   "look [prepos] [o_kitchen_door]"           -> o_kitchen_window.t_look_through
   "put [o_player] [prepos] [o_kitchen_door]" -> o_kitchen_window.t_look_through
   "open [o_kitchen_door]"                    -> t_open
   "open [o_kitchen_door] with [o_rusty_key]" -> t_open_key
   "close [o_kitchen_door]"                   -> t_close

   t_entrance
     if owns(l_kitchen, %this) then
       printcr(d_entr)
     else
       printcr(d_entr1)

   t_exa
     if owns(l_kitchen, %this) then
       print(d_exa)
     else
       print(d_exa1)
     endif
     print(" and it is currently ")
     if testflag(f_open) then
       printcr("open.")
     else printcr("closed.")
     endif
     # print info about window and keyhole
     if testflag(o_kitchen_window.f_broken) then
       print(d_no_window)
     else
       print(o_kitchen_window.d_entr_short)
     endif
     printcr("\nAbout halfway in the door is a [o_keyhole_door].")
     contents(o_keyhole_door)

   t_open
     if testflag(f_open) then
       printcr("But the door is already open.")
       disagree()
     endif
     if not(testflag(f_locked)) then
       printcr("Ok, the kitchen door is now open.")
       setflag(f_open)
       newexit(l_kitchen, north, l_garden)
       newexit(l_garden, south, l_kitchen)
     else
       printcr("The door seems to be locked.")
       disagree()

   t_open_key 
     # one of the testers wanted to "open door with key" instead of unlock
     if testflag(f_open) then
       printcr("But the door is already open.")
       disagree()
     endif
     if not(testflag(f_locked)) then
       printcr("(the door isn't locked).")
     else
       if not(try(l_location, 1, 1, "unlock [o_kitchen_door] with [o_rusty_key]")) then
         disagree()
     endif
     if trigger(t_open) then endif
     
   t_close
     if not(testflag(f_open)) then
       printcr("But the door is already closed.")
       disagree()
     endif
     printcr("Ok, the kitchen door is now closed.")
     clearflag(f_open)
     blockexit(l_kitchen, n)
     blockexit(l_garden, s)
END_OBJ

Hope this helps a bit.

Doors are so weird! Here’s what I know about doors: Doors are not in rooms; doors connect rooms. Doors may behave in different ways and have different attributes depending on which side you are in. For example, a door may lock people out of an office building and yet may open from the inside. A door may have a one-way mirror. A time portal may only work in one direction or both. Travel times between cities may be longer in one direction than the other.

These last two examples are about other kinds of connectors, not doors, but doors are connectors and I think the issue is really about connectors in general

In terms of graph theory, Connectors behave like edges (and rooms, vertices.) Connector edges are bidirectional with different attributes associated with each direction. To my mind, the most accurate model is a weighted digraph. With “weight” being defined as a collection of attributes.

Ahh, thanks, this is what I was missing. I might need to have “enter door” and “go north” happen together somehow, so that overriding either one can deny travel.

I didn’t find that either, but thanks for the heads up on this. I’m still working on my Escape! port and will try to get this right.

I’ve been thinking about this a bit. We all know that nodes/verts can have extra info associated with them that are not relevant to the graph. For example, in a social media network, the graph of who’s friends with who doesn’t care at all that Bob lives at 123 Bluebird Ln. But we can sometimes forget that edges also can have info like this (maybe because they often have info that are relevant to the graph).

For example, in the graph of what rooms are connected to other rooms, the fact that office is north of lobby doesn’t matter, just that they’re connected. The north relationship doesn’t matter to the graph. I feel like connectors aren’t edges themselves, but are more data about existing edges – partly this kind of metadata (a description of a door), partly turning undirected edges into directed ones (door is locked from this side), and partly weights (two turns to go through closed door instead of one turn). Except, if we allow creating connections between rooms that aren’t already connected in a compass direction, then it’s a new edge…

Thanks for all the replies, very helpful as usual!

Yeah, it’s not just about the fact of the relationship. It’s also the quality(ies) of the relationship. Or at least I think it should be. Also, this goes way beyond doors. Consider modeling something like friendship that relates two people.

You can certainly do three nodes (Eliza)–(friendship)–(Charlotte) and carry the attributes for friendship relation in the friendship node. The friendship node then just acts like a door, which is to say it only exists to describe the relation and can’t be connected to anything else but Eliza and Charlotte.

I think Inform 7 (and Dialog?) you would have to model friendship this way if you wanted to say anything about the quality of friendship in either direction. Maybe I’m wrong about that? Maybe you would need two friendship nodes? One for each direction?

Anyhow, your question came up at a very good time since I’m working on a system in Javascript that contains the story world as a weighted digraph and have been thinking about this quite a bit.

So, here’s what I’ve been thinking… most times there’s a data model, and a graph is just an abstraction of the data model, as viewed from some particular perspective.

We can view friendship as an undirected graph, because I can’t be friends with you without you being friends with me. So, let’s say we’re storing friendship in a relational database. Friendship is an n:m relationship… You can have many friends, and many people can be friends with you.

So, we store friendship relationships in a “linking table.” Does that mean friendship is a node instead of an edge? I don’t think so… even if we put some more info in that linking table, like a timestamp or whatever, it’s just metadata about that edge, it doesn’t make friendship a node, from the perspective of a graph of who’s friends with who. Of course, we could consider friendships to be nodes, but that’d be a different graph. Either way, the data representation is the same.

Here’s another way to look at it. Suppose the network also allows “following.” I can follow you without you following me, so a graph of followers is a directed graph. But suppose we want a graph of who knows who? We can take those “friend” edges and “follower” edges and put them all in the same graph. The graph is directed, the friend edges are always bidirectional (one in each direction), and the follower edges are one-way. From the perspective of a graph that only cares about who knows who, the fact that the edges came from “friend” or “follower” relationships is no longer important to navigating or analyzing the graph – it’s just metadata.

Basically, I guess what I’m thinking is that edges can be “first-class” things and have application-specific data associated with them that’s not necessarily relevant to the graph, just like nodes can. Whether “facts” or records represent nodes or edges depends on what we want to graph, but that doesn’t need to affect the underlying data model.

Sorry if I’m rambling here… I hope it made sense!

Hmm, movement code just got pretty gnarly.

# Directions

east is a direction
west is a direction
north is a direction
south is a direction

northeast is a direction
northwest is a direction
southeast is a direction
southwest is a direction

east is the opposite of west
west is the opposite of east
north is the opposite of south
south is the opposite of north

northeast is the opposite of southwest
northwest is the opposite of southeast
southeast is the opposite of northwest
southwest is the opposite of northeast

# Combined (connectors and directional) movement (3)

$somebody is moving $direction by entering $something,
$somebody tries to enter $something,
$somebody wants to move $direction, $somebody is in $place?
$destination is $direction of $place?
    $somebody tries to move $direction.
    $somebody travels to $destination.
    time passes for $somebody.

$somebody is moving $direction by entering $something,
$somebody tries to enter $something,
$somebody wants to move $direction, $somebody is in $place?
$direction is the opposite of $opposite?
$place is $opposite of $destination?
    $somebody tries to move $direction.
    $somebody travels to $destination.
    time passes for $somebody.

$somebody is moving $direction by entering $something,
$somebody wants to move $direction|,
$somebody tries to enter $something|,
    .

# Connectors -> combined movement (2)

$somebody wants to enter $something,
$something connects $here to $there?
$somebody is in $here? $there is $direction of $here?
    $somebody tries to enter $something.
    $somebody wants to move $direction.
    $somebody is moving $direction by entering $something.

$somebody wants to enter $something,
$something connects $here to $there?
$somebody is in $here? $here is $direction of $there?
$opposite is the opposite of $direction?
    $somebody tries to enter $something.
    $somebody wants to move $opposite.
    $somebody is moving $opposite by entering $something.

$somebody wants to enter $something,
    report $somebody not seeing $something.

# Directional movement -> connectors (1)

$somebody wants to move $direction, $somebody is in $place?
$destination is $direction of $place?
$something connects $place to $destination?
    $somebody wants to enter $something.

$somebody wants to move $direction, $somebody is in $place?
$direction is the opposite of $opposite?
$place is $opposite of $destination?
$something connects $place to $destination?
    $somebody wants to enter $something.

$somebody tries to enter $something,
    .

# Directional movement, without connectors

$somebody wants to move $direction, $somebody is in $place?
$destination is $direction of $place?
    $somebody tries to move $direction.
    $somebody travels to $destination.
    time passes for $somebody.

$somebody wants to move $direction, $somebody is in $place?
$direction is the opposite of $opposite?
$place is $opposite of $destination?
    $somebody tries to move $direction.
    $somebody travels to $destination.
    time passes for $somebody.
    
$somebody wants to move $direction,
    report $somebody cannot travel $direction.

player tries to move $direction, player is in $place,
player travels to $destination,
    player is in $destination.
    player wants to look.

$somebody tries to move $direction, $somebody is in $place,
$somebody travels to $destination, player is in $place?
    $somebody is in $destination.
    report departure of $somebody to the $direction.
    
$somebody tries to move $direction, $somebody is in $place,
$somebody travels to $destination, player is in $destination?
$opposite is the opposite of $direction?
    $somebody is in $destination.
    report arrival of $somebody from the $opposite.
    
$somebody tries to move $direction, $somebody is in $place,
$somebody travels to $destination,
    $somebody is in $destination.

$somebody travels to $destination,
    .

The original code was just the stuff under “directional movement, without connectors.” This still doesn’t account for closed doors and things like that, yet. But author can intercept either “player wants/tries to go north” or “player wants/tries to enter door,” so that’s good.

A lot of redundancy here comes from inferring that if “office is north of lobby” then lobby must be south of office, in the absence of some other directional relationship from office to lobby. Starting to think I need either a way to separate rules into different rulebooks, and throw out the rulebooks somehow, or higher-order logic (current system is all first-order logic).

Either these new facts could be inferred at startup, and then that rulebook thrown out, or some syntax for higher-order logic to wrap stuff like this up in function-like things could be invented. Or both. I think the first way might be more efficient and clean for this particular case.