parser.t
#charset "us-ascii"
/*
* Copyright (c) 2000, 2006 Michael J. Roberts. All Rights Reserved.
*
* TADS 3 Library: parser
*
* This modules defines the language-independent parts of the command
* parser.
*
* Portions based on xiny.t, copyright 2002 by Steve Breslin and
* incorporated by permission.
*/
#include "adv3.h"
#include "tok.h"
#include <dict.h>
#include <gramprod.h>
#include <strcomp.h>
/* ------------------------------------------------------------------------ */
/*
* property we add to ResolveInfo to store the remaining items from an
* ambiguous list
*/
property extraObjects;
/* ------------------------------------------------------------------------ */
/*
* ResolveResults - an instance of this class is passed to the
* resolveNouns() routine to receive the results of the resolution.
*
* This class's main purpose is to virtualize the handling of error or
* warning conditions during the resolution process. The ResolveResults
* object is created by the initiator of the resolution, so it allows
* the initiator to determine how errors are to be handled without
* having to pass flags down through the match tree.
*/
class ResolveResults: object
/*
* Instances must provide the following methods:
*
* noVocabMatch(action, txt) - there are no objects in scope matching
* the given noun phrase vocabulary. This is "worse" than noMatch(),
* in the sense that this indicates that the unqualified noun phrase
* simply doesn't refer to any objects in scope, whereas noMatch()
* means that some qualification applied to the vocabulary ruled out
* any matches. 'txt' is the original text of the noun phrase.
*
* noMatch(action, txt) - there are no objects in scope matching the
* noun phrase. This is used in cases where we eliminate all
* possible matches because of qualifications or constraints, so it's
* possible that the noun phrase vocabulary, taken by itself, does
* match some object; it's just that when the larger noun phrase
* context is considered, there's nothing that matches.
*
* noMatchPossessive(action, txt) - same as noMatch, but the
* unmatched phrase is qualified with a possessive phrase. For
* ranking matches, the possessive version ranks ahead of treating
* the possessive words as raw vocabulary when neither matches, since
* it's more informative to report that we can't even match the
* underlying qualified noun phrase, let alone the whole phrase with
* qualification.
*
* noMatchForAll() - there's nothing matching "all"
*
* noMatchForAllBut() - there's nothing matching "all except..."
* (there might be something matching all, but there's nothing left
* when the exception list is applied)
*
* noMatchForListBut() - there's nothing matching "<list> except..."
*
* noteEmptyBut() - a "but" (or "except") list matches nothing. We
* don't consider this an error, but we rate an interpretation with a
* non-empty "but" list and complete exclusion of the "all" or "any"
* phrase whose objects are excluded by the "but" higher than one
* with an empty "but" list and a non-empty all/any list - we do this
* because it probably means that we came up with an incorrect
* interpretation of the "but" phrase in the empty "but" list case
* and failed to exclude things we should have excluded.
*
* noMatchForPronoun(typ, txt) - there's nothing matching a pronoun.
* 'typ' is one of the PronounXxx constants, and 'txt' is the text of
* the word used in the command.
*
* allNotAllowed() - the command contained the word ALL (or a
* synonym), but the verb doesn't allow ALL to be used in its noun
* phrases.
*
* reflexiveNotAllowed(typ, txt) - the reflexive pronoun isn't
* allowed in this context. This usually means that the pronoun was
* used with an intransitive or single-object action. 'typ' is one
* of the PronounXxx constants, and 'txt' is the text of the word
* used.
*
* wrongReflexive(typ, txt) - the reflexive pronoun doesn't agree
* with its referent in gender, number, or some other way.
*
* noMatchForPossessive(owner, txt) - there's nothing matching the
* phrase 'txt' owned by the resolved possessor object 'owner'. Note
* that 'owner' is a list, since we can have plural possessive
* qualifier phrases.
*
* noMatchForLocation(loc, txt) - there's nothing matching 'txt' in
* the location object 'loc'. This is used when a noun phrase is
* explicitly qualified by location ("the book on the table").
*
* noteBadPrep() - we have a noun phrase or other phrase
* incorporating an invalid prepositional phrase structure. This is
* called from "badness" rules that are set up to match phrases with
* embedded prepositions, as a last resort when no valid
* interpretation can be found.
*
* nothingInLocation(loc) - there's nothing in the given location
* object. This is used when we try to select the one object or all
* of the objects in a given container, but the container doesn't
* actually have any contents.
*
* ambiguousNounPhrase(keeper, asker, txt, matchLst, fullMatchList,
* scopeList, requiredNum, resolver) - an ambiguous noun phrase was
* entered: the noun phrase matches multiple objects that are all
* equally qualified, but we only want the given exact number of
* matches. 'asker' is a ResolveAsker object that we'll use to
* generate any prompts; if no customization is required, simply pass
* the base ResolveAsker. 'txt' is the original text of the noun
* list in the command, which the standard prompt messages can use to
* generate their questions. 'matchLst' is a list of the qualified
* objects matching the phrase, with only one object included for
* each set of equivalents in the original full list; 'fullMatchList'
* is the full list of matches, including each copy of equivalents;
* 'scopeList' is the list of everything in scope that matched the
* original phrase, including illogical items. If it's desirable to
* interact with the user at this point, prompt the user to resolve
* the list, and return a new list with the results. If no prompting
* is desired, the original list can be returned. If it isn't
* possible to determine the final set of objects, and a final set of
* objects is required (this is up to the subclass to determine), a
* parser exception should be thrown to stop further processing of
* the command. 'keeper' is an AmbigResponseKeeper object, which is
* usually simply the production object itself; each time we parse an
* interactive response (if we are interactive at all), we'll call
* addAmbigResponse() on the calling production object to add it to
* the saved list of responses, and we'll call getAmbigResponses() to
* find previous answers to the same question, in case of
* re-resolving the phrase with 'again' or the like.
*
* unknownNounPhrase(match, resolver) - a noun phrase that doesn't
* match any known noun phrase syntax was entered. 'match' is the
* production match tree object for the unknown phrase. Returns a
* list of the resolved objects for the noun phrase, if possible. If
* it is not possible to resolve the phrase, and a resolution is
* required (this is up to the subclass to determine), a parser
* exception should be thrown.
*
* getImpliedObject(np, resolver) - a noun phrase was left out
* entirely. 'np' is the noun phrase production standing in for the
* missing noun phrase; this is usually an EmptyNounPhraseProd or a
* subclass. If an object is implicit in the command, or a
* reasonable default can be assumed, return the implicit or default
* object or objects. If not, the routine can return nil or can
* throw an error. The result is a ResolveInfo list.
*
* askMissingObject(asker, resolver, responseProd) - a noun phrase
* was left out entirely, and no suitable default can be found
* (getImpliedObject has already been called, and that returned nil).
* If it is possible to ask the player interactively to fill in the
* missing object, ask the player. If it isn't possible to resolve
* an object, an error can be thrown, or an empty list can be
* returned. 'asker' is a ResolveAsker object, which can be used to
* customize the prompt (if any) that we show; pass the base
* ResolveAsker if no customization is needed. 'responseProd' is the
* production to use to parse the response. The return value is the
* root match tree object of the player's interactive response, with
* its 'resolvedObjects' property set to the ResolveInfo list from
* resolving the player's response. (The routine returns the match
* tree for the player's response so that, if we must run resolution
* again on another pass, we can re-resolve the same response without
* asking the player the same question again.)
*
* noteLiteral(txt) - note the text of a literal phrase. When
* selecting among alternative interpretations of a phrase, we'll
* favor shorter literals, since treating fewer tokens as literals
* means that we're actually interpreting more tokens.
*
* askMissingLiteral(action, which) - a literal phrase was left out
* entirely. If possible, prompt interactively for a player response
* and return the result. If it's not possible to ask for a
* response, an error can be thrown, or nil can be returned. The
* return value is simply the text string the player enters.
*
* emptyNounPhrase(resolver) - a noun phrase involving only
* qualifiers was entered ('take the'). In most cases, an exception
* should be thrown. If the empty phrase can be resolved to an
* object or list of objects, the resolved list should be returned.
*
* zeroQuantity(txt) - a noun phrase referred to a zero quantity of
* something ("take zero books").
*
* insufficientQuantity(txt, matchList, requiredNumber) - a noun
* phrase is quantified with a number exceeding the number of objects
* available: "take five books" when only two books are in scope.
*
* uniqueObjectRequired(txt, matchList) - a noun phrase yields more
* than one object, but only one is allowed. For example, we'll call
* this if the user attempts to use more than one result for a
* single-noun phrase (such as by answering a disambiguation question
* with 'all').
*
* singleObjectRequired(txt) - a noun phrase list was used where a
* single noun phrase is required.
*
* noteAdjEnding() - a noun phrase ends in an adjective. This isn't
* normally an error, but is usually less desirable than interpreting
* the same noun phrase as ending in a noun (in other words, if a
* word can be used as both an adjective and a noun, it is usually
* better to interpret the word as a noun rather than as an adjective
* when the word appears at the end of a noun phrase, as long as the
* noun interpretation matches an object in scope).
*
* noteIndefinite() - a noun phrase is phrased as an indefinite ("any
* book", "one book"), meaning that we can arbitrarily choose any
* matching object in case of ambiguity. Sometimes, an object will
* have explicit vocabulary that could be taken to be indefinite: a
* button labeled "1" could be a "1 button", for example, or a subway
* might have an "A train". By noting the indefinite interpretation,
* we can give priority to the alternative definite interpretation.
*
* noteMatches(matchList) - notifies the results object that the
* given list of objects is being matched. This allows the results
* object to inspect the object list for its strength: for example,
* by noting the presence of truncated words. This should only be
* called after the nouns have been resolved to the extent possible,
* so any disambiguation or selection that is to be performed should
* be performed before this routine is called.
*
* noteMiscWordList(txt) - a noun phrase is made up of miscellaneous
* words. A miscellaneous word list as a noun phrase has non-zero
* badness, so we will never use a misc word list unless we can't
* find any more structured interpretation. In most cases, a
* miscellaneous word list indicates an invalid phrasing, but in some
* cases objects might use this unstructured type of noun phrase in
* conjunction with matchName() to perform dynamic or special-case
* parsing.
*
* notePronoun() - a noun phrase is matching as a pronoun. In
* general, we prefer a match to an object's explicit vocabulary
* words to a match to a pronoun phrase: if the game goes to the
* trouble of including a word explicitly among an object's
* vocabulary, that's a better match than treating the same word as a
* generic pronoun.
*
* notePlural() - a plural phrase is matching. In some cases we
* require a single object match, in which case a plural phrase is
* undesirable. (A plural phrase might still match just one object,
* though, so it can't be ruled out on structural grounds alone.)
*
* beginSingleObjSlot() and endSingleObjSlot() are used to bracket
* resolution of a noun phrase that needs to be resolved as a single
* object. We use these to explicitly lower the ranking for plural
* structural phrasings within these slots.
*
* beginTopicSlot() and endTopicSlot() are used to bracket resolution
* of a noun phrase that's being used as a conversation topic. These
* are of the *form* of single nouns, but singular and plural words
* are equivalent, so when we're in a topic we don't consider a
* plural to be a minus the way we would for an ordinary object noun
* phrase.
*
* incCommandCount() - increment the command counter. This is used
* to keep track of how many subcommands are in a command tree.
*
* noteActorSpecified() - note that the command is explicitly
* addressed to an actor.
*
* noteNounSlots(cnt) - note the number of "noun slots" in the verb
* phrase. This is the number of objects the verb takes: an
* intransitive verb has no noun slots; a transitive verb with a
* direct object only has one; a verb with a direct and indirect
* object has two. Note this has nothing to do with the number of
* objects specified in a noun list - TAKE BOX, BOOK, AND BELL has
* only one noun slot (a direct object) even though the slot is
* occupied by a list with three objects.
*
* noteWeakPhrasing(level) - note that the phrasing is "weak," with
* the given weakness level - higher is weaker. The exact meaning of
* the weakness levels is up to the language module to define. The
* English module, for example, considers VERB IOBJ DOBJ phrasing
* (with no preposition, as in GIVE BOB BOOK) to be weak when the
* DOBJ part doesn't have a grammatical marker that clarifies that
* it's really a separate noun phrase (an article serves this purpose
* in English: GIVE BOB THE BOOK).
*
* allowActionRemapping - returns true if we can remap the action
* during noun phrase resolution. Remapping is usually allowed only
* during the actual execution phase, not during the ranking phase.
*
* allowEquivalentFiltering - returns true if we can filter an
* ambiguous resolution list by making an arbitrary choice among
* equivalent objects. This is normally allowed only during a final
* resolution phase, not during a tentative resolution phase.
*/
;
/* ------------------------------------------------------------------------ */
/*
* Noun phrase resolver "asker." This type of object can be passed to
* certain ResolveResults methods in order to customize the messages
* that the parser generates for interactive prompting.
*/
class ResolveAsker: object
/*
* Ask for help disambiguating a noun phrase. This asks which of
* several possible matching objects was intended. This method has
* the same parameter list as the equivalent message object method.
*/
askDisambig(targetActor, promptTxt, curMatchList, fullMatchList,
requiredNum, askingAgain, dist)
{
/* let the target actor's parser message object handle it */
targetActor.getParserMessageObj().askDisambig(
targetActor, promptTxt, curMatchList, fullMatchList,
requiredNum, askingAgain, dist);
}
/*
* Ask for a missing object. This prompts for an object that's
* structurally required for an action, but which was omitted from
* the player's command.
*/
askMissingObject(targetActor, action, which)
{
/* let the target actor's parser message object handle it */
targetActor.getParserMessageObj().askMissingObject(
targetActor, action, which);
}
;
/* ------------------------------------------------------------------------ */
/*
* The resolveNouns() method returns a list of ResolveInfo objects
* describing the objects matched to the noun phrase.
*/
class ResolveInfo: object
construct(obj, flags, np = nil)
{
/* remember the members */
obj_ = obj;
flags_ = flags;
np_ = np;
}
/*
* Look for a ResolveInfo item in a list of ResolveInfo items that
* is equivalent to us. Returns true if we find such an item, nil
* if not.
*
* Another ResolveInfo is equivalent to us if it refers to the same
* underlying game object that we do, or if it refers to a game
* object that is indistinguishable from our underlying game object.
*/
isEquivalentInList(lst)
{
/*
* if we can find our exact item in the list, or we can find an
* equivalent object, we have an equivalent
*/
return (lst.indexWhich({x: x.obj_ == obj_}) != nil
|| lst.indexWhich(
{x: x.obj_.isVocabEquivalent(obj_)}) != nil);
}
/*
* Look for a ResolveInfo item in a list of ResolveInfo items that
* is equivalent to us according to a particular Distinguisher.
*/
isDistEquivInList(lst, dist)
{
/*
* if we can find our exact item in the list, or we can find an
* equivalent object, we have an equivalent
*/
return (lst.indexWhich({x: x.obj_ == obj_}) != nil
|| lst.indexWhich(
{x: !dist.canDistinguish(x.obj_, obj_)}) != nil);
}
/* the object matched */
obj_ = nil
/* flags describing how we matched the object */
flags_ = 0
/*
* By default, each ResolveInfo counts as one object, for the
* purposes of a quantity specifier (as in "five coins" or "both
* hats"). However, in some cases, a single resolved object might
* represent a collection of discrete objects and thus count as more
* than one for the purposes of the quantifier.
*/
quant_ = 1
/*
* The possessive ranking, if applicable. If this object is
* qualified by a possessive phrase ("my books"), we'll set this to
* a value indicating how strongly the possession applies to our
* object: 2 indicates that the object is explicitly owned by the
* object indicated in the possessive phrase, 1 indicates that it's
* directly held by the named possessor but not explicitly owned,
* and 0 indicates all else. In cases where there's no posessive
* qualifier, this will simply be zero.
*/
possRank_ = 0
/* the pronoun type we matched, if any (as a PronounXxx enum) */
pronounType_ = nil
/* the noun phrase we parsed to come up with this object */
np_ = nil
/*
* The pre-calculated multi-object announcement text for this object.
* When we iterate over the object list in a command with multiple
* direct or indirect objects (TAKE THE BOOK, BELL, AND CANDLE), we
* calculate the little announcement messages ("book:") for the
* objects BEFORE we execute the actual commands. We then use the
* pre-calculated announcements during our iteration. This ensures
* consistency in the basis for choosing the names, which is
* important in cases where the names include state-dependent
* information for the purposes of distinguishing one object from
* another. The relevant state can change over the course of
* executing the command on the objects in the iteration, so if we
* calculated the names on the fly we could end up with inconsistent
* naming. The user thinks of the objects in terms of their state at
* the start of the command, so the pre-calculation approach is not
* only more internally consistent, but is also more consistent with
* the user's perspective.
*/
multiAnnounce = nil
;
/*
* Intersect two resolved noun lists, returning a list consisting only
* of the unique objects from the two lists.
*/
intersectNounLists(lst1, lst2)
{
/* we don't have any results yet */
local ret = [];
/*
* run through each element of the first list to see if it has a
* matching object in the second list
*/
foreach(local cur in lst1)
{
local other;
/*
* if this element's object occurs in the second list, we can
* include it in the result list
*/
if ((other = lst2.valWhich({x: x.obj_ == cur.obj_})) != nil)
{
/* if one or the other has a noun phrase, keep it */
local np = cur.np_ ?? other.np_;
/*
* include this one in the result list, with the combined
* flags from the two original entries
*/
ret += new ResolveInfo(cur.obj_, cur.flags_ | other.flags_, np);
}
}
/* return the result list */
return ret;
}
/*
* Extract the objects from a list obtained with resolveNouns().
* Returns a list composed only of the objects in the resolution
* information list.
*/
getResolvedObjects(lst)
{
/*
* return a list composed only of the objects from the ResolveInfo
* structures
*/
return lst.mapAll({x: x.obj_});
}
/* ------------------------------------------------------------------------ */
/*
* The basic production node base class. We'll use this as the base
* class for all of our grammar rule match objects.
*/
class BasicProd: object
/* get the original text of the command for this match */
getOrigText()
{
/* if we have no token list, return an empty string */
if (tokenList == nil)
return '';
/* build the string based on my original token list */
return cmdTokenizer.buildOrigText(getOrigTokenList());
}
/* get my original token list, in canonical tokenizer format */
getOrigTokenList()
{
/*
* return the subset of the full token list from my first token
* to my last token
*/
return nilToList(tokenList).sublist(
firstTokenIndex, lastTokenIndex - firstTokenIndex + 1);
}
/*
* Set my original token list. This replaces the actual token list
* we matched from the parser with a new token list provided by the
* caller.
*/
setOrigTokenList(toks)
{
tokenList = toks;
firstTokenIndex = 1;
lastTokenIndex = toks.length();
}
/*
* Can this object match tree resolve to the given object? We'll
* resolve the phrase as though it were a topic phrase, then look for
* the object among the matches.
*/
canResolveTo(obj, action, issuingActor, targetActor, which)
{
/* set up a topic resolver */
local resolver = new TopicResolver(
action, issuingActor, targetActor, self, which,
action.createTopicQualifierResolver(issuingActor, targetActor));
/*
* set up a results object - use a tentative results object,
* since we're only looking at the potential matches, not doing a
* full resolution pass
*/
local results = new TentativeResolveResults(issuingActor, targetActor);
/* resolve the phrase as a topic */
local match = resolveNouns(resolver, results);
/*
* make sure it's packaged in the canonical topic form, as a
* ResolvedTopic object
*/
match = resolver.packageTopicList(match, self);
/* if the topic can match it, it's a possible resolution */
return (match != nil
&& match.length() > 0
&& match[1].obj_.canMatchObject(obj));
}
/*
* Is this match a match to the special syntax for a custom missing
* object query? This returns true if the match has a wording that
* strongly distinguishes it from an ordinary new command. In the
* English parser, for example, this returns true for the
* PrepSingleTopicProd matches (e.g., inSingleNoun) if the phrase
* starts with the preposition for the match.
*
* This property is used when we ask a missing object question:
*
*. >dig
*. What do you want to dig in?
*.
*. >in the dirt
*
* In English, the DIG command sets up to receive a response phrased
* as "in <noun phrase>" - that's done by setting the response
* production to inSingleNoun. In this case, "in the dirt" would
* return true, since that's pretty clearly a match to the expected
* inSingleNoun syntax. In contrast, "the dirt" would return false,
* since that's just a noun phrase without the special wording for
* this particular verb.
*/
isSpecialResponseMatch = nil
/*
* Grammar match objects that come from a GrammarProd.parseTokens()
* call will always have a set of properties indicating which tokens
* from the input matched the grammar rule. However, we sometimes
* synthesize match trees internally rather than getting them from
* parser input; for synthesized trees, the parser obviously won't
* supply those properties for us, so we need to define suitable
* defaults that synthesized match tree nodes can inherit.
*/
firstTokenIndex = 0
lastTokenIndex = 0
;
/* ------------------------------------------------------------------------ */
/*
* Basic disambiguation production class
*/
class DisambigProd: BasicProd
/*
* Remove the "ambiguous" flags from a result list. This can be
* used to mark the response to a disambiguation query as no longer
* ambiguous.
*/
removeAmbigFlags(lst)
{
/* remove the "unclear disambig" flag from each result item */
foreach (local cur in lst)
cur.flags_ &= ~UnclearDisambig;
/* return the list */
return lst;
}
;
/* ------------------------------------------------------------------------ */
/*
* Base class for "direction" productions. Each direction (the compass
* directions, the vertical directions, the shipboard directions, and so
* on) must have an associated grammar rule, which must produce one of
* these. This should be subclassed with grammar rules like this:
*
* grammar directionName: 'north' | 'n' : DirectionProd
*. dir = northDirection
*. ;
*/
class DirectionProd: BasicProd
/*
* Each direction-specific grammar rule subclass must set this
* property to the associated direction object (northDirection,
* etc).
*/
dir = nil
;
/* ------------------------------------------------------------------------ */
/*
* The base class for commands. A command is the root of the grammar
* match tree for a single action. A command line can consist of a
* number of commands joined with command separators; in English,
* command separators are things like periods, semicolons, commas, and
* the words "and" and "then".
*/
class CommandProd: BasicProd
hasTargetActor()
{
/*
* By default, a command production does not include a
* specification of a target actor. Command phrases that
* contain syntax specifically directing the command to a target
* actor should return true here.
*/
return nil;
}
/*
* Get the match tree for the target actor phrase, if any. By
* default, we have no target actor phrase, so just return nil.
*/
getActorPhrase = nil
/*
* "Execute" the actor phrase. This lets us know that the parser
* has decided to use our phrasing to specify the target actor.
* We're not required to do anything here; it's just a notification
* for subclass use. Since we don't have a target actor phrase at
* all, we obviously don't need to do anything here.
*/
execActorPhrase(issuingActor) { }
;
/*
* A first-on-line command. The first command on a command line can
* optionally start with an actor specification, to give orders to the
* actor.
*/
class FirstCommandProd: CommandProd
countCommands(results)
{
/* count commands in the underlying command */
cmd_.countCommands(results);
}
getTargetActor()
{
/*
* we have no actor specified explicitly, so it's the current
* player character
*/
return libGlobal.playerChar;
}
/*
* The tokens of the entire command except for the target actor
* specification. By default, we take all of the tokens starting
* with the first command's first token and running to the end of
* the token list. This assumes that the target actor is specified
* at the beginning of the command - languages that use some other
* word ordering must override this accordingly.
*/
getCommandTokens()
{
return tokenList.sublist(cmd_.firstTokenIndex);
}
/*
* Resolve my first action. This returns an instance of a subclass
* of Action that represents the resolved action. We'll ask our
* first subcommand to resolve its action.
*/
resolveFirstAction(issuingActor, targetActor)
{
return cmd_.resolveFirstAction(issuingActor, targetActor);
}
/* resolve nouns in the command */
resolveNouns(issuingActor, targetActor, results)
{
/* resolve nouns in the underlying command */
cmd_.resolveNouns(issuingActor, targetActor, results);
/* count our commands */
countCommands(results);
}
/*
* Does this command end a sentence? The exact meaning of a
* sentence may vary by language; in English, a sentence ends with
* certain punctuation marks (a period, a semicolon, an exclamation
* point).
*/
isEndOfSentence()
{
/* ask the underlying command phrase */
return cmd_.isEndOfSentence();
}
/*
* Get the token index of the first command separator token. This
* is the first token that is not part of the underlying command.
*/
getCommandSepIndex()
{
/* get the separator index from the underlying command */
return cmd_.getCommandSepIndex();
}
/*
* get the token index of the next command - this is the index of
* the next token after our conjunction if we have one, or after our
* command if we don't have a conjunction
*/
getNextCommandIndex()
{
/* get the next command index from the underlying command */
return cmd_.getNextCommandIndex();
}
;
/*
* Define the simplest grammar rule for a first-on-line command phrase,
* which is just an ordinary command phrase. The language-specific
* grammar must define any other alternatives; specifically, the
* language might provide an "actor, command" syntax to direct a command
* to a particular actor.
*/
grammar firstCommandPhrase(commandOnly): commandPhrase->cmd_
: FirstCommandProd
;
/*
* A command with an actor specification. This should be instantiated
* with grammar rules in a language-specific module.
*
* Instantiating grammar rules should set property actor_ to the actor
* match tree, and cmd_ to the underlying 'commandPhrase' production
* match tree.
*/
class CommandProdWithActor: CommandProd
hasTargetActor()
{
/* this command explicitly specifies an actor */
return true;
}
getTargetActor()
{
/* return my resolved actor object */
return resolvedActor_;
}
getActorPhrase()
{
/* return the actor phrase production */
return actor_;
}
/*
* Execute the target actor phrase. This is a notification, for use
* by subclasses; we don't have anything we need to do in this base
* class implementation.
*/
execActorPhrase(issuingActor) { }
/*
* Resolve nouns. We'll resolve only the nouns in the target actor
* phrase; we do not resolve nouns in the command phrase, because we
* must re-parse the command phrase after we've finished resolving
* the actor phrase, and thus we can't resolve nouns in the command
* phrase until after the re-parse is completed.
*/
resolveNouns(issuingActor, targetActor, results)
{
local lst;
/*
* Resolve the actor, then we're done. Note that we do not
* attempt to resolve anything in the underlying command yet,
* because we can only resolve the command after we've fully
* processed the actor clause.
*/
lst = actor_.resolveNouns(getResolver(issuingActor), results);
/*
* there are no noun phrase slots, since we're not looking at the
* verb
*/
results.noteNounSlots(0);
/*
* if we have an object in the list, use it - note that our
* actor phrase is a single-noun production, and hence should
* never yield more than a single entry in its resolution list
*/
if (lst.length() > 0)
{
/* remember the resolved actor */
resolvedActor_ = lst[1].obj_;
/* note in the results that an actor is specified */
results.noteActorSpecified();
}
/* count our commands */
countCommands(results);
}
/* get or create my actor resolver */
getResolver(issuingActor)
{
/* if I don't already have a resolver, create one and cache it */
if (resolver_ == nil)
resolver_ = new ActorResolver(issuingActor);
/* return our cached resolver */
return resolver_;
}
/* my resolved actor object */
resolvedActor_ = nil
/* my actor resolver object */
resolver_ = nil
;
/*
* First-on-line command with target actor. As with
* CommandProdWithActor, instantiating grammar rules must set the
* property actor_ to the actor match tree, and cmd_ to the underlying
* commandPhrase match.
*/
class FirstCommandProdWithActor: CommandProdWithActor, FirstCommandProd
;
/* ------------------------------------------------------------------------ */
/*
* The 'predicate' production is the grammar rule for all individual
* command phrases. We don't define anything about the predicate
* grammar here, since it is completely language-dependent, but we do
* *use* the predicate production as a sub-production of our
* commandPhrase rules.
*
* The language-dependent implementation of the 'predicate' production
* must provide the following methods:
*
* resolveAction(issuingActor, targetActor): This method returns a
* newly-created instance of the Action subclass that the command refers
* to. This method must generally interpret the phrasing of the command
* to determine what noun phrases are present and what roles they serve,
* and what verb phrase is present, then create an appropriate Action
* subclass to represent the verb phrase as applied to the noun phrases
* in their determined roles.
*
* resolveNouns(issuingActor, targetActor, results): This method
* resolves all of the noun phrases in the predicate, using the
* 'results' object (an object of class ResolveResults) to store
* information about the status of the resolution. Generally, this
* routine should collect information about the roles of the noun phrase
* production matches, plug these into the Action via appropriate
* properties (dobjMatch for the direct object of a transitive verb, for
* example), resolve the Action, and then call the Action to do the
* resolution. This method has no return value, since the Action is
* responsible for keeping track of the results of the resolution.
*
* For languages like English which encode most information about the
* phrase roles in word ordering, a good way to design a predicate
* grammar is to specify the syntax of each possible verb phrase as a
* 'predicate' rule, and base the match object for that rule on the
* corresponding Action subclass. For example, the English grammar has
* this rule to define the syntax for the verb 'take':
*
* VerbRule
*. ('take' | 'pick' 'up' | 'get') dobjList
*. | 'pick' dobjList 'up'
*. : TakeAction
*. ;
*
* Since English encodes everything in this command positionally, it's
* straightforward to write grammar rules for the possible syntax
* variations of a given action, hence it's easy to associate command
* syntax directly with its associated action. When each 'predicate'
* grammar maps to an Action subclass for its match object,
* resolveAction() is especially easy to implement: it simply returns
* 'self', since the grammar match and the Action are the same object.
* It's also easy to plug each noun phrase into its appropriate property
* slot in the Action subclass: the parser can be told to plug in each
* noun phrase directly using the "->" notation in the grammar.
*
* Many languages encode the roles of noun phrases with case markers or
* other syntactic devices, and as a result do not impose such strict
* rules as English does on word order. For such languages, it is
* usually better to construct the 'predicate' grammar separately from
* any single action, so that the various acceptable phrase orderings
* are enumerated, and the verb phrase is just another phrase that plugs
* into these top-level orderings. In such grammars, the predicate must
* do some programmatic work in resolveAction(): it must figure out
* which Action subclass is involved based on the verb phrase sub-match
* object and the noun phrases present, then must create an instance of
* that Action subclass. Furthermore, since we can't plug the noun
* phrases into the Action using the "->" notation in the grammar, the
* resolveAction() routine must pick out the sub-match objects
* representing the noun phrases and plug them into the Action itself.
*
* Probably the easiest way to accomplish all of this is by designing
* each verb phrase match object so that it is associated with one or
* more Action subclasses, according to the specific noun phrases
* present. The details of this mapping are obviously specific to each
* language, but it should be possible in most cases to build a base
* class for all verb phrases that uses parameters to create the Action
* and noun phrase associations. For example, each verb phrase grammar
* match object might have a list of possible Action matches, such as
* this example from a purely hypothetical English grammar based on this
* technique
*
* grammar verbPhrase: 'take' | 'pick' 'up': VerbProd
*. actionMap =
*. [
*. [TakeAction, BasicProd, &dobjMatch],
*. [TakeWithAction, BasicProd, &dobjMatch, WithProd, &iobjMatch]
*. ]
*. ;
*
* The idea is that the base VerbProd class looks through the list given
* by the actionMap property for a template that matches the number and
* type of noun phrases present in the predicate; when it finds a match,
* it creates an instance of the Action subclass given in the template,
* then plugs the noun phrases into the listed properties of the new
* Action instance.
*
* Note that our verbPhrase example above is not an example of actual
* working code, because there is no such thing in the
* language-independent library as a VerbProd base class that reads
* actionMap properites - this is a purely hypothetical bit of code to
* illustrate how such a construction might work in a language that
* requires it, and it is up to the language-specific module to define
* such a mechanism for its own use.
*/
/* ------------------------------------------------------------------------ */
/*
* Base classes for grammar matches for full commands.
*
* There are two kinds of command separators: ambiguous and unambiguous.
* Unambiguous separators are those that can separate only commands, such
* as "then" and periods. Ambiguous separators are those that can
* separate nouns in a noun list as well as commands; for example "and"
* and commas.
*
* First, CommandProdWithDefiniteConj, which is for a single full
* command, unambiguously terminated with a separator that can only mean
* that a new command follows. We parse one command at a time when a
* command line includes several commands, since we can only resolve
* objects - and thus can only choose structural interpretations - when
* we reach the game state in which a given command is to be executed.
*
* Second, CommandProdWithAmbiguousConj, which is for multiple commands
* separated by a conjunction that does not end a sentence, but could
* just as well separate two noun phrases. In this case, we parse both
* commands, to ensure that we actually have a well-formed command
* following the conjunction; this allows us to try interpreting the part
* after the conjunction as a command and also as a noun phrase, to see
* which interpretation looks better.
*
* When we find an unambiguous command separator, we can simply use the
* '*' grammar match to put off parsing everything after the separator
* until later, reducing the complexity of the grammar tree. When we
* find an ambiguous separator, we can't put off parsing the rest with
* '*', because we need to know if the part after the separator can
* indeed be construed as a new command. During resolution, we'll take a
* noun list interpretation of 'and' over a command list whenever doing
* so would give us resolvable noun phrases.
*/
/*
* Match class for a command phrase that is separated from anything that
* follows with an unambiguous conjunction.
*
* Grammar rules based on this match class must set property cmd_ to the
* underlying 'predicate' production.
*/
class CommandProdWithDefiniteConj: CommandProd
resolveNouns(issuingActor, targetActor, results)
{
/* resolve nouns */
cmd_.resolveNouns(issuingActor, targetActor, results);
/* count commands */
countCommands(results);
}
countCommands(results)
{
/* we have only one subcommand */
if (cmdCounted_++ == 0)
results.incCommandCount();
}
/* counter: have we counted our command in the results object yet? */
cmdCounted_ = 0
/* resolve my first action */
resolveFirstAction(issuingActor, targetActor)
{
return cmd_.resolveAction(issuingActor, targetActor);
}
/* does this command end a sentence */
isEndOfSentence()
{
/*
* it's the end of the sentence if our predicate encompasses all
* of the tokens in the command, or the conjunction is a
* sentence-ending conjunction
*/
return conj_ == nil || conj_.isEndOfSentence();
}
/*
* Get the token index of the first command separator token. This
* is the first token that is not part of the underlying command.
*/
getCommandSepIndex()
{
/*
* if we have a conjunction, return the first token index of the
* conjunction; otherwise, return the index of the next token
* after the command itself
*/
if (conj_ != nil)
return conj_.firstTokenIndex;
else
return cmd_.lastTokenIndex + 1;
}
/*
* get the token index of the next command - this is the index of
* the next token after our conjunction if we have one, or after our
* command if we don't have a conjunction
*/
getNextCommandIndex()
{
return (conj_ == nil ? cmd_ : conj_).lastTokenIndex + 1;
}
;
/*
* Match class for two command phrases separated by an ambiguous
* conjunction (i.e., a conjunction that could also separate two noun
* phrases). Grammar rules based on this class must set the properties
* 'cmd1_' to the underlying 'predicate' production match of the first
* command, and 'cmd2_' to the underlying 'commandPhrase' production
* match of the second command.
*/
class CommandProdWithAmbiguousConj: CommandProd
resolveNouns(issuingActor, targetActor, results)
{
/*
* Resolve nouns in the first subcommand only. Do NOT resolve
* nouns in any of the additional subcommands (there might be
* more than one, since cmd2_ can be a list of subcommands, not
* just a single subcommand), because we cannot assume that the
* current scope will continue to be valid after executing the
* first subcommand - the first command could take us to a
* different location, or change the lighting conditions, or add
* or remove objects from the location, or any number of other
* things that would invalidate the current scope.
*/
cmd1_.resolveNouns(issuingActor, targetActor, results);
/* count our commands */
countCommands(results);
}
countCommands(results)
{
/* count our first subcommand (cmd1_) */
if (cmdCounted_++ == 0)
results.incCommandCount();
/* count results in the rest of the list (cmd2_ and its children) */
cmd2_.countCommands(results);
}
/* counter: have we counted our command in the results object yet? */
cmdCounted_ = 0
/* resolve my first action */
resolveFirstAction(issuingActor, targetActor)
{
return cmd1_.resolveAction(issuingActor, targetActor);
}
/* does this command end a sentence */
isEndOfSentence()
{
/*
* it's the end of the sentence if the conjunction is a
* sentence-ending conjunction
*/
return conj_.isEndOfSentence();
}
/*
* Get the token index of the first command separator token. This
* is the first token that is not part of the underlying command.
*/
getCommandSepIndex()
{
/* return the conjunction's first token index */
return conj_.firstTokenIndex;
}
/*
* get the token index of the next command - this is simply the
* starting index for our second subcommand tree
*/
getNextCommandIndex() { return cmd2_.firstTokenIndex; }
;
/*
* The basic grammar rule for an unambiguous end-of-sentence command.
* This matches either a predicate with nothing following, or a predicate
* with an unambiguous command-only conjunction following.
*/
grammar commandPhrase(definiteConj):
predicate->cmd_
| predicate->cmd_ commandOnlyConjunction->conj_ *
: CommandProdWithDefiniteConj
;
/*
* the basic grammar rule for a pair of commands with an ambiguous
* separator (i.e., a separator that could separate commands or
* conjunctions)
*/
grammar commandPhrase(ambiguousConj):
predicate->cmd1_ commandOrNounConjunction->conj_ commandPhrase->cmd2_
: CommandProdWithAmbiguousConj
;
/* ------------------------------------------------------------------------ */
/*
* We leave almost everything about the grammatical rules of noun
* phrases to the language-specific implementation, but we provide a set
* of base classes for implementing several conceptual structures that
* have equivalents in most languages.
*/
/*
* Basic noun phrase production class.
*/
class NounPhraseProd: BasicProd
/*
* Determine whether this kind of noun phrase prefers to keep a
* collective or the collective's individuals when filtering. If
* this is true, we'll keep a collective and discard its individuals
* when filtering a resolution list; otherwise, we'll drop the
* collective and keep the individuals.
*/
filterForCollectives = nil
/*
* Filter a "verify" result list for the results we'd like to keep
* in the final resolution of the noun phrase. This is called after
* we've run through the verification process on the list of
* candidate matches, so 'lst' is a list of ResolveInfo objects,
* sorted in descending order of logicalness.
*
* By default, we keep only the items that are equally as logical as
* the best item in the results. Since the items are sorted in
* descending order of goodness, the best item is always the first
* item.
*/
getVerifyKeepers(results)
{
local best;
/*
* Reduce the list to the most logical elements - in other
* words, keep only the elements that are exactly as logical as
* the first element, which we know to have the best logicalness
* ranking in the list by virtue of having sorted the list in
* descending order of logicalness.
*/
best = results[1];
return results.subset({x: x.compareTo(best) == 0});
}
/*
* Filter a match list of results for truncated matches. If we have
* a mix of truncated matches and exact matches, we'll keep only the
* exact matches. If we have only truncated matches, though, we'll
* return the list unchanged, as we don't have a better offer going.
*/
filterTruncations(lst, resolver)
{
local exactLst;
/*
* If we're in "global scope," it means that we're resolving
* something like a topic phrase, and we're not limited to the
* current physical scope but can choose objects from the entire
* game world. In this case, don't apply any truncation
* filtering. The reason is that we could have several unrelated
* objects in the game with vocabulary that differs only in
* truncation, but the user has forgotten about the ones with the
* longer names and is thinking of something she's referred to
* with truncation in the past, when the longer-named objects
* weren't around. We're aware of all of those longer-named
* objects, but it's not helpful to the player, since they might
* currently be out of sight.
*/
if (resolver.isGlobalScope)
return lst;
/* get the list of exact (i.e., untruncated) matches */
exactLst = lst.subset(
{x: (x.flags_ & (VocabTruncated | PluralTruncated)) == 0});
/*
* if we have any exact matches, use only the exact matches; if
* we don't, use the full list as is
*/
return (exactLst.length() != 0 ? exactLst : lst);
}
;
/*
* Basic noun phrase list production class.
*/
class NounListProd: BasicProd
;
/*
* Basic "layered" noun phrase production. It's often useful to define
* a grammar rule that simply defers to an underlying grammar rule; we
* make this simpler by defining this class that automatically delegates
* resolveNouns to the underlying noun phrase given by the property np_.
*/
class LayeredNounPhraseProd: NounPhraseProd
resolveNouns(resolver, results)
{
/* let the underlying match tree object do the work */
return np_.resolveNouns(resolver, results);
}
;
/* ------------------------------------------------------------------------ */
/*
* A single noun is sometimes required where, structurally, a list is
* not allowed. Single nouns should not be used to prohibit lists where
* there is no structural reason for the prohibition - these should be
* used only where it doesn't make sense to use a list structurally.
*/
class SingleNounProd: NounPhraseProd
resolveNouns(resolver, results)
{
/* tell the results object we're resolving a single-object slot */
results.beginSingleObjSlot();
/* resolve the underlying noun phrase */
local lst = np_.resolveNouns(resolver, results);
/* tell the results object we're done with the single-object slot */
results.endSingleObjSlot();
/* make sure the list has only one element */
if (lst.length() > 1)
results.uniqueObjectRequired(getOrigText(), lst);
/* return the results */
return lst;
}
;
/*
* A user could attempt to use a noun list where a single noun is
* required. This is not a grammatical error, so we accept it
* grammatically; however, for disambiguation purposes we score it lower
* than a singleNoun production with only one noun phrase, and if we try
* to resolve it, we'll fail with an error.
*/
class SingleNounWithListProd: NounPhraseProd
resolveNouns(resolver, results)
{
/* note the problem */
results.singleObjectRequired(getOrigText());
/*
* In case we have any unknown words or other detrimental
* aspects, resolve the underlying noun list as well, so we can
* score even lower. Note that doing this after noting our
* single-object-required problem will avoid any interactive
* resolution (such as asking for disambiguation information),
* since once we get to the point where we're ready to do
* anything interactive, the single-object-required results
* notification will fail with an error, so we won't make it to
* this point during interactive resolution.
*/
np_.resolveNouns(resolver, results);
/* we have no resolution */
return [];
}
;
/*
* A topic is a noun phrase used in commands like "ask <person> about
* <topic>." For our purposes, this works as an ordinary single noun
* production.
*/
class TopicProd: SingleNounProd
/* get the original text and tokens from the underlying phrase */
getOrigTokenList() { return np_.getOrigTokenList(); }
getOrigText() { return np_.getOrigText(); }
resolveNouns(resolver, results)
{
/* note that we're entering a topic slot */
results.beginTopicSlot();
/* do the inherited work */
local ret = inherited(resolver, results);
/* we're leaving the topic slot */
results.endTopicSlot();
/* return the resolved list */
return ret;
}
;
/*
* A literal is a string enclosed in quotes.
*/
class LiteralProd: BasicProd
;
/*
* Basic class for pronoun phrases. The specific pronouns are
* language-dependent; each instance should define its pronounType
* property to an appropriate PronounXxx constant.
*/
class PronounProd: NounPhraseProd
resolveNouns(resolver, results)
{
local lst;
/*
* check for a valid anaphoric binding (i.e., a back-reference to
* an object mentioned earlier in the current command: ASK BOB
* ABOUT HIMSELF)
*/
lst = checkAnaphoricBinding(resolver, results);
/*
* if we didn't get an anaphoric binding, find an antecedent from
* a previous command
*/
if (lst == nil)
{
/* ask the resolver to get the antecedent */
lst = resolver.resolvePronounAntecedent(
pronounType, self, results, isPossessive);
/* if there are no results, note the error */
if (lst == [])
results.noMatchForPronoun(pronounType,
getOrigText().toLower());
/* remember the pronoun type in each ResolveInfo */
lst.forEach({x: x.pronounType_ = pronounType});
/*
* If the pronoun is singular, but we got multiple potential
* antecedents, it means that the previous command had
* multiple noun slots (as in UNLOCK DOOR WITH KEY) and
* didn't want to decide a priori which one is the antecedent
* for future pronouns. So, we have to decide now which of
* the potential antecedents to use as the actual antecedent.
* Choose the most logical, if there's a clearly more logical
* one.
*/
if (!isPlural && lst.length() > 1)
{
/* filter the phrase using the normal disambiguation */
lst = resolver.filterAmbiguousNounPhrase(lst, 1, self);
/*
* if that leaves more than one item, pick the first one
* and mark it as unclearly disambiguated
*/
if (lst.length() > 1)
{
/* arbitrarily keep the first item only */
lst = lst.sublist(1, 1);
/* mark it as an arbitrary choice */
lst[1].flags_ |= UnclearDisambig;
}
}
}
/* note that we have phrase involving a pronoun */
results.notePronoun();
/* return the result list */
return lst;
}
/*
* our pronoun specifier - this must be set in each rule instance to
* one of the PronounXxx constants to specify which pronoun to use
* when resolving the pronoun phrase
*/
pronounType = nil
/* is this a possessive usage? */
isPossessive = nil
/*
* Is this pronoun a singular or a plural? A pronoun like "it" or
* "he" is singular, because it refers to a single antecedent; "them"
* is plural. Language modules that define their own custom pronoun
* subclasses should override this as needed.
*/
isPlural = nil
/*
* Check for an anaphoric binding. Returns a list (which is allowed
* to be empty) if this can refer back to an earlier noun phrase in
* the same command, nil if not. By default, we consider pronouns
* to be non-anaphoric, meaning they refer to something from a
* previous sentence, not something in this same sentence. In most
* languages, pronouns don't refer to objects in other noun phrases
* within the same predicate unless they're reflexive.
*/
checkAnaphoricBinding(resolver, results) { return nil; }
;
/*
* For simplicity, define subclasses of PronounProd for the basic set of
* pronouns found in most languages. Language-specific grammar
* definitions can choose to use these or not, and can add their own
* extra subclasses as needed for types of pronouns we don't define
* here.
*/
class ItProd: PronounProd
pronounType = PronounIt
;
class ThemProd: PronounProd
pronounType = PronounThem
isPlural = true
;
class HimProd: PronounProd
pronounType = PronounHim
;
class HerProd: PronounProd
pronounType = PronounHer
;
class MeProd: PronounProd
pronounType = PronounMe
;
class YouProd: PronounProd
pronounType = PronounYou
;
/*
* Third-person anaphoric reflexive pronouns. These refer to objects
* that appeared earlier in the sentence: "ask bob about himself."
*/
class ReflexivePronounProd: PronounProd
resolveNouns(resolver, results)
{
/* ask the resolver for the reflexive pronoun binding */
local lst = resolver.getReflexiveBinding(pronounType);
/*
* if the result is empty, the verb will provide the binding
* later, on a second pass
*/
if (lst == [])
return lst;
/*
* If the result is nil, the verb is saying that the reflexive
* pronoun makes no sense internally within the predicate
* structure. In this case, or if we did get a list that
* doesn't agree with the pronoun type (in number or gender, for
* example), consider the reflexive to refer back to the actor,
* for a construct such as TELL BOB TO HIT HIMSELF. However,
* only do this if the issuer and target actor aren't the same,
* since we generally don't refer to the PC in the third person.
*/
if ((lst == nil || !checkAgreement(lst))
&& resolver.actor_ != resolver.issuer_)
{
/* try treating the actor as the reflexive pronoun */
lst = [new ResolveInfo(resolver.actor_, 0, self)];
}
/* if the list is nil, it means reflexives aren't allowed here */
if (lst == nil)
{
results.reflexiveNotAllowed(pronounType, getOrigText.toLower());
return [];
}
/*
* Check the list for agreement (in gender, number, and so on).
* Don't bother if the list is empty, as this is the action's
* way of telling us that it doesn't have a binding for us yet
* but will provide one on a subsequent attempt at re-resolving
* this phrase.
*/
if (!checkAgreement(lst))
results.wrongReflexive(pronounType, getOrigText().toLower());
/* return the result list */
return lst;
}
/*
* Check that the binding we found for our reflexive pronoun agrees
* with the pronoun in gender, number, and anything else that it has
* to agree with. This must be defined by each concrete subclass.
* Note that language-specific subclasses can and *should* override
* this to test agreement for the local language's rules.
*
* This should return true if we agree, nil if not.
*/
checkAgreement(lst) { return true; }
;
class ItselfProd: ReflexivePronounProd
pronounType = PronounIt
;
class ThemselvesProd: ReflexivePronounProd
pronounType = PronounThem
;
class HimselfProd: ReflexivePronounProd
pronounType = PronounHim
;
class HerselfProd: ReflexivePronounProd
pronounType = PronounHer
;
/*
* The special 'all' constructions are full noun phrases.
*/
class EverythingProd: BasicProd
resolveNouns(resolver, results)
{
local lst;
/* check to make sure 'all' is allowed */
if (!resolver.allowAll())
{
/* it's not - flag an error and give up */
results.allNotAllowed();
return [];
}
/* get the 'all' list */
lst = resolver.getAll(self);
/*
* set the "always announce" flag for each item - the player
* didn't name these items specifically, so always show what we
* chose
*/
foreach (local cur in lst)
cur.flags_ |= AlwaysAnnounce | MatchedAll;
/* make sure there's something in it */
if (lst.length() == 0)
results.noMatchForAll();
/* return the list */
return lst;
}
/* match Collective objects instead of their individuals */
filterForCollectives = true
;
/* ------------------------------------------------------------------------ */
/*
* Basic exclusion list ("except the silver one") production base class.
*/
class ExceptListProd: BasicProd
;
/*
* Basic "but" rule, which selects a list of plurals minus a list of
* specifically excepted objects. This can be used to construct more
* specific production classes for things like "everything but the book"
* and "all books except the red ones".
*
* In each grammar rule based on this class, the 'except_' property must
* be set to a suitable noun phrase for the exception list. We'll
* resolve this list and remove the objects in it from our main list.
*/
class ButProd: NounPhraseProd
resolveNouns(resolver, results)
{
local mainList;
local butList;
local butRemapList;
local action;
local role;
local remapProp;
/* get our main list of items to include */
mainList = getMainList(resolver, results);
/* filter out truncated matches if we have any exact matches */
mainList = filterTruncations(mainList, resolver);
/*
* resolve the 'except' list - use an 'except' resolver based on
* our list so that we resolve these objects in the scope of our
* main list
*/
butList = except_.resolveNouns(
new ExceptResolver(mainList, getOrigText(), resolver),
new ExceptResults(results));
/* if the exception list is empty, tell the results about it */
if (butList == [])
results.noteEmptyBut();
/*
* Get the remapping property for this object role for this
* action. This property applies to each of the objects we're
* resolving, and tells us if the resolved object is going to
* remap its handling of this action when the object is used in
* this role. For example, the Take action's remapping property
* for the direct object would usually be remapDobjTake, so
* book.remapDobjTake would tell us if TAKE BOOK were going to
* be remapped.
*/
action = resolver.getAction();
role = resolver.whichObject;
remapProp = action.getRemapPropForRole(role);
/* get the list of simple synonym remappings for the 'except' list */
butRemapList = butList.mapAll(
{x: action.getSimpleSynonymRemap(x.obj_, role, remapProp)});
/*
* scan the 'all' list, and remove each item that appears in the
* 'except' list
*/
for (local i = 1, local len = mainList.length() ; i <= len ; ++i)
{
local curRemap;
/* get the current 'all' list element */
local cur = mainList[i].obj_;
/* get the simple synonym remapping for this item, if any */
curRemap = action.getSimpleSynonymRemap(cur, role, remapProp);
/*
* If this item appears in the 'except' list, remove it.
*
* Similarly, if this item is remapped to something that
* appears in the 'except' list, remove it.
*
* Similarly, if something in the 'except' list is remapped
* to this item, remove this item.
*/
if (butList.indexWhich({x: x.obj_ == cur}) != nil
|| butRemapList.indexWhich({x: x == cur}) != nil
|| butList.indexWhich({x: x.obj_ == curRemap}) != nil)
{
/* remove it and adjust our loop counters accordingly */
mainList = mainList.removeElementAt(i);
--i;
--len;
}
}
/* if that doesn't leave anything, it's an error */
if (mainList == [])
flagAllExcepted(resolver, results);
/* perform the final filtering on the list for our subclass */
mainList = filterFinalList(mainList);
/* add any flags to the result list that our subclass indicates */
foreach (local cur in mainList)
cur.flags_ |= addedFlags;
/* note the matched objects in the results */
results.noteMatches(mainList);
/* return whatever we have left after the exclusions */
return mainList;
}
/* get my main list, which is the list of items to include */
getMainList(resolver, results) { return []; }
/* flag an error - everything has been excluded by the 'but' list */
flagAllExcepted(resolver, results) { }
/* filter the final list - by default we just return the same list */
filterFinalList(lst) { return lst; }
/* by default, add no extra flags to our resolved object list */
addedFlags = 0
;
/* ------------------------------------------------------------------------ */
/*
* Base class for "all but" rules, which select everything available
* except for objects in a specified list of exceptions; for example, in
* English, "take everything but the book".
*/
class EverythingButProd: ButProd
/* our main list is given by the "all" list */
getMainList(resolver, results)
{
/* check to make sure 'all' is allowed */
if (!resolver.allowAll())
{
/* it's not - flag an error and give up */
results.allNotAllowed();
return [];
}
/* return the 'all' list */
return resolver.getAll(self);
}
/* flag an error - our main list has been completely excluded */
flagAllExcepted(resolver, results)
{
results.noMatchForAllBut();
}
/*
* set the "always announce" flag for each item - the player didn't
* name the selected items specifically, so always show what we
* chose
*/
addedFlags = AlwaysAnnounce
/* match Collective objects instead of their individuals */
filterForCollectives = true
;
/*
* Base class for "list but" rules, which select everything in an
* explicitly provided list minus a set of exceptions; for example, in
* English, "take all of the books except the red ones".
*
* Subclasses defining grammar rules must set the 'np_' property to the
* main noun list; we'll resolve this list to find the objects to be
* included before exclusions are applied.
*/
class ListButProd: ButProd
/* our main list is given by the 'np_' subproduction */
getMainList(resolver, results)
{
return np_.resolveNouns(resolver, results);
}
/* flag an error - everything has been excluded */
flagAllExcepted(resolver, results)
{
results.noMatchForListBut();
}
/*
* set the "unclear disambig" flag in our results, so we provide an
* indication of which object we chose
*/
addedFlags = UnclearDisambig
;
/* ------------------------------------------------------------------------ */
/*
* Pre-resolved phrase production. This isn't normally used in any
* actual grammar; instead, this is for use when building actions
* programmatically. This allows us to fill in an action tree when we
* already know the object we want to resolve.
*/
class PreResolvedProd: BasicProd
construct(obj)
{
/* if it's not a collection, we need to make it a list */
if (!obj.ofKind(Collection))
{
/* if it's not already a ResolveInfo, wrap it in a ResolveInfo */
if (!obj.ofKind(ResolveInfo))
obj = new ResolveInfo(obj, 0, self);
/* the resolved object list is simply the one ResolveInfo */
obj = [obj];
}
/* store the new ResolveInfo list */
obj_ = obj;
}
/* resolve the nouns: this is easy, since we started out resolved */
resolveNouns(resolver, results)
{
/* return our pre-resolved object */
return obj_;
}
/*
* Our pre-resolved object result. This is a list containing a
* single ResolveInfo representing our resolved object, since this is
* the form required by callers of resolveNouns.
*/
obj_ = nil
;
/*
* A pre-resolved *ambiguous* noun phrase. This is used when the game
* or library wants to suggest a specific set of objects for a new
* action, then ask which one to use.
*/
class PreResolvedAmbigProd: DefiniteNounProd
construct(objs, asker, phrase)
{
/* remember my list of possible objects as a resolved object list */
objs_ = objs.mapAll({x: new ResolveInfo(x, 0, nil)});
/* remember the ResolveAsker to use */
asker_ = asker;
/* remember the noun phrase to use in disambiguation questions */
phrase_ = phrase;
}
resolveNouns(resolver, results)
{
/* resolve our list using definite-phrase rules */
return resolveDefinite(asker_, phrase_, objs_,
self, resolver, results);
}
/* my pre-resolved list of ambiguous objects */
objs_ = nil
/* the noun phrase to use in disambiguation questions */
phrase_ = nil
/* the ResolveAsker to use when prompting for the selection */
asker_ = nil
;
/*
* Pre-resolved literal phrase production
*/
class PreResolvedLiteralProd: BasicProd
construct(txt)
{
/*
* If the argument is a ResolveInfo, assume its obj_ property
* gives the literal string, and retrieve the string.
*/
if (txt.ofKind(ResolveInfo))
txt = txt.obj_;
/* save the text */
text_ = txt;
}
/* get the text */
getLiteralText(results, action, which) { return text_; }
getTentativeLiteralText() { return text_; }
/* our underlying text */
text_ = nil
;
/*
* A token list production is an internally generated placeholder when we
* synthesize a production rather than matching grammar, and we want to
* keep track of the token list that triggered the node.
*/
class TokenListProd: BasicProd
construct(toks)
{
tokenList = toks;
firstTokenIndex = 1;
lastTokenIndex = toks.length();
}
/* the token list */
tokenList = nil
;
/* ------------------------------------------------------------------------ */
/*
* Mix-in class for noun phrase productions that use
* ResolveResults.ambiguousNounPhrase(). This mix-in provides the
* methods that ambiguousNounPhrase() uses to keep track of past
* responses to the disambiguation question.
*/
class AmbigResponseKeeper: object
addAmbigResponse(resp)
{
/* add an ambiguous response to our list */
ambigResponses_ += resp;
}
getAmbigResponses()
{
/* return our list of past interactive disambiguation responses */
return ambigResponses_;
}
/* our list of saved interactive disambiguation responses */
ambigResponses_ = []
;
/* ------------------------------------------------------------------------ */
/*
* Base class for noun phrase productions with definite articles.
*/
class DefiniteNounProd: NounPhraseProd, AmbigResponseKeeper
resolveNouns(resolver, results)
{
/* resolve our underlying noun phrase using definite rules */
return resolveDefinite(ResolveAsker, np_.getOrigText(),
np_.resolveNouns(resolver, results),
self, resolver, results);
}
/*
* Resolve an underlying phrase using definite noun phrase rules.
*/
resolveDefinite(asker, origText, lst, responseKeeper, resolver, results)
{
local scopeList;
local fullList;
/* filter the list to remove truncations if we have exact matches */
lst = filterTruncations(lst, resolver);
/*
* Remember the current list, before filtering for logical
* matches and before filtering out equivalents, as our full
* scope list. If we have to ask for clarification, this is the
* scope of the clarification.
*/
scopeList = lst;
/* filter for possessive qualification strength */
lst = resolver.filterPossRank(lst, 1);
/* filter out the obvious mismatches */
lst = resolver.filterAmbiguousNounPhrase(lst, 1, self);
/*
* remember the current list as the full match list, before
* filtering equivalents
*/
fullList = lst;
/*
* reduce the list to include only one of each set of
* equivalents; do this only if the results object allows this
* kind of filtering
*/
if (results.allowEquivalentFiltering)
lst = resolver.filterAmbiguousEquivalents(lst, self);
/* do any additional subclass-specific filtering on the list */
lst = reduceDefinite(lst, resolver, results);
/*
* This is (explicitly or implicitly) a definite noun phrase, so
* we must resolve to exactly one object. If the list has more
* than one object, we must disambiguate it.
*/
if (lst.length() == 0)
{
/* there are no objects matching the phrase */
results.noMatch(resolver.getAction(), origText);
}
else if (lst.length() > 1)
{
/*
* the noun phrase is ambiguous - pass in the full list
* (rather than the list with the redundant equivalents
* weeded out) so that we can display the appropriate
* choices for multiple equivalent items
*/
lst = results.ambiguousNounPhrase(
responseKeeper, asker, origText, lst, fullList,
scopeList, 1, resolver);
}
/* note the matched objects in the results */
results.noteMatches(lst);
/* return the results */
return lst;
}
/*
* Do any additional subclass-specific filtering to further reduce
* the list before we decide whether or not we have sufficient
* specificity. We call this just before deciding whether or not to
* prompt for more information ("which book do you mean...?"). By
* default, this simply returns the same list unchanged; subclasses
* that have some further way of narrowing down the options can use
* this opportunity to apply that extra narrowing.
*/
reduceDefinite(lst, resolver, results) { return lst; }
;
/*
* Base class for a plural production
*/
class PluralProd: NounPhraseProd
/*
* Basic plural noun resolution. We'll retrieve the matching objects
* and filter them using filterPluralPhrase.
*/
basicPluralResolveNouns(resolver, results)
{
local lst;
/* resolve the underlying noun phrase */
lst = np_.resolveNouns(resolver, results);
/* if there's nothing in it, it's an error */
if (lst.length() == 0)
results.noMatch(resolver.getAction(), np_.getOrigText());
/* filter out truncated matches if we have any exact matches */
lst = filterTruncations(lst, resolver);
/* filter the plurals for the logical subset and return the result */
lst = resolver.filterPluralPhrase(lst, self);
/* if we have a list, sort it by pluralOrder */
if (lst != nil)
lst = lst.sort(SortAsc,
{a, b: a.obj_.pluralOrder - b.obj_.pluralOrder});
/* note the matches */
results.noteMatches(lst);
/* note the plural in the results */
results.notePlural();
/* return the result */
return lst;
}
/*
* Get the verify "keepers" for a plural phrase.
*
* If the "filter plural matches" configuration flag is set to true,
* we'll return the subset of items which are logical for this
* command. If the filter flag is nil, we'll simply return the full
* set of vocabulary matches without any filtering.
*/
getVerifyKeepers(results)
{
/* check the global "filter plural matches" configuration flag */
if (gameMain.filterPluralMatches)
{
local sub;
/* get the subset of items that don't exclude plurals */
sub = results.subset({x: !x.excludePluralMatches});
/*
* if that's non-empty, use it as the result; otherwise, just
* use the original list
*/
return (sub.length() != 0 ? sub : results);
}
else
{
/* we don't want any filtering */
return results;
}
}
;
/*
* A plural phrase that explicitly selects all of matching objects. For
* English, this would be a phrase like "all of the marbles". This type
* of phrase matches all of the objects that match the name in the
* plural, except that if we have a collective object and we also have
* objects that are part of the collective (such as a bag of marbles and
* some individual marbles), we'll omit the collective, and match only
* the individual items.
*
* Grammar rule instantiations in language modules should set property
* np_ to the plural phrase match tree.
*/
class AllPluralProd: PluralProd
resolveNouns(resolver, results)
{
/* return the basic plural resolution */
return basicPluralResolveNouns(resolver, results);
}
/*
* since the player explicitly told us to use ALL of the matching
* objects, keep everything in the verify list, logical or not
*/
getVerifyKeepers(results) { return results; }
/* prefer to keep individuals over collectives */
filterForCollectives = nil
;
/*
* A plural phrase qualified by a definite article ("the books"). This
* type of phrasing doesn't specify anything about the specific number of
* items involved, except that they number more than one.
*
* In most cases, we take this to imply that all of the matching objects
* are intended to be included, with one exception: when we have an
* object that can serve as a collective for some of the other objects,
* we match only the collective but not the other objects. For example,
* if we type "take marbles," and we have five marbles and a bag of
* marbles that serves as a collective object for three of the five
* marbles, we'll match the bag and two marbles not in the bag, but NOT
* the marbles that are in the bag. This is usually desirable when
* there's a collective object, since it applies the command to the
* object standing in for the group rather than applying the command one
* by one to each of the individuals in the group.
*/
class DefinitePluralProd: PluralProd
resolveNouns(resolver, results)
{
/* return the basic plural resolution */
return basicPluralResolveNouns(resolver, results);
}
/* prefer to keep collectives instead of their individuals */
filterForCollectives = true
;
/*
* Quantified plural phrase.
*/
class QuantifiedPluralProd: PluralProd
/*
* Resolve the main noun phrase. By default, we simply resolve np_,
* but we make this separately overridable to allow this class to be
* subclassed for quantifying other types of plural phrases.
*
* If this is unable to resolve the list, it can flag an appropriate
* error via the results object and return nil. If this routine
* returns nil, our main resolver will simply return an empty list
* without further flagging of any errors.
*/
resolveMainPhrase(resolver, results)
{
/* resolve the main noun phrase */
return np_.resolveNouns(resolver, results);
}
/*
* get the quantity specified - by default, this comes from the
* quantifier phrase in "quant_"
*/
getQuantity() { return quant_.getval(); }
/* resolve the noun phrase */
resolveNouns(resolver, results)
{
local lst;
local num;
/* resolve the underlying noun phrase */
if ((lst = resolveMainPhrase(resolver, results)) == nil)
return [];
/* filter out truncated matches if we have any exact matches */
lst = filterTruncations(lst, resolver);
/* sort the list in ascending order of pluralOrder */
if (lst != nil)
lst = lst.sort(SortAsc,
{a, b: a.obj_.pluralOrder - b.obj_.pluralOrder});
/* get the quantity desired */
num = getQuantity();
/*
* if we have at least the desired number, arbitrarily choose
* the desired number; otherwise, it's an error
*/
if (num == 0)
{
/* zero objects makes no sense */
results.zeroQuantity(np_.getOrigText());
}
else
{
local qsum;
/* if we have too many, disambiguate */
if (lst.length() >= num)
{
local scopeList;
/* remember the list of everything in scope that matches */
scopeList = lst;
/* filter for possessive qualifier strength */
lst = resolver.filterPossRank(lst, num);
/*
* Use the normal disambiguation ranking to find the best
* set of possibilities.
*/
lst = resolver.filterAmbiguousNounPhrase(lst, num, self);
/*
* If that left us with more than we're looking for, call
* our selection routine to select the subset. If it
* left us with too few, note it in the results.
*/
if (lst.length() > num)
{
/* select the desired exact count */
lst = selectExactCount(lst, num, scopeList,
resolver, results);
}
}
/*
* Check to make sure we have enough items matching. Go by
* the 'quant_' property of the ResolveInfo entries, since we
* might have a single ResolveInfo object that represents a
* quantity of objects from the player's perspective.
*/
qsum = 0;
lst.forEach({x: qsum += x.quant_});
if (qsum < num)
{
/* note in the results that there aren't enough matches */
results.insufficientQuantity(np_.getOrigText(), lst, num);
}
}
/* note the matched objects in the results */
results.noteMatches(lst);
/* return the results */
return lst;
}
/*
* Select the desired number of matches from what the normal
* disambiguation filtering leaves us with.
*
* Note that this will never be called with 'num' larger than the
* number in the current list. This is only called to select a
* smaller subset than we currently have.
*
* By default, we'll simply select an arbitrary subset, since we
* simply want any 'num' of the matches. This can be overridden if
* other behaviors are needed.
*/
selectExactCount(lst, num, scopeList, resolver, results)
{
/*
* If we want less than what we actually got, arbitrarily pick
* the first 'num' elements; otherwise, return what we have.
*/
if (lst.length() > num)
return lst.sublist(1, num);
else
return lst;
}
/*
* Since the player explicitly told us to use a given number of
* matching objects, keep the required number, logical or not.
*/
getVerifyKeepers(results)
{
/* get the quantity desired */
local num = getQuantity();
/*
* if we have at least the number required, arbitrarily choose
* the initial subset of the desired length; otherwise, use them
* all
*/
if (results.length() > num)
return results.sublist(1, num);
else
return results;
}
;
/*
* Exact quantified plural phrase. This is similar to the normal
* quantified plural, but has the additional requirement of matching an
* unambiguous set of the exact given number ("the five books" means
* that we expect to find exactly five books matching the phrase - no
* fewer, and no more).
*/
class ExactQuantifiedPluralProd: QuantifiedPluralProd, AmbigResponseKeeper
/*
* Select the desired number of matches. Since we want an exact set
* of matches, we'll run disambiguation on the set.
*/
selectExactCount(lst, num, scopeList, resolver, results)
{
local fullList;
/* remember the list before filtering for redundant equivalents */
fullList = lst;
/* reduce the list by removing redundant equivalents, if allowed */
if (results.allowEquivalentFiltering)
lst = resolver.filterAmbiguousEquivalents(lst, self);
/*
* if the reduced list has only one element, everything in the
* original list must have been equivalent, so arbitrarily pick
* the desired number of items from the original list
*/
if (lst.length() == 1)
return fullList.sublist(1, num);
/* we still have too many items, so disambiguate the results */
return results.ambiguousNounPhrase(
self, ResolveAsker, np_.getOrigText(),
lst, fullList, scopeList, num, resolver);
}
/* get the keepers in the verify stage */
getVerifyKeepers(results)
{
/*
* keep everything: we want an exact quantity, so we want the
* keepers to match the required quantity on their own, without
* any arbitrary paring down
*/
return results;
}
;
/*
* Noun phrase with an indefinite article
*/
class IndefiniteNounProd: NounPhraseProd
/*
* resolve the main phrase - this is separately overridable to allow
* subclassing
*/
resolveMainPhrase(resolver, results)
{
/* by default, resolve the main noun phrase */
return np_.resolveNouns(resolver, results);
}
resolveNouns(resolver, results)
{
local lst;
local allEquiv = nil;
/* resolve the underlying list */
if ((lst = resolveMainPhrase(resolver, results)) == nil)
return [];
/* filter out truncated matches if we have any exact matches */
lst = filterTruncations(lst, resolver);
/* see what we found */
if (lst.length() == 0)
{
/* it turned up nothing - note the problem */
results.noMatch(resolver.getAction(), np_.getOrigText());
}
else if (lst.length() > 1)
{
/*
* There are multiple objects, but the phrase is indefinite,
* which means that it doesn't refer to a specific matching
* object but could refer to any of them.
*/
/* start by noting if the choices are all equivalent */
allEquiv = areAllEquiv(lst);
/*
* Filter using possessive qualifier strength and then normal
* disambiguation ranking to find the best set of
* possibilities, then pick which we want.
*/
lst = resolver.filterPossRank(lst, 1);
lst = resolver.filterAmbiguousNounPhrase(lst, 1, self);
lst = selectFromList(resolver, results, lst);
}
/*
* Set the "unclear disambiguation" flag on the item we picked -
* our selection was arbitrary, so it's polite to let the player
* know which we chose. However, don't do this if the possible
* matches were all equivalent to start with, as the player's
* input must already have been as specific as we can be in
* reporting the choice.
*/
if (lst.length() == 1 && !allEquiv)
lst[1].flags_ |= UnclearDisambig;
/* note the matched objects in the results */
results.noteMatches(lst);
/* note that this is an indefinite phrasing */
results.noteIndefinite();
/* return the results */
return lst;
}
/* are all of the items in the resolve list equivalents? */
areAllEquiv(lst)
{
local first = lst[1].obj_;
/* check each item to see if it's equivalent to the first */
for (local i = 2, local cnt = lst.length() ; i <= cnt ; ++i)
{
/*
* if this one isn't equivalent to the first, then they're
* not all equivalent
*/
if (!first.isVocabEquivalent(lst[i].obj_))
return nil;
}
/* we didn't find any non-equivalents, so they're all equivalents */
return true;
}
/*
* Select an item from the list of potential matches, given the list
* sorted from most likely to least likely (according to the
* resolver's ambiguous match filter). We'll ask the resolver to
* make the selection, because indefinite noun phrases can mean
* different things in different contexts.
*/
selectFromList(resolver, results, lst)
{
/* ask the resolver to select */
return resolver.selectIndefinite(results, lst, 1);
}
;
/*
* Noun phrase explicitly asking us to choose an object arbitrarily
* (with a word like "any"). This is similar to the indefinite noun
* phrase, but differs in that this phrase is *explicitly* arbitrary,
* rather than merely indefinite.
*/
class ArbitraryNounProd: IndefiniteNounProd
/*
* Select an object from a list of potential matches. Since the
* choice is explicitly arbitrary, we simply choose the first
* (they're in order from most likely to least likely, so this will
* choose the most likely).
*/
selectFromList(resolver, results, lst)
{
/* simply select the first item */
return lst.sublist(1, 1);
}
;
/*
* Noun phrase with an indefinite article and an exclusion ("any of the
* books except the red one")
*/
class IndefiniteNounButProd: ButProd
/* resolve our main phrase */
resolveMainPhrase(resolver, results)
{
/* note that this is an indefinite phrasing */
results.noteIndefinite();
/* by default, simply resolve the underlying noun phrase */
return np_.resolveNouns(resolver, results);
}
/* get our main list */
getMainList(resolver, results)
{
local lst;
/* resolve the underlying list */
if ((lst = resolveMainPhrase(resolver, results)) == nil)
return [];
/* filter for possessive qualifier strength */
lst = resolver.filterPossRank(lst, 1);
/* filter it to pick the most likely objects */
lst = resolver.filterAmbiguousNounPhrase(lst, 1, self);
/* return the filtered list */
return lst;
}
/* flag an error - everything has been excluded */
flagAllExcepted(resolver, results)
{
results.noMatchForListBut();
}
/* filter the final list */
filterFinalList(lst)
{
/* we want to keep only one item - arbitrarily take the first one */
return (lst.length() == 0 ? [] : lst.sublist(1, 1));
}
/*
* set the "unclear disambig" flag in our results, so we provide an
* indication of which object we chose
*/
addedFlags = UnclearDisambig
;
/*
* A qualified plural phrase explicitly including two objects (such as,
* in English, "both books").
*/
class BothPluralProd: ExactQuantifiedPluralProd
/* the quantity specified by a "both" phrase is 2 */
getQuantity() { return 2; }
;
/* ------------------------------------------------------------------------ */
/*
* Possessive adjectives
*/
class PossessivePronounAdjProd: PronounProd
/*
* Possessive pronouns can refer to the earlier noun phrases of the
* same predicate, which is to say that they're anaphoric. For
* example, in GIVE BOB HIS BOOK, 'his' refers to Bob.
*/
checkAnaphoricBinding(resolver, results)
{
local lst;
/* if we simply can't be an anaphor, there's no binding */
if (!canBeAnaphor)
return nil;
/* ask the resolver for the reflexive binding, if any */
lst = resolver.getReflexiveBinding(pronounType);
/*
* If there's no binding from the verb, or it doesn't match in
* number and gender, try an anaphoric binding from the actor.
*/
if (lst == nil || (lst != [] && !checkAnaphorAgreement(lst)))
{
/* get the actor's anaphoric possessive binding */
local obj = resolver.actor_.getPossAnaphor(pronounType);
/* if we got an object or a list, make a resolve list */
if (obj != nil)
{
if (obj.ofKind(Collection))
lst = obj.mapAll({x: new ResolveInfo(x, 0, self)});
else
lst = [new ResolveInfo(obj, 0, self)];
}
}
/*
* If we got a binding, make sure we agree in number and gender;
* if not, don't use the anaphoric form. This isn't an error;
* it just means we're falling back on the regular antecedent
* binding. If we have an empty list, it means the action isn't
* ready to tell us the binding yet, so we can't verify it yet.
*/
if (lst != nil && (lst == [] || checkAnaphorAgreement(lst)))
return lst;
/* don't use an anaphoric binding */
return nil;
}
/* this is a possessive usage of the pronoun */
isPossessive = true
/*
* Can we be an anaphor? By default, we consider third-person
* possessive pronouns to be anaphoric, and others to be
* non-anaphoric. For example, in GIVE BOB MY BOOK, MY always refers
* to the speaker, so it's clearly not anaphoric within the sentence.
*/
canBeAnaphor = true
/*
* Check agreement to a given anaphoric pronoun binding. The
* language module should override this for each pronoun type to
* ensure that the actual contents of the list agree in number and
* gender with this type of pronoun. If so, return true; if not,
* return nil. It's not an error or a ranking demerit if we don't
* agree; it just means that we'll fall back on the regular pronoun
* antecedent rather than trying to use an anaphoric binding.
*/
checkAnaphorAgreement(lst) { return true; }
/*
* By default, the "main text" of a possessive pronoun is the same as
* the actual token text. Languages can override this as needed>
*/
getOrigMainText() { return getOrigText(); }
;
class ItsAdjProd: PossessivePronounAdjProd
pronounType = PronounIt
;
class HisAdjProd: PossessivePronounAdjProd
pronounType = PronounHim
;
class HerAdjProd: PossessivePronounAdjProd
pronounType = PronounHer
;
class TheirAdjProd: PossessivePronounAdjProd
pronounType = PronounThem
;
class YourAdjProd: PossessivePronounAdjProd
pronounType = PronounYou
canBeAnaphor = nil
;
class MyAdjProd: PossessivePronounAdjProd
pronounType = PronounMe
canBeAnaphor = nil
;
/*
* Possessive nouns
*/
class PossessivePronounNounProd: PronounProd
/* this is a possessive usage of the pronoun */
isPossessive = true
;
class ItsNounProd: PossessivePronounNounProd
pronounType = PronounIt
;
class HisNounProd: PossessivePronounNounProd
pronounType = PronounHim
;
class HersNounProd: PossessivePronounNounProd
pronounType = PronounHer
;
class TheirsNounProd: PossessivePronounNounProd
pronounType = PronounThem
;
class YoursNounProd: PossessivePronounNounProd
pronounType = PronounYou
;
class MineNounProd: PossessivePronounNounProd
pronounType = PronounMe
;
/*
* Basic possessive phrase. The grammar rules for these phrases must map
* the possessive qualifier phrase to poss_, and the noun phrase being
* qualified to np_. We are based on DefiniteNounProd because we resolve
* the possessive qualifier as though it had a definite article.
*
* The possessive production object poss_ must define the method
* getOrigMainText() to return the text of its noun phrase in a format
* suitable for disambiguation prompts or error messages. In English,
* for example, this means that the getOrigMainText() must omit the
* apostrophe-S suffix if present.
*/
class BasicPossessiveProd: DefiniteNounProd
/*
* To allow this class to be mixed with other classes that have
* mixed-in ambiguous response keepers, create a separate object to
* hold our ambiguous response keeper for the possessive phrase. We
* will never use our own ambiguous response keeper properties, so
* those are available to any other production class we're mixed
* into.
*/
construct()
{
/* create an AmbigResponseKeeper for the possessive phrase */
npKeeper = new AmbigResponseKeeper;
}
/*
* Resolve the possessive, and perform preliminary resolution of the
* qualified noun phrase. We find the owner object and reduce the
* resolved objects for the qualified phrase to those owned by the
* owner.
*
* If we fail, we return nil. Otherwise, we return a list of the
* tentatively resolved objects. The caller can further resolve
* this list as needed.
*/
resolvePossessive(resolver, results, num)
{
/* resolve the underlying noun phrase being qualified */
local lst = np_.resolveNouns(resolver, results);
/* if we found no matches for the noun phrase, so note */
if (lst.length() == 0)
{
results.noMatchPossessive(resolver.getAction(), np_.getOrigText());
return nil;
}
/*
* resolve the possessive phrase and reduce the list to select
* only the items owned by the possessor
*/
lst = selectWithPossessive(resolver, results, lst,
np_.getOrigText(), num);
/* return the tentative resolution list for the qualified phrase */
return lst;
}
/*
* Resolve the possessive, and reduce the given match list by
* selecting only those items owned by the resolution of the
* possessive phrase.
*
* 'num' is the number of objects we want to select. If the noun
* phrase being qualified is singular, this will be 1; if it's
* plural, this will be nil, to indicate that there's no specific
* target quantity; if the phrase is something like "bob's five
* books," the the number will be the qualifying quantity (5, in this
* case).
*/
selectWithPossessive(resolver, results, lst, lstOrigText, num)
{
/*
* Create the possessive resolver. Note that we resolve the
* possessive phrase in the context of the resolver's indicated
* qualifier resolver, which might not be the same as the
* resolver for the overall phrase.
*/
local possResolver = resolver.getPossessiveResolver();
/* enter a single-object slot for the possessive phrase */
results.beginSingleObjSlot();
/* resolve the underlying possessive */
local possLst = poss_.resolveNouns(possResolver, results);
/* perform the normal resolve list filtering */
possLst = resolver.getAction().finishResolveList(
possLst, resolver.whichObject, self, nil);
/* done with the single-object slot */
results.endSingleObjSlot();
/*
* If that resolved to an empty list, return now with an empty
* list. The underlying possessive resolver will have noted an
* error if this is indeed an error; if it's not an error, it
* means that we're pending resolution of the other noun phrase
* to resolve an anaphor in the possessive phrase.
*/
if (possLst == [])
{
/*
* we must have a pending anaphor to resolve - simply return
* the current list without any possessive filtering
*/
return lst;
}
/*
* If the possessive phrase itself is singular, treat the
* possessive phrase as a definite phrase, requiring an
* unambiguous referent. If it's plural ("the men's books"),
* leave it as it is, taking it to mean that we want to select
* things that are owned by any/all of the possessors.
*
* Note that the possessive phrase has no qualifier - any
* qualifier applies to the noun phrase our possessive is also
* qualifying, not to the possessive phrase itself.
*/
local owner;
if (poss_.isPluralPossessive)
{
/*
* The possessive phrase is plural, so don't reduce its match
* to a single object; instead, select all of the objects
* owned by any of the possessors. The owner is anyone in
* the list.
*/
owner = possLst.mapAll({x: x.obj_});
}
else
{
/*
* the possessive phrase is singular, so resolve the
* possessive qualifier as a definite noun
*/
possLst = resolveDefinite(
ResolveAsker, poss_.getOrigMainText(), possLst,
npKeeper, possResolver, results);
/*
* if we didn't manage to find a single resolution to the
* possessive phrase, we can't resolve the rest of the phrase
* yet
*/
if (possLst.length() != 1)
{
/* if we got more than one object, it's a separate error */
if (possLst.length() > 1)
results.uniqueObjectRequired(poss_.getOrigMainText(),
possLst);
/* we can't go on */
return [];
}
/* get the resolved owner object */
owner = [possLst[1].obj_];
}
/* select the objects owned by any of the owners */
local newLst = lst.subset({x: owner.indexWhich(
{y: x.obj_.isOwnedBy(y)}) != nil});
/*
* If that didn't leave any results, try one more thing: if the
* owner is itself in the list of possessed objects, keep the
* owner. This allows for sequences like this:
*
* >take book
*. Which book?
*. >the man's
*. Which man, Bob or Dave?
*. >bob's
*
* In this case, the qualified object list will be [bob,dave],
* and the owner will be [bob], so we want to keep [bob] as the
* result list.
*/
if (newLst == [])
newLst = lst.subset({x: owner.indexOf(x.obj_) != nil});
/* use the new list we found */
lst = newLst;
/*
* Give each item an ownership priority ranking, since the items
* are being qualified by owner: explicitly owned items have the
* highest ranking, items directly held but not explicitly owned
* have the second ranking, and items merely held have the
* lowest ranking. If we deem the list to be ambiguous later,
* we'll apply the ownership priority ranking first in trying to
* disambiguate.
*/
foreach (local cur in lst)
{
/*
* give the item a ranking: explicitly owned, directly held,
* or other
*/
if (owner.indexOf(cur.obj_.owner) != nil)
cur.possRank_ = 2;
else if (owner.indexWhich({x: cur.obj_.isDirectlyIn(x)}) != nil)
cur.possRank_ = 1;
}
/* if we found nothing, mention it */
if (lst.length() == 0)
{
results.noMatchForPossessive(owner, lstOrigText);
return [];
}
/* return the reduced list */
return lst;
}
/* our ambiguous response keeper */
npKeeper = nil
;
/*
* Possessive phrase + singular noun phrase. The language grammar rule
* must map poss_ to the possessive production and np_ to the noun
* phrase being qualified.
*/
class PossessiveNounProd: BasicPossessiveProd
resolveNouns(resolver, results)
{
local lst;
/*
* perform the initial qualification; if that fails, give up now
* and return an empty list
*/
if ((lst = resolvePossessive(resolver, results, 1)) == nil)
return [];
/* now resolve the underlying list definitely */
return resolveDefinite(ResolveAsker, np_.getOrigText(), lst, self,
resolver, results);
}
/* our AmbigResponseKeeper for the qualified noun phrase */
npKeeper = nil
;
/*
* Possessive phrase + plural noun phrase. The grammar rule must set
* poss_ to the possessive and np_ to the plural.
*/
class PossessivePluralProd: BasicPossessiveProd
resolveNouns(resolver, results)
{
local lst;
/* perform the initial qualifier resolution */
if ((lst = resolvePossessive(resolver, results, nil)) == nil)
return [];
/* filter out truncated matches if we have any exact matches */
lst = filterTruncations(lst, resolver);
/* filter the plurals for the logical subset */
lst = resolver.filterPluralPhrase(lst, self);
/* if we have a list, sort it by pluralOrder */
if (lst != nil)
lst = lst.sort(SortAsc,
{a, b: a.obj_.pluralOrder - b.obj_.pluralOrder});
/* note the matched objects in the results */
results.noteMatches(lst);
/* we want everything in the list, so return what we found */
return lst;
}
;
/*
* Possessive plural with a specific quantity that must be exact
*/
class ExactQuantifiedPossessivePluralProd:
ExactQuantifiedPluralProd, BasicPossessiveProd
/*
* resolve the main noun phrase - this is the possessive-qualified
* plural phrase
*/
resolveMainPhrase(resolver, results)
{
return resolvePossessive(resolver, results, getQuantity());
}
;
/*
* Possessive noun used in an exclusion list. This is for things like
* the "mine" in a phrase like "take keys except mine".
*/
class ButPossessiveProd: BasicPossessiveProd
resolveNouns(resolver, results)
{
/*
* resolve the possessive phrase, and use it to reduce the
* resolver's main list (this is the list before the "except"
* from which are choosing items to exclude) to those items
* owned by the object indicated in the possessive phrase
*/
return selectWithPossessive(resolver, results, resolver.mainList,
resolver.mainListText, nil);
}
;
/*
* Possessive phrase production for disambiguation. This base class can
* be used for grammar productions that match possessive phrases in
* disambiguation prompt ("which book do you mean...?") responses.
*/
class DisambigPossessiveProd: BasicPossessiveProd, DisambigProd
resolveNouns(resolver, results)
{
local lst;
/*
* Remember the original qualified list (this is the list of
* objects from which we're trying to choose on the basis of the
* possessive phrase we're resolving now). We can feed the
* qualified-object list back into the selection process for the
* qualifier itself, because we're looking for a qualifier that
* makes sense when combined with one of the qualified objects.
*/
qualifiedList_ = resolver.matchList;
/* select from the match list using the possessive phrase */
lst = selectWithPossessive(resolver, results,
resolver.matchList, resolver.matchText, 1);
/*
* if the list has more than one entry, treat the result as still
* ambiguous - a simple possessive response to a disambiguation
* query is implicitly definite, so we must select a single
* object
*/
if (lst != nil && lst.length() > 1)
lst = results.ambiguousNounPhrase(
self, ResolveAsker, resolver.matchText,
lst, resolver.fullMatchList, lst, 1, resolver);
/*
* if we failed to resolve it, return an empty list; otherwise
* return the list
*/
return (lst == nil ? [] : lst);
}
/*
* Do extra filter during disambiguation. Since we have a list of
* objects we're trying to qualify, we can look at that list to see
* if some of the possible matches for the qualifier phrase are
* owners of things in the qualified list.
*/
reduceDefinite(lst, resolver, results)
{
local newLst;
/*
* try reducing the list to owners of objects that appear in the
* qualified object list
*/
newLst = lst.subset({x: qualifiedList_.indexWhich(
{y: y.obj_.isOwnedBy(x.obj_)}) != nil});
/* if there's anything in that list, keep only the subset */
if (newLst.length() > 0)
lst = newLst;
/* return the result */
return lst;
}
/*
* the list of objects being qualified - this is the list of books,
* for example, in "bob's books"
*/
qualifiedList_ = []
;
/* ------------------------------------------------------------------------ */
/*
* A noun phrase with explicit containment. Grammar rules based on this
* class must set the property np_ to the main noun phrase, and cont_ to
* the noun phrase giving the container.
*
* We're based on the definite noun phrase production, because we need
* to resolve the underlying container phrase to a singe, unambiguous
* object.
*/
class ContainerNounPhraseProd: DefiniteNounProd
resolveNouns(resolver, results)
{
local lst;
local cRes;
local cLst;
local cont;
/*
* We have two separate noun phrases to resolve: the qualified
* noun phrase in np_, and the locational qualifier in cont_.
* We then want to filter the object matches for np_ to select
* the subset that is contained in cont_.
*
* We must resolve cont_ to a single, unambiguous object.
* However, we want to be smart about it by limiting the range
* of choices to objects that actually contain something that
* could match the possible resolutions of np_. So, tentatively
* resolve np_ first, to get the range of possible matches.
*/
lst = np_.resolveNouns(resolver, results);
/*
* We have the tentative resolution of the main noun phrase, so
* we can now resolve the locational qualifier phrase. Use our
* special container resolver for this step, since this will try
* to pick objects that contain something in the tentative
* results for the main list. Resolve the container as a
* definite noun phrase, since we want a single, unambiguous
* match.
*
* Note that we must base our special container resolver on the
* qualifier resolver, not on the main resolver. The location is
* a qualifying phrase, so it's resolved in the scope of any
* other qualifying phrase.
*
* The container phrase has to be a single object, so note in the
* results that we're working on a single-object slot.
*/
results.beginSingleObjSlot();
cRes = new ContainerResolver(lst, np_.getOrigText(),
resolver.getQualifierResolver());
cLst = resolveDefinite(ResolveAsker, cont_.getOrigText(),
cont_.resolveNouns(cRes, results),
self, cRes, results);
results.endSingleObjSlot();
/*
* We need a single object in the container list. If we have
* no objects, or more than one object, it's an error.
*/
if (cLst.length() != 1)
{
/* it's a separate error if we got more than one object */
if (cLst.length() > 1)
results.uniqueObjectRequired(cont_.getOrigText(), cLst);
/* we can't go on */
return [];
}
/* we have a unique item, so it's the container */
cont = cLst[1].obj_;
/* reduce the list to those objects inside the container */
lst = lst.subset({x: x.obj_.isNominallyIn(cont)});
/*
* If we have some objects directly in the container, and other
* objects indirectly in the container, filter the list to
* include only the directly contained items.
*/
if (lst.indexWhich({x: x.obj_.isDirectlyIn(cont)}) != nil)
lst = lst.subset({x: x.obj_.isDirectlyIn(cont)});
/* if that leaves nothing, mention it */
if (lst.length() == 0)
{
results.noMatchForLocation(cont, np_.getOrigText());
return [];
}
/* return the list */
return lst;
}
;
/*
* Basic container resolver. This is a common subclass for the standard
* container resolver and the "vague" container resolver.
*/
class BasicContainerResolver: ProxyResolver
/* we're a sub-phrase resolver */
isSubResolver = true
/* resolve any qualifiers in the main scope */
getQualifierResolver() { return origResolver; }
/* filter an ambiguous noun phrase */
filterAmbiguousNounPhrase(lst, requiredNum, np)
{
local outer;
local lcl;
/* do the normal filtering first */
lst = inherited(lst, requiredNum, np);
/*
* get the subset that includes only local objects - that is,
* objects within the same outermost room as the target actor
*/
outer = actor_.getOutermostRoom();
lcl = lst.subset({x: x.obj_.isIn(outer)});
/* if there's a local subset, take the subset */
if (lcl.length() != 0)
lst = lcl;
/* return the result */
return lst;
}
;
/*
* Container Resolver. This is a proxy for the main qualifier resolver
* that prefers to match objects that are plausible in the sense that
* they contain something in the tentative resolution of the main list.
*/
class ContainerResolver: BasicContainerResolver
construct(mainList, mainText, origResolver)
{
/* inherit base handling */
inherited(origResolver);
/* remember my tentative main match list */
self.mainList = mainList;
self.mainListText = mainText;
}
/* filter ambiguous equivalents */
filterAmbiguousEquivalents(lst, np)
{
local vec;
/*
* Check to see if any of the objects in the list are plausible
* containers for objects in our main list. If we can find any
* plausible entries, keep only the plausible ones.
*/
vec = new Vector(lst.length());
foreach (local cur in lst)
{
/* if this item is plausible, add it to our result vector */
if (mainList.indexWhich(
{x: x.obj_.isNominallyIn(cur.obj_)}) != nil)
vec.append(cur);
}
/*
* if we found anything plausible, return only the plausible
* subset; otherwise, return the full original list, since
* they're all equally implausible
*/
if (vec.length() != 0)
return vec.toList();
else
return lst;
}
/* the tentative match list for the main phrase we're qualifying */
mainList = nil
/* the text of the main phrase we're qualifying */
mainListText = nil
;
/* ------------------------------------------------------------------------ */
/*
* A "vague" container noun phrase. This is a phrase that specifies a
* container but nothing else: "the one in the box", "the ones in the
* box", "everything in the box".
*/
class VagueContainerNounPhraseProd: DefiniteNounProd
resolveNouns(resolver, results)
{
local cRes;
local cLst;
local lst;
local cont;
/* resolve the container as a single-object slot */
results.beginSingleObjSlot();
/*
* Resolve the container. Use a special resolver that prefers
* objects that have any contents.
*/
cRes = new VagueContainerResolver(resolver.getQualifierResolver());
cLst = resolveDefinite(ResolveAsker, cont_.getOrigText(),
cont_.resolveNouns(cRes, results),
self, cRes, results);
/* done with the single-object slot */
results.endSingleObjSlot();
/* make sure we resolved to a unique container */
if (cLst.length() != 1)
{
/* it's a separate error if we got more than one object */
if (cLst.length() > 1)
results.uniqueObjectRequired(cont_.getOrigText(), cLst);
/* we can't go on */
return [];
}
/* we have a unique item, so it's the container */
cont = cLst[1].obj_;
/*
* If the container is the nominal drop destination for the
* actor's location, then use the actor's location as the actual
* container. This way, if we try to get "all on floor" or the
* like, we'll correctly get objects that are directly in the
* room.
*/
if (cont == resolver.actor_.location.getNominalDropDestination())
cont = resolver.actor_.location;
/* get the contents */
lst = cont.getAllForTakeFrom(resolver.getScopeList());
/* keep only visible objects */
lst = lst.subset({x: resolver.actor_.canSee(x)});
/* map the contents to a resolved object list */
lst = lst.mapAll({x: new ResolveInfo(x, 0, self)});
/* make sure the list isn't empty */
if (lst.length() == 0)
{
/* there's nothing in this container */
results.nothingInLocation(cont);
return [];
}
/* make other subclass-specific checks on the list */
lst = checkContentsList(resolver, results, lst, cont);
/* note the matches */
results.noteMatches(lst);
/* return the list */
return lst;
}
/* check a contents list */
checkContentsList(resolver, results, lst, cont)
{
/* by default, just return the list */
return lst;
}
;
/*
* "All in container"
*/
class AllInContainerNounPhraseProd: VagueContainerNounPhraseProd
/* check a contents list */
checkContentsList(resolver, results, lst, cont)
{
/* keep only items that aren't hidden from "all" */
lst = lst.subset({x: !x.obj_.hideFromAll(resolver.getAction())});
/* set the "matched all" and "always announce as multi" flags */
foreach (local cur in lst)
cur.flags_ |= AlwaysAnnounce | MatchedAll;
/* if that emptied the list, so note */
if (lst.length() == 0)
results.nothingInLocation(cont);
/* return the result list */
return lst;
}
;
/*
* A definite vague container phrase. This selects a single object in a
* given container ("the one in the box"). If more than one object is
* present, we'll try to disambiguate it.
*
* Grammar rules instantiating this class must set the property
* 'mainPhraseText' to the text to display for a disambiguation prompt
* involving the main phrase.
*/
class VagueContainerDefiniteNounPhraseProd: VagueContainerNounPhraseProd
construct()
{
/* create a disambiguator for the main phrase */
npKeeper = new AmbigResponseKeeper();
}
/* check a contents list */
checkContentsList(resolver, results, lst, cont)
{
/*
* This production type requires a single object in the
* container (since it's for phrases like "the one in the box").
* If we have more than one object, try disambiguating.
*/
if (lst.length() > 1)
{
local scopeList;
local fullList;
/*
* There's more than one object in this container. First,
* try filtering it by possessive qualifier strength and
* then the normal disambiguation ranking.
*/
scopeList = lst;
lst = resolver.filterPossRank(lst, 1);
lst = resolver.filterAmbiguousNounPhrase(lst, 1, self);
/* try removing redundant equivalents */
fullList = lst;
if (results.allowEquivalentFiltering)
lst = resolver.filterAmbiguousEquivalents(lst, self);
/* if we still have too many objects, it's ambiguous */
if (lst.length() > 1)
{
/* ask the results object to handle the ambiguous phrase */
lst = results.ambiguousNounPhrase(
npKeeper, ResolveAsker, mainPhraseText,
lst, fullList, scopeList, 1, resolver);
}
}
/* return the contents of the container */
return lst;
}
/* our disambiguation result keeper */
npKeeper = nil
;
/*
* An indefinite vague container phrase. This selects a single object,
* choosing arbitrarily if multiple objects are in the container.
*/
class VagueContainerIndefiniteNounPhraseProd: VagueContainerNounPhraseProd
/* check a contents list */
checkContentsList(resolver, results, lst, cont)
{
/* choose one object arbitrarily */
if (lst.length() > 1)
lst = lst.sublist(1, 1);
/* return the (possibly trimmed) list */
return lst;
}
;
/*
* Container Resolver for vaguely-specified containment phrases. We'll
* select for objects that have contents, but that's about as much as we
* can do, since the main phrase is bounded only by the container in
* vague containment phrases (and thus provides no information that
* would help us narrow down the container itself).
*/
class VagueContainerResolver: BasicContainerResolver
/* filter ambiguous equivalents */
filterAmbiguousEquivalents(lst, np)
{
local vec;
/* prefer objects with contents */
vec = new Vector(lst.length());
foreach (local cur in lst)
{
/* if this item has any contents, add it to the new list */
if (cur.obj_.contents.length() > 0)
vec.append(cur);
}
/*
* if we found anything plausible, return only the plausible
* subset; otherwise, return the full original list, since
* they're all equally implausible
*/
if (vec.length() != 0)
return vec.toList();
else
return lst;
}
;
/* ------------------------------------------------------------------------ */
/*
* Noun phrase with vocabulary resolution. This is a base class for the
* various noun phrases that match adjective, noun, and plural tokens.
* This class provides dictionary resolution of a vocabulary word into a
* list of objects.
*
* In non-declined languages such as English, the parts of speech of our
* words are usually simply 'adjective' and 'noun'. A language
* "declines" its noun phrases if the words in a noun phrase have
* different forms that depend on the function of the noun phrase in a
* sentence; for example, in German, adjectives take suffixes that
* depend upon the gender of the noun being modified and the function of
* the noun phrase in the sentence (subject, direct object, etc). In a
* declined language, it might be desirable to use separate parts of
* speech for separate declined adjective and noun forms.
*/
class NounPhraseWithVocab: NounPhraseProd
/*
* Get a list of the matches in the main dictionary for the given
* token as the given part of speech (&noun, &adjective, &plural, or
* others as appropriate for the local language) that are in scope
* according to the resolver.
*/
getWordMatches(tok, partOfSpeech, resolver, flags, truncFlags)
{
local lst;
/* start with the dictionary matches */
lst = cmdDict.findWord(tok, partOfSpeech);
/* filter it to eliminate redundant matches */
lst = filterDictMatches(lst);
/* add the objects that match the full dictionary word */
lst = inScopeMatches(lst, flags, flags | truncFlags, resolver);
/* return the combined results */
return lst;
}
/*
* Combine two word match lists. This simply adds each entry from
* the second list that doesn't already have a corresponding entry
* in the first list, returning the combined list.
*/
combineWordMatches(aLst, bLst)
{
/* create a vector copy of the original 'a' list */
aLst = new Vector(aLst.length(), aLst);
/*
* add each entry from the second list whose object isn't
* already represented in the first list
*/
foreach (local b in bLst)
{
local ia;
/* look for an existing entry for this object in the 'a' list */
ia = aLst.indexWhich({aCur: aCur.obj_ == b.obj_});
/*
* If this 'b' entry isn't already in the 'a' list, simply
* add the 'b' entry. If both lists have this entry, combine
* the flags into the existing entry.
*/
if (ia == nil)
{
/* it's not already in the 'a' list, so simply add it */
aLst += b;
}
else
{
/*
* Both lists have the entry, so keep the existing 'a'
* entry, but merge the flags. Note that the
* original 'a' might still be interesting to the
* caller separately, so create a copy of it before
* modifying it.
*/
local a = aLst[ia];
aLst[ia] = a = a.createClone();
combineWordMatchItems(a, b);
}
}
/* return the combined list */
return aLst;
}
/*
* Combine the given word match entries. We'll merge the flags of
* the two entries to produce a single merged entry in 'a'.
*/
combineWordMatchItems(a, b)
{
/*
* If one item was matched with truncation and the other wasn't,
* remove the truncation flag entirely. This will make the
* combined entry reflect the fact that we were able to match the
* word without any truncation. The fact that we also matched it
* with truncation isn't relevant, since matching without
* truncation is the stronger condition.
*/
if ((b.flags_ & VocabTruncated) == 0)
a.flags_ &= ~VocabTruncated;
/* likewise for plural truncation */
if ((b.flags_ & PluralTruncated) == 0)
a.flags_ &= ~PluralTruncated;
/*
* Other flags generally are set for the entire list of matching
* objects for a production, rather than at the level of
* individual matching objects, so we don't have to worry about
* combining them - they'll naturally be the same at this point.
*/
}
/*
* Filter a dictionary match list. This is called to clean up the
* raw match list returned from looking up a vocabulary word in the
* dictionary.
*
* The main purpose of this routine is to eliminate unwanted
* redundancy from the dictionary matches; in particular, the
* dictionary might have multiple matches for a given word at a given
* object, due to truncation, upper/lower folding, accent removal,
* and so on. In general, we want to keep only the single strongest
* match from the dictionary for a given word matching a given
* object.
*
* The meaning of "stronger" and "exact" matches is
* language-dependent, so we abstract these with the separate methods
* dictMatchIsExact() and dictMatchIsStronger().
*
* Keep in mind that the raw dictionary match list has alternating
* entries: object, comparator flags, object, comparator flags, etc.
* The return list should be in the same format.
*/
filterDictMatches(lst)
{
local ret;
/* set up a vector to hold the result */
ret = new Vector(lst.length());
/*
* check each inexact element of the list for another entry with
* a stronger match; keep only the ones without a stronger match
*/
truncScan:
/* scan for a stronger match */
for (local i = 1, local len = lst.length() ; i < len ; i += 2)
{
/* get the current object and its flags */
local curObj = lst[i];
local curFlags = lst[i+1];
/* if it's not an exact match, check for a stronger match */
if (!dictMatchIsExact(curFlags))
{
/* scan for an equal or stronger match later in the list */
for (local j = i + 2 ; j < len ; j += 2)
{
/*
* if entry j is stronger than or identical to the
* current entry, omit the current entry in favor of
* entry j
*/
local jObj = lst[j], jFlags = lst[j+1];
if (jObj == curObj
&& (jFlags == curFlags
|| dictMatchIsStronger(jFlags, curFlags)))
{
/* there's a better entry; omit the current one */
continue truncScan;
}
}
}
/* keep this entry - add it to the result vector */
ret.append(curObj);
ret.append(curFlags);
}
/* return the result vector */
return ret;
}
/*
* Check a dictionary match's string comparator flags to see if the
* match is "exact." The exact meaning of "exact" is dependent on
* the language's lexical rules; this generic base version considers
* a match to be exact if it doesn't have any string comparator flags
* other than the base "matched" flag and the case-fold flag. This
* should be suitable for most languages, as (1) case folding usually
* doesn't improve match strength, and (2) any additional comparator
* flags usually indicate some kind of inexact match status.
*
* A language that depends on upper/lower case as a marker of match
* strength will need to override this to consider the case-fold flag
* as significant in determining match exactness. In addition, a
* language that uses additional string comparator flags to indicate
* better (rather than worse) matches will have to override this to
* require the presence of those flags.
*/
dictMatchIsExact(flags)
{
/*
* the match is exact if it doesn't have any qualifier flags
* other than the basic "yes I matched" flag and the case-fold
* flag
*/
return ((flags & ~(StrCompMatch | StrCompCaseFold)) == 0);
}
/*
* Compare two dictionary matches for the same object and determine
* if the first one is stronger than the second. Both are for the
* same object; the only difference is the string comparator flags.
*
* Language modules might need to override this to supplement the
* filtering with their own rules. This generic base version
* considers truncation: an untruncated match is stronger than a
* truncated match. Non-English languages might want to consider
* other lexical factors in the match strength, such as whether we
* matched the exact accented characters or approximated with
* unaccented equivalents - this information will, of course, need to
* be coordinated with the dictionary's string comparator, and
* reflected in the comparator match flags. It's the comparator
* match flags that we're looking at here.
*/
dictMatchIsStronger(flags1, flags2)
{
/*
* if the first match (flags1) indicates no truncation, and the
* second (flags2) was truncated, then the first match is
* stronger; otherwise, there's no distinction as far as we're
* concerned
*/
return ((flags1 & StrCompTrunc) == 0
&& (flags2 & StrCompTrunc) != 0);
}
/*
* Get a list of the matches in the main dictionary for the given
* token, intersecting the resulting list with the given list.
*/
intersectWordMatches(tok, partOfSpeech, resolver, flags, truncFlags, lst)
{
local newList;
/* get the matches to the given word */
newList = getWordMatches(tok, partOfSpeech, resolver,
flags, truncFlags);
/* intersect the result with the other list */
newList = intersectNounLists(newList, lst);
/* return the combined results */
return newList;
}
/*
* Given a list of dictionary matches to a given word, construct a
* list of ResolveInfo objects for the matches that are in scope.
* For regular resolution, "in scope" means the resolver thinks the
* object is in scope.
*/
inScopeMatches(dictionaryMatches, flags, truncFlags, resolver)
{
local ret;
/* set up a vector to hold the results */
ret = new Vector(dictionaryMatches.length());
/*
* Run through the list and include only the words that are in
* scope. Note that the list of dictionary matches has
* alternating objects and match flags, so we must scan it two
* elements at a time.
*/
for (local i = 1, local cnt = dictionaryMatches.length() ;
i <= cnt ; i += 2)
{
/* get this object */
local cur = dictionaryMatches[i];
/* if it's in scope, add a ResolveInfo for this object */
if (resolver.objInScope(cur))
{
local curResults;
local curFlags;
/* get the comparator match results for this object */
curResults = dictionaryMatches[i+1];
/*
* If the results indication truncation, use the the
* truncated flags; otherwise use the ordinary flags.
*/
if (dataType(curResults) == TypeInt
&& (curResults & StrCompTrunc) != 0)
curFlags = truncFlags;
else
curFlags = flags;
/* add the item to the results */
ret.append(new ResolveInfo(cur, curFlags, self));
}
}
/* return the results as a list */
return ret.toList();
}
/*
* Resolve the objects.
*/
resolveNouns(resolver, results)
{
local matchList;
local starList;
/*
* get the preliminary match list - this is simply the set of
* objects that match all of the words in the noun phrase
*/
matchList = getVocabMatchList(resolver, results, 0);
/*
* get a list of any in-scope objects that match '*' for nouns -
* these are objects that want to do all of their name parsing
* themselves
*/
starList = inScopeMatches(cmdDict.findWord('*', &noun),
0, 0, resolver);
/* combine the lists */
matchList = combineWordMatches(matchList, starList);
/* run the results through matchName */
return resolveNounsMatchName(results, resolver, matchList);
}
/*
* Run a set of resolved objects through matchName() or a similar
* routine. Returns the filtered results.
*/
resolveNounsMatchName(results, resolver, matchList)
{
local origTokens;
local adjustedTokens;
local objVec;
local ret;
/* get the original token list for the command */
origTokens = getOrigTokenList();
/* get the adjusted token list for the command */
adjustedTokens = getAdjustedTokens();
/* set up to receive about the same number of results as inputs */
objVec = new Vector(matchList.length());
/* consider each preliminary match */
foreach (local cur in matchList)
{
/* ask this object if it wants to be included */
local newObj = resolver.matchName(
cur.obj_, origTokens, adjustedTokens);
/* check the result */
if (newObj == nil)
{
/*
* it's nil - this means it's not a match for the name
* after all, so leave it out of the results
*/
}
else if (newObj.ofKind(Collection))
{
/*
* it's a collection of some kind - add each element to
* the result list, using the same flags as the original
*/
foreach (local curObj in newObj)
objVec.append(new ResolveInfo(curObj, cur.flags_, self));
}
else
{
/*
* it's a single object - add it ito the result list,
* using the same flags as the original
*/
objVec.append(new ResolveInfo(newObj, cur.flags_, self));
}
}
/* convert the result vector to a list */
ret = objVec.toList();
/* if our list is empty, note it in the results */
if (ret.length() == 0)
{
/*
* If the adjusted token list contains any tokens of type
* "miscWord", send the phrase to the results object for
* further consideration.
*/
if (adjustedTokens.indexOf(&miscWord) != nil)
{
/*
* we have miscWord tokens, so this is a miscWordList
* match - let the results object process it specially.
*/
ret = results.unknownNounPhrase(self, resolver);
}
/*
* if the list is empty, note that we have a noun phrase
* whose vocabulary words don't match anything in the game
*/
if (ret.length() == 0)
results.noVocabMatch(resolver.getAction(), getOrigText());
}
/* return the result list */
return ret;
}
/*
* Each subclass must override getAdjustedTokens() to provide the
* appropriate set of tokens used to match the object. This is
* usually simply the original set of tokens, but in some cases it
* may differ; for example, spelled-out numbers normally adjust to
* the numeral form of the number.
*
* For each adjusted token, the list must have two entries: the
* first is a string giving the token text, and the second is the
* property giving the part of speech for the token.
*/
getAdjustedTokens() { return nil; }
/*
* Get the vocabulary match list. This is simply the set of objects
* that match all of the words in the noun phrase. Each rule
* subclass must override this to return an appropriate list. Note
* that subclasses should use getWordMatches() and
* intersectWordMatches() to build the list.
*/
getVocabMatchList(resolver, results, extraFlags) { return nil; }
;
/* ------------------------------------------------------------------------ */
/*
* An empty noun phrase production is one that matches, typically with
* non-zero badness value, as a placeholder when a command is missing a
* noun phrase where one is required.
*
* Each grammar rule instance of this rule class must define the
* property 'responseProd' to be the production that should be used to
* parse any response to an interactive prompt for the missing object.
*/
class EmptyNounPhraseProd: NounPhraseProd
/* customize the way we generate the prompt and parse the response */
setPrompt(response, asker)
{
/* remember the new response production and ResolveAsker */
responseProd = response;
asker_ = asker;
}
/* resolve the empty phrase */
resolveNouns(resolver, results)
{
/*
* if we've filled in our missing phrase already, return the
* resolution of that list
*/
if (newMatch != nil)
return newMatch.resolveNouns(resolver, results);
/*
* The noun phrase was left out entirely, so try to get an
* implied object.
*/
local match = getImpliedObject(resolver, results);
/* if that succeeded, return the result */
if (match != nil)
return match;
/*
* Parse the next input with our own (subclassed) responseProd if
* we have one. If we don't (i.e., it's nil), get one from the
* action.
*/
local rp = responseProd;
if (rp == nil && resolver.getAction() != nil)
rp = resolver.getAction().getObjResponseProd(resolver);
/* as a last resort, use the fallback response */
if (rp == nil)
rp = fallbackResponseProd;
/*
* There is no implied object, so try to get a result
* interactively. Use the production that our rule instance
* specifies via the responseProd property to parse the
* interactive response.
*/
match = results.askMissingObject(asker_, resolver, rp);
/* if we didn't get a match, we have nothing to return */
if (match == nil)
return [];
/* we got a match - remember it as a proxy for our noun phrase */
newMatch = match;
/* return the resolved noun phrase from the proxy match */
return newMatch.resolvedObjects;
}
/*
* Get an implied object to automatically fill in for the missing
* noun phrase. By default, we simply ask the 'results' object for
* the missing object.
*/
getImpliedObject(resolver, results)
{
/* ask the 'results' object for the information */
return results.getImpliedObject(self, resolver);
}
/*
* Get my tokens. If I have a new match tree, return the tokens
* from the new match tree. Otherwise, we don't have any tokens,
* since we're empty.
*/
getOrigTokenList()
{
return (newMatch != nil ? newMatch.getOrigTokenList() : []);
}
/*
* Get my original text. If I have a new match tree, return the
* text from the new match tree. Otherwise, we have no original
* text, since we're an empty phrase.
*/
getOrigText()
{
return (newMatch != nil ? newMatch.getOrigText() : '');
}
/*
* I'm an empty noun phrase, unless I already have a new match
* object.
*/
isEmptyPhrase { return newMatch == nil; }
/*
* the new match, when we get an interactive response to a query for
* the missing object
*/
newMatch = nil
/*
* Our "response" production - this is the production we use to parse
* the player's input in response to our disambiguation prompt. A
* subclass can leave this as nil, in which case we'll attempt to get
* the appropriate response production from the action.
*/
responseProd = nil
/*
* Our fallback response production - if responseProd is nil, this
* must be supplied for cases where we can't get the production from
* the action. This is ignored if responseProd is non-nil.
*/
fallbackResponseProd = nil
/*
* The ResolveAsker we use to generate our prompt. Use the base
* ResolveAsker by default; this can be overridden when the prompt
* is to be customized.
*/
asker_ = ResolveAsker
;
/*
* An empty noun phrase production for verb phrasings that imply an
* actor, but don't actually include one by name.
*
* This is similar to EmptyNounPhraseProd, but has an important
* difference: if the actor carrying out the command has a current or
* implied conversation partner, then we choose the conversation partner
* as the implied object. This is important in that we don't count the
* noun phrase as technically missing in this case, for the purposes of
* command ranking. This is useful for phrasings that inherently imply
* an actor strongly enough that there should be no match-strength
* penalty for leaving it out.
*/
class ImpliedActorNounPhraseProd: EmptyNounPhraseProd
/* get my implied object */
getImpliedObject(resolver, results)
{
/*
* If the actor has a default interlocutor, use that, bypassing
* the normal implied object search.
*/
local actor = resolver.actor_.getDefaultInterlocutor();
if (actor != nil)
return [new ResolveInfo(actor, DefaultObject, nil)];
/* ask the 'results' object for the information */
return results.getImpliedObject(self, resolver);
}
;
/*
* Empty literal phrase - this serves a purpose similar to that of
* EmptyNounPhraseProd, but can be used where literal phrases are
* required.
*/
class EmptyLiteralPhraseProd: LiteralProd
getLiteralText(results, action, which)
{
local toks;
local prods;
/* if we already have an interactive response, return it */
if (newText != nil)
return newText;
/*
* ask for the missing phrase and remember it for the next time
* we're asked for our text
*/
newText = results.askMissingLiteral(action, which);
/*
* Parse the text (if any) as a literal phrase. In most cases,
* anything can be parsed as a literal phrase, so this might
* seem kind of pointless; however, this is useful when the
* language-specific library defines rules for things like
* removing quotes from a quoted string.
*/
if (newText != nil)
{
try
{
/* tokenize the input */
toks = cmdTokenizer.tokenize(newText);
}
catch (TokErrorNoMatch exc)
{
/* note the token error */
gLibMessages.invalidCommandToken(exc.curChar_.htmlify());
/* treat the command as empty */
throw new ReplacementCommandStringException(nil, nil, nil);
}
/* parse the input as a literal phrase */
prods = literalPhrase.parseTokens(toks, cmdDict);
/* if we got a match, use it */
if (prods.length() > 0)
{
/*
* we got a match - this will be a LiteralProd, so ask
* the matching LiteralProd for its literal text, and
* use that as our new literal text
*/
newText = prods[1].getLiteralText(results, action, which);
}
}
/* return the text */
return newText;
}
/*
* Tentatively get my literal text. This is used for pre-resolution
* when we have another phrase we want to resolve first, but we want
* to provide a tentative form of the text in the meantime. We won't
* attempt to ask for more information interactively, but we'll
* return any information we do have.
*/
getTentativeLiteralText()
{
/*
* if we have a result from a previous interaactive request,
* return it; otherwise we have no tentative text
*/
return newText;
}
/* I'm an empty phrase, unless I already have a text response */
isEmptyPhrase { return newText == nil; }
/* the response to a previous interactive query */
newText = nil
;
/*
* Empty topic phrase production. This is the topic equivalent of
* EmptyNounPhraseProd.
*/
class EmptyTopicPhraseProd: TopicProd
resolveNouns(resolver, results)
{
local match;
/*
* if we've filled in our missing phrase already, return the
* resolution of that list
*/
if (newMatch != nil)
return newMatch.resolveNouns(resolver, results);
/* ask for a topic interactively, using our responseProd */
match = results.askMissingObject(asker_, resolver, responseProd);
/* if we didn't get a match, we have nothing to return */
if (match == nil)
return [];
/* we got a match - remember it as a proxy for our noun phrase */
newMatch = match;
/* return the resolved noun phrase from the proxy match */
return newMatch.resolvedObjects;
}
/* we're an empty phrase if we don't have a new topic yet */
isEmptyPhrase { return newMatch = nil; }
/* get my tokens - use the underlying new match tree if we have one */
getOrigTokenList()
{
return (newMatch != nil ? newMatch.getOrigTokenList() : inherited());
}
/* get my original text - use the new match tree if we have one */
getOrigText()
{
return (newMatch != nil ? newMatch.getOrigText() : inherited());
}
/* our new underlying topic phrase */
newMatch = nil
/*
* by default, parse our interactive response as an ordinary topic
* phrase
*/
responseProd = topicPhrase
/* our ResolveAsker object - this is for customizing the prompt */
asker_ = ResolveAsker
;
/* ------------------------------------------------------------------------ */
/*
* Look for an undefined word in a list of tokens, and give the player a
* chance to correct a typo with "OOPS" if appropriate.
*
* If we find an unknown word and we can prompt for interactive
* resolution, we'll do so, and we'll throw an appropriate exception to
* handle the response. If we can't resolve the missing word
* interactively, we'll throw a parse failure exception.
*
* If there are no undefined words in the command, we'll simply return.
*
* tokList is the list of tokens under examination; this is a subset of
* the full command token list. cmdTokenList is the full command token
* list, in the usual tokenizer format. firstTokenIndex is the index of
* the first token in tokList within cmdTokenList.
*
* cmdType is an rmcXxx code giving the type of input we're reading.
*/
tryOops(tokList, issuingActor, targetActor,
firstTokenIndex, cmdTokenList, cmdType)
{
/* run the main "oops" processor in the player character sense context */
callWithSenseContext(nil, sight,
{: tryOopsMain(tokList, issuingActor, targetActor,
firstTokenIndex, cmdTokenList,
cmdType) });
}
/* main "oops" processor */
tryOopsMain(tokList, issuingActor, targetActor,
firstTokenIndex, cmdTokenList, cmdType)
{
local str;
local unknownIdx;
local oopsMatch;
local toks;
local w;
/*
* Look for a word not in the dictionary.
*/
for (unknownIdx = nil, local i = 1, local len = tokList.length() ;
i <= len ; ++i)
{
local cur;
local typ;
/* get the token value for this word */
cur = getTokVal(tokList[i]);
typ = getTokType(tokList[i]);
/* check to see if this word is defined in the dictionary */
if (typ == tokWord && !cmdDict.isWordDefined(cur))
{
/* note that we found an unknown word */
unknownIdx = i;
/* no need to look any further */
break;
}
}
/*
* if we didn't find an unknown word, there's no need to offer the
* user a chance to correct a typo - simply return without any
* further processing
*/
if (unknownIdx == nil)
return;
/*
* We do have an unknown word, but check one more thing: if we were
* asking some kind of follow-up question, such as a missing-object
* or disambiguation query, check to see if the new entry would parse
* successfully as a new command. It's possible that the new entry
* is a brand new command rather than a response to our question, and
* that the unknown word is valid in the context of a new command -
* it could be part of a literal-phrase, for example.
*/
if (cmdType != rmcCommand)
{
/* parse the command */
local lst = firstCommandPhrase.parseTokens(cmdTokenList, cmdDict);
/* resolve actions */
lst = lst.subset(
{x: x.resolveFirstAction(issuingActor, targetActor) != nil});
/* if we managed to match something, treat it as a new command */
if (lst.length() != 0)
throw new ReplacementCommandStringException(
cmdTokenizer.buildOrigText(cmdTokenList), nil, nil);
}
/* get the unknown word, in presentable form */
w = getTokOrig(tokList[unknownIdx]).htmlify();
/*
* Give them a chance to correct a typo via OOPS if the player
* issued the command. If the command came from an actor other than
* the player character, simply fail the command.
*/
if (!issuingActor.isPlayerChar())
{
/* we can't do this interactively - treat it as a failure */
throw new ParseFailureException(&wordIsUnknown, w);
}
/*
* tell the player about the unknown word, implicitly asking for
* an OOPS to fix it
*/
targetActor.getParserMessageObj().askUnknownWord(targetActor, w);
/*
* Prompt for a new command. We'll use the main command prompt,
* because we want to pretend that we're asking for a brand new
* command, which we'll accept. However, if the player enters
* an OOPS command, we'll process it specially.
*/
getResponse:
str = readMainCommandTokens(rmcOops);
/* re-enable the transcript, if we have one */
if (gTranscript)
gTranscript.activate();
/*
* if the command reader fully processed the input via preparsing,
* we have nothing more to do here - simply throw a replace-command
* exception with the nil string
*/
if (str == nil)
throw new ReplacementCommandStringException(nil, nil, nil);
/* extract the tokens and string from the result */
toks = str[2];
str = str[1];
/* try parsing it as an "oops" command */
oopsMatch = oopsCommand.parseTokens(toks, cmdDict);
/*
* if we found a match, process the OOPS command; otherwise,
* treat it as a brand new command
*/
if (oopsMatch.length() != 0)
{
local badIdx;
/*
* if they typed in just OOPS without any tokens, show an error
* and ask again
*/
if (oopsMatch[1].getNewTokens() == nil)
{
/* tell them they need to supply the missing word */
gLibMessages.oopsMissingWord;
/* go back and try reading another response */
goto getResponse;
}
/*
* Build a new token list by removing the errant token, and
* splicing the new tokens into the original token list to
* replace the errant one.
*
* Note that we'll arbitrarily take the first "oops" match,
* even if there are several. There should be no ambiguity
* in the "oops" grammar rule, but even if there is, it
* doesn't matter, since ultimately all we care about is the
* list of tokens after the "oops".
*/
badIdx = firstTokenIndex + unknownIdx - 1;
cmdTokenList = spliceList(cmdTokenList, badIdx,
oopsMatch[1].getNewTokens());
/*
* Turn the token list back into a string. Since we've edited
* the original text, we want to start over with the new input,
* including running pre-parsing on the text.
*/
str = cmdTokenizer.buildOrigText(cmdTokenList);
/*
* run it through pre-parsing as the same kind of input the
* caller was reading
*/
str = StringPreParser.runAll(str, cmdType);
/*
* if it came back nil, it means the preparser fully handled it;
* in this case, simply throw a nil replacement command string
*/
if (str == nil)
throw new ReplacementCommandStringException(nil, nil, nil);
/* re-tokenize the result of pre-parsing */
cmdTokenList = cmdTokenizer.tokenize(str);
/* retry parsing the edited token list */
throw new RetryCommandTokensException(cmdTokenList);
}
else
{
/*
* they didn't enter something that looks like an "OOPS", so
* it must be a brand new command - parse it as a new
* command by throwing a replacement command exception with
* the new string
*/
throw new ReplacementCommandStringException(str, nil, nil);
}
}
/*
* splice a new sublist into a list, replacing the item at 'idx'
*/
spliceList(lst, idx, newItems)
{
return (lst.sublist(1, idx - 1)
+ newItems
+ lst.sublist(idx + 1));
}
/* ------------------------------------------------------------------------ */
/*
* Try reading a response to a missing object question. If we
* successfully read a noun phrase that matches the given production
* rule, we'll resolve it, stash the resolved list in the
* resolvedObjects_ property of the match tree, and return the match
* tree. If they enter something that doesn't look like a response to
* the question at all, we'll throw a new-command exception to process
* it.
*/
tryAskingForObject(issuingActor, targetActor,
resolver, results, responseProd)
{
/*
* Prompt for a new command. We'll use the main command prompt,
* because we want to pretend that we're asking for a brand new
* command, which we'll accept. However, if the player enters
* something that looks like a response to the missing-object query,
* we'll handle it as an answer rather than as a new command.
*/
local str = readMainCommandTokens(rmcAskObject);
/* re-enable the transcript, if we have one */
if (gTranscript)
gTranscript.activate();
/*
* if it came back nil, it means that the preparser fully processed
* the input, which means we have nothing more to do here - simply
* treat this is a nil replacement command
*/
if (str == nil)
throw new ReplacementCommandStringException(nil, nil, nil);
/* extract the input line and tokens */
local toks = str[2];
str = str[1];
/* keep going as long as we get replacement token lists */
for (;;)
{
/* try parsing it as an object list */
local matchList = responseProd.parseTokens(toks, cmdDict);
/*
* if we didn't find any match at all, it's probably a brand new
* command - go process it as a replacement for the current
* command
*/
if (matchList == [])
{
/*
* they didn't enter something that looks like a valid
* response, so assume it's a brand new command - parse it
* as a new command by throwing a replacement command
* exception with the new string
*/
throw new ReplacementCommandStringException(str, nil, nil);
}
/* if we're in debug mode, show the interpretations */
dbgShowGrammarList(matchList);
/* create an interactive sub-resolver for resolving the response */
local ires = new InteractiveResolver(resolver);
/*
* rank them using our response ranker - use the original
* resolver to resolve the object list
*/
local rankings = MissingObjectRanking.sortByRanking(matchList, ires);
/*
* If the best item has unknown words, try letting the user
* correct typos with OOPS.
*/
if (rankings[1].nonMatchCount != 0
&& rankings[1].unknownWordCount != 0)
{
try
{
/*
* complain about the unknown word and look for an OOPS
* reply
*/
tryOops(toks, issuingActor, targetActor,
1, toks, rmcAskObject);
}
catch (RetryCommandTokensException exc)
{
/* get the new token list */
toks = exc.newTokens_;
/* replace the string as well */
str = cmdTokenizer.buildOrigText(toks);
/* go back for another try at parsing the response */
continue;
}
}
/*
* If the best item we could find has no matches, check to see
* if it has miscellaneous noun phrases - if so, it's probably
* just a new command, since it doesn't have anything we
* recognize as a noun phrase.
*/
if (rankings[1].nonMatchCount != 0
&& rankings[1].miscWordListCount != 0)
{
/*
* it's probably not an answer at all - treat it as a new
* command
*/
throw new ReplacementCommandStringException(str, nil, nil);
}
/* the highest ranked object is the winner */
local match = rankings[1].match;
/*
* Check to see if this looks like an ordinary new command as
* well as a noun phrase. For example, "e" looks like the noun
* phrase "east wall," in that "e" is a synonym for the adjective
* "east". But "e" also looks like an ordinary new command.
*
* We'd have to be able to read the user's mind to know which
* they mean in such cases, so we have to make some assumptions
* to deal with the ambiguity. In particular, if the phrasing
* has special syntax that makes it look like a particularly
* close match to the query phrase, assume it's a query response;
* otherwise, assume it's a new command. For example:
*
*. >dig
*. What do you want to dig in? [sets up for "in <noun>" reply]
*
* If the user answers "IN THE DIRT", we have a match to the
* special syntax of the reply (the "in <noun>" phrasing), so we
* will assume this is a reply to the query, even though it also
* matches a valid new command phrasing for ENTER DIRT. If the
* user answers "E" or "EAST", we *don't* have a match to the
* special syntax, but merely an ordinary noun phrase match, so
* we'll assume this is an ordinary GO EAST command.
*/
local cmdMatchList = firstCommandPhrase.parseTokens(toks, cmdDict);
if (cmdMatchList != [])
{
/*
* The phrasing looks like it's a valid new command as well
* as a noun phrase reply. Check the query reply match for
* special syntax that would distinguish it from a new
* command; if it doesn't match any special syntax, assume
* that it is indeed a new command instead of a query reply.
*/
if (!match.isSpecialResponseMatch)
throw new ReplacementCommandStringException(str, nil, nil);
}
/* show our winning interpretation */
dbgShowGrammarWithCaption('Missing Object Winner', match);
/*
* actually resolve the response to objects, using the original
* results and resolver objects
*/
local objList = match.resolveNouns(ires, results);
/* stash the resolved object list in a property of the match tree */
match.resolvedObjects = objList;
/* return the match tree */
return match;
}
}
/*
* a property we use to hold the resolved objects of a match tree in
* tryAskingForObject
*/
property resolvedObjects;
/*
* Missing-object response ranker.
*/
class MissingObjectRanking: CommandRanking
/*
* missing-object responses have no verb, so they won't count any
* noun slots; we just need to give these an arbitrary value so that
* we can compare them (and find them equal)
*/
nounSlotCount = 0
;
/* ------------------------------------------------------------------------ */
/*
* An implementation of ResolveResults for normal noun resolution.
*/
class BasicResolveResults: ResolveResults
/*
* The target and issuing actors for the command being resolved.
*/
targetActor_ = nil
issuingActor_ = nil
/* set up the actor parameters */
setActors(target, issuer)
{
targetActor_ = target;
issuingActor_ = issuer;
}
/*
* Results gatherer methods
*/
noVocabMatch(action, txt)
{
/* indicate that we couldn't match the phrase */
throw new ParseFailureException(&noMatch,
action, txt.toLower().htmlify());
}
noMatch(action, txt)
{
/* show an error */
throw new ParseFailureException(&noMatch,
action, txt.toLower().htmlify());
}
noMatchPossessive(action, txt)
{
/* use the basic noMatch handling */
noMatch(action, txt);
}
allNotAllowed()
{
/* show an error */
throw new ParseFailureException(&allNotAllowed);
}
noMatchForAll()
{
/* show an error */
throw new ParseFailureException(&noMatchForAll);
}
noteEmptyBut()
{
/* this isn't an error - ignore it */
}
noMatchForAllBut()
{
/* show an error */
throw new ParseFailureException(&noMatchForAllBut);
}
noMatchForListBut()
{
/* show an error */
throw new ParseFailureException(&noMatchForListBut);
}
noMatchForPronoun(typ, txt)
{
/* show an error */
throw new ParseFailureException(&noMatchForPronoun,
typ, txt.toLower().htmlify());
}
reflexiveNotAllowed(typ, txt)
{
/* show an error */
throw new ParseFailureException(&reflexiveNotAllowed,
typ, txt.toLower().htmlify());
}
wrongReflexive(typ, txt)
{
/* show an error */
throw new ParseFailureException(&wrongReflexive,
typ, txt.toLower().htmlify());
}
noMatchForPossessive(owner, txt)
{
/* if the owner is a list, it's a plural owner */
if (owner.length() > 1)
{
/* it's a plural owner */
throw new ParseFailureException(&noMatchForPluralPossessive, txt);
}
else
{
/* let the (singular) owner object generate the error */
owner[1].throwNoMatchForPossessive(txt.toLower().htmlify());
}
}
noMatchForLocation(loc, txt)
{
/* let the location object generate the error */
loc.throwNoMatchForLocation(txt.toLower().htmlify());
}
nothingInLocation(loc)
{
/* let the location object generate the error */
loc.throwNothingInLocation();
}
noteBadPrep()
{
/*
* bad prepositional phrase structure - assume that the
* preposition was intended as part of the verb phrase, so
* indicate that the entire command is not understood
*/
throw new ParseFailureException(&commandNotUnderstood);
}
/*
* Service routine - determine if we can interactively resolve a
* need for more information. If the issuer is the player, and the
* target actor can talk to the player, then we can resolve a
* question interactively; otherwise, we cannot.
*
* We can't interactively resolve a question if the issuer isn't the
* player, because there's no interactive user to prompt for more
* information.
*
* We can't interactively resolve a question if the target actor
* can't talk to the player, because the question to the player
* would be coming out of thin air.
*/
canResolveInteractively(action)
{
/*
* If this is an implied action, and the target actor is in
* "NPC" mode, we can't resolve interactively. Note that if
* there's no action, it can't be implicit (we won't have an
* action if we're resolving the actor to whom the action is
* targeted).
*/
if (action != nil
&& action.isImplicit
&& targetActor_.impliedCommandMode() == ModeNPC)
return nil;
/*
* if the issuer is the player, and either the target is the
* player or the target can talk to the player, we can resolve
* interactively
*/
return (issuingActor_.isPlayerChar()
&& (targetActor_ == issuingActor_
|| targetActor_.canTalkTo(issuingActor_)));
}
/*
* Handle an ambiguous noun phrase. We'll first check to see if we
* can find a Distinguisher that can tell at least some of the
* matches apart; if we fail to do that, we'll just pick the required
* number of objects arbitrarily, since we have no way to distinguish
* any of them. Once we've chosen a Distinguisher, we'll ask the
* user for help interactively if possible.
*/
ambiguousNounPhrase(keeper, asker, txt,
matchList, fullMatchList, scopeList,
requiredNum, resolver)
{
/* put the match list in disambigPromptOrder order */
matchList = matchList.sort(
SortAsc, {a, b: (a.obj_.disambigPromptOrder
- b.obj_.disambigPromptOrder) });
/*
* Get the token list for the original phrase. Since the whole
* point is to disambiguate a phrase that matched multiple
* objects, all of the matched objects came from the same phrase,
* so we can just grab the tokens from the first item.
*/
local np = matchList[1].np_;
local npToks = np ? np.getOrigTokenList() : [];
/* for the prompt, use the lower-cased version of the input text */
local promptTxt = txt.toLower().htmlify();
/* ask the response keeper for its list of past responses, if any */
local pastResponses = keeper.getAmbigResponses();
/*
* set up a results object - use the special disambiguation
* results object instead of the basic resolver type
*/
local disambigResults = new DisambigResults(self);
/*
* start out with an empty still-to-resolve list - we have only
* the one list to resolve so far
*/
local stillToResolve = [];
/* we have nothing in the result list yet */
local resultList = [];
/* determine if we can ask for clarification interactively */
if (!canResolveInteractively(resolver.getAction()))
{
/*
* don't attempt to resolve this interactively - just treat
* it as a resolution failure
*/
throw new ParseFailureException(
&ambiguousNounPhrase, txt.htmlify(),
matchList, fullMatchList);
}
/* we're asking for the first time */
local everAsked = nil;
local askingAgain = nil;
/*
* Keep going until we run out of things left to resolve. Each
* time an answer looks valid but doesn't completely
* disambiguate the results, we'll queue it for another question
* iteration, so we must continue until we run out of queued
* questions.
*/
queryLoop:
for (local pastIdx = 1 ;; )
{
local str;
local toks;
local dist;
local curMatchList = [];
/*
* Find the first distinguisher that can tell one or more of
* these objects apart. Work through the distinguishers in
* priority order, which is the order they appear in an
* object's 'distinguishers' property.
*
* If these objects aren't all equivalents, then we'll
* immediately find that the basic distinguisher can tell
* them all apart. Every object's first distinguisher is
* the basic distinguisher. If these objects are all
* equivalents, then they'll all have the same set of
* distinguishers. In either case, we don't have to worry
* about what set of objects we have, because we're
* guaranteed to have a suitable set of distinguishers in
* common - so just arbitrarily pick an object and work
* through its distinguishers.
*/
foreach (dist in matchList[1].obj_.distinguishers)
{
/* filter the match list using this distinguisher only */
curMatchList = filterWithDistinguisher(matchList, dist);
/*
* if this list has more than the required number of
* results, then this distinguisher is capable of
* telling apart enough of these objects, so use this
* distinguisher for now
*/
if (curMatchList.length() > requiredNum)
break;
}
/*
* If we didn't find any distinguishers that could tell
* apart enough of the objects, then we've exhausted our
* ability to choose among these objects, so return an
* arbitrary set of the desired size.
*/
if (curMatchList.length() <= requiredNum)
return fullMatchList.sublist(1, requiredNum);
/*
* If we have past responses, try reusing the past responses
* before asking for new responses.
*/
if (pastIdx <= pastResponses.length())
{
/* we have another past response - use the next one */
str = pastResponses[pastIdx++];
/* tokenize the new command */
toks = cmdTokenizer.tokenize(str);
}
else
{
/*
* Try filtering the options with the basic
* distinguisher, to see if all of the remaining options
* are basic equivalents - if they are, then we can
* refer to them by the object name, since every one has
* the same object name.
*/
local basicDistList = filterWithDistinguisher(
matchList, basicDistinguisher);
/*
* if we filtered to one object, everything remaining is
* a basic equivalent of that one object, so they all
* have the same name, so we can use that name
*/
if (basicDistList.length() == 1)
promptTxt = basicDistList[1].obj_.disambigEquivName;
/*
* let the distinguisher know we're prompting for more
* information based on this distinguisher
*/
dist.notePrompt(curMatchList);
/*
* We don't have any special insight into which object
* the user might have intended, and we have no past
* responses to re-use, so simply ask for clarification.
* Ask using the full match list, so that we correctly
* indicate where there are multiple equivalents.
*/
asker.askDisambig(targetActor_, promptTxt,
curMatchList, fullMatchList, requiredNum,
everAsked && askingAgain, dist);
/* ask for a new command */
str = readMainCommandTokens(rmcDisambig);
/* re-enable the transcript, if we have one */
if (gTranscript)
gTranscript.activate();
/* note that we've asked for input interactively */
everAsked = true;
/*
* if it came back nil, the input was fully processed by
* the preparser, so throw a nil replacement string
* exception
*/
if (str == nil)
throw new ReplacementCommandStringException(
nil, nil, nil);
/* extract the tokens and the string result */
toks = str[2];
str = str[1];
}
/* presume we won't have to ask again */
askingAgain = nil;
/*
* Add the new tokens to the noun phrase token list, at the
* end, enclosed in parentheses. The effect is that each
* time we answer a disambiguation question, the answer
* appears as a parenthetical at the end of the phrase: "take
* the box (cardboard) (the big one)".
*/
npToks = npToks.append(['(', tokPunct, '(']);
npToks += toks;
npToks = npToks.append([')', tokPunct, ')']);
retryParse:
/*
* Check for narrowing syntax. If the command doesn't
* appear to match a narrowing syntax, then treat it as an
* entirely new command.
*/
local prodList = mainDisambigPhrase.parseTokens(toks, cmdDict);
/*
* if we didn't get any structural matches for a
* disambiguation response, it must be an entirely new
* command
*/
if (prodList == [])
throw new ReplacementCommandStringException(str, nil, nil);
/* if we're in debug mode, show the interpretations */
dbgShowGrammarList(prodList);
/* create a disambiguation response resolver */
local disResolver = new DisambigResolver(
txt, matchList, fullMatchList, fullMatchList, resolver, dist);
/*
* create a disambiguation resolver that uses the full scope
* list - we'll use this as a fallback if we can't match any
* objects with the narrowed possibility list
*/
local scopeDisResolver = new DisambigResolver(
txt, matchList, fullMatchList, scopeList, resolver, dist);
/*
* Run the alternatives through the disambiguation response
* ranking process.
*/
local rankings =
DisambigRanking.sortByRanking(prodList, disResolver);
/*
* If the best item has unknown words, try letting the user
* correct typos with OOPS.
*/
if (rankings[1].nonMatchCount != 0
&& rankings[1].unknownWordCount != 0)
{
try
{
/*
* complain about the unknown word and look for an
* OOPS reply
*/
tryOops(toks, issuingActor_, targetActor_,
1, toks, rmcDisambig);
}
catch (RetryCommandTokensException exc)
{
/* get the new token list */
toks = exc.newTokens_;
/* replace the string as well */
str = cmdTokenizer.buildOrigText(toks);
/* go back for another try at parsing the response */
goto retryParse;
}
}
/*
* If the best item we could find has no matches, check to
* see if it has miscellaneous noun phrases - if so, it's
* probably just a new command, since it doesn't have
* anything we recognize as a noun phrase.
*/
if (rankings[1].nonMatchCount != 0
&& rankings[1].miscWordListCount != 0)
{
/*
* it's probably not an answer to our disambiguation
* question, so treat it as a whole new command -
* abandon the current command and start over with the
* new string
*/
throw new ReplacementCommandStringException(str, nil, nil);
}
/* if we're in debug mode, show the winning intepretation */
dbgShowGrammarWithCaption('Disambig Winner', rankings[1].match);
/* get the response list */
local respList = rankings[1].match.getResponseList();
/*
* Select the objects for each response in our winning list.
* The user can select more than one of the objects we
* offered, so simply take each one they specify here
* separately.
*/
foreach (local resp in respList)
{
try
{
try
{
/*
* select the objects for this match, and add
* them into the matches so far
*/
local newObjs = resp.resolveNouns(
disResolver, disambigResults);
/* set the new token list in the new matches */
for (local n in newObjs)
{
if (n.np_ != nil)
n.np_.setOrigTokenList(npToks);
}
/* add them to the matches so far */
resultList += newObjs;
}
catch (UnmatchedDisambigException udExc)
{
/*
* The response didn't match anything in the
* narrowed list. Try again with the full scope
* list, in case they actually wanted to apply
* the command to something that's in scope but
* which didn't make the cut for the narrowed
* list we originally offered.
*/
resultList +=
resp.resolveNouns(scopeDisResolver,
disambigResults);
}
}
catch (StillAmbiguousException saExc)
{
/*
* Get the new "reduced" list from the exception.
* Use our original full list, and reduce it to
* include only the elements in the reduced list -
* this will ensure that the items in the new list
* are in the same order as they were in the
* original list, which keeps the next iteration's
* question in the same order.
*/
local newList = new Vector(saExc.matchList_.length());
foreach (local cur in fullMatchList)
{
/*
* If this item from the original list has an
* equivalent in the reduced list, include it in
* the new match list.
*/
if (cur.isDistEquivInList(saExc.matchList_, dist))
newList.append(cur);
}
/* convert it to a list */
newList = newList.toList();
/*
* If that left us with nothing, just keep the
* original list unchanged. This can occasionally
* happen, such as when a sub-phrase (a locational
* qualifier, for example) is itself ambiguous.
*/
if (newList == [])
newList = matchList;
/*
* Generate the new "full" list. This is a list of
* all of the items from our current "full" list
* that either are directly in the new match list,
* or are indistinguishable from items in the new
* reduced list.
*
* The exception thrower is not capable of providing
* us with a new full list, because it only knows
* about the reduced list, as the reduced list is
* its scope for narrowing the results.
*/
local newFullList = new Vector(fullMatchList.length());
foreach (local cur in fullMatchList)
{
/*
* if this item is in the new reduced list, or
* has an equivalent in the new match list,
* include it in the new full list
*/
if (cur.isDistEquivInList(newList, dist))
{
/*
* we have this item or something
* indistinguishable - include it in the
* revised full match list
*/
newFullList.append(cur);
}
}
/* convert it to a list */
newFullList = newFullList.toList();
/*
* They answered, but with insufficient specificity.
* Add the given list to the still-to-resolve list,
* so that we try again with this new list.
*/
stillToResolve +=
new StillToResolveItem(newList, newFullList,
saExc.origText_);
}
catch (DisambigOrdinalOutOfRangeException oorExc)
{
/*
* Explain the problem (note that if we didn't want
* to offer another chance here, we could simply
* throw this message as a ParseFailureException
* instead of continuing with the query loop).
*
* If we've never asked interactively for input
* (because we've obtained all of our input from
* past responses to a command we're repeating),
* don't show this message, because it makes no
* sense when they haven't answered any questions in
* the first place.
*/
if (everAsked)
targetActor_.getParserMessageObj().
disambigOrdinalOutOfRange(
targetActor_, oorExc.ord_, txt.htmlify());
/* go back to the outer loop to ask for a new response */
askingAgain = true;
continue queryLoop;
}
catch (UnmatchedDisambigException udExc)
{
local newList;
/*
* They entered something that looked like a
* disambiguation response, but didn't refer to any
* of our objects. Try parsing the input as though
* it were a new command, and if it matches any
* command syntax, treat it as a new command.
*/
newList = firstCommandPhrase.parseTokens(toks, cmdDict);
if (newList.length() != 0)
{
/*
* it appears syntactically to be a new command
* - treat it as such by throwing a command
* replacement exception
*/
throw new ReplacementCommandStringException(
str, nil, nil);
}
/*
* Explain the problem (note that if we didn't want
* to continue with the query loop, we could simply
* throw this message as a ParseFailureException).
*
* Don't show any error if we've never asked
* interactively on this turn (which can only be
* because we're using interactive responses from a
* previous turn that we're repeating), because it
* makes no sense when we haven't apparently asked
* for anything yet.
*/
if (everAsked)
targetActor_.getParserMessageObj().
noMatchDisambig(targetActor_, txt.htmlify(),
udExc.resp_);
/* go back to the outer loop to ask for a new response */
askingAgain = true;
continue queryLoop;
}
}
/*
* If we got this far, the last input we parsed was actually
* a response to our question, as opposed to a new command
* or something unintelligible.
*
* If this was an interactive response, add the response to
* the response keeper's list of past responses. This will
* allow the production to re-use the same list if it has to
* re-resolve the phrase in the future, without asking the
* user to answer the same questions again
*/
if (everAsked)
keeper.addAmbigResponse(str);
/*
* if there's nothing left in the still-to-resolve list, we
* have nothing left to do, so we can stop working
*/
if (stillToResolve.length() == 0)
break;
/*
* set up for the next iteration with the first item in the
* still-to-resolve list
*/
matchList = stillToResolve[1].matchList;
fullMatchList = stillToResolve[1].fullMatchList;
txt = stillToResolve[1].origText;
/* remove the first item, since we're going to process it now */
stillToResolve = stillToResolve.sublist(2);
}
/* success - return the final match list */
return resultList;
}
/*
* filter a match list with a specific Distinguisher
*/
filterWithDistinguisher(lst, dist)
{
local result;
/* create a vector for the result list */
result = new Vector(lst.length());
/* scan the list */
foreach (local cur in lst)
{
/*
* if we have no equivalent of the current item (for the
* purposes of our Distinguisher) in the result list, add
* the current item to the result list
*/
if (!cur.isDistEquivInList(result, dist))
result.append(cur);
}
/* return the result list */
return result.toList();
}
/*
* handle a noun phrase that doesn't match any legal grammar rules
* for noun phrases
*/
unknownNounPhrase(match, resolver)
{
local wordList;
local ret;
/*
* ask the resolver to handle it - if it gives us a resolved
* object list, simply return it
*/
wordList = match.getOrigTokenList();
if ((ret = resolver.resolveUnknownNounPhrase(wordList)) != nil)
return ret;
/*
* The resolver doesn't handle unknown words, so we can't
* resolve the phrase. Look for an undefined word and give the
* player a chance to correct typos with OOPS.
*/
tryOops(wordList, issuingActor_, targetActor_,
match.firstTokenIndex, match.tokenList, rmcCommand);
/*
* If we didn't find any unknown words, it means that they used
* a word that's in the dictionary in a way that makes no sense
* to us. Simply return an empty list and let the resolver
* proceed with its normal handling for unmatched noun phrases.
*/
return [];
}
getImpliedObject(np, resolver)
{
/* ask the resolver to supply an implied default object */
return resolver.getDefaultObject(np);
}
askMissingObject(asker, resolver, responseProd)
{
/* if we can't resolve this interactively, fail with an error */
if (!canResolveInteractively(resolver.getAction()))
{
/* interactive resolution isn't allowed - fail */
throw new ParseFailureException(&missingObject,
resolver.getAction(),
resolver.whichMessageObject);
}
/* have the action show objects already defaulted */
resolver.getAction().announceAllDefaultObjects(nil);
/* ask for a default object */
asker.askMissingObject(targetActor_, resolver.getAction(),
resolver.whichMessageObject);
/* try reading an object response */
return tryAskingForObject(issuingActor_, targetActor_,
resolver, self, responseProd);
}
noteLiteral(txt)
{
/*
* there's nothing to do with a literal at this point, since
* we're not ranking anything
*/
}
askMissingLiteral(action, which)
{
local ret;
/* if we can't resolve this interactively, fail with an error */
if (!canResolveInteractively(action))
{
/* interactive resolution isn't allowed - fail */
throw new ParseFailureException(&missingLiteral, action, which);
}
/* ask for the missing literal */
targetActor_.getParserMessageObj().
askMissingLiteral(targetActor_, action, which);
/* read the response */
ret = readMainCommand(rmcAskLiteral);
/* re-enable the transcript, if we have one */
if (gTranscript)
gTranscript.activate();
/* return the response */
return ret;
}
emptyNounPhrase(resolver)
{
/* abort with an error */
throw new ParseFailureException(&emptyNounPhrase);
}
zeroQuantity(txt)
{
/* abort with an error */
throw new ParseFailureException(&zeroQuantity,
txt.toLower().htmlify());
}
insufficientQuantity(txt, matchList, requiredNum)
{
/* abort with an error */
throw new ParseFailureException(
&insufficientQuantity, txt.toLower().htmlify(),
matchList, requiredNum);
}
uniqueObjectRequired(txt, matchList)
{
/* abort with an error */
throw new ParseFailureException(
&uniqueObjectRequired, txt.toLower().htmlify(), matchList);
}
singleObjectRequired(txt)
{
/* abort with an error */
throw new ParseFailureException(
&singleObjectRequired, txt.toLower().htmlify());
}
noteAdjEnding()
{
/* we don't care about adjective-ending noun phrases at this point */
}
noteIndefinite()
{
/* we don't care about indefinites at this point */
}
noteMiscWordList(txt)
{
/* we don't care about unstructured noun phrases at this point */
}
notePronoun()
{
/* we don't care about pronouns right now */
}
noteMatches(matchList)
{
/* we don't care about the matches just now */
}
notePlural()
{
/* we don't care about these right now */
}
beginSingleObjSlot() { }
endSingleObjSlot() { }
beginTopicSlot() { }
endTopicSlot() { }
incCommandCount()
{
/* we don't care about how many subcommands there are */
}
noteActorSpecified()
{
/*
* we don't care about this during execution - it only matters
* for determining the strength of the command during the
* ranking process
*/
}
noteNounSlots(cnt)
{
/*
* we don't care about this during execution; it only matters
* for the ranking process
*/
}
noteWeakPhrasing(level)
{
/* ignore this during execution; it only matters during ranking */
}
/* allow remapping the action */
allowActionRemapping = true
/* allow making an arbitrary choice among equivalents */
allowEquivalentFiltering = true
;
/*
* List entry for the still-to-resolve list
*/
class StillToResolveItem: object
construct(lst, fullList, txt)
{
/* remember the equivalent-reduced and full match lists */
matchList = lst;
fullMatchList = fullList;
/* note the text */
origText = txt;
}
/* the reduced (equivalent-eliminated) match list */
matchList = []
/* full (equivalent-inclusive) match list */
fullMatchList = []
/* the original command text being disambiguated */
origText = ''
;
/* ------------------------------------------------------------------------ */
/*
* Specialized noun-phrase resolution results gatherer for resolving a
* command actor (i.e., the target actor of a command).
*/
class ActorResolveResults: BasicResolveResults
construct()
{
/* do the inherited work */
inherited();
/*
* set the initial actor context to the PC - this type of
* resolver is set up to determine the actor context, so we don't
* usually know the actual actor context yet when setting up this
* resolver
*/
targetActor_ = issuingActor_ = gPlayerChar;
}
getImpliedObject(np, resolver)
{
/*
* there's no default for the actor - it's usually simply a
* syntax error when the actor is omitted
*/
throw new ParseFailureException(&missingActor);
}
uniqueObjectRequired(txt, matchList)
{
/* an actor phrase must address a single actor */
throw new ParseFailureException(&singleActorRequired);
}
singleObjectRequired(txt)
{
/* an actor phrase must address a single actor */
throw new ParseFailureException(&singleActorRequired);
}
/* don't allow action remapping while resolving the actor */
allowActionRemapping = nil
;
/*
* A results object for resolving an actor in a command with an unknown
* word or invalid phrasing in the predicate. For this type of
* resolution, we're trying to interpret the actor portion of the command
* as a noun phrase referring to an actor, but it could also just be
* another command. E.g., we could have "bob, asdf" or "east, asdf".
* Since we're only tentatively interpreting the phrase as a noun phrase,
* to see if that interpretation goes anywhere, we don't want to throw
* any errors on failures; instead we simply allow empty match lists.
*/
class TryAsActorResolveResults: ResolveResults
noVocabMatch(action, txt) { }
noMatch(action, txt) { }
noMatchPossessive(action, txt) { }
noMatchForAll() { }
noMatchForAllBut() { }
noMatchForListBut() { }
noteEmptyBut() { }
noMatchForPronoun() { }
allNotAllowed() { }
reflexiveNotAllowed() { }
wrongReflexive(typ, txt) { }
noMatchForPossessive(owner, txt) { }
noMatchForLocation(loc, txt) { }
noteBadPrep() { }
nothingInLocation(loc) { }
ambiguousNounPhrase(keeper, askwer, txt, matchLst, fullMatchLst,
scopeList, requiredNum, resolver) { }
unknownNounPhrase(match, resolver) { }
getImpliedObject(np, resolver) { return []; }
askMissingObject(asker, resolver, responseProd) { return []; }
noteLiteral(txt) { }
askMissingLiteral(action, which) { return nil; }
emptyNounPhrase(resolver) { }
zeroQuantity(txt) { }
insufficientQuantity(txt, matchList, requiredNum) { }
uniqueObjectRequired(txt, matchList) { }
singleObjectRequired(txt) { }
noteAdjEnding() { }
noteIndefinite() { }
noteMatches(matchList) { }
noteMiscWord(txt) { }
notePronoun() { }
notePlural() { }
beginSingleObjSlot() { }
endSingleObjSlot() { }
beginTopicSlot() { }
endTopicSlot() { }
incCommandCount() { }
noteActorSpecified() { }
noteNounSlots(cnt) { }
noteWeakPhrasing() { }
allowActionRemapping = true
allowEquivalentFiltering = true
;
/* ------------------------------------------------------------------------ */
/*
* Command ranking criterion. This is used by the CommandRanking class
* to represent one criterion for comparing two parse trees.
*
* Rankings are performed in two passes. The first pass is the rough,
* qualitative pass, meant to determine if one parse tree has big,
* obvious differences from another. In most cases, this means that one
* tree has a particular type of problem or special advantage that the
* other doesn't have at all.
*
* The second pass is the fine-grained pass. We only reach the second
* pass if we can't find any coarse differences on the first rough pass.
* In most cases, the second pass compares the magnitude of problems or
* advantages to determine if one tree is slightly better than the other.
*/
class CommandRankingCriterion: object
/*
* Compare two CommandRanking objects on the basis of this criterion,
* for the first, coarse-grained pass. Returns a positive number if
* a is better than b, 0 if they're indistinguishable, or -1 if a is
* worse than b.
*/
comparePass1(a, b) { return 0; }
/* compare two rankings for the second, fine-grained pass */
comparePass2(a, b) { return 0; }
;
/*
* A command ranking criterion that measures a "problem" by a count of
* occurrences stored in a property of the CommandRanking object. For
* example, we could count the number of noun phrases that don't resolve
* to any objects.
*
* On the first, coarse-grained pass, we measure only the presence or
* absence of our problem. That is, if one parse tree has zero
* occurrences of the problem and the other has a non-zero number of
* occurrences of the problem (as measured by our counting property),
* then we'll prefer the one with zero occurrences. If both have no
* occurrences, or both have a non-zero number of occurrences, we'll
* consider the two equivalent for the first pass, since we only care
* about the presence or absence of the problem.
*
* On the second, fine-grained pass, we measure the actual number of
* occurrences of the problem, and choose the parse tree with the lower
* number.
*/
class CommandRankingByProblem: CommandRankingCriterion
/*
* our ranking property - this is a property of the CommandRanking
* object that gives us a count of the number of times our "problem"
* has occurred in the ranking object's parse tree
*/
prop_ = nil
/* first pass - compare by presence or absence of the problem */
comparePass1(a, b)
{
local acnt = a.(self.prop_);
local bcnt = b.(self.prop_);
/* if b has the problem but a doesn't, a is better */
if (acnt == 0 && bcnt != 0)
return 1;
/* if a has the problem but b doesn't, b is better */
if (acnt != 0 && bcnt == 0)
return -1;
/* we can't tell the difference at this stage */
return 0;
}
/* second pass - compare by number of occurrences of the problem */
comparePass2(a, b)
{
/*
* Return the difference in the problem counts. We want to
* return >0 if a has fewer problems, <0 if b has fewer problems:
* so compute (a-b) and negate it, which is the same as computing
* (a-b).
*/
return b.(self.prop_) - a.(self.prop_);
}
;
/*
* A "weakness" criterion. This is similar to the rank-by-problem
* criterion, but rather than ranking on an actual structural problem, it
* ranks on a structural weakness. This is suitable for things like
* adjective endings and truncations, where the weakness isn't on the
* same order as a "problem" but where we'd still rather avoid the
* weakness if we can.
*
* The point of the separate "weakness" criterion is that we only allow
* weaknesses to come into play on pass 2, after we've already
* discriminated based on problems. If we can discriminate based on
* problems, we'll do so in pass 1 and won't even get to pass 2; we'll
* only discriminate based on weakness if we can't tell the difference
* based on real problems.
*/
class CommandRankingByWeakness: CommandRankingCriterion
/* on pass 1, ignore weaknesses */
comparePass1(a, b) { return 0; }
/* on pass 2, compare based on weaknesses */
comparePass2(a, b) { return b.(self.prop_) - a.(self.prop_); }
/* our command-ranking property */
prop_ = nil
;
/*
* command-ranking-by-problem and by-weakness objects for the pre-defined
* ranking criteria
*/
rankByVocabNonMatch: CommandRankingByProblem prop_ = &vocabNonMatchCount;
rankByNonMatch: CommandRankingByProblem prop_ = &nonMatchCount;
rankByInsufficient: CommandRankingByProblem prop_ = &insufficientCount;
rankByListForSingle: CommandRankingByProblem prop_ = &listForSingle;
rankByEmptyBut: CommandRankingByProblem prop_ = &emptyButCount;
rankByAllExcluded: CommandRankingByProblem prop_ = &allExcludedCount;
rankByActorSpecified: CommandRankingByProblem prop_ = &actorSpecifiedCount;
rankByMiscWordList: CommandRankingByProblem prop_ = &miscWordListCount;
rankByPluralTrunc: CommandRankingByWeakness prop_ = &pluralTruncCount;
rankByEndAdj: CommandRankingByWeakness prop_ = &endAdjCount;
rankByIndefinite: CommandRankingByProblem prop_ = &indefiniteCount;
rankByTrunc: CommandRankingByWeakness prop_ = &truncCount;
rankByMissing: CommandRankingByProblem prop_ = &missingCount;
rankByPronoun: CommandRankingByWeakness prop_ = &pronounCount;
rankByWeakness: CommandRankingByWeakness prop_ = &weaknessLevel;
rankByUnwantedPlural: CommandRankingByProblem prop_ = &unwantedPluralCount;
/*
* Rank by unmatched possessive-qualified phrases. If we have two
* unknown phrases, one with a possessive qualifier and one without, and
* other things being equal, prefer the one with the possessive
* qualifier.
*
* We prefer the qualified version because it lets us report a smaller
* phrase that we can't match. For example, in X BOB'S WALLET, if we
* can't match WALLET all by itself, it's more useful to report that "you
* see no wallet" than to report that you see no "bob's wallet", because
* the latter incorrectly implies that there might still be a wallet in
* scope as long as it's not Bob's we're looking for.
*/
rankByNonMatchPoss: CommandRankingCriterion
/*
* ignore on pass 1 - this only counts if other factors are equal, so
* we want to consider all of the other factors on pass 1 before
* taking this criterion into account
*/
comparePass1(a, b) { return 0; }
/* pass 2 - more possessives are better */
comparePass2(a, b)
{
/*
* if we don't have the same underlying non-match count,
* possessive qualification is irrelevant
*/
if (a.nonMatchCount != b.nonMatchCount)
return 0;
/* more possessives are better */
return a.nonMatchPossCount - b.nonMatchPossCount;
}
;
/*
* Command ranking by literal phrase length. We prefer interpretations
* that treat less text as uninterpreted literal text. By "less text,"
* we simply mean that one has a shorter string treated as literal text
* than the other. (We prefer shorter literals because when the parser
* matches a string of literal text, it's essentially throwing up its
* hands and admitting it can't parse the text; so the less text is
* contained in literals, the more text the parser is actually parsing,
* and more parsed is better.)
*/
rankByLiteralLength: CommandRankingCriterion
/* first pass */
comparePass1(a, b)
{
/*
* Compare our lengths. We want to return >0 if a is shorter and
* <0 if a is longer (and, of course, 0 if they're the same
* length). So, we can just compute (a-b) and negate the result,
* which is the same as computing (b-a).
*
* The CommandRanking objects keep track of the length of text in
* literals in their literalLength properties.
*/
return b.literalLength - a.literalLength;
}
/*
* Second pass - we use our full powers of discrimination on the
* first pass, so if we make it to the second pass, we couldn't tell
* a difference on the first pass and thus can't tell a difference
* now. So, just inherit the default implementation, which simply
* returns 0 to indicate that there's no difference.
*/
;
/*
* Command ranking by subcommand count: we prefer the match with fewer
* subcommands. If one has fewer subcommands than the other, it means
* that we were able to interpret ambiguous conjunctions (such as "and")
* as noun phrase conjunctions rather than as command conjunctions; other
* things being equal, we'd rather take the interpretation that gives us
* noun phrases than the one that involves more separate commands.
*/
rankBySubcommands: CommandRankingCriterion
/* first pass - compare subcommand counts */
comparePass1(a, b)
{
/*
* if a has fewer subcommands, return <0, and if b has fewer
* subcommands, return >0: so we can just return the negative of
* (a-b), or (b-a)
*/
return b.commandCount - a.commandCount;
}
/* second pass - do nothing, as we do all of our work on the first pass */
;
/*
* Rank by token count. Other things being equal, we'd rather pick a
* longer match. If one match is shorter than the other in terms of the
* number of tokens it encompasses, then it means that the shorter match
* left more tokens at the end of the command to be interpreted as
* separate commands. If we have an interpretation that can take more of
* those tokens and parse them as part of the current command, that
* interpretation is probably better.
*/
rankByTokenCount: CommandRankingCriterion
/* first pass - compare token counts */
comparePass1(a, b)
{
/* choose the one that matched more tokens */
return a.tokCount - b.tokCount;
}
/* first pass - we do all our work on the first pass */
;
/*
* Rank by "verb structure." This gives more weight to an
* interpretation that has more structural noun phrases in the verb.
* For example, "DETACH dobj FROM iobj" is given more weight than
* "DETACH dobj", because the former has two structural noun phrases
* whereas the latter has only one. This will make us prefer to treat
* DETACH WIRE FROM BOX as a two-object action, for example, even if we
* could treat WIRE FROM BOX as a single "locational" noun phrase.
*/
rankByVerbStructure: CommandRankingCriterion
comparePass2(a, b)
{
/* take the one with more structural noun slots in the verb phrase */
return a.nounSlotCount - b.nounSlotCount;
}
;
/*
* Rank by ambiguous noun phrases. We apply this criterion on the second
* pass only, because it's a weak test: we might end up narrowing things
* down through automatic "logicalness" tests during the noun resolution
* process, so ambiguity at this stage in the parsing process doesn't
* necessarily indicate that there's real ambiguity in the command.
* However, if we can already tell that one interpretation is unambiguous
* and another is ambiguous, and the two interpretations are otherwise
* equally good, pick the one that's already unambiguous: the ambiguous
* interpretation might or might not stay ambiguous, but the unambiguous
* interpretation will definitely stay unambiguous.
*/
rankByAmbiguity: CommandRankingCriterion
/*
* Do nothing on the first pass, because we want any first-pass
* criterion to prevail over our weak test. Instead, check for a
* difference in ambiguity only on the second pass.
*/
comparePass2(a, b)
{
/* the one with lower ambiguity is better */
return b.ambigCount - a.ambigCount;
}
;
/*
* Production match ranking object. We create one of these objects for
* each match tree that we wish to rank.
*
* This class is generally not instantiated by client code - instead,
* clients use the sortByRanking() class method to rank a list of
* production matches.
*/
class CommandRanking: ResolveResults
/*
* Sort a list of productions, as returned from
* GrammarProd.parseTokens(), in descending order of command
* strength. We return a list of CommandRanking objects whose first
* element is the best command interpretation.
*
* Note that this can be used as a class-level method.
*/
sortByRanking(lst, [resolveArguments])
{
/*
* create a vector to hold the ranking information - we
* need one ranking item per match
*/
local rankings = new Vector(lst.length());
/* get the ranking information for each command */
foreach(local cur in lst)
{
local curRank;
/* create a ranking item for the entry */
curRank = self.createInstance(cur);
/* rank this entry */
curRank.calcRanking(resolveArguments);
/* add this to our ranking list */
rankings.append(curRank);
}
/* sort the entries by descending ranking, and return the results */
return rankings.sort(SortDesc, {x, y: x.compareRanking(y)});
}
/* create a new entry */
construct(match)
{
/* remember the match object */
self.match = match;
/* remember the number of tokens in the match */
tokCount = match.lastTokenIndex - match.firstTokenIndex + 1;
}
/* calculate my ranking */
calcRanking(resolveArguments)
{
/*
* Ask the match tree to resolve nouns, using this ranking
* object as the resolution results receiver - when an error or
* warning occurs during resolution, we'll merely note the
* condition rather than say anything about it.
*
* Note that 'self' is the results object, because the point of
* this resolution pass is to gather statistics into this
* results object.
*/
match.resolveNouns(resolveArguments..., self);
}
/*
* Compare two production list entries for ranking purposes. Returns
* a negative number if this one ranks worse than the other, 0 if
* they have the same ranking, or a positive number if this one ranks
* better than the other one.
*
* This routine is designed to run entirely off of our
* rankingCriteria property. In most cases, subclasses should be
* able to customize the ranking system simply by overriding the
* rankingCriteria property to provide a customized list of criteria
* objects.
*/
compareRanking(other)
{
local ret;
/*
* Run through our ranking criteria and apply the first pass to
* each one. Return the indication of the first criterion that
* can tell a difference.
*/
foreach (local cur in rankingCriteria)
{
/* if the rankings differ in this criterion, return the result */
if ((ret = cur.comparePass1(self, other)) != 0)
return ret;
}
/*
* We couldn't tell any difference on the first pass, so try
* again with the finer-grained second pass.
*/
foreach (local cur in rankingCriteria)
{
/* run the second pass */
if ((ret = cur.comparePass2(self, other)) != 0)
return ret;
}
/* we couldn't tell any difference between the two */
return 0;
}
/*
* Our list of ranking criteria. This is a list of
* CommandRankingCriterion objects. The list is given in order of
* importance: the first criterion is the most important, so if it
* can discriminate the two match trees, we use its result; if the
* first criterion can't tell any difference, then we move on to the
* second criterion; and so on through the list.
*
* The most important thing is whether or not we have irresolvable
* noun phrases (vocabNonMatchCount). If one of us has a noun phrase
* that refers to nothing anywhere in the game, it's not as good as a
* phrase that at least matches something somewhere.
*
* Next, if one of us has noun phrases that cannot be resolved to
* something in scope (nonMatchCount), and the other can successfully
* resolve its noun phrases, the one that can resolve the phrases is
* preferred.
*
* Next, check for insufficient numbers of matches to counted phrases
* (insufficientCount).
*
* Next, check for noun lists in single-noun-only slots
* (listForSingle).
*
* Next, if we have an empty "but" list in one but not the other,
* take the one with the non-empty "but" list (emptyButCount). We
* prefer a non-empty "but" list with an empty "all" even to a
* non-empty "all" list with an empty "but", because in the latter
* case we probably failed to exclude anything because we
* misinterpreted the noun phrase to be excluded.
*
* Next, if we have an empty "all" or "any" phrase due to "but"
* exclusion, take the one that's not empty (allExcludedCount).
*
* Next, prefer a command that addresses an actor
* (actorSpecifiedCount) - if the actor name looks like a command (we
* have someone named "Open Bob," maybe?), we'd prefer to interpret
* the name appearing as a command prefix as an actor name.
*
* Next, prefer no unstructured word lists as noun phrases
* (miscWordList phrases) (miscWordListCount).
*
* Next, prefer interpretations that treat less text as uninterpreted
* literal text. By "less text," we simply mean that one has a
* shorter string treated as a literal than the other.
*
* Prefer no indefinite noun phrases (indefiniteCount).
*
* Prefer no truncated plurals (pluralTruncCount).
*
* Prefer no noun phrases ending in adjectives (endAdjCount).
*
* Prefer no truncated words of any kind (truncCount).
*
* Prefer fewer pronouns. If we have an interpretation that matches
* a word to explicit vocabulary, take it over matching a word as a
* pronoun: if a word is given explicitly as vocabulary for an
* object, use it if possible.
*
* Prefer no missing phrases (missingCount).
*
* Prefer the one with fewer subcommands - if one has fewer
* subcommands than the other, it means that we were able to
* interpret ambiguous conjunctions (such as "and") as noun phrase
* conjunctions rather than as command conjunctions; since we know by
* now that we both either have or don't have unresolved noun
* phrases, we'd rather take the interpretation that gives us noun
* phrases than the one that involves more separate commands.
*
* Prefer the tree that matches more tokens.
*
* Prefer the one with more structural noun phrases in the verb. For
* example, if we have one interpretation that's DETACH (X FROM Y)
* (where X FROM Y is a 'locational' phrase that we treat as the
* direct object), and one that's DETACH X FROM Y (where X is the
* direct object and Y is in the indirect object), prefer the latter,
* because it has both direct and indirect object phrases, whereas
* the former has only a direct object phrase. English speakers
* almost always try to put prepositions into a structural role in
* the verb phrase like this when they could be either in the verb
* phrase or part of a noun phrase.
*
* If all else fails, prefer the one that is initially less
* ambiguous. Ambiguity is a weak test at this point, since we might
* end up narrowing things down through automatic "logicalness" tests
* later, but it's slightly better to have the match be less
* ambiguous now, all other things being equal.
*/
rankingCriteria = [rankByVocabNonMatch,
rankByNonMatch,
rankByNonMatchPoss,
rankByInsufficient,
rankByListForSingle,
rankByEmptyBut,
rankByAllExcluded,
rankByActorSpecified,
rankByUnwantedPlural,
rankByMiscWordList,
rankByWeakness,
rankByLiteralLength,
rankByIndefinite,
rankByPluralTrunc,
rankByEndAdj,
rankByTrunc,
rankByPronoun,
rankByMissing,
rankBySubcommands,
rankByTokenCount,
rankByVerbStructure,
rankByAmbiguity]
/* the match tree I'm ranking */
match = nil
/* the number of tokens my match tree consumes */
tokCount = 0
/*
* Ranking information. calcRanking() fills in these members, and
* compareRanking() uses these to calculate the relative ranking.
*/
/*
* The number of structural "noun phrase slots" in the verb. An
* intransitive verb has no noun phrase slots; a transitive verb
* with a direct object has one; a verb with a direct and indirect
* object has two slots.
*/
nounSlotCount = nil
/* number of noun phrases matching nothing anywhere in the game */
vocabNonMatchCount = 0
/* number of noun phrases matching nothing in scope */
nonMatchCount = 0
/*
* Number of possessive-qualified noun phrases matching nothing in
* scope. For example, "bob's desk" when there's no desk in scope
* (Bob's or otherwise).
*/
nonMatchPossCount = 0
/* number of phrases requiring quantity higher than can be fulfilled */
insufficientCount = 0
/* number of noun lists in single-noun slots */
listForSingle = 0
/* number of empty "but" lists */
emptyButCount = 0
/* number of "all" or "any" lists totally excluded by "but" */
allExcludedCount = 0
/* missing phrases (structurally omitted, as in "put book") */
missingCount = 0
/* number of truncated plurals */
pluralTruncCount = 0
/* number of phrases ending in adjectives */
endAdjCount = 0
/* number of phrases with indefinite noun phrase structure */
indefiniteCount = 0
/* number of miscellaneous word lists as noun phrases */
miscWordListCount = 0
/* number of truncated words overall */
truncCount = 0
/* number of ambiguous noun phrases */
ambigCount = 0
/* number of subcommands in the command */
commandCount = 0
/* an actor is specified */
actorSpecifiedCount = 0
/* unknown words */
unknownWordCount = 0
/* total character length of literal text phrases */
literalLength = 0
/* number of pronoun phrases */
pronounCount = 0
/* weakness level (for noteWeakPhrasing) */
weaknessLevel = 0
/* number of plural phrases encountered in single-object slots */
unwantedPluralCount = 0
/* -------------------------------------------------------------------- */
/*
* ResolveResults implementation. We use this results receiver when
* we're comparing the semantic strengths of multiple structural
* matches, so we merely note each error condition without showing
* any message to the user or asking the user for any input. Once
* we've ranked all of the matches, we'll choose the one with the
* best attributes and then resolve it for real, at which point if
* we chose one with any errors, we'll finally get around to showing
* the errors to the user.
*/
noVocabMatch(action, txt)
{
/* note the unknown phrase */
++vocabNonMatchCount;
}
noMatch(action, txt)
{
/* note that we have a noun phrase that matches nothing */
++nonMatchCount;
}
noMatchPossessive(action, txt)
{
/* note that we have an unmatched possessive-qualified noun phrase */
++nonMatchCount;
++nonMatchPossCount;
}
allNotAllowed()
{
/* treat this as a non-matching noun phrase */
++nonMatchCount;
}
noMatchForAll()
{
/* treat this as any other noun phrase that matches nothing */
++nonMatchCount;
}
noteEmptyBut()
{
/* note it */
++emptyButCount;
}
noMatchForAllBut()
{
/* count the total exclusion */
++allExcludedCount;
}
noMatchForListBut()
{
/* treat this as any other noun phrase that matches nothing */
++allExcludedCount;
}
noMatchForPronoun(typ, txt)
{
/* treat this as any other noun phrase that matches nothing */
++nonMatchCount;
}
reflexiveNotAllowed(typ, txt)
{
/* treat this as any other noun phrase that matches nothing */
++nonMatchCount;
}
wrongReflexive(typ, txt)
{
/* treat this as any other noun phrase that matches nothing */
++nonMatchCount;
}
noMatchForPossessive(owner, txt)
{
/* treat this as any other noun phrase that matches nothing */
++nonMatchCount;
}
noMatchForLocation(loc, txt)
{
/* treat this as any other noun phrase that matches nothing */
++nonMatchCount;
}
noteBadPrep()
{
/* don't do anything at this point */
}
nothingInLocation(txt)
{
/* treat this as any other noun phrase that matches nothing */
++nonMatchCount;
}
ambiguousNounPhrase(keeper, asker, txt,
matchList, fullMatchList, scopeList,
requiredNum, resolver)
{
local lst;
/* note the ambiguity */
++ambigCount;
/*
* There's no need to disambiguate the list at this stage, since
* we're only testing the strength of the structure.
*
* As a tentative approximation of the results, return a list
* consisting of the required number only, but stash away the
* remainder of the full list as a property of the first element
* of the return list so we can find the full list again later.
*/
lst = matchList.sublist(1, requiredNum);
if (matchList.length() > requiredNum && lst.length() >= 1)
lst[1].extraObjects = matchList.sublist(requiredNum + 1);
/* return the abbreviated list */
return lst;
}
unknownNounPhrase(match, resolver)
{
local wordList;
local ret;
/*
* if the resolver can handle this set of unknown words, treat
* it as a good noun phrase; otherwise, treat it as an unmatched
* noun phrase
*/
wordList = match.getOrigTokenList();
if ((ret = resolver.resolveUnknownNounPhrase(wordList)) == nil)
{
/* count the unmatchable phrase */
++nonMatchCount;
/* count the unknown word */
++unknownWordCount;
/*
* since this is only a ranking pass, resolve to an empty
* list for now
*/
ret = [];
}
/* return the results */
return ret;
}
getImpliedObject(np, resolver)
{
/* count the missing object phrase */
++missingCount;
return nil;
}
askMissingObject(asker, resolver, responseProd)
{
/*
* no need to do anything here - we'll count the missing object
* in getImpliedObject, and we don't want to ask for anything
* interactively at this point
*/
return nil;
}
noteLiteral(txt)
{
/* add the length of this literal to the total literal length */
literalLength += txt.length();
}
emptyNounPhrase(resolver)
{
/* treat this as a non-matching noun phrase */
++nonMatchCount;
return [];
}
zeroQuantity(txt)
{
/* treat this as a non-matching noun phrase */
++nonMatchCount;
}
insufficientQuantity(txt, matchList, requiredNum)
{
/* treat this as a non-matching noun phrase */
++insufficientCount;
}
singleObjectRequired(txt)
{
/* treat this as a non-matching noun phrase */
++listForSingle;
}
uniqueObjectRequired(txt, matchList)
{
/*
* ignore this for now - we might get a unique object via
* disambiguation during the execution phase
*/
}
noteAdjEnding()
{
/* count it */
++endAdjCount;
}
noteIndefinite()
{
/* count it */
++indefiniteCount;
}
noteMiscWordList(txt)
{
/* note the presence of an unstructured noun phrase */
++miscWordListCount;
/* count this as a literal as well */
noteLiteral(txt);
}
notePronoun()
{
/* note the presence of a pronoun */
++pronounCount;
}
noteMatches(matchList)
{
/*
* Run through the match list and note each weak flag. Note
* that each element of the match list is a ResolveInfo
* instance.
*/
foreach (local cur in matchList)
{
/* if this object was matched with a truncated word, note it */
if ((cur.flags_ & VocabTruncated) != 0)
++truncCount;
/* if this object was matched with a truncated plural, note it */
if ((cur.flags_ & PluralTruncated) != 0)
++pluralTruncCount;
}
}
beginSingleObjSlot() { ++inSingleObjSlot; }
endSingleObjSlot() { --inSingleObjSlot; }
inSingleObjSlot = 0
beginTopicSlot() { ++inTopicSlot; }
endTopicSlot() { --inTopicSlot; }
inTopicSlot = 0
notePlural()
{
/*
* If we're resolving a single-object slot, we want to avoid
* plurals, since they could resolve to multiple objects as
* though we'd typed a list of objects here. This isn't a
* problem for topics, though, since a topic slot isn't iterated
* for execution.
*/
if (inSingleObjSlot && !inTopicSlot)
++unwantedPluralCount;
}
incCommandCount()
{
/* increase our subcommand counter */
++commandCount;
}
noteActorSpecified()
{
/* note it */
++actorSpecifiedCount;
}
noteNounSlots(cnt)
{
/*
* If this is the first noun slot count we've received, remember
* it. If we already have a count, ignore the new one - we only
* want to consider the first verb phrase if there are multiple
* verb phrases, since we'll reconsider the next verb phrase when
* we're ready to execute it.
*/
if (nounSlotCount == nil)
nounSlotCount = cnt;
}
noteWeakPhrasing(level)
{
/* note the weak phrasing level */
weaknessLevel = level;
}
/* don't allow action remapping while ranking */
allowActionRemapping = nil
;
/*
* Another preliminary results gatherer that does everything the way the
* CommandRanking results object does, except that we perform
* interactive resolution of unknown words via OOPS.
*/
class OopsResults: CommandRanking
construct(issuingActor, targetActor)
{
/* remember the actors */
issuingActor_ = issuingActor;
targetActor_ = targetActor;
}
/*
* handle a phrase with unknown words
*/
unknownNounPhrase(match, resolver)
{
/*
* if the resolver can handle this set of unknown words, treat
* it as a good noun phrase
*/
local wordList = match.getOrigTokenList();
local ret = resolver.resolveUnknownNounPhrase(wordList);
if (ret != nil)
return ret;
/*
* we still can't resolve it; try prompting for a correction of
* any misspelled words in the phrase
*/
tryOops(wordList, issuingActor_, targetActor_,
match.firstTokenIndex, match.tokenList, rmcCommand);
/*
* if we got this far, we still haven't resolved it; resolve to
* an empty phrase
*/
return [];
}
/* the command's issuing actor */
issuingActor_ = nil
/* the command's target actor */
targetActor_ = nil
;
/* ------------------------------------------------------------------------ */
/*
* Exception list resolver. We use this type of resolution for noun
* phrases in the "but" list of an "all but" construct.
*
* We scope the "all but" list to the objects in the "all" list, since
* there's no point in excluding objects that aren't in the "all" list.
* In addition, if a phrase in the exclusion list matches more than one
* object in the "all" list, we consider it a match to all of those
* objects, even if it's a definite phrase - this means that items in
* the "but" list are never ambiguous.
*/
class ExceptResolver: ProxyResolver
construct(mainList, mainListText, resolver)
{
/* invoke the base class constructor */
inherited(resolver);
/* remember the main list, from which we're excluding items */
self.mainList = mainList;
self.mainListText = mainListText;
}
/* we're a sub-phrase resolver */
isSubResolver = true
/*
* match an object's name - we'll use the disambiguation name
* resolver, so that they can give us partial names just like in
* answer to a disambiguation question
*/
matchName(obj, origTokens, adjustedTokens)
{
return obj.matchNameDisambig(origTokens, adjustedTokens);
}
/*
* Resolve qualifiers in the enclosing main scope, since qualifier
* phrases are not part of the narrowed list - qualifiers apply to
* the main phrase from which we're excluding, not to the exclusion
* list itself.
*/
getQualifierResolver() { return origResolver; }
/*
* determine if an object is in scope - it's in scope if it's in the
* original main list
*/
objInScope(obj)
{
return mainList.indexWhich({x: x.obj_ == obj}) != nil;
}
/* for 'all', simply return the whole original list */
getAll(np)
{
return mainList;
}
/* filter ambiguous equivalents */
filterAmbiguousEquivalents(lst, np)
{
/*
* keep all of the equivalent items in an exception list,
* because we want to exclude all of the equivalent items from
* the main list
*/
return lst;
}
/* filter an ambiguous noun list */
filterAmbiguousNounPhrase(lst, requiredNum, np)
{
/*
* noun phrases in an exception list are never ambiguous,
* because they implicitly refer to everything they match -
* simply return the full matching list
*/
return lst;
}
/* filter a plural noun list */
filterPluralPhrase(lst, np)
{
/* return all of the original plural matches */
return lst;
}
/* the main list from which we're excluding things */
mainList = nil
/* the original text for the main list */
mainListText = ''
/* the original underlying resolver */
origResolver = nil
;
/*
* Except list results object
*/
class ExceptResults: object
construct(results)
{
/* remember the original results object */
origResults = results;
}
/*
* ignore failed matches in the exception list - if they try to
* exclude something that's not in the original list, the object is
* excluded to begin with
*/
noMatch(action, txt) { }
noMatchPoss(action, txt) { }
noVocabMatch(action, txt) { }
/* ignore failed matches for possessives in the exception list */
noMatchForPossessive(owner, txt) { }
/* ignore failed matches for location in the exception list */
noMatchForLocation(loc, txt) { }
/* ignore failed matches for location in the exception list */
nothingInLocation(loc) { }
/*
* in case of ambiguity, simply keep everything and treat it as
* unambiguous - if they say "take coin except copper", we simply
* want to treat "copper" as unambiguously excluding every copper
* coin in the original list
*/
ambiguousNounPhrase(keeper, asker, txt,
matchList, fullMatchList, scopeList,
requiredNum, resolver)
{
/* return the full match list - exclude everything that matches */
return fullMatchList;
}
/* proxy anything we don't override to the underlying results object */
propNotDefined(prop, [args])
{
return origResults.(prop)(args...);
}
/* my original underlying results object */
origResults = nil
;
/* ------------------------------------------------------------------------ */
/*
* Base class for parser exceptions
*/
class ParserException: Exception
;
/*
* Terminate Command exception - when the parser encounters an error
* that makes it impossible to go any further processing a command, we
* throw this error to abandon the current command and proceed to the
* next. This indicates a syntax error or semantic resolution error
* that renders the command meaningless or makes it impossible to
* proceed.
*
* When this exception is thrown, all processing of the current command
* termintes immediately. No further action processing is performed; we
* don't continue iterating the command on any additional objects for a
* multi-object command; and we discard any remaining commands on the
* same command line.
*/
class TerminateCommandException: ParserException
;
/*
* Cancel Command Line exception. This is used to cancel any *remaining*
* commands on a command line after finishing execution of one command on
* the line. For example, if the player types "TAKE BOX AND GO NORTH",
* the handler for TAKE BOX can throw this exception to cancel everything
* later on the command line (in this case, the GO NORTH part).
*
* This is handled almost identically to TerminateCommandException. The
* only difference is that some games might want to alert the player with
* an explanation that extra commands are being ignored.
*/
class CancelCommandLineException: TerminateCommandException
;
/*
* Parsing failure exception. This exception is parameterized with
* message information describing the failure, and can be used to route
* the failure notification to the issuing actor.
*/
class ParseFailureException: ParserException
construct(messageProp, [args])
{
/* remember the message property and the parameters */
message_ = messageProp;
args_ = args;
}
/* notify the issuing actor of the problem */
notifyActor(targetActor, issuingActor)
{
/*
* Tell the target actor to notify the issuing actor. We route
* the notification from the target to the issuer in keeping
* with conversation we're modelling: the issuer asked the
* target to do something, so the target is now replying with
* information explaining why the target can't do as asked.
*/
targetActor.notifyParseFailure(issuingActor, message_, args_);
}
displayException() { "Parse failure exception"; }
/* the message property ID */
message_ = nil
/* the (varargs) parameters to the message */
args_ = nil
;
/*
* Exception: Retry a command with new tokens. In some cases, the
* parser processes a command by replacing the command with a new one
* and processing the new one instead of the original. When this
* happens, the parser will throw this exception, filling in newTokens_
* with the replacement token list.
*
* Note that this is meant to replace the current command only - this
* exception effectively *edits* the current command. Any pending
* tokens after the current command should be retained when this
* exception is thrown.
*/
class RetryCommandTokensException: ParserException
construct(lst)
{
/* remember the new token list */
newTokens_ = lst;
}
/*
* The replacement token list. Note that this is in the same format
* as the token list returned from the tokenizer, so this is a list
* consisting of two sublists - one for the token strings, the other
* for the corresponding token types.
*/
newTokens_ = []
;
/*
* Replacement command string exception. Abort any current command
* line, and start over with a brand new input string. Note that any
* pending, unparsed tokens on the previous command line should be
* discarded.
*/
class ReplacementCommandStringException: ParserException
construct(str, issuer, target)
{
/* remember the new command string */
newCommand_ = str;
/*
* note the issuing actor; if the caller specified this as nil,
* use the current player character as the default
*/
issuingActor_ = (issuer == nil ? libGlobal.playerChar : issuer);
/* note the default target actor, defaulting to the player */
targetActor_ = (target == nil ? libGlobal.playerChar : target);
}
/* the new command string */
newCommand_ = ''
/* the actor issuing the command */
issuingActor_ = nil
/* the default target actor of the command */
targetActor_ = nil
;
/* ------------------------------------------------------------------------ */
/*
* Parser debugging helpers
*/
#ifdef PARSER_DEBUG
/*
* Show a list of match trees
*/
showGrammarList(matchList)
{
/* show the list only if we're in debug mode */
if (libGlobal.parserDebugMode)
{
/* show each match tree in the list */
foreach (local cur in matchList)
{
"\n----------\n";
showGrammar(cur, 0);
}
}
}
/*
* Show a winning match tree
*/
showGrammarWithCaption(headline, match)
{
/* show the list only if we're in debug mode */
if (libGlobal.parserDebugMode)
{
"\n----- <<headline>> -----\n";
showGrammar(match, 0);
}
}
/*
* Show a grammar tree
*/
showGrammar(prod, indent)
{
local info;
/* if we're not in parser debug mode, do nothing */
if (!libGlobal.parserDebugMode)
return;
/* indent to the requested level */
for (local i = 0 ; i < indent ; ++i)
"\ \ ";
/* check for non-production objects */
if (prod == nil)
{
/* this tree element isn't used - skip it */
return;
}
else if (dataType(prod) == TypeSString)
{
/* show the item literally, and we're done */
"'<<prod>>'\n";
return;
}
/* get the information for this item */
info = prod.grammarInfo();
/* if it's nil, there's nothing more to do */
if (info == nil)
{
"<no information>\n";
return;
}
/* show the name */
"<<info[1]>> [<<prod.getOrigText()>>]\n";
/* show the subproductions */
for (local i = 2 ; i <= info.length ; ++i)
showGrammar(info[i], indent + 1);
}
#endif /* PARSER_DEBUG */
TADS 3 Library Manual
Generated on 5/16/2013 from TADS version 3.1.3