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