| !% -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 : jx<ix-1 : jx++) { | |
| val = func(arr-->jx, 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 : ix<scope_count : ix++) { | |
| obj = scope_list-->ix; | |
| 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 : i<INPUT_BUFFER_LEN : i++) buffer2->i = ' '; | |
| #Endif; ! TARGET_ZCODE | |
| answer_words=Keyboard(buffer2, parse2); | |
| ! Conveniently, parse2-->1 is the first word in both ZCODE and GLULX. | |
| first_word = (parse2-->1); | |
| ! Take care of "all", because that does something too clever here to do | |
| ! later on: | |
| if (first_word == ALL1__WD or ALL2__WD or ALL3__WD or ALL4__WD or ALL5__WD) { | |
| if (context == MULTI_TOKEN or MULTIHELD_TOKEN or MULTIEXCEPT_TOKEN or MULTIINSIDE_TOKEN) { | |
| l = multiple_object-->0; | |
| for (i=0 : i<number_matched && l+i<63 : i++) { | |
| k = match_list-->i; | |
| 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<l : i++) buffer->(k+i) = buffer2->(2+i); | |
| buffer->(k+l-1) = ' '; | |
| buffer->1 = buffer->1 + l; | |
| if (buffer->1 >= (buffer->0 - 1)) buffer->1 = buffer->0; | |
| #Ifnot; ! TARGET_GLULX | |
| k = WordAddress(match_from) - buffer; | |
| l = (buffer2-->0) + 1; | |
| for (j=buffer+INPUT_BUFFER_LEN-1 : j>=buffer+k+l : j--) j->0 = j->(-l); | |
| for (i=0 : i<l : i++) buffer->(k+i) = buffer2->(WORDSIZE+i); | |
| buffer->(k+l-1) = ' '; | |
| buffer-->0 = buffer-->0 + l; | |
| if (buffer-->0 > (INPUT_BUFFER_LEN-WORDSIZE)) buffer-->0 = (INPUT_BUFFER_LEN-WORDSIZE); | |
| #Endif; ! TARGET_ | |
| ! Having reconstructed the input, we warn the parser accordingly | |
| ! and get out. | |
| 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 : i<INPUT_BUFFER_LEN : i++) buffer2->i=' '; | |
| #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 : j<pcount : j++) { | |
| if (pattern-->j == PATTERN_NULL) continue; | |
| #Ifdef TARGET_ZCODE; | |
| i = 2+buffer->1; (buffer->1)++; buffer->(i++) = ' '; | |
| #Ifnot; ! TARGET_GLULX | |
| i = WORDSIZE + buffer-->0; | |
| (buffer-->0)++; buffer->(i++) = ' '; | |
| #Endif; ! TARGET_ | |
| #Ifdef DEBUG; | |
| if (parser_trace >= 5) print "[Gluing in inference 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 : l<i+k : l++) buffer->l = 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 : j<buffer2->1 : i++,j++) { | |
| buffer->i = buffer2->(j+2); | |
| (buffer->1)++; | |
| if (buffer->1 == INPUT_BUFFER_LEN) break; | |
| } | |
| #Ifnot; ! TARGET_GLULX | |
| i = WORDSIZE + buffer-->0; | |
| (buffer-->0)++; buffer->(i++) = ' '; | |
| for (j=0 : j<buffer2-->0 : i++,j++) { | |
| buffer->i = buffer2->(j+WORDSIZE); | |
| (buffer-->0)++; | |
| if (buffer-->0 == INPUT_BUFFER_LEN) break; | |
| } | |
| #Endif; ! TARGET_ | |
| ! (3) we fill up the buffer with spaces, which is unnecessary, but may | |
| ! help incorrectly-written interpreters to cope. | |
| #Ifdef TARGET_ZCODE; | |
| for (: i<INPUT_BUFFER_LEN : i++) buffer->i = ' '; | |
| #Endif; ! TARGET_ZCODE | |
| 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<num_words : i++) { | |
| #Ifdef TARGET_ZCODE; | |
| j = parse-->(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 : m<l : m++) print (char) k->m; 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 : i<INPUT_BUFFER_LEN : i++) buffer->i = buffer3->i; | |
| jump ReParse; | |
| } | |
| ! Save the present input in case of an "again" next time | |
| if (verb_word ~= AGAIN1__WD) | |
| for (i=0 : i<INPUT_BUFFER_LEN : i++) buffer3->i = 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 <one or more prepositions> 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 (: i<j : i++) i->0 = ' '; | |
| i = NextWord(); | |
| if (i == AGAIN1__WD or AGAIN2__WD or AGAIN3__WD) { | |
| ! Delete the words "then again" from the again buffer, | |
| ! in which we have just realised that it must occur: | |
| ! prevents an infinite loop on "i. again" | |
| i = WordAddress(wn-2)-buffer; | |
| if (wn > num_words) j = INPUT_BUFFER_LEN-1; | |
| else j = WordAddress(wn)-buffer; | |
| for (: i<j : i++) buffer3->i = ' '; | |
| } | |
| 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) { | |
| <<Insert obj second>>; | |
| } | |
| "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) { | |
| <<PutOn obj second>>; | |
| } | |
| "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) { | |
| <<Drop 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 : ix<scope_count : ix++) { | |
| obj = scope_list-->ix; | |
| if (obj provides check_extract && obj.check_extract()) { | |
| print "(extracting ", (the) NotionalObject, " from ", | |
| (the) obj, ")^"; | |
| <Extract noun 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 : jx<scope_count : jx++) { | |
| obj2 = scope_list-->jx; | |
| 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 : ix<len : ix++) { | |
| o1 = arr-->ix; | |
| give o1 ~ao_handling; | |
| give o1 ~ao_handled; | |
| } | |
| for (ix=0 : ix<len : ix++) { | |
| o1 = arr-->ix; | |
| if (o1 has ao_handled) | |
| continue; | |
| give o1 ao_handling; | |
| num = 1; | |
| for (jx=0 : jx<len : jx++) { | |
| if (ix==jx) | |
| continue; | |
| o2 = arr-->jx; | |
| 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 : jx<len : jx++) { | |
| o2 = arr-->jx; | |
| 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 : ix<len : ix++) { | |
| obj = arr-->ix; | |
| 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 (jx<len) { | |
| o2 = arr-->jx; | |
| jx++; | |
| if (o2 hasnt ao_handling) | |
| continue; | |
| if (o2 hasnt ao_keepval) | |
| continue; | |
| else { | |
| nextval = o2.ordinal; | |
| break; | |
| } | |
| } | |
| for (ix=0 : ix<len : ix++) { | |
| obj = arr-->ix; | |
| 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 (jx<len) { | |
| o2 = arr-->jx; | |
| 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 <whatever> 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 : ix<compar_count : ix++) { | |
| comp2 = compar_list-->ix; | |
| 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 : ix<number_matched : ix++) { | |
| ! obj = match_list-->ix; | |
| ! 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 : ix<number_matched : ix++) { | |
| obj = match_list-->ix; | |
| if (obj == -1) | |
| continue; | |
| match_list-->jx = obj; | |
| jx++; | |
| } | |
| number_matched = jx; | |
| !print "After selecting: ", number_matched, " left in list:^"; | |
| !for (ix=0 : ix<number_matched : ix++) { | |
| ! obj = match_list-->ix; | |
| ! 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 <whatever> 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 : ix<len : ix++) { | |
| obj = arr-->ix; | |
| 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 : ix<len : ix++) { | |
| obj = arr-->ix; | |
| 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 : ix<len : ix++) { | |
| obj = arr-->ix; | |
| 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 : ix<len : ix++) { | |
| obj = arr-->ix; | |
| 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 : ix<len : ix++) { | |
| obj = arr-->ix; | |
| 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 : ix<len : ix++) { | |
| obj = arr-->ix; | |
| 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 : ix<len : ix++) { | |
| obj = arr-->ix; | |
| 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 : ix<len : ix++) { | |
| obj = arr-->ix; | |
| 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 : ix<len : ix++) { | |
| obj = arr-->ix; | |
| 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 : ix<len : ix++) { | |
| obj = arr-->ix; | |
| 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 : ix<len : ix++) { | |
| obj = arr-->ix; | |
| 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 : ix<len : ix++) { | |
| obj = arr-->ix; | |
| 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: | |
| <<Insert noun child(self)>>; | |
| ], | |
| 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) | |
| <<Take self>>; | |
| 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: | |
| <<Insert noun child(self)>>; | |
| ], | |
| 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 [; | |
| <<Examine bucket>>; | |
| ], | |
| 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) | |
| <<Take self>>; | |
| 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.)"; | |
| ]; | |
Xet Storage Details
- Size:
- 132 kB
- Xet hash:
- 0ea7ff5745940e646e888a00413dc5fbdad1f19f4db504c0b5dfa600e3b7756d
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.