New Extension: Actions on Groups

The extension documentation mentions a tricky implementation of “The Facts Were These.” Here it is:

"The Facts Were These, Refactored"

Include Actions on Groups by Matt Weiner.

Section 1 - Procedure

[From the original example, with the multiple-giving mechanics snipped, and changing the ungivability rules to strip out the flag ("already gave at the office") that the original example used to keep track of whether multiple actions were tried; Actions on Groups takes care of that. The rule for implicitly taking also had to be modified; see below.]

[We start by creating the idea that everything in the game has a monetary value:]

A price is a kind of value. $10 specifies a price. A thing has a price.

A thing can be given or ungiven. A thing is usually ungiven.

[This is for record-keeping purposes so that we can print an attractive list of what was given at the end of the turn.]

[Now we create our own variation of implicitly taking in order to customize the output for the multiply-giving action. The "ungivability rules" should disallow any object that the player absolutely cannot take, because we want "carry out the implicitly taking activity" to succeed every time -- and therefore not print out any less-attractive results from implicit takes that don't succeed. Otherwise, the player's GIVE TREE AND DOG TO ATTENDANT might produce the reply "That's fixed in place" -- without specifying which object is fixed in place.]

[Because of the way this works, we will want to be careful: if we have any "instead of taking..." rules for special objects in the game, we should be sure to mirror those with an ungivability rule to print something more suitable in the case that the player tries taking that object as part of the multiple giving action.]

The ungivability rules are an object-based rulebook.

An ungivability rule for a person:
	say "Slavery is illegal.[paragraph break]" instead.

An ungivability rule for something (called the item) which is enclosed by someone who is not the player:
	say "[The item] [aren't] yours to give.[paragraph break]" instead.

An ungivability rule for something which encloses the player:
	say "You don't want to end up as part of the gift.[paragraph break]" instead;

An ungivability rule for something (called the item) which is part of something:
	say "[The item] [are] attached to [a random thing which incorporates the item][paragraph break]" instead.

An ungivability rule for something (called the item) which is scenery:
	say "[The item] [are] unremovable.[paragraph break]" instead.

An ungivability rule for something (called the item) which is fixed in place:
	say "[The item] [are] fixed in place.[paragraph break]" instead.

An ungivability rule for a direction (called the item):
	say "[The item] [are] not susceptible to giving.[paragraph break]" instead.

Rule for implicitly taking something (called target) while giving: [This is meant to preempt the "(first taking the dollar)" message when we are giving more than one thing. The original example had this as an action on "multiply-giving," but with actions on groups the action is just "giving," so we need to check the multiple object list so that we do not preempt the implicit taking message when we are only giving one thing.]
	if the number of entries in the multiple object list is greater than 0:
		silently try taking the target;
		if the player carries the target:
			add the target to the recently-collected list;
	otherwise:
		continue the activity.

The recently-collected list is a list of objects that varies.

[And now, since this ought to work symmetrically if the player provides just one high-value item:]

Check giving something to someone:
	if the price of the noun is less than the price of the second noun:
		say "[The second noun] angrily rejects your piffling bribe." instead.

[As we've seen elsewhere, the giving action by default returns a refusal, but is also written to start working if we remove the blockage. So we do that here, and revise the report rule to match the report rule we have for multiple giving.]

The block giving rule is not listed in any rulebook.

The new report giving rule is listed instead of the standard report giving rule in the report giving it to rules.

This is the new report giving rule:
	say "[The second noun] rather shamefacedly tucks [the noun] away into a pocket."

[After each instance of the multiply-giving action, we need to clear the variables we used to track its state. We could do this in "Before reading a command", but that's unsafe because the player might type GIVE PIE AND CAP TO ATTENDANT. GIVE DOLLARS TO ATTENDANT. all on a single line, and we would like to be able to clear the variables between one action and the next. The correct place to attach this behavior is immediately before the generate action rule, thus:]

The before-generation rule is listed before the generate action rule in the turn sequence rules.

This is the before-generation rule:
	now every thing is ungiven;
	truncate the recently-collected list to 0 entries.

Section 2 - Implementation of Multiple Giving with Actions on Groups

[Setting up giving as a groupable action:]

Giving is groupable action.

Understand "give [things preferably held] to [someone]" as giving it to.
Understand "give [things] to [someone]" as giving it to.
[as in the original example, we need the [things preferably held] line so that if the player holds two dollars and the attendant holds one, "dollars" will match the dollars the player holds and not the one the attendant holds]

[We now take the check/carry out/report multiple-giving rules from the original example and turn them into Actions on groups for giving rules, in order:]

Last action on groups rule for giving something to the player (this is the can't give multiple things to yourself rule):
		say "You can hardly bribe yourself.[paragraph break]";
		rule fails. [The "instead" should make the rule fail and stop the rest of the actions on groups rules from happening.]

[To quote the original documentation: "The following rule is longish because it processes the entire list at once, generating implicit takes if necessary (but processing those implicit takes silently according to its own special rule, so that the output can be managed attractively). We are also, at the same time, calculating the total value of the player's offer."
Furthermore, for an Actions on Groups rule, if the rule successfully makes it to the end we must "make no decision" so the rulebook will proceed to the next rules.]

Last action on groups rule for giving (this is the check givability and total the bribe-price rule):
	let L be the multiple object list;
	let bribe-price be $0;
	repeat with item running through L:
		if the player does not carry the item:
			anonymously abide by the ungivability rules for the item; 
			[a subtlety here; the original example had "abide by the ungivability rules," and the ungivability rules themselves, if they failed, set the flag that told Inform not to continue trying multiple actions. But for us, that flag gets set when an action on groups rule ends in success or failure. If we merely wrote "abide by," when the ungivability rules ended in failure then the action on groups rule would end with no decision made, while stopping the action on groups rulebook. We need "anonymously abide by," which ensures that if the ungivability rule ends in failure then this rule will also end in failure. See WI §19.14.]
			carry out the implicitly taking activity with the item;
			if the player does not carry the item:
				say "You can't include [the item] in your bribe, since you're not holding [them]![paragraph break]";
				rule fails; [a very subtle subtlety: here and below, we have to write "rule fails" instead of putting "instead" on the previous line, as the in-line "instead" causes the action on groups rulebook to end without result, which will fail to set the flag that makes the group action preempt the ordinary action. "Rule fails" causes both the rule and the rulebook to end in failure.]
		increase bribe-price by the price of item;
	if the number of entries in the recently-collected list is greater than 0:
		repeat with item running through the recently-collected list:
			now item is marked for listing;
		say "You pick up [the list of marked for listing things] and make your offer. [run paragraph on]";
		now everything is unmarked for listing;
	if the bribe-price is less than the price of the second noun:
		say "[The second noun] angrily rejects your piffling bribe.[paragraph break]";
		rule fails; [again, this makes the action on groups rulebook itself fail]
	make no decision. [otherwise the rule will succeed and stop the rest of the actions on groups rules from firing]
	
[From the documentation of the original example: The bit about making some items "marked for listing", above, rather than printing the list directly, is that using the "[the list of....]" syntax guarantees that Inform will respect grouping rules in writing its description. For instance, if the player has automatically taken all three dollars, the output will say "the three dollars" instead of "the dollar, the dollar, and the dollar."]

Last action on groups rule for giving (this is the deliver the loot rule):
	let L be the multiple object list;
	repeat with item running through L:
		now the second noun carries the item;
		now the item is given;
	make no decision. [Again, necessary to allow the rulebook to proceed to the next rule.]
	
Last action on groups rule for giving (this is the report giving as an action on groups rule):
	say "[The second noun] rather shamefacedly tucks [the list of given things] away into a pocket.[paragraph break]". [and since this finishes the rules, we don't write "make no decision," but allow the rule to succeed, so that the giving action won't run for the rest of the objects in the multiple object list.]
	
[By defining each rule as "Last," we guaranteed that they would run in the order they appear in the source code, which is the order we want.]

Section 3 - Scenario

The Morgue Office is a room. "This is not the Morgue itself; this is only its outer office. The familiar room full of silver drawers and cold air lies beyond."

The Morgue Attendant is a man in the Morgue Office. "The Attendant has seen you come through a number of times, and is becoming suspicious of your abiding interest in dead people." The description is "The Morgue Attendant is fifty-four years, six months, five days, and three minutes old." The price of the Morgue Attendant is $3.

A dollar is a kind of thing. The player carries three dollars. The price of a dollar is always $1.

The player carries a miniature rhubarb pie. The price of the miniature rhubarb pie is $5.

The player carries a knitted cap. The price of the knitted cap is $2.

Test me with "test dollars / purloin three dollars / test multi-line / purloin three dollars / purloin pie / purloin cap / test specificity / purloin three dollars / test largesse / test mixed-gift / purloin three dollars / test failure".

Test multi-line with "give dollar and pie to attendant. give dollars and cap to attendant".

Test dollars with "drop all / give dollar to Morgue Attendant / give dollars to Morgue Attendant / get dollars / give dollars to morgue attendant / purloin three dollars / drop dollars / give dollars to Morgue Attendant".

Test specificity with "give three dollars to Morgue Attendant".

Test largesse with "give pie to Morgue Attendant".

Test mixed-gift with "give dollar and cap to Morgue Attendant / get cap / give dollar and cap to morgue attendant / give me and dollar to attendant".

Test failure with "give dollars to Morgue Attendant / give dollars to Morgue Attendant". [This test demonstrates that, when the ungivability rules stop the action, we do not get spurious announcements from the multiple object list.]

This is fairly tricky because the Action on Groups rules must end in either success or failure for the extension to realize that a rule run, and that it should suppress the announcements from the multiple object lists and the ordinary action-processing machinery. The Action on Groups rules for giving themselves call the ungivability rulebook, so we must be sure that if an ungivability rule ends in failure then the Action on Groups rules will also end in failure, meaning that we must “anonymously abide by” the ungivability rules rather than simply abiding by them. Whether this is actually simpler than the original implementation may be a matter of debate.