Running a routine once only before parsing each component command of a multi-command input line

I need to run a routine that amends the player’s command before parsing but have come unstuck because the ‘after reading a command’ rule only runs once- after the command line is read in- if the player types in multiple commands at once: e.g. ‘go east. examine flowers then take the bucket’

I can’t make substitutions to the player’s command at this point because they depend on the game state (e.g. location) which, in the example above, will have changed by the time the flowers are examined.

A klutzy workaround is to put the routine in the ‘before deciding the scope of the player’ rule, which fires each turn for each fresh command as the multi-command line is worked through, but this means the rule often tries to run several times each turn as the parser comes back to determining scope for various different purposes.

Surely there must be a better way?

2 Likes

Would it be doable to just force the player to enter commands one at a time? I had a game with some command hacking where I did this:

	say "Sorry, but it looks like you've chained more than one command together with the word 'THEN.' This game doesn't support that feature.";
	reject the player's command.

First after reading a command when the player's command matches the regular expression "\.":
	say "Sorry, but it looks like you've chained more than one command together with periods. This game doesn't support that feature.";
	reject the player's command.

(My source notes tell me Andrew Schultz helped me with the second code block.)

This doesn’t catch every chained command, because you can sometimes chain commands together with commas. That’s pretty buggy in itself, though, because you can only do that when the first command is nounless. “Jump, jump” will work, “x me, x me” is interpreted as if you had typed “x me and x me.” So if you don’t mind the possibility of additional bugs when people use that (not very common I think) syntax of chaining commands with commas, that could be done.

Unfortunately “n, x flowers” is one of the cases that does work and that you might want to keep. I think that you could remove the possibility of chained commands entirely by editing the I6 parser at a couple of places (look for calls to “THEN1__WD or THEN2__WD or THEN3__WD or comma_WORD”; I think there’s one in Parser Letter G and Parser Letter K).

I have another sort of idea, but I don’t have time to try it out now and it seems dangerous. This is:
put the code for the After Reading a Command Rules in a special rulebook called “command modification rules” or something.
Find the code from Parser Letter A for running multiple commands, which is this I think:

@p Parser Letter A.
Get the input, do OOPS and AGAIN.

@c
    if (held_back_mode == 1) {
        held_back_mode = 0;
        VM_Tokenise(buffer, parse);
        jump ReParse;
    }

Insert a call to the command modification rules.
Pray to the Ancient Gods of the Parser that things don’t break in horrible ways.

Hi, thank you for this incredibly rapid response. I have a working hack at present purely in I7, using the ‘before deciding the scope of the player’ rule, but you have just pointed out a new problem I hadn’t realised, in that it’s possible to string (some) commands together with commas.

Currently:

First the routine searches using ‘if the player’s command includes the topic entry:’ for a match against any one of a sequence of topics in a table relevant to the location in the whole of the residual ‘player’s command’ line as typed (which gets truncated at the front by one command each turn as the parser works through the command sequence).

Having found a potential match, it then slices off the front end of the residual ‘player’s command’ at the first occurence of ‘.’ or ‘then’ to produce something I’ve called the ‘current command’. I hadn’t considered the possibility of commas demarcating commands. Aaargh!

Finally, it walks through the ‘current command’ word by word to see if the ‘matched text’ found in the residual ‘player’s command’ as a whole occurred in this first command in the sequence and, if so, replaces the matched text in the player’s command with a substitute text.

I have been trying to avoid descending into I6 hackery as my knowledge of the parser code could be written on the back of a matchbox, but thanks for some useful pointers.

Leaving aside the ‘commas’ issue for now, my existing I7 hack seems so far to work but offends because it’s clearly sitting in an inappropriate place to hook into the turn sequence rules and not only is that inelegant but suggests a likelihood of throwing up bugs when running in unanticipated circumstances.

In a different way, bluntly restricting a conventional parser convenience (i.e. typing several commands on one line) in order to implement my ‘brilliant’ code also makes me a bit queasy.

It occurs to me that this could be made a whole lot simpler if the full command was put into a separate buffer, then each “sentence” of it was individually put into the main buffer (the one accessed by “the player’s command”), and the whole command-reading machinery then ran on each of those individually.

For those who know the parser better than me, is there any huge obstacle preventing this? There’s a nice place right after tokenizing and before parsing where the whole mess of periods and "then"s could be handled.

It would undoubtedly be preferable to get the parser to do the job of chopping up the typed command line into ‘sentences’ according to its usual processes then hook in just at the point where an individual command is ready to be parsed into an action- at which point the periods, commas and ‘thens’ will have already been interpreted according to usual parser behaviour and stripped away from the command to be parsed.

If this is indeed the way the parser works. It may instead just parse the full line in situ, building the current action, until it reaches a ‘THEN_WD’ and without ever creating a separate buffer to contain the ‘current command/sentence’?

Well, if you’re already slicing the player’s command up at every “.” or “then,” you should just be able to feed the bits that you slice back into the parser by writing a rule For reading a command.

This is what I came up with, for periods only:

Command chaining is a truth state that varies.

The command remainder is a text that varies.

First after reading a command when command chaining is false and the player's command matches the regular expression "(.+?)\.(.+)":
	now the command remainder is the text matching subexpression 2;
	change the text of the player's command to the text matching subexpression 1;
	now command chaining is true.
	
For reading a command when command chaining is true:
	if the command remainder matches the regular expression "(.+?)\.(.+)":
		change the text of the player's command to the text matching subexpression 1;
		now the command remainder is the text matching subexpression 2;
	otherwise:
		change the text of the player's command to the command remainder;	
		now command chaining is false.
	
After reading a command:
	say "command: [player's command]."

For my sins, the idea behind the regular expression the regular expression “(.+?)\.(.+)” is that the first expression in parentheses matches everything up to the first period, as long as there’s one or more characters before the period, and then the second expression matches everything after it. (The . means “any character,” the + means “at least one repetition,” the ? means “match lazily, so as little text as possible,” the \. escapes the period so it’s an actual period, and then the .+ means “any sequence of at least one character” again.) “text matching subexpression 1” and “text matching subexpression 2” are the texts matching the respective parenthesized groups, so what’s before the first period and what’s after it.

I guess you can extend this to . and then like this:

Command chaining is a truth state that varies.

The command remainder is a text that varies.

First after reading a command when command chaining is false and the player's command matches the regular expression "(.+?)(\.|\bthen\b)(.+)":
	now the command remainder is the text matching subexpression 3;
	change the text of the player's command to the text matching subexpression 1;
	now command chaining is true.
	
For reading a command when command chaining is true:
	if the command remainder matches the regular expression "(.+?)(\.|\bthen\b)(.+)":
		change the text of the player's command to the text matching subexpression 1;
		now the command remainder is the text matching subexpression 3;
	otherwise:
		change the text of the player's command to the command remainder;	
		now command chaining is false.
	
After reading a command:
	say "command: [player's command]."
	
Lab is a room.

The pipe character | is an “or,” and \b means a word boundary, so (\.|\bthen\b)
means “either a period or ‘then’ as a whole word.” And we’ve put in a new group so now the command remainder is the text matching subexpression 3 rather than 2.

I’ve only tested this a little and not with After reading rules that changes the text, so handle with care. Also of course it still doesn’t deal with commas, but since those are kind of buggy anyway it might be legitimate to hope nobody tries entering multiple commands that way.

Also, do you have a toy example of what you’re trying to do? Maybe there’s a more natural way of accomplishing it than amending the command before parsing (I’ve occasionally tried something that needs to be done that way, but those were usually pretty weird experiments).

As far as trying to handle it in the parser, this always makes me feel as though I am messing with Things Which Humans Were Not Meant To Know, but looking through Parser.i6t for “held_back_mode” yields these bits.

From Parser Letter G:

{

                ! If the player has entered enough already but there's still
                ! text to wade through: store the pattern away so as to be able to produce
                ! a decent error message if this turns out to be the best we ever manage,
                ! and in the mean time give up on this line

                ! However, if the superfluous text begins with a comma or "then" then
                ! take that to be the start of another instruction

                if (wn <= num_words) {
                    l = NextWord();
                    if (l == THEN1__WD or THEN2__WD or THEN3__WD or comma_word) {
                        held_back_mode = 1; hb_wn = wn-1;
                    }
                    else {
                        for (m=0 : m<32 : m++) pattern2-->m = pattern-->m;
                        pcount2 = pcount;
                        etype = UPTO_PE;
                        break;
                    }
                }

So I think this is storing the word number of the separator in hb_wn. Or actually just before, so when the parser goes to check again it can run NextWord to make sure that it really is a separator.

Further down in Parser Letter G:

 if (held_back_mode == 1) {
                    wn=hb_wn;
                    jump LookForMore;
                }

which is basically sending us to LookForMore if the above code triggered, and setting wn back to the word number stored in hb_wn.

LookForMore is Parser Letter K:

.LookForMore;

    if (wn > num_words) rtrue;

    i = NextWord();
    if (i == THEN1__WD or THEN2__WD or THEN3__WD or comma_word) {
        if (wn > num_words) {
           held_back_mode = false;
           return;
        }
        if (verb_wordnum > 0) i = WordAddress(verb_wordnum); else i = WordAddress(1);
        j = WordAddress(wn);
        if (i<=j) for (: i<j : i++) i->0 = ' ';
        i = NextWord();
        if (i == AGAIN1__WD or AGAIN2__WD or AGAIN3__WD) {
            ! Delete the words "then again" from the again buffer,
            ! in which we have just realised that it must occur:
            ! prevents an infinite loop on "i. again"

            i = WordAddress(wn-2)-buffer;
            if (wn > num_words) j = INPUT_BUFFER_LEN-1;
            else j = WordAddress(wn)-buffer;
            for (: i<j : i++) buffer3->i = ' ';
        }
        VM_Tokenise(buffer,parse);
        held_back_mode = true;
        return;
    }
    best_etype = UPTO_PE;
    jump GiveError;

which…I’m not totally sure, but I think it’s checking that it really is a separator and that we haven’t overrun the word buffer, doing some trickery to make sure that an “again” after the separator doesn’t try to repeat the whole line including the “again,” and then running VM_Tokenise? I don’t know.

Also Parser Letter A, the very beginning:

 if (held_back_mode == 1) {
        held_back_mode = 0;
        VM_Tokenise(buffer, parse);
        jump ReParse;
    }

It does seem to me that it parses in situ, builds the action, and then if it hits a “THEN” starts over with the remainder, which seems like what you don’t want to do. But I sure don’t know!

It all began with an effort to replicate the I5 scenery.h extension by Joe Mason, which provided an elegant solution to provide verbose descriptions of non-object scenery mentioned in room descriptions.

The original seems to have been overtaken by developments in parser dependencies even with the advent of I6, never mind I7. Maybe everyone just implements multiple scenery objects nowadays.

Anyway, the idea is to have a nice table of verbose non-object scenery descriptions for each room. The (possibly crazy) way I hit on of implementing this was to have a ‘dummy object’ which temporarily stood in as a scenery object every time the player referred to something mentioned in the ‘Table of Scenery’; setting appropriate printed name, description and plural named properties; then substitute the name of this dummy object for the ‘virtual scenery’ the player had actually typed in the command.

So for example ‘examine rosebed’ would become ‘examine sobj’ and sobj would have the printed name “rosebed” the description “A riot of floral colour” and be singular-named.

In practice, this approach has led to snowballing difficulties separate to the parser problem, such as keeping pronouns working, and I ended up implementing two objects, one for singular named and one for plural named scenery, a fix to avoid confusing the parser by manipulating the command at the scoping stage, code to deal with the player doing things other than examining the scenery etc. etc. and I do wonder if there is a fundamentally better way of tackling this.

Regarding the parser issue, your suggestion of a ‘For reading a command rule’ is certainly more elegant than the one I hit on, and returns the command line manipulation to its proper place in the turn sequence.

"A Scenic View" by PB

Book 1 - Setup

Part 1 - Out-of-World Setup

Chapter 1 - When Play Begins

When play begins:
	[now scenery_debug_1 is true;]
	[now scenery_debug_2 is true;]
	[now command_debug_1 is true;]
	continue the action;

Chapter 2 - Every Turn

[[Every turn]]	
Every turn:
	[scenery handling]
	now the sobj is nowhere;
	now the pobj is nowhere;
	if command_debug_1 is true:
		say "Every turn runs....";
		say "Original typed command- [original command typed]  This turn's command- [player's command][line break]";
		

Chapter 3 - Global Variables

command_debug_1 is a truth state that varies. command_debug_1 is usually false.
scenery_debug_1 is a truth state that varies. scenery_debug_1 is usually false.
scenery_debug_2 is a truth state that varies. scenery_debug_2 is usually false. 
   
Chapter 4 - Scenery

Section 1 - Definitions

[[Scenery]]
A room has a table name called the detailed scenery.  The detailed scenery is usually the Table of Default Scenery.

Table of Default Scenery
topic	plural	description
"default"	false	"It's really just scenery"

The sobj is scenery.
The pobj is scenery. It is plural-named.

The original command typed is a text that varies. [original command typed, including all multiple commands]
The matched_description is a text that varies.
The provisional_action is an action that varies.

singular_matching_scenery is a truth state that varies. [flag for having matched a singular-named piece of scenery]
plural_matching_scenery is a truth state that varies. [flag for having matched a plural-named piece of scenery]
match_count is a number that varies [counts the number of scenery matches made during iterations]

Section 2 - Processing the Command

Before reading a command:
	if command_debug_1 is true:
		say "Reading command....";
		say "Previous typed command- [original command typed]  Last turn translation- [player's command][line break]";
		now the original command typed is "";

After reading a command:
	If the original command typed is "":
		let snp be the player's command;
		[need to use a temporary variable copy of a snippet to force a deep copy casting to text]
		now the original command typed is "[snp]"; [casts a permanent copy to type text]

Before deciding the scope of the player (this is the scenery substitution rule): [can't do this after reading command as it fails to run again for each command if there are multiple commands typed into one line]
	if scenery_debug_2 is true or command_debug_1 is true, say "Processing command: [the player's command][line break]";
	now singular_matching_scenery is false;
	now plural_matching_scenery is false;
	now the matched_description is "##none##";
	now the match_count is 0;
	repeat with iteration running from 1 to 2:
		if scenery_debug_2 is true, say "iteration [iteration][line break]";
		repeat through the detailed scenery of the location:
			if the player's command includes the topic entry: [we have a potential match]
				unless "[matched text]" is quoted in the immediate command:
					next; [deal with multiple commands on one line by rejecting any not in the immediate command]
				increment the match_count; [match found, so continue]
				if scenery_debug_1 is true, say "matched [description entry][line break]";
				if scenery_debug_1 is true, say "[matched text]";
				now the matched_description is the description entry;
				if the plural entry is true:
					now plural_matching_scenery is true;
					now the description of the pobj is the matched_description; 
					now the printed name of the pobj is the matched text;
					replace the matched text with "[padpluralmatchedwords]pobj"; [need to replace with same number of words or parser gets confused when already at stage of deciding scope]
					set pronouns from the pobj;
				otherwise:
					now singular_matching_scenery is true;
					now the description of the sobj is the matched_description; 
					now the printed name of the sobj is the matched text;
					replace the matched text with "[padsingularmatchedwords]sobj"; [need to replace with same number of words or parser gets confused when already at stage of deciding scope]
					set pronouns from the sobj;
				if scenery_debug_1 is true, announce_pronouns;
		if match_count is 0:
			break; [no match so no point repeating]
	if the player's command includes "them":
		if scenery_debug_1 is true, say "the them object is [the them_object].";
		if the them_object is the pobj:
			if "them" is quoted in the immediate command:
				increment the match_count;
				now plural_matching_scenery is true;
				[set pronouns from the scenic_object;]
				if scenery_debug_1 is true,  announce_pronouns;
	if the player's command includes "it":
		if scenery_debug_1 is true, say "the it object is [the it_object].";
		if the it_object is the sobj:
			if "it" is quoted in the immediate command:
				increment the match_count;
				now singular_matching_scenery is true;
				[set pronouns from the scenic_object;]
				if scenery_debug_1 is true, announce_pronouns;
	[continue the activity;]
[Before deciding the scope of the player:]
	if singular_matching_scenery is true:
		move sobj to location;
	if plural_matching_scenery is true:
		move pobj to location;
	if scenery_debug_2 is true, say "(trying [player's command])[line break]";
	continue the activity;
	
Section 3 - Supporting Functions
	
To say padpluralmatchedwords:
	let N be the number of punctuated words in the matched text;
	if N > 1:
		repeat with S running from 2 to N:
			say "pobj ";

To say padsingularmatchedwords:
	let N be the number of punctuated words in the matched text;
	if N > 1:
		repeat with S running from 2 to N:
			say "sobj ";

To decide whether (T - some text) is quoted in the immediate command:
	if scenery_debug_2 is true, say "potential match for '[T]'[line break]";
	[check that it occurs before the first period '.' or 'then']
	let cmd_text be "[the player's command]";
	let match_text be "[T]";
	let match_found be false;
	let word_match be 0;
	repeat with word_index running from 1 to number of punctuated words in cmd_text:
		if scenery_debug_2 is true,  say "trying command word [word_index]; ";
		let match_found be true; [try to find a match at this position in cmd_text]
		if punctuated word number word_index in cmd_text exactly matches the regular expression "\.|then":
			if scenery_debug_2 is true,  say ". or then found at position [word_index][line break]";
			now match_found is false;
			break;[not a match in the first phrase]
		else:
			 repeat with match_index running from 1 to number of punctuated words in match_text:
				if scenery_debug_2 is true,  say "trying matched word [match_index] '[word number match_index in match_text]'; ";
				if punctuated word number (word_index + match_index - 1) in cmd_text exactly matches the text word number match_index in match_text:
					if scenery_debug_2 is true,  say "matched; ";
					next;
				else:
					now match_found is false;
					break; [not a match, try next word posiion in cmd_text]
			now word_match is word_index; [final word position tried]
		if scenery_debug_2 is true,  say "[line break]";
		if match_found is true:
			break;
	if match_found is false:
		decide no; [no match for this entry in the scenery table, so try next]
	if scenery_debug_2 is true,  say "match for '[T]' found at word [word_match][line break]";
	decide yes;
		
After deciding the scope of the player:
	now the provisional_action is the current action;

Chapter 5 - Generic Action Interventions
	
[[Actions Generic]]	
Before doing something:
	if the match_count is greater than 1: [reject multiple references to different items of scenery]
		[[SPECIAL CASES]]
		if the provisional_action is examining nothing: [special case for EXAMINING. nouns are set to nothing at scoping stage]
			say "I couldn't fully understand what [we] [are] wanting to look at." instead;
		else:	 [GENERIC RESPONSE]
			say "[We] [are] best not interfering too much with the scenery." instead;
		stop the action;
		
Part 2 - Pronouns

[[Pronouns]]
[! ==== ==== ==== ==== ==== ==== ==== ==== ==== ====
!  Pronoun Functions
! ==== ==== ==== ==== ==== ==== ==== ==== ==== ====]

To announce_pronouns: (- ANNOUNCE_PRONOUN_MEANINGS_R(); -)

Include
(- [ GET_PRONOUN_OBJ_R  x ;
				return LanguagePronouns-->(x*3);
]; -)

Include
(- [ SET_PRONOUN_OBJ_R  y x ;
				LanguagePronouns-->(x*3) =  y;
]; -)


To set the it object to (O - an object):
	(- SET_PRONOUN_OBJ_R( {O}, 1 ); -)

To set the him object to (O - an object):
	(- SET_PRONOUN_OBJ_R( {O}, 2 ); -)
	
To set the her object to (O - an object):
	(- SET_PRONOUN_OBJ_R( {O}, 3 ); -)
	
To set the them object to (O - an object):
	(- SET_PRONOUN_OBJ_R( {O}, 4 ); -)	


To decide which object is the it_object:
	(- GET_PRONOUN_OBJ_R(1) -)

To decide which object is the him_object:
	(- GET_PRONOUN_OBJ_R(2) -)

To decide which object is the her_object:
	(- GET_PRONOUN_OBJ_R(3) -)

To decide which object is the them_object:
	(- GET_PRONOUN_OBJ_R(4) -)
		

Book 2 - Scenario

Part 1 - Samares Gardens

Chapter 1 - The Gardens

Section 1 - Description

Samares Gardens is a room. "A beautiful and lush English garden in the height of summer. The hedges are neatly clipped, the borders carefully tended and the distant stately house a gothic shimmer in the midday heat.[line break][line break]A mossy stone path winds east from the lawn and down through a dense shrubbery."  It is proper-named. It is plural-named. The detailed scenery is the Table of Garden Scenery.

Section 2 - Scenery

Table of Garden Scenery
topic	plural	description
"hedges"	true	"Beautifully clipped privet, including a number of baroque topiary forms."
"borders"	true	"In early June the borders are a riot of fragrant colour."
"grandfather's/distant/gothic/stately/house/pile"	false	"[Our] grandfather's stately pile."
"bright/white/sun/heat/haze"	false	"The bright white sun casts a shimmering haze over the garden."
"lawn/grass/sward"	false	"Lovingly manicured green sward.  Imprinted by tiny trotter-marks."
" tiny/-- trotter/-- marks/trotter-marks"	true	"Hmm. That won't put the gardener in a good mood..."

Chapter 2 - Japanese Pond

Section 1 - Room

[[Japanese Pond]]
The Japanese Pond is east of the Gardens. "This is the edge of a pond in the Japanese gardens.  Dragonflies flit and hover like iridescent sparks over the unruffled mirror of the water; tall bullrushes stand silently in the summer heat.[line break][line break]A mossy stone path leads west back up to the main gardens." The detailed scenery is the Table of Pond Scenery.

Section 2 - Scenery

Table of Pond Scenery
topic	description	plural
"tall/-- bullrushes/rushes"	"Like serried ranks of green-garbed soldiers they march into the still waters."	true
"glittering/-- jewels/dragonflies/flies"	"Like glittering jewels, they flit, swoop and hover over the bright surface of the pond."	true

So I would definitely say that the thing most people do here is to implement multiple scenery objects.

If that makes too many objects, I wondered if you might be able to have every synonym of all the various scenery things refer to the sobj, and then search through the command to see exactly which one the player was referring to. Although that would lead to problems with “x borders. x hedges” when the parser would be unable to tell whether you meant the borders or hedges.

Also I edited code tags into your post, mostly to make it easier for me to swipe your code and look at it! if you type ``` before and after your code (on separate lines), or highlight the code and hit the </> button, it’ll make a special code block, which preserves indentation and generates a “copy code” button that will copy the code into the clipboard.

Anyway this is cool stuff and very thought-provoking for some other potential experiments, so I’m really glad to see it! (One thing that I think about a lot: You have a few objects that you keep recycling to assign procedurally generated descriptions for. You want the player to be able to refer to them by anything in the description. This kind of code would help with that.)

Thanks for the tip regarding posting code. I hadn’t managed to work that out :frowning:

I suspect the technique I’m attempting is a throwback to bygone days when the maximum stock of available objects was a concern.

Still, as a newcomer to Inform 7 I’ve learned a lot trying this, not least the helpfulness and responsiveness of this forum.

I’ve had another thought, which seems to work and avoids changing the text of the command, which always feels like an uncomfortable hack-
‘Understand the printed name property as referring to the sobj’ means that once the printed name of the sobj is updated to be the ‘matched text’ in the command that matched up to the topic entry in the table, the parser then recognises the sobj as being whatever the player typed in the command…