Load-bearing space between conditionals-- what is even happening?!

Okay, Inform detectives, a fun new mystery to solve: In a description with a few conditionals there’s a specific set of brackets that, if they have a space in between them, give the correct description but leave an unnecessary space when the object is examined the second time, slightly indenting that second description. Not a huge problem, but something you’d like to polish up before publication.

Deleting that space, though, means the thing is treated as though it doesn’t have a description when examined, but only for the first time. After the first examination, you get the “first time” description plus the [if known] clause, because it becomes known after the first “nothing special” examination. Turning on actions confirms Xing is succeeding; showme doodad is showing a blank description prior to examination, then updating with the written description after the first “nothing special” response is returned.

Code and example images are below. This is more a mystery than a problem, and I’m not asking for workarounds, just curious if anyone knows what is happening. How can the game not read the description at all, then seemingly find that description after the first X doodad command, and why is not including a space the cause?

(Note: example text has been rewritten/shortened, leaving relevant conditionals and other bits for context.)

The code:

"test" by baezil

The lab is a room. 

A doohickey is a kind of thing. 

A thing can be known or unknown.

A thing can be prepared. A thing is usually not prepared. 

The doodad is a doohickey in the lab.  The doodad is undescribed. The doodad is unknown. The description of the doodad is "[first time]You've never seen this doodad before. There's some sort of stain on it. 

You wonder if you should take it.[only][if known]That doodad from earlier. [first time]Still stained.[only][end if][if the doodad is prepared]No more stains![end if]".

After examining the doodad: 
	now the doodad is known.
	
Report taking the doodad: 
	say "[first time]The outside is stained with black dots.[only]It isn't heavy."; 
	stop the action.
	


Preparing is an action applying to one thing. 

Understand "prepare [something]" as preparing. 

Check preparing: 
	if the thing is not a doohickey, say "This seems fine as it is." instead. 
	
Check preparing the doodad: 
	say "A little elbow grease and... it's ready!";
		now the doodad is prepared.
5 Likes

Oh, this is a fun one! It comes down to how Inform decides if something has a description or not. Rather than just checking if the property is assigned, it evaluates the description of the object and checks if the result is the empty string. (This means that, if it only conditionally has a description, it’ll still say “You see nothing special…” instead of a blank line when that condition isn’t met.)

And when evaluating strings (rather than printing them), [first time] constructs aren’t evaluated, because presumably you want that text to appear when it’s first shown to the player, and not get used up on a random comparison deep in the guts of the library.

This means that, when the doodad is not known, the space makes the description evaluate to " " instead of “”, and " " is not the empty string, therefore the description should be shown.

To fix it, try using a [one of]First time text here[or][stopping] construction instead of a [first time]First time text here[only]. They’re treated slightly differently by the comparison mechanism, while displaying exactly the same.

Alternately, use this:

To say comparison shim:
    if expanding text for comparison purposes, say "~".

Now sticking a [comparison shim] anywhere in your description will ensure it always evaluates to a non-empty string, without affecting how it’s actually printed to the screen.

7 Likes

I knew someone would know how Inform’s guts worked! Thanks for this, it was such a bizarre phenomenon with a cause that was clearly connected to some basic, invisible processes. A fun one, indeed!

4 Likes

As a side note, I think this might be a bug in how [first time] is implemented. Comparing a text with a [first time] substitution in it shouldn’t use up the first time (i.e. shouldn’t set the “already executed this once” flag), but it should recognize that the flag isn’t set already. I’ll have to dig into the internals and see.

1 Like

Ah, yep, here’s the internal code.

To say first time -- beginning say_first_time (documented at phs_firsttime):
    (- {-counter-makes-array:say_first_time}
    if ((say__comp == false) && (({-counter-storage:say_first_time}-->{-counter:say_first_time}{-counter-up:say_first_time})++ == 0)) {-open-brace}
        -).
To say only -- ending say_first_time (documented at phs_firsttime):
    (- {-close-brace} -).

To break this down:

  • beginning say_first_time: This is a construction named say_first_time, and the beginnings and ends of this construction have to be matched.
  • {-counter-makes-array:say_first_time}: The compiler needs to generate an array with an entry for each usage of say_first_time in the program.
  • if ((say__comp == false) &&: If we’re not expanding text for comparison purposes, and
  • {-counter-storage:say_first_time}-->{-counter:say_first_time}{-counter-up:say_first_time}: The first bit will translate into the name of the array created above, the second bit will translate into the index of the current construction (i.e. which say_first_time is this, numbered sequentially?), and the third bit will increment that index (this is how they get numbered sequentially).
  • )++ == 0)): Take that whole mess (which translates to “this particular construction’s entry in the array”), compare it to 0, then increment it afterward.

So this skips evaluation completely if say_comp is true. Ideally, we’d have it only increment the variable if say_comp is false, but still check the variable if say_comp is true.

Which is doable! It’s a huge mess, but it’s doable.

As a first attempt:

To say first time -- beginning say_first_time (documented at phs_firsttime):
    (- {-counter-makes-array:say_first_time}
    if(
        ((say_comp == true) && (COUNTER == 0)) ||
        (COUNTER++ == 0)
    ) {-open-brace}
    {-counter-up:say_first_time} -).
To say only -- ending say_first_time (documented at phs_firsttime):
    (- {-close-brace} -).

Where COUNTER means {-counter-storage:say_first_time}-->{-counter:say_first_time}. So if we substitute that in:

To say first time -- beginning say_first_time (documented at phs_firsttime):
    (- {-counter-makes-array:say_first_time}
    if(
        ((say_comp == true) && ({-counter-storage:say_first_time}-->{-counter:say_first_time} == 0)) ||
        ({-counter-storage:say_first_time}-->{-counter:say_first_time}++ == 0)
    ) {-open-brace}
    {-counter-up:say_first_time} -).
To say only -- ending say_first_time (documented at phs_firsttime):
    (- {-close-brace} -).

Will this work? No idea! Testing this will be a pain, and I’m in a bit of a rush right now. But I think it should, and it should fix this bug.

2 Likes

Actually, we could make the logic a bit less convoluted, no?

To say first time -- beginning say_first_time (documented at phs_firsttime):
    (- {-counter-makes-array:say_first_time}
    if({-counter-storage:say_first_time}-->{-counter:say_first_time} == 0){-open-brace}
        if(say_comp == false) ({-counter-storage:say_first_time}-->{-counter:say_first_time})++;
    {-counter-up:say_first_time} -).
To say only -- ending say_first_time (documented at phs_firsttime):
    (- {-close-brace} -).

This should have the same effect in a less messy way. Basically we first check if it’s 0, then within the body we check if say_comp == false and tick up the counter if so.

This also means the variable won’t overflow after 18,446,744,073,709,551,616 iterations!

3 Likes