Pre-release
AdventureJS Docs Downloads
Score: 0 Moves: 0
// parseInput.js

(function () {
  /* global AdventureJS A */

  var p = AdventureJS.Parser.prototype;

  /**
   * Parse an input string.
   * @memberOf AdventureJS.Parser
   * @method AdventureJS.Parser#parseInput
   * @param {String} input Player input.
   */
  p.parseInput = function Parser_parseInput(input) {
    this.game.log(
      "L1091",
      "log",
      "high",
      `[parseInput.js] parseInput() ${input}`,
      "Parser"
    );
    let results;
    const raw_input = input;

    // we already dispatched enterInput when player entered input
    // but parser.parseInput() can be called by other means so we distinguish
    // between dispatching enterInput vs beginTurn
    this.game.reactor.emit("beginTurn");
    this.game.turn_duration = new Date().getTime();

    // The undo verb supercedes all parsing.
    if ("undo" === input) {
      this.game.display.printInput(input);
      this.game.dictionary.doVerb("undo");
      this.game.endTurn();
      return;
    }

    /**
     * Save a snapshot of the game state.
     * Technically this is saving LAST turn's snapshot
     * before engaging in this turn.
     * IMPORTANT: undo must come first.
     */
    A.FX.addWorldToHistory.call(
      this.game,
      A.FX.getBaselineDiff.call(this.game)
    ); // delta from baseline

    /**
     * input_history_index is part of a convenience that lets
     * players use the arrow keys to re-enter their own
     * prior input, as in a shell. Player might have used it
     * to enter current input, so reset it now.
     */
    this.input_history_index = -1;

    /**
     * output_class is one of the params that's carried through
     * the entire parse and then used to apply css styles to
     * the output, if applicable.
     */
    var output_class = "";

    /**
     * Run any custom parsers created by author.
     * If custom parser returns a string, parse will continue.
     * If it returns null or false, parse will end.
     */
    if (0 < this.custom_parsers.length) {
      var keys = Object.keys(this.custom_parsers);
      for (var i = 0; i < this.keys.length; i++) {
        var parser_name = keys[i];
        if (true === this.custom_parsers_enabled[parser_name]) {
          input = this.custom_parsers[parser_name].parseInput();
          // false & null give same results but following the
          // pattern we're using almost everywhere else,
          // we're leaving open the option to handle distinctly
          if (false === input) {
            this.is_input_queued = false;
            this.game.endTurn();
            return false;
          } else if (null === input) {
            this.is_input_queued = false;
            this.game.endTurn();
            return null;
          }
        }
      }
    }

    // --------------------------------------------------
    // make a new input object
    // @TODO moved this block from below next block
    // and watching to ensure no bad side effects
    // --------------------------------------------------
    this.input_history.unshift(
      new AdventureJS.Parser.Input({ game_name: this.game.game_name })
    );
    var this_turn = this.input_history[0];

    // --------------------------------------------------
    // document the input
    // At this point "input" is just the string entered by player.
    // If we're processing stacked input, then input will already
    // have been parsed giving us serialized object ids.
    // --------------------------------------------------
    var parsed_input = input;
    var normalized_input = input;

    // --------------------------------------------------
    // keep a reference to last turn
    // --------------------------------------------------
    var last_turn = this.input_history[1];

    // --------------------------------------------------
    // Are we parsing queued commands?
    // Queued commands are made when user inputs
    // something like "do this THEN do that".
    // if so, carry several items from original input
    // --------------------------------------------------
    var is_queued = false;

    var did_print_input = false;

    // --------------------------------------------------
    // process new item input...
    // --------------------------------------------------
    if (!this.input_queue.length) {
      this_turn.raw_input = raw_input;

      // --------------------------------------------------
      // temporarily convert quoted substrings to symbols
      // --------------------------------------------------
      parsed_input = this.dehydrateStrings(parsed_input);
      if (false === parsed_input) return false;

      // --------------------------------------------------
      // clean input of several conditions
      // parsed and normalized remain the same for now
      // --------------------------------------------------
      parsed_input = this.normalizeInput(parsed_input);
    }

    // --------------------------------------------------
    // ... or process next item in queue
    // --------------------------------------------------
    else if (this.input_queue.length > 0) {
      is_queued = true;

      let queued_input = this.input_queue[0];

      // enqueued inputs may pass output classes
      if ("undefined" !== typeof queued_input.output_class) {
        output_class = queued_input.output_class;
      }

      // enqueued inputs may include pre-set output
      // see goto for example
      if ("undefined" !== typeof queued_input.output) {
        this.game.print(queued_input.output, output_class);
      }

      // enqueued inputs may request linefeeds between lines of output
      if (
        "undefined" !== typeof queued_input.linefeed &&
        queued_input.linefeed
      ) {
        this.game.print("", "linefeed");
      }

      if (queued_input.input) {
        this_turn.input = queued_input.input;
      }

      if (queued_input.raw_input) {
        this_turn.raw_input = queued_input.raw_input;
      }

      if (queued_input.quote_tokens) {
        this_turn.quote_tokens = queued_input.quote_tokens;
      }

      this_turn.subject = queued_input.subject;

      this_turn.is_command = queued_input.is_command;

      this_turn.tokens = Object.assign(this_turn.tokens, last_turn.tokens);

      if (queued_input.printInput) {
        // @TODO this needs to print the actual original input
        // preserving capitalization but separating into clauses
        this.game.display.printInput(input);
        did_print_input = true;
      }

      // We're currently parsing the first item in the stack,
      // so remove that item.
      this.input_queue.shift();
    }
    // --------------------------------------------------
    // end queued input
    // --------------------------------------------------

    // --------------------------------------------------
    // remove some irrelevant social niceties
    // --------------------------------------------------
    parsed_input = this.roboticizeInput(parsed_input);
    // normalize again in case roboticization left extra spaces
    parsed_input = this.normalizeInput(parsed_input);

    // --------------------------------------------------
    // save our normalized input
    // from this point forward, parsed_input may diverge
    // --------------------------------------------------
    normalized_input = parsed_input;

    // --------------------------------------------------
    // rehydrate quoted substrings for printInput()
    // --------------------------------------------------
    normalized_input = this.rehydrateStrings(normalized_input);

    // --------------------------------------------------
    // save input as its processed to this point
    // --------------------------------------------------
    this_turn.input = normalized_input;

    this_turn.output_class += output_class;

    // --------------------------------------------------
    // Handle numbers in input.
    // @TODO tokenize numbers?
    // --------------------------------------------------
    parsed_input = this.parseNumbers(parsed_input);

    // --------------------------------------------------
    // tokenizeNouns
    // resolves multi word asset names into IDs
    // we temporarily split the string by 'but' because
    // this was breaking on 'item but other item'
    // we handle 'but' in a later step
    // --------------------------------------------------
    let but_arr = parsed_input.split(" but ");
    let but_str = "";
    for (let i = 0; i < but_arr.length; i++) {
      but_str += this.tokenizeNouns(but_arr[i]);
      if (i < but_arr.length - 1) but_str += " but ";
    }
    parsed_input = but_str;

    // --------------------------------------------------
    // convert dehydrated quoted substrings to global_string
    // --------------------------------------------------
    parsed_input = this.tokenizeStrings(parsed_input);

    // --------------------------------------------------
    // did player input "with thing" in response to
    // "which thing did you mean?" (aka soft prompt)?
    // if so remove "with"
    // --------------------------------------------------
    if (
      parsed_input.startsWith("with ") &&
      last_turn.soft_prompt.enabled &&
      last_turn.soft_prompt.noun
    ) {
      parsed_input = parsed_input.replace("with ", "");
    }

    // --------------------------------------------------
    // join some common compound verbs into single words
    // --------------------------------------------------
    parsed_input = this.tokenizeVerbs(parsed_input);

    // --------------------------------------------------
    // join some common compound prepositions into single words
    // --------------------------------------------------
    parsed_input = this.tokenizePrepositions(parsed_input);

    // --------------------------------------------------
    // Look for NPC commands
    // this can return false, if target is provided but not found
    // and we need to carry target through queue
    //
    // @TODO better handling for multi-turn NPC commands
    // In this iteration, the NPC's turns must all be
    // handled before resuming control to player.
    // --------------------------------------------------
    parsed_input = this.tokenizeCommands(parsed_input);
    if (false === parsed_input) return false;

    // --------------------------------------------------
    // convert periods at sentence boundaries to pipes
    // --------------------------------------------------
    parsed_input = this.tokenizeBoundaries(parsed_input);

    // --------------------------------------------------
    // strip out articles
    // --------------------------------------------------
    parsed_input = this.stripArticles(parsed_input);

    // --------------------------------------------------
    // convert then to sentence boundaries
    // --------------------------------------------------
    parsed_input = this.tokenizeThen(parsed_input);

    // --------------------------------------------------
    // convert and to conjunction or sentence boundary
    // --------------------------------------------------
    parsed_input = this.tokenizeAnd(parsed_input);

    // --------------------------------------------------
    // tokenize but
    // --------------------------------------------------
    parsed_input = this.tokenizeBut(parsed_input);

    // ==================================================
    // split input into sentences
    // When input is split, processing from this point
    // forward only continues for the first item.
    // The remainder get queued for next turn(s).
    // ==================================================
    if (!this.input_queue.length) {
      parsed_input = this.splitSentences(parsed_input);
      if (false === parsed_input) return false;
    }
    // ==================================================

    // --------------------------------------------------
    // Done parsing input
    // Save parsed_input back to the turn.
    // --------------------------------------------------
    this_turn.parsed_input = parsed_input;

    // --------------------------------------------------
    // print the processed input
    // --------------------------------------------------
    if (!did_print_input) {
      this.game.display.printInput();
      did_print_input = true;
    }

    // --------------------------------------------------
    // callPreScripts
    // this must come after printInput
    // --------------------------------------------------
    results = this.game.callPreScripts(input);
    if ("undefined" !== typeof results) return results;

    // --------------------------------------------------
    // Split input into an array of individual words.
    // --------------------------------------------------
    var parsed_input_array = parsed_input.split(" ");

    // --------------------------------------------------
    // Save the input array to the global object.
    // --------------------------------------------------
    this_turn.parsed_input_array = parsed_input_array;

    // Each of the "compressVerb" functions can write to input_verb
    // but if verb was just one word, it won't have been saved yet,
    // and we may want to send to handleWord. But it's possible
    // that player is replying to a soft prompt and hasn't entered
    // a verb, so first let's verify whether the first word is in
    // fact a verb.

    if (!this_turn.input_verb) {
      // resume normal singular verb handling here
      var parsedVerb = this.parseVerb(parsed_input_array[0]);
      var input_verb;

      if (parsedVerb) {
        input_verb = parsed_input_array[0];
      }

      // we only handle a couple of cases where first word is not verb
      else if (this.game.dictionary.getAdverb(parsed_input_array[0])) {
        // "adverb verb"
        parsedVerb = this.parseVerb(parsed_input_array[1]);
        if (parsedVerb) {
          input_verb = parsed_input_array[1];
        }
      }
      // we also handle "character verb" as in "Floyd go east"
      // but we check for that in verifySentence

      // recognize it as a verb, save it
      if (parsedVerb) {
        this_turn.input_verb = input_verb;
      } else if (last_turn.soft_prompt.noun1) {
        // last turn prompted for a word so assume we're recyling last turn's verb
        parsed_input_array.unshift(last_turn.getVerb());
        parsed_input = this.input_history[1].parsed_input + " " + parsed_input;
        this_turn.parsed_input = parsed_input;
        parsed_input_array = parsed_input.split(" ");
        this_turn.parsed_input_array = parsed_input_array;
        this.game.log(
          "L1093",
          "log",
          "high",
          `[parseInput.js] handle soft prompt for noun1`,
          "Parser"
        );
        this_turn.input_verb =
          last_turn.soft_prompt.input_verb || last_turn.input_verb;
        this_turn.verb_phrase =
          last_turn.soft_prompt.verb_phrase || last_turn.verb_phrase;
        this_turn.soft_prompt.satisfied = true;
      }
    }

    // for use in cases of number prompts
    var isWordNumber =
      null !== last_turn.disambiguate.index &&
      !isNaN(Number(parsed_input_array[0]));

    // IS INPUT USEABLE?
    // We can't do anything with the input.
    if (
      1 === parsed_input_array.length &&
      "" === parsed_input_array[0] &&
      false === isWordNumber
    ) {
      this.parseNoInput();
    }

    // valid one word inputs include intransitive verbs
    // such as look, inventory, and directions
    // we don't know that one of these has been input, but we can move forward
    else if (1 === parsed_input_array.length) {
      this_turn.found_word = parsed_input_array[0];
      this.handleWord();
    }

    // Parse the full sentence
    else {
      if (false === this.parseSentence()) return false;
      if (false === this.verifySentence()) return false;
      if (false === this.verifyAdverbs()) return false;
      // @TODO can I put a verifyPronouns() here?
      // @TODO can I put a verifyPossessives() here?
      if (false === this.verifyCharacterVerb()) return false;
      if (false === this.saveVerbPhrase()) return false;
      if (false === this.handleSentence()) return false;
    }

    // TODO special handling for say / ask / tell

    this.display.updateRoom();

    // If player used a period to input distinct actions,
    // perform the next one in the queue.
    if (this.input_queue.length > 0) {
      this.game.midTurn();
      this.is_input_queued = true;
      this.parseInput(this.input_queue[0].input);
    } else {
      this.is_input_queued = false;
      this.game.endTurn();
      const duration = new Date().getTime() - this.game.turn_duration;

      console.warn({ duration });
      return;
    }
  }; // parseInput
})();