// 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;
};
})();