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>>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.