Sitting On the Nearest Thing

I have a sneaking suspicion about this, but I need more eyes on it. Here’s the setup:

The Lab is a room.
The player wears the fancy cloak.
The seating is a backdrop in the lab. Understand "seat", "seats", "seating" as the seating. The seating can be enterable. The seating is enterable.
The box is a scenery supporter in the lab. The box can be enterable. The box is enterable.
The silver fork is in the lab.
Test me with "x seat / x cloak / sit / sit on seat".

The result is this:

Summary

[1] x seat
You see nothing special about the seating.

[2] x cloak
You see nothing special about the fancy cloak.

[3] sit
(on the fancy cloak)
That’s not something you can sit down on.

[4] sit on seat
That’s not something you can sit down on.

ACTIONS and RULES show that Inform chooses the fancy cloak before any rules are processed, including the entering action’s find what to enter rule. My sneaking suspicion was that the parser was having trouble matching the command, and it was choosing the nearest thing to hand. So, then I tried TRACE. You have to use a level 4 trace to find out that NounDomain is what’s picking the cloak.

Now I’m thinking all I have to do is make the background enterable and that will fix it. No dice. Even with an enterable backdrop, and enterable piece of scenery, and another object in the location, NounDomain skill picks the cloak. Even making the scenery a supporter doesn’t get Inform to pick something else.

Since the most obvious solution didn’t leap into my mind, I added a Does the Player Mean line for the fancy cloak. The result is not what I expected:

Summary

[3] sit
(on the silver fork)
That’s not something you can sit down on.

This sounds like a bug. I’m pretty sure NounDomain doesn’t know what the action is, so it’s just picking the nearest thing, which wouldn’t be a bug.

Of course, the simplest solution is:

Understand "sit" as entering.

Edit: Further testing indicates the simplest solution automatically chooses the background. If you get rid of the understand statement and make it very unlikely Inform will choose the cloak or the fork, Inform will finally ask you what you want to sit on. I’d hate to have to DTPM every object that might be picked to sit on.

What’s the behaviour that you’re trying to achieve?

The goal is for SIT to work the same as SIT ON [SOMETHING].

The simplest solution does that, but only because the Standard Rules has:

Rule for supplying a missing noun while entering (this is the find what to enter rule):
	if something enterable (called the box) is in the location,
		now the noun is the box;
	otherwise continue the activity.

That’s fine if there’s only one enterable thing in the room. If there’s more than one thing in the room you’d have to replace this rule with one that asks what you want to sit on.

Is a textbook case for Tads 3’s badness variable; In inform there’s only the boolean likely/unlikely, so I guess isn’t a bug, but an actual feature :wink:
the best handling I can figure is a table (or a case/select) giving the likely/unlikely state, the priority being established by the table or case/select order.

I don’t think is an helpful post, but anyway, HTAH (Hope That Actually Help) :slight_smile:

Best regards from Italy,
dott. Piergiorgio

As you’ve discovered, a “supplying a missing noun” rule only does anything if the action should apply to a noun but has a grammar line without a noun. I believe the “find what to enter” rule is there to cover the situation when the player types >IN.

I’m not sure if it covers all the subtleties you were after, but adding this line to your example seemed to fix at least the simple case:

Does the player mean entering a supporter: it is likely.
2 Likes

Likely/unlikely is actually implemented as a five-step continuum in Inform (it goes very likely/likely/possible/unlikely/very unlikely) so it is possible to manually set up a priority table like this, but it’s definitely a little clunky - so @jwalrus’s approach looks good to me unless there are further edge cases messing things up (in which case a few DTPM rules setting the exceptions to “very likely” should hopefully work).

2 Likes

There is an underappreciated debugging verb called >SHOWVERB, which can be used to see how the parser will try to interpret a particular verb in the player’s command.

>SHOWVERB SIT
Verb 'sit' 
	 * 'on' / 'in' / 'inside' noun -> Enter
	 * 'on' 'top' 'of' noun -> Enter

As you can see, the command verb “sit” maps to the entering action in all cases. However, by default via the Standard Rules, the parser won’t understand >SIT as a complete command; instead it is trying to complete the first grammar line using inference. That’s why the clarification includes the preposition “on”, which is the first in the chain of legal prepositions in the first grammar line for “sit”.

The Standard Rules includes the find what to enter rule, as you have seen. However, as jwalrus notes, in order for any rule for supplying a missing... to have an effect, a complete grammar line must be parsed that is missing a noun. That’s not happening here.

The simplest approach may be to add a grammar line for “sit”, with:

Understand "sit" as entering.

With that in place, you get:

>SHOWVERB SIT
Verb 'sit' 
	 * -> Enter
	 * 'on' / 'in' / 'inside' noun -> Enter
	 * 'on' 'top' 'of' noun -> Enter

>SIT
You get into the seating.

If you also want to handle the two-word style >SIT CHAIR, then you might prefer:

Understand the command "sit" as something new.
Understand the command "sit" as "enter".
[next two lines repeated from Standard Rules]
Understand "sit on top of [something]" as entering.
Understand "sit on/in/inside [something]" as entering.
3 Likes

If you didn’t know about the “supplying a missing noun” rules, you’d write:

Vague-sitting is an action applying to nothing.
Understand "sit" as vague-sitting.

Check vague-sitting:
	let O be an object;
	[set O to whatever object you like...]
	if O is nothing:
		instead say "There's nowhere to sit.";
	else:
		instead try entering O;

The only problem with this is that… well, that you don’t have to remember how the “supplying a missing noun” rules work. Maybe that’s not a problem.

2 Likes

This was my guess, which led to my discovery that Inform was calling NounDomain to pick a noun. NounDomain seems to limit itself to portable nouns, which is why DTPMing the cloak results in the fork being chosen. It’s only after NounDomain exhausts all the portable nouns that it finally gives up and asks the player what they want to sit on.

This solves the problem of Inform always picking portable things, but the rule for the missing noun picks the first enterable thing. If there’s more than one enterable thing, you’d have to right a new rule for the missing noun to ask the player what they want to sit on.

If I’m understanding you correctly, you want the command >SIT to result in a disambiguation question. These are asked only when the parser sees two or more objects that match the noun words used and have the same highest score. In this case, there are no noun words used, so no clarification question will be asked.

Jon Ingold wrote an extension called Disambiguation Control that is alert to this possibility and changes the behavior. Just by adding the “sit” grammar line and including the extension via

Include Disambiguation Control by Jon Ingold.

the response will be changed to:

>SIT
What do you want to sit on: the seating or the box?
2 Likes

Note that AFAIK, the latest version of Disambiguation Control is Version 10/170416, which works as stated above with 6M62, but will not compile under Inform 10.1.2.

I worked out something similar for this particular feature.

10.1.2-compatible
Definition: A thing is entrance-inviting if it is enterable and it is not enclosed by the player.

Does the player mean entering an entrance-inviting thing:
	it is likely. [a little broader than jwalrus's suggestion]

Asking what do you want to is an activity.

For asking what do you want to when best match count is at most six:
	say "What do you want to [parser command so far]: [disambiguation list]?"

Include (-

[ BestMatchScore      i best;
	best = NULL;
	for (i = 0: i < number_matched : i++ ) 
		if (match_scores-->i > best) {
			best = match_scores-->i;
		}
	return best;
];

[ MatchesAtScore s      i n ;
	for (i = 0, n = 0: i < number_matched : i++ ) 
		if (match_scores-->i == s)
			n++;
	return n;
];

[ PrintMatchListHighest     i j k marker bs nms ;
	bs = BestMatchScore();
	nms = MatchesAtScore(bs);
	j = number_of_classes; marker = 0;
	for (i = 1, j = nms : i <= number_of_classes : i++) {
		if (match_scores-->i == bs) {
			k = match_list-->i;
			if (match_classes-->i > 0) print (the) k; else print (a) k;

			j--;
			if (j > 1)  print ", ";
			if (j == 1) {
				#Ifdef SERIAL_COMMA;
				if (nms > 2) print ",";
				#Endif; ! SERIAL_COMMA
				PARSER_CLARIF_INTERNAL_RM('H');
			}
		}
	}
];

-).

To decide which number is best match count:
	(- (MatchesAtScore(BestMatchScore())) -).

To say disambiguation list: (- PrintMatchListHighest(); -).


Include (-

[ NounDomain domain1 domain2 context dont_ask
	first_word i j k l answer_words marker;
	#Ifdef DEBUG;
	if (parser_trace >= 4) {
	    print "   [NounDomain called at word ", wn,
	    	" (domain1 ", (name) domain1, ", domain2 ", (name) domain2, ")^";
	    print "   ";
	    if (indef_mode) {
	        print "seeking indefinite object: ";
	        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 "seeking definite object^";
	}
	#Endif; ! DEBUG

	match_length = 0; number_matched = 0; match_from = wn;

	SearchScope(domain1, domain2, context);

	#Ifdef DEBUG;
	if (parser_trace >= 4) print "   [ND made ", number_matched, " matches]^";
	#Endif; ! DEBUG

	wn = match_from+match_length;

	! If nothing worked at all, leave with the word marker skipped past the
	! first unmatched word...

	if (number_matched == 0) { wn++; rfalse; }

	! Suppose that there really were some words being parsed (i.e., we did
	! not just infer).  If so, and if there was only one match, it must be
	! right and we return it...

	if (match_from <= num_words) {
	    if (number_matched == 1) {
	        i=match_list-->0;
	        return i;
	    }

	    ! ...now suppose that there was more typing to come, i.e. suppose that
	    ! the user entered something beyond this noun.  If nothing ought to follow,
	    ! then there must be a mistake, (unless what does follow is just a full
	    ! stop, and or comma)

	    if (wn <= num_words) {
	        i = NextWord(); wn--;
	        if (i ~=  AND1__WD or AND2__WD or AND3__WD or comma_word
	               or THEN1__WD or THEN2__WD or THEN3__WD
	               or BUT1__WD or BUT2__WD or BUT3__WD) {
	            if (lookahead == ENDIT_TOKEN) rfalse;
	        }
	    }
	}

	! Now look for a good choice, if there's more than one choice...

	number_of_classes = 0;

	if (number_matched == 1) {
		i = match_list-->0;
		if (indef_mode == 1 && indef_type & PLURAL_BIT ~= 0) {
			if (context == MULTI_TOKEN or MULTIHELD_TOKEN or
				MULTIEXCEPT_TOKEN or MULTIINSIDE_TOKEN or
				NOUN_TOKEN or HELD_TOKEN or CREATURE_TOKEN) {
				BeginActivity(DECIDING_WHETHER_ALL_INC_ACT, i);
				if ((ForActivity(DECIDING_WHETHER_ALL_INC_ACT, i)) &&
					(RulebookFailed())) rfalse;
				EndActivity(DECIDING_WHETHER_ALL_INC_ACT, i);
			}
		}
	}
	if (number_matched > 1) {
		i = true;
		if (number_matched > 1)
			for (j=0 : j<number_matched-1 : j++)
				if (Identical(match_list-->j, match_list-->(j+1)) == false)
					i = false;
		if (i) dont_infer = true;
	    i = Adjudicate(context);
	    if (i == -1) rfalse;
	    if (i == 1) rtrue;   !  Adjudicate has made a multiple
	                         !  object, and we pass it on
	    dont_infer_pronoun = true; ! See bug I7-2115 for discussion of this
	}

	! If i is non-zero here, one of two things is happening: either
	! (a) an inference has been successfully made that object i is
	!     the intended one from the user's specification, or
	! (b) the user finished typing some time ago, but we've decided
	!     on i because it's the only possible choice.
	! In either case we have to keep the pattern up to date,
	! note that an inference has been made and return.
	! (Except, we don't note which of a pile of identical objects.)

	if (i ~= 0) {
		if (dont_infer) return i;
	    if (inferfrom == 0) inferfrom=pcount;
	    pattern-->pcount = i;
	    return i;
	}

	if (dont_ask) return match_list-->0;

	! If we get here, there was no obvious choice of object to make.  If in
	! fact we've already gone past the end of the player's typing (which
	! means the match list must contain every object in scope, regardless
	! of its name), then it's foolish to give an enormous list to choose
	! from - instead we go and ask a more suitable question...

	if (match_from > num_words) jump Incomplete;

	! Now we print up the question, using the equivalence classes as worked
	! out by Adjudicate() so as not to repeat ourselves on plural objects...

	BeginActivity(ASKING_WHICH_DO_YOU_MEAN_ACT);
	if (ForActivity(ASKING_WHICH_DO_YOU_MEAN_ACT)) jump SkipWhichQuestion;
	j = 1; marker = 0;
	for (i=1 : i<=number_of_classes : i++) {
		while (((match_classes-->marker) ~= i) && ((match_classes-->marker) ~= -i))
			marker++;
		if (match_list-->marker hasnt animate) j = 0;
	}
	if (j) PARSER_CLARIF_INTERNAL_RM('A');
	else PARSER_CLARIF_INTERNAL_RM('B');

	j = number_of_classes; marker = 0;
	for (i=1 : i<=number_of_classes : i++) {
	    while (((match_classes-->marker) ~= i) && ((match_classes-->marker) ~= -i)) marker++;
	    k = match_list-->marker;

	    if (match_classes-->marker > 0) print (the) k; else print (a) k;

	    if (i < j-1)  print ", ";
	    if (i == j-1) {
			if (KIT_CONFIGURATION_BITMAP & SERIAL_COMMA_TCBIT) {
				if (j ~= 2) print ",";
	    	}
	    	PARSER_CLARIF_INTERNAL_RM('H');
	    }
	}
	print "?^";

	.SkipWhichQuestion; EndActivity(ASKING_WHICH_DO_YOU_MEAN_ACT);

	! ...and get an answer:

  .WhichOne;
	#Ifdef TARGET_ZCODE;
	for (i=2 : i<INPUT_BUFFER_LEN : i++) buffer2->i = ' ';
	#Endif; ! TARGET_ZCODE
	answer_words=Keyboard(buffer2, parse2);

	! Conveniently, parse2-->1 is the first word in both ZCODE and GLULX.
	first_word = (parse2-->1);

	! Take care of "all", because that does something too clever here to do
	! later on:

	if (first_word == ALL1__WD or ALL2__WD or ALL3__WD or ALL4__WD or ALL5__WD) {
	    if (context == MULTI_TOKEN or MULTIHELD_TOKEN or MULTIEXCEPT_TOKEN or MULTIINSIDE_TOKEN) {
	        l = multiple_object-->0;
	        for (i=0 : i<number_matched && l+i<MATCH_LIST_WORDS : i++) {
	            k = match_list-->i;
	            multiple_object-->(i+1+l) = k;
	        }
	        multiple_object-->0 = i+l;
	        rtrue;
	    }
	    PARSER_CLARIF_INTERNAL_RM('C');
	    jump WhichOne;
	}

	! Look for a comma, and interpret this as a fresh conversation command
	! if so:

	for (i=1 : i<=answer_words : i++)
		if (WordFrom(i, parse2) == comma_word) {
	        VM_CopyBuffer(buffer, buffer2);
	        jump RECONSTRUCT_INPUT;
		}

	! If the first word of the reply can be interpreted as a verb, then
	! assume that the player has ignored the question and given a new
	! command altogether.
	! (This is one time when it's convenient that the directions are
	! not themselves verbs - thus, "north" as a reply to "Which, the north
	! or south door" is not treated as a fresh command but as an answer.)

	if (first_word == 0) {
	    j = wn; first_word = LanguageIsVerb(buffer2, parse2, 1); wn = j;
	}
	if (first_word ~= 0) {
	    j = first_word->#dict_par1;
	    if ((0 ~= j&1) && ~~LanguageVerbMayBeName(first_word)) {
	        VM_CopyBuffer(buffer, buffer2);
	        jump RECONSTRUCT_INPUT;
	    }
	}

	! Now we insert the answer into the original typed command, as
	! words additionally describing the same object
	! (eg, > take red button
	!      Which one, ...
	!      > music
	! becomes "take music red button".  The parser will thus have three
	! words to work from next time, not two.)

	#Ifdef TARGET_ZCODE;
	k = WordAddress(match_from) - buffer; l=buffer2->1+1;
	for (j=buffer + buffer->0 - 1 : j>=buffer+k+l : j-- ) j->0 = 0->(j-l);
	for (i=0 : i<l : i++) buffer->(k+i) = buffer2->(2+i);
	buffer->(k+l-1) = ' ';
	buffer->1 = buffer->1 + l;
	if (buffer->1 >= (buffer->0 - 1)) buffer->1 = buffer->0;
	#Ifnot; ! TARGET_GLULX
	k = WordAddress(match_from) - buffer;
	l = (buffer2-->0) + 1;
	for (j=buffer+INPUT_BUFFER_LEN-1 : j>=buffer+k+l : j-- ) j->0 = j->(-l);
	for (i=0 : i<l : i++) buffer->(k+i) = buffer2->(WORDSIZE+i);
	buffer->(k+l-1) = ' ';
	buffer-->0 = buffer-->0 + l;
	if (buffer-->0 > (INPUT_BUFFER_LEN-WORDSIZE)) buffer-->0 = (INPUT_BUFFER_LEN-WORDSIZE);
	#Endif; ! TARGET_

	! Having reconstructed the input, we warn the parser accordingly
	! and get out.

	.RECONSTRUCT_INPUT;

	num_words = WordCount(); players_command = 100 + num_words;
	wn = 1;
	#Ifdef LanguageToInformese;
	LanguageToInformese();
	! Re-tokenise:
	VM_Tokenise(buffer,parse);
	#Endif; ! LanguageToInformese
	num_words = WordCount(); players_command = 100 + num_words;
	actors_location = ScopeCeiling(player);
	FollowRulebook(Activity_after_rulebooks-->READING_A_COMMAND_ACT);

	return REPARSE_CODE;

	! Now we come to the question asked when the input has run out
	! and can't easily be guessed (eg, the player typed "take" and there
	! were plenty of things which might have been meant).

  .Incomplete;

	! BEGIN MODIFICATION
	BeginActivity( (+ asking what do you want to +) );
	if (ForActivity( (+ asking what do you want to +) ) == 0) {
		if (context == CREATURE_TOKEN) PARSER_CLARIF_INTERNAL_RM('D', actor);
		else                           PARSER_CLARIF_INTERNAL_RM('E', actor);
		new_line;
	}
	EndActivity( (+ asking what do you want to +) );
	! END MODIFICATION

	#Ifdef TARGET_ZCODE;
	for (i=2 : i<INPUT_BUFFER_LEN : i++) buffer2->i=' ';
	#Endif; ! TARGET_ZCODE
	answer_words = Keyboard(buffer2, parse2);

	! Look for a comma, and interpret this as a fresh conversation command
	! if so:

	for (i=1 : i<=answer_words : i++)
		if (WordFrom(i, parse2) == comma_word) {
			VM_CopyBuffer(buffer, buffer2);
			jump RECONSTRUCT_INPUT;
		}

	first_word=(parse2-->1);
	if (first_word==0) {
	    j = wn; first_word=LanguageIsVerb(buffer2, parse2, 1); wn = j;
	}

	! Once again, if the reply looks like a command, give it to the
	! parser to get on with and forget about the question...

	if (first_word ~= 0) {
	    j = first_word->#dict_par1;
	    if ((0 ~= j&1) && ~~LanguageVerbMayBeName(first_word)) {
	        VM_CopyBuffer(buffer, buffer2);
	        jump RECONSTRUCT_INPUT;
	    }
	}

	! ...but if we have a genuine answer, then:
	!
	! (1) we must glue in text suitable for anything that's been inferred.

	if (inferfrom ~= 0) {
	    for (j=inferfrom : j<pcount : j++) {
	        if (pattern-->j == PATTERN_NULL) continue;
	        #Ifdef TARGET_ZCODE;
	        i = 2+buffer->1; (buffer->1)++; buffer->(i++) = ' ';
	        #Ifnot; ! TARGET_GLULX
	        i = WORDSIZE + buffer-->0;
	        (buffer-->0)++; buffer->(i++) = ' ';
	        #Endif; ! TARGET_

	        #Ifdef DEBUG;
	        if (parser_trace >= 5)
	        	print "[Gluing in inference at ", j, " with pattern code ", pattern-->j, "]^";
	        #Endif; ! DEBUG

	        ! Conveniently, parse2-->1 is the first word in both ZCODE and GLULX.

	        parse2-->1 = 0;

	        ! An inferred object.  Best we can do is glue in a pronoun.
	        ! (This is imperfect, but it's very seldom needed anyway.)

	        if (pattern-->j >= 2 && pattern-->j < REPARSE_CODE) {
	        	if (dont_infer_pronoun == false) {
					PronounNotice(pattern-->j);
					for (k=1 : k<=LanguagePronouns-->0 : k=k+3)
						if (pattern-->j == LanguagePronouns-->(k+2)) {
							parse2-->1 = LanguagePronouns-->k;
							#Ifdef DEBUG;
							if (parser_trace >= 5)
								print "[Using pronoun '", (address) parse2-->1, "']^";
							#Endif; ! DEBUG
							break;
						}
				}
	        }
	        else {
	            ! An inferred preposition.
	            parse2-->1 = VM_NumberToDictionaryAddress(pattern-->j - REPARSE_CODE);
	            #Ifdef DEBUG;
	            if (parser_trace >= 5)
	            	print "[Using preposition '", (address) parse2-->1, "']^";
	            #Endif; ! DEBUG
	        }

	        ! parse2-->1 now holds the dictionary address of the word to glue in.

	        if (parse2-->1 ~= 0) {
	            k = buffer + i;
	            #Ifdef TARGET_ZCODE;
	            @output_stream 3 k;
	             print (address) parse2-->1;
	            @output_stream -3;
	            k = k-->0;
	            for (l=i : l<i+k : l++) buffer->l = buffer->(l+2);
	            i = i + k; buffer->1 = i-2;
	            #Ifnot; ! TARGET_GLULX
	            k = Glulx_PrintAnyToArray(buffer+i, INPUT_BUFFER_LEN-i, parse2-->1);
	            i = i + k; buffer-->0 = i - WORDSIZE;
	            #Endif; ! TARGET_
	        }
	    }
	}

	! (2) we must glue the newly-typed text onto the end.

	#Ifdef TARGET_ZCODE;
	i = 2+buffer->1; (buffer->1)++; buffer->(i++) = ' ';
	for (j=0 : j<buffer2->1 : i++,j++) {
	    buffer->i = buffer2->(j+2);
	    (buffer->1)++;
	    if (buffer->1 == INPUT_BUFFER_LEN) break;
	}
	#Ifnot; ! TARGET_GLULX
	i = WORDSIZE + buffer-->0;
	(buffer-->0)++; buffer->(i++) = ' ';
	for (j=0 : j<buffer2-->0 : i++,j++) {
	    buffer->i = buffer2->(j+WORDSIZE);
	    (buffer-->0)++;
	    if (buffer-->0 == INPUT_BUFFER_LEN) break;
	}
	#Endif; ! TARGET_

	! (3) we fill up the buffer with spaces, which is unnecessary, but may
	!     help incorrectly-written interpreters to cope.

	#Ifdef TARGET_ZCODE;
	for (: i<INPUT_BUFFER_LEN : i++) buffer->i = ' ';
	#Endif; ! TARGET_ZCODE

	jump RECONSTRUCT_INPUT;

]; ! end of NounDomain

-) replacing "NounDomain";

otistdog’s code does what I wanted, so I’m marking it as the solution. The less optimal solution I came up with doesn’t require re-writing NounDomain (way too complex for a training exercise), or using Disambiguation Control (compiling in 10.1.2).

Understand "sit" as entering.

Rule for supplying a missing noun while entering (this is the revised find what to enter rule):
	if the number of enterable things in the location is 1:
		now the noun is a random enterable thing in the location;
	otherwise if the number of enterable things in the location is not 0:
		let liosta be the list of enterable things in the location;
		instead say "You can sit on [liosta with definite articles].";
	otherwise:
		continue the activity.

The revised find what to enter rule is listed instead of the find what to enter rule in the for supplying a missing noun rulebook.

This does not trigger a disambiguation question. It just tells the player what can be sat upon, which is why I didn’t phrase the response as a question.