Getting the specific verb used by the player

What’s the proper TADS3 ritual for reliably obtaining the verb entered by the player?

There are a few things like gAction.getOrigText() that look promising, but might not get set depending on the actions of the parser. I.e.:

#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>

startRoom:      Room 'Void'
        "This is a featureless void. "
;
+ Thing 'small round pebble' 'pebble'
        "It's a small, round pebble. "
;
+ Thing 'rock' 'rock'
        "It is not a small, round pebble. "
;
modify playerMessages
        allNotAllowed(actor) {
                "gAction.getOrigText = <q><<gAction.getOrigText()>></q>\n ";
                "gAction.getEnteredVerbPhrase = <q><<gAction.getEnteredVerbPhrase()>></q>\n ";
        }
;
me:     Person
        location = startRoom
        desc() {
                "gAction.getOrigText = <q><<gAction.getOrigText()>></q>\n ";
                "gAction.getEnteredVerbPhrase = <q><<gAction.getEnteredVerbPhrase()>></q>\n ";
        }
;
versionInfo:    GameID
        name = 'sample'
        byline = 'nobody'
        authorEmail = 'nobody <foo@bar.com>'
        desc = '[This space intentionally left blank]'
        version = '1.0'
        IFID = '12345'
;
gameMain:       GameMainDef
        initialPlayerChar = me
        allVerbsAllowAll = nil
;

This gives us a player, a starting room, a pebble, and a rock.

me.desc() outputs gAction.getOrigText() and gAction.getEnteredVerbPhrase(), and something like >X ME produces the expected results:

>x me
gAction.getOrigText = "x me"
gAction.getEnteredVerbPhrase = "x (dobj)"

But something like >SMELL ALL, which will output playerMessages.allNotAllowed() (which is identical to me.desc()) doesn’t:

>smell all
gAction.getOrigText = ""
gAction.getEnteredVerbPhrase = ""

In Things, you can do something like:

        lastTyped = nil
        matchNameCommon(origTokens, adjustedTokens) {
                local txt;

                txt = nil;
                adjustedTokens.forEach(function(o) {
                        if(dataTypeXlat(o) == TypeSString) 
                                txt = (txt ? (txt + ' ') : '') + o;
                });
                lastTyped = txt;
                return(self);
        }

…or to give a full example…

#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>

startRoom:      Room 'Void'
        "This is a featureless void. "
;
modify Thing
        lastTyped = nil
        matchNameCommon(origTokens, adjustedTokens) {
                local txt;

                txt = nil;
                adjustedTokens.forEach(function(o) {
                        if(dataTypeXlat(o) == TypeSString) 
                                txt = (txt ? (txt + ' ') : '') + o;
                });
                lastTyped = txt;
                return(self);
        }
;
me:     Person 'foo bar baz me' 'me'
        location = startRoom
        desc() {
                "Last term used was <q><<lastTyped>></q>. ";
        }
;
versionInfo:    GameID
        name = 'sample'
        byline = 'nobody'
        authorEmail = 'nobody <foo@bar.com>'
        desc = '[This space intentionally left blank]'
        version = '1.0'
        IFID = '12345'
;
gameMain:       GameMainDef
        initialPlayerChar = me
        allVerbsAllowAll = nil
;

…which gets us something like…

>x me
Last term used was "me".

>x foo
Last term used was "foo".

>x foo bar
Last term used was "foo bar".

…and so on. What I’d like to be able to do is something similar for verbs/actions. That is, keep track of which version of a verb the player is actually using: GET versus TAKE or whatever.

Without checking, I’d guess that by the time allNotAllowed is called, gAction is nil. It seems like you should be able to store the getEnteredVerbPhrase somewhere on each firing of an action. I modified the doAction routine to debug print the action kind plus parent/original action if I toggle a testing mode…

This is also a handy macro if you don’t have a similar one yet…

  //debugPrint
#define dp(val) "\n" ## #val ## " = <<reflectionServices.valToSymbol(val)>> \n"
//method(arg) { doWork;
          dp(arg);
          dp(arg.someProp);
          doMoreWork(); }

I always suspected that the lack of recording of the actual verb typed led to one of the minor but somewhat unsatisfactory reporting issue, the “telegraphic” (and very undifferentiated) reporting of TAKE/GET/PICK UP:

>drop letter
Dropped. 


>take letter
Taken. 


>drop letter
Dropped. 


>get letter
Taken. 


>drop letter
Dropped. 


>pick up letter
Taken. 

since 3.1.0 TADS got what I consider the second-best embedded adaptive prose around (the best being Inform 7…), one capable of embedding the actual verb typed in the response, so I’m interested on a solution.

The best tackling of this issue seems preserving the typed verb prior of the clearing of gAction, sometime prior of the call of allNotAllowed, as Ziegler points. So perhaps the solution can be found within the system and/or the technical manual ?

really HTH, because I’m interested, and
Best regards from Italy,
dott. Piergiorgio.

Is this what you are looking for?

#define dp(a) "\n" ## #a ## " = <<reflectionServices.valToSymbol(a)>> \n"
#define TEST libGlobal.testing
modify libGlobal 
	lastVbPhraseTab = static new LookupTable()
	lastOrigTextTab = static new LookupTable()
	testing = nil 
;
verbRule(testing) 'testing' : IAction
	execAction { libGlobal.testing = (!libGlobal.testing); 
		if(libGlobal.testing) "Testing is now on. \n";
		else "Testing is now off. \n"; } 
;
modify Action
	doActionOnce { 
		if(TEST && testDmn.showActionStats) { 
			dp(gPlayerChar.lastAction);
			if(gAction && gAction.grammarInfo() && gAction.grammarInfo().oK(Collection) 
					&& gAction.grammarInfo().length) 
				dp(gAction.grammarInfo()[1]);
			if(gActor!=gPlayerChar) dp(gActor);
			dp(gAction); 
			dp(isImplicit);
			dp(isRemapped);
			dp(originalAction);
			dp(parentAction);
			dp(gDobj);
			dp(gIobj);
            dp(getOrigText);
			dp(getEnteredVerbPhrase);
			dp(libGlobal.lastOrigTextTab[baseActionClass]);
			dp(libGlobal.lastVbPhraseTab[baseActionClass]);
			"\b";
			libGlobal.lastVbPhraseTab[baseActionClass] = getEnteredVerbPhrase;
			libGlobal.lastOrigTextTab[baseActionClass] = getOrigText;
			}
		inherited; } 
	afterAction { gActor.lastAction = gAction; }
;
modify Actor
	lastAction = static WaitAction.createActionInstance()
;
testDmn: PromptDaemon, InitObject 
	execute() {
		construct(self, &beforePrompt); 
		startCode();
		if(cmdStr && cmdStr.find(R'<AlphaNum>')) nestedCmd(cmdStr);
		}
	startCode() { /* set flags/run routines/disable obstacles... */ }
	cmdStr = '' //type in a couple of "GoNear"s or "Purloins" or whatever...
	nestedCmd(str) { if(!str) return; 
		local toks = Tokenizer.tokenize(str);
		executeCommand(gPlayerChar, gPlayerChar, toks, true); }
	beforePrompt() { 
		if(TEST) {
			//dp(...); any additional info you want to monitor from turn to turn
			}
		}	
	showActionStats = true
;

Not quite. It’s possible that what I’m looking for just isn’t practical in TADS3 at all.

Keeping only a very small number of moving parts from your example, we can do something like:

#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>

gVerb: object
        _verb = "deadbeef"
        get() { return(_verb); }
        set(v) { _verb = v; }
;
modify Action
        doActionOnce() {
                gVerb.set(getEnteredVerbPhrase);
                inherited();
        }
;

startRoom:      Room 'Void'
        "This is a featureless void. "
;
+ Thing 'small round pebble' 'pebble'
        "It's a small, round pebble. "
;
+ Thing 'rock' 'rock'
        "It is not a small, round pebble. "
;
modify playerMessages
        allNotAllowed(actor) {
                "<.parser>
                {You/he} can only <<gVerb.get()>> one thing at a time.
                <./parser> ";
        }
;
me:     Person
        location = startRoom
        desc() {
                "The verb phrase is <q><<gVerb.get()>></q>. ";
        }
;
versionInfo:    GameID
        name = 'sample'
        byline = 'nobody'
        authorEmail = 'nobody <foo@bar.com>'
        desc = '[This space intentionally left blank]'
        version = '1.0'
        IFID = '12345'
;
gameMain:       GameMainDef
        initialPlayerChar = me
        allVerbsAllowAll = nil
;

In this example we globally modify Action to insert something to save stuff from the current action (as per your example). In this case we define a global gVerb that just holds the value of getEnteredVerbPhrase, but this is enough to illustrate the problems:

>smell all
You can only deadbeef one thing at a time.

That “deadbeef” is there because it’s the default value set when gVerb is initialized, and it’s there just to highlight for us that the verb is never being set because doActionOnce() is never being called because the parser is doing something else.

Beyond that, even when we have correctly anticipated the parser’s behavior, we still end up with something like:

>x me
The verb phrase is "x (dobj)".

Which is I guess slightly closer than before (in that we’ve managed to preserve part of gAction after the parser has cleared it) but so far as I know there’s no straightforward way of converting that into (in this case) “examine”.

It kinda looks like TADS3 just really isn’t set up for this sort of thing, which is…annoying. @Piergiorgio_d_errico touches on this above, but the specific problem I’m barking my shins on is trying to get various playerMessages to be more responsive so as to be more helpful/less misleading when the player is feeling their way around a solution to a problem but haven’t gotten the exact syntax right.

Anyway, unrelated to that there appear to be a couple typos in your code: it should be VerbRule(testing) instead of verbRule(testing) and the conditional that checks gAction.grammarInfo().oK(Collection) chucks a wobbly because gAction.grammarInfo().oK() doesn’t exist. As near as I can tell that part of the conditional can just be deleted because it doesn’t do anything here. I might be missing something though. And in order to compile, the sample code needs #include "reflect.t". Just throwing that out there in case anybody else is trying to compile it.

An update:

After digging around in the adv3 parser for awhile and following a few dead ends, it looks like this is a simple but reliable way to monkey patch in verb twiddling, with one caveat:

        resolveAction(issuingActor, targetActor) {
                gVerb.set(getEnteredVerbPhrase());
                return(inherited(issuingActor, targetActor));
        }

The adv3 parser always calls resolveFirstAction() on a production returned by parseTokens(), and this will eventually call resolveAction() on an underlying Action (assuming the parser can determine an underlying Action to call).

The caveat is that this may be language-specific: Action.resolveAction() isn’t part of the language-independent Action declaration in action.t, it’s in (for US English) en_us.t.

It also doesn’t fix the fact that Action.getEnteredVerbPhrase() is not quite what we want (which is the full, non-disambiguated version of the verb typed by the player) but it’s progress:

>smell all
You can only smell (dobj) one thing at a time.

Here’s a complete example:

#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>

gVerb: object
        _verb = "deadbeef"
        get() { return(_verb); }
        set(v) { _verb = v; }
;
modify Action
        resolveAction(issuingActor, targetActor) {
                gVerb.set(getEnteredVerbPhrase());
                return(inherited(issuingActor, targetActor));
        }
;
startRoom:      Room 'Void'
        "This is a featureless void. "
;
+ Thing 'small round pebble' 'pebble'
        "It's a small, round pebble. "
;
+ Thing 'rock' 'rock'
        "It is not a small, round pebble. "
;
modify playerMessages
        allNotAllowed(actor) {
                "<.parser>
                {You/he} can only <<gVerb.get()>> one thing at a time.
                <./parser> ";
        }
;
me:     Person
        location = startRoom
;
versionInfo:    GameID
        name = 'sample'
        byline = 'nobody'
        authorEmail = 'nobody <foo@bar.com>'
        desc = '[This space intentionally left blank]'
        version = '1.0'
        IFID = '12345'
;
gameMain:       GameMainDef
        initialPlayerChar = me
        allVerbsAllowAll = nil
;

I was about to suggest basically the same thing in executeAction:

modify executeAction(a,b,c,d,action) {
	// store action.getEnteredVerbPhrase somewhere
	replaced(a,b,c,d,action);
} ;

As for the text of getEnteredVerbPhrase, that should be a pretty simple rexReplace of ’ (dobj)/(iobj)’, right? And for those few verbs that have shortcuts like ‘x’, they may just need to be manually rexReplaced too with the correct text.

I apologize for the typos. That happens because I use tons of macros in my own code, and when I try to paste something on the forum here I am usually in a big rush, and miss a few things when I’m trying to modify my code to make sense to the general public. oK is ofKind, and I use vR for VerbRule, which is why I forgot to capitalize.

Doing it via regex seems like a perfect way to introduce subtle bugs that will only be discovered long after you’ve forgotten that you even fiddled around with this stuff. Or at least I’m about 90% certain that’s how it would work out for me.

Here’s a getVerb() method for Action that mirrors how adv3’s getEnteredVerbPhrase() works, so it’s probably no more brittle than anything else in adv3:

#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>

gVerb: object
        _verb = "deadbeef"
        get() { return(_verb); }
        set(v) { _verb = v; }
;
modify Action
        getVerb() {
                local isNoun, match, prop, i, toks, txt;

                if(getOriginalAction() != self)
                        return(getOriginalAction().getVerb());
                toks = getPredicate().getOrigTokenList();

                txt = nil;
                for(i = 1; i <= toks.length(); i++) {
                        isNoun = nil;
                        foreach(prop in predicateNounPhrases) {
                                match = self.(prop);
                                if(match && (i == match.firstTokenIndex)) {
                                        i = match.lastTokenIndex;
                                        isNoun = true;
                                        break;
                                }
                        }
                        if(!isNoun)
                                txt = (txt ? (txt + ' ') : '')
                                        + getTokVal(toks[i]);
                }
                return(txt);
        }
        resolveAction(issuingActor, targetActor) {
                gVerb.set(getVerb());
                return(inherited(issuingActor, targetActor));
        }
;
startRoom:      Room 'Void'
        "This is a featureless void. "
;
+ Thing 'small round pebble' 'pebble'
        "It's a small, round pebble. "
;
+ Thing 'rock' 'rock'
        "It is not a small, round pebble. "
;
modify playerMessages
        allNotAllowed(actor) {
                "<.parser>
                {You/he} can only <<gVerb.get()>> one thing at a time.
                <./parser> ";
        }
;
me:     Person
        location = startRoom
        desc() {
                "The verb is <q><<gVerb.get()>></q>. ";
        }
;
versionInfo:    GameID
        name = 'sample'
        byline = 'nobody'
        authorEmail = 'nobody <foo@bar.com>'
        desc = '[This space intentionally left blank]'
        version = '1.0'
        IFID = '12345'
;
gameMain:       GameMainDef
        initialPlayerChar = me
        allVerbsAllowAll = nil
;

This gets us:

>smell all
You can only smell one thing at a time.

…but doesn’t resolve short verb forms into their canonical form…

>x me
The verb is "x".

Okay, here’s an attempt.

Near as I can tell the actions don’t automagically have anything like a canonical verb form (like a Thing has a name, for example). So here’s a kludge based on grepping through the adv3 source for all the actions that have abbreviations:

#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>

gVerb: object
        _verb = "deadbeef"
        get() { return(_verb); }
        set(v) { _verb = v; }
;

// Add canonical verbs for actions that accept abbreviations
modify ExamineAction _canonicalVerb = [ 'examine', '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 Action
        // Attempt to determine what, if anything, the arg abbreviates
        _getCanonicalVerb(v) {
                local i;

                if(v == nil) return(nil);
                // If we have a list, see if any of them start with our arg,
                // and return that list element if it does.
                if(_canonicalVerb.ofKind(List)) {
                        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]);
                }

                // 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.
        getVerb() {
                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().getVerb());
                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() == 1) && _canonicalVerb)
                                        v = _getCanonicalVerb(v);
                                txt = (txt ? (txt + ' ') : '') + v;
                        }
                        
                }
                if(!txt || (txt.length() == 1) && _canonicalVerb)
                        return(_getCanonicalVerb(txt));
                return(txt);
        }
        resolveAction(issuingActor, targetActor) {
                gVerb.set(getVerb());
                return(inherited(issuingActor, targetActor));
        }
;
startRoom:      Room 'Void'
        "This is a featureless void. "
;
+ Thing 'small round pebble' 'pebble'
        "It's a small, round pebble. "
;
+ Thing 'rock' 'rock'
        "It is not a small, round pebble. "
;
modify playerMessages
        allNotAllowed(actor) {
                "<.parser>
                {You/he} can only <<gVerb.get()>> one thing at a time.
                <./parser> ";
        }
;
me:     Person
        location = startRoom
        desc() {
                "The verb is <q><<gVerb.get()>></q>. ";
        }
;
versionInfo:    GameID
        name = 'sample'
        byline = 'nobody'
        authorEmail = 'nobody <foo@bar.com>'
        desc = '[This space intentionally left blank]'
        version = '1.0'
        IFID = '12345'
;
gameMain:       GameMainDef
        initialPlayerChar = me
        allVerbsAllowAll = nil
;

First we add a _canonicalVerb to all the actions that have abbreviations, which are mostly variations on LOOK, ASK, and TELL (today I learned ask and tell have abbreviations!).

The _canonicalVerb property can either be a string or a list. If it’s a string, the string is always the preferred canonical form. If the property is a list, then _getCanonicalVerb() will go through the list and check each element to see if the verb the player actually typed is at the start of the element. If so, that element is the canonical verb. If no elements match, then the first element is the canonical form (useful mostly for X, which is not the first letter of EXAMINE, which as near as I can tell is the lone outlier of an action that has multiple verbs/abbreviations and one of them isn’t an initialism–G for AGAIN and Z for WAIT are unambiguous because they each only have one canonical verb, unlike EXAMINE and LOOK).

Anyway, it seems to do what I want for my currently short list of test cases:

>smell all
You can only smell one thing at a time.

>x me
The verb is "examine".

>examine me
The verb is "examine".

>l at me
The verb is "look at".

>l me
The verb is "look".

>look at me
The verb is "look at".

The main known issue/bug/whatever here that I know about off the bat is that this doesn’t work for the directional abbreviations, but that’s an implementation wart I’m not currently concerned about.

Nearing a solution ?

I haven’t (yet) tested jbg’s code, whose seems good, if not excellent, the only (inevitable) complication became the peculiar nature of “AGAIN/G” whose means “do my last non-again command”, whose in an ideal-response parser ought to output something e.g.

> EXAMINE GIZMO

You examine the gizmo, whose seems an un-ordinary gizmo to me.

> again

You examine again the gizmo, whose seems an un-ordinary gizmo to me.

how is now, should reply a rather questionable “you again the gizmo,…”

of course, I suggest that if all goes well, the resulting code ought to get published in its proper place, that is, if-archive/programming/tads3/library/contributions

Best regards from Italy,
dott. Piergiorgio.

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.

2 Likes

And here’s a simple “game” that illustrates how to use the above-posted library.

sample.t

#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>
#include "lastTyped.h"

startRoom:      Room 'Void'
        "This is a featureless void. "
;
+ Thing 'small round pebble/stone' 'pebble'
        "You just tried to <<gVerb.get()>> the {last/it dobj}.
        \nThe most frequently used term for this object has been
        {most/it dobj}. "
        canonicalNoun = [ 'pebble', 'stone' ]
;
+ Thing 'rock' 'rock'
        "It is not a small, round pebble. "
;
modify playerMessages
        allNotAllowed(actor) {
                "<.parser>
                {You/he} can only <<gVerb.get()>> one thing at a time.
                <./parser> ";
        }
;
me:     Person
        location = startRoom
        desc() {
                "The verb is <q><<gVerb.get()>></q>. ";
        }
;
versionInfo:    GameID
        name = 'sample'
        byline = 'nobody'
        authorEmail = 'nobody <foo@bar.com>'
        desc = '[This space intentionally left blank]'
        version = '1.0'
        IFID = '12345'
;
gameMain:       GameMainDef
        initialPlayerChar = me
        allVerbsAllowAll = nil
;

…and a makefile for it…

makefile.t3m

-D LANGUAGE=en_us
-D MESSAGESTYLE=neu
-Fy obj -Fo obj
-o game.t3
-lib system
-lib adv3/adv3
-lib /usr/local/share/frobtads/lib/lastTyped/lastTyped.tl
-source sample

In my case I have the files for the new library in /usr/local/share/frobtads/lib/lastTyped.

The game can be compiled with t3make -f makefile.t3m.

A couple of notes:

The library as presented does the stuff mentioned previously in the thread. It also adds a couple new parameter substitution strings, last and most. Their use is illustrated the pebble’s description:

+ Thing 'small round pebble/stone' 'pebble'
        "You just tried to <<gVerb.get()>> the {last/it dobj}.
        \nThe most frequently used term for this object has been
        {most/it dobj}. "
        canonicalNoun = [ 'pebble', 'stone' ]
;

In this case {last/it dobj} will output the most recent noun used to refer to the pebble and {most/it dobj} will output the noun most commonly used to refer to the pebble. So:

>x pebble
You just tried to examine the pebble.
The most frequently used term for this object has been pebble.

>l at pebble
You just tried to look at the pebble.
The most frequently used term for this object has been pebble.

>x stone
You just tried to examine the stone.
The most frequently used term for this object has been pebble.

>l at stone
You just tried to look at the stone.
The most frequently used term for this object has been pebble.

>l at stone
You just tried to look at the stone.
The most frequently used term for this object has been stone.

…and so on.

If you don’t want to track the noun usage stats you can comment out the #define LAST_TYPED_NOUN_STATS line in lastTyped.h.

There are a couple of other configuration options in lastTyped.h and the code should be fairly well explained in the comments.

3 Likes

it’s simple as filling a form:

http://upload.ifarchive.org/cgi-bin/upload.py

Best regards from Italy,
dott. Piergiorgio.

I hope you upload this to ifarchive, it seems quite useful. I’m thinking I might try and port a similar mini-library to adv3Lite.

Wow, I never knew that. Live and learn.

Just submitted a slightly updated version of the code posted above to if-archive.

Minor changes:

  • You get the resolved verb by using gVerb by itself instead of gVerb.get(). Also added a {verb} message param substitution string, so “You just tried to <<gVerb.get()>> the {last/it dobj}.” becomes “You just tried to {verb} the {last/it dobj}.”
  • The message param substitution string {most} now prefers the just-typed noun in cases of ties (before it always prefered the noun that occurs earliest in the list). So >X PEBBLE two times followed by X STONE twice will now prefer “stone” on the last command instead of “pebble”. This is super important why are your eyes glazing over?
  • The canonicalNoun property has been renamed lastTypedNounList.
  • More comments and examples, including pointing out that lastTypedNounList = noun will generally do what you want it to if the object’s vocabulary is declared correctly. That is, an object declared with pebble: Thing 'small round pebble/stone/rock' 'pebble' that has lastTypedNounList = noun is equivalent to lastTypedNounList = static [ 'pebble', 'stone', 'rock' ].

I’ll bump with a link if/when it gets accepted.

5 Likes

Great !

will be put immediately in active service !

thanks for the added {verb} call to gVerb.get, whose put Tads3 on par with alan3’s $v as ease of use, and also of code readability…

Best regards from Italy,
dott. Piergiorgio.

The main “gotcha” to be aware of (I don’t know if this is true in alan or not) is that {verb} is just returning the (canonicalized) form of the verb the player typed. It doesn’t apply any endings for case or tense.

This is mentioned in the comments in the code, but in general in TADS3 message param substitution you can do stuff like {You/he} look{s} around. and {You/he} watch{es} the clock. and so on, and you can’t reliably do that with this implementation of {verb}.

It works fine for the design case: Replacing things like playerMessages.allNotAllowed()'s "All" cannot be used with that verb. with something slightly more responsive like {You/he} can only {verb} one thing at a time. but care needs to be taken about how {verb} is used.

don’t worry. suffice for what I have in mind (it’s a conceptual experiment, so I can divulge the logic of that unusual experimental puzzle: replying differently to diverse gradation of rubbing (rub, clean, polish, shine…) and a very shining mirror give the most interesting response under the sun. so as is, “you {verb} the mirror. << if gverb == rub>> this happens<< end >> << if gverb = clean >> that happens…” and so on IS what I really needed in this experiment, whose else is done thru four different verbs, and with your extension the gain in brevity AND readability of code is considerable, and every coder known well that gaining both readability and compactness of code is a very rare feat.)

Best regards from Italy,
dott. Piergiorgio.

Bumping the thread because it finally showed up on ifarchive. The library is called “lastTyped”.

You can find it by scrolling through the list of library contributions, or here’s a direct link to the zip file.

4 Likes