!% -D ! MASS NOUNS -- an interactive demonstration ! Written by Andrew Plotkin. Most of this source file is public domain. ! Portions (the functions NounDomain, Parser__parse, Descriptors, ! Indefart, CInDefArt) are derived from the Inform library (version 6/11), ! with minor changes, and are copyright 2004 Graham Nelson. ! This source was developed with Inform 6.3, library 6/11. It has not ! been tested with any earlier or later versions. ! For an overview of this demonstration, why it exists, and what it ! does, see the placards in the game. (Their text is at the end of this ! file.) My comments are intended to explain how I accomplish this stuff ! technically. ! My plan was to create a completely general library. Properties like ! color, weight, purity should be added to the game by the game author, ! not hardwired into the library. My plan was about halfway successful... ! only *some* of the property system wound up hardwired. Constant Story "MASS NOUNS"; Release 6; Constant Headline "^A Demonstration by Andrew Plotkin.^ [First-time players should read the placard.]^"; Constant MANUAL_PRONOUNS; ! ---- ! Library declarations Attribute quantarticles; Attribute ordinarticles; Attribute ao_handling; Attribute ao_handled; Attribute ao_keepval; Object DeclareProperties with base_name, ordinal, visibly_distinct, comp_select, comp_opposite, nop_specify, check_extract, color, purity, weight, quantity, quantunit; Replace OffersLight; Replace NounDomain; Replace Parser__parse; Replace Indefart; Replace CInDefArt; Replace Descriptors; Include "Parser"; Include "VerbLib"; ! ---- ! Generic utilities and entry points ! SelSort: Sort the array arr (of len words) using func(a,b), which must ! return a value <, =, or > zero for every pair of objects. This is ! a selection sort, which takes N^2 time. So what. [ SelSort arr len func ix jx val; for (ix=len : ix>1 : ix--) { for (jx=0 : jxjx, arr-->(jx+1)); if (val < 0) { val = arr-->jx; arr-->jx = arr-->(jx+1); arr-->(jx+1) = val; } } } ]; ! nameid: A print token. print (nameid) obj; is handy in debugging messages. [ nameid obj; print (name) obj, " (#", obj, ")"; ]; ! ListTogetherCount ! If a list_together method wants to print "three onions..." it must ! count the onions first, by iterating down NextEntry. This utility ! function does that job. ! If the optional arguments (filter, arg) are provided, we only ! count objects o for which filter(o, arg) returns true. ! Note that, confusingly, the parser global lt_value is *not* set when ! list_together is called. (This is the global that NextEntry uses to ! filter its values to the listing-together class.) We must set it, and ! then restore it. (That was a fun bug to track down.) [ ListTogetherCount filter arg obj count tmp_lt_value; tmp_lt_value = lt_value; lt_value = parser_one.list_together; obj = parser_one; count = 0; while (obj) { if ((~~filter) || filter(obj, arg)) count++; obj = NextEntry(obj, parser_two); } lt_value = tmp_lt_value; return count; ]; [ ListTogetherOrdinals total filter arg obj count tmp_lt_value; tmp_lt_value = lt_value; lt_value = parser_one.list_together; obj = parser_one; count = 0; while (obj) { if ((~~filter) || filter(obj, arg)) { if (obj.ordinal) { if (count > 0) { if (total <= 2) print " and "; else { print ", "; if (count == total-1) print "and "; } } print "the "; OrdinalNumber(obj.ordinal); count++; } } obj = NextEntry(obj, parser_two); } if (count < total) { if (count == 0) print "which are identical"; else print ", plus ", (EnglishNumber) (total-count), " others"; } lt_value = tmp_lt_value; ]; ! We want to assign ordinals to identical objects before every command is ! parsed. We also want to do it when entering a new room, so that the ! player can see ordinals on his first move. ! (Actually, we'd *like* to assign an ordinal to an object as soon as it ! enters scope -- if necessary. But that isn't practical.) [ BeforeParsing; ApplyOrdinals(); ]; [ NewRoom; ApplyOrdinals(); ]; ! Accepting phrases like "take object on table" is only helpful if the ! player knows where objects *are*. And if the player accidentally refers ! to the wrong object, it would be helpful if he discovered his mistake ! as soon as possible. ! Therefore, we prepend "(the object, which is on the table)" to object ! descriptions. If they're in/on anything, I mean. ! Ideally, this would be integrated with the library code that prints ! "(the object)" when disambiguating. That way, the player wouldn't ! get both messages. For now, we do it the stupid way. [ GamePreRoutine par; Examine: if (noun provides ordinal) { par = parent(noun); if (par == location) rfalse; print "(", (the) noun, ", which"; if (par == player) { print " you are holding)^"; rfalse; } if (par has container) { print " is in ", (the) par, ")^"; rfalse; } if (par has supporter) { print " is on ", (the) par, ")^"; rfalse; } print " belongs to ", (the) par, ")^"; rfalse; } rfalse; default: rfalse; ]; ! AboutSub: Action routine for "about" (same as the initial placard). [ AboutSub; InitialMessage(); ]; ! AboutBugsSub: Action routine for "bugs" [ AboutBugsSub; BugsMessage(); ]; ! ---- ! Scope utilities ! When sorting out the ordinals, it is very useful to create a simple ! array of the objects in scope. (We sort the array, break it into ! parts, and iterate over the parts. So we can't just use the ! LoopOverScope iterator directly.) ! ! Obviously, this gives us a fixed-size limit to the number of objects ! which can be in scope. I am not worrying about this. Constant MAX_OBJ_IN_SCOPE 48; Array scope_list --> MAX_OBJ_IN_SCOPE; Global scope_count = 0; Global scope_filter = 0; ! CreateScopeList: Fill the scope_list array with a list of the objects ! in scope. If filter is provided, only list objects for which filter(o) ! is true. [ CreateScopeList filter; scope_count = 0; scope_filter = filter; LoopOverScope(AddToScopeList); ]; ! AddToScopeList: Utility used by CreateScopeList. [ AddToScopeList obj; if (scope_filter && (~~scope_filter(obj))) return; if (scope_count >= MAX_OBJ_IN_SCOPE) "[BUG] Too many objects in scope! MAX_OBJ_IN_SCOPE is ", MAX_OBJ_IN_SCOPE, "."; scope_list-->scope_count = obj; scope_count++; ]; ! DumpScopeList: Print the contents of the scope_list. (For debugging.) [ DumpScopeList ix obj; print scope_count, " objects found in scope:^"; for (ix=0 : ixix; print " ", (nameid) obj, "^"; } ]; ! ---- ! Replaced standard library functions ! OffersLight: Simple light function which says everything is lit. [ OffersLight i; if (i == 0) rfalse; rtrue; ]; ! NounDomain: (replaced with tiny modifications, see "--Z" comments) ! We handle comparatives ("largest", "muddiest", etc) by accumulating ! a list of them during parse_name. After every object in scope has ! been checked against the player's input, we refine the list of ! objects (match_list) by applying each comparative in turn. To do ! this, we must initialize the comparative list at the beginning of ! NounDomain, and then call ApplyComparatives in the middle. ! There's one tiny additional hack: I reset the global number_of_classes ! to zero after a disambiguation question is printed. This allows me to ! cleverly use number_of_classes as a test in a short_name procedure, ! to distinguish *whether* the name is being printed in a disambiguation ! question or not. [ NounDomain domain1 domain2 context first_word i j k l answer_words marker; #Ifdef DEBUG; if (parser_trace >= 4) { print " [NounDomain called at word ", wn, "^"; 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 == 100) 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; placed_in_flag = 0; SetupComparatives(); ! --Z 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; } } } ! If we have comparative descriptors ("largest", etc) then pare ! the list down. --Z ApplyComparatives(); if (number_matched == 0) { wn++; 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 (number_matched > 1) { i = Adjudicate(context); if (i == -1) rfalse; if (i == 1) rtrue; ! Adjudicate has made a multiple ! object, and we pass it on } ! 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 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... if (context==CREATURE_TOKEN) L__M(##Miscellany, 45); else L__M(##Miscellany, 46); 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 (string) COMMA__TX; if (i == j-1) print (string) OR__TX; } L__M(##Miscellany, 57); number_of_classes = 0; ! clear this again ! --Z ! ...and get an answer: .WhichOne; #Ifdef TARGET_ZCODE; for (i=2 : ii = ' '; #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 : ii; multiple_object-->(i+1+l) = k; } multiple_object-->0 = i+l; rtrue; } L__M(##Miscellany, 47); jump WhichOne; } ! 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.) #Ifdef LanguageIsVerb; if (first_word == 0) { j = wn; first_word = LanguageIsVerb(buffer2, parse2, 1); wn = j; } #Endif; ! LanguageIsVerb if (first_word ~= 0) { j = first_word->#dict_par1; if ((0 ~= j&1) && ~~LanguageVerbMayBeName(first_word)) { CopyBuffer(buffer, buffer2); return REPARSE_CODE; } } ! 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(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(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. 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; if (context == CREATURE_TOKEN) L__M(##Miscellany, 48); else L__M(##Miscellany, 49); #Ifdef TARGET_ZCODE; for (i=2 : ii=' '; #Endif; ! TARGET_ZCODE answer_words = Keyboard(buffer2, parse2); first_word=(parse2-->1); #Ifdef LanguageIsVerb; if (first_word==0) { j = wn; first_word=LanguageIsVerb(buffer2, parse2, 1); wn = j; } #Endif; ! LanguageIsVerb ! 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) { CopyBuffer(buffer, buffer2); return REPARSE_CODE; } } ! ...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 : jj == 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 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) { 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 = No__Dword(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 : ll = buffer->(l+2); i = i + k; buffer->1 = i-2; #Ifnot; ! TARGET_GLULX k = 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 : j1 : 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 : j0 : 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 (: ii = ' '; #Endif; ! TARGET_ZCODE return REPARSE_CODE; ]; ! end of NounDomain ! Descriptors: (replaced with tiny modifications, see "--Z" comments) ! The standard Inform parser accepts a phrase like "four oranges", and ! parses it correctly into a multiple object of four items (if possible). ! Unfortunately, this prevents me from parsing "four ounces of orange ! juice" as a single object's name. So, I ripped it out. ! I don't have a clever idea for making my code compatible with the ! standard (un-broken) library. I guess we could give certain objects ! a "numbers are part of my name, honest" property. But this still leaves ! a mess; the parser has to remember two descriptor states, with the numbers ! pre-parsed and not-pre-parsed. Sorry, haven't figured this out yet. [ Descriptors o x flag cto type; ResetDescriptors(); if (wn > num_words) return 0; for (flag=true : flag :) { o = NextWordStopped(); flag = false; for (x=1 : x<=LanguageDescriptors-->0 : x=x+4) if (o == LanguageDescriptors-->x) { flag = true; type = LanguageDescriptors-->(x+2); if (type ~= DEFART_PK) indef_mode = true; indef_possambig = true; indef_cases = indef_cases & (LanguageDescriptors-->(x+1)); if (type == POSSESS_PK) { cto = LanguageDescriptors-->(x+3); switch (cto) { 0: indef_type = indef_type | MY_BIT; 1: indef_type = indef_type | THAT_BIT; default: indef_owner = PronounValue(cto); if (indef_owner == NULL) indef_owner = InformParser; } } if (type == light) indef_type = indef_type | LIT_BIT; if (type == -light) indef_type = indef_type | UNLIT_BIT; } if (o == OTHER1__WD or OTHER2__WD or OTHER3__WD) { indef_mode = 1; flag = 1; indef_type = indef_type | OTHER_BIT; } if (o == ALL1__WD or ALL2__WD or ALL3__WD or ALL4__WD or ALL5__WD) { indef_mode = 1; flag = 1; indef_wanted = 100; if (take_all_rule == 1) take_all_rule = 2; indef_type = indef_type | PLURAL_BIT; } ! I have deleted the TryNumber clause, because it seriously ! interferes with my weight- and quantity-parsing. --Z if (flag == 1 && NextWordStopped() ~= OF1__WD or OF2__WD or OF3__WD or OF4__WD) wn--; ! Skip 'of' after these } wn--; return 0; ]; ! Parser__parse: (replaced with tiny modifications, see "--Z" comments) ! For reasons that I will be happy to explain later, parsing gets ! horribly screwed up if 'of' is a preposition. (Okay: the ! "trying look-ahead" phase for multiexcept/multiinside tokens gets ! snagged on the "of" in the phrase "three ounces of clay". It really ! should be looking only for the prepositions in the current grammar ! line!) ! ! Rather than fix this, I have gone for the easy hack: ignore the ! preposition-ness of 'of'. This doesn't affect anything else, ! because multiexcept/multiinside never appears in a grammar line ! with 'of'. [ Parser__parse results syntax line num_lines line_address i j k token l m; !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! ! A: Get the input, do "oops" and "again" ! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! Firstly, in "not held" mode, we still have a command left over from last ! time (eg, the user typed "eat biscuit", which was parsed as "take biscuit" ! last time, with "eat biscuit" tucked away until now). So we return that. if (notheld_mode == 1) { for (i=0 : i<8 : i++) results-->i = kept_results-->i; notheld_mode = 0; rtrue; } if (held_back_mode == 1) { held_back_mode = 0; Tokenise__(buffer, parse); jump ReParse; } .ReType; Keyboard(buffer,parse); .ReParse; parser_inflection = name; ! Initially assume the command is aimed at the player, and the verb ! is the first word #Ifdef TARGET_ZCODE; num_words = parse->1; #Ifnot; ! TARGET_GLULX num_words = parse-->0; #Endif; ! TARGET_ wn = 1; #Ifdef LanguageToInformese; LanguageToInformese(); #IfV5; ! Re-tokenise: Tokenise__(buffer,parse); #Endif; ! V5 #Endif; ! LanguageToInformese BeforeParsing(); #Ifdef TARGET_ZCODE; num_words = parse->1; #Ifnot; ! TARGET_GLULX num_words = parse-->0; #Endif; ! TARGET_ k=0; #Ifdef DEBUG; if (parser_trace >= 2) { print "[ "; for (i=0 : i(i*2 + 1); #Ifnot; ! TARGET_GLULX j = parse-->(i*3 + 1); #Endif; ! TARGET_ k = WordAddress(i+1); l = WordLength(i+1); print "~"; for (m=0 : mm; print "~ "; if (j == 0) print "?"; else { #Ifdef TARGET_ZCODE; if (UnsignedCompare(j, HDR_DICTIONARY-->0) >= 0 && UnsignedCompare(j, HDR_HIGHMEMORY-->0) < 0) print (address) j; else print j; #Ifnot; ! TARGET_GLULX if (j->0 == $60) print (address) j; else print j; #Endif; ! TARGET_ } if (i ~= num_words-1) print " / "; } print " ]^"; } #Endif; ! DEBUG verb_wordnum = 1; actor = player; actors_location = ScopeCeiling(player); usual_grammar_after = 0; .AlmostReParse; scope_token = 0; action_to_be = NULL; ! Begin from what we currently think is the verb word .BeginCommand; wn = verb_wordnum; verb_word = NextWordStopped(); ! If there's no input here, we must have something like "person,". if (verb_word == -1) { best_etype = STUCK_PE; jump GiveError; } ! Now try for "again" or "g", which are special cases: don't allow "again" if nothing ! has previously been typed; simply copy the previous text across if (verb_word == AGAIN2__WD or AGAIN3__WD) verb_word = AGAIN1__WD; if (verb_word == AGAIN1__WD) { if (actor ~= player) { L__M(##Miscellany, 20); jump ReType; } #Ifdef TARGET_ZCODE; if (buffer3->1 == 0) { L__M(##Miscellany, 21); jump ReType; } #Ifnot; ! TARGET_GLULX if (buffer3-->0 == 0) { L__M(##Miscellany, 21); jump ReType; } #Endif; ! TARGET_ for (i=0 : ii = buffer3->i; jump ReParse; } ! Save the present input in case of an "again" next time if (verb_word ~= AGAIN1__WD) for (i=0 : ii = buffer->i; if (usual_grammar_after == 0) { j = verb_wordnum; i = RunRoutines(actor, grammar); #Ifdef DEBUG; if (parser_trace >= 2 && actor.grammar ~= 0 or NULL) print " [Grammar property returned ", i, "]^"; #Endif; ! DEBUG #Ifdef TARGET_ZCODE; if ((i ~= 0 or 1) && (UnsignedCompare(i, dict_start) < 0 || UnsignedCompare(i, dict_end) >= 0 || (i - dict_start) % dict_entry_size ~= 0)) { usual_grammar_after = j; i=-i; } #Ifnot; ! TARGET_GLULX if (i < 0) { usual_grammar_after = verb_wordnum; i=-i; } #Endif; if (i == 1) { results-->0 = action; results-->1 = noun; results-->2 = second; rtrue; } if (i ~= 0) { verb_word = i; wn--; verb_wordnum--; } else { wn = verb_wordnum; verb_word = NextWord(); } } else usual_grammar_after = 0; !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! ! B: Is it a direction, and so an implicit "go"? If so go to (K) ! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! #Ifdef LanguageIsVerb; if (verb_word == 0) { i = wn; verb_word = LanguageIsVerb(buffer, parse, verb_wordnum); wn = i; } #Endif; ! LanguageIsVerb ! If the first word is not listed as a verb, it must be a direction ! or the name of someone to talk to if (verb_word == 0 || ((verb_word->#dict_par1) & 1) == 0) { ! So is the first word an object contained in the special object "compass" ! (i.e., a direction)? This needs use of NounDomain, a routine which ! does the object matching, returning the object number, or 0 if none found, ! or REPARSE_CODE if it has restructured the parse table so the whole parse ! must be begun again... wn = verb_wordnum; indef_mode = false; token_filter = 0; l = NounDomain(compass, 0, 0); if (l == REPARSE_CODE) jump ReParse; ! If it is a direction, send back the results: ! action=GoSub, no of arguments=1, argument 1=the direction. if (l ~= 0) { results-->0 = ##Go; action_to_be = ##Go; results-->1 = 1; results-->2 = l; jump LookForMore; } !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! ! C: Is anyone being addressed? ! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! Only check for a comma (a "someone, do something" command) if we are ! not already in the middle of one. (This simplification stops us from ! worrying about "robot, wizard, you are an idiot", telling the robot to ! tell the wizard that she is an idiot.) if (actor == player) { for (j=2 : j<=num_words : j++) { i=NextWord(); if (i == comma_word) jump Conversation; } verb_word = UnknownVerb(verb_word); if (verb_word ~= 0) jump VerbAccepted; } best_etype = VERB_PE; jump GiveError; ! NextWord nudges the word number wn on by one each time, so we've now ! advanced past a comma. (A comma is a word all on its own in the table.) .Conversation; j = wn - 1; if (j == 1) { L__M(##Miscellany, 22); jump ReType; } ! Use NounDomain (in the context of "animate creature") to see if the ! words make sense as the name of someone held or nearby wn = 1; lookahead = HELD_TOKEN; scope_reason = TALKING_REASON; l = NounDomain(player,actors_location,6); scope_reason = PARSING_REASON; if (l == REPARSE_CODE) jump ReParse; if (l == 0) { L__M(##Miscellany, 23); jump ReType; } .Conversation2; ! The object addressed must at least be "talkable" if not actually "animate" ! (the distinction allows, for instance, a microphone to be spoken to, ! without the parser thinking that the microphone is human). if (l hasnt animate && l hasnt talkable) { L__M(##Miscellany, 24, l); jump ReType; } ! Check that there aren't any mystery words between the end of the person's ! name and the comma (eg, throw out "dwarf sdfgsdgs, go north"). if (wn ~= j) { L__M(##Miscellany, 25); jump ReType; } ! The player has now successfully named someone. Adjust "him", "her", "it": PronounNotice(l); ! Set the global variable "actor", adjust the number of the first word, ! and begin parsing again from there. verb_wordnum = j + 1; ! Stop things like "me, again": if (l == player) { wn = verb_wordnum; if (NextWordStopped() == AGAIN1__WD or AGAIN2__WD or AGAIN3__WD) { L__M(##Miscellany, 20); jump ReType; } } actor = l; actors_location = ScopeCeiling(l); #Ifdef DEBUG; if (parser_trace >= 1) print "[Actor is ", (the) actor, " in ", (name) actors_location, "]^"; #Endif; ! DEBUG jump BeginCommand; } ! end of first-word-not-a-verb !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! ! D: Get the verb: try all the syntax lines for that verb ! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .VerbAccepted; ! We now definitely have a verb, not a direction, whether we got here by the ! "take ..." or "person, take ..." method. Get the meta flag for this verb: meta = ((verb_word->#dict_par1) & 2)/2; ! You can't order other people to "full score" for you, and so on... if (meta == 1 && actor ~= player) { best_etype = VERB_PE; meta = 0; jump GiveError; } ! Now let i be the corresponding verb number, stored in the dictionary entry ! (in a peculiar 255-n fashion for traditional Infocom reasons)... i = $ff-(verb_word->#dict_par2); ! ...then look up the i-th entry in the verb table, whose address is at word ! 7 in the Z-machine (in the header), so as to get the address of the syntax ! table for the given verb... #Ifdef TARGET_ZCODE; syntax = (HDR_STATICMEMORY-->0)-->i; #Ifnot; ! TARGET_GLULX syntax = (#grammar_table)-->(i+1); #Endif; ! TARGET_ ! ...and then see how many lines (ie, different patterns corresponding to the ! same verb) are stored in the parse table... num_lines = (syntax->0) - 1; ! ...and now go through them all, one by one. ! To prevent pronoun_word 0 being misunderstood, pronoun_word = NULL; pronoun_obj = NULL; #Ifdef DEBUG; if (parser_trace >= 1) print "[Parsing for the verb '", (address) verb_word, "' (", num_lines+1, " lines)]^"; #Endif; ! DEBUG best_etype = STUCK_PE; nextbest_etype = STUCK_PE; multiflag = false; ! "best_etype" is the current failure-to-match error - it is by default ! the least informative one, "don't understand that sentence". ! "nextbest_etype" remembers the best alternative to having to ask a ! scope token for an error message (i.e., the best not counting ASKSCOPE_PE). ! multiflag is used here to prevent inappropriate MULTI_PE errors ! in addition to its unrelated duties passing information to action routines !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! ! E: Break down a syntax line into analysed tokens ! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! line_address = syntax + 1; for (line=0 : line<=num_lines : line++) { for (i=0 : i<32 : i++) { line_token-->i = ENDIT_TOKEN; line_ttype-->i = ELEMENTARY_TT; line_tdata-->i = ENDIT_TOKEN; } ! Unpack the syntax line from Inform format into three arrays; ensure that ! the sequence of tokens ends in an ENDIT_TOKEN. line_address = UnpackGrammarLine(line_address); #Ifdef DEBUG; if (parser_trace >= 1) { if (parser_trace >= 2) new_line; print "[line ", line; DebugGrammarLine(); print "]^"; } #Endif; ! DEBUG ! We aren't in "not holding" or inferring modes, and haven't entered ! any parameters on the line yet, or any special numbers; the multiple ! object is still empty. not_holding = 0; inferfrom = 0; parameters = 0; nsns = 0; special_word = 0; special_number = 0; multiple_object-->0 = 0; multi_context = 0; etype = STUCK_PE; ! Put the word marker back to just after the verb wn = verb_wordnum+1; !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! ! F: Look ahead for advance warning for multiexcept/multiinside ! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! There are two special cases where parsing a token now has to be ! affected by the result of parsing another token later, and these ! two cases (multiexcept and multiinside tokens) are helped by a quick ! look ahead, to work out the future token now. We can only carry this ! out in the simple (but by far the most common) case: ! ! multiexcept noun ! ! and similarly for multiinside. advance_warning = NULL; indef_mode = false; for (i=0,m=false,pcount=0 : line_token-->pcount ~= ENDIT_TOKEN : pcount++) { scope_token = 0; if (line_ttype-->pcount ~= PREPOSITION_TT) i++; if (line_ttype-->pcount == ELEMENTARY_TT) { if (line_tdata-->pcount == MULTI_TOKEN) m = true; if (line_tdata-->pcount == MULTIEXCEPT_TOKEN or MULTIINSIDE_TOKEN && i == 1) { ! First non-preposition is "multiexcept" or ! "multiinside", so look ahead. #Ifdef DEBUG; if (parser_trace >= 2) print " [Trying look-ahead]^"; #Endif; ! DEBUG ! We need this to be followed by 1 or more prepositions. pcount++; if (line_ttype-->pcount == PREPOSITION_TT) { while (line_ttype-->pcount == PREPOSITION_TT) pcount++; if ((line_ttype-->pcount == ELEMENTARY_TT) && (line_tdata-->pcount == NOUN_TOKEN)) { ! Advance past the last preposition while (wn < num_words) { l=NextWord(); if ( l && ((l->#dict_par1) &8) && l ~= 'of' ) { ! if preposition (but not 'of' --Z) l = Descriptors(); ! skip past THE etc if (l~=0) etype=l; ! don't allow multiple objects l = NounDomain(actors_location, actor, NOUN_TOKEN); #Ifdef DEBUG; if (parser_trace >= 2) { print " [Advanced to ~noun~ token: "; if (l == REPARSE_CODE) print "re-parse request]^"; if (l == 1) print "but multiple found]^"; if (l == 0) print "error ", etype, "]^"; if (l >= 2) print (the) l, "]^"; } #Endif; ! DEBUG if (l == REPARSE_CODE) jump ReParse; if (l >= 2) advance_warning = l; } } } } break; } } } ! Slightly different line-parsing rules will apply to "take multi", to ! prevent "take all" behaving correctly but misleadingly when there's ! nothing to take. take_all_rule = 0; if (m && params_wanted == 1 && action_to_be == ##Take) take_all_rule = 1; ! And now start again, properly, forearmed or not as the case may be. ! As a precaution, we clear all the variables again (they may have been ! disturbed by the call to NounDomain, which may have called outside ! code, which may have done anything!). not_holding = 0; inferfrom = 0; parameters = 0; nsns = 0; special_word = 0; special_number = 0; multiple_object-->0 = 0; etype = STUCK_PE; wn = verb_wordnum+1; !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! ! G: Parse each token in turn (calling ParseToken to do most of the work) ! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! "Pattern" gradually accumulates what has been recognised so far, ! so that it may be reprinted by the parser later on for (pcount=1 : : pcount++) { pattern-->pcount = PATTERN_NULL; scope_token = 0; token = line_token-->(pcount-1); lookahead = line_token-->pcount; #Ifdef DEBUG; if (parser_trace >= 2) print " [line ", line, " token ", pcount, " word ", wn, " : ", (DebugToken) token, "]^"; #Endif; ! DEBUG if (token ~= ENDIT_TOKEN) { scope_reason = PARSING_REASON; parser_inflection = name; AnalyseToken(token); if (action_to_be == ##AskTo && found_ttype == ELEMENTARY_TT && found_tdata == TOPIC_TOKEN) { l=inputobjs-->2; wn--; j = wn; jump Conversation2; } l = ParseToken__(found_ttype, found_tdata, pcount-1, token); while (l<-200) l = ParseToken__(ELEMENTARY_TT, l + 256); scope_reason = PARSING_REASON; if (l == GPR_PREPOSITION) { if (found_ttype~=PREPOSITION_TT && (found_ttype~=ELEMENTARY_TT || found_tdata~=TOPIC_TOKEN)) params_wanted--; l = true; } else if (l < 0) l = false; else if (l ~= GPR_REPARSE) { if (l == GPR_NUMBER) { if (nsns == 0) special_number1 = parsed_number; else special_number2 = parsed_number; nsns++; l = 1; } if (l == GPR_MULTIPLE) l = 0; results-->(parameters+2) = l; parameters++; pattern-->pcount = l; l = true; } #Ifdef DEBUG; if (parser_trace >= 3) { print " [token resulted in "; if (l == REPARSE_CODE) print "re-parse request]^"; if (l == 0) print "failure with error type ", etype, "]^"; if (l == 1) print "success]^"; } #Endif; ! DEBUG if (l == REPARSE_CODE) jump ReParse; if (l == false) break; } else { ! 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; } } ! Now, we may need to revise the multiple object because of the single one ! we now know (but didn't when the list was drawn up). if (parameters >= 1 && results-->2 == 0) { l = ReviseMulti(results-->3); if (l ~= 0) { etype = l; results-->0 = action_to_be; break; } } if (parameters >= 2 && results-->3 == 0) { l = ReviseMulti(results-->2); if (l ~= 0) { etype = l; break; } } ! To trap the case of "take all" inferring only "yourself" when absolutely ! nothing else is in the vicinity... if (take_all_rule == 2 && results-->2 == actor) { best_etype = NOTHING_PE; jump GiveError; } #Ifdef DEBUG; if (parser_trace >= 1) print "[Line successfully parsed]^"; #Endif; ! DEBUG ! The line has successfully matched the text. Declare the input error-free... oops_from = 0; ! ...explain any inferences made (using the pattern)... if (inferfrom ~= 0) { print "("; PrintCommand(inferfrom); print ")^"; } ! ...copy the action number, and the number of parameters... results-->0 = action_to_be; results-->1 = parameters; ! ...reverse first and second parameters if need be... if (action_reversed && parameters == 2) { i = results-->2; results-->2 = results-->3; results-->3 = i; if (nsns == 2) { i = special_number1; special_number1 = special_number2; special_number2 = i; } } ! ...and to reset "it"-style objects to the first of these parameters, if ! there is one (and it really is an object)... if (parameters > 0 && results-->2 >= 2) PronounNotice(results-->2); ! ...and worry about the case where an object was allowed as a parameter ! even though the player wasn't holding it and should have been: in this ! event, keep the results for next time round, go into "not holding" mode, ! and for now tell the player what's happening and return a "take" request ! instead... if (not_holding ~= 0 && actor == player) { action = ##Take; i = RunRoutines(not_holding, before_implicit); ! i = 0: Take the object, tell the player (default) ! i = 1: Take the object, don't tell the player ! i = 2: don't Take the object, continue ! i = 3: don't Take the object, don't continue if (i > 2) { best_etype = NOTHELD_PE; jump GiveError; } if (i < 2) { ! perform the implicit Take if (i ~= 1) ! and tell the player L__M(##Miscellany, 26, not_holding); notheld_mode = 1; for (i=0 : i<8 : i++) kept_results-->i = results-->i; results-->0 = ##Take; results-->1 = 1; results-->2 = not_holding; } } ! (Notice that implicit takes are only generated for the player, and not ! for other actors. This avoids entirely logical, but misleading, text ! being printed.) ! ...and return from the parser altogether, having successfully matched ! a line. if (held_back_mode == 1) { wn=hb_wn; jump LookForMore; } rtrue; } ! end of if(token ~= ENDIT_TOKEN) else } ! end of for(pcount++) ! The line has failed to match. ! We continue the outer "for" loop, trying the next line in the grammar. if (etype > best_etype) best_etype = etype; if (etype ~= ASKSCOPE_PE && etype > nextbest_etype) nextbest_etype = etype; ! ...unless the line was something like "take all" which failed because ! nothing matched the "all", in which case we stop and give an error now. if (take_all_rule == 2 && etype==NOTHING_PE) break; } ! end of for(line++) ! The grammar is exhausted: every line has failed to match. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! ! H: Cheaply parse otherwise unrecognised conversation and return ! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .GiveError; etype = best_etype; ! Errors are handled differently depending on who was talking. ! If the command was addressed to somebody else (eg, "dwarf, sfgh") then ! it is taken as conversation which the parser has no business in disallowing. if (actor ~= player) { if (usual_grammar_after ~= 0) { verb_wordnum = usual_grammar_after; jump AlmostReParse; } wn = verb_wordnum; special_word = NextWord(); if (special_word == comma_word) { special_word = NextWord(); verb_wordnum++; } special_number = TryNumber(verb_wordnum); results-->0 = ##NotUnderstood; results-->1 = 2; results-->2 = 1; special_number1 = special_word; results-->3 = actor; consult_from = verb_wordnum; consult_words = num_words-consult_from+1; rtrue; } !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! ! I: Print best possible error message ! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! If the player was the actor (eg, in "take dfghh") the error must be printed, ! and fresh input called for. In three cases the oops word must be jiggled. if (ParserError(etype) ~= 0) jump ReType; pronoun_word = pronoun__word; pronoun_obj = pronoun__obj; if (etype == STUCK_PE) { L__M(##Miscellany, 27); oops_from = 1; } if (etype == UPTO_PE) { L__M(##Miscellany, 28); for (m=0 : m<32 : m++) pattern-->m = pattern2-->m; pcount = pcount2; PrintCommand(0); L__M(##Miscellany, 56); } if (etype == NUMBER_PE) L__M(##Miscellany, 29); if (etype == CANTSEE_PE) { L__M(##Miscellany, 30); oops_from=saved_oops; } if (etype == TOOLIT_PE) L__M(##Miscellany, 31); if (etype == NOTHELD_PE) { L__M(##Miscellany, 32); oops_from=saved_oops; } if (etype == MULTI_PE) L__M(##Miscellany, 33); if (etype == MMULTI_PE) L__M(##Miscellany, 34); if (etype == VAGUE_PE) L__M(##Miscellany, 35); if (etype == EXCEPT_PE) L__M(##Miscellany, 36); if (etype == ANIMA_PE) L__M(##Miscellany, 37); if (etype == VERB_PE) L__M(##Miscellany, 38); if (etype == SCENERY_PE) L__M(##Miscellany, 39); if (etype == ITGONE_PE) { if (pronoun_obj == NULL) L__M(##Miscellany, 35); else L__M(##Miscellany, 40); } if (etype == JUNKAFTER_PE) L__M(##Miscellany, 41); if (etype == TOOFEW_PE) L__M(##Miscellany, 42, multi_had); if (etype == NOTHING_PE) { if (results-->0 == ##Remove && results-->3 ofclass Object) { noun = results-->3; ! ensure valid for messages if (noun has animate) L__M(##Take, 6, noun); else if (noun hasnt container or supporter) L__M(##Insert, 2, noun); else if (noun has container && noun hasnt open) L__M(##Take, 9, noun); else if (children(noun)==0) L__M(##Search, 6, noun); else results-->0 = 0; } if (results-->0 ~= ##Remove) { if (multi_wanted==100) L__M(##Miscellany, 43); else L__M(##Miscellany, 44); } } if (etype == ASKSCOPE_PE) { scope_stage = 3; if (indirect(scope_error) == -1) { best_etype = nextbest_etype; jump GiveError; } } !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! ! J: Retry the whole lot ! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! And go (almost) right back to square one... jump ReType; ! ...being careful not to go all the way back, to avoid infinite repetition ! of a deferred command causing an error. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! ! K: Last thing: check for "then" and further instructions(s), return. ! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! At this point, the return value is all prepared, and we are only looking ! to see if there is a "then" followed by subsequent instruction(s). .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; } i = WordAddress(verb_wordnum); j = WordAddress(wn); for (: i0 = ' '; 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 (: ii = ' '; } Tokenise__(buffer,parse); held_back_mode = true; return; } best_etype = UPTO_PE; jump GiveError; ]; ! end of Parser__parse ! Indefart: (replaced with tiny modifications, see "--Z" comments) ! An object "two ounces of clay" does not take an indefinite article. ! ("You see here two ounces of clay", not "You see here a two ounces ! of clay".) Added attribute quantarticles to support this behavior. ! An object "the first onion", with an ordinal number, uses "the" ! for both definite and indefinite articles. Added attribute ! ordinarticles to support this behavior. [ Indefart o i; i = indef_mode; indef_mode = true; if (o has proper) { indef_mode = NULL; print (PSN__) o; return; } if (o has ordinarticles) { indef_mode = NULL; print "the ", (PSN__) o; return; } ! --Z if (o has quantarticles) { indef_mode = NULL; print (PSN__) o; return; } ! --Z if (o provides article) { PrintOrRun(o, article, 1); print " ", (PSN__) o; indef_mode = i; return; } PrefaceByArticle(o, 2); indef_mode = i; ]; ! CInDefArt: (replaced with tiny modifications, see "--Z" comments) [ CInDefArt o i; i = indef_mode; indef_mode = true; if (o has proper) { indef_mode = NULL; print (PSN__) o; return; } if (o has ordinarticles) { indef_mode = NULL; print "The ", (PSN__) o; return; } ! --Z if (o has quantarticles) { indef_mode = NULL; print (PSN__) o; return; } ! --Z ! this ought to capitalize the shortname, but it doesn't if (o provides article) { PrintCapitalised(o, article, 1); print " ", (PSN__) o; indef_mode = i; return; } PrefaceByArticle(o, 2, 0, 1); indef_mode = i; ]; ! ---- ! Parse-name routines ! DefaultParseName: The standard library behavior for parsing an ! object name -- look at its name property. We use this to test object ! references in other object names. (E.g., if the player types "the X on ! the Y", we have to recognize the name of Y in X's parse_name routine.) ! Bug: If Y has a parse_name routine of its own, this fails to use it. ! (Mind you, fixing that would lead to some unpleasant parsing behavior. ! "Put the gold on the plate on the table" *might* be correctly parsed ! with right-associativity, but it might not!) [ DefaultParseName obj num wd; wd = NextWord(); while (WordInProperty(wd, obj, name)) { num++; wd = NextWord(); } return num; ]; ! BigEvilParseName: Like it says. ! This is the workhorse of the whole property-recognition system. It ! recognizes color, purity, weight, ordinal, and location descriptors, ! based on the properties of the self object. It also recognizes ! comparatives. ! This is also my big generality failure. The code is hardwired to ! know about color, purity, etc. Those *should* exist strictly in game ! code -- that is, the game author should be the one to decide that ! color is a parsable property. There are various ways to fix this, ! all of them tedious. I haven't bothered. ! (I do better with comparatives. Note that this routine does not know ! about CompPurer, Complarger, etc. It just goes through a list of ! author-provided comparatives.) [ BigEvilParseName done num wd colorprop pureprop ordinalval weightval quantval savenum savewn val par skip; if (parser_action == ##TheSame) return -2; ! We don't want to keep invoking "provides" four times per word -- ! it's expensive -- so cache these values in local variables. ! For color and purity, the value is actually an object reference ! (NopRed, NopGreen, NopPure, NopMuddy. See objects defined later.) ! For weight and ordinal, the value is just an integer. For all cases, ! zero means "object lacks this property". if (self provides color) colorprop = self.color; if (self provides purity) pureprop = self.purity; if (self provides weight) weightval = self.weight; if (self provides quantity) quantval = self.quantity; if (self provides ordinal) ordinalval = self.ordinal; ! This is a slightly weird rendering of the standard parse_name ! idiom, but it works the same. Compare to DefaultParseName. num = -1; while (~~done) { num++; wd = NextWord(); ! Check the current word against words in the object's actual ! name property. if (WordInProperty(wd, self, name)) continue; ! Check against color, purity, weight properties. (But only if ! the object actually has those properties.) ! The color and purity property objects have names, so ! we use WordInProperty to check against them. if (colorprop && WordInProperty(wd, colorprop, name)) continue; if (pureprop && WordInProperty(wd, pureprop, name)) continue; if (quantval) { ! Check against "N" (if quantity is N) val = TryNumber(wn-1); if (val > 0 && quantval == val) { continue; } } ! Parsing for weight is baroque, because it can be a multiword ! phrase. if (weightval) { ! Check against "ounce of" (if weight is 1) if ((wd == 'ounce' or 'oz') && weightval == 1) { num++; wd = NextWord(); if (wd == 'of') { num++; wd = NextWord(); if (wd == 'a//' or 'an' or 'the') { num++; wd = NextWord(); } } num--; wn--; continue; } ! Check against "N ounces of" (if weight is N) val = TryNumber(wn-1); if (val > 0 && weightval == val) { num++; wd = NextWord(); if (wd == 'ounce' or 'ounces' or 'oz') { num++; wd = NextWord(); if (wd == 'of') { num++; wd = NextWord(); if (wd == 'a//' or 'an' or 'the') { num++; wd = NextWord(); } } } num--; wn--; continue; } } ! Check for comparatives ("larger", "muddier", etc.) Comparative ! objects are those contained in the ComparativeContainer. (This ! is set up in ComparativeGameInit.) These objects have name ! properties, so we can easily test against them. When we find ! one, add it to the current list with AddComparative. objectloop (val in ComparativeContainer) { if (WordInProperty(wd, val, name)) { if (~~AddComparative(val)) { ! Comparative was accepted on list. break; } } } if (val) { ! Comparative was accepted on list, so this word is a match. continue; } ! Check for ordinal words ("first", "second", etc.) if (ordinalval && OrdinalWord(ordinalval, wd)) continue; ! The current word did not match any of the options above. ! This is the end of the noun phrase. (Except possibly for ! "in the basket", which we'll check separately.) done = True; } ! "in the basket" has to be the last clause. Check for it now. ! Bug: If the parser asks a disambig question, the player's response ! is stitched into the input at the *beginning* of the noun phrase. ! Therefore, if the player answers "Which brick do you want?" with ! "in the basket", the reparse will be "in the basket brick", and ! we'll fail to recognize it. Probably I should relax my restriction ! and accept "in the basket" anywhere in the noun phrase... but only ! once. ! Bug: For a command like "pour water in tub", this code will absorb ! "in tub" into the object name, and *not* allow it to be parsed as ! a grammar line preposition. A cheap fix would be to not check ! for "in/on" descriptors during a PutOn/Insert action. A better fix ! involves looking ahead and counting prepositions. None of these ! fixes will be fun. if ((wd == 'in' or 'on' or 'from') && num > 0 && (self notin location)) { savenum = num; savewn = wn; skip = 1; num++; wd = NextWord(); while (wd == 'a//' or 'an' or 'the') { skip++; num++; wd = NextWord(); } num--; wn--; par = parent(self); if (par.parse_name ~= 0) { val = RunRoutines(par, parse_name); } else { val = DefaultParsename(par); } if (val > 0) { wn = savewn+val+skip; num = savenum+val+skip; } else { wn = savewn; num = savenum; } } return num; ]; ! ---- ! Code to handle notional objects ! A notional object is one which the player can refer to, even though ! it is not actually in the game world. It accumulates properties as the ! player mentions them. It can have any or all of the properties which ! are important in the game. (Except for location.) ! In other words, the player could say "take five ounces from the ! bucket". (There is no five-ounce object present -- yet!) The notional ! object acquires property "five ounce (weight)", and then is handed off ! to a special action (Extract) which checks the legality and then ! performs the work. ! "Take five ounces of gold from the bucket" is a different operation, ! because the notional object has *two* properties: "five ounce (weight)" ! and "gold (substance)". This could be a valuable disambiguation -- no ! pun intended -- if the bucket contains a mixture of gold and silver ! coins. ! My sop to generality: This code does not know about any particular ! list of properties. (Except for weight. That's a bug.) The game code ! provides property objects of class NotionalPropertyClass. For efficiency, ! we move these to the NotionalPropertyContainer at game startup time. ! The game must also provide an object to *be* the NotionalObject. ! (This object has to store properties like color and purity, which the ! library does not know about, so the library can't define it.) Class NotionalPropertyClass with nop_specify [; ! For each property p, p.nop_specify(obj) must apply p ! to the NotionalObject obj. The method should return true ! if it succeeds; false if the property could not be applied ! (say, because obj already has a conflicting property.) print "[BUG] NotionalPropertyClass object ", (name) self, " has no nop_specify method.^"; rfalse; ]; ! NopQuantity: Quantity (and units) are represented by this property object. ! It is the only one built into the library, instead of being provided by ! the game. This is because it is handled specially in NotionalToken. ! NopQuantity.nop_specify takes a second argument, val, and an optional ! third, unit. This indicates the quantity, in whatever the unit is. ! (If unit is zero, the quantity is in -- er -- units.) NotionalPropertyClass NopQuantity with nop_specify [ obj val unit; if (obj.quantunit && unit && obj.quantunit ~= unit) rfalse; if (obj.quantity && obj.quantity ~= val) rfalse; if (unit) obj.quantunit = unit; obj.quantity = val; rtrue; ]; ! NotionalObject: Global variable to hold the game's notional object. ! (In the game code below, this will be MyNotionalObject.) Global NotionalObject; ! NotionalPropertyContainer: Holds all the NotionalProperty objects ! in the game. (NotionalGameInit sets this up.) Object NotionalPropertyContainer; ! NotionalPropertyUnitContainer: Holds all the NotionalPropertyUnit objects ! in the game. (NotionalGameInit sets this up.) (We have a separate container ! for unit properties, because they're parsed differently: they occur in ! phrases like "5 ounces of".) Object NotionalPropertyUnitContainer; ! NotionalGameInit: The game's Initialise() routine must call this, and ! pass in the game-specific NotionalObject. [ NotionalGameInit notobj obj; NotionalObject = notobj; ! Move NotionalPropertyClass objects into NotionalPropertyContainer. ! This lets us efficiently iterate over them in our parsing procedure. ! (We don't bother moving properties like NopWeight, which have ! no name, but are parsed with hacky hard-wired code.) objectloop (obj ofclass NotionalPropertyClass) { if (obj provides name) move obj to NotionalPropertyContainer; if (obj ofclass NotionalPropertyUnitClass) move obj to NotionalPropertyUnitContainer; } ]; ! NotionalToken: The token-parsing routine which builds the NotionalObject. ! (Note that there's only one NotionalObject, so we can only have one ! NotionalToken per grammar line. Probably safer that way, anyhow.) [ NotionalToken num wd done obj val; ! Clear out all the property fields. NotionalObject.initial(); ! This parsing code looks a lot like BigEvilParseName, except that ! it doesn't recognize the location property ("on the sofa"), and ! it uses NotionalPropertyContainer for its list of properties ! that it *does* recognize. num = -1; while (~~done) { num++; wd = NextWord(); objectloop (obj in NotionalPropertyContainer) { if (WordInProperty(wd, obj, name)) { break; } } if (obj) { ! Matched this property's name. Add the property to NotionalObject. val = obj.nop_specify(NotionalObject); if (~~val) { ! Conflicting property means this word can't be accepted break; } continue; } ! Unit properties ("ounces") are handled separately, because they ! occur in their own kind of phrase: "5 ounces of..." They always ! count as NopQuantity, which is the only property object hardwired ! into this code. ! Check against "UNIT of" objectloop (obj in NotionalPropertyUnitContainer) { if (WordInProperty(wd, obj, sing_name)) { break; } } if (obj) { num++; wd = NextWord(); if (wd == 'of') { num++; wd = NextWord(); if (wd == 'a//' or 'an' or 'the') { num++; wd = NextWord(); } } num--; wn--; val = NopQuantity.nop_specify(NotionalObject, 1, obj); if (~~val) { ! Conflicting property means this word can't be accepted break; } continue; } ! Check against "N UNITs of", and also "N". val = TryNumber(wn-1); if (val > 0) { num++; wd = NextWord(); objectloop (obj in NotionalPropertyUnitContainer) { if (WordInProperty(wd, obj, name)) { break; } } if (obj) { num++; wd = NextWord(); if (wd == 'of') { num++; wd = NextWord(); if (wd == 'a//' or 'an' or 'the') { num++; wd = NextWord(); } } } ! if obj==0, then we just have "5", which is a unitless ! quantity. num--; wn--; val = NopQuantity.nop_specify(NotionalObject, val, obj); if (~~val) { ! Conflicting property means this word can't be accepted break; } continue; } done = True; } ! We return NotionalObject or nothing. if (num == 0) return GPR_FAIL; wn--; return NotionalObject; ]; ! ExtractableFilterFunc: Filter procedure, used with CreateScopeList. ! Pass only those objects which can be extracted from. [ ExtractableFilterFunc obj; if (obj provides check_extract) rtrue; rfalse; ]; ! ImplicitlyCreated: Global storage used by ImplicitTake. Global ImplicitlyCreated; ! NoticeImplicit: Utility used by ImplicitTake. ! Any object which provides an ExtractFrom action must call this ! after creating an object in an extraction. [ NoticeImplicit obj; ImplicitlyCreated = obj; ]; ! DebugNotionalSub: Action routine for a debugging action. ! Type "notion PHRASE" to match your PHRASE directly against ! NotionalToken, and display the result, without doing anything else. [ DebugNotionalSub; if (noun ~= NotionalObject) "[BUG] ~notional~ matched ", (nameid) noun, " instead of NotionalObject."; print "Matched the notional object: ", (the) NotionalObject, ".^"; ]; ! ExtractSub: Action routine for the Extract action. ! noun will always be NotionalObject; second will be the source you ! are extracting from. ! We handle this by invoking the fake action ExtractFrom on second. ! If the object does not handle ExtractFrom (in second.before), we ! go on to try its contents (if it is a container/supporter). ! This involves some hairiness, since we'd like to choose the *correct* ! contents -- that is, something which supports ExtractFrom, and indeed ! which can handle the requested extraction. To determine this, we use ! the check_extract() method. This must perform the same viability ! tests as the before:ExtractFrom clause, and return true or false, but ! without printing anything. ! (This means the game author has to duplicate a lot of code. Definitely ! a poor solution. Perhaps liberal use of the keep_silent mechanism ! would make this better.) [ ExtractSub obj; if (noun ~= NotionalObject) "[BUG] Extract matched ", (nameid) noun, " instead of NotionalObject."; !print "[DEBUG] Extract ", (the) NotionalObject, " from ", ! (the) second, ".^"; action = ##ExtractFrom; if (RunRoutines(second, before) ~= 0) { action = ##Extract; rtrue; } if (second has supporter || (second has container && second has open)) { action = ##ExtractFrom; objectloop (obj in second) { if (obj provides check_extract && obj.check_extract()) { if (RunRoutines(obj, before) ~= 0) { action = ##Extract; rtrue; } } } action = ##Extract; ! For a container, the more specific failure message makes sense. "You can't extract ", (the) NotionalObject, " from ", (the) second, "."; } "You can't extract anything from ", (the) second, "."; ]; ! ExtractFromSub: Action routine for the ExtractFrom fake action. ! This doesn't get called in the normal course of events -- we only ! trigger ExtractFrom using RunRoutines(obj, before). However, this ! routine is a handy place to put the default you-can't-do-that message. [ ExtractFromSub; "You can't extract ", (the) NotionalObject, " from ", (the) second, "."; ]; ! ExtractVagueSub: Action routine for the ExtractVague action. ! (I.e., "take five ounces of clay" with no "from" clause.) ! noun will always be NotionalObject. ! This does the same work as the Extract action, except that it tries ! to infer a source. See ImplicitTake. [ ExtractVagueSub obj; if (noun ~= NotionalObject) "[BUG] ExtractVague matched ", (nameid) noun, " instead of NotionalObject."; !print "[DEBUG] ExtractVague ", (the) NotionalObject, ".^"; obj = ImplicitTake(); if (obj) { ! Extract action already done. rtrue; } "You can't see anywhere to get ", (name) NotionalObject, "."; ]; ! ExtractInsertSub: Action routine for the ExtractInsert action. ! (I.e., "put five ounces of clay in kiln" where the five ounces ! requires an implicit extract.) [ ExtractInsertSub obj; if (noun ~= NotionalObject) "[BUG] ExtractInsert matched ", (nameid) noun, " instead of NotionalObject."; obj = ImplicitTake(); if (obj) { <>; } "You can't see anywhere to get ", (name) NotionalObject, "."; ]; ! ExtractPutOnSub: Action routine for the ExtractPutOn action. ! (I.e., "put five ounces of clay on table" where the five ounces ! requires an implicit extract.) [ ExtractPutOnSub obj; if (noun ~= NotionalObject) "[BUG] ExtractPutOn matched ", (nameid) noun, " instead of NotionalObject."; obj = ImplicitTake(); if (obj) { <>; } "You can't see anywhere to get ", (name) NotionalObject, "."; ]; ! ExtractDropSub: Action routine for the ExtractDrop action. ! (I.e., "drop five ounces of clay" where the five ounces ! requires an implicit extract.) [ ExtractDropSub obj; if (noun ~= NotionalObject) "[BUG] ExtractInsert matched ", (nameid) noun, " instead of NotionalObject."; obj = ImplicitTake(); if (obj) { <>; } "You can't see anywhere to get ", (name) NotionalObject, "."; ]; ! ImplicitTake: Try to extract NotionalObject from somewhere. ! This works by testing the check_extract method of every object ! in scope. If nothing in scope can provide the request, it prints an ! appropriate failure message. If multiple things in scope work, it ! does *not* print a disambig question -- that's very difficult in ! Inform 6. It just picks the first one. Oh well. ! Actually getting the identity of the extracted object requires a further ! (ugly) trick. The game has several ExtractFrom routines, each of which ! can potentially create an object. They have no direct way to return that ! object. Therefore, each one must call NoticeImplicit after creation. ! That sets a global variable, ImplicitlyCreated, which this routine ! checks up on. [ ImplicitTake ix obj; ImplicitlyCreated = nothing; CreateScopeList(ExtractableFilterFunc); for (ix=0 : ixix; if (obj provides check_extract && obj.check_extract()) { print "(extracting ", (the) NotionalObject, " from ", (the) obj, ")^"; ; break; } } if (ImplicitlyCreated) { return ImplicitlyCreated; } return nothing; ]; ! ---- ! Code to handle ordinal numbers ("first", "second") ! OrdinalNumber: Print the English ordinal for a number. ! (Does not handle negative numbers.) [ OrdinalNumber val rem; switch (val) { 0: print "zeroth"; 1: print "first"; 2: print "second"; 3: print "third"; 4: print "fourth"; 5: print "fifth"; 6: print "sixth"; 7: print "seventh"; 8: print "eighth"; 9: print "ninth"; 10: print "tenth"; 11: print "eleventh"; 12: print "twelfth"; 13: print "thirteenth"; 14: print "fourteenth"; 15: print "fifteenth"; 16: print "sixteenth"; 17: print "seventeenth"; 18: print "eighteenth"; 18: print "nineteenth"; 20: print "twentieth"; default: rem = val % 10; switch (rem) { 1: print val, "st"; 2: print val, "nd"; 3: print val, "rd"; default: print val, "th"; } } ]; ! OrdinalWord: Return true if the given numeric value matches the given ! English ordinal word. Only works up to 20. ! (Perhaps this should accept purely numeric words, like 'three' and '3'. ! It is not unreasonable for the player to type "take potato 3" instead of ! "take third potato". But it would probably be better for BigEvilParseName ! to call both TryNumber and OrdinalWord, instead of making OrdinalWord ! broader.) [ OrdinalWord val wd; switch (val) { 0: return (wd == 'zeroth' or '0th'); 1: return (wd == 'first' or '1st'); 2: return (wd == 'second' or '2nd'); 3: return (wd == 'third' or '3rd'); 4: return (wd == 'fourth' or '4th'); 5: return (wd == 'fifth' or '5th'); 6: return (wd == 'sixth' or '6th'); 7: return (wd == 'seventh' or '7th'); 8: return (wd == 'eighth' or '8th'); 9: return (wd == 'ninth' or '9th'); 10: return (wd == 'tenth' or '10th'); 11: return (wd == 'eleventh' or '11th'); 12: return (wd == 'twelfth' or '12th'); 13: return (wd == 'thirteenth' or '13th'); 14: return (wd == 'fourteenth' or '14th'); 15: return (wd == 'fifteenth' or '15th'); 16: return (wd == 'sixteenth' or '16th'); 17: return (wd == 'seventeenth' or '17th'); 18: return (wd == 'eighteenth' or '18th'); 18: return (wd == 'nineteenth' or '19th'); 20: return (wd == 'twentieth' or '20th'); } rfalse; ]; ! ApplyOrdinals: Set the ordinal property for objects in scope, as needed, ! to distinguish indistinguishable objects. ! This is the big hairy workbuffalo of the ordinal-number system. If it ! looks short, it's only because I have three layers of helper functions ! built here. ! The plan is this: First, create an array listing every object in scope ! which *might* need an ordinal. Then sort the array (yes, with a selection ! sort) to group together each class of possibly-identical objects. (Onions, ! potatoes, lumps of clay, etc.) Within each class, we sort by ordinal. ! (This is not relevant here, but will be important in ApplyOrdinalsBatch.) ! These classes are defined by the visibly_distinct property. That is, ! a bunch of objects that share a visibly_distinct method are assumed to be ! of the same class. (The method is not invoked at this stage, just compared.) ! Then we break up the array into classes, and call ApplyOrdinalsTo on ! each class in turn. [ ApplyOrdinals ix jx obj obj2 curident; CreateScopeList(OrdinableFilterFunc); if (scope_count == 0) return; SelSort(scope_list, scope_count, OrdinableSortFunc); ! DumpScopeList(); ix = 0; while (ix < scope_count) { obj = scope_list-->ix; curident = obj.visibly_distinct; for (jx = ix+1 : jxjx; if (obj2.visibly_distinct ~= curident) break; } ApplyOrdinalsTo(scope_list+WORDSIZE*ix, jx-ix, curident); ix = jx; } ]; ! ApplyOrdinalsTo: Apply ordinals to a class of potentially-identical ! functions. The identfunc argument is the visibly_distinct method they ! all share. ! The plan: For each object in the list, compare it to all others to ! see if it's identical. (This is where we call visibly_distinct. A lot. ! We are assuming that visibly_distinct is a transitive property.) If ! two or more objects are identical, apply ordinals to all of them, ! preserving an existing ordinal if possible. ! The implementation of this is tedious. We have to do two nested loops ! through the array, comparing everything to everything else. The work ! attributes ao_handling and ao_handled keep track of what items we're ! working on now, and what items we've already finished with. [ ApplyOrdinalsTo arr len identfunc ix jx o1 o2 num; for (ix=0 : ixix; give o1 ~ao_handling; give o1 ~ao_handled; } for (ix=0 : ixix; if (o1 has ao_handled) continue; give o1 ao_handling; num = 1; for (jx=0 : jxjx; if (o2 has ao_handled) continue; if (identfunc(o1, o2)) continue; give o2 ao_handling; num++; } if (num > 1) { ApplyOrdinalsBatch(arr, len); } for (jx=0 : jxjx; if (o2 hasnt ao_handling) continue; give o2 ~ao_handling; give o2 ao_handled; } } ]; ! ApplyOrdinalsBatch: Set the ordinal values for a group of (really) ! identical objects. The objects are listed in array arr, of length len; ! but we should only consider objects that have the ao_handling attribute. ! There will be at least two. ! Our goal is to leave objects which already have ordinals with the same ! ordinal value, if that's possible. It may not be -- two objects might ! have the same ordinal, in which case we'll have to change one. ! We do this with an intensely fiddly counting algorithm. The algorithm ! assumes that the ordinal values are sorted when ApplyOrdinalsBatch begins. ! (They won't be sorted when it finishes, but at that point we won't care.) [ ApplyOrdinalsBatch arr len ix jx obj o2 curval lastval nextval; curval = 0; for (ix=0 : ixix; if (obj hasnt ao_handling) continue; if (obj.ordinal == 0) { give obj ~ao_keepval; continue; } if (obj.ordinal > curval) { curval = obj.ordinal; give obj ao_keepval; } else { obj.ordinal = 0; give obj ~ao_keepval; } } curval = 1; lastval = 0; nextval = 0; jx = 0; while (jxjx; jx++; if (o2 hasnt ao_handling) continue; if (o2 hasnt ao_keepval) continue; else { nextval = o2.ordinal; break; } } for (ix=0 : ixix; if (obj hasnt ao_handling) continue; if (obj has ao_keepval) continue; while (curval >= nextval && nextval ~= -1) { lastval = nextval; curval = lastval+1; if (jx >= len) { nextval = -1; break; } while (jxjx; jx++; if (o2 hasnt ao_handling) continue; if (o2 hasnt ao_keepval) continue; else { nextval = o2.ordinal; break; } } } !print "Changing ", (nameid) obj, " to ordinal ", curval, "^"; obj.ordinal = curval; give obj ordinarticles; curval++; } ]; ! OrdinableFilterFunc: Filter procedure, used with CreateScopeList. ! Pass only those objects which belong to a distinguishability class. [ OrdinableFilterFunc obj; if (obj provides ordinal && obj provides visibly_distinct) rtrue; rfalse; ]; ! Sort procedure, used with SelSort. ! Sort first on distinguishability class, then on ordinal value. [ OrdinableSortFunc o1 o2; if (o1 == o2) return 0; if (o1.visibly_distinct ~= o2.visibly_distinct) return (o2.visibly_distinct - o1.visibly_distinct); return (o2.ordinal - o1.ordinal); ]; ! ---- ! Code to handle comparatives ("larger", "muddier", etc) ! When NounDomain is checking objects against the player's input, it ! has to accumulate a list of comparatives. (It can't evaluate the ! comparatives until it has a list of matched objects to compare.) ! The compar_list array is the list it accumulates. Constant MAX_COMPARATIVES 8; Global compar_count = 0; Array compar_list --> MAX_COMPARATIVES; ! When NounDomain is checking to see if a word is a comparative, it has ! to iterate through the comparatives efficiently. "objectloop (o ofclass ! ComparativeClass)" is not efficient. So, at the beginning of the game, ! we move all the ComparativeClass objects into ComparativeContainer. Object ComparativeContainer; ! ComparativeClass: Represents a comparative. ! The comp_opposite property should contain the opposite comparative ! ("smaller" to "larger", etc) if there is one. The library uses this to ! prevent the player from typing "take the largest smallest rock". ! The comp_select(arr, len) method must go through the array and ! select the most object or objects. All array members that ! don't fit the criterion should be set to -1. (The array may have -1 ! in it already, when comp_select is invoked.) Class ComparativeClass with comp_opposite 0, comp_select [; "[BUG] ComparativeClass object ", (name) self, " has no comp_select method."; ]; ! ComparativeGameInit: Must be called from Initialise to set up ! the comparatives module. [ ComparativeGameInit comp; objectloop (comp ofclass ComparativeClass) { move comp to ComparativeContainer; } ]; ! SetupComparatives: Prepare for the parsing of one noun phrase. ! Called at the beginning of NounDomain. [ SetupComparatives; compar_count = 0; ]; ! AddComparative: Add one comparative (from the player's input) to our ! accumulated list. ! This returns true if the comparative cannot be accepted (because it ! contradicts one we already have). It returns false if everything is ! fine. [ AddComparative comp ix comp2; for (ix=0 : ixix; if (comp2 == comp) rfalse; ! already in list if (comp2 == comp.comp_opposite) rtrue; ! contradicts one already in list } if (compar_count >= MAX_COMPARATIVES) { "[BUG] Too many comparatives! MAX_COMPARATIVES is ", MAX_COMPARATIVES, "."; } compar_list-->compar_count = comp; compar_count++; rfalse; ]; ! ApplyComparatives: Filter match_list, by applying our accumulated list ! of comparatives. ! When this is called (from NounDomain), match_list contains a list of ! objects (number_matched of them) which matched the player's input. ! compar_list contains a list of comparatives (compar_count of them) ! which were included in the input. We have to reduce match_list down ! to just the objects which match the comparatives. [ ApplyComparatives ix jx obj comp; if (compar_count == 0) rfalse; !print "ApplyComparatives: ", compar_count, " comparative(s) on list of ", ! number_matched, "^"; !for (ix=0 : ixix; ! print " ", ix, ": ", (nameid) obj, " in the ", ! (name) parent(obj), "^"; !} ! We're going to apply the comparatives in reverse order. This is ! because the intent of "the largest hottest potato" is to first ! trim the list to the hottest potatoes, and then trim *that* to ! the largest (of the hottest potatoes). Got it? Good. while (compar_count && number_matched > 1) { compar_count--; comp = compar_list-->compar_count; !print "Comparative ~", (name) comp, "~^"; ! Apply this comparative's comp_select method. This will set ! non-desired entries in the array to -1. comp.comp_select(match_list, number_matched); ! Condense match_list to eliminate the -1 entries. jx = 0; for (ix=0 : ixix; if (obj == -1) continue; match_list-->jx = obj; jx++; } number_matched = jx; !print "After selecting: ", number_matched, " left in list:^"; !for (ix=0 : ixix; ! print " ", ix, ": ", (nameid) obj, " in the ", ! (name) parent(obj), "^"; !} } ]; ! ---- ! The sample game begins here. Everything below this point -- well, nearly ! everything -- is game-specific code, which uses the library code above. [ Initialise obj; obj = DumpScopeList; ! shut up compiler inventory_style = FULLINV_BIT + ENGLISH_BIT + RECURSE_BIT; ! wide lookmode = 2; ! verbose location = Anteroom; move wand to player; ! create some clay for the workshop obj = ClayLumpClass.create(3, NopYellow, NopMuddy); move obj to table; obj = ClayLumpClass.create(1, NopYellow); move obj to table; obj = ClayLumpClass.create(4, NopRed); move obj to table; obj = ClayLumpClass.create(3, NopYellow); move obj to Workshop; ! stuff to set up the library NotionalGameInit(MyNotionalObject); ComparativeGameInit(); ]; ! I always throw in a magic wand, so that there's a plain, ordinary, ! no-class, no-parsename object to test with. Object wand "wand" with name 'magic' 'wand', description "It's your magic wand. Unfortunately, it's all out of magic."; ! Initial room. Object Anteroom "Anteroom" with description "This is a small room, antechamber to an exemplary house. The kitchen is to the south. The workshop is to the east.", s_to Kitchen, e_to Workshop; Object -> anteroomplacard "placard" with name 'placard', initial "A placard hangs on the wall.", description [; InitialMessage(); rtrue; ], has static; ! Kitchen: see the "distinguishable objects" code below. Object Kitchen "Kitchen" with description "This is the kitchen. The anteroom is to the north. You see another placard here.^^ Four levers are set in the far wall; one is red, one black, one white, and one yellow. In the corner is an open garbage chute.", n_to Anteroom; Object -> kitchenplacard "placard" with name 'another' 'placard', description [; KitchenMessage(); rtrue; ], has scenery; Object -> chest "chest" with name 'chest', has static container openable ~open; Object -> cabinet "cabinet" with name 'cabinet' 'cupboard', has static container openable ~open; ! Workshop: see the "Continuous substances" code below. Object Workshop "Workshop" with description [; print "This is the workshop. The anteroom is back to the west. You see another placard here.^^"; if (orangeclaybarrel in self) print "Six barrels of clay sit in the corner"; else print "A barrel of red clay sits in the corner"; ", next to an open garbage chute and a white lever. Nearby is a bucket of ball bearings."; ], w_to Anteroom; Object -> workshopplacard "placard" with name 'another' 'placard', description [; WorkshopMessage(); rtrue; ], has scenery; Object -> table "table" with name 'table', has static supporter; Object -> plate "plate" with name 'plate', has supporter; Object -> barrellever "white lever" with name 'white' 'lever', description "~In case of monochromaticity, pull lever.~", before [; Pull: if (orangeclaybarrel in Workshop) "Nothing further occurs."; move orangeclaybarrel to Workshop; move yellowclaybarrel to Workshop; move greenclaybarrel to Workshop; move blueclaybarrel to Workshop; move purpleclaybarrel to Workshop; "With a deafening WHUMP, five more barrels of clay fall out of the ceiling."; ], has scenery; ! ---- ! Notional properties used in this game ! "Notional properties" is a misnomer, because I wound up using these ! property objects to represent the properties of real objects as well. ! We divide the properties into classes: substance (clay, metal); ! color (red, orange, ...); purity (pure, impure, dull, muddy); ! weight (an integer number of ounces.) ! Each property has a nop_specify(obj) method (inherited from its class) ! which applies the property to obj, which will be a NotionalObject. The ! method returns false if the property could not be applied -- say, because ! obj already has a conflicting property. Class NotionalPropertySubstanceClass class NotionalPropertyClass, with nop_specify [ obj; if (obj.substance && obj.substance ~= self) rfalse; obj.substance = self; rtrue; ]; Class NotionalPropertyColorClass class NotionalPropertyClass, with nop_specify [ obj; if (obj.color && obj.color ~= self) rfalse; obj.color = self; rtrue; ]; Class NotionalPropertyPurityClass class NotionalPropertyClass, with nop_specify [ obj; if (obj.purity && obj.purity ~= self) rfalse; obj.purity = self; rtrue; ]; Class NotionalPropertyUnitClass class NotionalPropertyClass, with nop_specify [ obj; if (obj.quantunit && obj.quantunit ~= self) rfalse; obj.quantunit = self; rtrue; ]; ! Units. (Recall that the quantity property is handled by NopQuantity, ! in the library code. But the units are game-specific.) NotionalPropertyUnitClass NopOunces with short_name [ num; if (num == 1) print "ounce"; else print "ounces"; rtrue; ], sing_name 'ounce' 'oz', name 'ounce' 'ounces' 'oz'; NotionalPropertyUnitClass NopDollars with short_name [ num; if (num == 1) print "dollar"; else print "dollars"; rtrue; ], sing_name 'dollar' 'buck' '$//', name 'dollar' 'buck' '$//' 'dollars' 'bucks'; ! Substances. NotionalPropertySubstanceClass NopMetal "metal" with name 'metal' 'ball' 'balls' 'bearing' 'bearings'; NotionalPropertySubstanceClass NopClay "clay" with name 'clay'; ! Colors. The number value is used by the color-mixing algorithm. NotionalPropertyColorClass NopPurple "purple" with name 'purple' 'violet', number 5; NotionalPropertyColorClass NopBlue "blue" with name 'blue', number 4; NotionalPropertyColorClass NopGreen "green" with name 'green', number 3; NotionalPropertyColorClass NopYellow "yellow" with name 'yellow', number 2; NotionalPropertyColorClass NopOrange "orange" with name 'orange', number 1; NotionalPropertyColorClass NopRed "red" with name 'red', number 0; NotionalPropertyColorClass NopBlack "black" with name 'black'; NotionalPropertyColorClass NopWhite "white" with name 'white'; NotionalPropertyColorClass NopGrey "grey" with name 'grey' 'gray'; ! Purities. The number value is used by the color-mixing algorithm. NotionalPropertyPurityClass NopPure "pure" with name 'pure' 'bright', number 0; NotionalPropertyPurityClass NopImpure "impure" with name 'impure', number 1; NotionalPropertyPurityClass NopDull "dull" with name 'dull', number 2; NotionalPropertyPurityClass NopMuddy "muddy" with name 'muddy', number 3; ! PurityByValue: Utility function to map a number to the ! NotionalPropertyPurityClass which has that number value. [ PurityByValue val; switch (val) { 0: return NopPure; 1: return NopImpure; 2: return NopDull; 3: return NopMuddy; default: "[BUG] PurityByValue(", val, ") out of bounds.^"; } ]; ! ColorByValue: Utility function to map a number to the ! NotionalPropertyColorClass which has that number value. [ ColorByValue val; switch (val) { 0: return NopRed; 1: return NopOrange; 2: return NopYellow; 3: return NopGreen; 4: return NopBlue; 5: return NopPurple; default: "[BUG] ColorByValue(", val, ") out of bounds.^"; } ]; ! This is the game's notional object. It has fields to store all the game- ! specific properties. This object winds up stored in the NotionalObject ! global variable. (So when I've been talking about NotionalObject all ! this time, I mean this object.) ! The NotionalObject does not have a weight property. The quantity is ! represented by its quantity and quantunit (which might be "5" and ! "ounces", respectively, to represent a weight of five ounces.) When ! an extraction action is performed, the quantity properties are considered ! to determine the weight of the new object. (This allows us to accept ! "take 5 green" as shorthand for "5 ounces of green clay". It also allows ! us to accept "take 10 balls" and "take 1 oz of balls" as equivalent, ! by deciding that balls weigh a tenth of an ounce each.) ! The NotionalObject has the "proper" attribute, because it never takes ! an article. It's always "clay" or "five ounces of clay" or "pure white ! metal", never "the clay" or "a metal". Actual objects get articles, ! not notions. Object MyNotionalObject with initial [; ! The initial() method must clear all properties. self.color = 0; self.substance = 0; self.purity = 0; self.quantity = 0; self.quantunit = 0; ], color 0, substance 0, purity 0, quantity 0, quantunit 0, short_name [ issing isplur; if (self.quantunit) { if (self.quantity) { print (EnglishNumber) self.quantity, " "; self.quantunit.short_name(self.quantity); print " of "; } else { self.quantunit.short_name(0); print " of "; } } else { if (self.quantity) { print (EnglishNumber) self.quantity, " "; if (self.quantity > 1) isplur = true; else issing = true; } } if (self.purity) print (name) self.purity, " "; if (self.color) print (name) self.color, " "; if (issing || isplur) { if (self.substance) print (name) self.substance, " "; if (~~isplur) print "item"; else print "items"; } else { if (self.substance) print (name) self.substance; else print "substance"; } rtrue; ], has proper; ! ---- ! Comparatives used in this game ! Comparative objects are simple. Each has a name (a list of words), and ! a comp_select(arr, len) method. This must go through the array and ! select the most object or objects. All array members that ! don't fit the criterion should be set to -1. (The array may have -1 ! in it already, when comp_select is invoked.) ! All these comp_select methods are similar. Go through the array once ! to figure out the largest/smallest value of the relevant property. ! Then go through again to -1 all entries that don't match it. If ! none of the objects have the property, we do nothing -- it's better ! to leave all the objects in the array than to blot them all out. ComparativeClass CompLarger with name 'larger' 'largest' 'bigger' 'biggest' 'heavier' 'heaviest', comp_opposite CompSmaller, comp_select [ arr len ix obj sofar; sofar = -1; for (ix=0 : ixix; if (obj == -1) continue; if (~~(obj provides weight)) continue; if (sofar == -1 || obj.weight > sofar) sofar = obj.weight; } if (sofar == -1) { ! if nothing has a weight, fall back to comparing quantity CompMost.comp_select(arr, len); return; } for (ix=0 : ixix; if (obj == -1) continue; if (~~(obj provides weight)) { arr-->ix = -1; continue; } if (obj.weight ~= sofar) arr-->ix = -1; } ]; ComparativeClass CompSmaller with name 'smaller' 'smallest' 'lighter' 'lightest', comp_opposite CompLarger, comp_select [ arr len ix obj sofar; sofar = -1; for (ix=0 : ixix; if (obj == -1) continue; if (~~(obj provides weight)) continue; if (sofar == -1 || obj.weight < sofar) sofar = obj.weight; } if (sofar == -1) { ! if nothing has a weight, fall back to comparing quantity CompLeast.comp_select(arr, len); return; } for (ix=0 : ixix; if (obj == -1) continue; if (~~(obj provides weight)) { arr-->ix = -1; continue; } if (obj.weight ~= sofar) arr-->ix = -1; } ]; ComparativeClass CompPurer with name 'purer' 'purest' 'brighter' 'brightest', comp_opposite CompDuller, comp_select [ arr len ix obj sofar; sofar = 0; for (ix=0 : ixix; if (obj == -1) continue; if (~~(obj provides purity)) continue; if (sofar == 0 || obj.purity.number < sofar.number) sofar = obj.purity; } if (sofar == 0) return; for (ix=0 : ixix; if (obj == -1) continue; if (~~(obj provides purity)) { arr-->ix = -1; continue; } if (obj.purity ~= sofar) arr-->ix = -1; } ]; ! I suppose this ought to accept grey as duller than any other color ComparativeClass CompDuller with name 'duller' 'dullest' 'muddier' 'muddiest' 'impurer' 'impurest', comp_opposite CompPurer, comp_select [ arr len ix obj sofar; sofar = 0; for (ix=0 : ixix; if (obj == -1) continue; if (~~(obj provides purity)) continue; if (sofar == 0 || obj.purity.number > sofar.number) sofar = obj.purity; } if (sofar == 0) return; for (ix=0 : ixix; if (obj == -1) continue; if (~~(obj provides purity)) { arr-->ix = -1; continue; } if (obj.purity ~= sofar) arr-->ix = -1; } ]; ComparativeClass CompMost with name 'more' 'most', comp_opposite CompLeast, comp_select [ arr len ix obj sofar; sofar = -1; for (ix=0 : ixix; if (obj == -1) continue; if (~~(obj provides quantity)) continue; if (sofar == -1 || obj.quantity > sofar) sofar = obj.quantity; } if (sofar == -1) return; for (ix=0 : ixix; if (obj == -1) continue; if (~~(obj provides quantity)) { arr-->ix = -1; continue; } if (obj.quantity ~= sofar) arr-->ix = -1; } ]; ComparativeClass CompLeast with name 'fewer' 'fewest' 'lesser' 'least', comp_opposite CompMost, comp_select [ arr len ix obj sofar; sofar = -1; for (ix=0 : ixix; if (obj == -1) continue; if (~~(obj provides quantity)) continue; if (sofar == -1 || obj.quantity < sofar) sofar = obj.quantity; } if (sofar == -1) return; for (ix=0 : ixix; if (obj == -1) continue; if (~~(obj provides quantity)) { arr-->ix = -1; continue; } if (obj.quantity ~= sofar) arr-->ix = -1; } ]; ! ---- ! Distinguishable objects which get ordinals ! These objects go in the kitchen. ! Vegetables all use the BigEvilParseName routine, but their only properties ! are location and ordinal. (The single red onion doesn't make use of the ! NopRed property. It's just red in the ordinary Inform way.) Class OnionClass with short_name [; if (self.ordinal) { print (OrdinalNumber) self.ordinal, " "; } PrintOrRun(self, base_name, 1); rtrue; ], base_name "onion", name 'onion' 'onions//p', parse_name BigEvilParseName, ordinal 0, visibly_distinct [ o1 o2; ! The red onion is distinct from all the other onions. (For ! purposes of assigning distinguishing ordinals. The red onion ! doesn't need one.) if (o1 == onionred || o2 == onionred) rtrue; ], before [; Eat: "No eating the vegetables."; ]; ! PlainOnionClass: Handles all the onions except the red one. ! We use a list_together procedure which just prints "N plain onions", ! and skips the individual listing "(the first plain onion, the third ! plain onion...)" I'm not sure this is the right way to handle onions ! from the player's point of view. It makes object listings much easier ! to read; but it hides the ordinal information, which is supposed to ! be readily visible. Class PlainOnionClass class OnionClass, with base_name "plain onion", name 'plain', list_together [ count; if (inventory_stage == 1) { count = ListTogetherCount(); print (EnglishNumber) count, " plain onions"; print " ("; ListTogetherOrdinals(count); print ")"; rtrue; } ]; PlainOnionClass onion1 Kitchen; PlainOnionClass onion2 Kitchen; PlainOnionClass onion3 Kitchen; PlainOnionClass onion4 chest; PlainOnionClass onion5 cabinet; PlainOnionClass onion6 cabinet; OnionClass onionred chest with base_name "red onion", name 'red', list_together 0; ! TurnipClass: Handles all the turnips. Works the same as onions. Class TurnipClass with short_name [; if (self.ordinal) { print (OrdinalNumber) self.ordinal, " "; } print "turnip"; rtrue; ], name 'turnip' 'neep' 'turnips//p' 'neeps//p', list_together [ count; if (inventory_stage == 1) { count = ListTogetherCount(); print (EnglishNumber) count, " turnips"; print " ("; ListTogetherOrdinals(count); print ")"; rtrue; } ], parse_name BigEvilParseName, ordinal 0, visibly_distinct [; ], before [; Eat: "No eating the vegetables."; ]; TurnipClass turnip1 Kitchen; TurnipClass turnip2 Kitchen; TurnipClass turnip3 Kitchen; TurnipClass turnip4 chest; ! PotatoClass: Handles all the potatoes. ! These work the same as onions, except that they have a .color property, ! which can be NopRed, NopWhite, NopYellow, NopBlack. (Yes, there are ! black potatoes.) ! Potatoes are dynamically created. Initially, there are none in the world. Class PotatoClass(10) with create [ col; self.color = col; ! Clear out any lingering ordinal value from previous existence. self.ordinal = 0; ], short_name [; if (self.ordinal) { print (OrdinalNumber) self.ordinal, " "; } print (name) self.color, " "; print "potato"; rtrue; ], name 'potato' 'tater' 'potatoes//p' 'taters//p', list_together [ count total colnum; ! Print the number of potatoes, categorized by color. This code ! is not very general, not very clever. It also doesn't call ! ListTogetherOrdinals. if (inventory_stage == 1) { count = ListTogetherCount(); print (EnglishNumber) count, " potatoes ("; total = 0; colnum = ListTogetherCount(ColorFilter, NopRed); if (colnum) { if (colnum == count) print "all"; else print (EnglishNumber) colnum; print " red"; total = total+colnum; } colnum = ListTogetherCount(ColorFilter, NopBlack); if (colnum) { if (total) print ", "; if (colnum == count) print "all"; else print (EnglishNumber) colnum; print " black"; total = total+colnum; } colnum = ListTogetherCount(ColorFilter, NopWhite); if (colnum) { if (total) print ", "; if (colnum == count) print "all"; else print (EnglishNumber) colnum; print " white"; total = total+colnum; } colnum = ListTogetherCount(ColorFilter, NopYellow); if (colnum) { if (total) print ", "; if (colnum == count) print "all"; else print (EnglishNumber) colnum; print " yellow"; total = total+colnum; } print ")"; rtrue; } ], parse_name BigEvilParseName, color 0, ordinal 0, visibly_distinct [ o1 o2; ! Two potatoes are distinct if they're different colors. if (o1.color ~= o2.color) rtrue; ], before [; Eat: "No eating the vegetables."; ]; ! ColorFilter: Filter procedure, used with ListTogetherCount. [ ColorFilter obj arg; return (obj.color == arg); ]; ! PotatoLeverClass: Pull a lever, create a potato. Class PotatoLeverClass with name 'lever', short_name [; print (name) self.color, " lever"; rtrue; ], before [ obj; Pull: obj = PotatoClass.create(self.color); if (~~obj) "The ceiling has run out of potatoes."; move obj to location; "Clunk -- ", (a) obj, " falls out of the ceiling."; ], has scenery; PotatoLeverClass redpotatolever Kitchen with name 'red', color NopRed; PotatoLeverClass blackpotatolever Kitchen with name 'black', color NopBlack; PotatoLeverClass whitepotatolever Kitchen with name 'white', color NopWhite; PotatoLeverClass yellowpotatolever Kitchen with name 'yellow', color NopYellow; Object garbagechute "garbage chute" Kitchen with name 'garbage' 'trash' 'chute', description "The chute is a dispository for surplus potatoes.", before [; Receive: if (noun ofclass PotatoClass) { print (The) noun, " vanishes into the chute"; PotatoClass.destroy(noun); "."; } "The chute only accepts potatoes."; ], has scenery; ! ---- ! Continuous substances ! These objects go in the workshop. ! BarrelClass: A barrel full of clay. Does not use BigEvilParseName or ! the color properties (NopRed, etc). Has no special behavior except to ! map "put X in barrel" commands to "put X in child(barrel)". ! (The command "take NOTIONAL from barrel" is automatically mapped to ! "take NOTIONAL from child(barrel)". This is not handled by the ! BarrelClass. The notional library code does it magically for containers/ ! supporters. Class BarrelClass with name 'barrel' 'barrels', short_name [ obj; obj = child(self); print (name) obj.color, " barrel"; rtrue; ], description [ obj; obj = child(self); "The barrel contains a large stock of ", (name) obj.color, " clay."; ], before [; Receive: <>; ], has scenery container open; ! The barrels. The red one starts in the workshop; the rest appear when ! the lever is pulled. BarrelClass redclaybarrel Workshop with name 'red'; BarrelClass orangeclaybarrel with name 'orange'; BarrelClass yellowclaybarrel with name 'yellow'; BarrelClass greenclaybarrel with name 'green'; BarrelClass blueclaybarrel with name 'blue'; BarrelClass purpleclaybarrel with name 'purple'; ! ClayStockClass: The stock of clay in a barrel. ! This does use BigEvilParseName, and the color and purity properties. ! (Purity is always NopPure.) It does not use weight. Since there's ! exactly one stock of each color, it doesn't need ordinals. Class ClayStockClass with name 'stock' 'of' 'clay', short_name [; print "stock of pure ", (name) self.color, " clay"; rtrue; ], parse_name BigEvilParseName, purity NopPure, check_extract [; ! Annoyingly, this code is identical to the before:ExtractFrom ! code, except that it doesn't print or act. if (noun.substance && noun.substance ~= NopClay) rfalse; if (noun.color && noun.color ~= self.color) rfalse; if (noun.purity && noun.purity ~= NopPure) rfalse; if (noun.quantunit && noun.quantunit ~= NopOunces) rfalse; if (noun.quantity == 0) rfalse; rtrue; ], before [ val obj; Take: val = random(5) + 3; obj = ClayLumpClass.create(val, self.color); if (~~obj) "The universe has run out of lumps."; move obj to player; PronounNotice(obj); NoticeImplicit(obj); "You scoop ", (name) obj, " from the barrel."; ExtractFrom: if (noun.substance && noun.substance ~= NopClay) return ExtractFromSub(); if (noun.color && noun.color ~= self.color) "That clay is not ", (name) noun.color, "."; if (noun.purity && noun.purity ~= NopPure) "That clay is pure ", (name) self.color, ", not ", (name) noun.purity, "."; if (noun.quantunit && noun.quantunit ~= NopOunces) "Clay is measured in ounces."; if (noun.quantity == 0) <>; obj = ClayLumpClass.create(noun.quantity, self.color); if (~~obj) "The universe has run out of lumps."; move obj to player; PronounNotice(obj); NoticeImplicit(obj); "You scoop ", (name) obj, " from the barrel."; Receive: if (~~(noun ofclass ClayLumpClass)) "The barrel is only for clay."; if (noun.color ~= self.color) "You shouldn't mix ", (name) noun.color, " in with the ", (name) self, "."; if (noun.purity ~= NopPure) "You shouldn't mix ", (name) noun.purity, " clay in with the ", (name) self, "."; print "You push ", (the) noun, " back into the barrel"; ClayLumpClass.destroy(noun); "."; ], has scenery; ! The six stocks of clay. ClayStockClass redclaystock redclaybarrel with color NopRed; ClayStockClass orangeclaystock orangeclaybarrel with color NopOrange; ClayStockClass yellowclaystock yellowclaybarrel with color NopYellow; ClayStockClass greenclaystock greenclaybarrel with color NopGreen; ClayStockClass blueclaystock blueclaybarrel with color NopBlue; ClayStockClass purpleclaystock purpleclaybarrel with color NopPurple; ! ClayLumpClass: A lump of clay. ! This is the showcase class for the whole mass-noun library, so it's nice ! that it's so simple. It's a dynamically created class. You set a lump's ! properties (color, weight, purity) when you create it. (It's legal to ! change a property later, but this risks causing the ordinal to automatically ! change -- if the new set of properties can be confused with another ! existing lump.) Class ClayLumpClass(16) with create [ wgt col pur; if (pur == 0) pur = NopPure; self.weight = wgt; self.color = col; self.purity = pur; ! Clear out any lingering ordinal value from previous existence. self.ordinal = 0; ], short_name [; if (self.ordinal) { print (OrdinalNumber) self.ordinal, " "; } print (EnglishNumber) self.weight, " ounce"; if (self.weight ~= 1) print "s"; print " of "; if (self.purity) { ! Clever hack: if number_of_classes is nonzero, we are in ! a disambig question. if (self.purity ~= NopPure || number_of_classes ~= 0) print (name) self.purity, " "; } print (name) self.color, " "; print "clay"; rtrue; ], name 'clay', weight 0, color 0, purity 0, ordinal 0, parse_name BigEvilParseName, visibly_distinct [ o1 o2; ! Two lumps are distinct if they differ in any property. (Except ! location.) if (o1.color ~= o2.color) rtrue; if (o1.purity ~= o2.purity) rtrue; if (o1.weight ~= o2.weight) rtrue; ], check_extract [; ! Annoyingly, this code is identical to the before:ExtractFrom ! code, except that it doesn't print or act. if (noun.substance && noun.substance ~= NopClay) rfalse; if (noun.color && noun.color ~= self.color) rfalse; if (noun.purity && noun.purity ~= self.purity) rfalse; if (noun.quantunit && noun.quantunit ~= NopOunces) rfalse; if (noun.quantity == 0) rfalse; if (noun.quantity == self.weight) rfalse; if (noun.quantity > self.weight) rfalse; rtrue; ], before [ obj; ExtractFrom: if (noun.substance && noun.substance ~= NopClay) return ExtractFromSub(); if (noun.color && noun.color ~= self.color) "That clay is not ", (name) noun.color, "."; if (noun.purity && noun.purity ~= self.purity) "That clay is ", (name) self.purity, " ", (name) self.color, ", not ", (name) noun.purity, "."; if (noun.quantunit && noun.quantunit ~= NopOunces) "Clay is measured in ounces."; ! The notional object doesn't have a weight; it has a quantity. ! We have established that the quantity unit is either ounces ! or nothing. (Which are equivalent here -- we allow both "take ! 5 ounces" and "take 5 clay".) if (noun.quantity == 0) "You'll have to say how much to take."; if (noun.quantity == self.weight) { print "That would be the whole"; if (self.weight == 1) print " one ounce"; else print " ", (EnglishNumber) self.weight, " ounces"; " of clay."; } if (noun.quantity > self.weight) { print "There"; if (self.weight == 1) print " is only one ounce"; else print " are only ", (EnglishNumber) self.weight, " ounces"; " to begin with."; } obj = ClayLumpClass.create(noun.quantity, self.color, self.purity); if (~~obj) "The universe has run out of lumps."; move obj to player; PronounNotice(obj); NoticeImplicit(obj); print "You pull ", (name) obj, " from ", (the) self; self.weight = self.weight - noun.quantity; ", leaving ", (name) self, "."; Receive: if (~~(noun ofclass ClayLumpClass)) "You can mush one lump of clay into another one. Don't get fancy with anything else."; obj = MergeLumps(self, noun); if (~~obj) "The universe has run out of lumps."; move obj to parent(self); PronounNotice(obj); NoticeImplicit(obj); ClayLumpClass.destroy(noun); ClayLumpClass.destroy(self); "You blend the pieces of clay together, forming ", (name) obj, "."; Squeeze, Eat: "Go ahead, play with the clay instead of my clever parsing tricks. Sniff."; ], has quantarticles; Object claychute "garbage chute" Workshop with name 'garbage' 'trash' 'chute', description "The chute is a dispository for surplus clay.", before [; Receive: if (noun ofclass ClayLumpClass) { print (The) noun, " vanishes into the chute"; ClayLumpClass.destroy(noun); "."; } "The chute only accepts lumps of clay."; ], has scenery; ! MergeLumps: Create a new lump of clay, whose properties are those created ! by smooshing two existing lumps together. (This function does not destroy ! the two existing lumps.) ! This contains a lot of fiddly case-based and computational code for ! mixing colors. I won't bother explaining it, because it doesn't ! matter. [ MergeLumps o1 o2 val wgt ratio cdiff col pur lim; ! Make sure o1 is the larger lump if (o2.weight > o1.weight) { val = o2; o2 = o1; o1 = val; } wgt = o1.weight+o2.weight; ratio = o1.weight / o2.weight; if (o1.color == NopGrey) { col = NopGrey; jump mergefinal; } col = o1.color; pur = o1.purity; if (o2.color == NopGrey) { switch (ratio) { 0, 1: val = 3; 2, 3: val = 2; default: val = 1; } val = o1.purity.number + val; if (val > NopMuddy.number) col = NopGrey; else pur = PurityByValue(val); jump mergefinal; } cdiff = o1.color.number - o2.color.number; if (cdiff < 0) cdiff = (-cdiff); if (cdiff > 3) cdiff = 6-cdiff; switch (cdiff) { 0: switch (ratio) { 0, 1: lim = 4; 2, 3: lim = 2; default: lim = 1; } val = o1.purity.number; if (o2.purity.number > val) val = o2.purity.number; if (val > o1.purity.number + lim) val = o1.purity.number + lim; if (val > NopMuddy.number) col = NopGrey; else pur = PurityByValue(val); 1: switch (ratio) { 0, 1: lim = 4; 2, 3: lim = 2; default: lim = 1; } val = o1.purity.number; if (o2.purity.number+1 > val) val = o2.purity.number+1; if (val > o1.purity.number + lim) val = o1.purity.number + lim; if (val > NopMuddy.number) col = NopGrey; else pur = PurityByValue(val); 2: if (ratio >= 4) { val = o1.purity.number + 1; } else { val = o1.purity.number; if (o2.purity.number > val) val = o2.purity.number; if (ratio >= 2) val = val+1; if (col == NopGreen or NopOrange or NopPurple) val = val+2; if ((o1.color.number - o2.color.number) == 2 or -2) { col = (o1.color.number + o2.color.number) / 2; } else { if (o1.color.number == 0 or 4) col = 5; else col = 0; } col = ColorByValue(col); } if (val > NopMuddy.number) col = NopGrey; else pur = PurityByValue(val); 3: switch (ratio) { 0, 1: val = 4; 2, 3: val = 3; 4,5,6,7: val = 2; default: val = 1; } val = o1.purity.number + val; if (val > NopMuddy.number) col = NopGrey; else pur = PurityByValue(val); default: print "[BUG] MergeLumps: cdiff is ", cdiff, ".^"; rfalse; } .mergefinal; if (col == NopGrey) pur = NopPure; return ClayLumpClass.create(wgt, col, pur); ]; ! bucket: A bucket full of ball bearings. Like the BarrelClass, it is ! very simple. Object bucket "bucket" Workshop with name 'bucket', description "The bucket contains a bucketful of ball bearings. A tiny label on the bucket reads ~Ten-weight ball bearings (0.1 ounce each)~.", before [; Receive: <>; ], has scenery container open; ! ballstock: The stock of ball bearings in the bucket. ! This uses BigEvilParseName, but only so that it can understand ! "the balls in the bucket". Object ballstock "bucketful of ball bearings" bucket with name 'bucketful' 'of' 'ball' 'balls' 'bearings' 'metal', parse_name BigEvilParseName, description [; <>; ], check_extract [; ! Annoyingly, this code is identical to the before:ExtractFrom ! code, except that it doesn't print or act. if (noun.substance && noun.substance ~= NopMetal) rfalse; if (noun.color || noun.purity) rfalse; if (noun.quantunit && noun.quantunit ~= NopOunces) rfalse; if (noun.quantity == 0) rfalse; rtrue; ], before [ val obj; Take: val = random(5) + 10; obj = BallClass.create(val); if (~~obj) "The universe has run out of ball bearings."; move obj to player; PronounNotice(obj); "You scoop ", (name) obj, " from the bucket."; ExtractFrom: if (noun.substance && noun.substance ~= NopMetal) return ExtractFromSub(); if (noun.color || noun.purity) "There is only one color of ball bearing."; if (noun.quantunit && noun.quantunit ~= NopOunces) "Ball bearings are not measured in ", (name) noun.quantunit, "."; if (noun.quantity == 0) <>; if (noun.quantunit == NopOunces) val = noun.quantity * 10; else val = noun.quantity; obj = BallClass.create(val); if (~~obj) "The universe has run out of ball bearings."; move obj to player; PronounNotice(obj); NoticeImplicit(obj); "You scoop ", (name) obj, " from the bucket."; Receive: if (~~(noun ofclass BallClass)) "The bucket is only for ball bearings."; print "You pour ", (the) noun, " back into the bucket"; BallClass.destroy(noun); "."; ], has scenery; ! BallClass: A handful of ball bearings. ! This is very similar to ClayLumpClass. The only interesting difference ! is that, at extraction time, "2 ounces of balls" is considered to mean ! "20 balls". Class BallClass(16) with create [ quan; self.quantity = quan; if (self.quantity == 1) give self ~pluralname; ! Clear out any lingering ordinal value from previous existence. self.ordinal = 0; ], short_name [; if (self.ordinal) { print (OrdinalNumber) self.ordinal, " "; } if (self.quantity <= 20) print (EnglishNumber) self.quantity; else print self.quantity; print " ball bearing"; if (self.quantity > 1) print "s"; rtrue; ], name 'metal' 'ball' 'balls' 'bearing' 'bearings', quantity 0, ordinal 0, parse_name BigEvilParseName, visibly_distinct [ o1 o2; if (o1.quantity ~= o2.quantity) rtrue; ], check_extract [ val; ! Annoyingly, this code is identical to the before:ExtractFrom ! code, except that it doesn't print or act. if (noun.substance && noun.substance ~= NopMetal) rfalse; if (noun.color || noun.purity) rfalse; if (noun.quantunit && noun.quantunit ~= NopOunces) rfalse; if (noun.quantity == 0) rfalse; if (noun.quantunit == NopOunces) val = noun.quantity * 10; else val = noun.quantity; if (val == self.quantity) rfalse; if (val > self.quantity) rfalse; rtrue; ], before [ obj val; ExtractFrom: if (noun.substance && noun.substance ~= NopMetal) return ExtractFromSub(); if (noun.color || noun.purity) "There is only one color of ball bearing."; if (noun.quantunit && noun.quantunit ~= NopOunces) "Ball bearings are not measured in ", (name) noun.quantunit, "."; if (noun.quantity == 0) "You'll have to say how much to take."; if (noun.quantunit == NopOunces) val = noun.quantity * 10; else val = noun.quantity; if (val == self.quantity) { if (self.quantity == 1) "There's only one ball bearing there."; "That would be all ", (EnglishNumber) self.quantity, " ball bearings."; } if (val > self.quantity) { if (self.quantity == 1) "There's only one ball bearing there."; "There are only ", (EnglishNumber) self.quantity, " ball bearings there."; } obj = BallClass.create(val); if (~~obj) "The universe has run out of ball bearings."; move obj to player; PronounNotice(obj); NoticeImplicit(obj); print "You pull ", (name) obj, " from ", (the) self; self.quantity = self.quantity - val; if (self.quantity == 1) give self ~pluralname; ", leaving ", (name) self, "."; Receive: if (~~(noun ofclass BallClass)) "You can combine ball bearings together. Don't get fancy with anything else."; obj = BallClass.create(self.quantity + noun.quantity); if (~~obj) "The universe has run out of ball bearings."; move obj to parent(self); PronounNotice(obj); NoticeImplicit(obj); BallClass.destroy(noun); BallClass.destroy(self); "You collect ", (the) obj, " together."; ], has quantarticles pluralname; ! ---- ! Grammar. (Library-level code.) ! Grammar lines with NotionalToken are always after the equivalent line ! for real objects. Reality takes precedence. Include "Grammar"; Verb 'about' 'help' * -> About * 'bug'/'bugs' -> AboutBugs; Verb 'bug' 'bugs' * -> AboutBugs; Extend 'take' replace ! and 'carry' 'hold' * multi -> Take * NotionalToken -> ExtractVague * 'off' worn -> Disrobe * multiinside 'from' noun -> Remove * multiinside 'off' noun -> Remove * NotionalToken 'from' noun -> Extract * 'inventory' -> Inv; Extend 'get' replace * 'out'/'off'/'up' -> Exit * multi -> Take * NotionalToken -> ExtractVague * 'in'/'into'/'on'/'onto' noun -> Enter * 'off' noun -> GetOff * multiinside 'from' noun -> Remove * NotionalToken 'from' noun -> Extract; Extend 'drop' replace ! and 'discard' 'throw' * multiheld -> Drop * multiexcept 'in'/'into'/'down' noun -> Insert * multiexcept 'on'/'onto' noun -> PutOn * held 'at'/'against'/'on'/'onto' noun -> ThrowAt * NotionalToken -> ExtractDrop; Extend 'put' replace * multiexcept 'in'/'inside'/'into' noun -> Insert * multiexcept 'on'/'onto' noun -> PutOn * NotionalToken 'in'/'inside'/'into' noun -> ExtractInsert * NotionalToken 'on'/'onto' noun -> ExtractPutOn * 'on' held -> Wear * 'down' multiheld -> Drop * multiheld 'down' -> Drop; Extend 'remove' replace * held -> Disrobe * multi -> Take * NotionalToken -> ExtractVague * multiinside 'from' noun -> Remove; Verb 'extract' 'scoop' * NotionalToken -> ExtractVague * multiinside 'from' noun -> Remove * NotionalToken 'from' noun -> Extract; Verb 'mix' 'blend' 'combine' * noun 'with'/'into'/'onto' noun -> Insert * NotionalToken 'with'/'into'/'onto' noun -> ExtractInsert; Verb 'notional' 'notion' * NotionalToken -> DebugNotional; ! ---- ! Placards and "about" messages ! emph: print a string in italics/underline [ emph str; style underline; print (string) str; style roman; ]; ! InitialMessage: Anteroom placard (and "about" text) [ InitialMessage; "This is a worked-out example of my theory about how to handle mass nouns -- water, sand, rope -- in IF.^^ My theory is this: there is usually a sensible way to divide up the substance, such that each unit is an IF-style ~thing~. That is, the player will want to refer to one unit per command. (I should say, the player will want to refer to one unit per noun phrase. ~Put X in Y~ is two noun phrases.) There may be water in the bottle, a puddle of water on the floor, and a lake nearby. Each of those should be one IF object.^^ That much is not controversial. The reason this falls apart in implementation is this: the player can't figure out how to ", (emph) "refer", " to a particular IF object. They're all called ~water~.^^ This problem should be solvable. The whole point of having several units of a substance in a game -- presumably! -- is that they ", (emph) "differ", " in some way. Location, form, quantity, temperature, color. The various bits of water have properties, and these properties distinguish them.^^ So what we want is a system that will (1) make it clear to the player what properties a unit has, and (2) allow the player to refer to ", (emph) "any", " of those properties. And also (3) if two units are truly identical, distinguish them anyway as ~first~, ~second~, etc. so that the player can still refer to them.^^ I have two examples here so far. The kitchen, to the south, demonstrates point (3). The workshop, to the east, demonstrates (1) and (2). See the placards in each room for more detail.^^ Type BUGS for a list of the bugs I know of."; ]; [ KitchenMessage; "Here we see a bunch of identical objects: onions, turnips, potatoes. (Pull the levers for potatoes.)^^ Onions aren't a mass noun, but they suffer from the problem of multiple identical units. (Which can easily occur with mass nouns, if you allow the player to dispense or mix quantities freely.) Here we undertake two solutions.^^ First, the player can distinguish items by their location. The parser accepts ~the turnip in the chest~, ~the onion in the cabinet~, and so on. To aid this, examining any object which is in a container will display a message like ~(the plain onion, which is in the cabinet)~.^^ Second, if two items are indistinguishable, the parser automatically assigns them ordinal adjectives: ~first~, ~second~, ~third~. I did not number particular onions in the source code. The numbers are magically assigned to items as the player encounters them. (You can test this by opening the chest and the cabinet in whichever order you want.)^^ (I threw in a red onion just to test the case of an object which ", (emph) "is", " distinguishable.)^^ This algorithm is not entirely satisfactory. I decided that numbers should stick to items, once assigned. So you can leave the room and still be carrying ~a third turnip~. This seemed less confusing than having numbers constantly jump around; but it leads to awkward messages. Objects also get listed out of numerical order, which is confusing. Nonetheless, the system allows you to easily manipulate vegetables.^^ One more bug: the magic number-assignment occurs between commands (or when you enter a room). This means that if you open a container, or pull a potato lever, the game does not tell you the ordinal of the object you discover. There ", (emph) "is", " no ordinal, in fact, until your next command is parsed. So you tend to waste a turn examining things."; ]; [ WorkshopMessage; "I decided not to use liquids for my first continuous substance. Liquids have to be in containers. This is a whole messy problem which is not relevant to my point. (Although, as I said, ~the water in the bottle~ should be a valid noun phrase.)^^ So, I have here a lot of clay. Clay has color, weight, and purity. You can refer to ~yellow clay~, ~three ounces of clay~, ~4 oz red clay~, ~ounce of muddy clay~, ~pure red clay on the table~, and so on. (If you see clay that doesn't say how pure it is, it's ~pure~.) For variety, there are also ball bearings, which come by piece instead of by weight. (Although you can also ask for them by weight, if you want.)^^ This allows you to manipulate the objects you see. But wait! I have three more tricks for you.^^ First: the parser understands comparative adjectives. You can say ~take the largest clay~, or ~the dullest yellow clay~, or ~the smallest clay on the table~.^^ Second: you can use ", (emph) "notional", " objects with ~take~. The parser understands ~take five ounces from barrel~, even though no object in the world (yet) weighs five ounces. You can also take material from a lump of clay, effectively dividing it into two lumps. (And mix lumps back together. This doesn't demonstrate any parser tricks, but it's fun.)^^ Third: you can use notional objects with ~put~ and ~mix~, too. ~put five ounces of red clay on table~ will implicitly take five ounces of red clay -- if there's a source for it -- and then put it on the table.^^ The notional object parser is very general. You can refer to ~five ounces~, ~five ounces of clay~, ~5 oz of muddy clay~, ~muddy clay~, ~yellow metal~, and so on. The parser understands that these are all different property specifications. The objects know what divisions make sense and what don't.^^ (Note that if two identical bits of clay (or pools of ball bearings) are visible, the parser assigns ordinal numbers ~first~, ~second~. See the kitchen for more information on this.)"; ]; [ BugsMessage; "Known bugs: (some of which I may even try to fix):^^ This is not efficient code. I am told that Rezrov (a terp written in Perl) gets laggy if there are more than a few dynamic objects in scope. I suspect ApplyOrdinals.^^ ~Take one ounce of muddier clay~ does not do what you expect. It is parsed the same as ~take muddier one ounce of clay~ -- that is, it looks for the muddiest piece of clay which weighs one ounce. In the initial kitchen setup, this is the one-ounce pure yellow lump! Instead, try ~take one ounce of muddy clay~.^^ ~Take one ounce of X~ never asks for a source (~What do you want to take X from?~) It used to, but I broke it, and now it automatically chooses a source. It's clever enough to choose a source which matches the request, but not clever enough to ask for disambiguation if there's more than one. That's hard in Inform; sorry.^^ I broke (in fact, excised) the standard Inform library handling of numbers in commands. So ~take five onions~, which works by default in Inform, does not work here.^^ There is a long-standing Inform behavior (or bug), where the library likes to deprecate objects on supporters. So, in the initial setup, ~x 3~ will default to the three-ounce object which is ", (emph) "not", " on the table. I would prefer it to query for disambiguation, but I haven't tried to change the library behavior here.^^ The library also deprecates scenery objects, such as the stock of pure red clay in the barrel. Again, I haven't tried to fix it.^^ Why, yes, there ", (emph) "are", " sometimes two ~(the whatever...)~ clarifications printed.^^ It would be nice to accept ~take any onion~ -- replicate the original Inform behavior for identical objects, which was to silently pick one.^^ ~Combine the three ounces of pure yellow clay with the three ounces of muddy yellow clay~ generates array overflow warnings. I have no clue.^^ (Further bugs and comments welcome by email.)"; ];