Strange error "I didn't understand that sentence" when locking/unlocking

Consider this example code:

The Red Room is a room.

The White Room is a room.

The red door is a locked door. It is north of the White Room and south of the Red Room.

The player is in the White Room.
The player carries a wallet and a key. The key unlocks the red door.

The Green Room is a room.
The mysterious door is a locked door. It is west of the Green Room. 
The Mysterious Hall is west of the mysterious door.
After deciding the scope of the player:
	place the Green Room in scope;
	
After reading a command:
	say "[the player's command]."

Test me with "unlock door / key".

Result:

White Room
You can see a red door here.

unlock door
unlock door.

What do you want to unlock the red door with?

key
unlock door it with key.

I didn’t understand that sentence.

Does anyone know what’s causing the parser to interpret my command as “unlock door it with key”? It only seems to happen under these specific circumstances:

  1. The player is carrying at least two things.
  2. The player has typed “unlock door”. Not “unlock red door” or “unlock door with key”.
  3. Another door (or room, which a door leads to) has been placed in scope.
2 Likes

I haven’t analyzed this, but it’s the bit of the parser code with the comment

! An inferred object.  Best we can do is glue in a pronoun.
! (This is imperfect, but it's very seldom needed anyway.)

By the way, the scope is a bit of a red herring here. You can see the same bug with this code:

The Red Room is a room.

The White Room is a room.

The red door is a locked door. It is north of the White Room and south of the Red Room.

The player is in the White Room.
The player carries a wallet and a key. The key unlocks the red door.

The Green Room is a room.
The mysterious door is a locked door. It is west of the Green Room and east of the White Room. 

The mysterious door is scenery.

The situation is that there are two doors in scope, but the parser selects one (the non-scenery one in this example) and moves on to the next stage of disambiguation.

I’ve got a stab at what’s happening, I think. The parser notices that it’s missing a preposition and indirect object early, before it thinks about needing to disambiguate “door”. So it sets the inferfrom global to 1 to indicate we’re going to need to add something after token 1. But then it notices that “door” is ambiguous. Adjudicate decides the red door edges out the mysterious door and we won’t need disambiguation… but since inferfrom is already set when all of that is considered, it’s confused and thinks the direct object is what we’re inferring, so it adds the pronoun.

I’m not sure what the motivation is for the whole pronoun thing. I’m wondering if maybe it’s a vestige of an outdated strategy for how to deal with a missing noun. At any rate, there’s no attempt to eliminate the existing tokens in the command that led to the choice of object – it just adds a word to the end, either a pronoun or a preposition. (In the latter case, it’s the right thing.)

In NounDomain in Parser.i6t we have:

if (number_matched > 1) {
                i = true;
            if (number_matched > 1)
                for (j=0 : j<number_matched-1 : j++)
                                if (Identical(match_list-->j, match_list-->(j+1)) == false)
                                        i = false;
                if (i) dont_infer = true;
        i = Adjudicate(context);
        if (i == -1) rfalse;
        if (i == 1) rtrue;       !  Adjudicate has made a multiple
                             !  object, and we pass it on
    }

    ! If i is non-zero here, one of two things is happening: either
    ! (a) an inference has been successfully made that object i is
    !     the intended one from the user's specification, or
    ! (b) the user finished typing some time ago, but we've decided
    !     on i because it's the only possible choice.
    ! In either case we have to keep the pattern up to date,
    ! note that an inference has been made and return.
    ! (Except, we don't note which of a pile of identical objects.)

    if (i ~= 0) {
        if (dont_infer) return i;
        if (inferfrom == 0) inferfrom=pcount;
        pattern-->pcount = i;
        return i;
}

If we add dont_infer = true; to the bottom of the if (number_matched > 1) conditional block, it fixes the behavior for this example.

Does it also horribly break something else because I’m stumbling around in very long, complicated code? Maybe.

2 Likes

In case it helps, I tried including Locksmith by Emily Short (in Zarf’s modified version of your example) and it no longer gave me that error. Besides which, players detest being asked what they want to unlock a door with when they’re carrying a key, and Locksmith gets you past that, too. However, I don’t know if your test case was explicitly about what it seems to be about.

-Wade

2 Likes

All this NounDomain stuff is way over my head. That said, I tried compiling and testing both the original code I posted and zarf’s amended version in 6G60: strangely enough, my version worked correctly but not zarf’s, whereas under 6M62 both versions produce “I didn’t understand that sentence.”

Yes, you are right that this whole thing with unlocking can be avoided by including Locksmith. But I just realised that it’s not just the scope thing that’s a red herring (as zarf pointed out), but unlocking and locking as well. The same thing can also happen with giving and showing (and maybe other actions applying to two things?)

For example:

The Recording Studio is a room.

John, Paul, George and Ringo are men in the Recording Studio.

The acoustic guitar and the electric guitar are things in the Recording Studio. The acoustic guitar is scenery.
	
After reading a command:
	say "the player's command: [the player's command]."
	
Test me with "show guitar / john / give guitar / john".

The error produced this time is "You can only do that to something animate:

Recording Studio
You can see John, Paul, George, Ringo and an electric guitar here.

test me
the player’s command: test me.

(Testing.)

[1] show guitar
the player’s command: show guitar.

Whom do you want to show the electric guitar to?

[2] john
the player’s command: show guitar it to john.

You can only do that to something animate.

[3] give guitar
the player’s command: give guitar.

Whom do you want to give the electric guitar to?

[4] john
the player’s command: give guitar it to john.

You can only do that to something animate.

Right, but the problem in this example is the same as in Zarf’s example. When we just type ‘show guitar’, Inform happens to be picking the one that’s scenery, and that leads to the bug. I mean, maybe you already understand that. If you’re asking what can we do about it – I’m not sure off the top of my head. Things that happen before you can jimmy them with a ‘Does the player mean…’ rule (like your example does) can be hard to fix.

The bug-avoiding solution would be to not allow scenery and something with a similar name to be in scope at the same time. Which often just means, not in the same location as each other, unless your game meddles with scope or has transparent windows you can look through or something like that.

Another way is to add in rules for supplying a missing noun (18.32 in the docs) but if the bug is widespread in its domain, that might be impractical, because you’d be adding it for every affected verb.

-Wade

Yeah, I was asking what the best way to deal with this was. Sorry for being unclear.

It seems there are three options:

  1. Go through the Actions index, check for potentially affected verbs, write a whole bunch of Understand “verb [something]” as verbing. Rule for supplying a missing second noun while verbing: … rules. Or Understand “show [something]” as showing vaguely … This is what Locksmith does with unlocking, and, as you said, it’s a good idea to include it because constantly being asked “What do you want to unlock that with?” when there’s one obvious candidate is annoying. But sometimes there isn’t an obvious candidate and I want the game to ask the player to clarify.

  2. Make sure that the game never has to ask “what do you want to verb that with …?” in the first place by making sure things that share a part of its name with other things aren’t in the same location. Unfortunately, I do have transparent windows/doors in my test game, as well as things the player can pick up and move around, so this isn’t ideal.

  3. Replacing the long (and scary-looking) “Noun Domain” just to add a single line, as Zed suggested. I’m a little nervous about doing this, as I’m not sure if it’ll have unintended consequences. That said, it seems to fix the problems I had with both lock/unlock and give/show, so unless something comes up, I’ll go with that solution for now.

Good luck. If that fix seems to not break everything, try to remember to report back in this thread! Actually, try to remember to report the opposite as well.

-Wade

Might also be worth reporting at the unofficial bug tracker: Issues ¡ i7/inform7-bugs ¡ GitHub.

I’d say being nervous about that course of action is wholly justified. I’m still working on my test suite but have scarcely scratched the surface of the parser itself.

I remembered a couple of extensions rewrite NounDomain and indeed Disambiguation Control by Jon Ingold fixes this behavior. But it introduces its own known issues:

“Doing something with” only checks for first noun:
→ particularly annoying for give and show, which check their reversed forms first
→ meaning the first attempt at parsing will ignore all rules about animate contexts!
→ current workaround throws out non-animates for giving and showing to.
This isn’t great (see comments in the appropriate section) - but the problem is the order in which the parser defines the grammar lines
and that’s impossible to change!
The parser will “auto-guess” if it matches a group of “indistinguishable” items, but the parser doesn’t always know when items are indistinguishable. In particular, if you use “[something related by containment]” to refer to a generic container / kind of container, the parser will treats these items as indistinguishable (same grammar) despite the fact the player can - easily - distinguish them.
→ Need some way to step in and tell the parser “these are not the same”
→ was possible in I6 “##TheSame” , not sure how to do it now

Responsive Disambiguation for 6M62 by Matt Weiner blew up with run-time errors for this case.

So… your choice of problems.

“Leave things as they are” is always a possible solution. This bug affects many Inform games. If yours is one of them, it’s not the end of the world.

(If UNLOCK is the verb that players will run into most often in your game, then the Locksmith extension is an easy answer.)

2 Likes

I happened across this old problem and have been looking at it.

As far as I can tell, Zed is correct about the nature of the fault. The global inferfrom is used both to signal that an inference has been made and to record the token position in the grammar line at which the first inference was made. The intended function of the “pronoun gluing” code is not clear; it seems best suited for cases in which a single suitable object is assumed while trying to infer the rest of an incomplete command, but it does not appear to be executed in such cases.

The handling of pronouns is such that they immediately halt parsing of a noun phrase, so, as Zed points out, adding one to a noun phrase is inherently non-functional unless any other words in the noun phrase are eliminated from the command buffer.

The only way to address this conclusively that occurs to me is to attempt to blank out any matching words in the command buffer before inserting the pronoun. The parser is working within a framework of grammar line tokens and does not keep track of which command words correspond to each token. It would be relatively straightforward to set up another array used by the parser (alongside the other line_* arrays) to store this information, perhaps using the same storage format as snippets. This information could then be used to blank out the noun phrase that matched the inferred object prior to insertion of the pronoun.

I built a proof-of-concept of this that seems to solve the OP’s problem. The guts of it are:

Array line_matchspans --> 32;

...

[ WipeSpan wd_start wd_count buf chr_start chr_count i;
	chr_start = WordAddress(wd_start);
	chr_count = WordAddress(wd_start + wd_count - 1) + WordLength (wd_start + wd_count - 1) - chr_start;
	for (i = 0: i < chr_count: i++)
		chr_start->i = ' ';
];

...

! [following are changes within NounDomain]

...
if (number_matched > 1) {
	i = true;
	if (number_matched > 1)
		for (j=0 : j<number_matched-1 : j++)
			if (Identical(match_list-->j, match_list-->(j+1)) == false)
				i = false;
	if (i) dont_infer = true;
	i = Adjudicate(context);
	line_matchspans-->pcount = (wn - match_length)*100+match_length; 	! ADDED
	if (i == -1) rfalse;
	if (i == 1) rtrue;		!  Adjudicate has made a multiple
							!  object, and we pass it on
}
...
! An inferred object.  Best we can do is glue in a pronoun.
! (This is imperfect, but it's very seldom needed anyway.)

if (pattern-->j >= 2 && pattern-->j < REPARSE_CODE) {
	if (line_matchspans-->j % 100 ~= 0)												! ADDED
		WipeSpan(line_matchspans-->j / 100, line_matchspans-->j % 100, buffer);		! ADDED
	PronounNotice(pattern-->j);
...

The parser inserts the pronoun after the end of the partial command, so I don’t think that it’s necessary to check that the inserted pronoun will fit in the space originally occupied by the ambiguous noun phrase. However, this version is confused by partial commands that include a preposition such as >UNLOCK DOOR WITH (which is translated to >UNLOCK WITH IT WITH KEY). Since the code doesn’t seem to care whether the player included a preposition or not, the call to WipeSpan() can be modified to include everything from the start of the noun phrase to the end of the partial command:

		WipeSpan(line_matchspans-->j / 100, (num_words - (line_matchspans-->j % 100)), buffer);
1 Like

I had reported this (as I7-2115) and it was fixed in 10.1.

1 Like

How interesting! The official solution is to introduce a new global dont_infer_pronoun, used as a flag, which is set when Adjudicate() has determined a single best match, and which when set suppresses the “pronoun gluing” code.

@DavidG, you may be interested in adapting this to StdLib 6.12 if you have not already.

1 Like

I’ve been pondering something like this, but haven’t really explored it or even written it down in my notes.