disambig.t
#charset "us-ascii"
/*
* Copyright (c) 2000, 2006 Michael J. Roberts. All Rights Reserved.
*
* TADS 3 Library: disambiguation
*
* This module defines classes related to resolving ambiguity in noun
* phrases in command input.
*/
#include "adv3.h"
/* ------------------------------------------------------------------------ */
/*
* Distinguisher. This object encapsulates logic that determines
* whether or not we can tell two objects apart.
*
* Each game object has a list of distinguishers. For most objects, the
* distinguisher list contains only BasicDistinguisher, since most game
* objects are unique and thus are inherently distinguishable from all
* other objects.
*/
class Distinguisher: object
/* can we distinguish the given two objects? */
canDistinguish(a, b) { return true; }
/*
* Note that we're showing a prompt to the player asking for help in
* narrowing the object list, based on this distinguisher. 'lst' is
* the list of ResolveInfo objects which we're mentioning in the
* prompt.
*
* By default, we do nothing. Some types of distinguishers might
* want to do something special here. For example, an ownership
* distinguisher might want to set pronoun antecedents based on the
* owners mentioned in the disambiguation prompt, so that the
* player's response can refer anaphorically to the nouns in the
* prompt.
*/
notePrompt(lst) { }
/*
* Is the object in scope for the purposes of the disambiguation
* reply from the player? By default, any object in the full match
* list is in scope.
*
* Distinguishers that can use related objects to qualify the name
* should add those related objects to the scope by returning true
* here. For example, the locational distinguisher can use the
* location name as a qualifying phrase, so the location name is in
* scope.
*/
objInScope(obj, matchList, fullMatchList)
{
/* it's in scope if it's in the full match list */
return fullMatchList.indexWhich({x: x.obj_ == obj}) != nil;
}
/*
* Try matching an object to a noun phrase in a disambiguation reply
* from the player (that is, the player's response to a "Which foo
* did you mean" question). By default, we call the object's
* matchNameDisambig() method to let it try to match its
* disambiguation name.
*
* Subclasses can override this to check for additional phrasing
* specific to the subclass. For example, the locational
* distinguisher checks for a match to the container or owner name,
* so that the player can simply respond to the question with the
* location name rather than typing in a whole locational phrase.
* Note that subclasses will usually want to inherit the default
* handling if they don't find a match to their own special phrasing,
* because the player might respond with a simple adjective
* pertaining to the base object even if there's some external
* distinguishing characteristic handled by the subclass.
*/
matchName(obj, origTokens, adjustedTokens, matchList, fullMatchList)
{
/* try matching the object's disambiguation name */
return obj.matchNameDisambig(origTokens, adjustedTokens);
}
;
/*
* A "null" distinguisher. This can tell two objects apart if they have
* different names (so it's inherently language-specific).
*/
nullDistinguisher: Distinguisher
;
/*
* "Basic" Distinguisher. This distinguisher can tell two objects apart
* if one or the other object is not marked as isEquivalent, OR if the
* two objects don't have an identical superclass list. This
* distinguisher thus can tell apart objects unless they're "basic
* equivalents," marked with isEquivalent and having the same equivalence
* keys.
*/
basicDistinguisher: Distinguisher
canDistinguish(a, b)
{
/*
* If the two objects are both marked isEquivalent, and they have
* the same equivalence key, they are basic equivalents, so we
* cannot distinguish them. Otherwise, we consider them
* distinguishable.
*/
return !(a.isEquivalent
&& b.isEquivalent
&& a.equivalenceKey == b.equivalenceKey);
}
;
/*
* Ownership Distinguisher. This distinguisher can tell two objects
* apart if they have different owners. "Unowned" objects are
* identified by their immediate containers instead of their owners.
*
* Note that while location *can* distinguish items with this
* distinguisher, ownership takes priority: if an object has an owner,
* the owner is the distinguishing feature. The reason location is a
* factor at all is that we need something parallel to ownership for the
* purposes of phrasing distinguishing descriptions of unowned objects.
* The best-sounding phrasing, at least in English, is to refer to the
* unowned objects by location.
*/
ownershipDistinguisher: Distinguisher
canDistinguish(a, b)
{
local aOwner;
local bOwner;
/* get the nominal owner of each object */
aOwner = a.getNominalOwner();
bOwner = b.getNominalOwner();
/*
* If neither object is owned, we can't tell them apart on the
* basis of ownership, so check to see if we can tell them apart
* on the basis of their immediate locations.
*/
if (aOwner == nil && bOwner == nil)
{
/*
* neither is owned - we can tell them apart only if they
* have different immediate containers
*/
return a.location != b.location;
}
/*
* One or both objects are owned, so we can tell them apart if
* and only if they have different owners.
*/
return aOwner != bOwner;
}
objInScope(obj, matchList, fullMatchList)
{
/* it's in scope if it's an owner of an object in the base list */
if (matchList.indexWhich(function(m) {
/* get the owner, or the location if there's no owner */
m = m.obj_;
local l = m.getNominalOwner();
if (l == nil)
l = m.location;
/* if obj matches the owner/location, consider it in scope */
return obj == l;
}) != nil)
return true;
/* otherwise, use the inherited handling */
return inherited(obj, matchList, fullMatchList);
}
matchName(obj, origTokens, adjustedTokens, matchList, fullMatchList)
{
/* if the name matches, consider ownership relationships */
if (obj.matchName(origTokens, adjustedTokens))
{
/*
* Look for objects in the original list owned by 'obj'. We
* might be matching an owner or location name rather than an
* object from the original list, in which case we want to act
* like we're matching the original list object(s) instead.
*/
local owned = matchList.mapAll({m: m.obj_})
.subset(function(m) {
/* get the owner or location */
local o = m.getNominalOwner();
if (o == nil)
o = m.location;
/* if the owner/location is 'obj', keep it */
return o == obj;
});
/* if we found any matches, return them all */
if (owned.length() > 0)
return owned;
}
/* no match to the owner; inherit the default handling */
return inherited(obj, origTokens, adjustedTokens,
matchList, fullMatchList);
}
;
/*
* Location Distinguisher. This distinguisher identifies objects purely
* by their immediate locations.
*/
locationDistinguisher: Distinguisher
canDistinguish(a, b)
{
/* we tell the objects apart by their immediate locations */
return a.location != b.location;
}
objInScope(obj, matchList, fullMatchList)
{
/* it's in scope if it's a location of an object in the base list */
if (matchList.indexWhich({m: m.obj_.location == obj}) != nil)
return true;
/* otherwise, use the inherited handling */
return inherited(obj, matchList, fullMatchList);
}
matchName(obj, origTokens, adjustedTokens, matchList, fullMatchList)
{
/* if the name matches, consider location relationships */
if (obj.matchName(origTokens, adjustedTokens))
{
/* look for objects in the original list contained in 'obj' */
local cont = matchList.mapAll({m: m.obj_})
.subset({m: m.location == obj});
/* if we found any matches, return them all */
if (cont.length() > 0)
return cont;
}
/* no match to the owner; inherit the default handling */
return inherited(obj, origTokens, adjustedTokens,
matchList, fullMatchList);
}
;
/*
* Lit/unlit Distinguisher. This distinguisher can tell two objects
* apart if one is lit (i.e., its isLit property is true) and the other
* isn't.
*/
litUnlitDistinguisher: Distinguisher
canDistinguish(a, b)
{
/* we can tell them apart if one is lit and the other isn't */
return a.isLit != b.isLit;
}
;
/* ------------------------------------------------------------------------ */
/*
* A command ranking criterion for comparing by the number of ordinal
* phrases ("first", "the second one") we find in a result.
*/
rankByDisambigOrdinals: CommandRankingByProblem
prop_ = &disambigOrdinalCount
;
/*
* Disambiguation Ranking. This is a special version of the command
* ranker that we use to rank the intepretations of a disambiguation
* response.
*/
class DisambigRanking: CommandRanking
/*
* Add the ordinal count ranking criterion at the end of the
* inherited list of ranking criteria. If we can't find any
* differences on the basis of the other criteria, choose the
* interpretation that uses fewer ordinal phrases. (We prefer an
* non-ordinal interpretation, because this will prefer matches to
* explicit vocabulary for objects over matches for generic
* ordinals.)
*
* Insert the 'ordinal' rule just before the 'indefinite' rule -
* avoiding an ordinal match is more important.
*/
rankingCriteria = static (inherited().insertAt(
inherited().indexOf(rankByIndefinite), rankByDisambigOrdinals))
/*
* note the an ordinal response is out of range
*/
noteOrdinalOutOfRange(ord)
{
/* count it as a non-matching entry */
++nonMatchCount;
}
/*
* note a list ordinal (i.e., "the first one" to refer to the first
* item in the ambiguous list) - we take list ordinals as less
* desirable than treating ordinal words as adjectives or nouns
*/
noteDisambigOrdinal()
{
/* count it as an ordinal entry */
++disambigOrdinalCount;
}
/* number of list ordinals in the match */
disambigOrdinalCount = 0
/*
* disambiguation commands have no verbs, so there's no verb
* structure to rank; so just use an arbitrary noun slot count
*/
nounSlotCount = 0
;
/* ------------------------------------------------------------------------ */
/*
* Base class for resolvers used when answering interactive questions.
* This class doesn't do anything in the library directly, but it
* provides a structured point for language extensions to hook in as
* needed with 'modify'.
*/
class InteractiveResolver: ProxyResolver
;
/* ------------------------------------------------------------------------ */
/*
* Disambiguation Resolver. This is a special resolver that we use for
* resolving disambiguation responses.
*/
class DisambigResolver: InteractiveResolver
construct(matchText, ordinalMatchList, matchList, fullMatchList, resolver,
dist)
{
/* inherit the base class constructor */
inherited(resolver);
/* remember the original match text and lists */
self.matchText = matchText;
self.ordinalMatchList = ordinalMatchList;
self.matchList = matchList;
self.fullMatchList = fullMatchList;
self.distinguisher = dist;
}
/*
* Match an object's name. We'll send this to the distinguisher for
* handling.
*/
matchName(obj, origTokens, adjustedTokens)
{
return distinguisher.matchName(obj, origTokens, adjustedTokens,
matchList, fullMatchList);
}
/*
* Resolve qualifiers in the enclosing main scope, since qualifier
* phrases in responses are not part of the narrowed list being
* disambiguated.
*/
getQualifierResolver() { return origResolver; }
/*
* Determine if an object is in scope. We pass this to the
* distinguisher to decide.
*/
objInScope(obj)
{
return distinguisher.objInScope(obj, matchList, fullMatchList);
}
/*
* we allow ALL in interactive disambiguation responses, regardless
* of the verb, because we're just selecting from the list presented
* in the prompt in these cases
*/
allowAll = true
/* for 'all', use the full current full match list */
getAll(np) { return fullMatchList; }
/* filter an ambiguous noun list */
filterAmbiguousNounPhrase(lst, requiredNum, np)
{
/*
* we're doing disambiguation, so we're only narrowing the
* original match list, which we've already filtered as well as
* we can - just return the list unchanged
*/
return lst;
}
/* filter a plural noun list */
filterPluralPhrase(lst, np)
{
/*
* we're doing disambiguation, so we're only narrowing the
* original match list, which we've already filtered as well as
* we can - just return the list unchanged
*/
return lst;
}
/*
* Select the match for an indefinite noun phrase. In interactive
* disambiguation, an indefinite noun phrase simply narrows the
* list, rather than selecting any match, so treat this as still
* ambiguous.
*/
selectIndefinite(results, lst, requiredNumber)
{
/* note the ambiguous list in the results */
return results.ambiguousNounPhrase(nil, ResolveAsker, '',
lst, lst, lst,
requiredNumber, self);
}
/* the text of the phrase we're disambiguating */
matchText = ''
/*
* The "ordinal" match list: this includes the exact list offered as
* interactive choices in the same order as they were shown in the
* prompt. This list can be used to correlate ordinal responses to
* the prompt list, since it contains exactly the items listed in
* the prompt. Note that this list will only contain one of each
* indistinguishable object.
*/
ordinalMatchList = []
/*
* the original match list we are disambiguating, which includes all
* of the objects offered as interactive choices, and might include
* indistinguishable equivalents of offered items
*/
matchList = []
/*
* the full original match list, which might include items in scope
* beyond those offered as interactive choices
*/
fullMatchList = []
/*
* The distinguisher that was used to generate the prompt. Some
* distinguishers can tell objects apart by other characteristics
* than just their names, so when parsing we want to be able to give
* the distinguisher a look at the input to see if the player is
* referring to one of the distinguishing characteristics rather than
* the object's own name.
*/
distinguisher = nil
;
/* ------------------------------------------------------------------------ */
/*
* General class for disambiguation exceptions
*/
class DisambigException: Exception
;
/*
* Still Ambiguous Exception - this is thrown when the user answers a
* disambiguation question with insufficient specificity, so that we
* still have an ambiguous list.
*/
class StillAmbiguousException: DisambigException
construct(matchList, origText)
{
/* remember the new match list and text */
matchList_ = matchList;
origText_ = origText;
}
/* the narrowed, but still ambiguous, match list */
matchList_ = []
/* the text of the new phrasing */
origText_ = ''
;
/*
* Unmatched disambiguation - we throw this when the user answers a
* disambiguation question with a syntactically valid response that
* doesn't refer to any of the objects in the list of choices offered.
*/
class UnmatchedDisambigException: DisambigException
construct(resp)
{
/* remember the response text */
resp_ = resp;
}
/* the response text */
resp_ = nil
;
/*
* Disambiguation Ordinal Out Of Range - this is thrown when the user
* answers a disambiguation question with an ordinal, but the ordinal is
* outside the bounds of the offered list (for example, we ask "which
* book do you mean, the red book, or the blue book?", and the user
* answers "the fourth one").
*/
class DisambigOrdinalOutOfRangeException: DisambigException
construct(ord)
{
/* remember the ordinal word */
ord_ = ord;
}
/* a string giving the ordinal word entered by the user */
ord_ = ''
;
/* ------------------------------------------------------------------------ */
/*
* A disambiguation results gatherer object. We use this to manage the
* results of resolution of a disambiguation response.
*/
class DisambigResults: BasicResolveResults
construct(parent)
{
/* copy the actor information from the parent resolver */
setActors(parent.targetActor_, parent.issuingActor_);
}
ambiguousNounPhrase(keeper, asker, txt,
matchList, fullMatchList, scopeList,
requiredNum, resolver)
{
/* if we're resolving a sub-phrase, inherit the standard handling */
if (resolver.isSubResolver)
return inherited(keeper, asker, txt,
matchList, fullMatchList, scopeList,
requiredNum, resolver);
/*
* Before giving up, try filtering by possessive rank, in case we
* qualified by a possessive phrase.
*/
matchList = resolver.filterPossRank(matchList, requiredNum);
/*
* Our disambiguation response itself requires further
* disambiguation. Do not handle it recursively, since doing so
* could allow the user to blow the stack simply by answering
* with the same response over and over. Instead, throw a
* "still ambiguous" exception - the original disambiguation
* loop will note the situation and iterate on the resolution
* list, ensuring that we can run forever without blowing the
* stack, if that's the game the user wants to play.
*/
throw new StillAmbiguousException(matchList, txt.toLower().htmlify());
}
/*
* note the an ordinal response is out of range
*/
noteOrdinalOutOfRange(ord)
{
/* this is an error */
throw new DisambigOrdinalOutOfRangeException(ord);
}
/*
* show a message on not matching an object - for a disambiguation
* response, failing to match means that the combination of the
* disambiguation response plus the original text doesn't name any
* objects, not that the object in the response itself isn't present
*/
noMatch(action, txt)
{
/* throw an error indicating the problem */
throw new UnmatchedDisambigException(txt.toLower().htmlify());
}
noMatchPoss(action, txt)
{
throw new UnmatchedDisambigException(txt.toLower().htmlify());
}
noVocabMatch(action, txt)
{
/* throw an error indicating the problem */
throw new UnmatchedDisambigException(txt.toLower().htmlify());
}
noMatchForPossessive(owner, txt)
{
/* throw an error indicating the problem */
throw new UnmatchedDisambigException(txt.toLower().htmlify());
}
;
TADS 3 Library Manual
Generated on 5/16/2013 from TADS version 3.1.3