Version 4 of Interactive Parsing (for Glulx only) by Jon Ingold begins here. "Provides real-time suggestions for the player's input based on what they've typed so far, using the parser itself to ensure these suggestions are sensible." [ ISSUES: * Auto-build two-letter verb table (Might be slow? Might not help much?) * Ambiguous input goes to the main screen, followed by a prompt for more info. Would be better if this prompt was in the input line. * Similarly, would be better if parser errors stayed on the input line too. * Todo -- Instead of a "Table of boosted words", it would be better to use a "swap" list to reorder the entire dictionary list, so word A is simply encountered before, and takes precedence over word B. ] [ NOTES: Print Inferred Command - - currently clobbered by a "clarifying the parser's choice of..." rule - should actually print inference in blue after typed text, if all words AOK etc. - (But shouldn't print PRESS RETURN if there's an inference???) ] Book 1 - Establish System Chapter 1 - Set up the Game Interface Section 1 - Inclusions Include Basic Screen Effects by Emily Short. Include Flexible Windows by Jon Ingold. Include Text Window Input-Output Control by Erik Temple. Section 1a - Options Use interactive parsing override translates as (- Constant USE_INTERACTIVE_PARSER_OVERRIDE; -). Use debug advance parsing translates as (- Constant DEBUG_ADVANCE_PARSING; -). Use show interim parser errors translates as (- Constant SHOW_PARSER_ERRORS; -). Use long word check report translates as (- Constant LONG_WORD_CHECK_REPORT; -). [ Use debug advance parsing. ] Section 1b - Constants Use maximum dictionary size of at least 1000 translates as (- Constant MAX_WORDS = {N}; -). Use average word length of at least 6 translates as (- Constant AV_WORD_LENGTH = {N}; -). Use maximum words in scope of at least 256 translates as (- Constant MAX_WORD_IN_SCOPE = {N}; -). Use maximum verb words of at least 300 translates as (- Constant MAX_VERB_NAMES = {N}; -). Use maximum compass names of at least 35 translates as (- Constant MAX_COMPASS_NAMES = {N}; -). Section 2 - Set up basic windows The key-window is a text-grid g-window spawned by the main-window. The position of the key-window is g-placebelow. The measurement of the key-window is 10. The border-window is a graphics g-window spawned by the main-window. The position is g-placebelow. The scale method is g-fixed-size. The measurement is 2. The back-colour is g-dark-grey. When play begins: if the interactive parsing override option is inactive: open up the key-window as the main text input window; [ The border window has been disabled so that games work for Quixe. ] [ open up the border-window;] Section 3 - Using Text IO Control to read in command in a sub-window Before printing the command prompt when the current text input window is the key-window: clear the current text input window. Before reading a command when the current action is restarting the game or the current action is quitting the game: say line break. After reading a command when not Inputstyling: shut down the echo stream of the main-window; say ">[player's command in upper case][command clarification break]"; echo the stream of the main-window to the transcript. The command-pasting terminator is "[run paragraph on][if we are writing a transcript][echo stream of current text input window][line break][stream of current text input window][end if]". Section 3b - Line breaks after parser interruptions After printing a parser error: say line break. After asking which do you mean: say line break. Section 4 - Toggling Input Style Inputstyling is an action out of world. Understand "inputchange" as Inputstyling. Carry out Inputstyling: if the interactive parsing override option is inactive: if the current text input window is the main-window: open up the key-window as the main text input window; else: now the current text input window is the main-window; shut down the key-window. Chapter 2 - Hook into new input system Section 1 - Keyboard Primitive diverted to a new input routine Include (- [ KeyboardPrimitive a_buffer a_table; #Ifdef DEBUG; #Iftrue ({-value:NUMBER_CREATED(test_scenario)} > 0); return TestKeyboardPrimitive(a_buffer, a_table); #Endif; #Endif; ! We should only use this input routine if our mode is on. ! Incidentally, we should be careful with Disambiguation questions, too, which will come through here. #ifndef USE_INTERACTIVE_PARSER_OVERRIDE; if ((+current text input window+) == (+key-window+)) return New_Key_Input(a_buffer, a_table); else return VM_ReadKeyboard(a_buffer, a_table); #ifnot; return VM_ReadKeyboard(a_buffer, a_table); #endif; ]; -) instead of "Keyboard Primitive" in "Parser.i6t". Section 2 - Read in the input line using keystrokes Include (- Global prompted_word = 0; Global on_screen_suggestion = 0; Constant PENDING = 1; Constant PROCESSING = 2; Constant PERFORMED = 3; Global current_analysis_stage = PENDING; Constant TIME_SKIP = 1; Constant MAX_TESTS = 50; Global interrupt_keypress = 0; Global cursor_word = 0; Global cursor_position = 0; [ New_Key_Input a_buffer a_table key terminated rv character_press_engaged ; InitialiseCommandRecall(); !#ifdef SCOPE_STACK_OPTIMISATION; ClearStoredScope(); ClearMatchLists(); !#endif; .reDo; ! set up the line : initialise variables terminated = false; cursor_position = 0; a_buffer-->0 = 0; ! set up the window: clear prompts and suggestions glk_cancel_char_event((+key-window+).ref_number); ! build some quick look-up tables of verbs, in-scope nouns and prepositions, and the compass ! The compass list could be built once at the start of the game, but it's fast, and done here ! in case the game changes the names of directions at any point. WorkThroughScope(location, word_in_scope_list, AddThe, MAX_WORD_IN_SCOPE, EX_PREPOSITION, 0, "visible scoped nouns", "maximum words in scope"); WorkThroughScope(Compass, word_for_verb_list, 0, MAX_VERB_NAMES, EX_VERB, animate, "verb words", "maximum verb words"); WorkThroughScope(Compass, word_in_compass_list, 0, MAX_COMPASS_NAMES, EX_NONE, 0, "compass names", "maximum compass names"); ErasePreviousSuggestions(); ResetRunningChecks(); cursor_position = ApplyEditPoint(a_buffer); if (cursor_position == 0) { ClearPromptWindow(); PrintPromptInWindow(); } ! set up - request a character event glk_request_char_event((+key-window+).ref_number); ! begin outer loop - reading in the line while(~~terminated) { ! inner loop - wait for something to happen while(true) { ! if we're here from a keypress event that occurred during processing. ! We "fake" keypress input if (interrupt_keypress ~= 0) { ! write in a keypress event gg_event-->0 = evtype_CharInput; gg_event-->2 = interrupt_keypress; interrupt_keypress = 0; character_press_engaged = true; } else { ! Otherwise, stop and wait for an event CorrectlyPositionCursor(); glk_select(gg_event); } ! Event 1 - The Timer has pinged. ! Let's go off and do some more analysis if (gg_event-->0 == evtype_Timer) { ! Do some analysis if (current_analysis_stage ~= PERFORMED) SuggestionAnalysis(a_buffer); ! if we're done, cancel the timer if (current_analysis_stage == PERFORMED) glk_request_timer_events(0); } ! Event 2 - Window resize else if (gg_event-->0 == evtype_Arrange) { DrawStatusLine(); ClearPromptWindow(); PrintPromptInWindow(); on_screen_suggestion = 0; break; } else ! Event 3 - A Key was Pressed if (gg_event-->0 == evtype_CharInput) { key = gg_event-->2; ! get the character pressed ! awaiting_keypress = false; ! Different keys require different things switch(key) { ! a newline 13, 250, keycode_Return : CompleteSuggestionAnalysis (a_buffer); rv = WriteInSuggestion(a_buffer, cursor_position, true); if (rv > -1) cursor_position = rv - 1; ! -1 because we don't want to put the cursor *after* the last character ! glk_set_window((+main-window+).ref_number); ! print "Written in suggestion makes line ", rv, " characters long.^"; terminated = true; ! escape key keycode_Escape: if (cursor_position > 0) { InsertCharacterAt(' ', a_buffer, cursor_position); !a_buffer->(WORDSIZE + type_position) = ' '; ResetRunningChecks(); BlankSuggestion(); cursor_position++; } ! a space 32: CompleteSuggestionAnalysis (a_buffer); rv = WriteInSuggestion(a_buffer, cursor_position, true); if (rv > -1) { cursor_position = rv; } else if (cursor_position > 0) { InsertCharacterAt(' ', a_buffer, cursor_position); cursor_position ++; } ! TODO: moving the cursor drastically affects the suggestion system ! eventually... ! we need to ensure that the word the player's looking at == the word we're suggesting for ! right arrow keycode_Right: if (cursor_position < a_buffer-->0) cursor_position++; ! left arrow keycode_Left: if (cursor_position > 0) cursor_position--; ! end keycode_End: cursor_position = a_buffer-->0; ! home keycode_Home: cursor_position = 0; ! Tab 253, keycode_Tab, 9: ! forces auto-complete ! (so like space, but will write in suggestion ! even if the current word itself is recognised as in the dictionary) CompleteSuggestionAnalysis (a_buffer); rv = WriteInSuggestion(a_buffer, cursor_position, true); if (rv > -1) cursor_position = rv; ! redundant keys are silent keycode_Func1, keycode_Func2,keycode_Func3,keycode_Func4, keycode_Func5,keycode_Func6,keycode_Func7,keycode_Func8, keycode_Func9,keycode_Func10,keycode_Func11,keycode_Func12: ! up arrow - I intend to use this for command recall, later 252, keycode_Up, keycode_PageUp: rv = RecallPreviousCommandLine(a_buffer); if (rv > -1) { cursor_position = rv; ResetRunningChecks(false); BlankSuggestion(); ClearPromptWindow(); PrintPromptInWindow(); } ! down arrow - currently, these clear the text line (as backspace can be slow) 251, keycode_Down, keycode_PageDown: cursor_position = RecallLaterCommandLine(a_buffer); ResetRunningChecks(false); BlankSuggestion(); ClearPromptWindow(); PrintPromptInWindow(); ! a backspace 127, 8, 254, 249, keycode_Delete: if (cursor_position > 0) { RemoveCharacterAt(a_buffer, cursor_position); cursor_position--; ResetRunningChecks(true); BlankSuggestion(); } ! a character default: ! write in the letter InsertCharacterAt(key, a_buffer, cursor_position); ! a_buffer->(WORDSIZE + type_position) = key; cursor_position++; } if (~~character_press_engaged) { glk_request_char_event((+key-window+).ref_number); } character_press_engaged = false; break; } } ! We've broken from the loop, which means the text line has changed !a_buffer-->0 = input_string_length; if (~~terminated) { ! Cheap analysis pass BasicAnalysis(a_buffer, cursor_position); ! Print what we've got PrintInputLine(a_buffer, cursor_position); if (a_buffer-->0 > 0 && a_buffer--> cursor_position ~= ' ') { current_analysis_stage = PENDING; SuggestionAnalysis(a_buffer); ! Set up for analysis glk_request_timer_events(TIME_SKIP); } } } ! end of main line loop ! cancel timer and character events glk_cancel_char_event((+key-window+).ref_number); glk_request_timer_events(0); current_analysis_stage = PERFORMED; ! tidy up the input line and return VM_Tokenise(a_buffer, a_table); if (WordCount()== 0) jump reDo; StoreCommandLine(a_buffer); ! Close any quote window we've got going. if (gg_quotewin) { glk_window_close(gg_quotewin, 0); gg_quotewin = 0; } glk_set_window( (+ main-window +).ref_number); ]; [ InsertCharacterAt char1 in_buffer pos i ; (in_buffer-->0)++; for ( i = in_buffer-->0 : i > pos : i-- ) in_buffer->(i + WORDSIZE) =in_buffer->(i - 1 + WORDSIZE); in_buffer->(pos + WORDSIZE) = char1; ]; [ RemoveCharacterAt in_buffer pos i; if (pos == 0) rfalse; for ( i = pos : i <= in_buffer-->0 : i++) in_buffer->(i - 1 + WORDSIZE) = in_buffer->(i + WORDSIZE); (in_buffer-->0)--; ]; [ CompleteSuggestionAnalysis a_buffer; while (current_analysis_stage == PROCESSING) SuggestionAnalysis(a_buffer, true); glk_request_timer_events(0); ]; -) after "Keyboard Input" in "Glulx.i6t". Chapter 3 - Command Line Recall Section - Store the Command Line after the player hits Return Include (- Constant COMMAND_RECALL_DEPTH = 10; Constant COMMAND_LINE_AVERAGE_LENGTH = 16; Constant COMMAND_STORAGE = COMMAND_LINE_AVERAGE_LENGTH * COMMAND_RECALL_DEPTH; Array command_lines -> COMMAND_STORAGE + 10; Array command_line_pointers --> COMMAND_RECALL_DEPTH + 2; Global current_command_line = 0; ! 0 = player's typing. 1+ = going backwards in time [ StoreCommandLine input_buffer start_point i os ol ; ! store characters in command_lines array. ! store start point and length in command_line pointers (via snippety method 1,000 * length + start point) if (input_buffer-->0 == 0) rfalse; ! PrintCommandLineStorage("Before writing in new line."); ! start point = character after end of last line start_point = GetCommandLineStart(1) + GetCommandLineLength(1); if (start_point + input_buffer-->0 > COMMAND_STORAGE) ! We're out of storage so we start again from the beginning start_point = 0; ! find any commands in the stack which live in the part of the storage buffer we're using, and erase their pointers ! overlap between new and old command <=> either starts in the middle of the other for (i = COMMAND_RECALL_DEPTH : i >= 1 : i-- ) { os = GetCommandLineStart(i) ; ol = GetCommandLineLength(i); if (ol ~= 0 && (WordInsideWord(os, start_point, input_buffer-->0) || WordInsideWord(start_point, os, ol))) command_line_pointers-->i = 0; } WriteBufferToCommandLine(input_buffer, start_point); ! shift all the pointers one along - note this means we don't need to edit the command line buffer. So nyah. for (i = COMMAND_RECALL_DEPTH: i > 1 : i-- ) { command_line_pointers --> i = command_line_pointers --> (i - 1); } command_line_pointers --> 1 = ConstructCommandLinePoint(start_point, input_buffer-->0); ! PrintCommandLineStorage("After writing in new line."); ]; [ WordInsideWord teststart interval_start interval_length; return (teststart >= interval_start && teststart < interval_start + interval_length); ]; -) after "Parser.i6t". Section - Debugging Routines (not for release) Include (- [ PrintCommandLineStorage title_text i j ; glk_set_window((+main-window+).ref_number); print (string) title_text, "^"; for (i = 1: i <= COMMAND_RECALL_DEPTH : i ++) { print "Command number ", i, ": starts at ", GetCommandLineStart(i), " / length ", GetCommandLineLength(i), " ... "; print "> "; for (j = 0 : j < GetCommandLineLength(i) : j++) print (char) command_lines-> (GetCommandLineStart(i) + j); print "^"; } ]; -) after "Parser.i6t". Section - Recall of previous command lines Include (- [ RecallPreviousCommandLine input_buffer j ; ! writes the current command line into the input buffer array ! moves the current command line back in history if (command_line_pointers-->(current_command_line + 1) == 0 || current_command_line > COMMAND_RECALL_DEPTH) return -1; ! no such command line current_command_line++; ExtractCommandLine( current_command_line, input_buffer); return input_buffer-->0; ]; [ RecallLaterCommandLine input_buffer ; if (current_command_line == 0) rfalse; ! do nothing current_command_line--; if (current_command_line == 0) { ! wipe the input_buffer input_buffer-->0 = 0; } else { ExtractCommandLine(current_command_line, input_buffer); } return input_buffer-->0; ]; [ ExtractCommandLine n input_buffer; WriteCharactersToBuffer(command_lines, GetCommandLineStart(n), GetCommandLineLength(n), input_buffer); ]; -) after "Parser.i6t". Section - Reading and Writing Command Lines Include (- [ WriteBufferToCommandLine input_buffer storage_start_point i ; for (i = 0 : i < input_buffer-->0 : i++) command_lines->(i + storage_start_point) = input_buffer->(i + WORDSIZE); ]; [ WriteCharactersToBuffer from_ar from_start from_length to_ar i ; ! glk_set_window((+main-window+).ref_number); ! print "Writing out characters: from ", from_start, " , length = ", from_length, ".^"; for (i = 0: i < from_length: i++) { to_ar->(WORDSIZE + i) = from_ar->(from_start + i); ! print (char) to_ar->(WORDSIZE+i); } ! print "^"; to_ar--> 0 = from_length; ]; -). Section - The Command Line pointer structure Include (- [ ConstructCommandLinePoint start_point length; return (length * 1000) + start_point; ]; [ GetCommandLineStart n; return (command_line_pointers-->n) % 1000; ]; [ GetCommandLineLength n; return (command_line_pointers-->n) / 1000; ]; -). Section - Setting up Command Line System When play begins: initialise command line system; To initialise command line system: (- InitialiseCommandLineSystem(); -). Include (- [ InitialiseCommandLineSystem i ; for (i = 0 : i <= COMMAND_RECALL_DEPTH : i++) command_line_pointers-->i = 0; ]; [ InitialiseCommandRecall ; current_command_line = 0; ]; -). Chapter 4 - Initiating Command Line Recall for Disambiguation Section 1 - Record the Edit Point Include (- Global edit_character = -1; [ CreateEditPoint chn; ! glk_set_window((+main-window+).ref_number); !! print "Storing edit point = ", chn, "^"; edit_character = chn; ]; [ WipeEditPoint; edit_character = -1; ]; [ ApplyEditPoint input_buffer chn; chn = edit_character; WipeEditPoint(); if (chn > -1) { ! glk_set_window((+main-window+).ref_number); ! print "Applying edit point = ", chn, "^"; RecallPreviousCommandLine(input_buffer); InsertCharacterAt(' ', buffer, chn-1); BasicAnalysis(input_buffer, chn); PrintInputLine(input_buffer, chn); return chn; } return 0; ]; -) before "Parser.i6t". Book 2 - I6 Functions Chapter 1 - Utilities Section 1 - Turn a byte array into a buffer with length component Include (- [ CopyBufferWritingLength from_ar to_ar length max_length i ; for (i = 0: i < length && i < max_length: i++) { to_ar -> (i + WORDSIZE) = from_ar -> i; } to_ar --> 0 = i; ]; -). Chapter 2 - Dictionary Section 3 - Dictionary Access When play begins: set up dictionary access. To set up dictionary access: (- SetUpDictionaryAccess(); -). Include (- Global dictlen; Global entrylen; Global dictstart; [ SetUpDictionaryAccess; dictlen = #dictionary_table-->0; ! for use in later tokenisation entrylen = DICT_WORD_SIZE + 7; dictstart = #dictionary_table + WORDSIZE; ]; [ IndexFromWord wordnum; ! word to dict position return (wordnum - dictstart) / entrylen; ]; [ WordFromIndex i; ! dict position to word return entrylen * i + dictstart; ]; -) before "Parser.i6t". Section 4 - Word Type Include (- [ WordType j; if ((j-> #dict_par1) & 1) return EX_VERB; if ((j-> #dict_par1) & 8) { ! glk_set_window( (+main-window+) .ref_number); ! print "Word ", (address) j, " is declared to be a prep.^"; return EX_PREPOSITION; } if ((j-> #dict_par1) & 128) return EX_NOUN; !?? ]; -) Section 5 - Compare two dictionary/string words, compare typed word and string/dictionary word (considering all characters) Include (- Array wc1 -> DICT_WORD_SIZE + WORDSIZE + 2; Array wc2 -> DICT_WORD_SIZE + WORDSIZE + 2; [ WordCompare w1 w2 max_compare_length u; if (max_compare_length == 0) max_compare_length = DICT_WORD_SIZE; ! for text and dicts VM_PrintToBuffer(wc1, max_compare_length, w1); ! print word VM_PrintToBuffer(wc2, max_compare_length, w2); ! print word !print (string) w1, "/", (address) w2, "/", wc1-->0, "/", wc2-->0, "/^"; if (wc1-->0 ~= wc2-->0) rfalse; for (u = WORDSIZE : u < (wc1-->0) + WORDSIZE: u++) { ! print (char) wc1->u, ":", (char) wc2->u, "... "; if (glk_char_to_lower(wc1->u) ~= glk_char_to_lower(wc2->u)) rfalse; } rtrue; ]; -). Include (- [ CompareTextAndPlayersInput text_word wordnum print_buffer i players_word_length players_word ; players_word_length = WordLength(wordnum); players_word = WordAddressInBuffer(wordnum, print_buffer); VM_PrintToBuffer(test_bed, players_word_length + 1, text_word); !glk_set_window((+main-window+).ref_number); !print "Typed length = ", players_word_length, "^"; if (test_bed-->0 ~= players_word_length) { !print "NOT LONG ENOUGH.^"; rfalse; } for (i = 0 : i < players_word_length : i++) { !print (char) players_word->i, " / ", (char) test_bed->(i + WORDSIZE); if (players_word->i ~= test_bed->(i + WORDSIZE)) rfalse; } rtrue; ]; -). Section 6 - Word in Scope used to create quick lookup lists Include (- !Constant MAX_WORD_IN_SCOPE = 256; !Constant MAX_VERB_NAMES = 300; !Constant MAX_COMPASS_NAMES = 35; Array word_in_scope_list --> MAX_WORD_IN_SCOPE + 2; Array word_for_verb_list --> MAX_VERB_NAMES + 2; Array word_in_compass_list --> MAX_COMPASS_NAMES + 2; [ AddThe scope_list_name scope_list_size; AddToTableArray(scope_list_name, 'the', scope_list_size); ]; [ WorkThroughScope parent_of_scope scope_list_name pre_list_routine scope_list_size word_type_include property_include error_text_type error_text_use_option j ; scope_list_name --> 0 = 0; if (pre_list_routine ~= 0) pre_list_routine(scope_list_name, scope_list_size); for (j = dictstart : j < dictstart + dictlen * entrylen : j = j + entrylen) { if (WordInScope(j, parent_of_scope) || (word_type_include ~= EX_NONE && WordType(j) == word_type_include) || ( property_include > 0 && WordInScope(j, location, property_include) ) ) { AddToTableArray(scope_list_name, j, scope_list_size); } } if (scope_list_name-->0 >= scope_list_size ) { glk_set_window((+main-window+).ref_number); print "*** Interactive Parsing Error: Out of scoped words for ", (string) error_text_type, ". Please increase the limit (which is currently ", scope_list_size, ") using the command:^^Use ", (string) error_text_use_option, " of at least ", scope_list_name-->0 + 2, ".^^"; scope_list_name-->0 = 0; } ! #ifdef DEBUG; ! glk_set_window((+main-window+).ref_number); ! print "Scope list size:", scope_list_name-->0 ," for ", (the) parent_of_scope, "^"; ! #endif; ]; [ AddToTableArray scope_list_name j scope_list_size; (scope_list_name-->0)++; if (scope_list_name-->0 < scope_list_size) scope_list_name-->(scope_list_name-->0) = j; ]; [ WordInScope w r p; ! approximate scope rules to help choose the preferable object ! doesn't prevent player from checking if a word is recognised ! if p is specified, we require this too while (r) { if (WordInProperty(w, r, name)) { if (p == 0 || (p > 0 && r has p)) rtrue; } #Ifdef pname; if (r provides pname && WordInProperty(w, r, pname)) { if (p == 0 || (p > 0 && r has p)) rtrue; } #Endif; #Ifdef scenic; if (r provides scenic && WordInProperty(w, r, scenic)) { if (p == 0 || (p > 0 && r has p)) rtrue; } #Endif; ! find the next object if (child(r) && (r hasnt container || r has transparent or open or supporter or visited)) r = child(r); ! ?? if (r==Darkness) r=player; else { while (r && sibling(r) == 0) r = parent(r); if (r) (r = sibling(r)); } } ! could also check articles etc at this point when nouns are expected - only using for Compass, though, so not needed ! for (r=1: r<=LanguageDescriptors-->0: r=r+4) if (w == LanguageDescriptors-->r or LanguageDescriptors-->(r+3)) rtrue; rfalse; ]; [ CheckCompassWords check_word i; for (i = 1: i <= word_in_compass_list-->0 : i++) if (check_word == word_in_compass_list-->i) { ! glk_set_window((+main-window+).ref_number); ! print "Compass word found: ", (address) check_word, "^"; rtrue; } ! glk_set_window((+main-window+).ref_number); ! print "Compass word NOT FOUND: ", (address) check_word, "^"; rfalse; ]; -) Chapter - Metrics Core Algorithm Section - Find the Best Match Include (- [ BestMatch input_buffer dict_word_index best_score best_word_index local_score ; best_word_index = -1; for (dict_word_index = 0 : dict_word_index < dictlen : dict_word_index ++) { local_score = CompareWithIndexedDictionaryWord(input_buffer, dict_word_index); if (local_score > best_score) { best_score = local_score; best_word_index = dict_word_index; } } if (best_word_index > -1) { print "The best match is '", (address) WordFromIndex(best_word_index), "'.^"; } else { print "No match found."; } ]; -). Section - Compare Input Buffer against Dictionary Word and return a score Include (- Constant MATCH_RANGE = 1; Constant BASIC_UNIT_SCORE = 10; Constant FIRST_LETTER_BOOST = 2; Constant PRIORITY_BOOST = 1; Constant CORRECT_TYPE_SCORE = 4; Constant WORD_BOOST_UNIT = 3; Constant BASE_LINE_MATCH_SCORE = 30; Constant PRIORITISE_THRESHOLD = 40; Global max_match_made = false; [ MaxPossibleScore length wl; ! based on length of typed buffer ! each typed character can score in 3 multiples ! except the first and last character, which lose 1 group each ! Note the "-2" is specific to a MATCH_RANGE of 1. So sue me. ! two problems: ! -2 is only valid if the player has *typed* more than the number of letters in the dictionary word, otherwise it's -1 ! Priority score is ignored here (which is the boost that "the" gets over "that", and is important ! too high, and the system can't choose quickly. too low, and it gives up . Hmm . return BASIC_UNIT_SCORE * (length * (2* MATCH_RANGE + 1) - 1 - (wl < length) + FIRST_LETTER_BOOST); ]; [ AssignDictWordMult j val; dictionary_word_multiples-->j = val; ]; [ DictWordMult j; return dictionary_word_multiples-->j; ]; [ CompareWithIndexedDictionaryWord input_buffer dict_word_index dict_mult sc i j ; dict_mult = dictionary_word_pointer --> dict_word_index; if (DictWordMult(dict_mult) == 1) return -2; ! initialise the score sc = FIRST_LETTER_BOOST * BASIC_UNIT_SCORE * (dictionary_word_first_letter->dict_word_index == input_buffer->WORDSIZE); for (i = 0 : i < input_buffer-->0 : i++) { for (j = -MATCH_RANGE: j <= MATCH_RANGE : j++) { if (i + j >=0 && i + j < DictionaryIndexedWordLength(dict_word_index)) { if (DictWordMult(dict_mult + i + j) % PrimeFromCharacter(input_buffer->(i + WORDSIZE)) == 0) { ! this character features in this dictionary word multiple sc = sc + BASIC_UNIT_SCORE; } } } } ! boost for length of word correct sc = sc - (WORD_BOOST_UNIT * Max(input_buffer-->0 - DictionaryIndexedWordLength(dict_word_index), 0)); #ifdef DEBUG_ADVANCE_PARSING; if (sc >= 40) { glk_set_window( (+main-window+) .ref_number ); print "Word ", (address) WordFromIndex(dict_word_index), " scored ", sc, ".^"; } #endif; max_match_made = (sc >= MaxPossibleScore(input_buffer-->0, DictionaryIndexedWordLength(dict_word_index))); return sc; ]; [ Max a b; if (a>b) return a; return b;]; [ CompareWithDictionaryWord input_buffer dict_word; return CompareWithIndexedDictionaryWord(input_buffer, IndexFromWord(dict_word)); ]; [ Abs x; if (x > 0) return x; return -x; ]; -). Chapter 2 - Suggesting Words Section 1 - Prioritised words Include (- Constant MAX_BOOST_WDS = 30; Global wds_boosted = 0; Array boost_wds --> MAX_BOOST_WDS; -) before "Parser.i6t". Table of Useful Words word name "the" "look" "drop" "close" "attack" When play begins: sort the Table of Useful Words in word name order; set up boosted words; To set up boosted words: (- wds_boosted = TopicArrayFromTable( (+Table of Useful Words+), 1, boost_wds); -). Include (- [ TopicArrayFromTable tab col ar inrow lpwd lkwd notfound max; max=TableRows(tab); for (inrow = 1 : inrow <= max : ) ! shouldn't need an outer loop, but this helps if a word just isn't in the dictionary. { for (lpwd = dictstart : lpwd < dictstart + dictlen * entrylen && inrow <= max : lpwd = lpwd + entrylen) { !print (address) j, " / " , (string) TableLookUpEntry(tab, 1, i), ".^"; if (WordCompare(TableLookUpEntry(tab, col, inrow) , lpwd)) { ar-->(inrow - 1) = lpwd; inrow++; lkwd = lpwd; !print "located ", (address) lpwd, ".^"; } else if (lkwd == lpwd) { print_ret "***** PROGRAMMING ERROR: Word ", (string) (TableLookUpEntry(tab, col, inrow)), " used in an Interactive Parsing table is not in the dictionary! Please correct the Table of Useful Words or the Table of Single-Letter Verb Associations. *****^"; } } } !print "---------^"; return max; ]; -). Chapter - Initialising the Dictionary Metrics Section - Flow Control The last when play begins rule: compute the dictionary; To compute the dictionary: (- CreateDictionaryMultiples(); -). Section - Primes and Character correspondence Include (- !Constant TEST_CONVERTER; Array prime_list --> 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103; ! numbered 0 to 25 [ PrimeFromCharacter ch ; if (ch == '.' or ',') return 0; if (ch >=65 && ch <= 90) ch = ch + 32; ch = ch - 97; if (ch < 0 || ch > 25) return 103; return prime_list-->ch; ]; -) before "Parser.i6t". Section - Long words are stored as text Include (- Constant MAX_LONG_WORDS = 100; Global nine_plus_index_point = 0; Global longworderror_found = 0; Array nine_plus_words --> MAX_LONG_WORDS * 2; ! stored as word number // longer word as text string [ StoreLongerVersionOf n txt ; ! n is the dictionary number/address of the word txt = LocateTextEquivalentOf(n); if (txt ~= -1) { nine_plus_words-->nine_plus_index_point = n; nine_plus_words-->(nine_plus_index_point+1) = txt; nine_plus_index_point = nine_plus_index_point + 2; } else { #ifdef DEBUG; #ifdef LONG_WORD_CHECK_REPORT; print "(Dictionary word ", (address) n, " is 9 letters long - does it need a text equivalent in the Longer Words table?)^"; #endif; #endif; } ]; [ RetrieveLongerVersionOf n i ; if (n >= dictstart) { if (DictionaryWordLength(n) < DICT_WORD_SIZE) return n; for (i = 0 : i < nine_plus_index_point : i = i + 2) { if (nine_plus_words-->i == n) return nine_plus_words-->(i + 1); } } return n; ]; -) before "Parser.i6t". Include (- Constant long_words_table = (+Table of Longer Words+); [ LocateTextEquivalentOf n max i ; max=TableRows(long_words_table); for ( i=1:i<=max:i++ ) { if (WordCompare(TableLookUpEntry(long_words_table, 1, i) , n)) return TableLookUpEntry(long_words_table, 1, i); } return -1; ]; -). Table of Longer Words word "twenty-one" "twenty-two" "twenty-three" "twenty-four" "twenty-five" "twenty-six" "twenty-seven" "twenty-eight" "twenty-nine" "transcript" "superbrief" "everything" "inputchange" Section - Build the Dictionary metrics Include (- !Constant MAX_WORDS = 1000; !Constant AV_WORD_LENGTH = 6; Constant MAX_MULTIPLES = MAX_WORDS * AV_WORD_LENGTH; ! holds mapping from word -> position in multiples array Array dictionary_word_pointer --> MAX_WORDS; Array dictionary_word_first_letter -> MAX_WORDS; ! holds multiples, number of which is determined by length of word Array dictionary_word_multiples --> MAX_MULTIPLES; Array test_bed -> 50; [ CreateDictionaryMultiples i j word_count mult_pos avlen ; #ifdef DEBUG; ! print "Converting with space for ", MAX_WORDS, " words of average length ", AV_WORD_LENGTH, " and ", MAX_MULTIPLES, " multiples.^"; #endif; if (dictlen > MAX_WORDS) { "***Interactive Parsing Error***: Dictionary storage is too small. Please increase dictionary storage to at least ", dictlen + 4, " using the command:^^Use maximum dictionary size of at least ", dictlen, ".^^"; } mult_pos = 0; InitialiseMultiples(); ! loop through dictionary words by index for (word_count = 0 : word_count < dictlen : word_count++) { ! store the start of this word's data dictionary_word_pointer --> word_count = mult_pos; ! get the dictionary word we're looking at VM_PrintToBuffer(test_bed, DICT_WORD_SIZE, WordFromIndex(word_count)); dictionary_word_first_letter -> word_count = test_bed->WORDSIZE; #ifdef DEBUG; ! average word length stat avlen = avlen + ( (100 * test_bed-->0) - avlen) / (word_count + 1); #endif; if (test_bed-->0 == DICT_WORD_SIZE) StoreLongerVersionOf(WordFromIndex(word_count)); ! loop through the characters of the word. Each stores at WORDSIZE + i for (i = 0 : i < test_bed --> 0 : i++) { if (PrimeFromCharacter(test_bed->(i + WORDSIZE)) == 0) { ! this word had a comma in it ! print "Dodgy word found: ", (address) WordFromIndex(word_count), ".^"; for (j = 0 : j < test_bed-->0 : j ++) dictionary_word_multiples-->(j + mult_pos) = 1; test_bed-->0 = 1; break; } ! loop through the neighbourhood of the character ! to "add" this character into adjacents "sums" for (j = -MATCH_RANGE : j <= MATCH_RANGE : j++) { if (j + i >= 0 && j + i < test_bed-->0) { dictionary_word_multiples-->(mult_pos + j + i) = dictionary_word_multiples-->(mult_pos + j + i) * PrimeFromCharacter(test_bed->(WORDSIZE + i)); } } } #ifdef TEST_CONVERTER; print "Converted ", (address) WordFromIndex(word_count), " into "; for (i = mult_pos : i < mult_pos + test_bed-->0 : i++) print dictionary_word_multiples-->i, " / "; print "^"; !if (VM_KeyChar() == 'q') rfalse; #endif; ! move the position in multiples along because we've created new entries mult_pos = mult_pos + (test_bed-->0); if (mult_pos + 20 > MAX_MULTIPLES) { "***Interactive Parsing Error***: Dictionary prime storage is too small. Please increase your word length allowance using the command:^^Use average word length of at least ", (mult_pos / word_count) + 2, ".^^"; } } ! store last pointer, so we can calculate word lengths for the final word dictionary_word_pointer --> dictlen = mult_pos; #ifdef DEBUG; ! print "Finished with ", mult_pos, " metrics over ", dictlen, " words of average length ", avlen, "/100.^"; #endif; ]; [ InitialiseMultiples j; for (j = 0 : j < MAX_MULTIPLES : j++) dictionary_word_multiples-->j = 1; ]; -) after "Parser.i6t". Section - Accessing the Multiples [ we get the word length of any dictionary word for free. ] Include (- [ DictionaryWordLength wordnum ; return (dictionary_word_pointer-->(IndexFromWord(wordnum) + 1)) - (dictionary_word_pointer-->IndexFromWord(wordnum)); ]; [ DictionaryIndexedWordLength indexnum ; return (dictionary_word_pointer-->(indexnum + 1)) - (dictionary_word_pointer-->indexnum); ]; -). Chapter 4 - Parser Data Section 6 - Byte arrays of input Include (- [ WordUnderCursor cur_pos i; ! glk_set_window((+main-window+).ref_number); ! print "Word Under Cursor: ", cur_pos, ".^"; for (i = 1: i < WordCount() : i++) { ! print "(word ", i + 1, " starts at " , CharacterNumber(i+1), "...^"; if (CharacterNumber(i + 1) - 1 > cur_pos) { ! print "Word Under Cursor = ", i, "^"; return i; } } ! print "Word Under Cursor (F) = ", WordCount(), "^"; return WordCount(); ]; [ WordAddressInBuffer wordnum this_buffer; return this_buffer + CharacterAddress(wordnum); ]; [ CharacterAddress wordnum; ! in the parsed buffer #ifdef TARGET_ZCODE; return parse->(wordnum * 4 + 1); #ifnot; return parse-->(wordnum*3); #endif; ]; [ CharacterNumber wordnum; ! in actual text string of input return CharacterAddress(wordnum) - WORDSIZE + 1; ]; [ DictionaryValue wordnum; #ifdef TARGET_ZCODE; return parse-->(wordnum * 2 - 1); #ifnot; return parse-->(wordnum * 3 - 2); #endif; ]; -). Book 2 - Input Window Functionality Chapter 1 - Producing the Input Window Section 1 - Main Printing Loop Include (- Constant NORMAL = style_Normal; Constant ERROR = style_User2; Constant LONGWORD_ERROR = stylehint_Proportional; Constant VALID = style_User1; Constant TIP = style_Input; Constant MISPLACED = style_Note; Constant RETURN_STYLE = style_Emphasized; Constant CURSOR_STYLE = style_Alert; Array word_colours -> 50; ! store the chosen colours [ PrintInputLine a_buffer cur_pos; wn = 1; PrintPromptInWindow(); PrintTypedCommand(a_buffer, a_buffer-->0, cur_pos); !PrintCursor(cur_pos); #ifdef DEBUG; !PrintStats(print_buffer, length); #endif; ]; -) Section 2 - Debugging (not for release) [ Some debug info. Mostly redundant now. ] Include (- [ PrintStats print_buffer length i; if (length > 0) { glk_set_window ( (+main-window+).ref_number); print "^^Stats:^"; print "WordCount() = ", WordCount(), "^"; if (WordLength(WordCount()) > 1) { i = WordAddressInBuffer(WordCount(), print_buffer); print "WordAddress() = ", (char) i->0, (char) i->1, "...^"; } print "Current word length = ", WordLength(WordCount()), "^"; print "Buffer length typed = ", length, "^"; print "Place in buffer = ", CharacterAddress(WordCount()), "/", CharacterNumber(WordCount()), "^"; } ]; -). Section 3 - Formatted Print Routines [ A set of routines for printing in the appropriate styles and places. ] Include (- [ PrintCharacter char_val col_val; glk_set_style(col_val); print (char) char_val; ]; [ PrintCursor x; glk_set_window( (+ key-window +).ref_number); glk_window_move_cursor( (+ key-window +).ref_number, x + 1, 0); PrintCharacter('_', NORMAL); print " "; ]; [ PrintPromptInWindow; glk_set_window( (+ key-window +).ref_number); glk_window_move_cursor( (+ key-window +).ref_number, 0, 0); PrintCharacter(62, CURSOR_STYLE); glk_window_move_cursor( (+ key-window +).ref_number, 0, 2); print " "; ]; [ PrintTypedCommand print_buffer length cursor_character i wordnum ; !if (length > 0) !{ glk_set_window( (+ key-window +).ref_number); glk_window_move_cursor( (+ key-window +).ref_number, 1, 0); for (wordnum = 0 , i = WORDSIZE : i < WORDSIZE + length : i++) { if ( i == WORDSIZE || (print_buffer->(i-1) == 32 or '.' or ',' or '"' && ~~(print_buffer->i == 32 or '.' or ',' or '"')) || (print_buffer->i == ',' or '.') ) wordnum ++; if (i == cursor_character + WORDSIZE) { if (print_buffer->i == ' ') PrintCharacter('_', CURSOR_STYLE); else PrintCharacter(print_buffer -> i, CURSOR_STYLE); } else PrintCharacter(print_buffer -> i, word_colours -> wordnum); } print " "; if (cursor_character >= length) PrintCursor(cursor_character); !} ]; [ PrintPressReturn val i; glk_set_window( (+ key-window +).ref_number); i = WindowSize( (+ key-window +) , 0 ); if (i < 40) rtrue; glk_window_move_cursor( (+ key-window +).ref_number, i - 20, 0); glk_set_style(RETURN_STYLE); if (val) print "Press Return"; else print " "; ]; [ PrintSuggestion word_to_print x i ; if (on_screen_suggestion ~= word_to_print) { glk_set_window( (+key-window+). ref_number ); glk_window_move_cursor( (+key-window+). ref_number , 0, 1); for (i = 0: i MAX_PREVIOUS_CHECKS+1; Array previous_checks_passed --> MAX_PREVIOUS_CHECKS+1; Global previous_suggestion_best_score = 0; [ ErasePreviousSuggestions i; previous_checks_wordnum = 0; on_screen_suggestion = 0; current_analysis_stage = PENDING; prompted_word = 0; for (i = 0: i < 20: i++) word_colours->i = NORMAL; ]; [ ResetRunningChecks minor_only; ! set up storage of advance parsing by the suggestion system if (~~minor_only) { previous_checks_passed--> 0 = 0; previous_checks_failed--> 0 = 0; previous_checks_wordnum = 0; } current_analysis_stage = PERFORMED; previous_suggestion_best_score = BASE_LINE_MATCH_SCORE; PrintPressReturn(false); ]; [ BasicAnalysis print_buffer cur_pos i; ! clear last turn's suggestion ! prompted_word = 0; if (print_buffer-->0 > 0) { ! don't tokenise if there's no input as results are unpredictable VM_Tokenise(print_buffer, parse); wn = 1; cursor_word = WordUnderCursor(cur_pos); if (WordCount() == 0) rfalse; for (i = 1: i <= WordCount() : i++) { word_colours -> i = GetBaseColourOfNextWord(i, print_buffer); } ColourByAdvanceParse(); !glk_set_window( (+main-window+) .ref_number); !print "Word Count = ", WordCount(), "^"; ! this sets "previous checks wordnum" to the cursor word if (cursor_word ~= previous_checks_wordnum) { ResetRunningChecks(false); BlankSuggestion(); } if (word_colours -> WordCount() == LONGWORD_ERROR) PrintPressReturn(false); else PrintPressReturn(command_in_full && word_understood_to == WordCount()); ! unless the buffer ends in a space, go easy on the last word ColourLastWord(print_buffer, cursor_word); } ]; [ SuggestionAnalysis print_buffer run_to_halt; ! glk_set_window( (+main-window+) .ref_number); ! print "Word_colour of word 2 = ", word_colours->cursor_word, "...^"; if (word_colours->cursor_word ~= VALID || SuggestFromAbbreviation(DictionaryValue(cursor_word), WordLength(cursor_word))) { if (CreateSuggestionForWord(print_buffer, WordAddressInBuffer(cursor_word, print_buffer), WordLength(cursor_word) , cursor_word, run_to_halt)) { suggested_wordnum = cursor_word; PrintSuggestion(prompted_word, CharacterNumber(cursor_word)); } else BlankSuggestion(); } else BlankSuggestion(); ]; [ SuggestFromAbbreviation wd len; return (len == 1 && wd ~= ',//') ; ]; -) Chapter 2 - Simple Dictionary Check Include (- [ GetBaseColourOfNextWord wordnum print_buffer i j ; wn = wordnum; i = NextWordStopped(); if (i == 0) return ERROR; j = RetrieveLongerVersionOf(i); if (j == i) return VALID; ! glk_set_window( (+main-window+) .ref_number); ! print "Checking full-length word for ", (address) i, " which is ", (string) j, " ... "; if (CompareTextAndPlayersInput(j, wordnum, print_buffer)) { ! print " okay!^"; return VALID; } ! print " no good...^"; return LONGWORD_ERROR; ]; [ ColourLastWord print_buffer word_num; if (word_colours->word_num ~= VALID && ~~LastWordFinished(print_buffer, word_num)) word_colours->word_num = NORMAL; ]; [ LastWordFinished print_buffer wordnum i; if (wordnum == WordCount()) i = (print_buffer-->0); else i = CharacterNumber(wordnum + 1); return (print_buffer->(i + WORDSIZE - 1) == ' ' or '.' or ',' or '"'); ]; -); Book 4 - Suggestions for an unfinished word Chapter 1 - Main Suggestion System Section 1 - Hub [ We use different algorithms for verbs and for nouns/preps. Verbs don't require use to run the parser at all! ] Include (- Array analysed_word_buffer -> DICT_WORD_SIZE + 1; [ CreateSuggestionForWord ! input_buffer : whole text input ! word_buffer : 0 indexed array holding characters typed so far ! length : number of characters (so use < length in loops) ! wordnum : the number of the word in the current line; we can use parse if it's helpful input_buffer word_buffer length wordnum run_to_halt ! current word's valid, but in the wrong context alternate_suggestion_flag ; ! glk_set_window((+main-window+).ref_number); ! print "Suggesting for word num = ", wordnum, "^"; ! We allow the previous best score to be just as good again, otherwise the player can ! lose a good suggestion by typing an unhelpful letter ! but don't do this if we're mid-search if (current_analysis_stage == PENDING) { if (previous_suggestion_best_score > BASE_LINE_MATCH_SCORE) previous_suggestion_best_score--; } if (previous_checks_wordnum == 0) previous_checks_wordnum = wordnum; ! Flag that we consider matching *the same* number of words to be okay under the circumstances ! allows "Thesaurus" to win over "the"... alternate_suggestion_flag = (WordCount() == word_understood_to); CopyBufferWritingLength(word_buffer, analysed_word_buffer, length, DICT_WORD_SIZE); ! we always suggest verbs at word 1, even though you could talk to people. Them's the breaks. ! print "Verb wordnum = ", verb_wordnum, ".^"; if ( next_token_type == EX_VERB || wordnum == verb_wordnum) { ! print "Suggesting verb-type.^"; SuggestVerb(analysed_word_buffer, run_to_halt); } else { if (wordnum > word_understood_to + 1) ! eg. word 3 when word 2 failed and we knew word 1 { !glk_set_window((+main-window+).ref_number); !print "Word ", word_understood_to + 1, " failed already so word ", wordnum, " isn't worth suggesting for."; rfalse; } SuggestNonVerb(analysed_word_buffer, CharacterNumber(wordnum), input_buffer, alternate_suggestion_flag, run_to_halt); } ! repair the parse table after tests VM_Tokenise(input_buffer, parse); return (prompted_word > 0); ]; -). Section 2 - Writing In Suggestions [ We come here from the main input loop if the player presses space. If no suggestion is up, this should simply return 1 to move the input along the line ] Include (- [ WriteInSuggestion input_buffer cur_pos force_complete i j ; ! repair buffer, in case timed input has caused problems VM_Tokenise(input_buffer, parse); if (suggested_wordnum ~= cursor_word) ! we've changed to a different word return -1; ! TODO: Cope with writing in a suggestion somewhere other than the last word of the line !glk_set_window( (+main-window+) .ref_number); if (prompted_word == 0 ) { ! print "No suggestions.^"; return -1; } if (DictionaryValue(suggested_wordnum) ~= 0 && ~~force_complete) { !print "DV of sw = ", DictionaryValue(suggested_wordnum), "..^"; if ( WordLength(suggested_wordnum) > 1 ) { return -1; } } VM_PrintToBuffer(test_bed , DICT_WORD_SIZE * 2, RetrieveLongerVersionOf(prompted_word)); !glk_set_window( (+main-window+) .ref_number); !print "wordnum:", suggested_wordnum, "/", CharacterNumber(suggested_wordnum), " of " , WordCount(), "... "; !TODO: we want to write in the word over the suggested_wordnum, and then add the rest of the line on the end if (suggested_wordnum < WordCount()) { ! copy the old next word, starting at the space in front of it, to the end of the new word for (i = CharacterNumber(suggested_wordnum + 1) - 2 , j = (test_bed-->0) + WORDSIZE: i < input_buffer-->0 : i++, j++) { ! i is the position in the original buffer test_bed->j = input_buffer->(i + WORDSIZE); (test_bed-->0)++; } } ! now, starting from the first letter of the word to replace, write in the test_bed array for (i = WORDSIZE, j = CharacterNumber(suggested_wordnum) - 1: i < test_bed-->0 + WORDSIZE : j++, i++) { input_buffer->(j + WORDSIZE) = test_bed->i; ! print (char) input_buffer->(j+WORDSIZE); } word_colours->suggested_wordnum = VALID; if (input_buffer->(j - 1 + WORDSIZE) ~= ' ') { input_buffer->(j + WORDSIZE) = ' '; j++; } BlankSuggestion(); input_buffer-->0 = j; ! print "^Constructed new string has length = ", j, "^"; return j; ]; -) Section 2 - Single letters link to particular verbs directly Table of Single-Letter Verb Associations letter text "a" "attack" "b" "buy" "c" "cut" "d" "down" "e" "east" "f" "feel" "g" "get" "h" "hit" "i" "inventory" "j" "jump" "k" "kiss" "l" "look" "m" "move" "n" "north" "o" "open" "p" "put" "q" "quit" "r" "read" "s" "south" "t" "take" "u" "up" "v" "version" "w" "west" "x" "examine" "y" "yes" "z" "wait" When play begins: set up verb words; To set up verb words: (- TopicArrayFromTable( (+Table of Single-Letter Verb Associations+), 2, single_letter_verbs); -). Section 2 - Score verbs and suggest the best match Include (- Array single_letter_verbs --> 27; Array recorded_search_values --> 4; [ SuggestVerb word_array run_to_halt i best_yet this_score ar j first_pass skipping_out k start_pass_number pass_number prerecorded_start_point loop_word top_score start_word_list size_word_list ; ! word_arrays runs WORDSIZE -> WORDSIZE + word_array-->0 - 1 max_match_made = false; ar = ar; first_pass = first_pass; best_yet = 0; switch(word_array-->0) { 1: ! 65-90 = A to Z ! 97 - 122 = a to z i = word_array->WORDSIZE; if (i >=65 && i <= 90) i = i + 32; if (i - 97 > 25) { glk_set_window( (+main-window+) .ref_number ); print "*** ERROR in intparse.h:- ", i, " -- blown up the single letter verbs array.^^"; } else best_yet = single_letter_verbs-->(i - 97); previous_suggestion_best_score = BASE_LINE_MATCH_SCORE + 1; current_analysis_stage = PERFORMED; !glk_set_window(BaseWindow.refnum); !print "Suggesting a verb...", (address) best_yet; !2: default: skipping_out = false; if (current_analysis_stage == PENDING) { start_pass_number = 0; prerecorded_start_point = 0; current_analysis_stage = PROCESSING; ! print "Setting up a new run-through...^^"; } else { ! we're picking up a search in progress top_score = recorded_search_values -->0; ! PRERECORDED TOP SCORE; best_yet = recorded_search_values -->1; ! PRERECORDED BEST YET; start_pass_number = recorded_search_values -->2; ! PRERECORD PASS NUMBER prerecorded_start_point = recorded_search_values -->3; ! PRERECORDED DICTIONARY POSITION; ! print "Picking up the previous run-through with top_score = ", top_score, " best_yet = ", (address) best_yet, " start_pass_number = ", start_pass_number, " and start_dict_position = ", prerecorded_start_point, " word_understood_to = ", word_understood_to , "...^^"; } for (pass_number == start_pass_number : pass_number <= 2 && ~~(max_match_made) : pass_number ++) { switch(pass_number) { 0: start_word_list = 0; size_word_list = (prompted_word > 0); 1: start_word_list = 0; size_word_list = wds_boosted; 2: start_word_list = 1; size_word_list = (word_for_verb_list-->0) + 1; } if (prerecorded_start_point > 0) { start_word_list = prerecorded_start_point; prerecorded_start_point = 0; } for (j = start_word_list : j < size_word_list && ~~(skipping_out || max_match_made) : j++) { switch(pass_number) { 0: loop_word = prompted_word; 1: loop_word = boost_wds-->j; 2: loop_word = word_for_verb_list-->j; } if (~~run_to_halt) k++; this_score = CompareWithDictionaryWord(word_array, loop_word); if (~~ (pass_number == 1 && WordType(loop_word) ~= EX_VERB && ~~CheckCompassWords(loop_word))) this_score = this_score + CORRECT_TYPE_SCORE; ! glk_set_window( (+main-window+) .ref_number ); ! print "^Pass No. = ", pass_number, " ", (address) loop_word, " tested, score = ", this_score, " ( > top? )", this_score >top_score, ", max_match_made = ", max_match_made, ".^"; if (this_score > top_score) { top_score = this_score; best_yet = loop_word; if (max_match_made) { ! glk_set_window( (+main-window+) .ref_number); ! print "Word ", (address) best_yet, " has achieved the max score.^"; break; } } else if (this_score == top_score && top_score > 0) { ! we've found a tie. TODO: Do something with this information? if (word_array-->0 == 1) best_yet =-1; ! if one letter only, we use this info to wipe the suggestion } if (k > MAX_TESTS) { skipping_out = CheckForExitCondition(k); k = 0; } } ! record data ! *** if we're exiting mid-search, we return here, and miss out the next few steps if (skipping_out) { recorded_search_values --> 3 = j; !RECORD J (DICT POSITION) recorded_search_values --> 2 = pass_number-1; !RECORD PASS NUMBER recorded_search_values --> 1 = best_yet; !RECORD BEST YET recorded_search_values --> 0 = top_score; !RECORD TOP SCORE ! print "Skipping out with top_score = ", top_score, " best_yet = ", (address) best_yet, " start_pass_number = ", pass_number, " and start_dict_position = ", j, " word_understood_to = ", word_understood_to , "...^^"; rfalse; } } } if (best_yet > 0) { prompted_word = best_yet; previous_suggestion_best_score = top_score; } if (current_analysis_stage == PROCESSING) { current_analysis_stage = PERFORMED; } ]; -). Chapter 3 - Suggesting Non Verbs Include (- [ SuggestNonVerb word_array word_start_point input_buffer alternate_suggestion_flag run_to_halt ! run_to_halt indicates that we cannot leave this loop; we're here till we get an answer i best_yet this_score ar j k start_pass_number pick_up_start_point skipping_out pass_number top_score loop_start loop_end loop_incr loop_word ; i = i; ar = ar; ! word_arrays runs 0-> word_length ! in this version, when we hit a tie we do nothing and stick with the result we have ! then (or rather, before) we use LocateWordInScope to push sensible word choices ! through the system first, in the hope of preventing parser checks on stupider words. glk_set_window( (+main-window+) .ref_number); ! print "(Analyse)^"; max_match_made = false; skipping_out = false; if (current_analysis_stage == PENDING) { best_yet = 0; top_score = previous_suggestion_best_score; start_pass_number = 0; pick_up_start_point = 0; current_analysis_stage = PROCESSING; } else { ! we're picking up a search in progress top_score = recorded_search_values-->0; ! PRERECORDED TOP SCORE; best_yet = recorded_search_values -->1; ! PRERECORDED BEST YET; start_pass_number = recorded_search_values --> 2; ! PRERECORDED PASS NUMBER; pick_up_start_point = recorded_search_values -->3; ! PRERECORDED DICTIONARY POSITION; ! print "Picking up the previous run-through with top_score = ", top_score, " best_yet = ", (address) best_yet, " start_pass_number = ", start_pass_number, " and start_dict_position = ", start_dict_position, " word_understood_to = ", word_understood_to , "...^^"; } !glk_set_window(BaseWindow.refnum); !for (i = 0: i< word_length: i++) print (char) word_array->i; !print "Current score to beat: ", top_score, "^"; for (pass_number = start_pass_number: pass_number < 4 && ~~(skipping_out || max_match_made) : pass_number++) { switch(pass_number) { 0: loop_start = 0; loop_end = (prompted_word > 0); loop_incr = 1; 1: loop_start = 1; loop_end = (word_in_scope_list-->0) + 1; loop_incr = 1; 2: loop_start = 0; loop_incr = 1; loop_end = wds_boosted; 3: loop_start = dictstart; loop_end = dictstart + dictlen * entrylen; loop_incr = entrylen; } if (pick_up_start_point > 0) { loop_start = pick_up_start_point; pick_up_start_point = 0; } for (j = loop_start : j < loop_end && skipping_out == false : j = j + loop_incr) { switch(pass_number) { 0: loop_word = prompted_word; 1: loop_word = word_in_scope_list-->j; 2: loop_word = boost_wds-->j; 3: loop_word = j; } if (PreviousParseResult(loop_word) ~= -1) { if (~~run_to_halt) k++; this_score = CompareWithDictionaryWord(word_array, loop_word); if (pass_number < 2 || (WordType(loop_word) == EX_NOUN && ~~CheckCompassWords(loop_word))) this_score = this_score + CORRECT_TYPE_SCORE; ! glk_set_window( BaseWindow.refnum ); ! print "^Pass No. = ", pass_number, " ", (address) loop_word, " tested, score = ", this_score, " ( > top? )", this_score >top_score, ", max_match_made = ", max_match_made, ".^"; if (this_score > top_score && WordImprovesParsing(input_buffer, loop_word, word_start_point, word_array-->0, alternate_suggestion_flag)) { top_score = this_score; best_yet = loop_word; if (max_match_made) break; } if (k > MAX_TESTS) { skipping_out = CheckForExitCondition(k); k = 0; } } } } ! *** if we're exiting mid-search, we return here, and miss out the next few steps if (skipping_out) { recorded_search_values --> 2 = pass_number - 1; !RECORD PASS NUMBER ! -1, because when we drop out of the inner loop, we get a +1 on the outer loop!! recorded_search_values --> 3 = j; !RECORD J (DICT POSITION) recorded_search_values --> 1 = best_yet; !RECORD BEST YET recorded_search_values --> 0 = top_score; !RECORD TOP SCORE ! print "Skipping out with top_score = ", top_score, " best_yet = ", (address) best_yet, " start_pass_number = ", pass_number, " and start_dict_position = ", j, " word_understood_to = ", word_understood_to , "...^^"; rfalse; } ! print "Completed!^^"; if (best_yet > 0) { prompted_word = best_yet; previous_suggestion_best_score = top_score; } if (current_analysis_stage == PROCESSING) { current_analysis_stage = PERFORMED; } ]; -) Section - Early exit conditions for loops Include (- [ CheckForExitCondition k; ! glk_set_window((+main-window+).ref_number); ! print "(Keycheck).^"; k = CheckForCharacter(); ! second true indicates "short timer" mode if (k ~= 0) { ! glk_set_window((+main-window+).ref_number); ! print "Keypress interrupted handling : ", (char) k, ".^"; ! store up the k and pass it back to the next grab character call... interrupt_keypress = k; rtrue; } rfalse; ]; [ CheckForCharacter done key ; glk_request_timer_events(1); done = false; while(~~done) { CorrectlyPositionCursor(); glk_select(gg_event); switch(gg_event-->0) { evtype_Timer: key = 0; done = true; evtype_CharInput: key = gg_event-->2; done = true; glk_request_char_event((+key-window+).ref_number); } } glk_request_timer_events(0); return key; ]; -). Chapter 4 - Run the Parser over what we've got So far Section 1 - Colour based on parse results Include (- [ ColourByAdvanceParse i ; command_in_full = false; #ifdef DEBUG_ADVANCE_PARSING; glk_set_window((+main-window+).ref_number); print "++++++^"; #endif; word_understood_to = PerformAdvanceParse(true); #ifdef DEBUG_ADVANCE_PARSING; glk_set_window((+main-window+).ref_number); ! print "Scored ", word_understood_to, ".^"; ! print "-------^"; #endif; if (word_understood_to < WordCount()) { for (i = word_understood_to + 1 : i<= WordCount() : i++) { if (word_colours->i == VALID) ! we knew the word, but it's not where the parser wants it to be word_colours->i = MISPLACED; } } for (i = 1 : i <= word_understood_to : i++) { if (word_colours->i == ERROR ) ! we didn't know the word, but the parser accepted it => it's a input, so we accept (!!) word_colours->i = VALID; } ]; -) Section 2 - Does a word replacement help the parser? [ Here's the clever bit. Stitch a suggestion into the input, run the parser, and see if it matches more words than the line did without that word. If it does, this word is worth suggesting. We store results of this, because it's the slowest part of the whole deal. Would be good to optimise this section more; for example, the game will do the same Scope calculations over and over again. ] Include (- [ WordImprovesParsing input_buffer new_word word_start_point word_length alternate_suggestion_flag result ; ! a cheeky lookahead ! if we replace the new_word for the wordnum, does the parser improve on the word_understood_to count? ! glk_set_window((+main-window+).ref_number); ! print "Scoring up for ", (address) new_word, ".^"; ! print "Previous score for word_understood_to is ", word_understood_to, ".^"; switch(PreviousParseResult(new_word)) { 1: rtrue; -1: rfalse; 0: BuildSecondBuffer(input_buffer, word_start_point, word_length, new_word); ! glk_set_window((+main-window+).ref_number); ! print "Considering NEW ", (address) new_word, "...^"; result = AdvanceParseFromSecondBuffer(); if (result > word_understood_to || (result == word_understood_to && alternate_suggestion_flag)) { if (previous_checks_passed-->0 < MAX_PREVIOUS_CHECKS) { (previous_checks_passed-->0)++; previous_checks_passed-->(previous_checks_passed-->0) = new_word; } rtrue; } else { if (previous_checks_failed-->0 < MAX_PREVIOUS_CHECKS) { (previous_checks_failed-->0)++; previous_checks_failed-->(previous_checks_failed-->0) = new_word; } rfalse; } } ]; -). Section 3 - Some functions for shifting buffers around Include (- Array buffer4 -> INPUT_BUFFER_LEN + 4; [ AdvanceParseFromSecondBuffer result; ! Store the buffer; copy buffer2 into place of buffer for use by the parser VM_CopyBuffer(buffer4, buffer); VM_CopyBuffer(buffer, buffer2); ! use buffer VM_Tokenise(buffer, parse); result = PerformAdvanceParse(false); ! glk_set_window((+main-window+).ref_number); ! print "Achieved a result of ", result, ".^"; ! repair the buffer before we destroy anything the player's typed VM_CopyBuffer(buffer, buffer4); return result; ]; [ BuildSecondBuffer input_buffer old_word_start old_word_length new_word i j ; ! glk_set_window((+main-window+).ref_number); ! print "Building advance parser buffer over start ", old_word_start, ", length ", old_word_length, ", and inserting new word ", (address) new_word , " to give : "; ! TODO: cope with placing a suggestion anywhere except the last word ! word transformed to byte array VM_PrintToBuffer(test_bed, DICT_WORD_SIZE, new_word); ! build new buffer2 for ( i = 0 : i < input_buffer-->0 - old_word_length + test_bed-->0 : i++) { if ( i < old_word_start - 1) { ! copy the first part of the buffer straight buffer2->(i + WORDSIZE) = input_buffer->(i + WORDSIZE); } ! eg. examine broze cross (sub in ABOUT) ! ows = 9, tb0 = 5 (14) ! when i = 13 start writing in (from the space) ! start writing in from position 13 + WORDSIZE = 13 - 5 + 5 + WORDSIZE) else if (i >= old_word_start + test_bed-->0 - 1) { ! copy the second part of the buffer straight buffer2->(i + WORDSIZE) = input_buffer->(i - test_bed-->0 + old_word_length + WORDSIZE); } else { buffer2->(i + WORDSIZE) = test_bed->(j + WORDSIZE); j++; } ! print (char) buffer2->(i+WORDSIZE); } buffer2-->0 = input_buffer-->0 + test_bed-->0 - old_word_length; ! print " (new length = ", buffer2-->0, ".^"; ]; -). Section 4 - System for using previous results Include (- [ PreviousParseResult new_word i ; ! returns 1 => success, -1 => fail, and 0 => no known data !glk_set_window((+main-window+).ref_number); for (i = 1 : i <= previous_checks_passed-->0 || i <= previous_checks_failed-->0 : i++) { if (i <= previous_checks_passed-->0 && previous_checks_passed-->i == new_word) { !print "Found ", (address) new_word, " in previous PASS! Declaring it OK.^"; return 1; } else if (i <= previous_checks_failed-->0 && previous_checks_failed-->i == new_word) { !print "Found ", (address) new_word, " in previous FAIL! Declaring it BUNK.^"; return -1; } } return 0; ]; -). Section 3 - The Advance Parsing Routine, for use by anyone - returns length of match made (and sets context vars) Include (- Constant RETURN_TO_INPUT_LINE = 3; Constant PERFORMING = 2; Constant ERROR_GIVEN = 1; Constant INACTIVE = 0; Constant EX_NONE = -1; Constant EX_NOUN = 0; Constant EX_VERB = 1; Constant EX_PREPOSITION = 2; Constant EX_ENDOFLINE = 3; Global pre_command = 0; Global word_understood_to = 0; Global command_in_full = false; Global next_token_type = 0; ! for storing prepositions in, when I get that far... Global next_expected_word = 0; ! Do a parse. Call with true to store results; false simply to return length of match made [ PerformAdvanceParse setting_flags i rv j k; i = word_understood_to; k = command_in_full; #ifdef DEBUG; !glk_set_window ( (+main-window+).ref_number); #endif; pre_command = PERFORMING; next_expected_word = 0; word_understood_to = -1; j = next_token_type; Parser__parse(); ! put back global vars rv = word_understood_to; if (~~setting_flags) { next_token_type = j; word_understood_to = i; command_in_full = k; } pre_command = INACTIVE; ! print "Returning ", rv, ".^"; ! return matched number return rv; ]; -) before "Language.i6t". Book 4 - Optimisations Chapter 1 - The Stored Scope Stack [ To speed up repeated processing, we split the parser's noun handling into two sweeps - firstly gathering items in scope, and then parsing over them. We store the results of the scope sweep, and thereby skip several iterations in every run of the parser! This may have unintended consequences, but I don't think it does. ] Include (- Constant SCOPE_STACK_OPTIMISATION; #ifdef SCOPE_STACK_OPTIMISATION; !Constant DEBUG_SCOPE_STACK; !Constant DEBUG_MATCH_LISTS; Constant MAX_STORED_SCOPE = 100; Constant NUM_STORED_SCOPE = 12; Global stored_scope = 0; Array stored_scope1 --> MAX_STORED_SCOPE; Array stored_scope2 --> MAX_STORED_SCOPE; Array stored_scope3 --> MAX_STORED_SCOPE; Array stored_scope4 --> MAX_STORED_SCOPE; Array stored_scope5 --> MAX_STORED_SCOPE; Array stored_scope6 --> MAX_STORED_SCOPE; Array stored_scope7 --> MAX_STORED_SCOPE; Array stored_scope8 --> MAX_STORED_SCOPE; Array stored_scope9 --> MAX_STORED_SCOPE; Array stored_scope10 --> MAX_STORED_SCOPE; Array stored_scope11 --> MAX_STORED_SCOPE; Array stored_scope12 --> MAX_STORED_SCOPE; Array scope_stack --> stored_scope1 stored_scope2 stored_scope3 stored_scope4 stored_scope5 stored_scope6 stored_scope7 stored_scope8 stored_scope9 stored_scope10 stored_scope11 stored_scope12; Constant FIRST_STORED_SCOPE_ITEM = 4; !-------- USEAGE ------------ [ AddToStoredScope thing; if (stored_scope-->0 == MAX_STORED_SCOPE) { glk_set_window( (+main-window+).ref_number); print "***PROGRAMMING ERROR: MAX_STORED_SCOPE has been exceeded (current limit is ", MAX_STORED_SCOPE, ".) Please increase!****^"; } (stored_scope-->0) ++; stored_scope-->(stored_scope-->0) = thing; ! print "Stored ", (the) thing, " in the scopeable list.^"; ]; [ TestScopeList i nw; wn = match_from; if (match_from <= num_words) nw = NounWord(); else nw = 0; for (i = FIRST_STORED_SCOPE_ITEM : i <= stored_scope-->0 : i++) { ! try prepositions, its, thems, etc. if (nw == 1 && stored_scope-->i == player) { MakeMatch(stored_scope-->i, 1); } else if (nw >=2 && nw < 128) { if (stored_scope-->i == LanguagePronouns-->nw) { MakeMatch(stored_scope-->i, 1); } } ! anyway, match normally MatchTextAgainstObject(stored_scope-->i); } ]; !-------- TESTS ------------ [ IsEmptyStoredScope scope_store; return ((scope_store-->0) == 0); ]; [ IsVanillaStoredScope scope_store; return ((scope_store-->0) == (FIRST_STORED_SCOPE_ITEM - 1)); ]; [ IsMatchingStoredScope scope_store id1 id2 id3; return (scope_store-->1 == id1 && scope_store-->2 == id2 && scope_store-->3 == id3); ]; !------ SETTING UP----------- [ ClearStoredScope i; for (i = 0 : i < NUM_STORED_SCOPE : i++) (scope_stack-->i)-->0 = 0; stored_scope = 0; ]; [ InitialiseStoredScope scope_id1 scope_id2 scope_id3 i; ! do we have this one? stored_scope = -1; for (i = 0 : i < NUM_STORED_SCOPE : i++) if (IsMatchingStoredScope(scope_stack-->i, scope_id1, scope_id2, scope_id3 ) || IsEmptyStoredScope(scope_stack-->i)) { #ifdef DEBUG_SCOPE_STACK; glk_set_window( (+main-window+).ref_number); print "[Using/Restoring scope_stack ", i, ".]^"; #endif; LabelScopeStore(scope_stack-->i, scope_id1, scope_id2, scope_id3); rtrue; } #ifdef DEBUG; glk_set_window( (+main-window+).ref_number); print "****PROGRAMMING ERROR: Exceeded NUM_STORED_SCOPE (current limit is ", NUM_STORED_SCOPE, ". Please increase.)****^"; #endif; ClearStoredScope(); LabelScopeStore(scope_stack-->0, scope_id1, scope_id2, scope_id3); rtrue; ]; [ LabelScopeStore scope_store scope_id1 scope_id2 scope_id3; stored_scope = scope_store; if (IsEmptyStoredScope(scope_store)) { stored_scope-->1 = scope_id1; stored_scope-->2 = scope_id2; stored_scope-->3 = scope_id3; stored_scope-->0 = FIRST_STORED_SCOPE_ITEM - 1; } ]; #endif; -) before "Noun Domain" in "Parser.i6t". Include (- [ MatchTextAgainstObject item i; # ifndef SCOPE_STACK_OPTIMISATION; if (match_from <= num_words) { ! If there's any text to match, that is wn = match_from; i = NounWord(); if ((i == 1) && (player == item)) MakeMatch(item, 1); ! "me" if ((i >= 2) && (i < 128) && (LanguagePronouns-->i == item)) MakeMatch(item, 1); } #endif; ! Construing the current word as the start of a noun, can it refer to the ! object? wn = match_from; if (TryGivenObject(item) > 0) if (indef_nspec_at > 0 && match_from ~= indef_nspec_at) { ! This case arises if the player has typed a number in ! which is hypothetically an indefinite descriptor: ! e.g. "take two clubs". We have just checked the object ! against the word "clubs", in the hope of eventually finding ! two such objects. But we also backtrack and check it ! against the words "two clubs", in case it turns out to ! be the 2 of Clubs from a pack of cards, say. If it does ! match against "two clubs", we tear up our original ! assumption about the meaning of "two" and lapse back into ! definite mode. wn = indef_nspec_at; if (TryGivenObject(item) > 0) { match_from = indef_nspec_at; ResetDescriptors(); } wn = match_from; } ]; -) instead of "Parsing Object Names" in "Parser.i6t" Chapter 2 - Caching the matched lists Include (- !-------- MATCH LIST DEFINITIONS ------------ Constant NUM_MATCH_LISTS = 12; Constant MAX_MATCHED_LIST = 32; Array matched_list1 --> MAX_MATCHED_LIST + 2; Array matched_list2 --> MAX_MATCHED_LIST + 2; Array matched_list3 --> MAX_MATCHED_LIST + 2; Array matched_list4 --> MAX_MATCHED_LIST + 2; Array matched_list5 --> MAX_MATCHED_LIST + 2; Array matched_list6 --> MAX_MATCHED_LIST + 2; Array matched_list7 --> MAX_MATCHED_LIST + 2; Array matched_list8 --> MAX_MATCHED_LIST + 2; Array matched_list9 --> MAX_MATCHED_LIST + 2; Array matched_list10 --> MAX_MATCHED_LIST + 2; Array matched_list11 --> MAX_MATCHED_LIST + 2; Array matched_list12 --> MAX_MATCHED_LIST + 2; Array match_list_stack --> matched_list1 matched_list2 matched_list3 matched_list4 matched_list5 matched_list6 matched_list7 matched_list8 matched_list9 matched_list10 matched_list11 matched_list12; !-------------- STORE AND RETRIEVE THE MATCH LIST ----------------- [ StoreMatchedList store_match_list i; ! ID = store_stack / match_from / last_safe_word store_match_list-->0 = number_matched + FIRST_STORED_SCOPE_ITEM - 1; store_match_list-->1 = stored_scope; store_match_list-->2 = match_from; store_match_list-->3 = cursor_word; for (i = 0 : i < number_matched && i < MAX_MATCHED_LIST: i++) store_match_list-->( FIRST_STORED_SCOPE_ITEM + i) = match_list-->i; #ifdef DEBUG_MATCH_LISTS; glk_set_Window( (+main-window +).ref_number); print "[Storing match list ", (MatchListN) store_match_list, " with ss = ", stored_scope, ",", number_matched, " items, mf = ", match_from, " and cw = ", cursor_word, ".]^"; SpitMatchList(store_match_list); #endif; ]; #ifdef DEBUG; [ SpitMatchList sml i; for (i = FIRST_STORED_SCOPE_ITEM : i <= sml-->0 && i < MAX_MATCHED_LIST: i++) print (name) sml--> i, "... "; new_line; ]; [ MatchListN sml i; for(i = 0: i < NUM_MATCH_LISTS : i++) if (match_list_stack-->i == sml) { print i; rtrue; } ]; #endif; [ ApplyExistingMatchList id1 id2 id3ormore i; for(i = 0: i < NUM_MATCH_LISTS : i++) if (IsMatchList(match_list_stack-->i, id1, id2, id3ormore)) { #ifdef DEBUG_MATCH_LISTS; glk_set_Window( (+main-window +).ref_number); print "[Restoring match list ", i, ".] "; SpitMatchList(match_list_stack-->i); #endif; return (match_list_stack-->i); } #ifdef DEBUG_MATCH_LISTS; glk_set_Window( (+main-window +).ref_number); print "[No match list located.]^"; #endif; return stored_scope; ]; [ IsMatchList mlist id1 id2 id3ormore; return (mlist-->1 == id1 && mlist-->2 == id2 && mlist-->3 <= id3ormore); ]; [ FirstEmptyMatchList i; for (i = 0 : i < NUM_MATCH_LISTS : i++) if ((match_list_stack-->i)-->0 == 0) return match_list_stack-->i; #ifdef DEBUG; glk_set_window( (+main-window+).ref_number); print "*** PROGRAMMING ERROR: NUM_MATCH_LISTS has been exceeded (currently ", NUM_MATCH_LISTS, ".) Please increase!***^"; #endif; return match_list_stack-->0; ]; [ ClearMatchLists i; for (i = 0 : i < NUM_MATCH_LISTS : i++) (match_list_stack-->i)-->0 = 0; ]; -) before "Noun Domain" in "Parser.i6t". Book 5 - Parser with added tracking information [ Here's where we start rewriting the internals. If you're adapting IP to work with other extensions, this is where you need to look. There are no structural or flow changes here. All we do is record how far we got on each pass through, and additional information like what we were hoping to find next, for use in making suggestions. ] Chapter 1 - Parser Section 0 - Silence the advance parser's clarification messages Include (- [ PrintInferredCommand from singleton_noun; ! ------------IP CHANGE HERE----------------- if (pre_command == INACTIVE) { ! ------------IP CHANGE DONE----------------- singleton_noun = FALSE; if ((from ~= 0) && (from == pcount-1) && (pattern-->from > 1) && (pattern-->from < REPARSE_CODE)) singleton_noun = TRUE; if (singleton_noun) { BeginActivity(CLARIFYING_PARSERS_CHOICE_ACT, pattern-->from); if (ForActivity(CLARIFYING_PARSERS_CHOICE_ACT, pattern-->from) == 0) { print "("; PrintCommand(from); print ")^"; } EndActivity(CLARIFYING_PARSERS_CHOICE_ACT, pattern-->from); } else { print "("; PrintCommand(from); print ")^"; } ! ------------IP CHANGE HERE----------------- } ! ------------IP CHANGE DONE----------------- ]; [ PrintCommand from i k spacing_flag; ! ------------IP CHANGE HERE----------------- if (pre_command == INACTIVE) { ! ------------IP CHANGE DONE----------------- if (from == 0) { i = verb_word; if (LanguageVerb(i) == 0) if (PrintVerb(i) == 0) print (address) i; from++; spacing_flag = true; } for (k=from : kk; if (i == PATTERN_NULL) continue; if (spacing_flag) print (char) ' '; if (i == 0) { print (string) THOSET__TX; jump TokenPrinted; } if (i == 1) { print (string) THAT__TX; jump TokenPrinted; } if (i >= REPARSE_CODE) print (address) VM_NumberToDictionaryAddress(i-REPARSE_CODE); else if (i ofclass K3_direction) print (LanguageDirection) i; ! the direction name as adverb else print (the) i; .TokenPrinted; spacing_flag = true; } ! ------------IP CHANGE HERE----------------- } ! ------------IP CHANGE DONE----------------- ]; -) instead of "Print Command" in "Parser.i6t". Section 1 - Intro Include (- if (held_back_mode == 1) { held_back_mode = 0; VM_Tokenise(buffer, parse); jump ReParse; } .ReType; ! ------------IP CHANGE HERE----------------- if (pre_command == ERROR_GIVEN) return; ! ------------IP CHANGE DONE----------------- cobj_flag = 0; actors_location = ScopeCeiling(player); ! ------------IP CHANGE HERE----------------- if (pre_command == INACTIVE) { ! ------------IP CHANGE DONE----------------- BeginActivity(READING_A_COMMAND_ACT); if (ForActivity(READING_A_COMMAND_ACT)==false) { Keyboard(buffer,parse); players_command = 100 + WordCount(); num_words = WordCount(); } if (EndActivity(READING_A_COMMAND_ACT)) jump ReType; ! ------------IP CHANGE HERE----------------- } ! ------------IP CHANGE DONE----------------- .ReParse; ! ------------IP CHANGE HERE----------------- if (pre_command == RETURN_TO_INPUT_LINE) { pre_command = INACTIVE; jump ReType; } ! ------------IP CHANGE DONE----------------- parser_inflection = name; ! Initially assume the command is aimed at the player, and the verb ! is the first word num_words = WordCount(); wn = 1; #Ifdef LanguageToInformese; LanguageToInformese(); ! Re-tokenise: VM_Tokenise(buffer,parse); #Endif; ! LanguageToInformese num_words = WordCount(); k=0; #Ifdef DEBUG; if (parser_trace >= 2) { print "[ "; for (i=0 : i(i*2 + 1); #Ifnot; ! TARGET_GLULX j = parse-->(i*3 + 1); #Endif; ! TARGET_ k = WordAddress(i+1); l = WordLength(i+1); print "~"; for (m=0 : mm; print "~ "; if (j == 0) print "?"; else { #Ifdef TARGET_ZCODE; if (UnsignedCompare(j, HDR_DICTIONARY-->0) >= 0 && UnsignedCompare(j, HDR_HIGHMEMORY-->0) < 0) print (address) j; else print j; #Ifnot; ! TARGET_GLULX if (j->0 == $60) print (address) j; else print j; #Endif; ! TARGET_ } if (i ~= num_words-1) print " / "; } print " ]^"; } #Endif; ! DEBUG verb_wordnum = 1; actor = player; actors_location = ScopeCeiling(player); usual_grammar_after = 0; .AlmostReParse; scope_token = 0; action_to_be = NULL; ! Begin from what we currently think is the verb word .BeginCommand; wn = verb_wordnum; verb_word = NextWordStopped(); ! If there's no input here, we must have something like "person,". if (verb_word == -1) { best_etype = STUCK_PE; jump GiveError; } ! Now try for "again" or "g", which are special cases: don't allow "again" if nothing ! has previously been typed; simply copy the previous text across ! ------------IP CHANGE HERE----------------- if (pre_command == INACTIVE) { ! ------------IP CHANGE DONE----------------- if (verb_word == AGAIN2__WD or AGAIN3__WD) verb_word = AGAIN1__WD; if (verb_word == AGAIN1__WD) { if (actor ~= player) { L__M(##Miscellany, 20); jump ReType; } #Ifdef TARGET_ZCODE; if (buffer3->1 == 0) { L__M(##Miscellany, 21); jump ReType; } #Ifnot; ! TARGET_GLULX if (buffer3-->0 == 0) { L__M(##Miscellany, 21); jump ReType; } #Endif; ! TARGET_ for (i=0 : ii = buffer3->i; VM_Tokenise(buffer,parse); num_words = WordCount(); players_command = 100 + WordCount(); jump ReParse; } ! Save the present input in case of an "again" next time if (verb_word ~= AGAIN1__WD) for (i=0 : ii = buffer->i; ! ------------IP CHANGE HERE----------------- } ! ------------IP CHANGE DONE----------------- 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 if ((i ~= 0 or 1) && (VM_InvalidDictionaryAddress(i))) { usual_grammar_after = verb_wordnum; i=-i; } if (i == 1) { parser_results-->ACTION_PRES = action; parser_results-->NO_INPS_PRES = 0; parser_results-->INP1_PRES = noun; parser_results-->INP2_PRES = second; if (noun) parser_results-->NO_INPS_PRES = 1; if (second) parser_results-->NO_INPS_PRES = 2; rtrue; } if (i ~= 0) { verb_word = i; wn--; verb_wordnum--; } else { wn = verb_wordnum; verb_word = NextWord(); } } else usual_grammar_after = 0; -) instead of "Parser Letter A" in "Parser.i6t". Section 2 - Verbs Include (- ! 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; } } jump NotConversation; ! 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; ! Now j = word number of the comma itself ! ------------IP CHANGE HERE----------------- MatchUpTo(j, EX_VERB, "Verb not found, was it conversation?"); ! ------------IP CHANGE DONE----------------- if (j == 1) { L__M(##Miscellany, 22); ! ------------IP CHANGE HERE----------------- if (pre_command == PERFORMING) pre_command = ERROR_GIVEN; MatchUpTo(0, EX_VERB, "Not conversation!"); ! ------------IP CHANGE DONE----------------- 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) { if (verb_word && ((verb_word->#dict_par1) & 1)) jump NotConversation; L__M(##Miscellany, 23); ! ------------IP CHANGE HERE----------------- if (pre_command == PERFORMING) pre_command = ERROR_GIVEN; ! ------------IP CHANGE DONE----------------- ! No MatchUpTo as we've used Noun Domain 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); ! ------------IP CHANGE HERE----------------- if (pre_command == PERFORMING) pre_command = ERROR_GIVEN; ! ------------IP CHANGE DONE----------------- 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) { if (verb_word && ((verb_word->#dict_par1) & 1)) jump NotConversation; L__M(##Miscellany, 25); ! ------------IP CHANGE HERE----------------- if (pre_command == PERFORMING) pre_command = ERROR_GIVEN; ! ------------IP CHANGE DONE----------------- 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); ! ------------IP CHANGE HERE----------------- if (pre_command == PERFORMING) pre_command = ERROR_GIVEN; ! ------------IP CHANGE DONE----------------- 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; -) instead of "Parser Letter C" in "Parser.i6t". Section - Letter F Include (- advance_warning = -1; ! ------------IP CHANGE HERE----------------- if (pre_command == INACTIVE) { ! ------------IP CHANGE DONE----------------- 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) { ! skip ahead to a preposition word in the input do { l = NextWord(); } until ((wn > num_words) || (l && (l->#dict_par1) & 8 ~= 0)); if (wn > num_words) { #Ifdef DEBUG; if (parser_trace >= 2) print " [Look-ahead aborted: prepositions missing]^"; #Endif; jump LineFailed; } do { if (PrepositionChain(l, pcount) ~= -1) { ! advance past the chain if ((line_token-->pcount)->0 & $20 ~= 0) { pcount++; while ((line_token-->pcount ~= ENDIT_TOKEN) && ((line_token-->pcount)->0 & $10 ~= 0)) pcount++; } else { pcount++; } } else { ! try to find another preposition word do { l = NextWord(); } until ((wn >= num_words) || (l && (l->#dict_par1) & 8 ~= 0)); if (l && (l->#dict_par1) & 8) continue; ! lookahead failed #Ifdef DEBUG; if (parser_trace >= 2) print " [Look-ahead aborted: prepositions don't match]^"; #endif; jump LineFailed; } l = NextWord(); } until (line_ttype-->pcount ~= PREPOSITION_TT); ! put back the non-preposition we just read wn--; if ((line_ttype-->pcount == ELEMENTARY_TT) && (line_tdata-->pcount == NOUN_TOKEN)) { l = Descriptors(); ! skip past THE etc if (l~=0) etype=l; ! don't allow multiple objects k = parser_results-->INP1_PRES; @push k; @push parameters; parameters = 1; parser_results-->INP1_PRES = 0; l = NounDomain(actors_location, actor, NOUN_TOKEN); @pull parameters; @pull k; parser_results-->INP1_PRES = k; #Ifdef DEBUG; if (parser_trace >= 2) { print " [Advanced to ~noun~ token: "; if (l == REPARSE_CODE) print "re-parse request]^"; else { 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; } } } ! ------------IP CHANGE HERE----------------- } ! ------------IP CHANGE DONE----------------- ! 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!). inferfrom = 0; parameters = 0; nsns = 0; special_word = 0; multiple_object-->0 = 0; etype = STUCK_PE; wn = verb_wordnum+1; -) instead of "Parser Letter F" in "Parser.i6t". Section 3 - Tokens Include (- 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) { ! ------------IP CHANGE HERE----------------- MatchUpTo(wn-1, EX_NOUN, "Parse Token going round the loop."); ! we've gotten this far, right? ! ------------IP CHANGE DONE----------------- scope_reason = PARSING_REASON; AnalyseToken(token); l = ParseToken(found_ttype, found_tdata, pcount-1, token); while ((l >= GPR_NOUN) && (l < -1)) 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; parser_results-->(parameters+INP1_PRES) = 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 { MatchUpTo(wn-2, EX_ENDOFLINE, "Grammar line's run out but text hasn't"); ! why is it -2? Because we've just done a NextWord on the line above ( +1) and we're on a word that didn't match ( + 1). So we matched 2. 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 && parser_results-->INP1_PRES == 0) { l = ReviseMulti(parser_results-->INP2_PRES); if (l ~= 0) { etype = l; parser_results-->ACTION_PRES = action_to_be; break; } } if (parameters >= 2 && parser_results-->INP2_PRES == 0) { l = ReviseMulti(parser_results-->INP1_PRES); 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 && parser_results-->INP1_PRES == 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) { PrintInferredCommand(inferfrom); ClearParagraphing(); } ! ...copy the action number, and the number of parameters... parser_results-->ACTION_PRES = action_to_be; parser_results-->NO_INPS_PRES = parameters; ! ...reverse first and second parameters if need be... if (action_reversed && parameters == 2) { i = parser_results-->INP1_PRES; parser_results-->INP1_PRES = parser_results-->INP2_PRES; parser_results-->INP2_PRES = 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 && parser_results-->INP1_PRES >= 2) PronounNotice(parser_results-->INP1_PRES); ! ...and return from the parser altogether, having successfully matched ! a line. if (held_back_mode == 1) { wn=hb_wn; jump LookForMore; } ! ------------IP CHANGE HERE----------------- MatchUpTo(wn-1, EX_ENDOFLINE, "Parser matched"); ! wn as next word has taken us past the end of input ! ------------IP CHANGE DONE----------------- rtrue; } ! end of if(token ~= ENDIT_TOKEN) else } ! end of for(pcount++) .LineFailed; ! 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. -) instead of "Parser Letter G" in "Parser.i6t". Section 4 - Errors Include (- ! 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. ! ------------IP CHANGE HERE----------------- if (pre_command == PERFORMING) { if (etype == VERB_PE) { MatchUpTo(0, EX_VERB, "Verb Error"); } pre_command = ERROR_GIVEN; jump reType; } ! ------------IP CHANGE DONE----------------- if ((etype ofclass Routine) || (etype ofclass String)) { if (ParserError(etype) ~= 0) jump ReType; } else { if (verb_wordnum == 0 && etype == CANTSEE_PE) etype = VERB_PE; players_command = 100 + WordCount(); ! The snippet variable ``player's command'' BeginActivity(PRINTING_A_PARSER_ERROR_ACT); if (ForActivity(PRINTING_A_PARSER_ERROR_ACT)) jump SkipParserError; } 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 (parser_results-->ACTION_PRES == ##Remove && parser_results-->INP2_PRES ofclass Object) { noun = parser_results-->INP2_PRES; ! 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 parser_results-->ACTION_PRES = 0; } if (parser_results-->ACTION_PRES ~= ##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; if (~~((etype ofclass Routine) || (etype ofclass String))) EndActivity(PRINTING_A_PARSER_ERROR_ACT); jump GiveError; } } if (etype == NOTINCONTEXT_PE) L__M(##Miscellany, 73); .SkipParserError; if ((etype ofclass Routine) || (etype ofclass String)) jump ReType; say__p = 1; EndActivity(PRINTING_A_PARSER_ERROR_ACT); -) instead of "Parser Letter I" in "Parser.i6t". Section - Letter K Include (- ! 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) { ! ------------IP CHANGE HERE----------------- MatchUpTo(num_words, EX_ENDOFLINE, "We've run out of text to read"); ! ------------IP CHANGE DONE----------------- rtrue; } i = NextWord(); if (i == THEN1__WD or THEN2__WD or THEN3__WD or comma_word) { if (wn > num_words) { held_back_mode = false; return; } i = WordAddress(verb_wordnum); j = WordAddress(wn); for (: i0 = ' '; i = NextWord(); if (i == AGAIN1__WD or AGAIN2__WD or AGAIN3__WD) { ! Delete the words "then again" from the again buffer, ! in which we have just realised that it must occur: ! prevents an infinite loop on "i. again" i = WordAddress(wn-2)-buffer; if (wn > num_words) j = INPUT_BUFFER_LEN-1; else j = WordAddress(wn)-buffer; for (: ii = ' '; } VM_Tokenise(buffer,parse); held_back_mode = true; return; } best_etype = UPTO_PE; jump GiveError; -) instead of "Parser Letter K" in "Parser.i6t". Chapter 2 - Parse Token Section - Parse Token A Include (- token_filter = 0; parser_inflection = name; switch (given_ttype) { ELEMENTARY_TT: switch (given_tdata) { SPECIAL_TOKEN: l = TryNumber(wn); special_word = NextWord(); #Ifdef DEBUG; if (l ~= -1000) if (parser_trace >= 3) print " [Read special as the number ", l, "]^"; #Endif; ! DEBUG if (l == -1000) { #Ifdef DEBUG; if (parser_trace >= 3) print " [Read special word at word number ", wn, "]^"; #Endif; ! DEBUG l = special_word; } parsed_number = l; return GPR_NUMBER; NUMBER_TOKEN: l=TryNumber(wn++); if (l == -1000) { etype = NUMBER_PE; return GPR_FAIL; } #Ifdef DEBUG; if (parser_trace>=3) print " [Read number as ", l, "]^"; #Endif; ! DEBUG parsed_number = l; return GPR_NUMBER; CREATURE_TOKEN: if (action_to_be == ##Answer or ##Ask or ##AskFor or ##Tell) scope_reason = TALKING_REASON; TOPIC_TOKEN: consult_from = wn; if ((line_ttype-->(token_n+1) ~= PREPOSITION_TT) && (line_token-->(token_n+1) ~= ENDIT_TOKEN)) RunTimeError(13); do o = NextWordStopped(); until (o == -1 || PrepositionChain(o, token_n+1) ~= -1); wn--; consult_words = wn-consult_from; if (consult_words == 0) return GPR_FAIL; if (action_to_be == ##Ask or ##Answer or ##Tell) { o = wn; wn = consult_from; parsed_number = NextWord(); wn = o; return 1; } if (o==-1 && (line_ttype-->(token_n+1) == PREPOSITION_TT)) return GPR_FAIL; ! don't infer if required preposition is absent return GPR_PREPOSITION; } PREPOSITION_TT: ! Is it an unnecessary alternative preposition, when a previous choice ! has already been matched? if ((token->0) & $10) return GPR_PREPOSITION; ! If we've run out of the player's input, but still have parameters to ! specify, we go into "infer" mode, remembering where we are and the ! preposition we are inferring... if (wn > num_words) { if (inferfrom==0 && parameterspcount = REPARSE_CODE + VM_DictionaryAddressToNumber(given_tdata); } ! If we are not inferring, then the line is wrong... if (inferfrom == 0) { ! this is where we end up when typing "PUT BOOK". How do we ! know we're not going to infer? return -1; } ! If not, then the line is right but we mark in the preposition... pattern-->pcount = REPARSE_CODE + VM_DictionaryAddressToNumber(given_tdata); return GPR_PREPOSITION; } o = NextWord(); pattern-->pcount = REPARSE_CODE + VM_DictionaryAddressToNumber(o); ! Whereas, if the player has typed something here, see if it is the ! required preposition... if it's wrong, the line must be wrong, ! but if it's right, the token is passed (jump to finish this token). if (o == given_tdata) { ! ------------IP CHANGE HERE----------------- !glk_set_window((+main-window+).ref_number); !print "Matched a solo preposition : word num = ", wn-1, ".^"; MatchUpTo(wn - 1, EX_NOUN, "Matched solo prep.^"); ! ------------IP CHANGE DONE----------------- return GPR_PREPOSITION; } if (PrepositionChain(o, token_n) ~= -1) { ! ------------IP CHANGE HERE----------------- !glk_set_window((+main-window+).ref_number); !print "Matched a alternate preposition : word num = ", wn-1, ".^"; MatchUpTo(wn - 1, EX_NOUN, "Matched chained prep.^"); ! ------------IP CHANGE DONE----------------- return GPR_PREPOSITION; } return -1; GPR_TT: l = indirect(given_tdata); #Ifdef DEBUG; if (parser_trace >= 3) print " [Outside parsing routine returned ", l, "]^"; #Endif; ! DEBUG return l; SCOPE_TT: scope_token = given_tdata; scope_stage = 1; #Ifdef DEBUG; if (parser_trace >= 3) print " [Scope routine called at stage 1]^"; #Endif; ! DEBUG l = indirect(scope_token); #Ifdef DEBUG; if (parser_trace >= 3) print " [Scope routine returned multiple-flag of ", l, "]^"; #Endif; ! DEBUG if (l == 1) given_tdata = MULTI_TOKEN; else given_tdata = NOUN_TOKEN; ATTR_FILTER_TT: token_filter = 1 + given_tdata; given_tdata = NOUN_TOKEN; ROUTINE_FILTER_TT: token_filter = given_tdata; given_tdata = NOUN_TOKEN; } ! end of switch(given_ttype) token = given_tdata; -) instead of "Parse Token Letter A" in "Parser.i6t". Section - Parse Token F [ NOTE, it would appear there's actually no changes in this section at all. ] Include (- ! Happy or unhappy endings: .PassToken; if (many_flag) { single_object = GPR_MULTIPLE; multi_context = token; } else { if (indef_mode == 1 && indef_type & PLURAL_BIT ~= 0) { if (indef_wanted < INDEF_ALL_WANTED && indef_wanted > 1) { multi_had = 1; multi_wanted = indef_wanted; etype = TOOFEW_PE; jump FailToken; } } } return single_object; .FailToken; ! If we were only guessing about it being a plural, try again but only ! allowing singulars (so that words like "six" are not swallowed up as ! Descriptors) if (allow_plurals && indef_guess_p == 1) { #Ifdef DEBUG; if (parser_trace >= 4) print " [Retrying singulars after failure ", etype, "]^"; #Endif; prev_indef_wanted = indef_wanted; allow_plurals = false; wn = desc_wn; jump TryAgain; } if ((indef_wanted > 0 || prev_indef_wanted > 0) && (~~multiflag)) etype = MULTI_PE; ! ------------IP CHANGE HERE----------------- ! - NOTE, it would appear there's actually no changes in this section at all. ! MatchUpTo(wn - 2, EX_NOUN, "Parser token failed (using wn-2. Is this arbitrary?)"); ! why -2? Assumes we've failed on the first word (I think this is true, otherwise we get an "UPTO" error instead. ! ------------IP CHANGE DONE----------------- return GPR_FAIL; ]; ! end of ParseToken__ -) instead of "Parse Token Letter F" in "Parser.i6t" Section 2 - Descriptors Include (- [ Descriptors o x flag cto type n; 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 = INDEF_ALL_WANTED; if (take_all_rule == 1) take_all_rule = 2; indef_type = indef_type | PLURAL_BIT; } if (allow_plurals) { if (NextWordStopped() ~= -1) { wn--; n = TryNumber(wn-1); } else { n=0; wn--; } if (n == 1) { indef_mode = 1; flag = 1; } if (n > 1) { indef_guess_p = 1; indef_mode = 1; flag = 1; indef_wanted = n; indef_nspec_at = wn-1; indef_type = indef_type | PLURAL_BIT; } } if (flag == 1 && NextWordStopped() ~= OF1__WD or OF2__WD or OF3__WD or OF4__WD) wn--; ! Skip 'of' after these } wn--; ! ------------IP CHANGE HERE----------------- MatchUpTo(wn-1, EX_NOUN, "Descriptors"); ! ------------IP CHANGE DONE----------------- return 0; ]; [ SafeSkipDescriptors; @push indef_mode; @push indef_type; @push indef_wanted; @push indef_guess_p; @push indef_possambig; @push indef_owner; @push indef_cases; @push indef_nspec_at; Descriptors(); @pull indef_nspec_at; @pull indef_cases; @pull indef_owner; @pull indef_possambig; @pull indef_guess_p; @pull indef_wanted; @pull indef_type; @pull indef_mode; ]; -) instead of "Parsing Descriptors" in "Parser.i6t". Section 1 - NounDomain Include (- [ MatchUpTo wordnum type debugprint; if (type == EX_ENDOFLINE && inferfrom == 0) command_in_full = true; if (word_understood_to < wordnum || type == EX_VERB) { #ifdef DEBUG_ADVANCE_PARSING; glk_set_window( (+main-window+).ref_number ); print "Establishing ", wordnum, " words and token type ", type, ".^"; print (string) debugprint, "^"; #endif; word_understood_to = wordnum; next_token_type = type; } ]; [ NounDomain domain1 domain2 context scope_to_test 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 == INDEF_ALL_WANTED) print "all"; else print indef_wanted; new_line; print " most likely GNAs of names: ", indef_cases, "^"; } else print "seeking definite object^"; } #Endif; ! DEBUG match_length = 0; number_matched = 0; match_from = wn; #ifdef SCOPE_STACK_OPTIMISATION; InitialiseStoredScope(domain1, domain2, context); if (IsVanillaStoredScope(stored_scope)) SearchScope(domain1, domain2, context); ! Do we have a match_list that matches this scope / match_from, and its cursor word was previous to ours? scope_to_test = ApplyExistingMatchList(stored_scope, match_from, cursor_word); TestScopeList(scope_to_test); if (cursor_word > match_from) ! so we are working on at least some input that is non-volatile { if (scope_to_test == stored_scope) StoreMatchedList(FirstEmptyMatchList()); else StoreMatchedList(scope_to_test); } #ifnot; SearchScope(domain1, domain2, context); #endif; #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; } ! ------------IP CHANGE HERE----------------- if (number_matched > 1 && pre_command == PERFORMING) { !glk_set_window((+main-window+).ref_number); !print "Multilmatch : wn = ", wn, " but we record wn-1 = ", wn-1, ".^"; MatchUpTo(wn-1, EX_PREPOSITION, "Multiple match in NounDomain. We return the first and move on."); return match_list-->0; } ! ------------IP CHANGE DONE----------------- ! 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) { ! ------------IP CHANGE HERE----------------- !glk_set_window((+main-window+).ref_number); !print "Splurge text on end of line : wn = ", wn, " but we record wn-2 = ", wn-2, ".^"; MatchUpTo(wn-1, EX_ENDOFLINE, "End of text input (Noun domain)"); ! ------------IP CHANGE DONE----------------- 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 = true; if (number_matched > 1) for (j=0 : jj, match_list-->(j+1)) == false) i = false; if (i) dont_infer = true; i = Adjudicate(context); if (i == -1) rfalse; if (i == 1) rtrue; ! Adjudicate has made a multiple ! object, and we pass it on } ! 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.) ! ------------IP CHANGE HERE----------------- #ifdef DEBUG_ADVANCE_PARSING; glk_set_window((+main-window+).ref_number); print "Noun Domain: matched ", (the) i, " with wn = ", wn-1, ". Inferfrom == ", inferfrom, " or ", pcount, "?"; #endif; MatchUpTo(wn-1, EX_NOUN, "NounDomain"); ! really? or preposition? ! ------------IP CHANGE DONE----------------- if (i ~= 0) { if (dont_infer) return i; if (inferfrom == 0) inferfrom=pcount; pattern-->pcount = i; ! ------------IP CHANGE HERE----------------- next_expected_word = i; ! ------------IP CHANGE DONE----------------- return i; } ! ------------IP CHANGE HERE----------------- if (pre_command == PERFORMING) rfalse; ! ------------IP CHANGE DONE----------------- ! 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) { ! ------------IP CHANGE HERE----------------- MatchUpTo(num_words, EX_NOUN, "ND: This one's never reached."); ! ------------IP CHANGE DONE----------------- jump Incomplete; } ! Now we print up the question, using the equivalence classes as worked ! out by Adjudicate() so as not to repeat ourselves on plural objects... BeginActivity(ASKING_WHICH_DO_YOU_MEAN_ACT); if (ForActivity(ASKING_WHICH_DO_YOU_MEAN_ACT)) jump SkipWhichQuestion; j = 1; marker = 0; for (i=1 : i<=number_of_classes : i++) { while (((match_classes-->marker) ~= i) && ((match_classes-->marker) ~= -i)) marker++; if (match_list-->marker hasnt animate) j = 0; } if (j) 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) { #Ifdef SERIAL_COMMA; if (j ~= 2) print ","; #Endif; ! SERIAL_COMMA print (string) OR__TX; } } L__M(##Miscellany, 57); .SkipWhichQuestion; EndActivity(ASKING_WHICH_DO_YOU_MEAN_ACT); ! ...and get an answer: .WhichOne; ! ------------IP CHANGE HERE----------------- ! Under IP, we don't do questions in this way. Instead, we record where in the input line the player's text is going to ! go, and then we give them an input line to edit if ((+current text input window+) == (+key-window+)) { CreateEditPoint(CharacterNumber(match_from) - 1); pre_command = RETURN_TO_INPUT_LINE; return REPARSE_CODE; } ! ------------IP CHANGE DONE----------------- #Ifdef TARGET_ZCODE; for (i=2 : ii = ' '; #Endif; ! TARGET_ZCODE answer_words=Keyboard(buffer2, parse2); ! Conveniently, parse2-->1 is the first word in both ZCODE and GLULX. first_word = (parse2-->1); ! Take care of "all", because that does something too clever here to do ! later on: if (first_word == ALL1__WD or ALL2__WD or ALL3__WD or ALL4__WD or ALL5__WD) { if (context == MULTI_TOKEN or MULTIHELD_TOKEN or MULTIEXCEPT_TOKEN or MULTIINSIDE_TOKEN) { l = multiple_object-->0; for (i=0 : ii; multiple_object-->(i+1+l) = k; } multiple_object-->0 = i+l; rtrue; } L__M(##Miscellany, 47); jump WhichOne; } ! Look for a comma, and interpret this as a fresh conversation command ! if so: for (i=1 : i<=answer_words : i++) if (WordFrom(i, parse2) == comma_word) { VM_CopyBuffer(buffer, buffer2); jump RECONSTRUCT_INPUT; } ! If the first word of the reply can be interpreted as a verb, then ! assume that the player has ignored the question and given a new ! command altogether. ! (This is one time when it's convenient that the directions are ! not themselves verbs - thus, "north" as a reply to "Which, the north ! or south door" is not treated as a fresh command but as an answer.) #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)) { VM_CopyBuffer(buffer, buffer2); jump RECONSTRUCT_INPUT; } } ! Now we insert the answer into the original typed command, as ! words additionally describing the same object ! (eg, > take red button ! Which one, ... ! > music ! becomes "take music red button". The parser will thus have three ! words to work from next time, not two.) #Ifdef TARGET_ZCODE; k = WordAddress(match_from) - buffer; l=buffer2->1+1; for (j=buffer + buffer->0 - 1 : j>=buffer+k+l : j-- ) j->0 = 0->(j-l); for (i=0 : i(k+i) = buffer2->(2+i); buffer->(k+l-1) = ' '; buffer->1 = buffer->1 + l; if (buffer->1 >= (buffer->0 - 1)) buffer->1 = buffer->0; #Ifnot; ! TARGET_GLULX k = WordAddress(match_from) - buffer; l = (buffer2-->0) + 1; for (j=buffer+INPUT_BUFFER_LEN-1 : j>=buffer+k+l : j-- ) j->0 = j->(-l); for (i=0 : i(k+i) = buffer2->(WORDSIZE+i); buffer->(k+l-1) = ' '; buffer-->0 = buffer-->0 + l; if (buffer-->0 > (INPUT_BUFFER_LEN-WORDSIZE)) buffer-->0 = (INPUT_BUFFER_LEN-WORDSIZE); #Endif; ! TARGET_ ! Having reconstructed the input, we warn the parser accordingly ! and get out. .RECONSTRUCT_INPUT; num_words = WordCount(); wn = 1; #Ifdef LanguageToInformese; LanguageToInformese(); ! Re-tokenise: VM_Tokenise(buffer,parse); #Endif; ! LanguageToInformese num_words = WordCount(); players_command = 100 + WordCount(); actors_location = ScopeCeiling(player); FollowRulebook(Activity_after_rulebooks-->READING_A_COMMAND_ACT, true); 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); ! ------------IP CHANGE HERE----------------- ! Under IP, we don't do questions in this way. Instead, we record where in the input line the player's text is going to ! go, and then we give them an input line to edit if ((+current text input window+) == (+key-window+)) { ! we had a sneaky line break. print "^"; CreateEditPoint((buffer-->0) + 1); pre_command = RETURN_TO_INPUT_LINE; return REPARSE_CODE; } ! ------------IP CHANGE DONE----------------- #Ifdef TARGET_ZCODE; for (i=2 : ii=' '; #Endif; ! TARGET_ZCODE answer_words = Keyboard(buffer2, parse2); first_word=(parse2-->1); #Ifdef LanguageIsVerb; if (first_word==0) { j = wn; first_word=LanguageIsVerb(buffer2, parse2, 1); wn = j; } #Endif; ! LanguageIsVerb ! Once again, if the reply looks like a command, give it to the ! parser to get on with and forget about the question... if (first_word ~= 0) { j = first_word->#dict_par1; if (0 ~= j&1) { VM_CopyBuffer(buffer, buffer2); return REPARSE_CODE; } } ! ...but if we have a genuine answer, then: ! ! (1) we must glue in text suitable for anything that's been inferred. if (inferfrom ~= 0) { for (j=inferfrom : jj == PATTERN_NULL) continue; #Ifdef TARGET_ZCODE; i = 2+buffer->1; (buffer->1)++; buffer->(i++) = ' '; #Ifnot; ! TARGET_GLULX i = WORDSIZE + buffer-->0; (buffer-->0)++; buffer->(i++) = ' '; #Endif; ! TARGET_ #Ifdef DEBUG; if (parser_trace >= 5) print "[Gluing in inference with pattern code ", pattern-->j, "]^"; #Endif; ! DEBUG ! Conveniently, parse2-->1 is the first word in both ZCODE and GLULX. parse2-->1 = 0; ! An inferred object. Best we can do is glue in a pronoun. ! (This is imperfect, but it's very seldom needed anyway.) if (pattern-->j >= 2 && pattern-->j < REPARSE_CODE) { PronounNotice(pattern-->j); for (k=1 : k<=LanguagePronouns-->0 : k=k+3) if (pattern-->j == LanguagePronouns-->(k+2)) { parse2-->1 = LanguagePronouns-->k; #Ifdef DEBUG; if (parser_trace >= 5) print "[Using pronoun '", (address) parse2-->1, "']^"; #Endif; ! DEBUG break; } } else { ! An inferred preposition. parse2-->1 = VM_NumberToDictionaryAddress(pattern-->j - REPARSE_CODE); #Ifdef DEBUG; if (parser_trace >= 5) print "[Using preposition '", (address) parse2-->1, "']^"; #Endif; ! DEBUG } ! parse2-->1 now holds the dictionary address of the word to glue in. if (parse2-->1 ~= 0) { k = buffer + i; #Ifdef TARGET_ZCODE; @output_stream 3 k; print (address) parse2-->1; @output_stream -3; k = k-->0; for (l=i : ll = buffer->(l+2); i = i + k; buffer->1 = i-2; #Ifnot; ! TARGET_GLULX k = Glulx_PrintAnyToArray(buffer+i, INPUT_BUFFER_LEN-i, parse2-->1); i = i + k; buffer-->0 = i - WORDSIZE; #Endif; ! TARGET_ } } } ! (2) we must glue the newly-typed text onto the end. #Ifdef TARGET_ZCODE; i = 2+buffer->1; (buffer->1)++; buffer->(i++) = ' '; for (j=0 : j1 : i++,j++) { buffer->i = buffer2->(j+2); (buffer->1)++; if (buffer->1 == INPUT_BUFFER_LEN) break; } #Ifnot; ! TARGET_GLULX i = WORDSIZE + buffer-->0; (buffer-->0)++; buffer->(i++) = ' '; for (j=0 : j0 : i++,j++) { buffer->i = buffer2->(j+WORDSIZE); (buffer-->0)++; if (buffer-->0 == INPUT_BUFFER_LEN) break; } #Endif; ! TARGET_ ! (3) we fill up the buffer with spaces, which is unnecessary, but may ! help incorrectly-written interpreters to cope. #Ifdef TARGET_ZCODE; for (: ii = ' '; #Endif; ! TARGET_ZCODE return REPARSE_CODE; ]; ! end of NounDomain -) instead of "Noun Domain" in "Parser.i6t". Section - Adding Scope Stack functionality to the Scope routines Include (- [ PlaceInScope O opts; ! If opts is set, do not place contents in scope wn = match_from; if (opts == false) DoScopeActionAndRecurse(O); else DoScopeAction(O); return; ]; [ AddToScope obj; if (ats_flag >= 2) DoScopeActionAndRecurse(obj, 0, ats_flag-2); if (ats_flag == 1) { if (HasLightSource(obj)==1) ats_hls = 1; } ]; -) instead of "Scope" in "Parser.i6t". Include (- [ DoScopeAction item; #Ifdef DEBUG; if (parser_trace >= 6) print "[DSA on ", (the) item, " with reason = ", scope_reason, " p1 = ", parser_one, " p2 = ", parser_two, "]^"; #Endif; ! DEBUG @push parser_one; @push scope_reason; switch(scope_reason) { TESTSCOPE_REASON: if (item == parser_one) parser_two = 1; LOOPOVERSCOPE_REASON: if (parser_one ofclass Routine) indirect(parser_one, item); PARSING_REASON, TALKING_REASON: #ifdef SCOPE_STACK_OPTIMISATION; AddToStoredScope(item); #ifnot; MatchTextAgainstObject(item); #endif; } @pull scope_reason; @pull parser_one; ]; -) instead of "DoScopeAction" in "Parser.i6t". Section - Error Reporting Include (- [ L__M act n x1 x2 rv flag; ! ------------IP CHANGE HERE----------------- #ifndef SHOW_PARSER_ERRORS; if (pre_command == INACTIVE) { #endif; ! ------------IP CHANGE DONE----------------- @push sw__var; sw__var = act; if (n == 0) n = 1; @push action; lm_act = act; lm_n = n; lm_o = x1; lm_o2 = x2; switch (act) { ##Miscellany: rv = (+ whether or not intervened in miscellaneous message +); ##ListMiscellany: rv = (+ whether or not intervened in miscellaneous list message +); default: rv = (+ whether or not intervened in action message +); } action = sw__var; if (rv == false) rv = RunRoutines(LibraryMessages, before); @pull action; if (rv == false) LanguageLM(n, x1, x2); @pull sw__var; ! ------------IP CHANGE HERE----------------- #ifndef SHOW_PARSER_ERRORS; } #endif; ! ------------IP CHANGE DONE----------------- ]; -) instead of "Printing Mechanism" in "Language.i6t". Interactive Parsing ends here. ---- DOCUMENTATION ---- Chapter: Overview Section: What does this extension do? Interactive Parsing removes the traditional IF command prompt, in which the players types a command, hits return, and then gets feedback, in favour of a more modern, real-time command prompt, in which the player's command in constantly processed and interpreted while they are typing. The system colour-codes the words typed by the player to reflect if the game is understanding them. It also suggests corrections for words, if the player has mistyped, and these corrections are automatically written into the input line. Section: Why would I want to do this? The text-game interface is notoriously difficult for beginners to "get into". Part of the reason for this is poor feedback on misunderstood commands, coupled with the negative play experience of having a command not be understood. (The player, after all, knows English better than the computer; so it should be the computer's problem, and not the player's, to understand.) Interactive Parsing attempts to remove the frustration involved with finding and entering a successful command, by ensuring that players are discouraged to type commands that don't work, and pointed in the direction of commands that do. The design of the system follows that of search engines and mobile phone keyboards, with "opt-out" spell-correction rather than "opt-in", and predictive suggestions. Chapter: Getting Started Section: Setting Up To begin using Interactive Parsing, you will first need to download a copy of Erik Temple's extension, "Text Input-Output Control" from the following page: http://inform7.com/extensions/Erik%20Temple/Text%20Window%20Input-Output%20Control/index.html and also "Flexible Windows": http://inform7.com/extensions/Jon%20Ingold/Flexible%20Windows/index.html You can then simply include the Interactive Parsing extension at the top of your game-file. The system should automatically begin suggesting and correcting based on your game's dictionary. Section: Changing Input Mode You can toggle the input mode using the "inputchange" command. This flips the game between input modes. This command is available in the released game as well, but can be disabled by including the line: Understand the command "inputchange" as something new. Section: Disabling Interactive Parsing You can disable the extension easily by including the line: Use interactive parsing override. at the top of your game file - this is provided on an option because interactive parsing tends to run slowly in the I7 IDE, and it does not interact with the Skein and Transcript functions. So during development you may want to leave the features of Interactive Parser turned off, and include it only at the end of a project. You may well want to include the use option in a "(not for release)" section in your source code. Section: Glulx only Interactive Parsing only works for Glulx. While it might be possible to do a similar process for the Z-Machine, the lack of window control means the player would be entering text into the status line. This is unattractive, and Glulx is, these days, a fast, viable interpreter with web-based options with considerably more memory to spare. Section: Dictionary Size Constants The system requires a few constants to store information. These are set to sensible defaults, but if any are exceeded by a large game, the system should print a warning message on starting up with instructions on how to fix it. The most important, and most likely to be needed, is: Use maximum dictionary size of at least 1000. This tells the game how big the dictionary is. Less importantly, there is a constant to determine letter-by-letter storage: Use average word length of at least 6. It's unlikely you'll need to extend this (6 is already much large than the default dictionary average.) The game also stores, turn by turn, a list of words that are in scope, words that are verbs, and words that refer to visible items. The limits for the number of these can also be increased. Note that errors displayed for these will only occur if and when the limit is exceeded, so you will need to test the game fully to be sure they have not been reached! Use maximum words in scope of at least 256. Use maximum verb words of at least 300. Use maximum compass names of at least 35. Section: Overheads The system performs parsing and dictionary searches throughout the player's typing session, so it will inevitably slow down the player's ability to type into the game at times. However, the system has been heavily optimised, and in test cases runs with no more perceptible slowdown than a mobile-phone typing interface. The system may become more expensive in larger games, and the author would be interested to know if and when it becomes impractical. (Though note, testing in the IDE is not representative of testing in the released game.) The system also uses a lot of storage, but since Glulx has an enormous brain, this shouldn't cause too much of a problem. Section: Interaction with other extensions Note this extension replaces large amounts of the parser, so it may not work with other extensions which are designed to alter parsing behaviour. However, it should be easy to fix any conflicts. Refer to the extension authors, or the "Parser Rewrites" section below. Section: Credits Interactive Parsing is built out of two other, fine extensions - Text Input-Output Control by Erik Temple, which handles the separate input window (and is a required file to run this extensions) - and Mistype by Cedric Knight, from which several tips, tricks, algorithms and general inspiration have been drawn. This extension also relies on Basic Screen Effects by Emily Short, and Flexible Windows, by myself and Erik Temple. Section: Special Keys The following keys have particular behaviour in the interface. You may wish to document these for the user. Space - accept a suggestion if the typed word is not in the dictionary Tab - accept a suggestion regardless of what the player typed Up / Page Up - go back through command history Down / Home - go forward through command history Right / Left - move the cursor across the input line Escape - accept typed version word ignoring suggestions Chapter: Fine Tuning Section: Long Words By default, the Inform library truncates all dictionary words to 9 characters long. This can cause the suggestion system to accept, suggest and write into the transcript words which are cut-off, such as "ROADRUNNE" or "MISCHEVIO". To correct this, we need to tell the library what these words look like, in full. This is done using a table, which is already defined in the extension to cover long library words (such as "SUPERBRIEF" and "TWENTY-ONE"). It has one column, "word", in which we put the (text string) of the word in full. Don't forget, this is a "continuation" of a table, not a new one. Table of Longer Words (continued) word "mischievous" "roadrunner" Note if the game contains two long words which share the first nine-letters, this won't work, and can't be worked around. Apologies, but there's just no data in the compiled game-file to detect this. To help you find words that need a longer definition, the use option "long word check report" is provided. Compiling with this option enabled will cause the game to list on start-up all the long words in the dictionary which don't have an alternative in table. Some will be internal names (such as "main-window" and "key-window", which are the names of the Glulx window objects); and some will be valid 9-letter words 9such as "southeast" and "southwest"). But any that appear cut-off here and can be entered by the player should be given a table entry. Release versions will never print this text, which is for debugging only. Section: Boosting Words Sometimes, the system will find two or more words that are equally good matches. We can force some suggestions to take priority. (Note, this shouldn't be used much, as there's no support for any contextual data. I've mainly implemented it to ensure "DROP" wins over "DRAG". But here's how it works, in case you need it.) The game contains a table called the "Table of Useful Words". By default it looks as follows: Table of Useful Words word name "the" "look" "drop" "close" "attack" The defaults exist to promote LOOK over LOCK, THE over THAT, DROP over DRAG, CLOSE over CLEAN and CLIMB, ATTACK over ATTACH. By default, when two equal-length words match the player's input (for example, CL matches CLEAN and CLIMB, and LO matches LOOK and LOCK), the system will pick the first one alphabetically. To change this, add a word to the table above. This word will then score a one-point bonus ever tme it's matched. Section: Single-Letter Verb Associations To improve speed, the game has a fixed table mapping single keystrokes to verbs (since these are always context-independent). This table is called the Table of Single-Letter Verb Associations, and it looks as follows: Table of Single-Letter Verb Associations letter text "a" "attack" "b" "buy" "c" "cut" "d" "down" "e" "east" "f" "feel" "g" "get" "h" "hit" "i" "inventory" "j" "jump" "k" "kiss" "l" "look" "m" "move" "n" "north" "o" "open" "p" "put" "q" "quit" "r" "read" "s" "south" "t" "take" "u" "up" "v" "version" "w" "west" "x" "examine" "y" "yes" "z" "wait" If your game features different verbs, or you disagree with any of the key-mappings provided, you can change this at the start of the game using a When play begins rule: When play begins: now the text corresponding to a letter of "a" in the Table of Single-Letter Verb Associations is "admonish". Chapter: How it Works Section: Continuous Tokenisation and Colour-Coding At its heart, Interactive Parsing works by reanalysing the input buffer after every keystroke and tokenising what it finds. (Tokenising means, looking up in the game dictionary). Tokenisation is fast, and returns information on whether the typed words are recognised or not. This information is turned into colour-coding and relayed back to the player as colour. This is the simplest level of analysis. We also run the full game parser on what the player has typed, and record how many words of the input are successfully matched. If the whole line is understood, the "Press Return" text is displayed on the input line. Otherwise, the number of words understood is stored to be used as a benchmark for suggested word alternatives. Section: Near-Matches, Mistypes and Suggestions When the player has typed a word which is not understood, the game attempts to deduce if this is a mistype, or the beginning of, an expected dictionary word. If one can be found which is close to what the player has typed in, it will be suggested as an alternative by the correction/prediction system. The algorithm for comparing typed input against dictionary words uses a pre-analysis system for additional speed, based on prime numbers. A quick explanation follows, because I think it's quite clever. The algorithm aims to score how close a typed word (say, "XAEMI") is to a dictionary word ("EXAMINE"). The first version of this algorithm looped through the typed letters, then for each of these through the dictionary letters, and if a matching letter was found, the word was scored based on the distance between the typed and dictionary positions. (This is the algorithm used by the Mistype extension.) However, this meant two full loops as well as printing every dictionary word to a text buffer before every comparison. The second version of this algorithm uses a preprocess of the dictionary. While the game is starting up, it loops through each word in the dictionary and converts each letter into a prime number (A = 2, B = 3, C = 5, up to Z = 101). It then multiplies the prime-values of the word in blocks of three and stores these in a large look-up table. So for example, EXAMINE becomes 11, 89, 2, 41, 23, 43, 11 giving the set of multiples: 979, 1958, 7298, 1886, 40549, 10879, 473 When the game then comes to check this against what the player types, all it needs to do is loop through the typed letters and see which of their prime-values divide into which of these multiples. This saves printing the word to the buffer, and only requires use to loop over the local neighbourhood of each typed letter. Section: Pre-parsing When a new close word has been found, the game creates a copy of the input buffer as if the player had typed in their input with this new suggestion. The full game parser is then run on this input, and we record how many words it matches in the player's input. If more words are matched than were with what the player actually typed, this suggestion is considered valid and it will be offered to the player, unless one can be found which is closer to what the player typed. For speed, the system checks sensible words first: words associated with visible objects and prepositions. At the start of the input line, it will only check verbs and compass directions. Results of pre-parsing are cached so the parser does not need to be run again over the same suggested input (if, say, the player types in one additional letter). However, pre-parsing is still the slowest part of the process; and when the game cannot find any matches for what the player has typed it gets very slow, as the game must try a very large number of dictionary words. To resolve this problem, we use a threading system. Section: Threaded Input and Background Word-checking The extension makes use of Glulx's timer capability to provide background processing on the player's input. When any new letter is typed, an suggestion/predictive analysis is begun, but while it executes it checks (every hundred cycles or so) to see if a key has been pressed. If it has, analysis exits, the keypress is dealt with, and the new data is used to either continue or restart the analysis process. This isn't as smooth as I'd like - Glulx cannot check to see if a key has been pressed without imposing at least a 1 millisecond time delay, so it cannot check for keypresses in a fluid way, but has to do so at set intervals. Section: Further Optimisation The next biggest saving would probably be to start caching the Scope Lists produced by the parser, as these are recalculated for every new word tried in a certain position on the input line. However, this would require a fair bit more parser hacking. Chapter: Changelog and Known Issues Section: Changelog Version 4: * Minor bug-fixes, including one error which would cause AGAIN to follow under certain circumstances. * Blank input lines are now once more silently rejected. * Providing customised parser errors no longer causes infinite loop (*facepalm*). * Optimised the calculation of scope by caching results of scope searches and matches against scope. * Changed the way scores are calculated so that it's easier for words to be maximal, so the loop can drop out earlier. * Changed the order in which words are considered, so the most recent suggestion is checked first, making it more likely that the loops will drop out earlier. * Changed the logic for word length. The system now preferences words which are longer that what the player's typed over words that are shorter. * Handled changes in window size correctly. Version 3: * Added functionality to correctly detect, suggest and write in words of more than 9 letters, using the Table of Longer Words. * Added support for talking to animate characters and providing them with instructions. The system now correctly suggests and parses this kind of input, although note it will not suggest the comma after the name of the animate thing. (It would be good if it did, maybe, but the player has either typed the comma or not, so the game doesn't have much information to go on.) Version 2: * Added command line recall (up/down arrow keys) * Added command line editing (left/right arrow keys) * Added handling for disambiguating input, by recalling the previous command line and placing the cursor at the appropriate edit point (!) Section: Known Issues Interactive Parsing fails on mobile devices running Quixe. This extension is incompatible with any other that alters any parsing routines (including Disambiguation Control). However, the extension attempts to clearly mark what parser changes have been made, so it should be possible to upgrade other extensions to achieve compatibility. Section: Parser Rewrites All parser rewrites are in Book 5 and are flagged by the comments: ! ------------IP CHANGE HERE----------------- ! ------------IP CHANGE DONE----------------- At last count, there are 36 changes, most consisting of a single additional line.