Collective Group not Collecting

TLDR; "Why is my CollectiveGroup not being matched by the parser, unless PC is holding one? Also, how do I correctly set up a nounPhrase object?"

It is with some shame I report my WIP progress has stalled for over a month, run aground on the rocky shores of CollectiveGroup debug. My situation is I have two series of portable objects that should each be referred to collectively when more than one is present. When an Examine fails, instead of a CollectiveGroup description, I get individual descriptions of the objects as if responding to a plural ala

>x balls
green ball:  A green ball.

blue ball:  How it feels debugging this.

Obviously, what I want is

>x balls
Some well behaved balls colored blue and green.

In one case, if three are present, it seems to work as designed, but with only two does not, unless PC is holding one of them! In the other case, even three are not collected unless one is held.

It is clearly some code interaction in my WIP, as when I tried to construct a reduced example, everything worked as expected (two or more unheld objects collected just fine).

Through laborious debug print statements, I have determined that my CollectiveGroup objects are present me.connectionTable(). I have further determined via print statements in the CollectiveGroup isCollectiveQuant() function that when it fails, the CollectiveGroup filterResolveList() is not invoked, which I interpret to mean action.resolveNouns() does not match the CollectiveGroup for some reason. (But does if PC holds one!) I understand filterResolveList() to be invoked on every object that matches as a possible target for the action, and is in fact how CollectiveGroup does its substitution for its constituents!

My current debug step is to try and use the individual object’s filterResolveList() (which I interpret to be matched, due to the plural descriptions when examined) to explicitly add the CollectiveGroup back to the list, but now I am stymied by not being able to reverse engineer a 'noun phrase' data object. I am trying to

class CollectionResisterObj : Thing 'collection resister/individual' 'thing that resists collection`
    "Maddening bane of my existence.  "
    collectiveGroups = [impotentCollectionGroup]
    filterResolveList(lst, action, whichObj, np, requiredNum) {
        local collectiveRI = new ResolveInfo(
           obj = collectiveGroups[1],
           flags = 0, np = '???');

        // only add it if not already in list
        if (lst.indexWhich( {x: x.obj_.ofKind(collectiveGroups[1]) } ))
            lst += collectiveRI;

        inherited(lst, action, whichObj, np, requiredNum);
    } 

Which fails because I cannot suss out what to set flags and np params to. I tried directly setting the np from the function call, but that did not work. I have been deep in the Technical Manual but have not yet found the answer. Help much appreciated!

FWIW, this thread consulted much earlier, too late as it turned out. Linking because may be useful to future debuggers. Handling action with count via CollectiveGroup

3 Likes

I’d like to have a firmer grasp on the parser than I do, also… but you said you already tried using the argument in the np parameter of filterResolveList to create the new ResolveInfo and it didn’t work?
Certainly very mysterious sounding why the CollectiveGroup functions or doesn’t function based on heldness!

1 Like

Has anyone referenced this example? I can’t quite get it to work in adv3lite. Maybe it will help here.

1 Like

Does your CollectiveGroup have a concrete base class (which)? Do you have 'ball*balls' on the single and '*balls' on the collective?

The first thing that comes to mind as to trying to figure out what’s happening is to tap into a couple methods and do some valToSymbol printing. In debug mode I hacked into filterAmbiguousWithVerify and getSortedVerifyResults to print out their lists of candidates at various points in the process. I also tweaked ResolveInfo and VerifyResultList to show information about themselves when printed with valToSymbol. I’ve figured out some real headscratchers when I can see all of the objects the parser is considering at a given time, in conjunction with their various logical/illogical scores.

2 Likes

I think what’s needed here isn’t a CollectiveGroup, it’s a report manager.

CollectiveGroup seems to be intended for situations where you have actual indistinguishable objects and want something to handle actions directed at more than one of them.

If you want the behavior of something happening individually to however many different objects but the output to be merged in a neat way, then I think you just want to use a report manager instead of a CollectiveGroup.

“Report mananger” isn’t a formal class or anything in TADS3 (or at least in adv3, I don’t know about adv3lite), although a sort of template is presented in the combineReports.t extension.

I wrote a fairly complicated example of this kind of thing to handle poker playing for my WIP (specifically handling things like discards and draws). A simpler version for what I think you’re trying to do looks something like:

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

// Another thrilling game world.
startRoom: Room 'Void' "This is a featureless void. ";
+me: Person;
+redBall: Ball color = 'red';
+greenBall: Ball color = 'green';
+blueBall: Ball color = 'blue';

// A class to handle all the balls, because I'm lazy.
class Ball: Thing 'ball*balls' 'ball'
        "A <<color>> ball. "

        // The color-setting logic has nothing to do with handling multiple
        // balls, it's just to make adding addition balls easier.
        color = nil
        initializeThing() {
                inherited();
                setColor();
        }
        setColor() {
                cmdDict.addWord(self, color, &adjective);
                name = color + ' ball';
        }

        // Hook for our report manager.
        dobjFor(Examine) {
                action() {
                        inherited();

                        // Add ourselves to the report.
                        ballReportManager.addBall(self);

                        // Make sure the report manager is called to summarize
                        // the action.
                        gAction.callAfterActionMain(ballReportManager);
                }
        }
;

// Summarize the action.
ballReportManager: object
        // Vector to hold which balls we're going to summarize.
        balls = perInstance(new Vector())

        // We only summarize if we're dealing with at least this many
        // balls.
        minLength = 2

        // Add a ball to the list.
        addBall(v) { balls.append(v); }

        afterActionMain() {
                // Make sure we have enough actions to summarize.
                if(gAction.dobjList_.length() < minLength) {
                        if(balls.length > 0)
                                balls.setLength(0);
                        return;
                }

                // Do the summary.
                gTranscript.summarizeAction(
                        // Shouldn't be necessary, but make sure we're part
                        // of the turn's action.  This would make more sense
                        // if we were summarizing both success and failure
                        // reports, but I don't think there's any case in
                        // this demo where we could be called on a failure.
                        function(x) { return(x.action_ == gAction); },
                        // The actual summary logic.
                        function(vec) {
                                local txt;

                                // Redundant check.
                                if(balls.length > 0) {
                                        txt = ballDesc(balls);
                                        balls.setLength(0);
                                } else {
                                        txt = '';
                                }
                                return('<<txt>>');
                        }
                );

                // Shouldn't be necessary, but we clear out the list anyway.
                balls.setLength(0);
        }

        // Generate the actual report summary.  Doesn't really need to
        // be broken out like this in this trivial example, but doing it
        // this way would make more sense if we were summarizing more kinds
        // of actions.
        ballDesc(lst) {
                local v;

                v = new Vector(lst.length);
                lst.forEach(function(o) {
                        v.append(o.theName);
                });
                return('It\'s <<stringLister.makeSimpleList(v)>>. ');
        }
;

versionInfo: GameID;
gameMain: GameMainDef initialPlayerChar = me;

This gets you:

Void
This is a featureless void.

You see a red ball, a blue ball, and a green ball here.

>x balls
It's the blue ball, the green ball, and the red ball.

>x red ball
A red ball.

>x red and blue ball
It's the red ball and the blue ball.

…which I think is what you’re after.

Depending on what situations/actions you need to do this for, you might instead want to take the approach taken in combineReports.t, which is to modify the Action objects to call your report manager in their afterActionMain(), whereas here I did it on the object class’ dobjFor(Examine). In my case I wanted to call the report manager only for specific kinds actions on specific kinds of objects, but I don’t know if your example is like that, of if you’d want it to apply it more generally.

3 Likes

I am using CollectiveGroup as the abstract class, and only to service EXAMINE. Yes to vocab question.

Ooh, a much more coherent approach than my rabbit chasing, thx, will run with that a bit.

Also, FTR, the nounPhrase was a red herring. I misinterpreted an error statement as bad np, when it was actually a bad list.

UPDATE: OMG, where has valToSymbol() been all my life??? This is an indespensible debug tool, that I somehow never knew existed until now! does require adding source reflect to the makefile, but man, just super helpful. Not solved yet, but some actual progress!

2 Likes

So, funnily enough, I focused on one of my two CGs, which is as you describe, a readability question. The second is in fact a set of indistinguishable objects that happens to behave the same way! That said, I had not come across the report manager concept before (despite playing with the babystep versions, the various xListWith properties and xListGroups), and will be chewing on this for a bit. Thanks for the detailed example! Back to the salt mines.

2 Likes

Did you ever get it to work?

Not yet :confused: I am lost in the endless inherited grammar behavior of resolveNouns

I have confirmed that the CollectiveGroup (which I have defined as an abstract, locationless object) is simply not visible if I am not holding one of its constituents. Even if I manually add it back to the resolution list, it ultimately fails the EXAMINE for not being visible. I have also tried defining it as a concrete (has location) object, which does make it visible and behaves correctly, but is now tied to a location in a way I don’t want my portables to be.

I have gotten as far in as the basicPluralResolveNouns method of PluralProd, but am struggling to determine where np_.resolveNouns (next layer of the onion) points to. Grammar objects are still pretty opaque to me.

Depressing to think of how much progress has been forfeited to this effort so far…

(An out-of-the-box effort to define the CollectiveGroup as a MultiFaceted Component of each constituent went laughably wrong, as it behaved neither as a Multifaceted item - all Group instances were described, not pruned to one - nor a CollectiveGroup - I got a full list of all individuals and Group facets, as if I had 6 objects, not three that wanted a single CollectiveGroup summary. Lol, I tried!)

1 Like

The Tour Guide talks about a mobile CollectiveGroup: there are certain techniques they use to bring the no-location CG into scope at the right times which I can’t recall without looking it up…

Hold that thought, the tour guide doesn’t seem to mention the scope twiddling. Maybe that was in the Tech…
I might have to wait till I’m home to figure out what I thought I read…

(I found the Tour Guide writeup, and the Tech Man Group writeup, but didn’t get daylight there. Possible I missed the no-location writeup, will recheck.)

From the code, it LOOKS like the CollectiveGroup addToSenseInfoTable method attempts to do this, but for some reason is not working. Even if I manually invoke the method to keep the Group in scope during the various resolver cycles, it will ultimately fail during the Action execution.

1 Like

I’ll see if I can conjure up a mobile CG this evening and whether I hit the same issues…

Appreciate whatever you come up with, am starting to question a lot of assumptions about my base intelligence!!

1 Like

Hum… I just created a CollectiveGroup with no location, and no Fixture-based superclass. Unfortunately, it behaved as I think you would have wanted it to: x balls always gave the collective desc whenever one of the members was visible, regardless of heldness, but ‘touch balls’ listed the members individually (because the action wasn’t asked to be handled by the collective) and each said “It feels about how a ball should feel.”
So I haven’t helped you other than maybe highlighting the fact that it’s something custom in your code?

1 Like

This could be completely unhelpful, but, could you try sticking this hack under the CollectiveGroup object? Maybe if the CG is considered as seen, it will be picked by the parser at the right times? I’m pretty stumped without seeing your code…

canBeSeen {
   local single = gPlayerChar.scopeList().valWhich({
         obj:obj.ofKind(Thing) && obj.collectiveGroups.indexOf(self)});
   return single && single.canBeSeen();
}
1 Like

perhaps the answer lies in preCond handling ?

Best regards from Italy,
dott. Piergiorgio.

1 Like

Thanks guys for your continued help in debugging. I think I have FINALLY tumbled onto what appears to be the issue, and it feels like a real bug to me?

Based on my EXTREMELY deep dive into noun resolution, there is a very early stage where resolver.objInScope(obj) is invoked for the purpose its name indicates. This in turn, invokes the actor's scopeList() which builds a list of objects held and sensed. (The held list explains why held objects always resolved!). The sensing is handled by sensePresenceList(sense) invoking senseInfoTable(sense), both inherited from Thing. The latter is the workhorse.

What it does is cycle through the connectionTable (a table of everything 'in reach' as defined by sharing the same top level room) and sequentially processes every entry's SenseInfo by invoking those object's addToSenseInfoTable method.

Here is the relevant section from CollectiveGroup's method:

    addToSenseInfoTable(sense, tab)
    {
        /* if we have no location, mimic our best individual */
        if (location == nil && !ofKind(BaseMultiLoc))
        {
            /* check everything in the connection table */

            // JJMcC - HERE IS WHERE IT MATTERS
            // if it encounters the table before all its
            // constituents are present, it will get incomplete,
            // potentially deceptive info

            tab.forEachAssoc(function(cur, val) {
                /* if this is one of our individuals, check it */
                if (cur.hasCollectiveGroup(self))
                {
                    local t;
                    
                    /* 
                     *   If it's the best or only one so far, adopt its
                     *   sense status.  Consider it the best if it has a
                     *   more transparent transparency than the best so
                     *   far, or its transparency is the same and it has a
                     *   high ambient level.  
                     */
                    t = transparencyCompare(cur.tmpTrans_, tmpTrans_);
                    if (t > 0 || (t == 0 && cur.tmpAmbient_ > tmpAmbient_))
                    {
                        /* it's better than our settings; mimic it */
                        tmpTrans_ = cur.tmpTrans_;
                        tmpAmbient_ = cur.tmpAmbient_;
                        tmpObstructor_ = cur.tmpObstructor_;
                    }
                }
            });
        }

        /* inherit the standard handling */
        inherited(sense, tab);
    }

In the case of CollectiveGroup, order matters. If you get lucky enough to process ALL your constituents first, the CollectiveGroup method determines the MOST VISIBLE element, and adopts its SenseInfo. However, if a subset of the constituents show up later in the list (and are say, in a different room, or hidden), then the CollectiveGroup gets deceptive information and in my case called itself opaque.

What I can’t determine is how the connectionTable ordering is determined. addDirectConnections(tab) seems to add it after the first constituent. During the course of debug, I determined it was sometimes last (best case) and most times second, so was miscast by an obscured earlier constituent (and never saw the later ones). I haven’t figured out why it is sometimes last.

I was, however, able to fix it by overriding the constituent's addToSenseInfoTable to invoke the CollectiveGroup each time a constituent is processed, guaranteeing the most visible result. Absent other input, I think I leave it there for now. This has consumed way too much of my WIP design cycle. It is written as a possible upgrade to the base Thing class, but for now, am applying it to a new new Thing sub-class PortableCollective

    addToSenseInfoTable(sense, tab)
    {
        inherited(sense, tab);

        // JJMcC - if I modified SenseTable by adding myself,
        // force my Collective Groups to reevaluate
        if (tab.isKeyPresent(self))
            foreach (local cur in collectiveGroups)
                cur.addToSenseInfoTable(sense, tab); // force update!
    }
2 Likes

Wow, that was tricky! Surely does sound as though the library processing did not account for all cases of handling. Glad you’ve nailed it! Looking forward to your finished product!

1 Like