[solved] I7: Rejecting multiple objects

So for one of the games I wrote for the TMBG tribute, I abused the parser a bit.

after reading a command:
  if the player's command contains "\band\b":
    say "You don't have time to do two things at once.";
    reject the player's command;
  if the player's command contains ",":
    say "You don't have time to do anything with a comma in it."
    reject the player's command;

This worked okay in a pinch. However, I suspect I’m missing some more basic command such as “end the player’s turn” so that, if the player tries to pick up the apple, beer and backpack, the computer either

  1. only lets him pick up the apple
  2. rejects the command entirely.

So…

room 1 is a room.

apple is a thing in room 1. beer is a thing in room 2. backpack is a thing in room 3.

[understand "examine [things]" as a mistake ("No time for that!"). <- this prints "No time for that!" several times & I want something general for any command]

Example 420 in the recipe book also seems handy, but again, it applies only to one verb. I’m wondering about something more general that doesn’t rely on potentially dangerous parsing?

Thanks!

The correct (but very tedious) way to knock out all multiple verbs: redefine all of the grammar that accepts multiple objects. (Put, take, drop, remove, insert.) Then customize the can’t use multiple objects error.

There isn’t a direct way to cut off a multiple action sequence, although there probably should be. Currently the parser only cuts it off in case of death (ending the story), or if the player is moved by one of the actions. (In the latter case, it prints the message “Since something dramatic has happened, your list of commands has been cut short.”)

If you are trying to stop persuasion and the message “You seem to want to talk to someone, but I can’t see whom.” by blocking commands with commas in, this bit of code will do the trick.

Include (- -) instead of "Parser Letter C" in "Parser.i6t".

Try this.

[code]“Test”

The Testing Room is A Room.

An apple, a beer and a backpack are in the testing room.

Before doing anything other than taking or dropping or looking: say “You can only take the apple!” instead.

Before taking anything when the noun is not the apple: say “You can only take the apple!” instead.

Test me with “eat apple / drink beer / take backpack / take apple / drop apple / take all”.[/code]

This will only allow taking and dropping the apple.

This almost works, but it still says something like “apple: Let’s just stick to one thing at a time.” instead of just “Let’s just stick to one thing at a time.”

[code]“Test”

The cave is a room.

An apple, a banana, and a pear are in the cave.

Before when the multiple object list is not empty:
Say “Let’s just stick to one thing at a time.”;
Alter the multiple object list to {};
Stop the action.[/code]

EDIT: This works perfectly, so far as I can tell:

[code] This is the special rule:
If the multiple object list is empty, make no decision;
Say “Let’s just stick to one thing at a time.”;
Rule fails.

The special rule is listed before the generate action rule in the turn sequence rulebook.[/code]

If you just wanted to stop commands like “take the apple and the beer and the backpack”, “take all” and give the message “You can’t use multiple objects with that verb.” instead, then this should work.

[spoiler][code]“Test”

Include (-

! This is an actual specified object, and is therefore where a typing error
! is most likely to occur, so we set:

oops_from = wn;

! So, two cases.  Case 1: token not equal to "held" (so, no implicit takes)
! but we may well be dealing with multiple objects

! In either case below we use NounDomain, giving it the token number as
! context, and two places to look: among the actor's possessions, and in the
! present location.  (Note that the order depends on which is likeliest.)

if (token ~= HELD_TOKEN) {
	i = multiple_object-->0;
	#Ifdef DEBUG;
	if (parser_trace >= 3) print "  [Calling NounDomain on location and actor]^";
	#Endif; ! DEBUG
	l = NounDomain(actors_location, actor, token);
	if (l == REPARSE_CODE) return l;                  ! Reparse after Q&A
	if (indef_wanted == INDEF_ALL_WANTED && l == 0 && number_matched == 0)
		l = 1;  ! ReviseMulti if TAKE ALL FROM empty container

	if (token_allows_multiple && ~~multiflag) {
		if (best_etype==MULTI_PE) best_etype=STUCK_PE;
		multiflag = true;
	}
	if (l == 0) {
		if (indef_possambig) {
			ResetDescriptors();
			wn = desc_wn;
			jump TryAgain2;
		}
		!if (etype == MULTI_PE or TOOFEW_PE && multiflag) etype = STUCK_PE;
		etype=CantSee();
		jump FailToken;
	} ! Choose best error

	#Ifdef DEBUG;
	if (parser_trace >= 3) {
		if (l > 1) print "  [ND returned ", (the) l, "]^";
		else {
			print "  [ND appended to the multiple object list:^";
			k = multiple_object-->0;
			for (j=i+1 : j<=k : j++)
				print "  Entry ", j, ": ", (The) multiple_object-->j,
					  " (", multiple_object-->j, ")^";
			print "  List now has size ", k, "]^";
		}
	}
	#Endif; ! DEBUG

	if (l == 1) {
		if (~~many_flag) many_flag = true;
		else {                                ! Merge with earlier ones
			k = multiple_object-->0;            ! (with either parity)
			multiple_object-->0 = i;
			for (j=i+1 : j<=k : j++) {
				if (and_parity) MultiAdd(multiple_object-->j);
				else            MultiSub(multiple_object-->j);
			}
			#Ifdef DEBUG;
			if (parser_trace >= 3)
				print "  [Merging ", k-i, " new objects to the ", i, " old ones]^";
			#Endif; ! DEBUG
		}
	}
	else {
		! A single object was indeed found

		if (match_length == 0 && indef_possambig) {
			! So the answer had to be inferred from no textual data,
			! and we know that there was an ambiguity in the descriptor
			! stage (such as a word which could be a pronoun being
			! parsed as an article or possessive).  It's worth having
			! another go.

			ResetDescriptors();
			wn = desc_wn;
			jump TryAgain2;
		}

		if ((token == CREATURE_TOKEN) && (CreatureTest(l) == 0)) {
			etype = ANIMA_PE;
			jump FailToken;
		} !  Animation is required

		if (~~many_flag) single_object = l;
		else {
			if (and_parity) MultiAdd(l); else MultiSub(l);
			#Ifdef DEBUG;
			if (parser_trace >= 3) print "  [Combining ", (the) l, " with list]^";
			#Endif; ! DEBUG
		}
	}
}

else {

! Case 2: token is "held" (which fortunately can't take multiple objects)
! and may generate an implicit take

	l = NounDomain(actor,actors_location,token);       ! Same as above...
	if (l == REPARSE_CODE) return l;
	if (l == 0) {
		if (indef_possambig) {
			ResetDescriptors();
			wn = desc_wn;
			jump TryAgain2;
		}
		etype = CantSee(); jump FailToken;            ! Choose best error
	}

	! ...until it produces something not held by the actor.  Then an implicit
	! take must be tried.  If this is already happening anyway, things are too
	! confused and we have to give up (but saving the oops marker so as to get
	! it on the right word afterwards).
	! The point of this last rule is that a sequence like
	!
	!     > read newspaper
	!     (taking the newspaper first)
	!     The dwarf unexpectedly prevents you from taking the newspaper!
	!
	! should not be allowed to go into an infinite repeat - read becomes
	! take then read, but take has no effect, so read becomes take then read...
	! Anyway for now all we do is record the number of the object to take.

	o = parent(l);
	if (o ~= actor) {
		#Ifdef DEBUG;
		if (parser_trace >= 3) print "  [Allowing object ", (the) l, " for now]^";
		#Endif; ! DEBUG
	}
	single_object = l;
} ! end of if (token ~= HELD_TOKEN) else

! The following moves the word marker to just past the named object...

wn = oops_from + match_length;

-) instead of “Parse Token Letter D” in “Parser.i6t”.

Include (-

! Object(s) specified now: is that the end of the list, or have we reached
! "and", "but" and so on?  If so, create a multiple-object list if we
! haven't already (and are allowed to).

.NextInList;

o = NextWord();

if (o == AND1__WD or AND2__WD or AND3__WD or BUT1__WD or BUT2__WD or BUT3__WD or comma_word) {

	#Ifdef DEBUG;
	if (parser_trace >= 3) print "  [Read connective '", (address) o, "']^";
	#Endif; ! DEBUG

	!if (~~token_allows_multiple) {
		!if (multiflag) jump PassToken; ! give UPTO_PE error
		etype=MULTI_PE;
		jump FailToken;
	!}

	if (o == BUT1__WD or BUT2__WD or BUT3__WD) and_parity = 1-and_parity;

	if (~~many_flag) {
		multiple_object-->0 = 1;
		multiple_object-->1 = single_object;
		many_flag = true;
		#Ifdef DEBUG;
		if (parser_trace >= 3) print "  [Making new list from ", (the) single_object, "]^";
		#Endif; ! DEBUG
	}
	dont_infer = true; inferfrom=0;           ! Don't print (inferences)
	jump ObjectList;                          ! And back around
}

wn--;   ! Word marker back to first not-understood word

-) instead of “Parse Token Letter E” in “Parser.i6t”.

Include (-

[ Adjudicate context i j k good_ones last n ultimate flag offset;
#Ifdef DEBUG;
if (parser_trace >= 4) {
print " [Adjudicating match list of size ", number_matched,
" in context ", context, “^”;
print " ";
if (indef_mode) {
print "indefinite type: ";
if (indef_type & OTHER_BIT) print "other ";
if (indef_type & MY_BIT) print "my ";
if (indef_type & THAT_BIT) print "that ";
if (indef_type & PLURAL_BIT) print "plural ";
if (indef_type & LIT_BIT) print "lit ";
if (indef_type & UNLIT_BIT) print "unlit ";
if (indef_owner ~= 0) print “owner:”, (name) indef_owner;
new_line;
print " number wanted: ";
if (indef_wanted == INDEF_ALL_WANTED) print “all”; else print indef_wanted;
new_line;
print " most likely GNAs of names: ", indef_cases, “^”;
}
else print “definite object^”;
}
#Endif; ! DEBUG

j = number_matched-1; good_ones = 0; last = match_list-->0;
for (i=0 : i<=j : i++) {
	n = match_list-->i;
	match_scores-->i = good_ones;
	ultimate = ScopeCeiling(n);

	if (context==HELD_TOKEN && parent(n)==actor)
	{   good_ones++; last=n; }
	if (context==MULTI_TOKEN && ultimate==ScopeCeiling(actor)
		&& n~=actor && n hasnt concealed && n hasnt scenery) 
	{   good_ones++; last=n; }
	if (context==MULTIHELD_TOKEN && parent(n)==actor)
	{   good_ones++; last=n; }

	if (context==MULTIEXCEPT_TOKEN or MULTIINSIDE_TOKEN)
	{   if (advance_warning==-1)
		{   if (context==MULTIEXCEPT_TOKEN)
			{   good_ones++; last=n;
			 }
			if (context==MULTIINSIDE_TOKEN)
			{   if (parent(n)~=actor) { good_ones++; last=n; }
			 }
		}
		else
		{   if (context==MULTIEXCEPT_TOKEN && n~=advance_warning)
			{   good_ones++; last=n; }
			if (context==MULTIINSIDE_TOKEN && n in advance_warning)
			{   good_ones++; last=n; }
		}
	 }
	if (context==CREATURE_TOKEN && CreatureTest(n)==1)
	{   good_ones++; last=n; }
	
	match_scores-->i = 1000*(good_ones - match_scores-->i);
}
if (good_ones == 1) return last;

! If there is ambiguity about what was typed, but it definitely wasn't
! animate as required, then return anything; higher up in the parser
! a suitable error will be given.  (This prevents a question being asked.)

if (context == CREATURE_TOKEN && good_ones == 0) return match_list-->0;

if (indef_mode == 0) indef_type=0;

ScoreMatchL(context);
if (number_matched == 0) return -1;

if (indef_mode == 0) {
	!  Is there now a single highest-scoring object?
	i = SingleBestGuess();
	if (i >= 0) {

		#Ifdef DEBUG;
		if (parser_trace >= 4) print "   Single best-scoring object returned.]^";
		#Endif; ! DEBUG
		return i;
	}
}

if (indef_mode == 1 && indef_type & PLURAL_BIT ~= 0) {
	!if (context ~= MULTI_TOKEN or MULTIHELD_TOKEN or MULTIEXCEPT_TOKEN
				 !or MULTIINSIDE_TOKEN) {
		etype = MULTI_PE;
		return -1;
	!}
	i = 0; offset = multiple_object-->0;
	for (j=BestGuess(): j~=-1 && i<indef_wanted && i+offset<MATCH_LIST_WORDS-1:
		j=BestGuess()) {
		flag = 0;
		BeginActivity(DECIDING_WHETHER_ALL_INC_ACT, j);
		if ((ForActivity(DECIDING_WHETHER_ALL_INC_ACT, j)) == 0) {

			if (j hasnt concealed && j hasnt worn) flag = 1;
		
			if (context == MULTIHELD_TOKEN or MULTIEXCEPT_TOKEN && parent(j) ~= actor)
				flag = 0;

			if (action_to_be == ##Take or ##Remove && parent(j) == actor)
				flag = 0;

			k = ChooseObjects(j, flag);

			if (k == 1)
				flag = 1;
			else {
				if (k == 2) flag = 0;
			}
		} else {
			flag = 0; if (RulebookSucceeded()) flag = 1;
		}
		EndActivity(DECIDING_WHETHER_ALL_INC_ACT, j);
		if (flag == 1) {
			i++; multiple_object-->(i+offset) = j;
			#Ifdef DEBUG;
			if (parser_trace >= 4) print "   Accepting it^";
			#Endif; ! DEBUG
		}
		else {
			i = i;
			#Ifdef DEBUG;
			if (parser_trace >= 4) print "   Rejecting it^";
			#Endif; ! DEBUG
		}
	}
	if (i < indef_wanted && indef_wanted < INDEF_ALL_WANTED) {
		etype = TOOFEW_PE; multi_wanted = indef_wanted;
		multi_had=i;
		return -1;
	}
	multiple_object-->0 = i+offset;
	multi_context = context;
	#Ifdef DEBUG;
	if (parser_trace >= 4)
		print "   Made multiple object of size ", i, "]^";
	#Endif; ! DEBUG
	return 1;
}

for (i=0 : i<number_matched : i++) match_classes-->i = 0;

n = 1;
for (i=0 : i<number_matched : i++)
	if (match_classes-->i == 0) {
		match_classes-->i = n++; flag = 0;
		for (j=i+1 : j<number_matched : j++)
			if (match_classes-->j == 0 && Identical(match_list-->i, match_list-->j) == 1) {
				flag=1;
				match_classes-->j = match_classes-->i;
			}
		if (flag == 1) match_classes-->i = 1-n;
	}
 n--; number_of_classes = n;

#Ifdef DEBUG;
if (parser_trace >= 4) {
	print "   Grouped into ", n, " possibilities by name:^";
	for (i=0 : i<number_matched : i++)
		if (match_classes-->i > 0)
			print "   ", (The) match_list-->i, " (", match_list-->i, ")  ---  group ",
			  match_classes-->i, "^";
}
#Endif; ! DEBUG

if (indef_mode == 0) {
	if (n > 1) {
		k = -1;
		for (i=0 : i<number_matched : i++) {
			if (match_scores-->i > k) {
				k = match_scores-->i;
				j = match_classes-->i; j = j*j;
				flag = 0;
			}
			else
				if (match_scores-->i == k) {
					if ((match_classes-->i) * (match_classes-->i) ~= j)
						flag = 1;
				}
		}

	if (flag) {
		#Ifdef DEBUG;
		if (parser_trace >= 4) print "   Unable to choose best group, so ask player.]^";
		#Endif; ! DEBUG
		return 0;
	}
	#Ifdef DEBUG;
	if (parser_trace >= 4) print "   Best choices are all from the same group.^";
	#Endif; ! DEBUG
	}
}

!  When the player is really vague, or there's a single collection of
!  indistinguishable objects to choose from, choose the one the player
!  most recently acquired, or if the player has none of them, then
!  the one most recently put where it is.

if (n == 1) dont_infer = true;
return BestGuess();

]; ! Adjudicate

-) instead of “Adjudicate” in “Parser.i6t”.

The Testing Room is A Room.

An apple, a beer and a backpack are in the testing room.

Before doing anything other than taking or dropping or looking: say “You can only take the apple!” instead.

Before taking anything when the noun is not the apple: say “You can only take the apple!” instead.

Test me with “eat apple / drink beer / take backpack / take apple / drop apple / take all / take the apple and the beer and the backpack”.[/code][/spoiler]

This will prevent taking multiple things at once.

The special rule solution is elegant!

As far as the first solution goes, example 420 shows you how to suppress “apple:” and the like if that’s what you want to do; look at the selectively announce items from the multiple object list (which eliminates “apple:” etc. when you’re multiple-giving) and the business involving “already gave at the office,” which the example defines as a truth state that checks whether you’ve already run the special code that applies to multiple objects, and stops the action if you have. So, though Inform is trying to perform an action for each thing on the multiple object list, it only really runs the code for the first object (and consults the whole multiple object list to see what to do); for subsequent objects “already gave at the office” is true, and the action gets stopped.

I used a similar but simpler trick to make the “understand… as a mistake” approach work once:

[code]Understand “juggle [things]” as a mistake ("[if juggle-warned is false]Just type ‘JUGGLE’ and you’ll juggle what you’re holding.[juggle trigger][otherwise][run paragraph on][end if]")

Juggle-warned is a truth state that varies. Juggle-warned is false.
After reading a command: now juggle-warned is false.
To say juggle trigger: now juggle-warned is true. [This awful hack prevents the "Just type ‘Juggle’ message from printing three times in a turn if you try “Juggle all”.][/code]

Of course this only worked because I was trying to intercept one action with one applicable command. It’s also probably a bad idea in several ways; this was a small time-constrained project.

Wow. Thanks, everyone. I wasn’t getting mails from the forum, so when I remembered to check here, it was great to see all this good stuff.

It’s really cool that NYKevin’s solution was about rules. I’m starting to use them more nontrivially and this is another nice jump up.