Bubbling Beaker Awards (Award #28, June 28 2024)

The Glulx specification has very little to say about this capability. The basic description of the opcode in section 2.11 reads:

@setiosys L1 L2

Set the I/O system mode and rock. If the system L1 is not supported by the interpreter, it will default to the “null” system (0).

These systems are currently defined:

  • 0: The null system. All output is discarded. (When the Glulx machine starts up, this is the current system.)
  • 1: The filtering system. The rock (L2) value should be the address of a Glulx function. This function will be called for every character output (with the character value as its sole argument). The function’s return value is ignored.
  • 2: The Glk system. All output will be handled through Glk function calls, sent to the current Glk stream.
  • 20: The FyreVM channel system. See section 0.2, “Glulx and Other IF Systems”.

The values 140-14F are reserved for extension projects by ZZO38. These are not documented here.

It is important to recall that when Glulx starts up, the Glk I/O system is not set. And when Glk starts up, there are no windows and no current output stream. To make anything appear to the user, you must first do three things: select the Glk I/O system, open a Glk window, and set its stream as the current one. (It is illegal in Glk to send output when there is no stream set. Sending output to Glulx’s “null” I/O system is legal, but pointless.)

There’s also a little more in section 1.3.5 about how the “filtering” subsystem interacts with the call stack, but it doesn’t do much to illuminate the utility of this opcode.

The normal mode of a running game is mode 2, but the part of interest here is the “filtering system”, which is mode 1. As hinted above, when this mode is set, every character of output is submitted to this routine instead of being handled normally by the interpreter, and the return value is ignored. The utility may not be immediately apparent, but what it allows you to do as the author is inspect every outbound character and decide how to handle it, either by ignoring it, printing it, or transforming it in some way.

Conveniently, from the compiler’s perspective, the name of a routine is functionally the same as its address here.

Since the return value is ignored, the only thing that matters is what your “filtering” routine prints in response to a given input character. As a simple example, imagine a routine to produce “static” in output, such as might happen in a radio transmission:

Include (-

Global	chance_of_static_pct;

[ StaticText char    lc r ;
	lc = glk_char_to_lower(char);
	r = random(100);
	if (r <= (+ percent chance of static +) && lc >= 'a' && lc <= 'z')
		glk_put_char_uni('*');
	else
		glk_put_char_uni(char);
];

-).

Percent chance of static is initially 30.

To activate static filter:
	(- @setiosys 1 StaticText; -).

To return to unfiltered output:
	(- @setiosys 2 0; -).

To say (T - text) with (N - number) percent static:
	now percent chance of static is N;
	activate static filter;
	say T;
	return to unfiltered output.

When play begins:
	say "We're getting close to the energy source, Captain. It's starting to affect our communicators." with 15 percent static.

The logic of the “filtering” routine can be arbitrarily complex. What eu1’s post does is allow any I7 phrase to be swapped in as the “filtering” routine, with the following widget:

Include (-
[ set_io_system phrase;
phrase = phrase-->1;
@setiosys 1 phrase;
];
-).

To switch to printing by (P - a phrase Unicode character -> nothing): (- set_io_system({P}); -).

So, to modify the static example to use this method:

Include (-
[ set_io_system phrase;
phrase = phrase-->1;
@setiosys 1 phrase;
];
-).

To switch to printing by (P - a phrase Unicode character -> nothing): (- set_io_system({P}); -).

Include Unicode Character Names by Graham Nelson.

To add (C - a Unicode character) to the story’s output: (- glk_put_char_uni({C}); -).

newline translates into Unicode as 10. [not in the extension]

Definition: a Unicode character is white space if it is Unicode space or it is Unicode newline.

Percent chance of static is initially 30.

To print (C - a Unicode character) with static (this is static filter):
	if a random chance of percent chance of static in 100 succeeds and C is not white space:
		add Unicode asterisk to the story's output;
	otherwise:
		add C to the story's output.

To say (T - text) with (N - number) percent static:
	now percent chance of static is N;
	switch to printing by static filter;
	say T;
	return to unfiltered output.

When play begins:
	say "The interference is getting worse, Captain." with 35 percent static.
6 Likes

This week’s prize, Bubbling Beaker Award® #25 is presented to @Zed. (EDIT: I’m also pleased to announce that it is belatedly given the Peer Revue™ distinction!) His award-winning post provides some new phrases for looping through the values in relations. While two of them are primarily performance improvements over what the Standard Rules provide for iterating through the members of the left or right sides of a relation, the third provides the ability to loop through relation loading both left and right values into temporary variables. (It never even occurred to me that such a thing would be possible, and it’s not necessary to use I6 to obtain value from that technique.)

This is Zed’s fifth BBA, once again putting him at the top of the leaderboard. Congratulations, Zed! There’s noone madder than the King of Loopiness! If you’re willing to say a few words, I would love to hear more about your motivation in developing it and the kinds of uses you’ve envisioned.

Honorable mention goes to @Celtic_Minstrel, for a technique to manage a command like >WAIT FOR 10 MINUTES (see this thread). At first glance it doesn’t seem that different from the examples in the documentation for this type of command (RB Ex 388 Nine AM Appointment and RB Ex 389 Delayed Gratification), but Celtic_Minstrel’s solution strikes me as more elegant. Rather than trying to interrupt the turn sequence rules or drive them in an abnormal way, using this approach it’s the parse command rule that’s suppressed, allowing the model world to run without waiting for human player input.

3 Likes

Just last month I was talking about how useful custom loops are.

A disadvantage to my solution as written is that it’ll only work in the current development version of Inform, thus it’s only of use right now to someone who’s building Inform from source. Otis, however, provided a 9.3/6M62 translation of looping through relations. I just tried it in 10.1, and with just a small modification (removing the final 2 closing right braces) Otis’ solution works there. 10.1-ready code is below for easy copy-pasting.

10.1 version
To repeat for/with/-- (x - nonexisting K variable) running/-- through/in keys of (r - relation of values of kind K to values of kind L) begin -- end loop:
(-
	for ({-my:0} = BlkValueRead({-by-reference:r}, RRV_STORAGE), {-my:1} = RRV_DATA_BASE + (3 * {-my:0}) :
		  {-my:0} >= 0 : {-my:0}--, {-my:1} = RRV_DATA_BASE + (3 * {-my:0}) ) {
		if (BlkValueRead({-by-reference:r}, {-my:1}) & RRF_USED) {
			if (KOVIsBlockValue({-strong-kind:K}))
				BlkValueCopy({-by-reference:x}, BlkValueRead({-by-reference:r}, {-my:1} + 1));
			else
				{-by-reference:x} =  BlkValueRead({-by-reference:r}, {-my:1} + 1);
			if (1) {-block}
-).

To repeat for/with/-- (y - nonexisting L variable) running/-- through/in values of (r - relation of values of kind K to values of kind L) begin – end loop:
(-
for ({-my:0} = BlkValueRead({-by-reference:r}, RRV_STORAGE), {-my:1} = RRV_DATA_BASE + (3 * {-my:0}) :
{-my:0} >= 0 : {-my:0}–, {-my:1} = RRV_DATA_BASE + (3 * {-my:0}) ) {
if (BlkValueRead({-by-reference:r}, {-my:1}) & RRF_USED) {
if (KOVIsBlockValue({-strong-kind:K}))
BlkValueCopy({-by-reference:y}, BlkValueRead({-by-reference:r}, {-my:1} + 2));
else
{-by-reference:y} = BlkValueRead({-by-reference:r}, {-my:1} + 2);
if (1) {-block}
-).

To repeat for/with/-- (x - nonexisting K variable) and (y - nonexisting L variable) running/-- through/in (r - relation of values of kind K to values of kind L) begin – end loop:
(-
for ({-my:0} = BlkValueRead({-by-reference:r}, RRV_STORAGE), {-my:1} = RRV_DATA_BASE + (3 * {-my:0}) :
{-my:0} >= 0 : {-my:0}–, {-my:1} = RRV_DATA_BASE + (3 * {-my:0}) ) {
if (BlkValueRead({-by-reference:r}, {-my:1}) & RRF_USED) {
if (KOVIsBlockValue({-strong-kind:K}))
BlkValueCopy({-by-reference:x}, BlkValueRead({-by-reference:r}, {-my:1} + 1));
else
{-by-reference:x} = BlkValueRead({-by-reference:r}, {-my:1} + 1);
if (KOVIsBlockValue({-strong-kind:K}))
BlkValueCopy({-by-reference:y}, BlkValueRead({-by-reference:r}, {-my:1} + 2));
else
{-by-reference:y} = BlkValueRead({-by-reference:r}, {-my:1} + 2);
if (1) {-block}
-).

Though the 9.3 → 10.1 differences are slight, both have conspicuous differences from my version which makes use of something new in 11.0: an abstraction layer for dealing with kinds conforming to the pointer value type that’s built on top of the block value layer.

A really fundamental data structure with nigh-infinite application is a collection of mappings from X → Y. In Perl or Ruby these are called Hashes. In Python, it’s a dict. A more generic name is “associative array” (which is also what they’re called in PHP). Javascript objects can be employed this way, too, with some effort.

In inform, a one-to-various relation is the closest thing. But, historically, relations have been buggy, and there are undocumented gotchas. For instance, we can refer to the number of things that enclose a person but the number of numbers that boop a text would get a runtime error:

*** Run-time problem P59: You can't implicitly repeat through the values of this kind: a problem arising from a description which started out here [...]

And the absence of an ability to loop through keys and values at once severely limited utility as a hash. I think this is going to change my Inform programming and to decide what relation [...] will pop up a lot going forward.

A week ago, I wrote another novel loop I find useful for looping through the characters of a text, something that is infeasibly slow for all but the shortest of strings with the best way I7 directly offers.

To repeat with/for (c - nonexisting unicode character variable) running/-- through/in chars/characters of/in/for (t - text) begin -- end loop:
(-
  {-my:0} = {t}-->0;
  @push {-my:0};
  {-my:1} = TEXT_TY_Temporarily_Transmute({-by-reference:t});
  @push {-my:1};
  for ( {-my:1} = 0, {c} = BlkValueRead({-by-reference:t}, 0) : {c} : {c} = BlkValueRead({-by-reference:t}, ++{-my:1} ))
    {-block}
   @pull {-my:1};
   @pull {-my:0};
   TEXT_TY_Untransmute({-by-reference:t}, {-my:1}, {-my:0});
-)

(Also in that thread, Zarf posted a setiosys solution akin to previous BBA award #24.)

I will cherish the King of Loopiness title. :laughing:

5 Likes

Zed is being very generous. My “translation” was about 90% done by him pointing out where to find analagous code in the template and explaining what the new template routines did. My only real contribution was fighting with the 6M62 compiler to find out how the handling of loop inclusions differed. Structurally speaking it’s all his work.

Thanks for explaining, Zed – and for these interesting new toys!

Late breaking news: Although word from the assorted mad scientists wasn’t received in time for the original post, Zed’s latest invention was granted the Peer Revue™ distinction by unanimous acclaim. Zed’s the first person to receive that honor twice – double congratulations are in order!

4 Likes

This week’s prize, Bubbling Beaker Award® #26 is presented to @vaporware. The award-winning post demonstrates a method for using I7 list objects as temporary storage from I6. This is an excellent tool for the tool kit when tinkering in I6 – as vaporware points out, list constructs are a very easy way to leverage heap memory for many purposes.

Although the award is for the general technique, the winning post shows specifically how to use this to save data about the state of the match list, an aspect of the system not covered by anything in the Standard Rules. (One technical note: The code was written in 2012 and uses an outdated syntax for the parameters used by I7 phrases invoking I6 routines. Where it says -pointer-to one should substitute -by-reference to get it to work today.)

This is the first BBA awarded to vaporware, who has a long history with Inform and has managed some very impressive feats (such as the Dynamic Objects extension). Congratulations! If you would like to say a few words about your invention, please feel free to take the floor.

5 Likes

I thought I’d adapt the technique to construct something a little easier to work with in I7:

a match-list-kind is a kind of object.
a match-list-kind has a number called the number-matched.
a match-list-kind has a number called match-from.
a match-list-kind has a number called token-filter.
a match-list-kind has a number called match-length.
a match-list-kind has a number called number-of-classes.
a match-list-kind has a number called oops-from.
a match-list-kind has a list of objects called the match-list.
a match-list-kind has a list of numbers called the match-classes.
a match-list-kind has a list of numbers called the match-scores.

The number-matched property translates into I6 as "number_matched_prop".
The match-from property translates into I6 as "match_from_prop".
The token-filter property translates into I6 as "token_filter_prop".
The match-length property translates into I6 as "match_length_prop".
The number-of-classes property translates into I6 as "number_of_classes_prop".
The oops-from property translates into I6 as "oops_from_prop".
The match-list property translates into I6 as "match_list_prop".
The match-classes property translates into I6 as "match_classes_prop".
The match-scores property translates into I6 as "match_scores_prop".

the global match list is a match-list-kind.
the global match list object translates into I6 as "global_match_list".

To set the/-- match globals: (- SetMatchGlobals(); -).

Include (-
[ SetMatchGlobals i;
global_match_list.number_matched_prop = number_matched;
global_match_list.match_from_prop = match_from;
global_match_list.token_filter_prop = token_filter;
global_match_list.match_length_prop = match_length;
global_match_list.number_of_classes_prop = number_of_classes;
global_match_list.oops_from_prop = oops_from;
LIST_OF_TY_SetLength(global_match_list.match_list_prop, number_matched, 0);
LIST_OF_TY_SetLength(global_match_list.match_classes_prop, number_matched, 0);
LIST_OF_TY_SetLength(global_match_list.match_scores_prop, number_matched, 0);
for ( i = 0 : i < number_matched : i++ ) {
LIST_OF_TY_PutItem(global_match_list.match_list_prop, i + 1, match_list-–>i);
LIST_OF_TY_PutItem(global_match_list.match_classes_prop, i + 1, match_classes-–>i);
LIST_OF_TY_PutItem(global_match_list.match_scores_prop, i + 1, match_scores-–>i);
}
];
-)

to dump match list:
  say "number matched: [number-matched of global match list].";
  say "match from: [match-from of global match list].";
  say "token filter: [token-filter of global match list].";
  say "match length: [match-length of global match list].";
  say "number of classes: [number-of-classes of global match list].";
  say "oops from: [oops-from of global match list].";
  repeat with i running from 1 to number-matched of global match list begin;
    say "[i]. match: [entry i in match-list of global match list] ; ";
    say "class: [entry i in match-classes of global match list] ; ";
    say "score: [entry i in match-scores of global match list].";
  end repeat;

lab is a room.
the red paper is in the lab.
the red tree is scenery in the lab.
the red ghost is an undescribed thing in the lab.
the player holds the red pen.

every turn: set match globals;
dump match list;

test me with "x red".

Output is:

lab
You can see a red paper here.
 
>test me
(Testing.)
 
>[1] x red
(the red pen)
You see nothing special about the red pen.
 
number matched: 4.
match from: 2.
token filter: 0.
match length: 1.
number of classes: 0.
oops from: 0.
1. match: red paper ; class: 0 ; score: 2156.
2. match: red tree ; class: 0 ; score: 2146.
3. match: red ghost ; class: 0 ; score: 2056.
4. match: red pen ; class: 0 ; score: 2176.
1 Like

posted that and promptly had a better idea.

number-matched is a number variable.
the number-matched variable translates into I6 as "number_matched".

match-from is a number variable.
the match-from variable translates into I6 as "match_from".

token-filter is a number variable.
the token-filter variable translates into I6 as "token_filter".

match-length is a number variable.
the match-length variable translates into I6 as "match_length".

number-of-classes is a number variable.
the number-of-classes variable translates into I6 as "number_of_classes".

oops-from is a number variable.
the oops-from variable translates into I6 as "oops_from"

match-list-entry is a kind of object.
a match-list-entry has an object called the matched-object.
a match-list-entry has a number called the match-classes.
a match-list-entry has a number called the match-score.

dummy-mle is a match-list-entry.
the dummy-mle object translates into I6 as "dummy_mle".

the matched-object property translates into I6 as "matched_object_prop".
the match-classes property translates into I6 as "match_classes_prop".
the match-score property translates into I6 as "match_score_prop".

to repeat with/for (mle - nonexisting match-list-entry variable) running/-- through/in the/-- global match list begin -- end loop:
(- for ({-my:0} = 0, {mle} = dummy_mle, 
dummy_mle.matched_object_prop = match_list-->{-my:0},
  dummy_mle.match_classes_prop = match_classes-->{-my:0},
  dummy_mle.match_score_prop = match_scores-->{-my:0}
: {-my:0} < number_matched :
{-my:0}++,
dummy_mle.matched_object_prop = match_list-->{-my:0},
  dummy_mle.match_classes_prop = match_classes-->{-my:0},
  dummy_mle.match_score_prop = match_scores-->{-my:0}
)
-)

lab is a room.
the red paper is in the lab.
the red tree is scenery in the lab.
the red ghost is an undescribed thing in the lab.
the player holds the red pen.

every turn:
  say "number matched: [number-matched].";
  say "match from: [match-from].";
  say "token filter: [token-filter].";
  say "match length: [match-length].";
  say "number of classes: [number-of-classes].";
  say "oops from: [oops-from].";
let i be 1;
repeat for mle in the global match list begin;
  say "[i]. [matched-object of mle] ; class: [match-classes of mle] ; score [match-score of mle].";
increment i;
end repeat;

test me with "x red".
1 Like

This week’s prize, Bubbling Beaker Award® #27 is presented to @Erik_Temple, another figure who makes frequent appearance in the archives of this forum. His award-winning post refines a method introduced by @Ron_Newcomb at the start of the thread, which demonstrates a method for using the Glulx @linearsearch opcode to speed up row access for large tables.

This is the first BBA awarded to Erik_Temple. Congratulations! (Since he last visited in the fall of last year, I’ll post a brief write-up of the technique soon.)

More-than-honorable mention goes to Ron_Newcomb for the breakthrough conception and initial implementation of a working program using the technique; I chose Erik_Temple as the award recipient because his construction is easier to work with and to understand… and because ties would create problems for the leaderboard. (Though Ron_Newcomb is at another level entirely compared to me, so perhaps I just didn’t understand why his version was superior!)

Also: Extra special thanks to @Zed for suggesting this topic as a BBA candidate. As a reminder, anyone can nominate a post for consideration, so please do if you happen to come across something notable.

4 Likes

The technique depends on a working knowledge of the I6 infrastructure underlying the implementation of tables in I7, plus knowledge of Glulx opcodes. In short, the demonstration code:

  1. leverages the I7 template layer to find the address of the array used to store a particular table column’s data, then
  2. uses a specialized Glulx opcode to iterate through the values stored in that column much faster than can be accomplished via an I6 loop.

If the sought value is found, the return result from the opcode is used to determine in which row of the table it is located, and internal variables are set accordingly to make that the current row.

The syntax of some of the special inclusion bits have changed since the post originally went up, but here’s a working version for 6M62:

Section - Newcomb-Temple Table Search Acceleration

Include (-
Global seekStartAddress;
Global seekRows;
Global seekVal;
Global seekReturnValue;
-) after “Definitions.i6t”.

To select a/the/-- row with (TC - a word value valued table column) of (W - a word value) in/from (T - table name):
(-
	{-my:ct_0} = {T};
	{-my:ct_1} = -1;
	seekVal = {W};
	seekRows = TableRows({T});
	seekStartAddress = ({T})-->(TableFindCol({T}, {TC}, true)) + (COL_HSIZE * WORDSIZE);
	@linearsearch seekVal WORDSIZE seekStartAddress WORDSIZE seekRows 0 4 {-my:ct_1};
	if ({-my:ct_1} < 1) return RunTimeProblem(RTP_TABLE_NOCORR, {T});
-).


Table of Smiles
smile number	key
0	“: )”
1	“; )”
2	“:->”
with 2 blank rows.

When play begins:
	showme the contents of Table of Smiles;
	say "Rows: [number of rows in the Table of Smiles].";
	repeat with N running from 0 to 2:
		select row with a smile number of N in the Table of Smiles;
		say "smile [N] is [key entry] by the lights of the new select… phrase."
1 Like

This week’s prize, Bubbling Beaker Award® #28, is presented to Aaron Reed (@aaronius). Aaron Reed is the author of the game Blue Lacuna (among many others) and the book Creating Interactive Fiction with Inform 7. In this award-winning post from 2011, he looked into the problem of trying to make parser failure more intelligible to the untutored player, and in doing so dug deep into the parser code to tinker with the parts that decode a game’s recognizable grammar lines. This portion of the code has very limited documentation outside the code itself, much of it found in the obscure Inform Technical Manual.

This is the first BBA awarded to aaronius. Congratulations! The code in the post has unfortunately been a bit mangled by the forum’s conversion, but it will work more or less as-is once proper spacing is restored and em-dashes are converted back to double hyphens (--) when part of array access constructions. I believe that this technique (or its refined successor), along with many other pioneering user interface experiments, became part of the Smarter Parser extension, so it’s recommended to just look at that.

1 Like

I’ve taken the opportunity to fix the code tags, so you can now see it properly.

3 Likes

I did a 6M62 update of some of the code by Aaron and Andrew there along with some other mucking around with it: Parser Error Details.

2 Likes

Hey, great! Thanks, Zed!