Global Remapping - I can fix it, but don't know why it's broke!

TL;DR - Remapping nouns to topics does not like long plurals

Ran into an interesting bug/artifact during testing what I THOUGHT was a stable GlobalRemap refactoring of my bespoke SearchFor verb. As a reminder, I wanted to enable something like >SEARCH PILE OF PAPERS FOR DEED but preserve functionality like >LOOK UP DEED IN REGISTRY or, problematically, its alternate form >SEARCH REGISTRY FOR DEED What this amounts to is navigating the overlapping vocabularies with different verb types (TIAction for SEARCHFOR and TopicTAction for CONSULTABOUT).

Here is my refactored GlobalRemapping solution, which tries to accomodate incomplete commands. (ie >SEARCH FOR DEED What would you like to search for that?)

/* 
 *	SearchFor Action, to be able to find stuff
 *  Note, IOBJ checks but DOBJ executes
 */
modify Thing
	dobjFor(SearchFor) {  // allow all
    	preCond = [touchObj]
        verify() { }
        check() { }
        action() { 
            if (gIobj.isIn(self)) {
                "You found {it dobj/him}!  ";
                gIobj.discover();
                nestedAction(Take, gIobj);
            }
        }
    }
	iobjFor(SearchFor) {
		preCond = []
		verify() {
            if ((gDobj != nil) && !gDobj.ofKind(Consultable)) {
                if (isIn(gActor))
                    illogical('{You/he} {are} already holding {that iobj/him}.  ');
                else if (canBeSeen)
                    illogical('{It\'s iobj/he\'s} right here<<if !isDirectlyIn(getOutermostRoom())>>,
                        <<location.objInPrep>> <<location.theName>><<end>>!  ');
                else if ((!ofKind(Hidden) && seen) || discovered)
                    illogical('{You/he} already know{s} where {that iobj/he} {are}.  ');
            } // else let Consultable handle it
        }
        check() {
           if (!isIn(gDobj) && !gDobj.ofKind(Consultable)) 
             failCheck(gDobj.cannotBeFoundHereMsg);
           }
	}
	cannotBeFoundHereMsg = 'It is unlikely {that iobj/he} could be found here.  '
;
/*
 *  Remap SearchFor to ConsultAbout if the nominal DOBJ is a Consultable
 */
consultableSearchForRemap : GlobalRemapping
    getRemapping(issuingActor, targetActor, action) {

        if (action.ofKind(SearchForAction) ) {

          local dobjIsConsultable = (instanceWhich(Consultable, {x: action.canDobjResolveTo(x) }));

          if (dobjIsConsultable) { 

            local newAction = ConsultAboutAction.createActionInstance();
            newAction.setOriginalAction(action);

            local dobj = action.dobjMatch;
            local topic = newAction.reparseMatchAsTopic(action.iobjMatch, issuingActor, issuingActor);

            newAction.setObjectMatches(dobj, topic);
            return [issuingActor, newAction];
        } }
        return nil;
    }
;
//  HERE IS THE WEIRD PART
// post-remap, some new Resolve results do not set nounSlotCount
//   (default was nil), which threw error
// most sensitive when iobjMatch included plurals!
modify rankByVerbStructure
    comparePass2(a, b)
    {
        a.noteNounSlots(0); // only sets to 0 if currently nil
        b.noteNounSlots(0);
        return inherited(a,b); 
    }
;
/*
 *  Does not allow remap when prompted to complete with DOBJ:  SEARCHWHATFOR
 *    ie disallows >find sword -> 'What do you want to search for it?' -> ><consultable>
 *  instead, since are assured IOBJ will be a Thing, can just replace with impunity
 */
modify Consultable
    dobjFor(SearchFor) { 
        action() { replaceAction(ConsultAbout, self, gIobj); }
    }
;
// accepts all Hidden objects as possible, including default intangible one below
//
class IobjResolverSearchFor : IobjResolver
	objInScope(obj) { return ((obj.ofKind(Hidden) && obj.isKnown) || inherited(obj)); }
;
DefineTIAction(SearchFor)
	actionAllowsAll = nil
    createIobjResolver (issuingActor, targetActor) {  //see below
		return new IobjResolverSearchFor(self, issuingActor, targetActor);
	}
;
VerbRule(SearchFor)
    ('dig' | 'dig' 'in' | 'li' | 'lb' | 'lt' | 'search' | //'lu'
    | ('look' | 'l' | 'search' ) ('in' | 'inside' | 'on' | 'under' | 'behind' | 'through') ) singleDobj 'for' singleIobj
    | ('find' | ('search' | 'dig' | 'look' | 'l') 'for') singleIobj
        ('in' | 'inside' | 'on' | 'under' | 'behind') singleDobj : SearchForAction
    verbPhrase = 'search/searching (what) (for what)'
	omitIobjInDobjQuery = true
    askDobjResponseProd = forSingleNoun
;
VerbRule(SearchWhatFor)
    ('find' | ('search' | 'dig'| 'look' | 'l') 'for') singleIobj : SearchForAction
    verbPhrase = 'search/searching (what) (for what)'
    whichMessageTopic = DirectObject
    construct()
    {
        /* set up the empty direct object phrase */
        dobjMatch = new EmptyNounPhraseProd();
        dobjMatch.responseProd = inSingleNoun;
    }
;
modify VerbRule(ConsultAbout)
    'consult' singleDobj ('on' | 'about') singleTopic
    // | 'search' singleDobj 'for' singleTopic
    //| 'search' 'in' singleDobj 'for' singleTopic
    | (('look' | 'l') ('up' | 'for')
       // | 'find'
       // | 'search' 'for'
       | 'lu' | 'read' 'about' )
         singleTopic ('in' | 'on') singleDobj
    | ('look' | 'l') singleTopic 'up' 'in' singleDobj
    :
    defaultForRecursion = true // to ensure CA is chosen during remap, not CWA
;
replace VerbRule(ConsultWhatAbout)
    [badness 500] (('look' | 'l') ('up' | 'for')
      // | 'find'
      // | 'search' 'for'
     | 'lu' | 'read' 'about') singleTopic
     | ('look' | 'l') singleTopic 'up' : ConsultAboutAction
    // changed from default to accomodate SEARCHFOR reusing this vocab
    verbPhrase = 'look/looking up (what) (in what)'  
    whichMessageTopic = DirectObject
    construct()
    {
        /* set up the empty direct object ploohrase */
        dobjMatch = new EmptyNounPhraseProd();
        dobjMatch.responseProd = inSingleNoun;
    }
;

Ok, that’s a mouthful, should probably be a library of some kind. Note commented out ConsultAbout vocabulary, essentially giving SearchFor first rights to the vocab.

This was working great, until I inadvertently tested it with a long word that ALSO had some plurals. An object that existed in the world shelves ALSO was used for plurals elsewhere. Granted that isn’t very interesting to consult a Consultable about, but my theory was should not core dump if tried!

It did core dump though, when I tried >FIND SHELVES IN ENCYCLOPEDIA

I traced the error deep into the Verb resolution process, after it had successfully recognized the Consultable that needed verb remapping. What happens is, after successfully reparsing the iobjs as topics (finding about 3 of them), it creates and sorts attendant special purpose CommandRanking objects. The intent here is for the game to test each of these items and pick the one the player actually intended. There are 22 separate criteria, sorted into two phases to do this!

The 21st Criteria is called rankByVerbStructure which prioritizes high noun counts over low. The idea is that >DETACH WIRE FROM BOX is more likely to mean iobj = box (nounCount =2) than dobj = ‘wire from box’ (nounCount = 1).

This nounCount value gets set by the Verb, typically. (IAction = 0, TAction = 1, TIAction = 2, etc) Except when plurals were involved, it sometimes didn’t get set at all for reasons I could not determine… though apparently only during remaps? The default value is nil, and gets fed to a simple subtraction comparator (is a -b >0? <0?), which coredumps on nil. It seems like the REAL error is ‘why didn’t nounCount get set right?’ I found I could get usable results (since my use case pretty much guaranteed uniform noun counts when correctly analyzed) with this snippet from above:

//  HERE IS THE WEIRD PART
// post-remap, some new Resolve results do not set nounSlotCount
//   (default was nil), which threw error
// most sensitive when iobjMatch included plurals!
modify rankByVerbStructure
    comparePass2(a, b)
    {
        a.noteNounSlots(0); // only sets to 0 if currently nil
        b.noteNounSlots(0);
        return inherited(a,b); 
    }
;

Can’t help feel I’m patching something that could be deeper. Anyone played with this kinda thing before?

1 Like

If really interested, here is a sample game, a reduced example.

#charset "us-ascii"
//
// sample.t
// Version 1.0
// Copyright 2023 JJMcC
//  
// This is a very simple demonstration "game" for the plural GlobalRemap
// matching problem
//
// It can be compiled via the included makefile with
//
//  # t3make -f makefile_remap.t3m
//
// ...or the equivalent, depending on what TADS development environment
// youre using.
//  
// This "game" is distributed under the MIT License, see LICENSE.txt
// for details.
//
#include <adv3.h> 
#include <en_us.h>
    
versionInfo: GameID
    IFID = '12345'
    name = 'CG demo'
    byline = '(c) 2024 Jeff J McCoskey'
    version = '0.1'
    licenseType = 'Freeware'
;

startRoom : OutdoorRoom 'Front Yard'
    "This is a featureless front yard.  A house is to the north. "
    east = house
    in asExit(east)
    vocabWords = 'front outside/yard'
;
+me : Person;
+rock : Thing 'ordinary rock*rocks stones pebbles' 'rock'
    "An ordinary rock.  "
;
+stone : Thing 'notable stone*rocks stones pebbles' 'stone'
    "A notable stone.  "
;
house : Room 'Inside House'
    "This is the one room house (apparently with no door).
        The front yard lies to the south.  "
    vocabWords = 'inside/house/room'
    west = startRoom
    out asExit(west)
;
+tome : Consultable 'big book/knowledge' 'big book of knowledge'
    "A book of info.  "
    dobjFor(ConsultAbout) {
        action() { "Found lots of cool info. "; }
    }
;
+pebble : Thing 'extraordinary pebble*rocks stones pebbles' 'pebble'
    "An extraordinary pebble.  "
;

gameMain : GameMainDef
    initialPlayerChar = me
;
/* ===================================================== */
/* 
 *  SearchFor Action, to be able to find stuff.
 *  Note, IOBJ checks but DOBJ executes
 */
modify Thing
    dobjFor(SearchFor) {  // allow all
        preCond = [touchObj]
        verify() { }
        check() { }
        action() {
            if (gIobj.isIn(self)) {
                "You found {it dobj/him}!  ";
                gIobj.discover();
                nestedAction(Take, gIobj);
            }
        }
    }
    iobjFor(SearchFor) {
        preCond = []
        verify() {
            if ((gDobj != nil) && !gDobj.ofKind(Consultable)) {
                if (isIn(gActor))
                illogical('{You/he} {are} already holding {that iobj/him}.  ');
                else if (canBeSeen)
                    illogical('{It\'s iobj/he\'s} right here<<if !isDirectlyIn(getOutermostRoom())>>,
                      <<location.objInPrep>> <<location.theName>><<end>>!  ');
                else if ((!ofKind(Hidden) && seen) || discovered)
                    illogical('{You/he} already know{s} where {that iobj/he} {are}.  ');
            } // else let Consultable handle it
        }
        check() { if (!isIn(gDobj) && !gDobj.ofKind(Consultable))
           failCheck(gDobj.cannotBeFoundHereMsg); }
    }
    cannotBeFoundHereMsg = 'It is unlikely {that iobj/he} could be found here.  '
;
/*
 *  Remap SearchFor to ConsultAbout if the nominal DOBJ is Consultable
 */
consultableSearchForRemap : GlobalRemapping
    getRemapping(issuingActor, targetActor, action) {

        if (action.ofKind(SearchForAction) ) {

          local dobjIsConsultable = (instanceWhich(Consultable, {x: action.canDobjResolveTo(x) }));

          if (dobjIsConsultable) {

            local newAction = ConsultAboutAction.createActionInstance();
            newAction.setOriginalAction(action);

            local dobj = action.dobjMatch;
            local topic = newAction.reparseMatchAsTopic(action.iobjMatch, issuingActor, issuingActor);

            newAction.setObjectMatches(dobj, topic);
            return [issuingActor, newAction];
        } }
        return nil;
    }
;
/*
 *  Does not allow remap when prompted to complete with DOBJ:
    SEARCHWHATFOR causes core dump
 *    ie disallows >find sword -> 'What do you want to search for it?' -> ><consultable>
 *  instead, since are assured IOBJ will be a Thing, can just replace with impunity 
 */
modify Consultable
    dobjFor(SearchFor) {
        action() { replaceAction(ConsultAbout, self, gIobj); }
    }
;
// accepts all Hidden objects as possible
//  
class IobjResolverSearchFor : IobjResolver
    objInScope(obj) { return ((obj.ofKind(Hidden) && obj.isKnown) || inherited(obj)); }
;   
DefineTIAction(SearchFor)
    actionAllowsAll = nil
    createIobjResolver (issuingActor, targetActor) {  //see below
        return new IobjResolverSearchFor(self, issuingActor, targetActor);
    }
;
VerbRule(SearchFor)
    ('dig' | 'dig' 'in' | 'li' | 'lb' | 'lt' | 'search' | //'lu'
    | ('look' | 'l' | 'search' ) ('in' | 'inside' | 'on' | 'under' | 'behind' | 'through') ) singleDobj 'for' singleIobj
    | ('find' | ('search' | 'dig' | 'look' | 'l') 'for') singleIobj
        ('in' | 'inside' | 'on' | 'under' | 'behind') singleDobj : SearchForAction
    verbPhrase = 'search/searching (what) (for what)'
    omitIobjInDobjQuery = true
    askDobjResponseProd = forSingleNoun
;
VerbRule(SearchWhatFor)
    ('find' | ('search' | 'dig'| 'look' | 'l') 'for') singleIobj : SearchForAction
    verbPhrase = 'search/searching (what) (for what)'
    whichMessageTopic = DirectObject
    construct()
    {
        /* set up the empty direct object phrase */
        dobjMatch = new EmptyNounPhraseProd();
        dobjMatch.responseProd = inSingleNoun;
    }
;
// change from default to accomodate SEARCHFOR reusing this vocab
modify VerbRule(ConsultAbout)
    'consult' singleDobj ('on' | 'about') singleTopic
    // | 'search' singleDobj 'for' singleTopic
    //| 'search' 'in' singleDobj 'for' singleTopic
    | (('look' | 'l') ('up' | 'for')
       // | 'find'
       // | 'search' 'for'
       | 'lu' | 'read' 'about' )
         singleTopic ('in' | 'on') singleDobj
    | ('look' | 'l') singleTopic 'up' 'in' singleDobj
    :
    defaultForRecursion = true // to ensure CA is chosen during remap, not CWA
;
replace VerbRule(ConsultWhatAbout)
    [badness 500] (('look' | 'l') ('up' | 'for')
      // | 'find'
      // | 'search' 'for'
     | 'lu' | 'read' 'about') singleTopic
     | ('look' | 'l') singleTopic 'up' : ConsultAboutAction
    verbPhrase = 'look/looking up (what) (in what)'
    whichMessageTopic = DirectObject
    construct()
    {
        /* set up the empty direct object ploohrase */
        dobjMatch = new EmptyNounPhraseProd();
        dobjMatch.responseProd = inSingleNoun;
    }
;
//* FIXES ERROR WHEN UNCOMMENTED
// post-remap, some new Resolve results do not set nounSlotCount
//     (default was nil), which threw error
// most sensitive when iobjMatch included plurals!
modify rankByVerbStructure
    comparePass2(a, b)
    {
        a.noteNounSlots(0); // only sets to 0 if currently nil
        b.noteNounSlots(0);
        return inherited(a,b);
    }
; */

Most of the code is the SearchFor stuff from above, sorry.

Playing as is (with fix commented out) results in:

Front Yard
This is a featureless front yard.  A house is to the north.

You see a stone and a rock here.

>e
Inside House
This is the one room house (apparently with no door).  The front yard lies to
the south.

You see a pebble and a big book of knowledge here.

>find pebble
What do you want to search for it?

>house
It's right here!

>find pebble
What do you want to search for it?

>book
Found lots of cool info.

>find pebble in house
It's right here!

>find pebble in book
[Runtime error: invalid datatypes for subtraction operator
->rankByVerbStructure.comparePass2({obj:CommandRanking}, {obj:CommandRanking})
/usr/share/frobtads/tads3/lib/adv3/parser.t, line 6061
   {obj:CommandRanking}.compareRanking({obj:CommandRanking})
/usr/share/frobtads/tads3/lib/adv3/parser.t, line 6196
   {anonFunc}({obj:CommandRanking}, {obj:CommandRanking})
/usr/share/frobtads/tads3/lib/adv3/parser.t, line 6132
   {obj:Vector}.sort(true, {anonFunc})
   CommandRanking.sortByRanking([{obj:topicPhrase(main)},
{obj:topicPhrase(main)}, {obj:topicPhrase(misc)}], {obj:TActionTopicResolver})
/usr/share/frobtads/tads3/lib/adv3/parser.t, line 6132

and so on

Uncommenting the fix delivers:

Front Yard
This is a featureless front yard.  A house is to the north.

You see a stone and a rock here.

>e
Inside House
This is the one room house (apparently with no door).  The front yard lies to
the south.

You see a pebble and a big book of knowledge here.

>find pebble
What do you want to search for it?

>house
It's right here!

>find pebble
What do you want to search for it?

>book
Found lots of cool info.

>find pebble in house
It's right here!

>find pebble in book
Found lots of cool info.

>

For grins, note that you can change pebble vocab to peble (ie no longer a 6-letter match for a plural pebbles) and it works fine.

1 Like

Definitely don’t have any light to shed without a deep dive that would be hard to find time for right now…
Small side note, I think you want
You found {it iobj}
?