[I6] Using ChooseObjects() to provide preference scores fundamentally changes the meaning of "all"?

I was experimenting with ChooseObjects() to better understand why it is included as an entry point, and I found some behavior that doesn’t seem to make any sense, which is driven by code that also doesn’t seem to make any sense.

In the test scenario, the idea is for the parser to prefer objects with “cooler” colors, i.e. colors closer to violet than red in terms of wavelength:

(Expand for complete source code)
Constant Story "ChooseObjects Exercise";
Constant Headline "^from p. 239^";

Include "Parser";
Include "VerbLib";
! inclusion of "Grammar" following ChooseObjects() definition

! for easier reading of ChooseObjects() "code" parameter
Constant WILL_EXCLUDE_FROM_ALL = false;
Constant WILL_INCLUDE_IN_ALL = true;
Constant CHOOSING_BETWEEN_OBJECTS = 2;

! for easier reading of ChooseObjects() return values
Constant NO_OPINION = 0;
Constant FORCE_INCLUDE = 1;
Constant FORCE_EXCLUDE = 2;

[ ChooseObjects obj code ;
    switch(code)
        {
            CHOOSING_BETWEEN_OBJECTS:
                if (obj ofclass ColoredItem)
                    return ColorWordValue(obj.color); ! red = 1, ..., purple = 6
                else
                    return 0; ! no preference
            WILL_INCLUDE_IN_ALL:
                    return NO_OPINION; ! don't force either way
            WILL_EXCLUDE_FROM_ALL:
                    return NO_OPINION; ! don't force either way
            default:
                print "<ChooseObjects error: unknown code>";
                return false;
        }
];

Include "Grammar";

Array color_words table 'red' 'orange' 'yellow' 'green' 'blue' 'purple';

[ ColorWordValue wd   i ;
    for (i=1: i<=(color_words-->0): i++)
        {
            if (wd == color_words-->i)
                return i;
        }
    return false;
];

Class Room
    has light;

Class ColoredItem
    with    color NULL,
            parse_name
                [ wd n ;
                    ! DISTINGUISH
                    if (parser_action == ##TheSame)
                        {
                            if (parser_one.color == parser_two.color)
                                return -1; ! different
                            else
                                return -2; ! same
                        }

                    ! MATCH
                    while ( ((wd = NextWord()) ~= nothing) &&
                             ((wd == self.color) || (WordInProperty(wd, self, name))) )
                        {
                            n++;
                            if (wd->#dict_par1 & 4) ! plural word
                                parser_action = ##PluralFound;
                        }
                    return n;
                ],
            short_name
                [;
                    print (address) self.color, " ", (address) self.&name-->0;
                    return true;
                ];

Class Ball
    with    name 'ball' 'balls//p';

Class ToyBall
    class   ColoredItem Ball;

Room Start "Starting Point"
    with    description
                "An uninteresting room.",
            e_to End;

Toyball red_ball Start
    with    color 'red';

Toyball orange_ball Start
    with    color 'orange';

Toyball yellow_ball Start
    with    color 'yellow';

Room End "Ending Point"
    with    description
                "Another uninteresting room.",
            w_to Start;

Toyball green_ball End
    with    color 'green';

Toyball blue_ball End
    with    color 'blue';

Toyball purple_ball End
    with    color 'purple';

[ Initialise ;

    location = Start;

];

This works, but it creates very strange behavior. I looked over the description of the object preference algorithm on DM4 pp. 240-242, and I tried to match the behavior to the applicable rule. (Note that between each excerpt, the game was restarted.)

Case #1 (rule 6d)

>TAKE BALL
(the yellow ball)
Taken.

>G
(the yellow ball)
You already have that.

This appears to be working as described, as the yellow ball would be marked “good” and has the highest score. However, the rules for definite mode (as compared to indefinite mode) don’t reject items based on their location for ##Take or ##Drop, which makes for the unexpected result of trying to take what’s already held.

Case #2 (rule 7ip)

>TAKE TWO BALLS
yellow ball: Taken.
orange ball: Taken.

>G
You can't see any such thing.

The first response is working as intended – the two highest-scoring available balls are chosen. The second response is defensible in terms of parsing failure but seems like it should have a different error message (for TOOFEW_PE, as cited on DM4 p. 237).

Case #3 (rules 7ip(iv) and BG?)

>TAKE ALL
yellow ball: Taken.

>G
There are none at all available!

The first response does not work as intended. (“All” means all.) The second response is the parser being consistent in its assertion that the only ball that qualifies under “all” is the yellow one. The behavior seems to stem from an odd line of code related to the implementation of rule 7ip, subrule iv. The wording of the rule in the DM4 was indeciphereable to me, but the code in question (in the Adjudicate() routine, with added comments) is:

        if (sovert == -1) sovert = bestguess_score/SCORE__DIVISOR; ! bestguess_score_MAX
        else {
            if (indef_wanted == 100 && bestguess_score/SCORE__DIVISOR < sovert) ! bestguess_score_CURRENT
                flag = 0;
        }

Note that the functional meaning of bestguess_score changes significantly between the if clause and the else clause. The if clause, triggered when sovert is not yet defined, sets sovert based on the bestguess_score corresponding to the highest-scoring object. Once set, sovert will not be changed during execution of the enclosing for loop. The else clause makes use of the bestguess_score for the object currently being considered, which will always be something different than the highest-scoring object (because if the else clause is being executed, that means sovert has already been set by the highest-scoring object on a previous pass).

Trying to match the words in the 7ip-iv definition with their implementation code, I read the English rule definition as:

or the target number [indef_wanted] is “unlimited” [100] and S/20 (rounded down to the nearest integer) [bestguess_score(_CURRENT)/SCORE__DIVISOR] has fallen below its [the expression S/20’s] maximum value [sovert], where S [bestguess_score(_CURRENT)] is the score of the object.

I can’t make any sense of the program logic here, specifically why there is division by SCORE__DIVISOR at all. If x<y, then naively x/20 < y/20. In I6 world, with no floating point numbers, integer division means that small differences between x and y might (depending on modulo results) be erased by the division process – for example, if x == 2001 and y == 2012, then x/20 == y/20. (But if x == 1999 and y == 2000, then x/20 < y/20!) The only score differences small enough to be erased this way would be those from rule 5-v (scenery/~scenery), 5-vi (actor/not actor) and 5-vii (GNA matches), but it’s not clear why erasing those would be desirable.

Regardless, what happens in Case #3 is that the yellow ball sets the value of sovert, and then every other ball comes up short (as they must, given that a 1-point difference in preference translates to a 1,000-point difference in score) and is excluded from all (when flag is set to zero). (I suppose that, if there were other yellow balls present, they would be be included because bestguess_score(_CURRENT)/20 would equal sovert.)

Does anyone know what the original functional intent of rule 7ip-iv was? The English version of the rule describes the “what”, but it omits the “why” that motivates the rule in the first place. I know that it is possible to override the decision of Adjudicate() using ChooseObject()'s response to a different code, but the DM4 makes no mention of the fact that using ChooseObjects() to provide preference weighting may cause a radical change in the meaning of “all”!

Case #4 (strangest of all)

>TAKE BALLS
yellow ball: Taken.

>G
That can't contain things.

The first response seems to have the same root cause as in Case #3. The second response comes from something else. I think that the multi token for the ##Take grammar is failing completely this time because all non-yellow balls are deemed inapplicable. The parser then tries several other grammar lines (which can be seen with >TRACE active) and must be getting a “more interesting” best_etype set by one of those (##Remove?).

All of this leads up to my typical question: Is this a bug? And, if not, what’s the proper perspective to understand the existing behavior? (This time I triple-checked that the code in question is the same in the 6.12.4 parser.)

1 Like

(Please keep in mind that people are following along at http://www.inform-fiction.org/manual/html/contents.html , which doesn’t have page numbers.) (This stuff is in chapter 33.)

@zarf, thank you for pointing that out. I’ll be sure to cite the online version as well in the future.

I’m afraid my answer to the original question is “I don’t know what 7ip-iv is for.” This is where a good suite of test cases would be handy.

I appreciate that you took the time to look it over, @zarf. Any commentary with respect to whether or not this behavior should be deemed a bug?

In thinking about how this is working now, two things occurred to me:

  1. If the intent is to erase differences less than 20 points, rule 7ip-iv is an unexpected way to go about it. Why not just check bestguess_score(_CURRENT) > (sovert - 20)?

  2. Part of the reason for the behavior is the giant weight that Adjudicate() puts on numbers returned by ChooseObjects() (i.e. return value x 1000). If I’m doing the arithmetic correctly, even returning 1 swamps all of the other automatic score factors. Why not just allow ChooseObjects() to return any legal number and then add that number (guarding against overflow) to the score which is calculated automatically? That way the author could precisely specify the impact on scoring.

I’m curious as to what kinds of real-world use ChooseObjects() has been put to by other people. If anyone reading this has tried to use ChooseObjects() and been disappointed, would changing the way that it works make it more useful to you?

The high weight of ChooseObjects is intended to let it override any weight the library applies. Nearly always, if you want to customize the disambiguation process, you want your value to win. Authors don’t want to have to remember “Oh, I need to return 1000 or more or else the library will mostly ignore me.”

If you want a typical example of ChooseObjects in a complex I6 game, look at my Dreamhold source code. https://eblong.com/zarf/ftp/dreamhold-src.tar.gz

@zarf, thank you for pointing me to your Dreamhold source code. (And for sharing it with the public!) Assuming that I understood it correctly, I found it interesting that your use of ChooseObjects() seems to set a baseline return value of 4 for any object not caught by special conditions, so that returning a lower number would effectively penalize an object. Is this because the spec only allows for positive numbers to be returned from ChooseObjects()? Out of curiosity: Since the lowest value returned for anything is 2 and the highest value returned is 5, is there a functional difference between that and just rescaling for range 0 to 3 (with default 2)?

Also, I didn’t see anything that would involve the return values being used in indefinite mode. (In fact, the one mention of indefinite mode is when the weighting for masks checks to make sure that it won’t provide weighting in indefinite mode. All of the other conditions are for verbs that would apply to single objects.) Most of the unexpected behavior that I’m pointing to in the original post is linked to the way that ChooseObjects() operates in indefinite mode. (The exception is Case #1.) Did I miss something?

No, why would there be? I could have chosen any values from 0 to 5.

With the exception that you noted, the return values are used regardless of indefinite mode.

@zarf, forgive my imprecision. I should have said: “I didn’t see anything that would have caused weightings to be returned in indefinite mode in a situation where that would have meaningfully influenced the choice of objects.”

I highlighted the exception because that would definitely apply to >TAKE ALL or >TAKE MASKS, and in that case, the routine refrains from providing a weighting.

In all of the other cases I could think of, it seemed either a) the routine would be uniformly returning 4 for objects in a group (so no functional preference is created), or b) the probable role of the object with a non-4 weighting would not matter, because it would not be part of a group (e.g. the labtable object might be the second noun for ##PutOn, but second can’t hold a group).

Am I missing something obvious here? (It certainly happens sometimes!) I am genuinely trying to understand the way that ChooseObjects() is designed to work, and I appreciate your willingness to provide guidance.

I don’t have a complete account of why I wrote that code way back then. I’m sorry. You asked for an example, there it is.

My best reconstruction is that I was trying to separate the handling of TAKE ALL from naming of a specific object; I wanted to customize them (mostly) separately.

@zarf, there’s certainly no need to apologize; I didn’t mean to phrase my request as a demand, and I appreciate your continued patience with my many questions. I only pressed the point because I had hoped that your reason for suggesting the Dreamhold source code was that it had a direct bearing on the unexpected behavior mentioned in the original post, and that there was some aspect of the code that was escaping me.

It’s certainly a helpful example of an “in the wild” use of ChooseObjects() in a well-regarded work, so thank you again for directing me (and others happening upon this thread) to it. I’ll keep pondering Adjudicate(), which is the real source of the unexpected behavior even though it’s only prompted to that behavior via use of ChooseObjects().

Anybody looking for a simple fix limited to restoring normal >TAKE ALL behavior in this situation – an approach that is probably sufficient if there are no meaningful carrying limits in your game – is encouraged to look at Roger Firth’s solution from his Inform 6 FAQ (an extremely valuable resource which I can’t recommend enough). The full write-up can be found at: http://www.firthworks.com/roger/informfaq/tt.html#9

The relevant code excerpt is as follows:

  [ ChooseObject obj code;
      switch (code) {
       2: ! Parser wants an 'appropriateness' hint for obj
          if (indef_wanted == 100) return 0;
          ! Inspect obj, and then...
          ...
  ];

The parser global variable indef_wanted should equal 100 only in cases where the player has entered keyword 'all' in the command (or a synonym like 'every', or possibly if they type something like >TAKE 100 TRIBBLES or >TAKE 999 TRIBBLES), so this cleanly bypasses the Adjudicate() routine’s side effects for indefinite mode by not returning preference scores. This is similar to the method used by zarf in his Dreamhold code, though in that case the bypass is in place only for certain objects. (Thank you again for the pointer, zarf.)

1 Like

On the other hand, if you want behavior like this (as I do):

>TAKE BALL
(the yellow ball)
Taken.

>G
(the orange ball)
Taken.

or if you would like to learn a little more about how the I6 parser’s disambiguation works, then keep reading. (Fair warning: This is long. You may want to skip ahead to the case analysis part to see the “what” before you go back to read the “why” and “how”.)

The assumed desired behavior is to make sure that the most preferred items are processed first in response to a command like >TAKE ALL TRIBBLES – as is the normal Standard Library behavior when responding to commands like >TAKE TWO TRIBBLES, which include a “demanding number” in DM4 terminology. To get this it seems easiest to modify the way that Adjudicate() works.

Note that the version of Adjudicate() that I was using in my original post was from StdLib 6/11. StdLib 6.12.4 already fixes the incorrect parsing error message for the TOOFEW_PE result in Case 2, which seems to have had some other cause outside of Adjudicate(). Other case behavior is identical to what is seen under 6/11.

To obtain something close to the naively-expected behavior – again, in my case, defined as working so that all operations involving multiple objects are performed such that those objects with the highest preference scores are processed first – it is sufficient to simply comment out the following lines in Adjudicate():

!        if (sovert == -1) sovert = bestguess_score/SCORE__DIVISOR;
!        else {
!            if (indef_wanted == 100 && bestguess_score/SCORE__DIVISOR < sovert)
!                flag = 0;
!        }

This works for both the 6/11 Standard Library and the 6.12.4 Standard Library, and it fixes the object preference behavior for Case 2, Case 3 and Case 4. However, it does not fix Case 1 (definite mode), nor does it fix the strange parser response for Case 4 when there are no more matching objects to take.

It is possible to correct the behavior in definite mode. Case 1’s behavior is driven in part by a routine called SingleBestGuess(), which is identical between StdLib 6/11 and StdLib 6.12.4 [** comments added]:

[ SingleBestGuess  earliest its_score best i;
    earliest = -1; best = -1000;
    for (i=0 : i<number_matched : i++) {
        its_score = match_scores-->i;
        if (its_score == best) earliest = -1;  ! ** if two items share highest score, earliest is -1/NULL
        if (its_score > best) { best = its_score; earliest = match_list-->i; } ! ** for new high score, set best and earliest
    }
    bestguess_score = best;
    return earliest; ! ** an object ID if and only if just one object had the high score
];

SingleBestGuess() is called by this block in Adjudicate() [** comments added]:

! ** BLOCK: DEFINITE MODE - DECIDE ON SINGLE BEST SCORING ITEM IF ONE EXISTS
if (indef_mode == 0) {
    !  Is there now a single highest-scoring object?
    i = SingleBestGuess(); ! ** assigns -1/NULL if no single item found, otherwise item with unequaled highest score
    if (i >= 0) { ! ** equivalent to (i ~= NULL)

        #Ifdef DEBUG;
        if (parser_trace >= 4) print "   Single best-scoring object returned.]^";
        #Endif; ! DEBUG
        return i; ! ** if there's one item with a highest and unequaled score, then return item ID to NounDomain()
    }
}

In definite mode cases where there is a “single best guess” (i.e. only one item has the highest score), this item is decided to be the winner without further consideration of context or activity. In the Case 1 example, although the held yellow ball will score slightly less than it would if it were not held, the penalty is too little to matter in the face of the heavy weighting of different preference scores for different colors, and the yellow ball will still have the highest score of any ball in scope. Thus, while holding a yellow ball in a location with no other yellow balls, the Case 1 command >TAKE BALL will see the yellow ball as the “single best guess” and return it as the adjudicated best match, even though it is already held.

In cases where there is not a single best guess (i.e. two or more items share the highest score), Case 1 behavior is driven by a related function called BestGuess(), which is also identical between StdLib 6/11 and StdLib 6.12.4 [** comments added]:

[ BestGuess  earliest its_score best i;
    earliest = 0; best = -1;
    for (i=0 : i<number_matched : i++) {
        if (match_list-->i >= 0) {        ! ** if the match_list entry has a positive score...
            its_score = match_scores-->i; ! ** ... remember it in its_score
            if (its_score > best) { best = its_score; earliest = i; } ! ** redefine best and earliest if new high score
        }
    }
    #Ifdef DEBUG;
    if (parser_trace >= 4)
      if (best < 0) print "   Best guess ran out of choices^";
      else print "   Best guess ", (the) match_list-->earliest, " (", match_list-->earliest, ")^";
    #Endif; ! DEBUG
    if (best < 0) return -1;    ! ** if only negative scores, return -1/NULL
    i = match_list-->earliest;  ! ** i will be return value
    match_list-->earliest = -1; ! ** remove winning entry from the match list
    bestguess_score = best;     ! ** sets global bestguess_score, for use by rule 7ip-iv logic in Adjudicate()
    return i;                   ! ** return earliest object in match_list that shares high score
];

BestGuess() is called at the tail end of Adjudicate(), after constructing “match classes” (i.e. sets of indistinguishable objects) and checking to make sure that there aren’t two items from different match classes that share the highest score (in which case the parser will ask a “Which do you mean...?” question).

For the BestGuess() routine to matter, there must be a choice of multiple items in scope that share the highest score. For example, the player might be holding one yellow ball while three other yellow balls are in the room. In this case, the held yellow ball, having been scored slightly less than non-held yellow balls by ScoreMatchL(), will be discounted, and one of the non-held yellow balls (the “earliest” in the match list, though the order doesn’t matter) will be selected as the “best guess”. Thus, >TAKE BALL will continue to take yellow balls via “best guess” behavior until only one of them is not held. At that point, the unheld yellow ball will qualify as the “single best guess” and be chosen. After that, all yellow balls are held and will share the same score (substantially outscoring balls of less-preferred colors orange and red), so a held yellow ball will be selected (inappropriately) as the “best guess” even when other balls that are more likely to match the player’s intent are present.

Both SingleBestGuess() and BestGuess() make use of the scoring determined during an earlier block of Adjudicate() [** comments added, note that this is the StdLib 6/11 version, but the StdLib 6.12.4 version is essentially the same unless constants TRADITIONAL_TAKE_ALL or NO_TAKE_ALL are defined]:

! ** BLOCK: IDENTIFY "GOOD" OBJECTS IN MATCH LIST
j = number_matched-1; good_ones = 0; last = match_list-->0;
for (i=0 : i<=j : i++) {
    n = match_list-->i;     ! ** set n to next object in match_list
    match_scores-->i = 0;   ! ** delete any match_scores value associated with n

    good_flag = false;      ! ** default "good" status to false, will look for qualifying reasons

    switch (context) {
      HELD_TOKEN, MULTIHELD_TOKEN:
        if (parent(n) == actor) good_flag = true; ! ** items directly held by actor are "good" for *HELD tokens
      MULTIEXCEPT_TOKEN:
        if (advance_warning == -1) {    ! ** if no provisional second noun determined  ...
            good_flag = true;           ! ** then n is automatically "good"
        }
        else {
            if (n ~= advance_warning) good_flag = true; ! ** ... or if provisional second noun isn't n, then n is "good"
        }
      MULTIINSIDE_TOKEN:
        if (advance_warning == -1) {    ! ** if no provisional second noun determined ...
            if (parent(n) ~= actor) good_flag = true; ! ** then n is "good" if not held by actor
        }
        else {
            if (n in advance_warning) good_flag = true; ! ** ... or if n is directly inside provisional 2nd noun, it's "good"
        }
      CREATURE_TOKEN:
        if (CreatureTest(n) == 1) good_flag = true; ! ** ... if n is animate, or if n is talkable and it's a speech-like action
      default:
        good_flag = true; ! ** for any other token type, n is "good" by default
    }

    if (good_flag) {
        match_scores-->i = SCORE__IFGOOD;   ! ** give n a basic score for being "good"
        good_ones++; last = n;              ! ** add to the count of "good" objects; note last object checked
    }
}
if (good_ones == 1) return last;    ! ** if there's only one good item, then that's the winning item

! If there is ambiguity about what was typed, but it definitely wasn't
! animate as required, then return anything; higher up in the parser
! a suitable error will be given.  (This prevents a question being asked.)

if (context == CREATURE_TOKEN && good_ones == 0) return match_list-->0;

if (indef_mode == 0) indef_type=0; ! ** make indef_type consistent with indef_mode for definite mode

ScoreMatchL(context);
if (number_matched == 0) return -1; ! ** if number_matched reduced to zero by ScoreMatchL(), return -1/NULL to NounDomain()

Each item in the match list is scored via a routine called ScoreMatchL(), which applies all of the numeric scoring rules detailed in DM4 section 33, except for the “good” bonus (which is applied immediately after deciding whether the object is “good”).

To obtain more sensible disambiguation behavior in the context of preference scoring via ChooseObjects(), it is sufficient to remove the preference bonus for items marked “good” if they don’t “fit” the context of the grammar line being tested with respect to held/~held status. This can be done with a new block inserted just after the call to ScoreMatchL():

! ** NEW BLOCK: "DISQUALIFY" OBJECTS BASED ON ACTIVITY CONTEXT
for (i=0: i<number_matched: i++) {
    if ( (match_list-->i ~= NULL or nothing) &&
            ( ((parent(match_list-->i) == actor) && (context ~= HELD_TOKEN or MULTIHELD_TOKEN)) ||
              ((parent(match_list-->i) ~= actor) && (context == HELD_TOKEN or MULTIHELD_TOKEN)) ) ) {
        Disqualify(match_list-->i);
    }
}

with the new routine Disqualify() defined as:

[ Disqualify disqualified   i ;
    if (parser_trace >= 4)
        print "^<removing preference for  ", (name) disqualified, ">";
    for (i=0: i<number_matched: i++) {
        if (match_list-->i == disqualified) {
            match_scores-->i = match_scores-->i - (SCORE__CHOOSEOBJ * ChooseObjects(disqualified, 2));
            if (parser_trace >= 4)
                print "<adjusted score = ", match_scores-->i, ">";
            break;
        }
    }
];

This is not the most efficient code, but it minimizes the tampering with core machinery and prevents the need to replace routines other than Adjudicate(), which already must be modified to eliminate rule 7ip-iv logic. (It would certainly be more efficient to modify ScoreMatchL() – which is passed the value of context as a parameter – with something analogous to the “new block” above, to prevent adding the suppressed preference score in the first place. Alternatively, similar logic could be implemented via ChooseObjects(), with the additional burden as described below.)

With this modification in place, the generated behavior seems more sensible to me:

Case #1 (definite mode)

You can see a red ball, an orange ball and a yellow ball here.

>TAKE BALL
(the yellow ball)
Taken.

>G
(the orange ball)
Taken.

Note that previously the already-held yellow ball would be (inappropriately) selected for the repeated action. Now the best color not currently held is selected.

Case #2 (indefinite mode, with demanding number)

You can see a red ball, an orange ball and a yellow ball here.

>TAKE TWO BALLS
yellow ball: Taken.
orange ball: Taken.

>G
Only one of those is available.

Behavior is (appropriately) identical to before. Note that the version shown is for StdLib 6.12.4, because StdLib 6/11 responds “You can't see any such thing.” for the repeated action due to an unrelated issue in that version (as noted previously).

Case #3 (indefinite mode, taking all)

You can see a red ball, an orange ball and a yellow ball here.

>TAKE ALL
yellow ball: Taken.
orange ball: Taken.
red ball: Taken.

>G
There are none at all available!

Note that all balls count in the definition of “all,” and they are taken in preference order. (If present, non-ball objects with no preference scoring are also included.) Previously, “all” was interpreted as meaning the single yellow ball. The parsing error on the repeated action is appropriate, as there are no further items to take.

Case #4 (indefinite mode, without demanding number)

You can see a red ball, an orange ball and a yellow ball here.

>TAKE BALLS
yellow ball: Taken.
orange ball: Taken.
red ball: Taken.

>G
What do you want to take those things from?

Again, all balls are taken, in preference order. The clarifying question for the repeated action reflects the parser’s attempt to determine a second noun for a ##Remove action, after the grammar lines for ##Take have failed. This at least doesn’t seem worse than the previous response, which was “That can't contain things.

It’s possible that this modification will cause problems not identified by my limited testing.

One could easily argue that the same type of modification could be achieved via a suitably-constructed ChooseObjects() routine. However, note that this entry point does not have access to the value of context (i.e. the type of grammar token being evaluated), so it would be necessary to define behavior on a per-action basis instead of via the type of token being matched. This is not impossible, but it is nonetheless an added burden on the author. With the modifications outlined, suitable behavior will result even if new actions are defined, because any problematic preference weighting will be automatically “disqualified” on the basis of the action’s associated grammar tokens.

More to the point, while giving full credence to the DM4 quote that “Experience also shows that no two people ever quite agree on what the parser should ‘naturally’ do.” (p. 239, online at https://inform-fiction.org/manual/html/s33.html), nobody seems to be able to justify the current behavior of the Adjudicate() routine in indefinite mode when preference scoring is being provided by ChooseObjects(). It is very counterintuitive that activating this entry point for its designed purpose should essentially break indefinite mode disambiguation. At least two very smart people (zarf and Roger Firth) seem to have decided that, as a practical matter, the best way to handle the situation is to write ChooseObjects() such that it doesn’t provide any preference at all in indefinite mode. (Having spent most of a month’s free time looking at this in detail, I think I understand why.) The solution above attempts to overcome this core issue instead of simply bypassing it.

2 Likes