On the woes of disambiguation

Warning: Following treatise contains the word ‘disambiguation’ disgusting times.

In Dialog's disambiguation the following scenario always bothered me.
> take fish
Did you want to take the fish or the red fish?

> 
Did you want to take the fish or the red fish?

> 
Did you want to take the fish or the red fish?

> 
Did you want to take the fish or the red fish?

> 
Did you want to take the fish or the red fish?

> 
Did you want to take the fish or the red fish?

> 
Did you want to take the fish or the red fish?

> 
Did you want to take the fish or the red fish?

> 
Did you want to take the fish or the red fish?

> 
Did you want to take
(Technical trouble: Heap space exhausted. Attempting to recover with UNDO.)
Undoing the last turn (take fish).

If the player answers a single sentence disambiguation question in such a way that the disambiguation goes for another round, after several times heap space gets exhausted. It depends on the length of the candidate action list how fast the heap will get busted (nine rounds above are from an actual scenario, not imaginary). The really bothering part is that empty input (like in the example given above) always prompts for another round. This is due to (words [] sufficiently specify $Obj) always succeeding in conjunction with how (determine object $) statements matches successfully with an empty word list for matching.

While I was working on another tweak with the disambiguation mechanism, I have accidentally circumvented this issue. What I wanted was turning this:

> take pot
Did you want to take the plant pot or the pot plant?

> pot
Did you want to take the plant pot or the pot plant?

into this:

> take pot
Did you want to take the plant pot or the pot plant?

> pot
Did you want to:
1. take the plant pot, or
2. take the pot plant?
(Type the corresponding number)

Basically, I wanted the disambiguation mechanish branch into enumerated disambiguation instead of single sentence one if the player’s input failed to reduce the candidate list, to help with scenarios where one of the objects can’t be distinguished by any words over the other one. N.B. The ‘plant pot’ vs ‘pot plant’ issue can be solved by (heads $) quite easily and conveniently, but the ‘fish’ vs ‘red fish’ can’t be solved without declaring ‘red’ as head, thereby angering language purists.

Since the empty input matches all of the candidates, this behaviour also works for just pressing the enter without any text.

> take pot
Did you want to take the plant pot or the pot plant?

>
Did you want to:
1. take the plant pot, or
2. take the pot plant?
(Type the corresponding number)

While I was happy with the resulting change, I have noticed that the enumerated disambiguation doesn’t recurse like the single sentence one. If the player enters a text that is not a number on the list (inluding an empty string), this type of disambiguation question just fails and gives up. No recursion → no heap space issues. And that’s two dodos for the same stone.

The following code with an example scenario are presented as library overrides (5 of them) with comments used as diff to original library (can't use colors in the preformatted text).
%% Library overrides

%%+
%% Modified for new signature of (disambiguate action ...) rule
(consider action candidates $List $Words)
%%+
	(just)
	(if) ($List = [$Single]) (then)
		%% Optimize the common case.
		(try-complex $Single)
	(else)
		%% Eliminate options where the head noun is missing.
		(now) (head noun is required)
		(collect $A)
			*(understand $Words as $A)
			($A is one of $List)
		(into $ShortListWithDup)
		(remove duplicates $ShortListWithDup $ShortList)
		(if) ($ShortList = [$Single]) (then)
			(try-complex $Single)
		(else)
			(if) (empty $ShortList) (then)
%%+
				(disambiguate action $List $ComplexAction 0)
%%-				(disambiguate action $List $ComplexAction)
			(else)
%%+
				(disambiguate action $ShortList $ComplexAction 0)
%%-				(disambiguate action $ShortList $ComplexAction)
			(endif)
			(try-complex $ComplexAction)
		(endif)
	(endif)

%%+++
%% Now proceeds to enumerated disambiguation if the candidate list
%% remains the same after a disambiguation round
(disambiguate action $List $Result $PrevNumObjects)
%%-(disambiguate action $List $Result)
	(collect $Action)
		*($Action is one of $List)
		~{
			*($Action recursively contains $Obj)
			($Obj is hidden)
		}
	(into $NonSpoilery)
	(if) (empty $NonSpoilery) (then)
		($AskList = $List)
	(else)
		($AskList = $NonSpoilery)
	(endif)
	(if) ($AskList = [$Single]) (then)
		($Result = $Single)
	(elseif)
		(rephrase as object disambiguation
			$AskList
			$ComplexAction
			$Template
			$ObjList)
%%++
			%% Branch to enumeration if the object list remains same
			~(length of $ObjList into $PrevNumObjects)
	(then)
		Did you want to (describe action $ComplexAction)?
		(par)
		(prompt and input $Words)
		{
			(now) (head noun is required)
			(disambiguate by object name $Words $Template $ObjList $Result)
		(or)
			(now) ~(head noun is required)
			(disambiguate by object name $Words $Template $ObjList $Result)
		(or)
			(now) (deferred commandline $Words)
			(stop)
		}
	(else)
		Did you want to: (line)
		(enumerate actions $AskList 1 $)
		(if) ~{ (library links enabled) (interpreter supports links) } (then)
			\( Type the corresponding number \)
		(endif)
		(par)
		(prompt and input $Words)
		{
			(understand $Words as number $N)
			($N > 0)
			(nth $AskList $N $Result)
		(or)
			(now) (deferred commandline $Words)
			(stop)
		}
	(endif)

%%+
%% This one is not modified, here for proper fallback sequence
(disambiguate by object name [me/myself] $Template $ObjList $Result)
	(current player $Player)
	($Player is one of $ObjList)
	(recover implicit action $Template $Player into $Result)

(disambiguate by object name $Words $Template $ObjList $Result)
	%% Since the player was given an explicit list of '(the full $)'
	%% descriptions, we have to match against those, in addition to the
	%% normal dict rules.

	(collect $A)
		(determine object $Obj)
			*($Obj is one of $ObjList)
			~(direction $Obj)
			~(relation $Obj)
			(words $Words sufficiently specify $Obj)
		(from words)
			(the full $Obj)
		(matching all of $Words)
		(recover implicit action $Template $Obj into $A)
	(into $Candidates)
	(nonempty $Candidates) %% otherwise fail into the next rule
	{
		($Candidates = [$Result])
	(or)
%%+++
		%% Supporting branch to enumeration if the object list remains same
		(length of $ObjList into $NumObjects)
		(disambiguate action $Candidates $Result $NumObjects)
%%-		(disambiguate action $Candidates $Result)
	}

(disambiguate by object name $Words $Template $ObjList $Result)
%%+
	(just)
	(collect $A)
		(determine object $Obj)
			*($Obj is one of $ObjList)
			~(direction $Obj)
			~(relation $Obj)
			(words $Words sufficiently specify $Obj)
		(from words)
			*(dict $Obj)
		(matching all of $Words)
		(recover implicit action $Template $Obj into $A)
	(into $Candidates)
	(nonempty $Candidates)
	{
		($Candidates = [$Result])
	(or)
%%+++
		%% Supporting branch to enumeration if the object list remains same
		(length of $ObjList into $NumObjects)
		(disambiguate action $Candidates $Result $NumObjects)
%%-		(disambiguate action $Candidates $Result)

	}

%% Example scenario

#player
(current player *)
(* is #in #office)

#office
(name *)	the office
(room *)
(look *)	It's an office.

#fish
(name *)	fish
(item *)

#redfish
(name *)	red fish
(item *)

(#fish/#redfish is #in #office)
((item $) is handled)

4 Likes

Sorry, no help, but I will say that when I wrote my own game engine, disambiguation was one of the two real headaches to resolve (scope was the other).

2 Likes

If this was Inform 6, I’d suggest writing a parse_name routine. I do it all the time. Does Dialog have something similar?

3 Likes

Do you mean something to this effect?

> x plant      
Did you want to examine the plant pot or the pot plant?

> plant
It's a pot plant.

> x pot
Did you want to examine the plant pot or the pot plant?

> plant pot
It's a plant pot.

If yes, I have already done that. That is another tweak I have added to the disambiguation in Dialog, and can be achieved by the story author just by adding :

(prefer #plantpot over #potplant when *($ is one of [[pot] [plant pot]]))
(prefer #potplant over #plantpot when *($ is one of [[plant] [pot plant]]))

Those rules only affect the disambiguation parsing.

Dialog has a general parse name routine as well. But it is not needed or used in the disambiguation routines, they have their own mini parser and scope, I have just extended them.

3 Likes

Exactly! I don’t know Dialog, but even I can read that and it looks like a fairly elegant way of handling the classic Magnetic Scrolls conundrum.

Coincidentally, I had a pot plant in a plant pot in Acid Rain. It was a bit of an in-joke, following a discussion of this in the ZIL Facebook group (if I remember rightly). The same game also had a funny response to USE BROOM following another Facebook discussion on the use of USE.

3 Likes

i do, actually, need an inform 6 parse_name equivalent, outside of a disambiguation context.

i have an object that needs to know what (dict $) value was used to refer to it and i’m not sure the best way to capture that.

Here’s one way to do it.

The standard predicate for parsing object names, which all the special cases eventually descend to, is (parse object name $Words as $Result $All $Policy). At the end of that predicate, $Result is a list of the parsed objects to be passed to the disambiguator, and $Filtered is a list of words used to recognize those objects.

So add this code to the end of that rule in the standard library:

(exhaust) {
	*($Tmp is one of $Result)
	($Tmp was recognized with words $Filtered)
}

Then you should add a default rule for this new predicate, so that it doesn’t fail:

($ was recognized with words $)

But then in your game code, you can override it for the object where it matters:

(#cube was recognized with words $Words)
	%% your code here

Note that this happens before disambiguation. If that doesn’t work for your use case, you’ll have to tap in somewhere else.

3 Likes

that’s exactly what i needed. thx!

1 Like