Stop the action vs rule fails

I spent a few hours last night trying to figure out why a particular check rule was behaving unexpectedly, and I eventually discovered that it comes down to the difference between “stop the action” and “rule fails”. (There is actually a question at the end of this, in case my explanation is old news.)

Working example
The smooping rules are a rulebook.

A smooping rule (this is the default smooping rule):
	rule succeeds.

Smooping is an action applying to nothing.
To smoop is a verb.

Understand "smoop" as smooping.

Check an actor smooping (this is the smooping limitation rule):
	follow the smooping rules;
	[say "(smoop-checked [the actor])[line break]";]
	if the actor is the player, say "[We] [can't] smoop!";
	stop the action.

Report an actor smooping:
	say "[The actor] [smoop]."

The Lab is a room.

Annie is a person in the Lab.
A persuasion rule for asking Annie to try doing something: persuasion succeeds.

Unsuccessful attempt by Annie smooping:
	say "'I can't smoop,' says Annie."

In this example, we have a nonsense action that never succeeds (the report rule is there just to illustrate that it never happens). The check rule that prevents smooping calls a trivial rulebook and then stops the action. The puzzling part is that if you run this code and then smoop after turning on actions, it will list the action as successful. In particular, if you ask Annie to smoop, nothing gets printed.

If you replace stop the action with rule fails, the code works as intended.

I spent some time examining a more complicated version of this. After looking thorough the generated I6 code, I eventually discovered that Inform internally uses two methods to indicate whether a rule or rulebook succeeds or fails, and using stop the action only affects one of them. If a check rule invokes some other rulebook, directly or indirectly, then stop the action effectively picks up the result of that.

(This actually resolves another mystery for me. If you have a check rule that includes try other action instead, the original action succeeds or fails based on the whether the other action succeeds or fails. This also works if you use try without instead and then do stop the action later. Now I know why.)

But here is the part I haven’t figured out yet: If you uncomment the say line in the check rule, then stop the action works as expected (i.e., the action is considered a failure). This only seems to happen if the text being said includes bracketed expressions, but don’t know I6 well enough to dig any further.

3 Likes

I’m not sure I understand your final question. However I can see possible tanglements in the example that may be clouding the issues.

The attempt to ask Annie to do something is a separate action to Annie’s doing of it. Both can report success or failure independently of each other, and sometimes regardless of whether they may have succeeded or failed to the player’s eye. Though with persuasion, obviously success in the attempt to ask her allows her to actually try the action, and failure doesn’t. (I think! I never use persuasion rules.)

I think what will reveal to you a ton about the flow of events in your example is if you turn on the RULES test command in the same manner you already turned on ACTIONS. Then you’ll see what’s being checked and in what order. There’s also RULES ALL, but your example is so small, RULES ALL will probably make no difference.

-Wade

Yes, ACTIONS is what I was referring to when I said “turn on actions”. In my more complicated example, there were multiple checks, and RULES showed that none of them were tested after the one that behaved oddly.

In case I was too cryptic, here is my premise. Usually, a check rule that uses stop the action results in the action failing (as shown by ACTIONS, but also by asking someone else to do the action). However, if the check rule somehow invokes a rulebook that succeeds, then stop the action results in the action immediately succeeding. This is obscure enough that I’m not sure it’s intentional, but at least I now understand why my rule was behaving oddly.

What I still don’t understand is why having a say statement between the rulebook and stop the action results in the action failing. Presumably, the say changing the value of global variable that holds the last rulebook outcome, but I don’t have any further details.

1 Like

Right. Well, I followed it all through, and tried one variation (calling the smooping rules ‘the smooptronic rules’ instead… because I was suspicious of the followed rulebook containing the name of the verb and action verbatim) but it didn’t change the outcome. Everything I tried duplicated what you reported.

-Wade

This is clearly a bug: Writing With Inform §19.11 states: stop the action; means “end this rule in failure”.

It looks to me as though stop the action is instead reporting “whether or not the rule succeeded”, which always refers to the last rule processed. It may be that this is intended and the documentation at fault.

1 Like

I’m not sure whether the difference in behavior here is intended (well, as someone who isn’t on the dev team, I wouldn’t be). The behavior around rulebook success and failure when other rules/rulebooks are followed is very subtle, or perhaps completely opaque to me. I just rediscovered this discussion of the difference between the specific check rules getting anonymously abided by and the specific carry out rules getting considered, and I had completely forgotten how that makes a difference.

But the behavior where including the “say” phrase affects whether the action fails absolutely has to be a bug. I looked at the generated I6 code for a few seconds but I couldn’t figure out what was going on.

1 Like

Looking at the generated I6 code, I noticed that Before doing something: rule fails. produces:

! Before doing something:
[ R_807 ;
    if ((( (actor==player)))) { ! Runs only when pattern matches
    if (debug_rules) DB_Rule(R_807, 807);
    ! [2: rule fails]
    RulebookFails(); rtrue;
    } else if (debug_rules > 1) DB_Rule(R_807, 807, 'action');
    rfalse;
];

whereas writing Before doing something: stop the action. produces:

! Before doing something:
[ R_807 ;
    if ((( (actor==player)))) { ! Runs only when pattern matches
    if (debug_rules) DB_Rule(R_807, 807);
    ! [2: stop the action]
    rtrue;
    } else if (debug_rules > 1) DB_Rule(R_807, 807, 'action');
    rfalse;
];

Note the absence of RulebookFails(); in the second example. This does seem like a bug, as §27.20 (which concerns itself with writing your own rules in I6) states:

The rule should return false if it wants to make no decision, but call either RulebookSucceeds or RulebookFails and return true if it does.

As I said, “stop the action” returns true without calling one of the decision-making routines first, so the most recently recorded result is apparently reused (yay global variables). In @zednenem’s example, that would be the success result produced by “follow the smooping rules”.

3 Likes

It does seem like either the code or the documentation is wrong.

I did find a comment from @zarf a few years ago where he writes:

“instead” and “stop the action” end the rule in failure, which stops the current rulebook. “rule fails” ends the rulebook in failure, which stops it and also sets its outcome value to failed.

This is a subtle distinction which, if intentional, should be documented more clearly.

In 7.3, “stop the action” is documented:

This phrase stops the current rule, stops the rulebook being worked through, and finally stops the action being processed. Example:

This is correct; it doesn’t say anything about the rulebook outcome.

The key point in 19.11:

When a rulebook is followed, what happens is that each of its rules is followed in turn until one of them ends in success or failure

But as the rest of that section explains, the rulebook outcome is a separate matter. Stopping the rulebook without an outcome (rule succeeds / rule fails) takes you to the default outcome, which is often “no outcome”.

This could definitely be explained better. It’s also hard to get right in practice, because for many games, the rulebook outcome of an action is not important. You can write a great deal of Inform without ever worrying about whether your action counts as a success or a failure – you just want to get the right text and behavior out.

“Unsuccessful attempt” rules are one of the cases where it does matter, as you see.

4 Likes

I see from that thread that I filed a documentation bug about that back then (a few years ago), and I see from the bug report that it’s been marked as fixed.

It looks to me like the change from what I reported there is from:

rule fails
This causes the current rule to end immediately, with its outcome considered to be a failure. That means the rulebook being worked through will also end, and also be a failure.

to:

rule fails
This causes the current rule to end immediately, with its outcome considered to be a failure. The rulebook being worked through also ends, and is also a failure.

If I stare at it for long enough, the omission of “that means” might suggest that the original text implied that whenever a rule ends in failure, the rulebook also ended in failure; the new text without “that means” lacks this implication, and consequently does not mean that the the “end this rule in failure” phrases (…instead/stop the action) also end the rulebook in failure.

This remains something that I would call subtle/opaque/confusing as all get-out. The very difference between the rule ending in failure and the rulebook ending in failure is itself subtle/confusing, and the documentation isn’t really helping here.

4 Likes

I think something that would help would be a very explicit statement in §19.11 that the outcome of the rule is not necessarily the outcome of the rulebook. The sentence “But what happens if a rule simply doesn’t say whether it succeeds, fails or has no outcome?” seems like it suggests that if the rule does fail, we won’t advert to the default outcome of the rulebook; but it seems like what we want to talk about here is whether the rule says that the rulebook succeeds or fails or has no outcome?

It also might be worth mentioning that in most cases you don’t have to worry about this. What are the cases where you do have to worry about this? Explicit checks for “if the rule succeeded” and “if the rule failed,” as discussed later in this section; “Unsuccessful attempt,” as we’ve just discussed; what else? I’m not sure whether past tense formulations like “if we have smooped” depend on rulebook success or failure; they depend on the action success or failure, and the chart in §12.1 suggests that in the normal course of things that’s just a matter of whether the action reaches the carry out stage, but things might be more complicated.

Anyway, I’ve filed a bug report about the weird behavior with the “say” phrase.

1 Like

I’m not sure that fully describes what happens, though.

From what I can tell, a rulebook is implemented as an I6 function that returns 0 (no decision) or a rule (success or failure). Additionally, a global variable stores whether the last rulebook succeeded, failed, or made no decision. Each rule is also an I6 function, that returns success, failure, or no decision. The check action rulebook returns 0 if none of the rules made a decision. It also resets the last rulebook outcome to no decision each time a rule ends without making a decision.

So, stop the action causes the current rule to fail, which causes the check action rulebook to return that rule as the reason the action failed. Because the rulebook returns a non-zero value, the check stage rule will either succeed (if the last rulebook succeeded) or fail (if the last rulebook failed or made no decision).

This makes it difficult to characterize what effect stop the action has, since it depends on whether any rulebooks have been followed since the rule began and what their outcomes were.

Again, this comes down to the fact that rulebooks indicate their decision in two ways: the return value of the I6 function and the global variable. stop the action only affects the first one, presumably under the assumption that the global has already been set to the default value. However, following another rulebook during a rule clobbers the global variable, which can lead to the action ending successfully.

2 Likes

(Those trying to follow this discussion from deep in the code will want to know that “the global variable” refers to the I6 value latest_rule_result. This is actually a small global data structure: a SUCCEEDS/FAILS/NO_OUTCOME flag plus an actual rulebook outcome, which has whatever type the rulebook had.)

2 Likes

Thank you.

I just noticed that the line say "[We] [can't] smoop!" apparently does not change the last rulebook outcome, so it’s something more specific than bracketed text. I specifically see it with strings containing [actor] or [the actor], so maybe it has something to do with the printing the name of activity?

Just so people don’t have to look these things up themselves, here is some of the I6 code I’m talking about.

! Check an actor smooping ( this is the smooping limitation rule ):
[ R_800 ;
    if ((((act_requester==nothing)))) { ! Runs only when pattern matches
    self = noun;
    if (debug_rules) DB_Rule(R_800, 800);
    ! [2: follow the smooping rules]
    FollowRulebook(RULEBOOK_TY_to_RULE_TY(362));
    ! [3: if the actor is the player]
    if (((actor == player)))
    {! [4: say ~[We] [can't] smoop!~]
        say__p=1;! [5: we]
        ParaContent(); (PHR_768_r2 ());! [6: ~ ~]
        ParaContent(); print " ";! [7: can't]
        ConjugateVerb_77(CV_POS, PNToVP(), story_tense); say__p=1; ! [8: ~ smoop!~]
        ParaContent(); print " smoop!"; new_line; .L_Say293; .L_SayX293;}
    ! [9: stop the action]
    rtrue;
    } else if (debug_rules > 1) DB_Rule(R_800, 800, 'action');
    rfalse;
];

I don’t have much to say about this, except to note that replacing stop the action with rule succeeds or rule fails results in RulebookSucceeds() or RulebookFails() being added before the rtrue.

[ B363_check_smooping 
    forbid_breaks ! Implied call parameter
    rv ! return value
    ;
    rv = R_800();
    if (rv) {
        if (rv == 2) return reason_the_action_failed;
        return R_800;
    }
    latest_rule_result-->0 = 0;
    return 0; ! 1 rule(s)
];

Here, R_800 is the smooping limitation rule. If it returns true (decision made), then the rulebook function returns R_800 as the reason it failed. Otherwise, the rulebook sets the global last rule result flag to NO_OUTCOME and returns 0 (no error).

(My impression is that a rule returns 2 if it is anonymously abiding by the outcome of another rule.)

Note that B363_check_smooping only sets latest_rulebook_result when the rule has made no decision. If the rule uses stop the action, the rule reports that it made a decision, but actually keeps whatever the last decision was.

B363_check_smooping is called by the check stage rule:

! A specific action-processing rule ( this is the check stage rule ):
[ R_27 ;
    if (debug_rules) DB_Rule(R_27, 27);
    ! [2: anonymously abide by the specific check rulebook]
    if (temporary_value = FollowRulebook(RULEBOOK_TY_to_RULE_TY((MStack-->MstVO(11,2))))) {
    		if (RulebookSucceeded()) ActRulebookSucceeds(temporary_value);
    		else ActRulebookFails(temporary_value);
    		return 2;
    	}
    rfalse;
];

My reading here is that this rule looks up the correct rulebook to follow and follows it. If the rulebook does not report a decision (returns 0), then R_27 makes no decision (rfalse). If any rule in the rulebook returns true, then that rule will be returned by FollowRulebook and saved in temporary_value. Because temporary_value won’t be zero, R_27 will call ActRulebookSucceeds or ActRulebookFails. These go on to set things in the latest_rule_result structure.

Having thought about the situation…

I think the intent is that RulebookFailed()/RulebookSucceeded() (the “if rule failed”, “if rule succeeded” phrases) should only be checked immediately after calling a rulebook. That’s the implicit constraint on using a global.

Therefore, your original code:

Check an actor smooping (this is the smooping limitation rule):
	follow the smooping rules;
	[say "(smoop-checked [the actor])[line break]";]
	if the actor is the player, say "[We] [can't] smoop!";
	stop the action.

…is badly put together. If you care about the outcome of the smooping rulebook, do something like:

    follow the smooping rules;
    if rule failed:
        say "We can't.";
        rule fails;

The fact that the outcome leaks out to affect the success of the action looks like a bug. At least, I’d regard it as incorrect for the “follow” statement to affect anything other than an explicit “if rule succeeded/failed” check.

However, it’s possible that some of the examples are constructed to rely on this behavior. So I don’t know for sure.

2 Likes

Yes, if I was doing this on purpose I would write it explicitly. This would hopefully be more robust to future changes in the way the language works. My example here is just a very short bit of code that doesn’t behave in the way one might expect.

My original code was not supposed to depend on the outcome of a rulebook (in fact, it was using a phrase used a rulebook internally). What happened is that I noticed that a check rule was cancelling the action (as intended) but causing the action to be considered successful (not intended). I then stayed up way too late trying to understand what was happening.

I do wonder if there are any plans to rework how rules and rulebooks are implemented. The current design feels like it’s painted itself into a corner somewhat. (For example: rulebooks return the rule that caused them to make a decision, but they also need to maintain a global variable in order to make anonymously abide by work.)