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

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

  var p = adventurejs.Parser.prototype;

  /**
   * <p>
   * Search input for multi-word phrases that are not names,
   * such as "north door" to describe an aperture related
   * to a north exit,
   * by comparing the input string against entries in
   * game.world_lookup, which contains space delimited phrases
   * as well as single words which make up those phrases.
   * </p>
   * <p>
   * For example if we had an object named:
   * "Maxwell's silver hammer"
   * </p>
   * <p>
   * User might ask for "hammer" or "silver hammer" or "maxwell's silver hammer"
   * In order to maximize chances of understanding partial user input,
   * each word of the phrase becomes a separate entry in world_lookup.
   * </p>
   * <p>
   * When performing search/replace we don't want to
   * accidentally replace substrings of larger phrases,
   * which means we need to search for longer phrases first
   * and work our way down. We also need to ensure we don't
   * substitute words in phrases we've already serialized.
   * </p>
   * <p>
   * Unfortunately we can't just sort world_lookup by word count
   * because JS object properties aren't indexed, by definition.
   * Instead we figure out the longest word count property and
   * do a loop for each word count from the longest down to two words.
   * (No substitution is needed for single words.)
   * </p>
   * @memberOf adventurejs.Parser
   * @method adventurejs.Parser#joinCompoundPhrases
   * @param {String} input Player input.
   * @returns {String}
   * @todo
   * This might be too greedy, or perhaps need to exclude first word of input.
   * I had a room called "Put Plug In This"
   * and this method parsed "put plug in sink" to "put_plug_in_this in sink"
   * Alternately, revise the method that populates world_lookup
   * for instance world_lookup has keys for
   * "Plug Something With Something" and "sink with"
   */
  p.joinCompoundPhrases = function Parser_joinCompoundPhrases(input) {
    this.game.log(
      "log",
      "high",
      "joinCompoundPhrases.js > receive: " + input,
      "Parser"
    );
    var lookupKeys = Object.keys(this.game.world_lookup);
    var maxwords = 0;

    // TODO figure this at the end of Game.play() and store as global var
    // figure the longest word count
    for (var l = 0; l < lookupKeys.length; l++) {
      var length = lookupKeys[l].split(" ").length;
      if (length > maxwords) {
        maxwords = length;
      }
    }

    // iterate from longest to shortest, excluding single words
    for (var m = maxwords; m > 0; m--) {
      for (var i = 1; i < lookupKeys.length; i++) {
        if (
          lookupKeys[i].split(" ").length === m &&
          lookupKeys[i].split(" ").length > 1 && // don't find single words
          lookupKeys[i].split("_").length === 1
        ) {
          //console.log("*** input:",input,"key:",lookupKeys[i], this.game.world_lookup[ lookupKeys[i] ] );
          var lookupValue = this.game.world_lookup[lookupKeys[i]].IDs;
          //console.log("lookupKeys[i]",lookupKeys[i]);
          //console.log("lookupValue",lookupValue);

          /*
           * Exclude keywords with underscores because
           * those will be the results from earlier loops.
           * Ex: we're going to search for "icy door"
           * and convert to "icy_door"
           * and we're also going to search for "icy"
           * and we don't want to wind up with "icy_doordoor".
           *
           * We search with word boundaries
           * When we serialize we're replacing spaces with underscores
           * and underscore is not a word boundary,
           * so for example search will find "icy " or " icy" or " icy "
           * but it won't find "icy" in "icy_door".
           */

          var search = "\\b" + lookupKeys[i] + "\\b";
          var regex = new RegExp(search, "g");

          //
          var match = input.match(regex);

          if (match !== null) {
            //console.log( "match", match );

            /*
             * One clear match found.
             * This lookup key only had one object associated with it.
             * Ex: "wooden chest" returns "wooden_chest".
             */
            if (lookupValue.length === 1) {
              input = input.replace(regex, lookupValue);
              //console.log("lookupValue.length === 1");
              //console.log("input",input);

              // save a replacement record back to input
              // so we can find the original input string,
              // which may not be identical to the name
              // of the object we find
              let context = `joinCompoundPhrases.js > found only one lookup value and replaced '${lookupValue[0]}' with '${lookupKeys[i]}'`;
              //console.warn(context);
              if (!this.game.getInput().replacements[lookupValue[0]]) {
                this.game.getInput().replacements[lookupValue[0]] = {
                  source: lookupKeys[i],
                  context: context,
                };
              }
            } else {
              // we're going to use foundOne for testing after we run the for-loop
              var foundOne = false;
              this.game.log(
                "log",
                "high",
                "joinCompoundPhrases.js > lookupValue: " + lookupValue,
                "Parser"
              );
              for (var k = 0; k < lookupValue.length; k++) {
                this.game.log(
                  "log",
                  "high",
                  "joinCompoundPhrases.js > lookupValue: " + lookupValue[k],
                  "Parser"
                );

                /*
                 * The lookupKey found multiple values,
                 * which means an array of serialized IDs.
                 *
                 * Ex: we searched for "brass key" and found "small_brass_key"
                 * but also "melted_brass_key" and "broken_brass_key"
                 *
                 * So we need to deserialize each value to see if it's found in input.
                 * And as with the parent routine, we need items
                 * ordered from longest word count to shortest.
                 *
                 * Fortunately we already sorted these at startup
                 * in Game.play.sortLookupValues().
                 */
                var lookupValueDeserialized = A.deserialize(lookupValue[k]);
                //console.log("lookupValueDeserialized",lookupValueDeserialized);

                /*
                 * IMPORTANT: don't replace single words.
                 * This was already the intention, but this
                 * line fixes a bug when processing input such as:
                 * "take all keys but fookey".
                 * Prior to this step, "but fookey" has been converted
                 * to "-fookey", giving us "take all keys -fookey".
                 * This loop was finding "fookey" and replacing
                 * "all keys" with "fookey", leading to malformed
                 * input: "take fookey -fookey"
                 */
                if (-1 === lookupValueDeserialized.indexOf(" ")) {
                  continue;
                }

                /*
                 * we found an exact match
                 * Ex: user input included the phrase "melted brass key"
                 * so we can exclude "brass_key" and broken_brass_key"
                 */
                if (-1 !== input.indexOf(lookupValueDeserialized)) {
                  console.log("FOUND UNAMBIGUOUS MATCH!");
                  console.log(regex, "in", input);
                  console.log("original input", input);
                  //var match = [  ];
                  input = input.replace(regex, lookupValue[k]);
                  console.log("revised input", input);
                  foundOne = true;

                  // save a replacement record back to input
                  if (!this.game.getInput().replacements[lookupValue[k]]) {
                    this.game.getInput().replacements[lookupValue[k]] = {
                      source: lookupKeys[i],
                      context: `joinCompoundPhrases > found unambiguous match`,
                    };
                  }
                }
              }
              if (false === foundOne) {
                this.game.log(
                  "log",
                  "high",
                  "joinCompoundPhrases > Need to disamiguate " + lookupValue,
                  "Parser"
                );
                /*
                 * We need to disambiguate, but we're going to kick
                 * that can down the road. Normally we would convert
                 * multiples to an array – but unfortunately
                 * we're still working with strings at this point –
                 * because this may be just one "word" of player input.
                 * Instead we're going to pass on all found object IDs
                 * in a comma delimited list.
                 *
                 * Important: we're saving the original input as the first
                 * item in the comma-delimited list. Down the road,
                 * parseNoun will use that to set parsedNoun.input
                 */

                input = input.replace(
                  regex,
                  A.serialize(lookupKeys[i]) + "=" + lookupValue.toString()
                );

                // save a replacement record back to input
                if (!this.game.getInput().replacements[lookupValue]) {
                  this.game.getInput().replacements[lookupValue] = {
                    source: lookupKeys[i],
                    context: `joinCompoundPhrases.js > looking for disambiguation`,
                  };
                }
              }
            }
          } // if( match !== null )
        }
      } // for( var i = 1; i < lookupKeys.length; i++ )
    } // for( var m = maxwords; m > 0; m-- )

    var searchall = "\\ball_";
    var regexall = new RegExp(searchall, "g");
    input = input.replace(regexall, "");

    this.game.log(
      "log",
      "high",
      "joinCompoundPhrases.js > return: " + input,
      "Parser"
    );
    return input;
  };
})();