Forcing disambiguation prompt for action count (in TADS3/adv3)?

If you declare the VerbRule for an TAction via something like:

VerbRule(Draw)
        'draw' singleDobj : DrawAction
        verbPhrase = 'draw/drawing (what)'
;

…then if the player enters the bare verb >DRAW, they’ll get a disambiguation prompt:

>draw
What do you want to draw?

On the other hand if you require a count with the action, via something like:

VerbRule(Draw)
        'draw' singleNumber dobjList : DrawAction
        verbPhrase = 'draw/drawing (what)'
;

…you do not get a prompt for a count, and in fact >DRAW by itself will just give a generic failure message:

>draw
The story doesn't understand that command.

I started to fall down a rabbit-hole of implementing equivalents for askForDobj/replaceAction/Action.retryWithMissingDobj() and so on that work for actions with counts…but before I churn out another couple hundred lines of TADS3 hermeneutics here: is there a simpler way to do this?

It’s trivial to check if a count has been given in dobjFor([action]) { verify() {} } and complain appropriately. But if you do something like illogical('How many [whatever] do {you/he} want to [whatever]?') you won’t get normal dismbiguation behavior. I.e., if the player responds to the message by typing a count that’ll just produce another error, instead of actually attempting to re-resolve the action with the additional input (the way a real disambiguation resoponse would).

To be clear, what I want is something like:

> draw
What do you want to draw?

> cards
How many cards do you want to draw?

> 10
You draw ten cards.

>

…or something like that.

1 Like

My suspicion is that you’d need to somewhat copy what a TAction does when it has an empty dobjMatch. I don’t know where that gets triggered exactly; but if you use the grammar 'draw' singleNumber dobjList and you enter >draw 3 the parser will again ask What do you want to draw? instead of “I don’t understand” because the grammar has been satisfied except for the empty dobjMatch slot, which I believe TActions are set up to cope with in a more detailed way than other grammars are out of the box…

1 Like

Right, that’s what I’m talking about with using equivalents to askForDobj, replaceAction, and Action.retryWithMissingDobj().

Although looking more at that I think there’s going to be difficulty with the way the underlying grammar is implemented in adv3, in that it ultimately is relying on the parser to do the prompting based on the native adv3 grammar rules, and the way the compoundNounPhrase is written I don’t think there’s any straightforward way to force disambiguation through just adding additional grammatical rules (because there’s already a tagged production that includes a noun phrase with a number).

So now I’m leaning toward “just” implementing a little mini-parser, kinda like tryAskingForObject(), but not trying to invoke it automagically (in normal parser operation) but rely on a check in dobjFor([action) { action() {} } that manually calls it if gAction.numMatch isn’t set.

1 Like

Sounds like you already have a plan, but in case you didn’t already do the dig, and are academically interested, I believe the explanation (of why “draw” asks what you want to draw, until you add the singleNumber slot into the grammar) is in line 5415 of en_us. singleNoun can match (with badness) nothing at all, in which case it inherits from EmptyNounPhraseProd. EmptyNounPhraseProd will call askMissingObject in its resolveNouns routine (line 4076 of parser.t), so I expect you could make something like an EmptyNumberPhraseProd (and a singleNumber(empty) grammar) that mimics the resolveNouns process of EmptyNounPhraseProd, except the response prod/asker are tailored to the number situation.

Yeah.

And the problem with “cleanly” adding in a numerical disambiguation prompt in the same way is that there isn’t an existing mechanism like this to prompt for quantity, and there is a production (qualifiedPluralNounPhrase(anyNum)) that matches a noun phrase with a number in front of it.

I think the solution I’m going to go with is something like:

modify playerMessages
        askMissingCount(actor, action, which) {
                reportQuestion('<.parser>\^How many do you want '
                        + (actor.referralPerson == ThirdPerson
                                ? actor.theName : '')
                        + ' to '
                        + action.getQuestionInf(which) + '?<./parser> ');
        }
;

_requireCount() {
        if(!gAction.numMatch || (gAction.numMatch.getval == 0)) {
                gActor.getParserMessageObj().askMissingCount(gActor, gAction,
                        nil);
                tryAskingForCount();
        }
}

tryAskingForCount() {
        local n, str;

        str = readMainCommandTokens(rmcAskObject);

        if(gTranscript)
                gTranscript.activate();

        if(str == nil)
                throw new ReplacementCommandStringException(nil, nil, nil);

        str = str[1];

        if(rexMatch('^<space>*(<Digit>+)<space>*$', str) == nil)
                throw new ReplacementCommandStringException(str, nil, nil);

        n = toInteger(rexGroup(1)[3]);

        gAction.retryWithMissingCount(gAction, n);
}

…with a header file containing…

#define requireCount (_requireCount())

This lets you force a count in an object’s dobjFor([action]) { action() {} } stanza by just inserting requireCount;:

        dobjFor(Draw) {
                verify() {}
                action() {
                        local n;

                        requireCount;

                        n = gAction.numMatch.getval();
                        defaultReport('{You/He} draw{s} <<spellInt(n)>>
                                card<<((n == 1) ? '' : 's')>>. ');
                }
        }

That gets you:

Void
This is a featureless void.

You see a deck of cards here.

>draw cards
How many do you want to draw?

>10
You draw ten cards.

>draw cards
How many do you want to draw?

>x cards
It's a deck of playing cards.

That is, presenting the player with a disambiguation prompt and getting its input, and if the input is a number re-running the command with that as the count. Otherwise (if the input isn’t a number) throwing a ReplacementCommandStringException and handling it as a new command.

I also put together a few other bits and pieces to make this kind of thing a little easier to work with (like a replaceAction() for actions with counts) and so I’ll put it up as a module when I get the chance to take a pass at documentation.

1 Like

Just a random bump here.

This turns out to be one of those things where it’s fairly easy to get to a 90% solution, but handling all of the random corner cases makes things substantially more complicated.

In this case the labyrinth I’m stuck in is handling corner cases in parsing the response to the disambiguation prompt.

For example, if we define a >FOOZLE command that takes a number and a object/object list:

> foozle 10 pebbles
You foozle ten pebbles.

The solution described above works for this, although that solution requires defining both a version of the Action with a count and a version without a count, with the version without mapping itself to the version with. This is specifically to handle:

>foozle
What do you want to foozle?

Without a “no count” version of the action, it’ll instead give you:

>foozle
The story doesn't understand that command.

To get around this, we can define a different production for the number phrase. By default adv3 provides singleNumber, which uses numberPhrase. The only action in adv3 that uses singleNumber appears to be >FOOTNOTE, although a couple of the plural noun phrase productions use numberPhrase.

In any case, there’s no empty number phrase production. Empty productions are used for most of the noun phrase rules, the basic idea being that you create a production that matches nothing, give it a strategic amount of badness, and then if the player enters a command that matches some rule except for one missing bit, it’ll match the “empty” version of the phrase, and you stick the logic for prompting for the missing stuff in the production.

So we can add something like:

grammar literalCount(empty): [badness 400] : EmptyLiteralPhraseWithCountProd
        resolveLiteral(results) {}
;

grammar literalCount(digits): tokInt->num_ : NumberProd
        getval() { return(toInteger(num_)); }
        getStrVal() { return(num_); }
;

grammar literalCount(spelled): spelledNumber->num_ : NumberProd
        getval() { return num_.getval(); }
;

class EmptyLiteralPhraseWithCountProd: EmptyLiteralPhraseProd;

The (empty) production is for when the player doesn’t specify a number, the (digits) for when they use a number, and (spelled) for when they spell out a number.

Adding a macro to refer to a number match:

#define nounCount literalCount->numMatch

…and then defining our VerbRule for >FOOZLE like:

VerbRule(Foozle)
        'foozle' nounCount singleDobj
        : FoozleAction
        verbPhrase = 'foozle/foozling (what)'
;

…that gets us…

>foozle
What do you want to foozle?

>pebble
How many pebbles do you want to foozle?

>10
You foozle ten pebbles.

So far so good. But:

>foozle
What do you want to foozle?

>10 pebbles
You don't see that many pebbles here.

>foozle
What do you want to foozle?

>1 pebble
How many pebbles do you want to foozle?

Which is the current mood.

2 Likes

I can’t tell you the times TADS has put me in a 1-pebble mood. :confused:

2 Likes

Can you fix that by customizing the responseProd on your new grammar?

1 Like

Or I think the Actions that use the nounCount grammar can also supply the responseProd…

Eventually I want to…or I currently think I’ll eventually want to…handle all of this via something like the “normal” disambiguation mechanism(s), which is why I’m using a bespoke production class (EmptyLiteralPhraseWithCountProd) in the example above despite it not currently doing anything.

The problem is that the stuff I’ve added (the literalCount grammar rules) only prevent parsing from failing when the input is something like >FOOZLE. But none of those productions are the one that >FOOZLE will actually resolve to, which is definiteConj, which just pings resolveNouns() on the resolved Action.

This could be handled by tweaking Foozle.resolveNouns(), but then it’s not a general solution (which is what I want).

I had thought I was working toward implementing something where you’d declare VerbRule using a new production like countedDobj instead of singleNumber singleDobj. But I don’t think that can ever work without either requiring a lot of per-action customization or major tweaking to the stock adv3 parser/grammar.

I’m not thinking that what this needs is a separate Action subclass, like TAction and TIAction, but for actions requiring a count.

1 Like

Okay, I think I’ve got what I want. Here’s a repo: requireCount github repo.

I’ll go over the crunchy internals in a bit, but first a summary of the intended use cases and the basic usage.

Use Cases

There are two basic different-but-related ideas I’m working with here:

  • A “dispenser” type thing, where the player will be giving a command with an object count and the count does not correspond to the number of available in-game simulation objects. In this case my design test case is a deck of cards, where the individual cards aren’t modelled as separate in-game objects. So there’s a single Unthing to handle the vocabulary, but when the player tries to >DRAW 10 CARDS from the deck we don’t care if there are 10 in-game objects that match the vocabulary in scope.
    Below I’ll use the >DRAW action for this use case.

  • “Regular” objects where we do want to make sure that the count corresponds to the number of available simulation objects in scope.
    I’ll use the nonsense >FOOZLE action for examples for this use case.

Usage

The module provides a new TAction subclass, TActionWithCount. It also provides macros for a couple of new grammatical productions singleDobjWithCount (which works like singleDobj) and dobjListWithCount (which works like dobjList).

First we’ll declare a pair of new actions, one for each usage case discussed above:

DefineTActionWithCount(Draw);
VerbRule(Draw)
        'draw' singleDobjWithCount
        : DrawAction
        verbPhrase = 'draw/drawing (what)'
;

DefineTActionWithCount(Foozle);
VerbRule(Foozle)
        'foozle' dobjListWithCount
        : FoozleAction
        verbPhrase = 'foozle/foozling (what)'
        requireRealCount = true
;

In the first example the production used for the direct object(s) is singleDobjWithCount. This is generally what you want for the first use case—where you’re not expecting there to be enough simulation objects to match the count. More specifically, “cards” will match one object, so the direct object list will end up being a single object rather than a list of objects.

The opposite is true for >FOOZLE. There we use dobjListWithCount because something like >FOOZLE 3 PEBBLES will yield a direct object list instead of a single direct object because there are multiple simulation objects that match the noun phrase in the command.

Also note that the VerbRule for Foozle has the property requireRealCount = true. This separately will require that the there are enough simulation objects to match the count.

Having declared the actions, we can then just use the requiredCount macro in an action() stanza of an object’s dobjFor() handler:

class CardsUnthing: Unthing '(playing) card*cards' 'card'
        notHereMsg = 'The only thing you can do with the cards is
                <b>&gt;DRAW</b> them. '

        dobjFor(Draw) {
                verify() { dangerous; }
                action() {
                        requireCount;
                        defaultReport(&okayDrawCards, gActionCount);
                }
        }
;

The gActionCount macro returns the action’s count. Calling it after using requireCount guarantees that the value will be non-nil.

Note that the only checking of the count that requireCount does is:

  • That it exists
  • If requireRealCount is true, then the count must be equal to or less than the number of matching objects in scope

Additional Caveat

By default TActionWithCount.truncateDobjList is true. This means that instances of TActionWithCount will by default truncate the direct object list to a single object.

The assumption here is that it will generally be used with indistinguishable objects and that it’s more convenient to run the command once, instead of running it on each matching object. That is, that you generally want >FOOZLE 3 PEBBLES to look like:

>FOOZLE 3 PEBBLES
You foozle three pebbles.

…instead of…

>FOOZLE 3 PEBBLES
pebble: You foozle the pebble.
pebble: You foozle the pebble.
pebble: You foozle the pebble.

You can fix this by twiddle the transcript, but my assumption is that generally if you’re fiddling around with an action with a count it’s usually easier to handle it as a single action with a count instead of count-many individual actions.

If this isn’t true you can set truncateDobjList = nil in the action/verb phrase declaration and it’ll work the other way.

TECHNICAL DETAILS

I’ve covered some of the details, but putting it all together (and including the nomenclature as used in the module).

First, there are a number of new grammar rules:

  • nounCount — a set of productions matching a number expressed in digits, a number expressed in words, or an empty expression
  • _nounListWithCount — a set of productions matching a number followed by a noun list phrase (or an empty noun phrase)
  • _nounWithCount — a set of productions matching a number followed by a single noun (or an empty noun phrase)

The first is used in all the other grammar rules; the second is used for dobjListWithCount (and disambiguation prompts), and the last is used for singleDobjWithCount.

Next there’s the TActionWithCount class. To summarize the properties discussed above:

  • requireRealCount = nil — if true the count associated with the action must be matched by an equivalent number of matching objects in scope
  • truncateDobjList = true — if true the direct object list will be truncated to a single matching object
  • savedDobjList — regardless of whether or not the truncateDobjList is set, savedDobjList will always preserve the full original dobj list as generated by the parser
  • askDobjResponseProd = _nounListWithCount — the grammatical production used for parsing the response to a missing noun prompt. That is, if the player enters >FOOZLE and the game asks “What do you want to foozle?”, this is the grammatical production that will be used to evaluate the response to see if it looks like a noun phrase.

Additionally TActionWithCount.resolveNouns() does a certain amount of gymnastics to try to make sure any matching numerical count is remembered correctly. The reason this is fiddly is because there are multiple paths to getting a complete command phrase (player enters complete command; player enteres a verb without a noun; player enters a verb and noun but no count) and they all produce slightly different environments.

That’s all on the parsing end. The actual check happens in an action() stanza when the requireCount macro is called.

It calls a global function. The function checks to see if it can figure out a count for the current action. If it can, command execution continues normally.

If there’s currently no count associated with the action, it’ll present a prompt (“How many…”) and then attempt to parse the input. There are a few broad cases it checks for:

  • A number expressed as digits (“10”)
  • A number expressed as words (“three”)
  • A noun phrase (“three pebbles”)
  • None of the above (“north”)

The first two cases are handled via a new Action.retryWithMissingCount()method.

The third uses a little mini-parser (sorta like what’s used in tryAskingForObject() in stock adv3) to tokenize and parse the results in the assumption it’s a valid noun phrase. If parsing is successful it tries to replace the current command with the new command. If not, it returns and processing continues.

If none of that works, the input is treated as a new command.

That’s basically it. There are actually a few nasty corner cases in there that are mentioned in the comments in the code but I won’t bother trying to cover in detail here. Example: If you go through a three-step disambiguation process (>FOOZLE, “What do you want to foozle?”, >PEBBLES, “How many pebbles do you want to foozle?”, “>10”) and you have requireRealCount defined on the action you also have to build a new command because the parser will return a one-object dobj list after the first disambig step (“What do you want to foozle?”, >PEBBLES), so you have to re-evaluate the noun phrase with the count in order to get the “real” dobj list.

There’s a lot of that kind of thing.

Anyway, I think everything is doing what I want now:

Void                                                                        0/0
Exits: None
Void
This is a featureless void.

You see a deck of cards, three pebbles, and three rocks here.

>draw 2 cards
You draw two cards.

>draw
What do you want to draw?

>cards
How many cards do you want to draw?

>2
You draw two cards.

>draw
What do you want to draw?

>2 cards
You draw two cards.

>foozle 3 pebbles
You foozle three pebbles.

>foozle 
What do you want to foozle?

>pebbles
How many pebbles do you want to foozle?

>3
You foozle three pebbles.

>foozle
What do you want to foozle?

>3 pebbles
You foozle three pebbles.

>foozle 10 pebbles
You don't see that many pebbles here.

>foozle
What do you want to foozle?

>pebbles
How many pebbles do you want to foozle?

>10
You don't see that many pebbles here.

>foozle
What do you want to foozle?

>10 pebbles
You don't see that many pebbles here.

>draw  
What do you want to draw?

>fifty-two cards
You draw fifty-two cards.

>

This was a lot more work than it seems like it ought to have been.

2 Likes