Making an NPC copy the actions of the player

I’ve figured out that you can store an action as a property, called, let’s say, lastaction, but I’m wondering if there’s a way to then make an NPC try to perform every action the player does.

The problem is how to activate the npc trying that action. I’m honestly not even sure where in the manual to look for the grammar for it.

I thought maybe something like:

Every turn:
	now the lastaction of player is the current action;
	try your clone performing the lastaction;

So that, for example, when you go north, the clone also goes north. (This is part of a puzzle in which the clone is actually a room in front of you and you’re seeing them through glass). Anyway, how can that last action done by a player be copied by another character (even if, as in the puzzle, it isn’t always going to work)?

Chapter 12.20 is on Stored Actions, but the clearest examples might be Robo 1, which stores a program by copying the player for later playback, and Robo 2, which can store and playback multiple programs. I think it should be straightforward to have them just try the action immediately instead of storing it?

5 Likes

Robo 1 was the key to figuring out what’s actually included in an action that gets stored in a variable. Apparently the current action can be changed by changing global variables like the noun and actor (i.e., my code wasn’t working because current action includes all the parser commands). The full code of what I’m working on is below. Now the only issue is figuring out how to get the room headings back since I apparently prevented a rule from running that prints the room descriptions when entering a new room.

"The Clone Who Walked Through Glass Walls" by Zoe Lebeau

A room can be glass. A room is usually not glass. A person can be passable or unpassable. A person is usually unpassable. A person has an action called lastaction. A chair is a kind of supporter. A chair is always enterable.

After deciding the scope of the player:
	place the clone in scope; [required so that the report rules below work. If the clone is not in scope of the player, the report rules for the clone are simply ignored even though the clone has performed the action]

After the player doing something:
	now the actor is the clone;
	if the noun is the RustyChair:
		now the noun is the LeatherChair;
	try the current action;

Check an actor going to a glass room:
	if the person is passable:
		continue the action;
	otherwise:
		say "There's no way for you to pass through the glass wall to the other side.";
		stop the action;

After the clone getting off the LeatherChair:
	say "Your doppleganger mechanically stands up.";

After the clone entering the LeatherChair:
	say "Your doppelganger sits back down.";

After the clone looking:
	say "Your clone looks around, following your movements.";

After the player entering RustyChair:
	say "You sit down and stretch your legs out. This is incredibly comfortable.";

After the player getting off RustyChair:
	say "You get out of the chair.";

Hallway is south of Cloning Laboratory. "To the north you can see a window through which someone who looks identical to you is [if the clone is on a chair]sitting bolt upright in a leather chair.[else]standing straight ahead." A RustyChair is a chair in Hallway. Understand "chair" or "rusty chair" or "rusty" as RustyChair. The printed name of RustyChair is "rusty chair". 

Observation Window is east of hallway. "The sound of your breathing reverberates off of the hard tile surfaces."
		
Cloning Laboratory is a room. The printed name of Cloning Laboratory is "Cloning Laboratory". LeatherChair is a chair in Cloning Laboratory. Understand "chair" as LeatherChair. The printed name of LeatherChair is "chair". The clone is a person. The clone is on LeatherChair. Understand "clone" as the clone. The clone is passable.

Manufacturing Room is east of Cloning Laboratory. "A strange room full of beakers and 3D printing machines." Manufacturing Room is glass.```
1 Like

I’m on mobile at the moment so I can’t really dig in, but typically After rules terminate an action, meaning subsequent rules (like report ones) might not fire — so I suspect that’s the culprit (you can just swap them for Report rules, or I think ending them with “continue the action”’will get around this too).

I’ve tried changing after rules to be report rules and had the same issue.

What about using an Every Turn rule?

Every turn:
	now the actor is the clone;
	if the noun is the RustyChair:
		now the noun is the LeatherChair;
	try the current action;

That works but I would rather learn how to make the more specific rules do what I want and really understand what the order of execution is so I can master controlling it.

I’m still struggling to figure out why my after rule prevents the display routines for showing the new room being entered into. I can certainly use the every turn rule, but I’d love to understand what the “after the player doing something” rule is preventing from happening and why.

Can an Inform 7 genius explain this to me? Even looking at the output of the “rules” debugging output all I know is that there are a series of rules that aren’t being run but no idea as to chain of causation.

I’m not a Inform 7 genius, but I found something testing your code:

After the player doing something:
	now the actor is the clone;
	if the noun is the RustyChair:
		now the noun is the LeatherChair;
	try the current action;
	now the actor is the player;
	continue the action;

Result in game:

>e
The clone arrives from the west.

Observation Window
The sound of your breathing reverberates off of the hard tile surfaces.

You can see a clone here.

I didn't understand that instruction.

>w
The clone arrives from the east.

Hallway
To the north you can see a window through which someone who looks identical to you is standing straight ahead.

You can see a clone and a rusty chair here.

I didn't understand that instruction.

>

The “I didn’t understand that instruction.” is performed by the same After rule, I still do not understand why it is fired again.

This seems to work:

"The Clone Who Walked Through Glass Walls" by Zoe Lebeau

A room can be glass. A room is usually not glass. A person can be passable or unpassable. A person is usually unpassable. A person has an action called lastaction. A chair is a kind of supporter. A chair is always enterable.

CloneFlag is a truth state that varies.
CloneFlag is initially true.

Every turn:
	now CloneFlag is true.

After deciding the scope of the player:
	place the clone in scope; [required so that the report rules below work. If the clone is not in scope of the player, the report rules for the clone are simply ignored even though the clone has performed the action]

After the player doing something while CloneFlag is true:
	now the actor is the clone;
	if the noun is the RustyChair:
		now the noun is the LeatherChair;
	try the current action;
	now CloneFlag is false;
	now the actor is the player;
	continue the action;
	
Check an actor going to a glass room:
	if the person is passable:
		continue the action;
	otherwise:
		say "There's no way for you to pass through the glass wall to the other side.";
		stop the action;

After the clone getting off the LeatherChair:
	say "Your doppleganger mechanically stands up.";

After the clone entering the LeatherChair:
	say "Your doppelganger sits back down.";

After the clone looking:
	say "Your clone looks around, following your movements.";

After the player entering RustyChair:
	say "You sit down and stretch your legs out. This is incredibly comfortable.";

After the player getting off RustyChair:
	say "You get out of the chair.";

Hallway is south of Cloning Laboratory. "To the north you can see a window through which someone who looks identical to you is [if the clone is on a chair]sitting bolt upright in a leather chair.[else]standing straight ahead." A RustyChair is a chair in Hallway. Understand "chair" or "rusty chair" or "rusty" as RustyChair. The printed name of RustyChair is "rusty chair". 

Observation Window is east of hallway. "The sound of your breathing reverberates off of the hard tile surfaces."
		
Cloning Laboratory is a room. The printed name of Cloning Laboratory is "Cloning Laboratory". LeatherChair is a chair in Cloning Laboratory. Understand "chair" as LeatherChair. The printed name of LeatherChair is "chair". The clone is a person. The clone is on LeatherChair. Understand "clone" as the clone. The clone is passable.

Manufacturing Room is east of Cloning Laboratory. "A strange room full of beakers and 3D printing machines." Manufacturing Room is glass.

I don’t fully understand, but I think that the change in the actor’s point of view during action processing causes the bypass and/or repetition of After rules, hence the use of the small CloneFlag so that each actor benefits from the After rule only once [EDIT : per turn, of course]

1 Like

You might take a look at the extension Editable Stored Actions by Ron Newcomb, which has some features to make it easy to modify the actor, noun, second noun, or action name of a given stored action.

1 Like

Again this is all really good info, but I’d really love to know why this code behaves like this because it’s going to have applications further down the road with other pieces of code I write and other game projects.

@Zed or @zarf, if you have a moment can you explain what’s going on here with the “I didn’t understand” / after rule behaviors? Right now I feel like I’m just taking stabs in the dark.

This works.

lab is room.

Dummying is an action applying to nothing.
clone-action is initially the action of dummying.

setting action variables when not looking after going:
if the actor is the player begin;
  now the actor is the clone;
  now clone-action is the current action;
  now the actor is the player;
else;
  now the clone-action is the action of dummying;
end if.

clone is a person in the lab.

every turn when the action name part of the clone-action is not dummying action:
try the clone-action.

parlor is east of lab.

Include (-
Global looking_after_going = 0;

[ LookAfterGoing;
  looking_after_going = 1;
  GoingLookBreak();
  AbbreviatedRoomDescription();
  looking_after_going = 0;
];
-) replacing "LookAfterGoing".

to decide if looking after going: (- looking_after_going -).

It was a pain in the butt for me to figure this out. There’s a lot of stuff that isn’t documented and can’t even be found in the Standard Rules, but takes delving through the Inform 6 code in the kits to find. (And sometimes it’s not even there and you have to look at the Inform 7 compiler itself to try to figure something out.)

Report going implicitly invokes a look action to print a new room description. At the I6 level this is mostly a real action, but it’s not the equivalent of saying try looking in I7. So without the looking_after_going tracking above, the sequence would be:

beginning of player going east action
  setting action variables sets clone-action to clone going east
  carry out player going east: the player is actually moved east
  report the player going east: invoke the implicit look action
    beginning of player looking action
      setting action variables sets clone-action to clone looking
      carry out the player looking
    end of player looking
end of player going east action
[...]

and now when we get to the every turn rule, clone-action is set to clone looking but it’s also the case that noun is still set to east from the player going east action. So ActionPrimitive (in Actions.i6t) balks with the Action Processing Rule Response ‘K’ because it knows that looking is an action applying to nothing, so something must be wrong if noun isn’t nothing, 'cause it expects the parser would have prevented this situation from coming about.

This happens before the before rules, before even the setting action variable rules, so there’s no good way to intervene at the I7 level. From Actions.i6t:

@h Abbreviated Room Description.
This is used when we want a room description with the same abbreviation
conventions as after a going action, and we don’t quite want a looking
action fully to take place. We nevertheless want to be sure that the
action variables for looking exist, and in particular, we want to set the
“room-describing action” variable to the action which was prevailing
when the room description was called for. We also set “abbreviated form
allowed” to “true”: when the ordinary looking action is running, this
is “false”.

The actual description occurs during |LookSub|, which is the specific
action processing stage for the “looking” action: thus, we use the
check, carry out, after and report rules as if we were “looking”, but
are unaffected by before or instead rules.

Uniquely, this pseudo-action does not use |BeginAction|: it works only
through the specific action processing rules, not the main action-processing
ones, though that is not easy to see from the code below because it is
hidden in the call to |LookSub|.

Edited: actually there’s still a big problem here for other player actions that invoke actions within them. What we could really use is the same action nesting level we get from the actions command, but that doesn’t exist when compiling for release. So we replace ActionPrimitive just to add a couple lines to track nesting level:

Include (-
Global action_nesting_level = 0;

[ ActionPrimitive rv p1 p2 p3 p4 p5 frame_id;
        MStack_CreateRBVars(ACTION_PROCESSING_RB);
        if ((keep_silent == false) && (multiflag == false)) DivideParagraphPoint();
        reason_the_action_failed = 0;

        frame_id = -1;
        p1 = FindAction(action);
        if ((p1) && (ActionData-->(p1+AD_VARIABLES_CREATOR))) {
                frame_id = ActionData-->(p1+AD_VARIABLES_ID);
                Mstack_Create_Frame(ActionData-->(p1+AD_VARIABLES_CREATOR), frame_id);
        }
        if (ActionVariablesNotTypeSafe()) {
                if (actor ~= player) { ACTION_PROCESSING_INTERNAL_RM('K'); new_line; }
                if (frame_id ~= -1)
                        Mstack_Destroy_Frame(ActionData-->(p1+AD_VARIABLES_CREATOR), frame_id);
                MStack_DestroyRBVars(ACTION_PROCESSING_RB);
                return;
        }
        FollowRulebook(SETTING_ACTION_VARIABLES_RB);
        if (actor == player) action_nesting_level++;

        #IFDEF DEBUG;
        if ((trace_actions) && (FindAction(-1)) && (action ~= ##ActionsOn) && (action ~= ##ActionsOff)) {
                print "["; p1=actor; p2=act_requester; p3=action; p4=noun; p5=second;
                DB_Action(p1,p2,p3,p4,p5);
                print "]^"; ClearParagraphing(5);
        }
        ++debug_rule_nesting;
        #ENDIF;
        TrackActions(false, meta);
        if ((meta) && (actor ~= player)) {
                ACTION_PROCESSING_INTERNAL_RM('A', actor); new_line; rv = RS_FAILS; }
        else if (meta) { DESCEND_TO_SPECIFIC_ACTION_R(); rv = RulebookOutcome(); }
        else { FollowRulebook(ACTION_PROCESSING_RB); rv = RulebookOutcome(); }
        if (actor == player) action_nesting_level--;
        #IFDEF DEBUG;
        --debug_rule_nesting;
        if ((trace_actions) && (FindAction(-1)) && (action ~= ##ActionsOn) && (action ~= ##ActionsOff)) {
                print "["; DB_Action(p1,p2,p3,p4,p5); print " - ";
                switch (rv) {
                        RS_SUCCEEDS: print "succeeded";
                        RS_FAILS: print "failed";
                                if (reason_the_action_failed)
                                        print " the ",
                                                (RulePrintingRule) reason_the_action_failed;
                        default: print "ended without result";
                }
                print "]^"; say__p = 1;
                }
                print "]^"; say__p = 1;
                SetRulebookOutcome(rv); ! In case disturbed by printing activities
        }
        #ENDIF;
        if (rv == RS_SUCCEEDS) UpdateActionBitmap();
        if (frame_id ~= -1) {
                p1 = FindAction(action);
                Mstack_Destroy_Frame(ActionData-->(p1+AD_VARIABLES_CREATOR), frame_id);
        }
        MStack_DestroyRBVars(ACTION_PROCESSING_RB);
        if ((keep_silent == false) && (multiflag == false)) DivideParagraphPoint();
        if (rv == RS_SUCCEEDS) rtrue;
        rfalse;
];
-) replacing "ActionPrimitive".

and then this is a better, more general approach – we no longer need to track look after going.

lab is room.

Dummying is an action applying to nothing.
clone-action is initially the action of dummying.

setting action variables when top level action:
if the actor is the player begin;
  now the actor is the clone;
  now clone-action is the current action;
  now the actor is the player;
else;
  now the clone-action is the action of dummying;
end if.

clone is a person in the lab.

every turn when the clone-action is not dummying:
try the clone-action.

parlor is east of lab.

to decide if top level action: (- ~~action_nesting_level -).
4 Likes

I have some questions about your scenario, xyzoe:

  1. Is the clone supposed to be telepathically linked (or the like) such that it always tries to execute the same action as the player, or does it have to see the player doing that action?

  2. If it is copycat behavior based on seeing the player, are the clone and the player supposed to be able to see each other when in different rooms?

1 Like

@Zed, I would argue that there is just a bug in AbbreviatedRoomDescription() in that it doesn’t bother itself about noun and second noun. A general fix of that might be helpful:

[ AbbreviatedRoomDescription  prior_action pos frame_id;

	! BEGIN MODIFICATION
	@push action; @push noun; @push second;
	action = ##Look; noun = nothing; second = nothing;
	! END MODIFICATION
	pos = FindAction(##Look);
	if ((pos) && (ActionData-->(pos+AD_VARIABLES_CREATOR))) {
		frame_id = ActionData-->(pos+AD_VARIABLES_ID);
		Mstack_Create_Frame(ActionData-->(pos+AD_VARIABLES_CREATOR), frame_id);
		FollowRulebook(SETTING_ACTION_VARIABLES_RB);
		(MStack-->MstVO(frame_id, 0)) = prior_action; ! "room-describing action"
		(MStack-->MstVO(frame_id, 1)) = true; ! "abbreviated form allowed"
	}
	LookSub(); ! The I6 verb routine for "looking"
	if (frame_id) Mstack_Destroy_Frame(ActionData-->(pos+AD_VARIABLES_CREATOR), frame_id);

	! BEGIN MODIFICATION
	@pull second; @pull noun; @pull action;
	! END MODIFICATION
];
3 Likes

Some general tips that might be of use:

  • The after rulebook, despite the name, runs before the report rulebook. Additionally, the after rulebook has default success, which means that if the preamble condition of a rule in it is met, the rulebook will by default end after the rule is run – and in this case will also halt action processing. If you want normal action processing to continue into report rules (or even other after rules), you have to use continue the action when terminating an after rule. (Your After the player doing something: rule did not do this, which is relevant to your question about missing output.)

  • When reporting NPC actions, it’s important to check that the NPC is visible to the player. There is a little intelligence built into report rules to prevent display of reports when the actor can’t be seen by the player, but no such logic applies to after rules.

  • Because of some conflation in internal representation, adding something to scope also adds it to visibility, which can have undesirable side effects. (It looks like maybe you were depending on this at one point?)

4 Likes

But Zoe tells us that she tested it with a ‘report’ rule and got the same result. Could the problem fundamentally come from changing the actor without returning that role to the player before continuing the action process?

2 Likes

I’m not sure that I really understand what xyzoe is trying to do (though it sounds straightforward). There is a lot going on in this thread.

In xyzoe’s original code, there is the rule:

After the player doing something:
	now the actor is the clone;
	if the noun is the RustyChair:
		now the noun is the LeatherChair;
	try the current action;

As originally pointed out by DeusIrae, this rule should terminate with a continue the action in order to allow additional action processing – most particularly in this case the Standard Rules’ describe room gone into rule (a report rule).

However, as Monsieur.HUT points out, there is also the problem that xyzoe’s rule changes the actor to the clone but does not change it back to the player! With his modification, you get the “I didn’t understand that instruction.” error, as described. The room description output is also suppressed if the actor is not set back to player, because of the internal logic of the relevant report rule for the PC’s going action. The rulebook sees only the modified current action, which is changed when changing the actor. (See the code for the describe room gone into rule for details. The short version is that for NPCs, understandably, no room description is produced.)

Zed tracked down the reason for the unusual error message noted by Monsieur.HUT, which is that noun is being incorrectly left as the value from the going action when processing the clone’s post-movement looking action. As Zed notes, for actions that invoke subactions there is a generic problem to be addressed to ensure that only the “top level” action is attempted by the clone (e.g. going but not looking). He offers a modification to template code that can be used to do this.

As bg originally suggests, it seems helpful to move the triggering of the clone’s actions to the every turn stage; this might make it seem more like the clone is copying the player instead of pre-empting the player.

Hopefully none of that is wrong. To sum up: xyzoe managed to run into several unexpected problems at once. Well done! (You get mad science cred for particularly showy explosions.)

4 Likes

Thank you very much for drafting the retrospective status report!

1 Like