Bubbling Beaker Awards (Award #23, Apr 19 2024)

This week’s prize, Bubbling Beaker Award® #11 is presented to @Draconis, which marks his second win. His award-winning post lays out a method for allowing the player to issue commands to multiple NPCs in a group, a la Infocom’s Suspended. I haven’t delved into the technical details, but from what I understand about the parser, this is not a simple modification. This work was developed into a public extension.

Congratulations, Draconis! Perhaps you would be willing to say a few words about what it took to get this to work?

[An important note: Award frequency will be decreasing to every other week between now and the end of the year, so the next award will be for Fri Nov 17.]

5 Likes

Oh, man, this one was a while ago! I need to refresh my memory for how this works!

Okay. Let’s imagine we’re giving the command ALICE AND BOB, GO NORTH.

The way the parser works by default, if it sees a comma, it jumps back to the beginning of the command and calls NounDomain. NounDomain is the routine that tries to parse the name of a single object, so most of the other parsing routines delegate to it.

That’s the bit that I modified: instead of calling NounDomain, I make it instead call ParseToken(ELEMENTARY_TT, MULTI_TOKEN)—that is, try to parse a “[things]” token.

This “[things]” token will fail, because we already know the list of actors ends with a comma—that’s how we ended up here in the first place. (If “[things]” sees something after a comma that it can’t understand, it thinks there’s a bad entry in the list.) But as it works, it puts the list of things it parses into the multiple object list, and it doesn’t clear that out when it fails! So now the multiple object list is {Alice, Bob}.

Once it fails, we go back into Inform 7 by calling the “multiple actor rulebook”, which takes the multiple object list, makes sure it’s a valid list of people, and stores it in a global variable of its own. It then takes the rest of the command—everything that comes after—and sticks this in another global variable. If this rulebook succeeds (the list was valid), then the parser continues after the comma, attempting to parse the rest of the line as a normal command. If it fails, then the parser reacts the same way it originally would if NounDomain failed—deciding the part before the comma isn’t an actor name after all, and trying to parse it as a verb instead (the .NotConversation label).

So now we have the list of intended actors {Alice, Bob} stored in a global variable, and the intended command GO NORTH in another global variable. Once we have this, a “rule for reading a command” takes over. As long as the list of intended actors isn’t empty, we set the player to the first element of that list, and parse the intended command. The player is now Alice, and the parser is given the text GO NORTH.

But wait, we don’t want this action to be performed as the player! We want it to be performed by an NPC! So a “first before doing anything” rule sees what’s happening, resets who the player is, and turns the action into a request: “going north” becomes “asking Alice to try going north”. It executes that action, then removes the first entry from the intended actor list.

This continues until the multiple actor list is empty. Since the command is actually parsed separately for each actor, it can deal with the actors having different surroundings: ALICE AND BOB, TAKE ROCK when each one has a different rock in their room, for example. The “rule for reading a command” also pauses when a disambiguation question is asked, letting the player answer before going back to multicommand processing.

As a side bonus, the extension also creates the action COMMAND [things] TO [text], which I used to test the multicommand machinery. In other words, this extension gives you a way to create new actions that convert to giving orders, which normally is nigh impossible! It just puts the list of actors and the intended command into the appropriate global variables, without any of the parser trickery.

9 Likes

Side note: the way Suspended does it is actually hilariously simple by comparison! It goes like this:

  • When parsing an actor, look for the exact phrase BOTH [person] AND [person]. If so, set the actor to a special “both” value.
  • If the “both” actor is commanded to perform any action except moving Fred, give an error.
  • Make sure all the actors involved are in the location of Fred.

Notably, it never even bothers to check that there are two different people involved! You never need multiple robots to accomplish something, even in the one specific place in the game where this syntax works—you can always say BOTH SENSA AND SENSA, MOVE FRED!

6 Likes

This week’s prize, Bubbling Beaker Award® #12 is presented to @capmikee, who used to be a frequent contributor to the forum. His award-winning post is really just a side note regarding a conversation extension that he was working on, but it demonstrates a method for handling a situation in which the author would like the grammar line for a two-object action to handle only the second noun.

Congratulations to capmikee! Because he has been inactive for so long, I will provide a short write-up in a follow-up post.

[An important note: Award frequency will be continued at every other week between now and the end of the year, so the next award will be for Fri Dec 01.]

6 Likes

This week’s award is interesting because the compiler will disallow code such as:

Quizzing it about is an action applying to one thing and one visible thing.

Understand "ask about [any known thing]" as quizzing it about (with nouns reversed).

The produced Problem message states: “...you can't use a 'reversed' action when you supply fewer than two values for it to apply to, since reversal is the process of exchanging them.” Despite use of the keyword “supplying,” the compiler does not recognize attempts to provide a rule for supplying a missing noun... as a valid way to address its concerns.

In capmikee’s post, he shows that a for supplying a missing second noun... rule can be set up to manipulate the noun and second noun manually:

For supplying a missing second noun when an actor quizzing something about:
	now the second noun is the noun;
	now the noun is the interlocutor of the person asked.

(Note that the interlocutor is a property of people in the context of his sample code, and the property is updated by rules for speech actions.)

The logic for assignment of the noun must be provided by the author in this case, as there is logic in the parser that will demand a non-nothing noun at run-time. (This is because the action definition in this case specifies that it applies to one thing and one visible thing.) However, the redefinition of the nouns persists throughout the remainder of action processing, so rules in other rulebooks can be constructed with action patterns that conform to the way that the author thinks about the action. For example:

After quizzing Bob about the soccer ball:
	say "Bob says, 'There's only one kind of football!'"

A question about does the player mean... (DTPM) rules comes up in capmikee’s post, but it is not addressed in detail. As he notes, the DTPM rules are handled prior to rules for supplying missing nouns, so there is a slight hitch in writing naturally-structured DTPM rules. For example, the rule

Does the player mean quizzing Bob about the soccer ball:
	it is likely.

will not work as expected because, even though the command entered might be >ASK ABOUT BALL, at the time that the DTPM rule is processed the noun is soccer ball and the second noun is nothing.

Fortunately, this is not hard to work around by also performing the switch within the DTPM rules. There’s a bit of a technicality here regarding the noun and second noun while processing DTPM rules, in that the assignments are temporary and will be undone at the end of processing the rulebook. Consequently, the manual swapping must be done twice: once during DTPM and once during supplying missing nouns.

Here’s a quick sample block showing the essentials in action:

DTPM with Noun Changes
"DTPM with Noun Changes"

Place is a room.

Bob is a man in Place.

A thing can be known.

A tennis ball is a known thing. A soccer ball is a known thing.

Quizzing it about is an action applying to one thing and one visible thing.

A thing has a thing called the interlocutor. [things not people, to leave room for "talkable" objects]

The interlocutor of the player is Bob. [set manually for brevity of example]

Understand "ask about [any known thing]" as quizzing it about.

To decide which object is the current actor: [needed because swaperoo rule is not in an action-processing rulebook]
	(- actor -).

This is the swaperoo rule:
	now the second noun is the noun;
	now the noun is the interlocutor of the current actor.

First does the player mean quizzing about: [placed first so that other DTPM rules see post-swap nouns]
	follow the swaperoo rule.

Does the player mean quizzing about the soccer ball: [soccer ball is *second* noun]
	it is likely.

For supplying a missing second noun when an actor quizzing about:
	follow the swaperoo rule.

Carry out quizzing about: [to show that the noun and second noun assignments persist from supply missing nouns stage]
	say "quizzing [noun] re: [second noun]..."

After quizzing Bob about the soccer ball: [sample specialized response]
	say "Bob says, 'There's only one kind of football!'"

In practice, a general report ... quizzing ... about ... rule to handle non-special cases would also be desirable, but that’s left as an exercise to the reader.

7 Likes

This week’s prize, Bubbling Beaker Award® #13 is presented to inimitable @Zed. It demonstrates a minor modification to the parser’s built-in ScoreMatchL() routine to ensure that it understands possessive pronouns when they apply to parts of a person instead of just things being carried by that person. (The OP is using the example of a command like >X HIS EYES.)

While this award-winning post may lack the scale of some of the more ambitious recipients, there is no doubt that it provides a general improvement in parsing. In addition to its utility, this post was selected because it’s a good example of the kind of small tinkering that starts one on the road to mad scientisthood, and so may encourage others to begin the journey.

Congratulations, Zed, on this your third BBA! I hope that you’ll drop by to say a few words about it; if not, then I’ll post a brief overview before the next award.

[An important note: Award frequency will continue at every other week between now and the end of the year, so the next award will be for Fri Dec 15.]

11 Likes

@Zed makes the following observation on the parser deficiency leading to this fix:

This is an oversight of the parser’s. When it’s evaluating the applicability of something with a possessive pronoun, it discounts parts and only looks at what the person has (i.e., carries or wears).

It would be more accurate to say that this is one of a number of places where the Inform parser has not been updated to take account of developments elsewhere in the language (in this case the introduction of ‘parts/incorporation’). At the time it was written, the existing code was fine.

5 Likes

aw, shucks! I didn’t even slightly remember this. :grinning: There’s not a lot to say about it: it was a fairly trivial tweak, with the only trick being finding where to make the trivial tweak.

Graham accepted this patch (PR #47), so it’ll be in the next version of Inform.

5 Likes

A quick overview of the details of Zed’s award for the beginner mad scientist…

The ScoreMatchL() routine (stands for “score match list”) is a routine inherited with some modifications from the I6 Standard Library. Its job is to generate a score that is used for automatic disambiguation of nouns, and it checks a number of conditions to decide on a composite score. If a player command is ambiguous about the noun (say >TAKE COIN when both a silver coin and golden coin are available) but ScoreMatchL() is able to determine a single highest scoring item, then it uses this “best guess” as the automatic choice, resulting in output like:

>TAKE COIN
(the silver coin)

Taken.

The I7 features that interact with this functionality directly are the does the player mean rules and the clarifying the parser's choice... activity, but the parser also has some I6-level functionality about possessive pronouns with special logic in this routine. This logic insists that certain I6-level conditions (which can’t be modified at the I7 level) are true in order to consider an object as a valid choice for the noun at all – if the conditions aren’t met, then the object is disqualified.

As drpeterbatesuk points out, the condition checked for a command like >X HER EYES is that the object matching the word “eyes” is an object tree child of the associated pronoun (in this example, “her”). In I6, if one wanted a person to have separate eyes as part of their body, the eyes object would be a child of the woman object in the object tree. In I7, the incorporation relation is handled with a separate, distributed object tree structure, which is missed by the older I6 logic, so even though the eyes object would match the word “eyes” in the player command, ScoreMatchL() would disqualify the object because it doesn’t understand the incorporation relation and can’t relate it to the woman object.

Zed’s change updates the logic to account for incorporation, so problem solved.

5 Likes

This week’s prize, Bubbling Beaker Award® #14 is presented to that paragon of productive mad science, @drpeterbatesuk. His award-winning post is ironically on this very thread! It shows a method for fetching the address of compiler-generated parsing routines for properties by crafting a special-purpose grammar line. This is useful when one wants to be able to have an action that uses two kinds of value (in this case, a colour and a colour), which is normally disallowed by the I7 compiler. (It’s also useful as an example of delving into the dictionary and grammar line structures generated at the I6 level, an obscure bit of knowledge.)

EDIT: I just realized that the way that I described the good doctor’s post above, it looks like a repeat of Award #6, given to BadParser. It’s true that the code takes the form of a solution to the same problem, but what’s significant and award-worthy is the way that it shows how to extract the relevant routine address from the compiled grammar lines, so that the code works even if the location of the routine changes. If one is trying to use a grammar line with two kinds of value, then the particular improvement here is that it can be set up once and then forgotten about, instead of having to hand-tweak the address with every new compilation.

Congratulations, drpeterbatesuk, on your own third Bubbling Beaker®! (Perhaps I should start a leaderboard.) Since your explanations are always excellent examples of completeness and clarity, there may not be much to add over what’s already been posted, but please feel free to take the floor.

[An important note: Award frequency will be continuing at every other week between now and the end of the year, so the next award will be for Fri Dec 30.]

8 Likes

I don’t think this is quite correct. That arrangement would imply that the eyes are either carried or worn by their owner, which often will not be what’s really wanted. The trick used in I6 that functionally most closely approximates the uses of I7’s incorporation relation is the property add_to_scope through which an author can add a list of objects that will always be in scope when the providing object is in scope. For this example, that means that whenever the player can interact with a given person through typed commands, they can also interact with that person’s eyes- even though the eyes are not children in the object tree of that person or even necessarily nearby (usually they will be kept offstage).

EDIT: this subterfuge works quite well for some purposes, but not others- e.g. a test for whether the person’s eyes are indirectly in the same room as their owner will fail.

3 Likes

It’s been a while since I’ve done any pure I6, but the approach that I recall using in the past is to mark the person object as transparent (so that children are placed in scope) and have any component parts be direct children.

Under that scheme, I guess body parts are indistinguishable from objects carried by the person object in terms of object tree and attribute state, but use of a BodyPart class with an appropriate before property (to handle ##Take and the like) always seemed like a good enough way to handle the functional distinction, if needed. (The default handling is Those seem to belong to the woman. Accurate, if understated.) I never really liked to use add_to_scope because of the “not really there” aspect to which you’re referring.

You made me curious, so I wrote a test scenario. Under StdLib 6/11, built-in possessive pronoun handling doesn’t work with objects handled via add_to_scope, for much the same reason that they don’t work in I7. In the scenario producing the following exchange, the eyes objects are handled via placement as direct child, and the nose objects are handled via add_to_scope:

Starting Point
An uninteresting room.

You can see a woman and a rock here.

>X WOMAN
Like you, she has a nose and eyes.

>X EYES
(your eyes)
You see nothing special about your eyes.

>X HER EYES
You see nothing special about the woman's eyes.

>X NOSE
You see nothing special about your nose.

>X HER NOSE
I only understood you as far as wanting to examine the woman.

>X HER ROCK
You see nothing special about the rock.

(The rock is interesting as a comparison. Because only one object matches via name, the restriction imposed by the possessive pronoun is ignored.)

3 Likes

Yes, that’s certainly another way in which add_to_scope falls short and the other method you describe performs better out of the box. Both of these ideas are explored in the DM4 (e.g. p.236 and Exercises 102 & 103 p.496).

EDIT: the standard way to deal with the possessive pronoun issue would be to either add ‘her’ to the name property of the eyes, or, slightly more sophisticated, write a parse_name routine so that her eyes is recognised.

2 Likes

This week’s prize, Bubbling Beaker Award® #15 is presented to @eu1, aka emacsuser, who was once a frequent visitor of the forum but has not been seen for almost ten years. The award-winning post demonstrates a method for understanding things by properties of their properties, via construction of a special-purpose relation. In the example, adjective words such as "wooden" are associated to materials such as wood, and each material thing has a material, so that a command like >X WOODEN STAFF will be correctly understood.

This may not sound like much, but it’s a trickier problem than it sounds like, and the core solution is quite elegant. This is the first Bubbling Beaker® awarded to eu1, but I doubt it will be the last – any mad scientist will learn many valuable tricks by studying the code that he has shared with the world. (The Object Kinds extension is also noteworthy.)

[An important note: Since a bi-weekly frequency of new awards seems to be working out OK, it will continue for the foreseeable future. Happy New Year!]

6 Likes

@eu1 is Brady Garvin, author of the invaluable Scopability and Object Kinds extensions. (I’ll publish my 10.1 sequel to Object Kinds someday…)

I have learned an enormous amount from his posts, though he was long gone before I got here.

3 Likes

This week’s prize, Bubbling Beaker Award® #16 is presented to @Draconis, who somehow continues to avoid forcible induction into the Mad Scientists’ Club. His award-winning post unleashes a new extension that allows for distance calculations between rooms via Dijkstra’s algorithm. The rulebook-based solution at its heart makes it fairly easy for an author to specify edge weights that vary under varying conditions, if desired, and for simple cases – such as making diagonal directions count as longer than cardinal ones, as one might wish for a region of rooms representing a grid of locations – only a few lines are required. (Note that the extension does not replace Inform’s built-in route-fnding logic; instead it creates parallel logic for the author to use instead. It does, however, disable fast route-finding for the built-in logic.)

Draconis, congratulations on your third BBA! The linked thread doesn’t contain a lot of under-the-hood discussion, nor does the extension documentation go into details – do you feel like giving a brief overview of the default path-finding functionality as compared to your implementation?

(Honorable mention goes to @drpeterbatesuk, who first proposed a limited solution for the particular case of treating diagonal directions different from cardinal directions at https://intfiction.org/t/best-route-and-diagonal-shortcutting/51085/16.)

3 Likes

I am honored with yet another suspiciously bubbling beaker! (And a bit embarrassed to say that I actually have no idea what the Mad Scientists’ Club is.)

This time the innovation comes from something I needed rather than an interesting problem on the forums! As you can see from that thread, it was for Death on the Stormrider. I needed precise control over NPC movements in order to make the puzzles work. Qarrad isn’t bothered by heat, but Bashti is; he can’t go through overheated rooms, but is also the only one who can use the crawlspaces, except he should avoid them unless absolutely necessary…

And after several failed attempts with Inform’s standard route-finding, I started looking for alternatives on the forum. That led me to this thread. I recommend reading the first post there for some cool technical details, but the essence is that Inform’s slow route-finding doesn’t actually use a standard algorithm. It’s something custom-built and not-well-documented.

Specifically, I needed an algorithm that could handle a “weighted graph”: a map where the connections between rooms can have different lengths, so going from A to B (weight 5) and B to C (weight 5) can actually be better than going from A to C (weight 20). Inform’s fast route-finding is the Floyd-Warshall algorithm, which can do this, but also calculates every possible route on the map at once—which is bad for my purposes, since I need to redo the calculations every time a different NPC is moving!

Specifically, I needed a “single-source shortest path” (SSSP) algorithm: a way to calculate the best route to every room from a single origin. This can run a lot faster than an “all-pairs shortest path” (APSP) algorithm like Floyd-Warshall. The linked thread uses a classic one called “breadth-first search”…which assumes every connection between rooms has the same length. Curses!

But there’s a very famous algorithm for the SSSP problem on a weighted graph, one of the classic algorithms that every CS student has to learn: Dijkstra’s algorithm, named after the father of computer science himself! There’s only one problem: the way it’s normally implemented requires fancy data structures like priority queues and Fibonacci heaps. At bare minimum, it needs a dynamic list that you can easily add and remove things from. And while I7 can do this easily, I6 cannot.

But, the things that get put in this list are rooms! And rooms are something I6 is very good at working with! I repurposed the room_index property (normally used for fast route-finding) to form a singly-linked list of rooms, with each one’s room_index being a pointer to the next. This gave me the queue I needed, so the algorithm could work!

After several failed experiments, I made it so that every room stores three relevant properties: the calculated distance, the room before this one on the route (formally, its parent in the minimal spanning tree), and the direction to go from that room to get to this one. Since the distance is stored as a property, you can use it to make a scalar adjective in I7: if you say “a room is near if its Dijkstra distance is is at most 3”, then you can look up “the nearest room”—or “the nearest unexplored light-filled room”, or as specific as you like!

The last problem to solve is—if this is a weighted graph, how do you specify the edge weights? How do you say that the connection from A to B is ten meters long and the connection from B to C is fifteen?

For Stormrider I could have done it in pure I6, but every game would want to do it differently. So I made it store the four relevant properties (room gone from, room gone to, direction gone, and door gone through) in global variables and call out to an I7 rulebook, which authors could customize as they pleased. You can make cardinal directions cost 10 and diagonal directions cost 14, for example, if you want to simulate some nice Euclidean geometry, or make closed doors cost 1 and locked doors cost 2 to represent how many turns it’ll take to go through them.

In the end, I’m very happy with it. Every NPC in Stormrider uses this to decide where to go next, with their own set of edge cost rules (“edge cost when the actor is Qarrad” and so on). And the adventurers in Labyrinthine Library of Xleksixnrewix use it too: every turn, they move toward the nearest unexplored room, with diagonal directions costing 1.4 times as much as cardinal ones. Blocking them out of rooms with barriers in them was just another edge cost rule!

For the future, I should probably create a convenience phrase that’s less clunky than “rule succeeds with result N”, and perhaps avoid the conflict with fast route-finding. And it has one significant drawback over the default route-finder, which is that you need to explicitly recalculate routes from your chosen origin before doing anything with them. But overall, I think it’s a solid improvement over the default implementation (in everything except speed—calling the rulebook every time it considers an edge is a serious cost!) and recommend using it for anything involving complicated pathfinding!

7 Likes

As a bonus, here are some of the edge cost rules from Stormrider and Labyrinthine Library.

Stormrider:

Edge cost rule: [Default pathfinding for most characters: don't use locked doors or crawl hatches, closed doors take 2 turns (one to open one to move), everything else takes 1 turn]
	if the edge door is a locked door, rule succeeds with result -1;
	if the edge door is a crawl hatch, rule succeeds with result -1;
	if the edge door is a closed door, rule succeeds with result 2;
	if connecting Shimat's Cabin and the Miscellany, rule succeeds with result -1; [I don't know why NPCs are getting stuck in Shimat's cabin but this should prevent it hopefully]
	rule succeeds with result 1.

Edge cost rule when the person asked is the concept of sound propagation:
	rule succeeds with result 1. [Sound doesn't care about doors, obstacles, etc; it can travel through every part of the ship]

Edge cost rule when the person asked is Bashti: [Bashti uses more elaborate route-finding than most]
	let the total be 1;
	[…several special cases to keep him out of other people's areas…]
	if the edge source is the Cargo Hold and the edge destination is the Lower Deck and the open tripod is in the Lower Deck: [Staircase is jammed, can't use it]
		rule succeeds with result -1; [Impassible]
	if the edge destination is an overheated boiler-room, increase the total by 10; [Bashti does not like the heat]
	if the edge destination is a crawlspace and the edge source is not a crawlspace, increase the total by 5; [Penalize entering the region but don't penalize moving through it]
	if the edge door is a locked door, increase the total by 2; [Unlocking doors is an inconvenience but not a fatal one]
	rule succeeds with result (the total).

Note that the edge between the Cargo Hold and the Lower Deck is only blocked in one direction—he’s free to route-find through it in the other direction, since he can unblock the door from below!

Library:

Edge cost rule:
	if the edge destination is barricaded or the edge source is barricaded, rule succeeds with result -1; [Cannot pathfind through a barricaded room - either for movement or for line of sight]
	if the edge direction is vertical, rule succeeds with result 12; [Disincentivize going up and down a little bit so that adventurers tend to stay on the same level]
	if the edge direction is diagonal, rule succeeds with result 14; [Approximation of 10×sqrt(2)]
	rule succeeds with result 10.
4 Likes

Thanks. Yes, I have been away for awhile.

3 Likes

Welcome back!

1 Like