I don’t know how to contribute things to if-archive, but since I tend to pack features like this into little libraries anyway, here’s what I just put together.
Since these are written as a library, each of these blocks of code should be saved as the named file in a directory by itself:
lastTyped.h
//
// lastTyped.h
//
// Uncomment to disable noun canonicalization
//#define NO_LAST_TYPED_NOUN
// Uncomment to track noun usage
// If enabled, this just counts how often noun canonicalization resolves
// to each canonical form.
#define LAST_TYPED_NOUN_STATS
// Uncomment to disable verb canonicalization
//#define NO_LAST_TYPED_VERB
// Uncomment the #define lines below to enable debugging output
#ifdef __DEBUG
//#define __DEBUG_LAST_TYPED_VERB
//#define __DEBUG_LAST_TYPED_NOUN
#endif // __DEBUG
#define gVerb (lastTypedVerb)
lastTyped.tl
name: Last Typed Library
source: lastTypedNoun
source: lastTypedVerb
lastTypedNoun.t
#charset "us-ascii"
//
// lastTypedNoun.t
//
#include <adv3.h>
#include <en_us.h>
#include "lastTyped.h"
modify MessageBuilder
execBeforeMe = [ lastTypedMessageBuilder ]
;
lastTypedMessageBuilder: PreinitObject
execute() {
langMessageBuilder.paramList_
= langMessageBuilder.paramList_.append(
[ 'last/he', &lastTypedNoun, nil, nil, nil ]);
langMessageBuilder.paramList_
= langMessageBuilder.paramList_.append(
[ 'last/she', &lastTypedNoun, nil, nil, nil ]);
langMessageBuilder.paramList_
= langMessageBuilder.paramList_.append(
[ 'last/it', &lastTypedNoun, nil, nil, nil ]);
langMessageBuilder.paramList_
= langMessageBuilder.paramList_.append(
[ 'last/him', &lastTypedNoun, nil, nil, nil ]);
langMessageBuilder.paramList_
= langMessageBuilder.paramList_.append(
[ 'last/her', &lastTypedNoun, nil, nil, nil ]);
langMessageBuilder.paramList_
= langMessageBuilder.paramList_.append(
[ 'most/he', &mostTypedNoun, nil, nil, nil ]);
langMessageBuilder.paramList_
= langMessageBuilder.paramList_.append(
[ 'most/she', &mostTypedNoun, nil, nil, nil ]);
langMessageBuilder.paramList_
= langMessageBuilder.paramList_.append(
[ 'most/it', &mostTypedNoun, nil, nil, nil ]);
langMessageBuilder.paramList_
= langMessageBuilder.paramList_.append(
[ 'most/him', &mostTypedNoun, nil, nil, nil ]);
langMessageBuilder.paramList_
= langMessageBuilder.paramList_.append(
[ 'most/her', &mostTypedNoun, nil, nil, nil ]);
}
;
#ifndef NO_LAST_TYPED_NOUN
modify Thing
_lastTypedNoun = nil
_lastTypedNounStats = nil
canonicalNoun = nil
// Misfeature? If we have a resolved typed noun we use it, otherwise
// we return the plain ol' name. Done so we can use lastTypedName()
// without having to do any conditional nonsense, but maybe we
// shouldn't.
lastTypedNoun = (_lastTypedNoun ? _lastTypedNoun : name)
// If we're collecting stats, return the canonical noun most
// frequently used to refer to this object.
// If we fail for any reason, we fall back on returning
// lastTypedNoun instead.
mostTypedNoun() {
#ifndef LAST_TYPED_NOUN_STATS
return(lastTypedNoun);
#else // LAST_TYPED_NOUN_STATS
local max, r;
if(!_lastTypedNounStats) return(lastTypedNoun);
max = 0;
_lastTypedNounStats.forEachAssoc(function(k, v) {
if(v > max) {
max = v;
r = k;
}
});
if(!r) return(lastTypedNoun);
return(r);
#endif // LAST_TYPED_NOUN_STATS
}
_countCanonicalNoun(v) {
#ifndef LAST_TYPED_NOUN_STATS
return;
#else // LAST_TYPED_NOUN_STATS
local l;
if(!canonicalNoun || !canonicalNoun.ofKind(List))
return;
l = _lastTypedNounStats;
if(!l) {
_lastTypedNounStats = new LookupTable();
l = _lastTypedNounStats;
}
canonicalNoun.forEach(function(o) {
if(!l[o]) l[o] = 0;
if(o == v) l[o] += 1;
});
#endif // LAST_TYPED_NOUN_STATS
}
// Attempt to determine which, if any, bits of the passed list
// represent a canonical noun referring to this object.
_getCanonicalNoun(v) {
local i, j;
if(!canonicalNoun || !canonicalNoun.ofKind(List)) return(nil);
for(j = 1; j <= v.length(); j++) {
for(i = 1; i <= canonicalNoun.length(); i++) {
if(canonicalNoun[i].find(v[j]) != nil)
return(canonicalNoun[i]);
}
}
return(nil);
}
// Used by matchName() and matchNameDisambig(), this is called
// pretty much any time the object is referenced.
matchNameCommon(origTokens, adjustedTokens) {
local l;
// If we don't have any canonical noun forms, we
// have nothing to do, so we immediately bail.
if(!canonicalNoun)
return(inherited(origTokens, adjustedTokens));
// Get a list of everything in the token list that's
// a string.
l = [];
adjustedTokens.forEach(function(o) {
if(dataTypeXlat(o) == TypeSString)
l += o;
});
_lastTypedNoun = _getCanonicalNoun(l);
_countCanonicalNoun(_lastTypedNoun);
return(inherited(origTokens, adjustedTokens));
}
;
#else // NO_LAST_TYPED_NOUN
modify Thing
canonicalNoun = nil
lastTypedNoun = name
;
#endif // NO_LAST_TYPED_NOUN
lastTypedVerb.t
#charset "us-ascii"
//
// lastTypedVerb.t
//
// Save the verb typed by the player.
// For non-abbreviated verbs this should be the full verb/verb phrase
// typed by the player. I.e., if the player types:
//
// > SMELL ALL
//
// ...this will result in gVerb.get() returning "smell".
//
// When an abbreviation is used, we attempt to figure out the canonical
// form of the abbreviated verb. I.e.:
//
// > X ME
//
// ...will get "examine", and...
//
// > L AT ME
//
// ...will get "look at".
//
#include <adv3.h>
#include <en_us.h>
#include "lastTyped.h"
// Object to hold the canonical form of the verb after we figure it out.
// lastTyped.h has a #define to point gVerb to this object.
lastTypedVerb: object
_verb = "deadbeef" // the resolved verb
get() { return(_verb); }
set(v) { _verb = v; }
;
#ifndef NO_LAST_TYPED_VERB
// Add canonical verbs for actions that accept abbreviations
modify ExamineAction _canonicalVerb =
static [ 'x' -> 'examine', 'l' -> 'look' ];
modify LookInAction _canonicalVerb = 'look';
modify LookThroughAction _canonicalVerb = 'look';
modify LookUnderAction _canonicalVerb = 'look';
modify LookBehindAction _canonicalVerb = 'look';
modify AskForAction _canonicalVerb = 'ask';
modify AskAboutAction _canonicalVerb = 'ask';
modify TellAboutAction _canonicalVerb = 'tell';
modify InventoryAction _canonicalVerb = 'inventory';
modify InventoryWideAction _canonicalVerb = 'inventory';
modify WaitAction _canonicalVerb = 'wait';
modify LookAction _canonicalVerb = 'look';
modify AgainAction _canonicalVerb = 'again';
modify TravelAction _canonicalVerb = static [ 'n' -> 'north', 's' -> 'south',
'e' -> 'east', 'w' -> 'west', 'u' -> 'up', 'd' -> 'down',
'nw' -> 'northwest', 'ne' -> 'northeast', 'sw' -> 'southwest',
'se' -> 'southeast', 'p' -> 'port', 'sb' -> 'starboard' ];
modify Action
// Attempt to determine what, if anything, the arg abbreviates
_getCanonicalVerb(v) {
local i, r;
#ifdef __DEBUG_LAST_TYPED_VERB
"_getCanonicalVerb(<<if v>><<v>><<else>>nil<<end>>)\n ";
#endif // __DEBUG_LAST_TYPED_VERB
if(v == nil) return(nil);
if(_canonicalVerb.ofKind(List)) {
#ifdef __DEBUG_LAST_TYPED_VERB
"\tUsing List\n ";
#endif // __DEBUG_LAST_TYPED_VERB
// If we have a list, see if any of them start with
// our arg, and return that list element if it does.
for(i = 1; i <= _canonicalVerb.length(); i++) {
if(_canonicalVerb[i].startsWith(v))
return(_canonicalVerb[i]);
}
// We have a list but no match, punt.
return(_canonicalVerb[1]);
} else if(_canonicalVerb.ofKind(LookupTable)) {
#ifdef __DEBUG_LAST_TYPED_VERB
"\tUsing LookupTable\n ";
#endif // __DEBUG_LAST_TYPED_VERB
// If we have a hash table, see if any value in it
// starts with our arg, and return it if it does.
r = nil;
_canonicalVerb.forEachAssoc(function(k, e) {
if(r) return;
if(k == v) r = e;
});
if(r) return(r);
return(v);
}
#ifdef __DEBUG_LAST_TYPED_VERB
"\tUsing string\n ";
#endif // __DEBUG_LAST_TYPED_VERB
// We only have a single canonical form, use it.
return(_canonicalVerb);
}
// Get the verb used in this action, prefering what the player actually
// typed unless we can't resolve that into something useful.
getCanonicalVerb() {
local isNoun, match, prop, i, toks, txt, v;
// Prefer the original tokens if that's not what we already have
if(getOriginalAction() != self)
return(getOriginalAction().getCanonicalVerb());
toks = getPredicate().getOrigTokenList();
// Now we go through the token list and record everything
// that isn't part of a noun phrase.
txt = nil;
for(i = 1; i <= toks.length(); i++) {
isNoun = nil;
// Walk through all the noun phrases
foreach(prop in predicateNounPhrases) {
match = self.(prop);
// If we have a match, skip to the end of
// this phrase.
if(match && (i == match.firstTokenIndex)) {
i = match.lastTokenIndex;
isNoun = true;
break;
}
}
// This stretch of tokens isn't a noun phrase, so
// we keep track of it.
if(!isNoun) {
v = getTokVal(toks[i]);
// If the token is a single character and
// we have a canonical verb(s) for this action,
// try to canonicalize the token.
if((v.length() < 3) && _canonicalVerb)
v = _getCanonicalVerb(v);
txt = (txt ? (txt + ' ') : '') + v;
}
}
if(!txt || (txt.length() == 1) && _canonicalVerb)
return(_getCanonicalVerb(txt));
return(txt);
}
resolveAction(issuingActor, targetActor) {
gVerb.set(getCanonicalVerb());
#ifdef __DEBUG_LAST_TYPED_VERB
"\tCanonical verb is <q><<gVerb.get()>></q><.p> ";
#endif // __DEBUG_LAST_TYPED_VERB
return(inherited(issuingActor, targetActor));
}
;
#else // NO_LAST_TYPED_VERB
// Stub methods
modify Action
_getCanonicalVerb(v) { return(v); }
getCanonicalVerb() { return(nil); }
;
#endif // NO_LAST_TYPED_VERB
Example “game” to follow.