Somewhat vague TADS3 design pattern question about overlapping vocabulary and ambiguous object announcements

Anyone have a general design pattern for when you have multiple objects that have overlapping vocabulary and, specifically, want specific objects to be the default in individual complex situations?

The specific design case I’m thinking about is with playing cards, where “cards” might refer to the deck of cards (>SHUFFLE CARDS), the player’s hand (>X CARDS when the player is holding them), or might be abstract and not refer to a specific in-game object at all (>PLAY CARDS, which ends up just being a special case Action).

A lot of this can be handled by making vocabLikelihood a method encapsulating a bunch of complex logic.

One of the solutions I was toying with was putting problematic vocabulary like this on an Unthing, not putting it on other in-game objects, and having the Unthing remapping the action to the appropriate object based on context. This feels very much like a kludge, however.

One of the problems with doing it with overlapping vocabulary on in-game objects is that this produces ambiguous object announcements (the response to >X CARDS starting out with (your hand) if the player is holding cards and (deck of cards) if they’re not and so the deck handles the action, for example). And I don’t think there’s any way to block announceAmbigActionObject(), like via something like tryImplicitActionMsg() and using &silentImplicitAction, like you would with an implicit action.

Basically I’ve got some stuff that more or less works for this, but it feels like an elaborate mess of special cases for something that feels like it ought to be a fairly common problem in IF. So I’m just wondering if I’m missing any obvious/simpler approaches.

3 Likes

mhm… around a green table, I will have added an isDealer boolean, and handling accordingly the relevant dobjFor, because there’s specific verbs to be created (not only SHUFFLE, but also DRAW and DEAL), I suppose that is a valid approach to the problem.
Together with appropriase use of owner property and like adv3 properties, I feel that is the best approach to the not-much-theorical problem of handling cards, hands of cards and stacks of cards.

Best regards from Italy,
dott. Piergiorgio.

I suspect my cases are a bit simpler than what you are going for, but for my cases, I use a combination of variable-controlled logicalRank and the message suppression method I stole from you or JZ:

// Suppress disambiguation message,
// via per-obj suppressAnnounceAmbig property
//
modify libMessages
    announceAmbigActionObject(obj, whichObj, action) {
        if (obj.suppressAnnounceAmbig) return '';
        else return inherited(obj,whichObj,action);
	}
    announceDefaultObject(obj, whichObj, action, resolvedAllObjects) { 
    	if (obj.suppressAnnounceAmbig) return '';
        else return inherited(obj,whichObj,action,resolvedAllObjects);
	}
;
// Then in thing defs
//
sharedVocabObj : Thing 'sharedadj sharednoun' 'common thing'
    suppressAnnounceAmbig = true
    dobjFor(SomeVerb) {
        verify() { logicalRank(thisRank, 'maybeThisOne'); }
    } 
;
// and of course
modify Thing
    suppressAnnounceAmbig = nil  
    thisRank = 100 // programmatically alter based on context
;

Every now and then I get into some weird logicalRank debug, but so far this works for the cases I need it to. This probably just moves your complicated code to thisRank.

UPDATE: It was JZ.

3 Likes

Yeah, I’ve prototyped-out a couple of other models, including:

  • Having card playing be entirely modal. That is, once the player starts playing a game, the only actions available are the card game verbs, and everything else gets a “not while you’re playing a game” failure message. And conversely the card playing verbs are only valid in the game, getting a generic “you’re not playing a game” failure message in other contexts.
  • Have an object for the card game and a state property on it which is another object, and then have all the card-playing actions call methods on the state object. So >SHUFFLE is handled via something like gCardGame.state.shuffle(). The state changes to reflect the game round, and the generic CardGameState class has placeholder failure messages on all the methods ("{You/He} can't shuffle the cards, {you/he} {are}n't the dealer." and so on). Then on the individual non-abstract state objects one or more methods specific to that state actually do something—cardGameShuffleState.shuffle() actually shuffles the cards and displays an appropriate message, for example.

I think vocabulary stuff ends up still being an issue anyway, because I still want to be able to handle all of the actions in a non-awkward way even outside of a game.

That is, I don’t want the deck of cards to suddenly become a non-responsive brick outside of a game context (“You can’t shuffle the cards because you’re not playing a game,” or whatever). Although that would probably be the most efficient way to implement things (or just whisking the cards away and not letting the player fiddle with them outside of the context of a card game).

But I want to allow the player to mess around with the cards outside of card games for other in-game reasons.

1 Like

Yeah, that’s kinda like what I’ve been doing, except instead of using logicalRank I was mostly stuffing the elaborate gymnastics in vocabLikelihood, which has the same effect. Or at least I’m pretty sure it’s the same effect…I think logicalRank and vocabLikelihood values both produce what the parser considers “unclear” disambiguation results. But I haven’t done a lot of experimenting with the ambigAnnounceMode to know for sure.

I’m also using a macro to set a flag on the gAction from inside dobjFor() { action() {} }, so it’s per-action-per-object, not just per-object (and is slightly more fiddly as a result).

And like I said this works. But it feels kinda like a giant mess for something that feels like…I’m not sure what the right word for it is, but it’s one of those problems where you look at it and think “yeah, I’m not the first person to have this problem, or the hundredth” and so it just kinda feels like there should be a known solution/approach/whatever to the problem.

Like if you encountered Towers of Hanoi for the first time and had no idea what it was but you were familiar with logic problems in the abstract, you’d probably immediately think yeah, there’s probably a name for this, and a standard approach to solving it. If that makes sense.

1 Like

Another related “am I missing something” question:

Is there a straightforward way of remapping an intransitive verb to a transitive verb, immediately prompting for a missing object?

As a concrete example, say we have both an intransitive >SHUFFLE action, and a transitive >SHUFFLE [object] action. In a game, >SHUFFLE gets handled directly by the card game logic. If a game isn’t in progress, it should produce a "What do you want to shuffle?" prompt/missing object handler.

It’s easy enough to just show the prompt, but there’s no (as far as I know) easy/direct way to use askMissingObject()/tryAskingForObject() in a replaceAction-like mechanism.

Having recently done something similar to this for the requireCount module I know how to roll my own prompt/mini-parser/action replacer. But I’m just wondering if I’m missing a simpler/less heavyweight approach.

(One alternative is to use an omnipresent Unthing that will handle the action if nothing else does and stick the logic in there to fake the intransitive behavior, but I’d rather not have to rely on a kludge like that).

To give an example of a solution(-ish) to the I-to-T action remapping thing:

First, modify Action to implement a createForRetry() workalike. We can’t use createForRetry() itself because we’re actually swapping underlying Action instances/classes, instead of cloning one action as a template for the replacement:

modify Action
        // Analog for createForRetry(), only instead of just copying the
        // original action (first arg) we copy things from the original
        // action to an instance of the given action class (second arg).
        createForReplace(orig, cls) {
                local action;

                action = cls.createActionInstance();
                action.tokenList = orig.tokenList;
                action.firstTokenIndex = orig.firstTokenIndex;
                action.lastTokenIndex = orig.lastTokenIndex;

                action.includeInUndo = nil;
                action.setNested();
                action.implicitMsg = nil;

                return(action);
        }
;

Then we create a global function kinda like _replaceAction():

// Args are the issuing actor (probably gIssuingActor), the actor taking
// the action (probably gActor), and the class for the new action.
_remapToTAction(srcActor, dstActor, cls) {
        local action, matchList, str, toks;

        // Use our new Action method to create the replacement action.
        action = gAction.createForReplace(gAction, cls);

        // Display the "What do you want to [action]?" prompt.
        dstActor.getParserMessageObj().askMissingObject(dstActor, action,
                DirectObject);

        // Read input.
        str = readMainCommandTokens(rmcAskObject);

        if(gTranscript)
                gTranscript.activate();

        // Got an empty-ish input, bail.
        if(str == nil)
                throw new ReplacementCommandStringException(nil, nil, nil);

        toks = str[2];          // The input tokens.
        str = str[1];           // The input string.

        // See if the input looks like a noun phrase.
        matchList = nounList.parseTokens(toks, cmdDict);

        // Nope, treat it like a new command.
        if(!matchList.length)
                throw new ReplacementCommandStringException(str, nil, nil);

        // The input looks like a noun phrase, so we'll try tacking it onto
        // the end of the original command and re-executing.  The heavy
        // wizarding in this line is just the ritual for summoning the
        // original command text (by getting the original token list,
        // converting it back into text form, adding the input string
        // obtained above, then re-tokenizing the resulting string).
        executeCommand(dstActor, srcActor, Tokenizer.tokenize(
                cmdTokenizer.buildOrigText(gAction.getPredicate()
                .getOrigTokenList()) + ' ' + str), true);

        // To avoid having this action double counted (advancing the
        // turn counter and so on for both the original command and the
        // command executed above) we replace the original command with
        // a zero-length action.
        replaceAction(_Blank);
}

This uses a NOP Action that we also have to define:

DefineIAction(_Blank) actionTime = 0 execAction() {};

And then we add a macro that works like replaceAction:

#define remapToTAction(cls) (_remapToTAction(gIssuingActor, gActor, cls##Action))

Then if we have a transitive/intransitive action pair we can handle the remapping in the IAction’s execAction() via:

DefineTAction(Shuffle);
VerbRule(Shuffle)
        'shuffle' singleDobj
        : ShuffleAction
        verbPhrase = 'shuffle/shuffling (what)'
;

DefineIAction(ShuffleI);
VerbRule(ShuffleI)
        'shuffle'
        : ShuffleIAction
        verbPhrase = 'shuffle/shuffling'
        execAction() {
                remapToTAction(Shuffle);
        }
;

In practice I want additional logic in ShuffleIAction.execAction(), but the fallback of mapping to ShuffleAction would work exactly the same way.

This sounds to me like what you mean…

DefineIAction(ShuffleI)
   execAction {
        // if this then that
        else askForDobj(Shuffle); }
;
3 Likes

Ah, right. Thanks. I thought there was something like this but couldn’t find it.

Knowing the right keyword it’s easy enough to look up, and in fact discover I’d asked this before and had it explained to me (the previous time by @Eric_Eve ).

2 Likes