Handling action with count via CollectiveGroup

I’m trying to use a CollectiveGroup to handle an action with a count and I’m obviously missing something.

Below is a simple but complete (as in compile-able) example. In it the player gets three pebbles. The game defines the new verb “stack”.

What I want, in this example, is for the player to be able to do something like “stack 3 pebbles”. The player has three pebbles, but the game reports “You don’t see that many pebbles here.” I assume that’s because the CollectiveGroup is handling the action (which I want), but then because there’s only one CollectiveGroup it throws an error because the command has a count.

Without the CollectiveGroup it works, but produces a line of output for each individual pebble, which I don’t want: I want to be able to handle the action in the CollectiveGroup and display a summary of stacking however many pebbles in total, rather than displaying a message for each individual one added to the stack.

Of course the game logic in the example doesn’t actually do anything, I’m just trying to figure out the parsing problem here.

I assume I’m missing something obvious here, but I haven’t been able to figure it out from the documentation.

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

DefineTAction(Stack);
VerbRule(Stack) 'stack' dobjList : StackAction
        verbPhrase = 'stack/stacking (what)'
;
modify Thing
        dobjFor(Stack) {
                verify() {
                        illogical('{You/he} can\'t stack {that dobj/him}. ');
                }
        }
;

pebble: Thing 'round pebble*pebbles' 'round pebble'
        "A round pebble. "
        isEquivalent = true

        collectiveGroups = [ pebbleCollective ]
        listWith = [ pebbleList ]
        dobjFor(Stack) {
                verify() {}
                action() { say('Individual stack. '); }
        }
;
pebbleList: ListGroupEquivalent
;
pebbleCollective: CollectiveGroup 'bunch pebble*pebbles' 'bunch of pebbles'
        "A bunch of pebbles. "
        isPlural = true
        isCollectiveAction(action, whichObj) { return(true); }
        isCollectiveQuant(np, reqNum) {
                return((reqNum == nil) || (reqNum > 1));
        }
        dobjFor(Stack) {
                verify() {}
                action() { say('Collective stack. '); }
        }
;

startRoom:      Room 'Featureless Void'
        "This is a room, more or less. "
;

me:     Actor
        location = startRoom
;
+ pebble;
+ pebble;
+ pebble;

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
;
1 Like

I looked into this and got pretty turned around by the inner workings of CollectiveGroup and the parser disambiguation code. I don’t have a definitive answer, but I would recommend looking at this example in the TADS 3 Tour Guide as well as this conversation from a while back.

In particular, from that conversation @bcressey says:

I believe isCollectiveQuant is meant to separate input like >look at the pen from >look at the pens. In the first case the player probably wants to examine just one pen, and it’s appropriate to ask which one. In the second case the player wants to see them all, and it’s better to show the collective description.

In other words, it’s better at dealing with the simpler problem of disambiguating actions for one or the collective, i.e., >stack pebble vs. >stack pebbles.

Every example I’ve seen for CollectiveGroup deals with situations where each individual in the collective can be disambiguated (“red shoes,” “flip-flops,” etc.) In your example, I can see the parser parsing the count (>stack 3 pebbles and >stack three pebbles both resolve to integer 3), but I cannot for the life of me figure out how to get CollectiveGroup to be selected to handle the action in that case.

Possible solution to my problem, although this feels somewhat kludgy: overwrite the default filterResolveList() for the CollectiveGroup, and in it manually twiddle the quantities:

pebbleCollective: CollectiveGroup 'bunch pebble*pebbles' 'bunch of pebbles'
        "A bunch of pebbles. "

        isPlural = true
        // to keep track of how we twiddled the noun resolution list
        countKludge = nil

        isCollectiveAction(action, whichObj) { return(true); }
        isCollectiveQuant(np, reqNum) {
                return((reqNum == nil) || (reqNum > 1));
        }
        dobjFor(Stack) {
                verify() {}
                action() {
                        say('Collective stack of <<self.countKludge>> pebbles. ');
                        // reset our kludge counter
                        self.countKludge = nil;
                }
        }
        filterResolveList(lst, action, whichObj, np, reqNum) {
                local len, vis;

                self.countKludge = nil;

                if(isCollectiveQuant(np, reqNum)
                        && isCollectiveAction(action, whichObj))
                {
                        lst = lst.subset({x: !x.obj_.hasCollectiveGroup(self)});

                        // figure out how many widgets we have access to
                        // in the collective
                        vis = self.getVisibleIndividuals(gActor.visibleInfoTable());
                        len = vis.length();
                        // only twiddle the none resolution list if we have
                        // enough widgets to handle the action--maybe a
                        // misfeature, 
                        if(reqNum <= len) {
                                // THIS IS THE KLUDGE
                                lst.forEach(function(o) {
                                        if(o.obj_ == self) {
                                                o.quant_ = reqNum;
                                        }
                                });
                                // record our misdeads for later reference
                                self.countKludge = reqNum;
                        }
                } else if(lst.indexWhich({x: x.obj_.hasCollectiveGroup(self)}) != nil) {
                        lst = lst.removeElementAt(lst.indexWhich({x: x.obj_ == self}));
                }
                return(lst);
        }
;

Most of the filterResolveList() method above is just copy & pasted from the stock version supplied by the CollectiveGroup class in objects.t. This just counts how many visible objects we have in the group, and if that’s less than or equal to the requested number, we just scribble that number into the .quant_ property of the group’s entry in the noun resolver list. It also saves a copy of the quantity for use in the CollectiveGroup’s action handler, I don’t know if there’s a more elegant way to get that information natively in dobjFor().

Anyway, this appears to work: “stack 1 pebble” is handled by the Thing-pebble, “stack 3 pebbles” is handled by the CollectiveGroup-pebbles, and "stack 4 pebbles chucks a wobbly.

This does absolutely feel like fighting the parser, though, and I don’t know whether or not this could have dire consequences in some set of circumstances I haven’t considered or tested yet.

I dug around in filterResolveList(), but obviously you were more successful than I was in getting results.

Does your kludge work with >stack pebbles? I recall being able to make that work, just not >stack 3 (or 2) pebbles

I would worry about this too. For example, if the player is holding two pebbles and one is on the floor, or if a box of Fruity Pebbles is on the kitchen table, etc.

Also, I forgot to mention: You might add a preCond = [ touchObj ] to your Stack handlers. It made no difference in my testing, but that might be important in your game as a whole.

If you come up with a better solution, I would certainly like to hear about it.

No, >stack pebbles throws a parser error, it being one of the set of circumstances I hadn’t considered yet.

Here’s a slightly more robust solution:

        // kludge to have CollectiveGroup handle an action with a count
        filterResolveListCount(lst, reqNum) {
                local len, vis;


                // figure out how many widgets we have access to
                // in the collective
                vis = self.getVisibleIndividuals(gActor.visibleInfoTable());
                if(vis == nil)
                        return;

                // only count widgets that are in the inventory of whoever
                // is trying to mess with them
                vis = vis.subset(function(o) {
                        if(o.getCarryingActor() == gActor)
                                return(true);
                        return(nil);
                });
                if(vis == nil)
                        return;
                len = vis.length();

                // if we don't have a count, use the entire group
                // maybe a misfeature?
                if(reqNum == nil)
                        reqNum = len;

                // only twiddle the noun resolution list if we have
                // enough widgets to handle the action--maybe a
                // misfeature...if we DON'T do this then it gets caught
                // by insufficientQuantity() for the default response,
                // but maybe the CollectiveGroup should handle it as a
                // special case
                if(reqNum <= len) {
                        lst.forEach(function(o) {
                                if(o.obj_ == self) {
                                        o.quant_ = reqNum;
                                }
                        });
                        // record our misdeads for later reference
                        self.countKludge = reqNum;
                }
        }
        filterResolveList(lst, action, whichObj, np, reqNum) {
                self.countKludge = nil;

                if(isCollectiveQuant(np, reqNum)
                        && isCollectiveAction(action, whichObj))
                {
                        lst = lst.subset({x: !x.obj_.hasCollectiveGroup(self)});
                        // do count twiddling for the CollectiveGroup
                        self.filterResolveListCount(lst, reqNum);

                } else if(lst.indexWhich({x: x.obj_.hasCollectiveGroup(self)}) != nil) {
                        lst = lst.removeElementAt(lst.indexWhich({x: x.obj_ == self}));
                }
                return(lst);
        }

This does our suitability and count checking in a new method, filterResolveListCount() which we call from filterResolveList(). It uses getVisibleIndividuals() to get a list of candidate objects, and then removes all of the ones that are not carried by the actor doing the action.

If no count is in the action, it uses the full number of matching objects in the actor’s inventory. Maybe this is a misfeature, but I’d be more worried about it if it was going in the library instead of an individual object’s behaviour.

The main “weirdness” is that this maybe means that the CollectiveGroup should also now handle the case where the requested count is less than the number available (>stack 4 pebbles when you only have three) because now you might have a situation where the player says to stack some number of pebbles and there are that number in the room…some in inventory and some on the ground, for example, but it still gets handled by insufficientQuantity() and therefore gets the stock “You don’t see that many pebbles here” response instead of something more natural.

I think that’s beyond the scope of my original problem, though, which I believe is now, happily, more or less resolved. With the remaining caveat that it really feels like this is a convoluted wrestling match with the parser instead of a neat solution.

1 Like

Another unforeseen problem: the above breaks the basic CollectiveGroup behaviour: >x pebbles returns “You cannot see that.”

This is because getVisibleIndividuals() changes the visibility table. Specifically, it removes all objects that are not part of the CollectiveGroup. Which…the CollectiveGroup itself is not.

Here’s an updated version of the filterResolveListCount() method that non-destructively walks through the visibility table to get a count of the visible items in the group which are currently in the actor’s possession:

        // kludge to have CollectiveGroup handle an action with a count
        filterResolveListCount(lst, reqNum) {
                local len, vis;


                // figure out how many widgets we have access to
                // in the collective
                vis = gActor.visibleInfoTable();
                if(vis == nil)
                        return;

                // count the number of visible items in the group
                // that are currently held by the actor doing the action
                len = 0;
                vis.forEachAssoc(function(key, val) {
                        if(key.hasCollectiveGroup(self)
                                && (key.getCarryingActor() == gActor)) {
                                len += 1;
                        }
                });
                        
                // if we don't have a count, use the entire group
                // maybe a misfeature?
                if(reqNum == nil)
                        reqNum = len;

                // only twiddle the noun resolution list if we have
                // enough widgets to handle the action--maybe a
                // misfeature...if we DON'T do this then it gets caught
                // by insufficientQuantity() for the default response,
                // but maybe the CollectiveGroup should handle it as a
                // special case
                if(reqNum <= len) {
                        lst.forEach(function(o) {
                                if(o.obj_ == self) {
                                        o.quant_ = reqNum;
                                }
                        });
                        // record our misdeads for later reference
                        self.countKludge = reqNum;
                }
        }

With this >x pebble, >x pebbles, >stack 1 pebble, >stack 3 pebbles, and >stack 4 pebbles all have the expected/desired behaviour.

But it also makes me more worried that twiddling with the noun resolution list might have other unforeseen consequences.

1 Like

I’m impressed with the depth of fiddling you’ve got going on there with the parser. I’m wondering though, if CollectiveGroup isn’t so much meant to handle numbered situations as it is for handling all of a given group at once. Is it possible what you’re really looking for is just combining reports, rather than handling the action with a collective group? They have extensions for that, along with a chapter in the Tech Manual. In other words, “stack 3 pebbles” will fire a ‘stack pebble’ action for each pebble in turn, but the transcript will report "You stack three pebbles. "
I don’t know what the full handling of your situation will entail, though, so maybe you need more than that…

Twiddling the report makes sense if the aggregate action is always identical to the sum of the individual actions (so >stack 3 pebbles and repeating >stack pebble three times are identical except for the formatting of the output). But not if the action itself wants to be handled differently.

Or at least I think so. All of the examples in “Manipulating the Transcript” in the tech manual, for example, depend on something like calling gAction.callAfterActionMain() and passing it a function to twiddle the report(s), which is predicated on the individual actions being completed individually.

So if we’re, say, stacking pebbles on a scale and all we care about whether or not the total weight matches some magic number, then the report-twiddling approach works: the behaviour only depends on how many pebbles are on the scale at any given moment in time, regardless of whether they’re added one at a time or by the dozen.

On the other hand if we’ve got, I dunno, a pile of magic pebbles, and if we add a prime number of pebbles it morphs the stack into a new form and if we add a non-prime number of pebbles it resets, this can’t really be turned into “just” a reporting problem without a bunch of juggling. Or at least not as I understand it.

In terms of code organization it also feels more natural (or at least to me) to store the logic for handling the behaviour of groups of items in CollectiveGroup (or something like it) instead of kinda dispersed throughout a bunch of handlers in the individual item. But that’s just general programming prejudice, I don’t know how well it dovetails with tads programming best practices specifically.

No, you’re right… I simply wasn’t sure if you needed special action handling for a counted number of objects or if the cleaner report was all you were going for. It sounds like you have a pretty good handle on TADS already. I’d say the CollectiveGroup class could stand some well-packaged modifications that allow counted handling like you’re going for.

I’ve been working on a large TADS game for almost 3 years now, and I encountered a situation that needed counted collectives like you do, but I handled it radically differently because of its nature. I was trying to deal with large numbers of tiny balls that could be put into different containers, but the game really bogged down with so many objects in scope. So I made invisible ball-group objects in each container (including the PC) that could hold balls, and interfered with the parser to increase or decrease a count property for that group object if balls were added to it or taken from it. Given the awkwardness of the situation, I restricted the balls to that one room and only allowed counted parsing for the necessary verbs like Take/From, PutIn, Drop, etc.
Currently all of the CollectiveGroup objects in my game let the individual Things handled counted actions, because I don’t have any other situations where, like you, the action handling is dependent upon the number of objects referenced.
It does seem like there should be a clean way of handling that with existing classes…

Bumping this thread with yet another special case that’s giving me trouble.

Working from the same basic code as developed above (with the filterResolveList() kludge), here’s an example in which actors other than the player also have pebbles:

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

DefineTAction(Stack);
VerbRule(Stack) 'stack' dobjList : StackAction
        verbPhrase = 'stack/stacking (what)'
;
modify Thing
        dobjFor(Stack) {
                verify() {
                        illogical('{You/he} can\'t stack {that dobj/him}. ');
                }
        }
;

pebble: Thing 'round pebble*pebbles' 'round pebble'
        "A round pebble. "
        isEquivalent = true

        collectiveGroups = [ pebbleCollective ]
        listWith = [ pebbleList ]
        dobjFor(Stack) {
                verify() {}
                action() { say('Individual stack. '); }
        }
        dobjFor(Default) {
                // deprecate interactions with other people's
                // pebbles
                verify() {
                        local a;
                        a = self.getCarryingActor();
                        if((a != nil) && (a != gActor))
                                dangerous;
                }
        }
;
pebbleList: ListGroupEquivalent;
pebbleCollective: CollectiveGroup 'bunch pebble*pebbles' 'bunch of pebbles'
        "A bunch of pebbles. "

        isPlural = true
        // to keep track of how we twiddled the noun resolution list
        countKludge = nil

        isCollectiveAction(action, whichObj) { return(true); }
        isCollectiveQuant(np, reqNum) {
                return((reqNum == nil) || (reqNum > 1));
        }
        dobjFor(Stack) {
                verify() {}
                action() {
                        say('Collective stack of <<self.countKludge>> pebbles. ');
                        // reset our kludge counter
                        self.countKludge = nil;
                }
        }
        // kludge to have CollectiveGroup handle an action with a count
        filterResolveListCount(lst, reqNum) {
                local len, vis;


                // figure out how many widgets we have access to
                // in the collective
                vis = gActor.visibleInfoTable();
                if(vis == nil)
                        return;

                // count the number of visible items in the group
                // that are currently held by the actor doing the action
                len = 0;
                vis.forEachAssoc(function(key, val) {
                        if(key.hasCollectiveGroup(self)
                                && (key.getCarryingActor() == gActor)) {
                                len += 1;
                        }
                });
                        
                // if we don't have a count, use the entire group
                // maybe a misfeature?
                if(reqNum == nil)
                        reqNum = len;

                // only twiddle the noun resolution list if we have
                // enough widgets to handle the action--maybe a
                // misfeature...if we DON'T do this then it gets caught
                // by insufficientQuantity() for the default response,
                // but maybe the CollectiveGroup should handle it as a
                // special case
                if(reqNum <= len) {
                        lst.forEach(function(o) {
                                if(o.obj_ == self) {
                                        o.quant_ = reqNum;
                                }
                        });
                        // record our misdeads for later reference
                        self.countKludge = reqNum;
                }
        }
        filterResolveList(lst, action, whichObj, np, reqNum) {
                self.countKludge = nil;

                if(isCollectiveQuant(np, reqNum)
                        && isCollectiveAction(action, whichObj))
                {
                        lst = lst.subset({x: !x.obj_.hasCollectiveGroup(self)});
                        // do count twiddling for the CollectiveGroup
                        self.filterResolveListCount(lst, reqNum);

                } else if(lst.indexWhich({x: x.obj_.hasCollectiveGroup(self)}) != nil) {
                        lst = lst.removeElementAt(lst.indexWhich({x: x.obj_ == self}));
                }
                return(lst);
        }
;

startRoom:      Room 'Featureless Void'
        "This is a room, more or less. "
;

me:     Actor
        location = startRoom
;
+ pebble;
+ pebble;
+ pebble;

alice:  Actor 'alice' 'Alice'
        "She looks like the first person you'd turn to in a problem. "
        location = startRoom
        isHer = true
        isProperName = true
;
+ pebble;
+ pebble;
+ pebble;
bob:    Actor 'bob' 'Bob'
        "He looks like a Robert, only shorter. "
        location = startRoom
        isHer = true
        isProperName = true
;
+ pebble;
+ pebble;
+ pebble;

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
;

All of the CollectiveGroup behaviours work as expected…except when examining other Actors’ pebbles:

>x pebble
A round pebble.

>x pebbles
A bunch of pebbles.

>x alice's pebbles
round pebble: A round pebble.
round pebble: A round pebble.
round pebble: A round pebble.

Irritating. Doing a little debugging-via-printf it looks like the CollectiveGroup.filterResolveList() is never even called in the last case.

This can be made to work by creating a different flavour of pebble for each Actor, each with its own CollectiveGroup object which then goes in the Actor’s inventory. But then the pebbles aren’t interchangeable–if Alice and Bob give the player their pebbles, then the player will have three separate bunches of pebbles instead of one big bunch.

Am I just thinking about this all fundamentally wrong? Is there some mechanism other than CollectiveGroup that TADS3 provides for this sort of thing? It doesn’t seem like that esoteric a situation to me, but if there’s a straightforward way to implement it cleanly it’s eluding me.

Lol, I have got to learn to search before I plunge into rabbit holes. I discovered this weird artifact while debugging some unexpected CollectiveGroup behavior. I addressed the destructive getVisibleIndividuals() behavior as follows:

modify CollectiveGroup
    isCollectiveQuant(np, requiredNum) {

        // do not use collective on single instances
        //
        if (getVisibleIndividuals(gActor.visibleInfoTable(), true).length() < 2)
            return nil;

        return inherited(np, requiredNum);
    }

    // adding optional 'nondestructive' parameter.  probably should be default,
    // but erring on side of caution.  certainly WIP code sets to true
    //
    getVisibleIndividuals(tab, nondestructive?)  {


        // logic to create sense table copy, to avoid modifying original table - JJMcC
        local loctab;
        
        if (nondestructive) {
            local tabCopy = new LookupTable();
            tab.forEachAssoc(function(key, value) {
                tabCopy[key] = value;
            });
            loctab = tabCopy;
        } else { loctab = tab; }

        // original logic below, using local table instead
        // keep only those items that are individuals of this collective
        loctab.forEachAssoc(function(key, val) {
            // remove this item if it's not an individual of mine
            if (!key.hasCollectiveGroup(self))
                loctab.removeElement(key);
        });

        // return a list of the objects (i.e., the table's keys)
        return loctab.keysToList();
    }
;

Figured I'd go ahead and ask. The original weird behavior I’m debugging is that CollectiveGroup does not come into scope unless 3 or more instances are present. My code requires two, but default code should hit even at one. The exception is if I am holding an item, and a second item is in scope, then CollectiveGroup performs as expected. As far as I can tell (with debug statements). Anyone else go down the path of 'when is CollectiveGroup in scope?' Am presuming at the moment that I need to dive into addToSenseInfoTable(sense, tab) but still debugging…