I chose a slightly different approach in Dialog.
There’s a routine to determine whether the player can see. This is a two-stage affair: First, traverse the object tree via parent-links from the player, until a room or closed opaque container is encountered (this is the “visibility ceiling”). If it is a room, check whether it’s inherently dark (this will often be compiled to a simple object flag). Otherwise, and this is the common case, we’re done; the player can see.
Second stage: If the room is inherently dark, or the visibility ceiling isn’t a room, consider each potentially light-providing object in the game. Does it currently provide light? If so, find the visibility ceiling of said object. If it is the same as the visibility ceiling of the player, the player can see. If there is no such light-providing object, the player is in darkness.
In code:
(player can see)
(current player $Player)
(visibility ceiling of $Player is $Ceil)
(light reaches ceiling $Ceil)
(visibility ceiling of (room $R) is $R)
(visibility ceiling of $Obj is $Parent)
($Obj is #in $Parent)
($Parent is opaque)
($Parent is closed)
(visibility ceiling of $Obj is $Ceil)
($Obj has parent $Parent)
(visibility ceiling of $Parent is $Ceil)
(light reaches ceiling (room $Ceil))
~(inherently dark $Ceil)
(light reaches ceiling $Ceil)
*($Obj provides light)
(visibility ceiling of $Obj is $Ceil)
In terms of performance, I think this computation is comparable to what Inform 6 needs to do. The difference being that Inform 6 will cache the result of the first stage, representing it as a special room. Zarf, am I mistaken?
Now the question is: How often does the game need to perform the computation? It needs to happen at least once per turn, when determining what objects are in scope. That is, the routine for determining the current scope needs to invoke ‘(player can see)’, and handle the result as two separate cases. In addition, the computation has to be carried out in certain action handlers (e.g. look, exits, read and a few more) to prevent actions that would be impossible in darkness, or to just phrase the standard response differently. Finally, the routine is invoked when printing the status line. So, roughly speaking, three times per move instead of one.
But this isn’t such a big deal. For games with a lot of potential light sources (usually there’s just a single lamp), the computation gets heavier. But it’s still negligible compared to parsing, which also happens on every move and involves word-matching against every object in scope.
Anyway, that’s my two cents. Hope they are helpful!