Okay, let's do this again: isEquivalent, CollectiveGroup, and related TADS3/adv3 woes

The broad problem: you have some objects that either are indistinguishable, interchangeable, or otherwise require some special handling to merge/combine/summarize action reports.

There are a number of easy half-solutions, this is an attempt to implement a more comprehensive solution. Specifically, here I’m presenting 1) a report manager framework for merging reports involving multiple related objects (as defined by their object type, i.e. a parent class), and 2) a framework for implementing “resource”-type objects—indistinguishable, interchangeable objects like coins or abstract resource-gathering resources (stone, wood, or whatever).

THE REPORT MANAGER

Basic usage is:

  • Declare a ReportManager instance.
    Each ReportManager needs to have a reportManagerFor property indicating the object or class it manages reports for.
  • Add ReportSummary instances to the ReportManager.
    Each ReportSummary needs to have a declared action it applies to and a summarize() method to construct the actual summary.
    The summarize() method takes a single argument, a ReportSummaryData instance, and returns a single-quoted string (the report summary text).
  • The ReportSummaryData includes the properties:
    vec a vector containing the reports being summarized
    objs a vector containing the (direct) objects in the reports
    count the integer number of objects being summarized
    dobj a “representative” object for the summary. By default this is the first object in objs, but ReportManager.getReportDobj() can be over-written to use a different object (including one that’s not in the reports at all, for example)

AN EXAMPLE

First, consider the gameworld:

class Pebble: Thing '(small) (round) pebble*pebbles' 'pebble'
        "A small, round pebble. "
        isEquivalent = true
;

startRoom: Room 'Void' "This is a featureless void.";
+me: Person;
++Pebble;
++Pebble;
+Pebble;
+Container '(wooden) box' 'box' "A wooden box. ";
++Pebble;
++Pebble;
++Pebble;
++Pebble;
+Pebble;
+Pebble;
+Thing '(ordinary) rock' 'rock' "An ordinary rock. ";

By default this gets us:

Void
This is a featureless void.

You see three pebbles, a box (which contains four pebbles), and a rock here.

>x all
your pebble: A small, round pebble.
your pebble: A small, round pebble.
the pebble on the floor: A small, round pebble.

box: A wooden box.  It contains four pebbles.

the pebble in the box: A small, round pebble.
the pebble in the box: A small, round pebble.
the pebble in the box: A small, round pebble.
the pebble in the box: A small, round pebble.
the pebble on the floor: A small, round pebble.
the pebble on the floor: A small, round pebble.
rock: An ordinary rock.

>

Now we add:

pebbleReportManager: ReportManager
        reportManagerFor = Pebble
;
+ReportSummary @ExamineAction
        summarize(data) {
                return('It\'s <<spellInt(data.count)>> small, round pebbles. ');
        }
;

This gets us:

Void
This is a featureless void.

You see three pebbles, a box (which contains four pebbles), and a rock here.

>x all
box: A wooden box.  It contains four pebbles.

rock: An ordinary rock.
your pebbles: It's two small, round pebbles.
pebbles on the floor: It's three small, round pebbles.
pebbles in the box: It's four small, round pebbles.

>

Note the wonky spacing around the container. That’s a TADS3/adv3 thing, not part of the report manager.

INTERCHANGEABLE OBJECTS

Everything above is just juggling the reports. This doesn’t fix things like commands with item counts and so on. That involves CollectiveGroup, listWith and so on. To make using all of that easier, here’s a “resource” model for intistinguishable/interchangeable objects.

The basic usage is simple:

  • Declare a item type as a Resource subclass
  • Declare a ResourceFactory for the resource

In this case we re-define the pebble class:

class Pebble: Resource '(small) (round) pebble*pebbles' 'pebble'
        "\^{A/Count resource} small, round {single/plural resource}. "
        smellDesc = "\^{It/They resource} smell{s resource} like
                {a/count resource} small, round {single/plural resource}. "
;

Note a couple of things here. One is that the desc uses a bunch of message parameter substitutions with the resource tag. These are specific to the resource module, and are:

  • singe/plural becomes the single or plural name of the resource depending on the number in the report
  • count becomes the spelled-out number of items in a resource report
  • a/count becomes either “a” if there’s one in the report or the spelled out number (“two”, “twelve”, or whatever) if there’s more in the report
  • it/they becomes “it” if there’s one, “they” if there’s more

We can use them directly (without having to declare any report summaries) because each Resource automagically gets a report manager and summary reports for each of the standard sense descriptions. So you can just declare desc, smellDesc, soundDesc, feelDesc, and tasteDesc and it’ll (hopefully) just work.

Continuing the example, the factory declaration is just:

pebbleFactory: ResourceFactory
        resourceClass = Pebble
;

Then with the same gameworld used above, at preinit the ResourceFactory will take care of setting up all the CollectiveGroups and ListGroupEquivalents. That gets us:

Void
This is a featureless void.

You see a box (which contains two pebbles) and three pebbles here.

>i
You are carrying five pebbles and a rock.

>put 2 pebbles in the box
You put two pebbles in the box.

>x pebbles
pebbles in the box: Four small, round pebbles.
pebbles on the floor: Three small, round pebbles.
your pebbles: Three small, round pebbles.

If you want to define addtional report summaries for a Resource you can add them (via the +ReportSummary format) on the ResourceFactory declaration.

THE REPOS

The reportManager github repo.
The resources github repo.

I’ll probably bump with some discussion of the gotchas and dead ends I explored in hammering this out.

I’d also be surprised if this doesn’t require additional tweaking/bugfixes, because there are a lot of fiddly corner cases I’m sure I haven’t tested yet. So comments/bug reports welcome.

6 Likes

Just a few comments on one of the traps in the stock adv3 method of combining reports.

There’s CommandReport.summarizeAction(cond, report). The two arguments are both functions. The first, cond, takes a report as an argument and should return boolean true if the report should be included in the summary, nil otherwise. The second, report, takes the vector of reports to be summarized as an argument, and returns a string (the summary text).

A “simple” implementation built around this works in some cases but there are several gotchas.

Say your cond function matches only reports where the direct object of the action is a pebble. You might think that the report function would be a) called once, and b) all the pebbles would be in the report vector.

But neither of these assumptions is necessarily true. If you look back up at the gameworld example, you’ll notice that the pebbles in the room (the ones reported as being on the floor) are split up…the rock and box declaration are in the middle.

The summarizeAction() always calls the report function on contiguous “blocks” of matching reports, and it doesn’t sort the reports in any way. So in this case you’ll get one “block” of pebble reports involving the pebbles declared before the rock and box, and a second “block” of pebble reports of all the ones declared after the rock and box.

In this specific case you could resolve the issue by changing the order of declaration in the source. But then you’ll run into the problem anyway when the player picks up and drops things in arbitrary order.

There’s also no builtin way to group reports by their distinguishers. In this case we have several different groups of pebbles: “your pebbles” vs “pebbles on the floor” vs “pebbles in the box”. But with the summarizeAction() method even if you’re lucky and all the matching reports occur in a single block you have to manually separate them into different groups for reporting unless you want them all in a single report. My first pass would have done something like:

>X PEBBLES
pebbles:  Nine small, round pebbles.

…ignoring that the individual pebbles were in different locations.

The way the reportManager module handles this is by implementing summarizeAction() workalike method, sortedSummarizeAction().

sortedSummarizeAction() automagically merges all the matching reports into a single vector before calling the reporting function. Note that this can result in breakage if the reporting order is important. The reason summarizeAction() does things the way it does is so it can remove the summarized reports put a replacement summary report in the spot they occupied, preserving the report ordering. The sorted method doesn’t do this. The assumption is: the kind of actions that will end up being summarized don’t care about report ordering in this way (things like >X ALL don’t really care about serialization order). If you do care about strictly preserving ordering, you’ll have to fiddle around with the sorting logic.

Anyway, sortedSummarizeAction() takes three arguments. The first and last are the same as arguments for summarizeAction(), and the new middle argument is for an explicit sorting function. It takes a vector of the reports to be summarized.

By default the sorting function ReportManager uses is ReportManager.sortReports().

By default it groups reports by their distinguishers…the “pebbles in the box” phrases prepended to the action report. By default the module uses the object’s getBestDistinguisher() method to pick a distingusher, which is what adv3 in general uses to generate disambiguation distinguishers.

This can be changed by re-writing ReportManager.getReportDistinguisher(obj, n). The method takes two arguments: the object; and the number of matched objects.

2 Likes

Minor update: this is no longer true. Summaries should now automagically be inserted into the report sequence wherever their parent reports were extracted from.

For whatever it’s worth, it does this by inserting a non-outputting placeholder report into the spot where the reports are removed, more or less like the native summarizeAction() does with the summary. The placeholder and the reports are both marked with an identifying “serial number”.

After sorting, the report manager checks each group of reports for the lowest serial number, and then inserts the summary at the location of the placeholder report with the matching number.

In the case of grouped reports that were split the summary will be inserted at the location of the first original report. In our earlier example some of the pebbles on the floor were listed before the box and some were listed after the box in the original, un-summarized output. In that case the summary for the pebbles on the floor would end up before the box, where the first of the floor pebble reports was.

2 Likes

Okay, before I bang my head against this any longer I figured I’d ask if anyone knows what’s happening here.

If we define a generic class, in this case Ball, and use initializeThing() to tweak the vocabulary (assigning it a color)…why do listers refuse to disambiguate them, even when the parser in general does. For example, given:

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

versionInfo: GameID;
gameMain: GameMainDef initialPlayerChar = me;

class Ball: Thing 'ball' 'ball'
        "It's a <<color>> ball. "
        color = nil

        isEquivalent = true

        initializeThing() {
                inherited();
                setColor();
        }

        setColor() {
                if(color == nil)
                        color = 'white';
                cmdDict.addWord(self, color, &adjective);
                name = color + ' ball';
        }
;

class RedBall: Ball color = 'red';
class BlueBall: Ball color = 'blue';

startRoom: Room 'Void' "This is a featureless void. ";
+me: Person;
+RedBall;
+BlueBall;

This creates a red ball and a blue ball. Each have isEquivalent set, but that should mean that all red balls are equivalent to other red balls, and the same for blue. But:

Void
This is a featureless void.

You see two red balls here.

>

Lister reports it as two red balls. This confusion isn’t universal, though, because:

>x all
red ball: It's a red ball.

blue ball: It's a blue ball.

>take red ball
Taken.

>l
Void
This is a featureless void.

You see a blue ball here.

>

Things otherwise work as expected.

At first I thought perhaps this was isEquivalent-related breakage—it only appears to affect objects with isEquivalent set—but it turns out that if you declare the name property:

class RedBall: Ball name = 'red ball' color = 'red';
class BlueBall: Ball name = 'blue ball' color = 'blue';

…it works as expected:

Void
This is a featureless void.

You see a red ball and a blue ball here.

>

In fact you can do something like:

class Ball: Thing
        vocabWords = 'ball'
        name = (color + ' ball')

        desc = "It's a <<color>> ball. "
        color = nil

        isEquivalent = true

        initializeThing() {
                inherited();
                setColor();
        }

        setColor() {
                if(color == nil)
                        color = 'white';
                cmdDict.addWord(self, color, &adjective);
        }
;

…and it works as expected.

So…am I missing something about runtime changing of object names? Or rather: I am missing something about runtime changing of object names, does anyone know what it is?

Note: This is all in stock adv3. This has nothing to do with the report manager/resource class stuff I presented earlier in the thread.

2 Likes

Don’t have time at the moment but would be interested to look into this later if no solutions arise in the meantime…

No immediate knowledge either sorry, but this is def the kind of thing I would deep dive on if it manifested in my WIP.

1 Like

Check out en_us, line 855. equivalenceKey is based on disambigName which comes from name.

I’m not sure I follow. equivalenceKey is by default disambigName and disambigName by default is name. You can insert debugging output into the initializeThing() to verify that all of those are what you’d expect them to be after setting name.

And:

>take ball
Which ball do you mean, the blue ball, or the red ball?

>

So the game isn’t confused about the disambigName or equivalenceKey, near as I can tell. It’s just that Lister (which is what generates the contents listing, in this case for the Room) is not using them.

Specifically Lister, as part of its normal operation, tries to group similar objects together (and so “two pebbles” instead of “a pebble and a pebble”) and that logic, for some reason, isn’t applying to same disambigation rules that everything else does. Or at least isn’t applying them in the same way.

1 Like

Got it.

There’s an global equivalentGrouperTable. When an object is initialized each Thing with isEquivalent adds its equivalenceKey to the table, associating it with a grouping object (instance of ListGroupEquivalent) for itself.

If the equivalenceKey changes (becauce it’s just name through a couple layers of indirection), it’ll still use the old grouper.

In our example, since all instance of Ball get initialized the same way, they all end up using the same entry in the global equivalenceGrouperTable, causing them to be listed together.

In this specific case, we can get around this by tweaking the name before doing the rest of the bookkeeping in initializeThing():

        initializeThing() {
                setColor();
                name = (color + ' ball');
                inherited();
        }

This appears to be the cleanest way of handling it.

Alternately, you could just call initializeEquivalent() on the object(s) after changing their name. This probably makes more sense if the name is changing as a result of gameplay. This leaves a stale entry in equivalentGrouperTable, but that’s probably not a big deal (you could presumably remove the old entry from the table if you were really worried about it, but you’d have to make sure there were no other instances using the same grouper).

2 Likes

Minor feature update.

You can now declare ReportManager.reportManagerFor to be a list of classes/objects, and then define reportName on the object(s) for disambiguation-for-report-summaries purposes.

This is for when you have a class or classes of related-but-different objects that you generally want to disambiguate “normally”, but want grouped together for summarizing.

As a concrete example, here’s a slight variation of the Ball example from before, only with flowers instead. We have a base Flower class that works like the Ball we looked at previously:

class Flower: Thing 'flower*flowers' 'flower'
        "A <<color>> flower. "

        color = nil
        isEquivalent = true

        initializeThing() {
                setColor();
                inherited();
        }

        setColor() {
                if(color == nil)
                        color = 'colorless';
                cmdDict.addWord(self, color, &adjective);
                name = '<<color>> flower';
        }
;

Now we define subclasses for each of the colors:

class RedFlower: Flower color = 'red';
class BlueFlower: Flower color = 'blue';
class GreenFlower: Flower color = 'green';

With this, if we define a gameworld:

startRoom: Room 'Void' "This is a featureless void.";
+me: Person;
++GreenFlower;
++RedFlower;

+RedFlower;
+BlueFlower;
+GreenFlower;
+GreenFlower;
+GreenFlower;

+box: Container '(wooden) box' 'box' "A wooden box. ";
++RedFlower;
++RedFlower;
++BlueFlower;

…then by default we get:

Void
This is a featureless void.

You see a box (which contains two red flowers and a blue flower), a red flower,
a blue flower, and three green flowers here.

>x all
your green flower: A green flower.

your red flower: A red flower.

box: A wooden box.  It contains two red flowers and a blue flower.

the red flower in the box: A red flower.

the red flower in the box: A red flower.

the blue flower in the box: A blue flower.

the red flower on the floor: A red flower.

the blue flower on the floor: A blue flower.

the green flower on the floor: A green flower.

the green flower on the floor: A green flower.

the green flower on the floor: A green flower.

>

This is obviously a mess. Now if we add a reportManager for Flower:

flowerReportManager: ReportManager
        reportManagerFor = Flower
;
+ReportSummary @ExamineAction
        summarize(data) {
                return('It\'s <<objectLister.makeSimpleList(data.objs)>>. ');
        }
;

…we get the slightly more manageable:

>x all
your green flowers: It's a green flower.
your red flowers: It's a red flower.

box: A wooden box.  It contains two red flowers and a blue flower.

red flowers in the box: It's two red flowers.
blue flowers in the box: It's a blue flower.
red flowers on the floor: It's a red flower.
blue flowers on the floor: It's a blue flower.
green flowers on the floor: It's three green flowers.

>

But the grouping flowers by their “native” disambiguators is a little awkward, so now we can add a reportName to Flower:

        reportName = 'flower'

…to group Flower instances as if their disambigName was just “flower” only for the purposes of summarizing reports. That gets us:

>x all
your flowers: It's a green flower and a red flower.

box: A wooden box.  It contains two red flowers and a blue flower.

flowers in the box: It's two red flowers and a blue flower.
flowers on the floor: It's a red flower, a blue flower, and three green
flowers.

In this case we have a clean class inheritance that allows us to define reportManagerFor = Flower on the report manager to catch all the objects we want to group together for summaries. If that isn’t true, we can just use a list. So:

        reportManagerFor = static [ RedFlower, BlueFlower, GreenFlower ]

Has exactly the same effects as reportManagerFor = Flower.

2 Likes

Bump for a moderately substantial update.

I’d originally written reportManager to work in parallel with @Eric_Eve’s combineReports.t. But after wrestling with some corner cases I’m in the process of implementing replacements for what combineReports.t does in the reportManager module.

The problem I was wrestling with that lead me to this approach is the corner case where there are successes and failures in a single transcript, and some parts would be handled by reportManager and some by combineReports. For example, a >TAKE ALL when some objects have their own report managers and some are being handled by combineReports…and there’s a mix of objects that can be taken and objects that cannot be taken.

To deal with this sort of thing there’s now a global reportManagerController singleton. You can add ReportSummary objects to it, just like any other ReportManager. Summaries on reportManagerController aren’t restricted to a specific object or class.

In the current system, reports are only ever “used” by a single report summary. Precedence is in favor of more specific report managers over the global reportManagerController. So for example if you have something like:

flowerReportManager: ReportManager reportManagerFor = Flower;
+ReportSummary @TakeAction
        summarize(data) {
                return('You pick <<objectLister.makeSimpleList(data.objs)>>. ');
        }
;

…and you have a >TAKE ALL involving multiple Flower instances and multiple Pebble instances (which don’t have a special ReportManager), then all of the flower-related reports will be summarized by the summarizer defined above, and all the pebble-related reports will be summarized by reportManagerController.

Additionally, there’s now a FailureSummary class that works exactly like ReportSummary but which only receives failure reports. So to summarize Flower-related successes and failures you can declare something like:

flowerReportManager: ReportManager reportManagerFor = Flower;
+ReportSummary @TakeAction
        summarize(data) {
                return('You pick <<objectLister.makeSimpleList(data.objs)>>. ');
        }
;
+FailureSummary @TakeAction
        summarize(data) {
                return('You can\'t pick <<objectLister
                        .makeSimpleList(data.objs)>>. ');
        }
;

All the logic described above is working correctly (as far as I know), but I haven’t implemented work-alikes for all of the features of combineReports yet. Specifically reportManager doesn’t summarize implicit action reports yet, which combineReports does.

3 Likes

Another bump. Updates:

  • The hard-to-type reportManagerController has been replaced by the slightly-easier-to-type transcriptManager. That’s the global singleton.
  • There are now TranscriptMarker and TranscriptSorter classes for widgets that modify the transcript “in place”…marking reports and sorting them, respectively. Instances can be added to transcriptManager via the standard +[declaration] syntax
  • transcriptManager now has a defaultTranscriptMarkers and defaultTranscriptSorters property for sorters and markers to be added by default. By default it gets MoveFailuresToEndOfTranscript, a sorter that does what it says
  • Implicit action summaries are kinda supported now. I haven’t added/tested replacements for all of the summaries supplied by combineReports, but implicit action summarizers can be added to transcriptManager (via the +[declaration] syntax). At the moment the only default implicit action summarizer is for >TAKE, which illustrates the syntax (pretty much the same as the other summarizers):
class ImplicitTakeSummary: ImplicitSummary
        action = TakeAction

        summarizeImplicit(data) {
                return('first taking <<equivalentLister
                        .makeSimpleList(data.objs)>>');
        }
;

There are undoubtedly more corner cases to identify and bugs to fix, but kinda creeping up on a workable general-purpose report summarizer/transcript re-writer here.

Main thing I know still isn’t implemented is summarizing implicit action failures.

3 Likes

Time for a refactor.

Here’s a new repo: transcriptTools github repo. This replaces the now-lightly-deprecated reportManager module.

Virtually all of the ReportManager and ReportSummary logic described above is in the new module, with the same usage. The new stuff is in the transcript-wide stuff (as opposed to the “summarize specific reports” stuff).

Short version (long version is documented in the comments in the code):

  • There’s now a TranscriptTools class for the “top level” transcript stuff, with each instance associated with a transcript (it’s done this way instead of just putting everything on CommandTranscript to avoid namespace collisions). In most cases you’ll only ever need the created-by-default instance transcriptTools, which operates on gTranscript (it’s more or less equivalent to transcriptManager from the old module)
  • The class above is holds one or more TranscriptTool instances, which are what actually manipulate the transcript. They have a numeric toolPriority which determines the order of operations (following the convention of lower numeric priority going before higher numeric priority, like AgendaItem)
  • Invocation of the logic in each TranscriptTool occurs in multiple, distinct phases:
  • First, all TranscriptTool instances have their clear() method called, to reset their state if necessary
  • TranscriptTool.preprocess() is called on all instances, followed in sequence by
  • TranscriptTool.run()
  • TranscriptTool.postprocess()
  • TranscriptTool.clear() is called again to discard any temporary data

The default TranscriptTools include

  • ReportGrouper, which groups reports, by default by their adv3-assigned iter_ value
  • MarkFailures, which marks all reports associated with a failure with isFailure = true (by default only the actual failure report is marked, not any surrounding reports associated with the same action)
  • MoveFailuresToEndOfTranscript, which does what it says…all failure reports are bumped to the end of the transcript, otherwise preserving their ordering
  • TranscriptReportManager, which handles all the ReportManagers

The TranscriptTool class provides a bunch of convenience methods for working with the transcript in the context of the module:

  • forEachReport(fn) iterates over every report in the transcript, calling the callback function on each report
  • forEachReportGroup(fn) iterates over every report group, as sorted by ReportGrouper (described above)
  • getReportGroup(report) returns the report group the given report belongs to
  • moveGroup(grp, idx?) moves all the reports in the given report group to the given index in the transcript, defaulting to the end of the transcript if no index is given
  • replaceReports(oldReports, newReports) removes a list of old reports from the transcript, inserting a list of new reports into the spot where the old reports were

The TranscriptReportManager class by default includes two “built-in” report managers:

  • GeneralReportManager, which handles summarizing reports by action, more or less like combineReports.t works
  • SelfReportManager, which allows objects to “self-summarize”

To illustrate the last bullet item, let’s look at the “flower” example used above. We have some object classees:

class Flower: Thing 'flower*flowers' 'flower'
        "A <<color>> flower. "

        reportName = 'flower'
        color = nil
        isEquivalent = true

        initializeThing() {
                setColor();
                inherited();
        }

        setColor() { 
                if(color == nil)
                        color = 'colorless';
                cmdDict.addWord(self, color, &adjective);
                name = '<<color>> flower';
        }
;
class RedFlower: Flower color = 'red';
class BlueFlower: Flower color = 'blue';
class GreenFlower: Flower color = 'green';

Then the “long form” is to declare a ReportManager instance for Flower:

flowerReportManager: ReportManager
        reportManagerFor = Flower
;
+ReportSummary @ExamineAction
        summarize(data) {
                return('It\'s <<objectLister.makeSimpleList(data.objs)>>. ');
        }
;

The SelfReportManager class simplifies this, allowing you to instead do something like:

modify Flower
        dobjFor(Examine) {
                summarize(data) { return('It\'s <<data.listNames()>>. '); }
        }

The summarize() method here works exactly like the ReportSummary.summarize(): it takes an instance of ReportSummaryData as its argument and returns a text string summarizing the reports.

You can insert summarize() methods into any dobjFor([action]) stanza to have the object self-summarize reports for that action.

The ReportSummaryData class has also been revised. The previous stuff was there, which is, briefly:

  • vec a Vector containing the reports to summarize
  • obj a Vector containing a (uniq-ified) list of all the direct objects in the reports
  • count the number of direct objects being summarized (probably just obj.length)
  • dobj and iobj “representative” direct and indirect objects from the reports. this is mostly for when you’re summarizing a bunch of isEquivalent objects and just want an easy way to get an instance to pull vocabulary off of (aName or theName or whatever)
  • action “representative” action for the summary. mostly used in ActionSummary instances

In addition to the properties above, there are now a number of convenience methods for generating summaries:

  • listNames() returns a list of direct objects, using “and” as the conjunction (“a pebble and a rock”)
  • listNamesWithAnd() alias for listNames() (above)
  • listNamesWithOr() returns a list of direct objects, using “or” as the conjunction (“a pebble or a rock”)
  • listIobj() returns the iobj if one exists. semantic sugar, maybe a misfeature
  • getAction() in most cases this just returns the action mentioned above in the properties list. the “gimmick” is that getAction() checks to see if there’s an iobj, and if so and action is not a TIAction, it iterates through the reports in the summary to find a TIAction. This somewhat fiddly nonsense is mostly useful when the summarizer is handling an action that gets re-written (by adv3 logic) to a different action (like >TAKE ALL FROM BOX will end up be re-written internally as a series of >TAKE [object] actions)
  • actionClause() returns the conjugated verby phrase along with the direct object and indirect object (if applicable). This is used by the ActionSummary summarizers to generate summaries
  • actionClauseWithAnd() alias for actionClause()
  • actionClauseWithOr() uses “or” as the conjunction. mostly useful for failure reports (to give (you can’t) “take the pebble or the rock” instead of (you can’t) “take the pebble and the rock”)

I’m not going to try to go through all the features of the module (although I’ll add another post going through the main points in a bit, mostly to call out some of the hidden gotchas that made me spend so much time on this in the first place). But a couple points:

  • The code is designed to be aggressively modular, allowing you to add or remove individual bits of the code
  • One of the ways you can do this is by checking out transcriptToolsDefaults.t in the source. This is where the default tools are added to transcriptTools, default report managers are added to TranscriptReportManager, and default summaries are added to GeneralReportManager. This is intended to make it easier to see what the defaults are, and to change them if you want
  • All of the widgets, from the top-level TranscriptTools to the individual ReportSummary instances, can be toggled on and off at runtime

I’ll be back in a little bit to cover the various use cases/gotchas addressed in the module as written.

1 Like

Okay, I’m just going to run through the test cases to try to illustrate what the module does.

BASIC SUMMARY

Given a pebble and a rock, the behavior of >TAKE ALL is:

You see a pebble and a rock here.

>take all
pebble: Taken.
rock: Taken.

With the module, you get:

You see a pebble and a rock here.

>take all
You take the pebble and the rock.

Simple enough. Now the hidden gotchas.

First, under the hood we’re using a bespoke Lister for our reports, and you’ll notice that in the room listing it’s “a rock” and “a pebble”, because they’re both instances of isEquivalent classes. But if there’s only a single pebble or rock in scope it sounds off (to me anyway) for the game to tell you “You take a pebble” instead of “You take the pebble”.

The custom lister (EquivalentLister, in transcriptToolsLister.t) figures out if there’s only one of an isEquivalent object in scope and, if so, prefers “the” instead of “a” only in report summaries.

Second, we’re also doing some fiddly checking of message properties. Say we do something like:

modify Pebble
        dobjFor(Take) {
                action() {
                        inherited();
                        mainReport('As you pick up the pebble, an alarm sounds
                                in the distance. ');
                }
        }
;

We probably don’t want to squash the non-default report there. Here’s how combineReports.t and the old reportManager module handle this:

You see a pebble and a rock here.

>take all
You take a pebble and a rock.

The non-default output is squashed by the summary process.

The slightly fiddly way transcriptTools handles this is to allow you to define a matchMessageProp (or matchMessageProps if you need an array of them) to give a property reference (as to playerActionMessages or npcActionMessages) and will only summarize away a report if its messageProp_ matches it. So here are the ActionSummary classes that handles >TAKE actions:

class TakeSummary: ActionSummary
        action = TakeAction
        gActionExclude = TakeFromAction
        matchMessageProp = &okayTakeMsg
;

class TakeFromSummary: ActionSummary
        action = TakeFromAction
        actionInclude = TakeAction
;

This means that a TakeAction report will only be summarized if it’s messageProp_ is &okayTakeMsg, which is the library default. In action, with our alarm-sounding pebble mod:

You see a pebble and a rock here.

>take all
pebble: As you pick up the pebble, an alarm sounds in the distance.
rock: You take the rock.

In addition to matchMessageProp you’ll notice some “include” and “exclude” properties.

ACTION REMAPPING

Another “gotcha” is when adv3 automagically re-maps one action to another. For example, any time you do a >TAKE FROM, it will internally be remapped to a series of >TAKE actions. This means if you’ve got a transcript for a command like >TAKE ALL FROM BOX, you’ll have some reports whose action_ is an instance of TakeAction and some that will be instances of TakeFromAction, so summarizers for each need to know how to look out for the other.

The ActionSummary class lets you define:

  • gActionExclude which tells summarizer that is mapped to to ignore reports it would otherwise match if the current gAction is the action that is matched from. So TakeSummary normally matches TakeAction reports, but not if gAction is TakeFromAction.
  • actionInclude which tells the summarizer to include reports for the given action even if it otherwise wouldn’t. So TakeFromSummary includes TakeAction (specifically when gAction is TakeFromAction and the report’s action_ is TakeAction)

The example we’re discussing here is handled correctly in combineReports.t, but it doesn’t correctly handle the similar situation involving PutOnAction being mapped internally to DropAction:

You see a pebble and a rock here.

>take all
You take a pebble and a rock.

>put all on floor
Dropped.  You put a pebble and a rock on the floor.

Dropped.

All the include/exclude gymnastics in ActionSummary is required to give:

You see a pebble and a rock here.

>take all
You take the pebble and the rock.

>drop all
You drop the pebble and the rock.

I’m going to take a break here, but I’ll cover some more “gotchas” later.

1 Like

Just going to add a little more here.

IMPLICIT ACTIONS

Implicit action summaries have pretty much the same problems regular action summaries have…and then have the additional complications because they’re implicit (and therefore are not the main action for the turn).

First off, the problem with non-default action reports discussed above also applies to implicit actions. Given two pebbles and a box and no special logic, by default we have:

>put pebbles in box
pebble:
(first taking the pebble)
Done.

pebble:
(first taking the pebble)
Done.

This works fine in the old reportManager code and combineReports.t:

You see two pebbles and a box here.

>put pebbles in box
(first taking two pebbles)
You put two pebbles in the box.

But if we create a Stone class that has:

        dobjFor(Take) {
                action() {
                        inherited();
                        mainReport('As you pick up {a dobj/him}, an alarm sounds
                                in the distance. ');
                }

Then by default that’s:

You see two stones and a box here.

>put stones in box
stone:
(first taking the stone)
As you pick up a stone, an alarm sounds in the distance.

Done.

stone:
(first taking the stone)
As you pick up a stone, an alarm sounds in the distance.

Done.

…but the “simple” summary approach squashes the custom action message:

You see two stones and a box here.

>put stones in box
(first taking two stones)
You put two stones in the box.

Okay, same problem described in my previous post. So we try to fix it the same way, by using matchMessageProp to check for a default message before summarizing the implicit action:

class ImplicitTakeSummary: ImplicitSummary
        action = TakeAction
        matchMessageProp = &okayTakeMsg
        summarize(data) { return('first taking <<data.listNames()>>'); }
;

That does what we asked it to…

You see two stones and a box here.

>put stones in box
(first taking two stones)
As you pick up a stone, an alarm sounds in the distance.  As you pick up a
stone, an alarm sounds in the distance.

You put two stones in the box.

…but now we have duplicate non-default action reports. Assuming that the non-default action report is coming from a bespoke action handler on the object (it is in our case, at least), we’ll handle that by adding a self-summarizer on the object:

                summarize(data) {
                        return('As you pick up the <<data.listNames()>>, an
                                alarm sounds in the distance. ');
                }

This gets us what we want:

You see two stones and a box here.

>put stones in box
(first taking two stones)
As you pick up the two stones, an alarm sounds in the distance.

You put two stones in the box.

Working throught this stuff made me notice a few more oddball cases. For example normally moving failure reports to the end makes sense…unless there are implicit actions, in which case they probably want to be moved to the end of the implicit actions, and so on.

This is one of those things where handling the basic use cases is just a few lines of code but handling all the special cases balloons it out into the thousands of LOC.

2 Likes

Okay, let’s do this again…again. A re-refactor.

A updated transcriptTools module (same repo url as the old transcriptTools module, but a new repo; re-clone if you’ve got a copy of the old module).

This is a substantial re-working of the stuff discussed in this thread. The basic report manager and report syntax is almost entirely the same, but there are new features and the internal workings have been completely re-worked.

INTEGRATION INTO ADV3

One of the biggest changes is that the module’s logic now functions more or less “normally” as a set of TranscriptTransform instances. Previously the hook into normal transcript processing was via Action.afterActionMain().

There are still three explicit processing phases used by the module: a preprocessor phase (before any other transcript processing); a “main” phase (after basic bookkeeping has been done—removal of redundant DefaultCommandReports, re-ordering of Before-, After- and MainCommandReports, and so on); and a postprocessor phase (after all other transcript processing).

FEATURES

I won’t go over the features that have remained the same or nearly the same: standalone ReportManagers, self-summarizing, and so on.

The main functional processing difference (apart from slightly smarter and more efficient processing in general) is that:

  • There is no longer any support for matchMessageProp or any similiar mechanism (as described in my previous post). That is, the module doesn’t have any “built-in” logic that attempts to determine if any reports are “non-default” to preempt summarizing
  • Instead, there’s a new whenSummarized() method that can be placed in a dobjFor([action]) stanza that will be called when a corresponding report is removed by a summarizer

REACTING TO REPORT DELETION WITH whenSummarized()

As an example, consider the same situation discussed previously, involving a stone that triggers an alarm when it is picked up. Here’s one way to insure the alarm notification shows up even if the original reports get squashed by a summarizer. This is from the demo (demo/src/sample.t in the module source), defining an abstract AlarmItem class of which Stone is a subclass:

class AlarmItem: Thing
        alarmReport = 'As {you/he} pick{s} up {a dobj/him}, an alarm sounds
                in the distance. '

        dobjFor(Take) {
                action() {
                        inherited();
                        mainReport(&alarmReport);
                }

                whenSummarized(data) {
                        if(data.report.messageProp_ != &alarmReport)
                                return;

                        extraSummaryReport(data, 'As {you/he} pick{s} up
                                <<data.summary.listDobjSubset(Stone)>>, an alarm
                                sounds in the distance. ');
                }
        }
;

This declares a alarmReport property, which we use just to make it easier to deal with.

In dobjFor(Take) { action() } we just add a report containing the alarm message.

If that report is subsequently removed by a summarizer, dobjFor(Take) { whenSummarized() } will be pinged. The argument will be an instance of the SummaryNotification class. its properties are:

  • report the report that was removed
  • summary the summary that will replace this report

In the example whenSummarized() method we check to see if the removed report’s messageProp_ matches &alarmReport.
This is just to make sure we’re being notified of that report being removed, as opposed to any other reports that might be associated with the same action (cosmetic separator reports, the DefaultCommandReport that just says “Taken.” by default, and so on).

When we have a report we’re interested in, we then call extraSummaryReport(), which is a macro that will insert a new report into the transcript in the same report group (that is, same iter_ value) as the report being removed.

extraSummaryReport() takes two args: the first is a SummaryNotification instance (the same argument that’s passed to whenSummarized()) and the second is the new report text.

Here we use a summary convenience method listDobjSubset(). Like listDobj() (discussed before) this generates a text string listing the direct objects associated with the summary (“the pebble, the rock, and the stone” or whatever). listDobjSubset() additionally takes a class as its only argument, and will only return the direct objects that match it. So here the >TAKE summary might contain multiple objects, but listDobjSubset(Stone) will just list the Stone instances.

In action (from the demo game):

Room 3C
This is room 3C.  Room 3B is to the north, 3D is to the south.

There's a sign on the wall.

You see two pebbles, two stones, and a box here.

>put all in box
(first opening the box, then taking two pebbles and two stones)
You put two pebbles and two stones in the box.

As you pick up two stones, an alarm sounds in the distance.

SUMMARIZING MULTI-OBJECT ANNOUNCEMENTS

Another weird corner-case. This usually shows up when you have a mix of action-based summaries, some of which succeeded and some of which failed…and you have additional “stuff” that isn’t part of that. For example:

Room 3D
This is room 3D.  Room 3C is to the north.

There's a sign on the wall.

You see a pebble, a stone, a rusty anchor, and a box here.

>take all
pebble and stone: You take the pebble and the stone.

As you pick up the stone, an alarm sounds in the distance.

rusty anchor and box: You can't take the rusty anchor or the box.

>

The thing that’s going on here is that we’re getting two multi-object announcements, one for the successes (the pebble and stone) and one for the failures (the anchor and the box). Since these aren’t single objects (by default distinguishers in adv3 are always based on the vocabulary of single objects or groups of equivalent objects) what’s happening here is that the summarized reports are each getting a distinguisher announcement that’s a list of their individual distinguishers.

If that’s not desired, it’s not too difficult to change.

INTERACTIVE TRANSCRIPT DEBUGGER

The new version of the module also includes an interactive transcript debugger. It’s mostly intended to make it easier to look at the state of the transcript at various points in processing.

From the command prompt (in normal gameplay):

  • >TTI ON turns the interactive debugger on. when on, processing will automatically drop into the debugger after execution of the next command, before output. by default the debugger starts durning postprocessing, meaning the default view of the transcript is what it looks like after all the re-writing has already happened
  • >TTI OFF turns the interactive debugger off
  • >TTI ? or >TTI STATUS show the current interactive debugger settings
  • >TTI HELP shows the available commands with a short description of each
  • >TTI PRE toggles the “pre” flag. if set, the debugger will run during the preprocessor phase
  • >TTI RUN toggles the “run” flag. if set, the debugger will run during the “main” processing phase (after main processing but before postprocessing)
  • >TTI POST toggles the “post” flag. if set, the debugger will run during postprocessing. this is the default

Note that the debugger can be set to run multiple times per turn. For example, you might want to enable the PRE and POST flags simultaneously, so you can check the state of the transcript before and after processing.

When the debugger runs, it will drop execution to a >>> prompt and input will be handled by a little toy debugger command parser. Available commands are:

  • help list the available debugger commands, with a short description of each. use “help [command]” for more help on individual debugger commands
  • exit exit the debugger. execution will resume where it left off. if you have the debugger set to run during multiple processing phases this may immediately drop you back into the debugger, but with the transcript in a different state
  • iter [number] list reports with the given iter_ value
  • list list all reports in the transcript
  • out output (without special formatting) the current contents of the transcript
  • show [number] show the specified report

Simple example of use:

Room Three
This is room three.  Room zero is to the north and room 3B is to the south.

There's a sign on the wall.

You see two pebbles and a box here.

>tti on
Transcript debugger on.

This means the debugger will run on the next (non-system) command:

>put all in box
===breakpoint in postprocessing===
===type HELP or ? for information on the interactive debugger===
>>> 

Instead of displaying the command output, we’re now at the debugger prompt. The banner tells us that we’re at the postprocessing phase, which means the transcript is in its final form. Essentially all command processing has occurred, all that’s happened is that we’re now in the debugger’s input loop instead of proceeding to normal command output.

>>> list
1:0    {obj:CommandSepAnnouncement}
=====
2:1    {obj:GroupSeparatorMessage}
       action = {obj:predicate(PutIn)}
3:1    {obj:MultiObjectAnnouncement}
       action = {obj:predicate(PutIn)}
       dobj_ = pebble @ box
       messageProp_: &announceMultiActionObject
           '<./p0>\n<.announceObj>pebbles:<./announceObj> <.p0>'
4:1    {obj:ImplicitActionAnnouncement}
       action = {obj:predicate(Take)}
       isActionImplicit = true
       dobj_ = pebble @ box
       messageProp_: &announceImplicitAction
           '<./p0>\n<.assume>first taking two pebbles<./assume>\n'
5:1    {obj:ImplicitActionAnnouncement}
       action = {obj:predicate(Open)}
       isActionImplicit = true
       dobj_ = box @ room three
[More]

This is the first page (in an xterm) of output of the list debugger command. The numbers in the left column are the report number, followed by a colon, followed by the iter_ value. Reports with different iter_ values are separated by a =====.

The debugger doesn’t attempt to list everything about each report, just enough to tell what kind of report it is, what action and object(s) it applies to, and so on.

At the end of the listing we can then:

>>> exit
Exiting debugger.
(first opening the box, then taking two pebbles)
You put two pebbles in the box.

>

…and it’ll finish output and return to the command prompt.

Note: exiting the debugger doesn’t toggle the debugger off, so by default it’ll drop into the debugger again after the next command. Use >TTI OFF to disable the debugger.

The interactive debugger is available whenever the module is compiled with the -d flag (or the __DEBUG preprocessor flag is defined some other way).

OTHER DEBUGGING COMMANDS

In addition to the interactive debugger, you can use:

  • >TT ON to enable the transcriptTools processing (this is the defaul)
  • >TT OFF to disable transcriptTools processing
  • >TT ? or >TT STATUS to see the current transcriptTools status

CONCLUSION

Aaaaaanyway. Hopefully this is the last refactor. I think I’ve either got most of the messy corner cases I was worried about…or at least have a mechanism for wrestling with them on an ad hoc basis in the future.

2 Likes