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

(function () {
  /*global adventurejs A*/
  "use strict";

  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("log", "high", "parseInput.js > " + input, "Parser");
    let results;

    // we already dispatched inputEnter when player entered input
    // but parser.parseInput() can be called by other means so we distinguish
    // between dispatching inputEnter vs inputParseBegin
    this.game.reactor.dispatchEvent(this.game.events.inputParseBegin);

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

    /**
     * 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 unparsed_input = input;

    /**
     * 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.addWorldToHistory.call(this.game, A.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();
          // TODO at the moment false & null have same results
          // but leaving open option to handle differently
          if (false === input) {
            this.is_input_queued = false;
            this.game.reactor.dispatchEvent(
              this.game.events.inputParseComplete
            );
            return false;
          } else if (null === input) {
            this.is_input_queued = false;
            this.game.reactor.dispatchEvent(
              this.game.events.inputParseComplete
            );
            return null;
          }
        }
      }
    }

    /**
     * Are we parsing stacked commands?
     * Stacked commands are made when user inputs
     * something like "do this THEN do that".
     */

    // if we're parsing stacked input, there are several items
    // we want to carry over from the original input
    var carried_input = new adventurejs.Input({
      game_name: this.game.game_name,
    });

    // when substrings are replaced in compound input,
    // we want to keep track of replacements for subsequent output

    if (this.input_queue.length > 0) {
      // We're currently parsing the first item in the stack,
      // so remove that item.
      // In the event of stacked input, we want to carry forward
      // a record of string replacements that have occurred
      carried_input.replacements = Object.assign(
        carried_input.replacements,
        this.input_history[0].replacements
      );

      // also carry forward a record of the original input string
      carried_input.unparsed_input = this.input_history[0].unparsed_input;

      if (this.input_queue[0].printInput) {
        this.game.printInput(A.deserialize(input));
      }

      if ("undefined" !== typeof this.input_queue[0].output_class) {
        output_class = this.input_queue[0].output_class;
      }

      // it's possible to include pre-set output in the input_queue
      // see goto for example
      if ("undefined" !== typeof this.input_queue[0].output) {
        this.game.print(this.input_queue[0].output, output_class);
      }

      //
      if (
        "undefined" !== typeof this.input_queue[0].linefeed &&
        this.input_queue[0].linefeed
      ) {
        this.game.print("", "linefeed");
      }

      this.input_queue.shift();
    } // if( this.input_queue.length > 0 )
    else {
      // sanitizeInput cleans input of several conditions.
      // Also has the ability to revise unparsed_input in the case of 'again'
      var sanitized_input = this.sanitizeInput(parsed_input, unparsed_input);
      this.game.log(
        "log",
        "high",
        "parseInput.js > sanitizeInput return parsed: " +
          sanitized_input[0] +
          ", unparsed: " +
          sanitized_input[1],
        "Parser"
      );

      parsed_input = sanitized_input[0];
      unparsed_input = sanitized_input[1];

      // Only prints the first item in queue
      this.game.printInput(A.deserialize(parsed_input));
    }

    // make a new input object
    this.input_history.unshift(
      new adventurejs.Input({ game_name: this.game.game_name })
    );
    var this_turn = this.input_history[0];

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

    // unparsed_input might not actually be the original input
    // in cases like 'again' where we've subbed in the prior turn's input
    this_turn.input = unparsed_input;

    this_turn.output_class = output_class;

    // if we're parsing compound input aka stacked commands,
    // we want to carry a record of string replacements
    // for later reference
    this_turn.replacements = Object.assign(
      this_turn.replacements,
      carried_input.replacements
    );

    if (carried_input.unparsed_input) {
      this_turn.unparsed_input = carried_input.unparsed_input;
    } else {
      this_turn.unparsed_input = unparsed_input;
    }

    // print the preparsed input back to the player
    // this.game.printInput( A.deserialize(input) );
    // when printed here, prints for each queue item,
    // but including for "this and that" which I don't want

    // Handle quote delimited substrings in input.
    parsed_input = this.parseStrings(parsed_input);

    // join compound names into asset IDs
    // this was breaking on 'item but other item'
    // parsed_input = this.joinCompoundPhrases(parsed_input);

    // we're going to handle 'but' in a later step
    // so we temporarily split the string by 'but'
    let but_arr = parsed_input.split(" but ");
    let but_str = "";
    for (let i = 0; i < but_arr.length; i++) {
      but_str += this.joinCompoundPhrases(but_arr[i]);
      if (i < but_arr.length - 1) but_str += " but ";
    }
    parsed_input = but_str;

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

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

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

    // strip conjunctions and convert them into symbols we can handle
    parsed_input = this.stripConjunctions(parsed_input);

    // Done parsing input, save parsed_input to the global input object.
    this_turn.parsed_input = parsed_input;

    // 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) {
      var parsedVerb = this.parseVerb(parsed_input_array[0]);

      // recognize it as a verb, save it
      if (parsedVerb) {
        this_turn.input_verb = parsed_input_array[0];
      } 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;
      }
    }

    // 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.verifySentenceStructure()) return false;
      this.handleSentence();
    }

    // TODO special handling for say / ask / tell

    this.game.updateDisplayRoom();

    // If player used a period to input distinct actions,
    // perform the next one in the queue.
    if (this.input_queue.length > 0) {
      this.game.reactor.dispatchEvent(this.game.events.inputQueueNext);
      this.is_input_queued = true;
      this.parseInput(this.input_queue[0].input);
    } else {
      this.is_input_queued = false;
      //this.game.worldSave();
      this.game.reactor.dispatchEvent(this.game.events.inputParseComplete);
      return;
    }
  }; // parseInput
})();