Test cases and logic checks for custom lighting system

Some of you probably already noticed from my previous test that I’m replacing the lighting system with rulebooks.

For reference, this is the core of what I did:

Lighting Rulebooks
Light emission rules are an object based rulebook.
Light emission rules have outcomes it is emitting light (success) and it is unlit (failure).
Light absorption rules are an object based rulebook.
Light absorption rules have outcomes it admits light (success) and it blocks light (failure).
Lighting level rules is an object based rulebook.
Lighting level rules have outcomes it is bright (success) and it is dark (failure).

Include (-
[ HasLightSource obj;
	return FollowRulebook((+ light emission rulebook +), obj, true) && RulebookSucceeded();
];
-) replacing "HasLightSource".

To decide whether (it - a thing) is emitting light: (-
	HasLightSource({it});
-).

To decide whether (it - a thing) is not emitting light: (-
	~~HasLightSource({it});
-).

Include (-
[ HidesLightSource obj;
	return FollowRulebook((+ light absorption rulebook +), obj, true) && RulebookFailed();
];
-) replacing "HidesLightSource".

To decide whether (it - a thing) admits light: (-
	~~HidesLightSource({it});
-).

To decide whether (it - a thing) blocks light: (-
	HidesLightSource({it});
-).

Include (-
[ OffersLight obj;
	FollowRulebook((+ lighting level rulebook +), obj, true);
	return RulebookSucceeded();
];
-) replacing "OffersLight".

Definition: a room is bright rather than dim if I6 routine "OffersLight" says so (it is offering light).

Definition: a container is bright rather than dim if I6 routine "OffersLight" says so (it is offering light).

But showing it off isn’t the purpose of my post. I want to make sure I covered all the cases that the core lighting routines cover. I have a simple test case:

The Chapel is a room.
"It's a high vaulted chapel. Very pretty!"

The chapel switch is a light switch in the Chapel.
It illuminates the Chapel.
It is switched on.

The confessional is a closed openable enterable opaque container.
It is in the Chapel.

The kiosk is a closed openable enterable transparent container.
It is in the Chapel.

Some pews are an enterable supporter in the Chapel.

The flashlight is a light source carried by the player.

There is a candy in the confessional.

There is some cash in the kiosk.

There is a hymnal on the pews.

There are a couple of custom kinds in there, but they’re fairly self-explanatory – the light switch effectively toggles the room between lighted and dark, and the light source toggles itself between lighted and dark (both are kinds of devices).

What follows are rules that I think are equivalent to the built-in checks.

OffersLight
[Note: I'm not sure if Inform would order these correctly by default, but assume that they're explicitly listed in the order shown here.]

Lighting level rule for a lighted room:
	[should cover "if (obj has light) rtrue"]
	it is bright.

Lighting level rule for a room (called there):
	[should cover "objectloop (j in obj) if (HasLightSource(j)) rtrue"]
	repeat with the article running through everything in there:
		[just checking: "in" is only directly contained things, right?]
		if the article is emitting light, it is bright.

Lighting level rule for a closed opaque container:
	[should cover "if ((obj has container) && (obj hasnt open) && (obj hasnt transparent)) rfalse]
	it is dark.

Lighting level rule for something incorporated by something (called the parent):
	[should cover "if ((obj provides component_parent) && (obj.component_parent))"]
	abide by the lighting level rules for the parent.

Lighting level rule for something held by something (called the parent):
	[should cover the "else" case; not quite sure if "held by something" is allowed, will check soon]
	abide by the lighting level rules for the parent.

Last lighting level rule:
	it is dark.
HasLightSource
Light emission rule for a lit thing:
	[should cover "if (i has light) rtrue"]
	it is emitting light.

Light emission rule for a lighted room:
	[also covers "if (i has light) rtrue" for the case of a room]
	[not sure if these rules need to cover rooms though]
	it is emitting light.

Definition: a thing is see-through if I6 routine "IsSeeThrough" says so.

Light emission rule for a see-through thing (called it):
	[should cover "if ((IsSeeThrough(i) && (~~(HidesLightSource(i)))) objectloop (j in i) if (HasLightSource(j)) rtrue"]
	if it admits light:
		repeat with the article running through everything held by it:
			if the article is emitting light, it is emitting light.

[I have no idea what "ad  = i.&add_to_scope" does, so by extension I have no idea what the following block does either.]

Light emission rule for something (called the parent) that incorporates something (this is the propagate lights from components rule):
	[should cover "if (ComponentHasLight(i)) rtrue"]
	repeat with the part running through things incorporated by the parent:
		if the part is lit or the part is emitting light, it is emitting light;
		if the part incorporates something:
			follow the propagate lights from components rule with the part;
			if the rule succeeded, it is emitting light.
HidesLightSource
Light absorption rule for the player:
	[should cover "if (obj == player) rfalse"]
	it admits light.

Light absorption rule for a something transparent
	[should cover "if (obj has transparent) rfalse"]
	it admits light.

Light absorption rule for a supporter:
	[should cover "if (obj has supporter) rfalse"]
	[note: this probably misses rideable animals though, while the default implementation handles them]
	it admits light.

Light absorption rule for a person:
	[should cover "if (obj has animate) rfalse"]
	[note: technically non-persons could be made animate, but that's not normally done]
	it admits light.

Light absorption rule for a container (called the box):
	if the box is open, it admits light;
	otherwise it blocks light.

Last light absorption rule for something (called it):
	if it is enterable, it admits light;
	otherwise it blocks light.

My first goal here is to verify that the rules I’ve written reflect the same logic as the original I6 code, and in particular to fill in that one big gap related to scope in HasLightSource. If the best way to fill it in is to put that code back in the I6 routine, that is an option, but I would like to know what it’s doing first.

My second goal here is to extend the test case to cover more possibilities. For example, probably the test case should have some cases involving things that are part of something. Is there anything else that’s missing?

I can say with confidence that the part concerning the .add_to_scope property is vestigial in Inform 7 and can be safely ignored. No part of the system uses that property, though internal notes indicate that it was left open as a possible hook of use in the future.

The primary function that .add_to_scope provided in I6 has been replaced by the incorporation relation in I7.

As far as test cases go, the things that I recall checking include:

  • Does light from outside enter open and/or transparent containers?
  • Does light from inside exit open and/or transparent containers?
  • Do lit things that are part of something (called T) have the same effect as if object T is lit?
  • Do carried or worn objects that are lit illuminate the area correctly?

There are a couple of minor issues that I recall encountering, too:

  • the question of whether a lit container illuminates both its inside and its outside
  • the fact that groups of “identical” objects, some lit and some unlit can appear to be listed twice in the contents of lit room, e.g. 7 coins, with 4 lit and 3 unlit
1 Like

So, this entire block is completely unnecessary?


	ad = obj.&add_to_scope;
	if (parent(obj) ~= 0 && ad ~= 0) {
		if (metaclass(ad-->0) == Routine) {
			ats_hls = 0; ats_flag = 1;
			sr = scope_reason; po = parser_one;
			scope_reason = LOOPOVERSCOPE_REASON; parser_one = 0;
			RunRoutines(obj, add_to_scope);
			scope_reason = sr; parser_one = po;
			ats_flag = 0; if (ats_hls == 1) rtrue;
		}
		else {
			for (j=0 : (WORDSIZE*j)<obj.#add_to_scope : j++)
				if ((ad-->j) && (HasLightSource(ad-->j) == 1)) rtrue;
		}
	}

(I suppose if add_to_scope is always false, that makes sense.)


This works (the confessional and the kiosk are the test cases for that).

Ah, that’s a good question. I guess I should’ve dropped the flashlight in the confessional and kiosk and verified that the light escapes.

Hmm, I’ll need to add a test case for this one… maybe something like this?

The lectern is a supporter in the Chapel.
The reading lamp is a light source. It is part of the lectern.
After printing the name of the lectern, say " (with [a reading lamp] attached)".

And now I’m wondering, what do you do when the parent of incorporation is a container…?

I verified carried things (I think I forgot to verify that the flashlight works wen set down though). I suppose something will be needed for worn things…

I guess something like this should do for testing that?

The player carries a headlamp. It is a wearable light source.

Eh, a lit container, that’s quite an unusual case… hmm, and it would need to be enterable as well… and since it’s a container, it can’t be a light source, meaning it’s either permanently lit or needs special rules to turn it on and off…


Okay, so it looks like this whole thing is working in the test case (though I haven’t yet tested everything, as you can see above). There are a few problems with it, though.

  1. This is actually pretty obvious, all things considered. In short, the system greatly diminishes the usefulness of rules, because all the lighting rules are cluttering up the output.
  2. It breaks Room Description Control by Emily Short. That is to say, adding it to my main project that uses that extension causes room descriptions to stop showing the visible items. I’m not sure yet what could be causing that, though I assume it has to be related to HasLightSource (ie, the light emission rulebook), since RDC works off of scope and scope detection relies on HasLightSource.
  3. Performance. It seems to become laggy in the larger project, which not only has a lot more rooms but also a lot more things in each room. I’m not sure what I can do about this. Maybe something like this that selectively enables it on specific objects would help. I don’t know how much it would help.
  4. I also notice that, if I turn on rules in a startup rule. it seems to check light for everything in the entire world before even getting to the initial room description. Maybe this is normal. I’m not really sure. A good chunk of it seems to boil down to checking whether thedark offers light, but it also seems to be checking a couple other random things in the world.

I’m also going to post the latest version of the rulebooks, in case anyone cares to look and maybe spot some errors.

OffersLight
Last lighting level rule for a dark room (this is the dark rooms are usually dim rule):
	it is dark.

Lighting level rule for a lighted room (called there) illuminated by a light switch (called the switch) (this is the rooms are bright only if the light switch is on rule):
	if the switch is switched on, it is bright;
	repeat with the article running through everything in there:
		if the article is emitting light:
			it is bright;
	it is dark.

First lighting level rule for a lighted room illuminated by no light switch (this is the rooms without a light switch have light rule):
	it is bright.

First lighting level rule for an object (called there) (this is the light sources light an area rule):
	repeat with the article running through everything held by there:
		if the article is emitting light:
			it is bright;

Lighting level rule for a closed opaque container (this is the closed containers don't offer light rule):
	it is dark.

Lighting level rule for something incorporated by something (called the parent) (this is the parts defer light to the parent rule):
	abide by the lighting level rules for the parent.

Lighting level rule for something directly-held by an object (called the parent) (this is the contents defer light to the parent rule):
	abide by the lighting level rules for the parent.

Last lighting level rule for a room (this is the rooms are usually bright rule):
	it is bright.

The contents defer light to the parent rule is listed after the rooms are bright only if the light switch is on rule in the lighting level rules.
The closed containers don't offer light rule is listed before the contents defer light to the parent rule in the lighting level rules.
HasLightSource
Light emission rule for a lit thing (this is the lit things emit light rule):
	it is emitting light.

Light emission rule for a see-through thing (called it):
	if it admits light:
		repeat with the article running through everything held by it:
			if the article is emitting light, it is emitting light.

Light emission rule for a person (called the observer) (this is the light escapes people unless concealed rule):
	repeat with the lamp running through things carried by the observer:
		if the lamp is concealed, no;
		if the lamp is emitting light, it is emitting light;
	repeat with the lamp running through things worn by the observer:
		if the lamp is concealed, next;
		if the lamp is emitting light, it is emitting light.

Last light emission rule for something (called it) that incorporates something (this is the light escapes components rule):
	repeat with the part running through things incorporated by it:
		if the part is emitting light, it is emitting light.

Last light emission rule (this is the most things don't emit light rule):
	it is unlit.
HidesLightSource
Light absorption rule for the player (this is the player is see-through rule):
	it admits light.

Light absorption rule for a supporter (this is the light enters supporters rule):
	it admits light.

Light absorption rule for a container (called the box) (this is the light enters containers only when open rule):
	if the box is open, it admits light;
	otherwise it blocks light.

Light absorption rule for something transparent (this is the light enters transparent things rule):
	it admits light.

Light absorption rule for a person (this is the people admit light rule):
	it admits light.

Last light absorption rule for something (called it) (this is the enterable things usually admit light rule):
	if it is enterable, it admits light;
	otherwise it blocks light.

Another option, if it gets too laggy, is to cache the results instead of calculating them on the fly. Light calculations are run a lot in I7, so the little overhead of each rule adds up.

The thought of a cache did occur to me, but I have no idea what such a cache would even look like… or when it’s best to invalidate the cache. I guess somewhere in the Every turn rules…

The easiest way is to make a new attribute, call it “illuminated” or the like, and just run the light-propagation code once, setting everything “illuminated” that it can touch. You’d have to invalidate it on any major change to the world, but it might still be faster in the end.

I did that sort of caching for the scope relation and it helped in the monstrosity that was Scroll Thief.

Using illuminated as a property when I have an illuminates relation already seems like a recipe for confusion.

Hmm… so, in essence, that would boil the I6 functions down to something like this:

Include (-
[ OffersLight obj a b;
	return obj.(+ illuminated +) ~= 0;
];
-) replacing "OffersLight".

Probably the syntax isn’t quite right, and HasLightSource would need to be covered too… HidesLightSource could be skipped, as it’s not used anywhere else.

Well, I guess that’s the difficult part though. What is a “major change to the world”? What exactly does “invalidating the cache” entail? Is there a need to ask if the cache is currently valid? Or would you just assume it has become invalid under specific circumstances and do a cache update that might end up being redundant?

Hmm? Wait, by scope, you more or less mean visibility, right? How does that work? Maybe a lighting cache could be somewhat modelled after it…

I’ve also run into performance problems related to the concealment relation, so it might be a good idea to cache that too…

This was the core of it:

A thing can be marked in scope. A thing is usually not marked in scope.
A room can be marked in scope. A room is usually not marked in scope.
The nonlocal visibility flag is initially false.
This is the re-cache scope at start of turn rule: recalculate scope.
The re-cache scope at start of turn rule is listed after the parse command rule in the turn sequence rulebook.
First every turn rule (this is the re-cache scope at end of turn rule): recalculate scope.

After deciding the scope of the player (this is the use cached scope if available rule):
	repeat with the item running through marked in scope things:
		place the item in scope, but not its contents;
	repeat with the item running through marked in scope rooms:
		place the item in scope, but not its contents.

The scope adjustment rules are a rulebook.
To recalculate scope:
	now every thing is not marked in scope;
	now every room is not marked in scope;
	now the nonlocal visibility flag is false;
	follow the scope adjustment rules.

And then I’d put my big expensive scope-modifying rules in the scope adjustment rulebook instead, which would only be run right before parsing and right after the action. It ended up with three rules: one to make ropes visible in multiple rooms, one to make adjacent rooms visible, and one to extend scope through scrying mirrors.

This seems doubly relevant to me, as I have both security cameras (which could be seen as absolutely identical to scrying mirrors except for the fluff) and a remote viewing power.

Only small issue I see with that approach is that it’s completely player-centric… I’m unsure how much that matters, but I do have NPCs wandering around without direct player input…

I think if, I used that approach for lighting though, it could fairly easily be general to the player and NPCs, as “whether something is lit” is generally not a question that depends on who’s looking… or at least, I don’t think it is… there is the case of two people on the inside and outside of a closed container…

This was the scrying mirror code:

Indirect visibility relates various things to various scrying devices. The verb to be seen through means the indirect visibility relation. The verb to show means the reversed indirect visibility relation.
Understand "through [something related by indirect visibility]" or "via [something related by indirect visibility]" or "in [something related by indirect visibility]" as a thing.

A thing can be scriable or unscriable. A thing is usually scriable. A scrying device is never scriable. [This prevents another sort of infinite recursion.]

Scope adjustment (this is the look through scrying devices rule):
	if the security lockdown is happening, make no decision; [Scrying devices don't work during this time.]
	repeat with the glass running through scrying devices: [Reset the relation first.]
		now the glass does not show anything;
	repeat with the glass running through usable visible scrying devices: [Now add in the other objects.]
		let the far glass be the mystical relative of the glass;
		if the far glass is off-stage:
			deactivate the glass;
			next;
		let the far side be the visibility ceiling of the far glass;
		if the far side is nothing:
			deactivate the glass;
			next;
		now the far side is marked in scope;
		repeat with the item running through scriable things enclosed by the far side:
			if the far glass can see the item:
				now the item is marked in scope;
				now the glass shows the item;
		repeat with the item running through scriable doors liminal to the far side:
			if the far glass can see the item:
				now the item is marked in scope;
				now the glass shows the item;
		now the nonlocal visibility flag is true.

Include (-
	[ VisibilityCeiling obj l up;
		up = obj;
		l = obj;
		while (true){
			up = VisibilityParent(up);
			if (up == 0) break;
			l = up;
		}
		return l;
	];
-).

To decide what object is the visibility ceiling of (O - an object):
	(- VisibilityCeiling({O}); -).

If I have An object can be light-emitting, then (+ light-emitting +) gives a shorten-wiring error about being unable to find P_light_emitting. Is that because light-emitting was compiled as an attribute instead of a property? Is there any way I can fix this?

This works (and lighting still seems to function, too – with an added rule to recalculate it, of course):

An object can be light-neutral, light-emitting, or light-dummy (this is its light-emission property).
Include (-
[ HasLightSource obj;
	return obj.(+ light-emission +);
];
-) replacing "HasLightSource".

It’s a bit unfortunate that there’s an extra unused value, but I suppose I can live with it for now…

(Using the name of an either-or property in (+ +) should probably be taken by the compiler as a sign that this property must not be an attribute… but the compiler doesn’t do that, I guess.)

Now I just need to decide whether OffersLight needs a separate property or should reuse the same property (which might even render the extra value not redundant).

Ugh. Somehow, just changing the name of the property broke it for no apparent reason.

An object can be light-neutral, light-emitting, or light-providing (this is its lighting response property).
Include (-
[ HasLightSource obj;
	if(~~obj) rfalse;
	return obj.(+ lighting response +) == (+ light-emitting +);
];
-) replacing "HasLightSource".
Include (-
[ OffersLight obj;
	if(~~obj) rfalse;
	return obj.(+ lighting response +) == (+ light-providing +);
];
-) replacing "OffersLight".

And now I get a “shorten-wiring” error that the name P_lighting_response (and also, weirdly enough, the name call) can’t be found… even though the I6 code generated if I comment out those includes clearly contains P_light_response all over, though I can’t really tell if any of that counts as a definition…

What on earth is going on?

I believe Inform 10 added a helper function called GetEitherOrProperty(object, prop) that you’re supposed to use instead of object.prop, because there’s no way to know ahead of time how exactly Inform is going to implement a particular property.

That might make sense if it was an either-or property, but it’s currently an enumerated value property (declared with 3 possible values).

For what it’s worth, I get the same result if I define it like this:

Lighting response is a kind of value.
The lighting responses are light-neutral, light-emitting, and light-providing.
An object has a lighting response.

Though, as far as I know, the previous form is supposed to be just an abbreviation of the above, so it makes sense if they give the same result.

EDIT: Flipping around the order of the I6/I7 versions of the routine works. That is, instead of this:


Include (-
[ HasLightSource obj;
	if(~~obj) rfalse;	
	return obj.(+ lighting response +) == (+ light-emitting +);
];
-) replacing "HasLightSource".

To decide whether (it - a thing) is emitting lig): (-
	HasLightSource({it});
-)..

To decide whether (it - a thing) is not emitting light (-
	~~HasLightSource({it});
-).

Include (-
[ OffersLight obj;
	if(~~obj) rfalse;
	return obj.(+ lighting response +) == (+ light-providing +);
];
-) replacing "OffersLight".

Definition: a room is bright rather than dim if I6 routine "OffersLight" says so (it is offering light).

Definition: a container is bright rather than dim if I6 routine "OffersLight" says so (it is offering light).

I did this:

Include (-
[ HasLightSource obj;
	if(~~obj) rfalse;
	return (+ has light source +)-->1(obj);
];
-) replacing "HasLightSource".

To decide whether (it - a thing) is emitting light (this is has light source):
	decide on whether or not it is light-emitting.

To decide whether (it - a thing) is not emitting light:
	decide on whether or not it is not light-emitting.

Include (-
[ OffersLight obj;
	if(~~obj) rfalse;
	return (+ provides light +)-->1(obj);
];
-) replacing "OffersLight".

To decide whether (it - a thing) provides light (this is provides light):
	decide on whether or not it is light-providing.

To decide whether (it - a room) provides light:
	decide on whether or not it is light-providing.

Definition: a room is bright rather than dim if it is light-providing.

Definition: a container is bright rather than dim if it is light-providing.

I’m getting an impression that there may be a bug if you try to put an adjective in (+ +) syntax?

Anyway, this compiles, but now the interpreter crashes instantly at startup (Glulxe fatal error: Call to non-function. (1)). It seems like (+ provides light +)-->1 is not a function?

EDIT2: Looks like a precedence issue. At least I can see the generated code now, which isn’t possible (as far as I know) when I get a shorten-wiring error. This looks distinctly wrong, and appears to match the error that suggests the number 1 is what was erroneously called (though it’s not immediately obvious from the error message that that’s what the 1 is):

[ OffersLight obj;
    if ((~~(obj))) {
        rfalse;
    }
    return (closure_data_U22-->((1)(obj)));
];

And sure enough, adding parentheses around (+ ... +)-->1 solves the crash. The game now starts pitch dark, but that’s probably because the cache isn’t running or something, so shouldn’t be hard to go from here.

1 Like

Ironically, the cache seems to make performance worse rather than better in my larger project (though at least it somehow dodged the issue of room description details not working properly). I think the solution is for the cache rule to only consider a subset of the world rather than the full world… which probably means I need to mark people as “active” if they might be doing stuff in another room… then I can update the cache only for “rooms containing an active person”.

Unless anyone has a better solution… or suggestions on additional criteria to narrow down the field would also be welcome.

Hmm. Maybe at this point it would be better to rewrite and optimize the lighting code on the I6 level? Transitioning back and forth between I6 and I7 always comes at a cost, and I6 code can be much faster.

Problems with that include:

  • I’m not really good with I6. I’d prefer to work in I6 as little as possible.
  • The (+ +) syntax needed to deal with I7 things in I6 seems to be buggy, or I’m missing something about how it’s supposed to work or how it’s supposed to be used.
  • I couldn’t find a way to define a rulebook in I6 but add rules to it in I7.

Do you think transitioning from I6 to the I7 phrase is a source of slowdown? To me, that seems unlikely, as the intermediary phrase is compiled down into a pretty simple I6 routine, which is directly called from the main I6 routine, so unless the simple act of calling a routine is a likely performance blocker, it doesn’t seem likely that this approach is inherently slow.

My project currently has 149 rooms and 731 things, so recalculating lighting for all of those every turn seems like a much more likely source of the slowdown in this case.

EDIT: But as it turns out, figuring out how best to implement selectively updating the cache is a bit confusing.

Light emission carries light outwards from a “lit object”, such that each holder of the object is successively considered to be emitting light as well, unless it blocks light (as does a closed opaque container). Meanwhile, light providing (aka offering) carries light inwards from the room to things nested within it, again unless something blocks light.

To me, that sounds like emission should be updated starting from the leaves and working down – in other words, walk the tree in post order (depth first) – while providing should be updated starting from the root and working up – in other words, walking the tree either in pre order (depth first) or level order (breadth first). But that means I need to walk the tree twice, which seems maybe bad? Though, that’s the tree nested within a single room, so it wouldn’t be nearly as bad as iterating over the entire world twice.

Maybe I’m overthinking things. Maybe just “update emission for everything and then update providing for everything” works in all cases. (It worked in the simple test case, after all.)

Oookay, this approach has painted me into a weird corner. For whatever reason, in order for the lighting cache to be ready in time for the initial room description, the first iteration of the cache must be populated before the position player in model world rule. But in order for it to be limited to the room the player is in, it must be populated after the position player in model world rule.

Any idea how to get around this limitation? Is there an easy way to get the starting room?