Unlocking while empty-handed always disambiguates to something worn

Hey All–

More disambiguation problems, of course.

I have a locked door. The player wears one item which they cannot remove but is otherwise empty-handed. Before the player finds the key to the door, if you try unlocking it, Inform will always choose the worn item to try unlocking the door with. I have rules up the wazoo about this:

Does the player mean doing something with the hat:
	it is very unlikely.			

Does the player mean unlocking something with the hat:
	it is very unlikely.

Rule for supplying a missing second noun while unlocking:
	if the player does not carry the brass key:
		say "You'll have to specify what you want to unlock it with." instead.

And here’s the unlocking action:

Check unlocking the wood doorA with something:
	if the second noun is the brass key:
		continue the action;
	otherwise:
		if the player does not carry the brass key:
			say "You don't have a key.";
            stop the action.

And all this does not stop Inform from trying to unlock the door with the hat. I have scoured the code for anything that could interfere with this, but I can’t find anything. How can I stop this annoying behavior?

This is a terrible answer, but I’ve always just made sure the player has some object at the start of the game that they probably won’t drop.

(Two objects, really. But the hat counts as one.)

1 Like

Having an “It is impossible” DTPM option would be nice.

1 Like

Unfortunately, I don’t think the underlying code supports that idea. That is, the list of possible objects can’t change length at that stage of the parser. (I haven’t looked in there for a good long while, though.)

1 Like

The parser has several places where a consideration of whether a given thing is a reasonable choice for a given action happens. For each it has its own default rules that can be overridden with your own rules:

  • whether all includes (only happens with all, of course, so only with taking, dropping, inserting it into, removing it from… I think that’s all of them) [Edited: oops, forgot putting it on]
  • supplying a missing noun (or second noun)
  • disambiguation (does the player mean)

Trouble is, when you just say >unlock door it uses none of them. Then you get inference, which pretty much doesn’t care how ridiculous something is. If the player weren’t wearing a hat, inference would result in trying to unlock the door with the door.

Per the example in WI 18.32, you could write a for supplying a missing second noun rule… but an essential ingredient is:

Understand "unlock [something]" as unlocking it with.

or it won’t get as far as considering supplying a missing second noun.

Some months ago, I was complaining about this behavior via direct message with @OtisTDog who went and elegantly solved the darn problem by implementing a deciding whether to infer something activity. I assume Otis wouldn’t mind my sharing…

6m62.txt (14.0 KB)
10.1.txt (14.6 KB)

3 Likes

Another imperfect-but-better-than-nothing option is to add Locksmith by Emily Short, or any other extension that adds a keyless unlocking action. (Implicit Actions by Eric Eve does too, and at least one other I can’t remember off the top of my head.)

Then UNLOCK DOOR will be parsed as a valid command all on its own, and you don’t have to worry about disambiguation.

1 Like

Oh, that’s an excellent addition. I hope it gets added to the extension repositories at some point because it’s a clear improvement.

1 Like

The reason the DTPM rules can’t help here is that when inferring a missing noun the parser first sets up a list of all objects in scope* (including the player object) then, before proceeding, checks that list against the context of the grammar token it’s trying to match- here the [HELD_TOKEN] token (in I7 terms the [something preferably held] token).

This preliminary check for context marks all objects possessed by the player (i.e. carried or worn) as ‘good’ and the rest ‘bad’ and if only one object match is needed, and there is just one ‘good’ object, further attempts at disambiguation are abandoned there and then.

Consequently, the disambiguation process doesn’t get as far as the DTPM rules.

EDIT: in case anyone was wondering, the reason Zarf’s trick works is that as long as the player is carrying or wearing more than one thing, the preliminary check marks both those things as ‘good’, can’t make an immediate decision, and therefore goes on to score all the objects on the list against a number of criteria, most importantly how they rank in the DTPM rules.

EDIT2:
*For the HELD_TOKEN (and some others) the direction objects are excluded from the list, but for the NOUN_TOKEN they are included.

1 Like

If you don’t like this behaviour, you can include this alternate form of the Adjudicate function that only marks objects as ‘good’ in the context of a HELD_TOKEN or MULTI_HELD token (the latter being the I7 grammar token [things preferably held]) if they are carried, not worn.

In the situation under discussion, this means that the hat is also marked as ‘bad’ and disambiguation goes on the next stage, which involves allocating ‘appropriateness scores’ to all objects on the list- which gives ‘Does the player mean’ rules a chance to influence the outcome.

Without a DTPM rule, the hat will probably still be chosen as the highest-scoring item on the list, because in the context of a HELD_TOKEN things in the possession of the player will score slightly higher. However, DTPM trumps everything else, and objects that outrank others in the DTPM rules will always score higher. So ‘Does the player mean unlocking something with the hat: it is very unlikely’ will definitively prevent the hat being chosen.

NB the below uses the new ‘replacement of functions syntax’ for Ver 10- would need tweaking for 6M62/9.3, but the Adjudicate function itself works fine in both

Include (-
[ Adjudicate context i j k good_ones last n ultimate flag offset;
	if ((parser_trace >= 4)) {
		print "   [Adjudicating match list of size ";
		print number_matched;
		print " in context  ";
		print context, "  (";
		switch (context) {
			0: print "NOUN_TOKEN";
			1: print "HELD_TOKEN";
			2: print "MULTI_TOKEN";
			3: print "MULTIHELD_TOKEN";
			4: print "MULTIEXCEPT_TOKEN";
			5: print "MULTIINSIDE_TOKEN";
			6: print "CREATURE_TOKEN";
			7: print "SPECIAL_TOKEN";
			8: print "NUMBER_TOKEN";
			9: print "TOPIC_TOKEN";
			15: print "TOPIC_TOKEN";
			default: print "Unknown token";
		}
		print ")";
		print "^";
		print "   ";
		if  (indef_mode) {
			print "indefinite type: ";
			if (((indef_type)&(OTHER_BIT))) {
				print "other ";
			}
			if (((indef_type)&(MY_BIT))) {
				print "my ";
			}
			if (((indef_type)&(THAT_BIT))) {
				print "that ";
			}
			if (((indef_type)&(PLURAL_BIT ))) {
				print "plural ";
			}
			if (((indef_type)&(LIT_BIT))) {
				print "lit ";
			}
			if (((indef_type)&(UNLIT_BIT))) {
				print "unlit ";
			}
			if ((indef_owner  ~= 0)) {
				print "owner:";
				PrintShortName(indef_owner);
			}
			print "^";
			print "   number wanted: ";
			if ((indef_wanted == INDEF_ALL_WANTED)) {
				print "all";
			} else {
				print indef_wanted ;
			}
			print "^";
			print "   most likely GNAs of names: ";
			print indef_cases ;
			print "^";
		} else {
			print "definite object^";
		}
	} 
	if (parser_trace >= 4) {
		print "      [Assessing matched objects in context]^";
	}
	( j = (number_matched - 1));
	(good_ones  = 0);
	(last  = (match_list-->(0)));
	for ((i = 0):(i <= j):(i)++) {
		(n = (match_list-->(i)));
		((match_scores-->(i)) = good_ones );
		(ultimate = ScopeCeiling(n));
		if ((((context == HELD_TOKEN )) && ((parent(n) == actor)) && ((n hasnt worn)))) {  !#########  && ((n hasn't worn)) inserted ########
			(good_ones)++;
			(last = n);
		}
		if ((((((((((context == MULTI_TOKEN )) && ((ultimate == ScopeCeiling(actor))))) && ((n ~= actor)))) && ((~~((n has concealed)))))) && ((~~((n has scenery)))))) {
			(good_ones)++;
			(last = n);
		}
		if ((((context == MULTIHELD_TOKEN)) && ((parent(n) == actor)) && ((n hasnt worn)))) { !#########  && ((n hasn't worn)) inserted ########
			(good_ones)++;
			(last = n);
		}
		if ((context == MULTIEXCEPT_TOKEN or MULTIINSIDE_TOKEN)) {
			if  ((advance_warning == -1)) {
				if ((context == MULTIEXCEPT_TOKEN)) {
					(good_ones)++;
					(last = n);
				}
				if ((context == MULTIINSIDE_TOKEN)) {
					if ((parent(n) ~= actor)) {
						(good_ones)++;
						(last = n);
					}
				}
			} else  {
				if ((((context == MULTIEXCEPT_TOKEN)) && ((n ~= advance_warning)))) {
					(good_ones)++;
					(last = n);
				}
				if ((((context == MULTIINSIDE_TOKEN)) && ((n in advance_warning)))) {
					(good_ones)++;
					(last = n);
				}
			}
		}
		if ((((context == CREATURE_TOKEN )) && ((CreatureTest(n) == 1)))) {
			(good_ones)++;
			(last = n);
		}
		((match_scores-->(i)) = (1000*(good_ones - (match_scores-->(i) ))));
		if (parser_trace >= 4) {
			print "         ", (CDefArt) n, ": ";
			if (good_ones >0 && last == n) 
				print "Good^";
			else
				print "Bad^";
			
		}
	}
	if ((good_ones == 1 )) {
		if  ((((((indef_mode == 1)) && ((((indef_type)&(PLURAL_BIT)) ~= 0)))) && ((context == MULTI_TOKEN or MULTIHELD_TOKEN or MULTIEXCEPT_TOKEN or MULTIINSIDE_TOKEN)))) {
			BeginActivity(DECIDING_WHETHER_ALL_INC_ACT, last);
			if (((ForActivity(DECIDING_WHETHER_ALL_INC_ACT, last)) && (RulebookFailed()))) {
				(good_ones = 0 );
			}
			EndActivity(DECIDING_WHETHER_ALL_INC_ACT, last);
			if ((good_ones == 1)) {
				if (parser_trace >= 4) {
					print "      [Single good object returned from Adjudicate]^";
				}
				return last ;
			} else {
				if (parser_trace >= 4) {
					print "      [Single good object excluded from ALL]^";
				}
			}
		} else   {
			if (parser_trace >= 4) {
				print "      [Single good object returned from Adjudicate]^";
			}
			return last ;
		}
	} 
	if  ((((context == CREATURE_TOKEN)) && ((good_ones == 0)))) {
		if (parser_trace >= 4) {
			print "      [Nothing matched CREATURE_TOKEN- first matched object returned from Adjudicate]^";
		}
		return (match_list-->(0));
	}
	if ((indef_mode == 0)) {
		(indef_type = 0 );
	}
	if (parser_trace >=4) print "      [more than one or no good object(s) found- proceeding to score all matches]^";
	ScoreMatchL (context);
	if ((number_matched == 0)) {
		return (-(1)); 
	}
	if ((indef_mode == 0 )) {
		(i = SingleBestGuess());
		if ((i >= 0)) {
			if ((parser_trace >= 4)) {
				print "    [Single best-scoring object returned.]^";
			}
			return i;
		}
	}
	if ((((indef_mode == 1 )) && ((((indef_type)&(PLURAL_BIT)) ~= 0)))) {
		if ((context ~= MULTI_TOKEN or MULTIHELD_TOKEN or MULTIEXCEPT_TOKEN or MULTIINSIDE_TOKEN)) {
			(etype = MULTI_PE );
			if ((parser_trace >= 4)) {
				print "    [Multiple items matched in non-multi-context]^";
			}
			return (-(1));
		}
		(i = 0);
		(offset = (multiple_object-->(0)));
		for  ((j = BestGuess()):(((((j ~= -1)) && ((i < indef_wanted)))) && (((i + offset) < (MATCH_LIST_WORDS - 1)))):(j = BestGuess())) {
			(flag = 0);
			BeginActivity(DECIDING_WHETHER_ALL_INC_ACT, j);
			if ((ForActivity(DECIDING_WHETHER_ALL_INC_ACT, j) == 0)) { ! made no decision if returns 0
				if ((((~~((j has concealed)))) && ((~~((j has worn)))))) {
					(flag = 1); ! reject concealed or worn items
				}
				if ((((context == MULTIHELD_TOKEN or MULTIEXCEPT_TOKEN)) && ((parent(j) ~= actor)))) {
					(flag = 0); ! and can't discard what's not held
				}
				if ((((action_to_be == ##Take or ##Remove)) && ((parent(j) == actor)))) {
					(flag = 0); ! and can't take what's already held
				}
				(k = ChooseObjects(j, flag)); ! flag =0 for presumed rejections. 
				if ((k == 1)) {
					(flag = 1); ! in theory allows ChooseObjects to overrule flag, by returning 1 or 2, but by default always returns false when called c flag 0 or 1
				} else {
					if ((k == 2)) {
						(flag = 0);
					}
				}
			} else { !DWAI made a decision
				(flag = 0);
				if (RulebookSucceeded()) {  !so flag as included if rulebook succeeded ('it does')
					(flag = 1);
				}
			}
			EndActivity(DECIDING_WHETHER_ALL_INC_ACT, j);
			if  ((flag == 1)) {
				(i)++;
				((multiple_object-->((i + offset))) = j);
				if ((parser_trace >= 4)) {
					print "   Accepting it^";
				}
			} else {
				(i = i);
				if ((parser_trace >= 4)) {
					print "   Rejecting it^";
				}
			}
		}
		if  ((((i < indef_wanted)) && ((indef_wanted < INDEF_ALL_WANTED)))) {
			(multi_wanted = indef_wanted);
			if ((parser_trace >= 4)) {
				print "Too few found in Adjudicate^";
			}
			(multi_had = i);
		}
		((multiple_object-->(0)) = (i + offset)) ;
		(multi_context = context);
		if ((parser_trace >= 4)) {
			print "   Made multiple object of size ";
			print i;
			print "]^";
		}
		rtrue;
	}
	for  ((i = 0):(i < number_matched):(i)++) {
		((match_classes-->(i)) = 0);
	}
	(n = 1);
	for ((i = 0):(i < number_matched):(i)++) {
		if (((match_classes-->(i)) == 0)) {
			((match_classes-->(i)) = (n)++);
			(flag = 0);
			for ((j = (i + 1)):(j < number_matched):(j)++) {
				if (((((match_classes-->(j)) == 0)) && ((Identical((match_list-->(i)), (match_list-->(j))) == 1)))) {
					(flag = 1);
					((match_classes-->(j)) = (match_classes-->(i)));
				}
			}
			if ((flag == 1)) {
				((match_classes-->(i)) = (1 - n));
			}
		}
	}
	(n)--;
	(number_of_classes = n);
	if ((parser_trace >= 4)) {
		print "   Grouped into ";
		print n;
		print " possibilities by name:^";
		for ((i = 0):(i < number_matched):(i)++) {
			if (((match_classes-->(i)) > 0)) {
				print "   ";
				CDefArt((match_list-->(i)));
				print " (";
				print (match_list-->(i));
				print ")  ---  group ";
				print (match_classes-->(i));
				print "^";
			}
		}
	}
	if  ((indef_mode == 0)) {
		if ((n > 1)) {
			(k = -1);
			for ((i = 0):(i < number_matched):(i)++) {
				if (((match_scores-->(i)) > k)) {
					(k = (match_scores-->(i)));
					(j = (match_classes-->(i)));
					(j = (j*j));
					(flag = 0);
				} else {
					if (((match_scores-->(i)) == k)) {
						if ((((match_classes-->(i))*(match_classes-->(i))) ~= j)) {
							(flag = 1);
						}
					}
				}
			}
			if (flag) {
				if ((parser_trace >= 4)) {
					print "   Unable to choose best group, so ask player.]^";
				}
				rfalse;
			}
			if ((parser_trace >= 4)) {
				print "   Best choices are all from the same group.^";
			}
		}
	}
	if  ((n == 1)) {
		(dont_infer = 1);
	}
	return BestGuess();
];
-) replacing "Adjudicate".
2 Likes

This is unduly harsh. Inference does care how appropriate something is, it’s just that you may not agree with how it goes about it. The issue here is that it really, really, really wants to infer a sole direct possession of the player in the context of a HELD_TOKEN. It’s unreasonable to expect it to know that an object named ‘hat’ is a ridiculous thing to unlock a door with.

Furthermore, if it can’t fulfil its very strong first preference, inference does go on to consider DTPM rules (as well as multiple other criteria) in an attempt to choose the best available object. If it winds up with a tied score for the best object, it will then ask the player to disambiguate.

The solution I posted tweaks inference so its very strong preference is to infer a sole carried object rather than a sole possessed object.

2 Likes
Lab is a room.
Alice is a person in the Lab.
Persuasion rule for asking people to try doing something: yes.
Test me with "alice, unlock"
Lab
You can see Alice here.
 
>alice, unlock
(Alice with Alice)
(Alice first taking Alice)
Alice has better things to do.
 
> 

My statement was, perhaps, unduly harsh in regard to cases where there is a plausible item about. For the case where there are fewer non-player things around than there are required nouns, I think the harshness is due.

3 Likes

Well yes and no. You can’t argue with the fact that Alice was the best object available!

In this contrived setup, all available objects:
for the inferred door/container (NOUN_TOKEN)- the direction objects, player object and Alice,
for the inferred key (HELD_TOKEN)- the player object and Alice,
are marked as ‘Bad’,
so these objects go on to be scored and Alice scores highest for both tokens,

  • vs the player object on account of her being unconcealed while the player object is concealed (undescribed, in I7 terminology)
  • vs the compass directions through being in the location and not scenery

These, I would suggest, are not bad decisions by the inference algorithm given the grammar tokens it’s matching against. It doesn’t and can’t know (at least, without additional help from DTPM rules) what objects are reasonable for the specific action being considered.

It’s a different question whether the Standard Rules should subsequently decide that the objects chosen by inference are inappropriate to the action they’ve been inferred into.

And here you’ve sidestepped the opportunity to tell the inference algorithm via DTPM that Alice unlocking stuff with herself is unreasonable, or that unlocking any person is unreasonable, or that unlocking anything with a person is unreasonable.

EDIT: I think to a certain extent you’re arguing here against the stated design preference of the parser: to put the best interpretation it can upon daft commands in context (like this one) rather than reject them outright. It could reject a command where all objects in scope fail the preliminary context check, rather than attempt to find a ‘best choice’ through scoring them (potentially helped along the way by DTPM rules), but such a strategy would reject a number of convenient and reasonable commands, such as unlocking a door with a key lying on the floor (after an implied take)- see post below.

1 Like

I usually do this. I mean I actually have zero doors that are unlocked conventionally with keys, but just adding Locksmith usually makes a bunch of annoying side-problems go away, even if you’re not really using it (like me).

It does add a lot of synonyms for unlocking things, so you have to have a look at those and support them or be mindful of them, but overall, I find it always improves the situation.

-Wade

3 Likes

I appreciate the vote of confidence, but what I sent was more in the spirit of a proof-of-concept that inference could be influenced than it was a full-blown solution. Anyone is free to play with it, but it almost certainly needs refinement.

Looking at drpeterbatesuk’s responses here, I’m wondering whether it would be better to insert something into the adjudication process instead. It could offer finer control.

2 Likes

As counterpoint, here’s inference making intelligent choices when provided with the necessary contextual information:

A key is a kind of thing.
Lab is a room. A key called the brass key is here. The the Dispensary door is a locked door. It is east of Lab. It is proper-named. The matching key is the brass key. The the Kitchen door is an open door. It is proper-named. It is north of Lab. Alice is a woman in the Lab. The workbench is a supporter in the Lab. The rat is an animal on the workbench. The aspidistra is a thing in the Lab.
Persuasion when asking Alice to try doing something: rule succeeds.
Does the player mean unlocking a locked door with: it is likely.
Does the player mean asking someone to try unlocking a locked door with: it is likely.
Does the player mean unlocking a locked door with a key: it is very likely.
Does the player mean asking someone to try unlocking a locked door with a key: it is very likely.
Does the player mean locking an unlocked closed door with: it is likely.
Does the player mean asking someone to try locking an unlocked closed door with: it is likely.
Does the player mean locking an unlocked closed door with a key: it is very likely.
Does the player mean asking someone to try locking an unlocked closed door with a key: it is very likely.
Does the player mean opening an open door: it is very unlikely.
Does the player mean asking someone to try opening an open door: it is very unlikely.
Does the player mean opening a closed door: it is likely.
Does the player mean asking someone to try opening a closed door: it is likely.


Test me with "open/unlock/lock/drop key/rat,unlock/Alice,unlock/Alice,lock/Alice,open".

output

Lab
You can see the brass key, the Dispensary door, the Kitchen door, Alice, a workbench (on which is a rat) and an aspidistra here.

>test me
(Testing.)

>[1] open
(the Dispensary door)
It seems to be locked.

>[2] unlock
(the Dispensary door with the brass key)
(first taking the brass key)
You unlock the Dispensary door.

>[3] lock
(the Dispensary door with the brass key)
You lock the Dispensary door.

>[4] drop key
Dropped.

>[5] rat,unlock
(the Dispensary door with the brass key)
(the rat first taking the brass key)
The rat has better things to do.

>[6] alice,unlock
(the Dispensary door with the brass key)
(Alice first taking the brass key)
Alice unlocks the Dispensary door.

>[7] alice,lock
(the Dispensary door with the brass key)
Alice locks the Dispensary door.

>[8] alice,open
(the Dispensary door)
Alice is unable to do that.
2 Likes

Thanks, everyone. I’ll have a look at Locksmith and see what it does.