Robust Parse Part 2:Object Disambiguation
Robust Parse (Adjectives & Object Disambiguation)
In our previous code for parsing nouns, we were setting the noun upon a successful match of a Said() string. Now, we will introduce the possibility of multiple nouns differentiated by the use adjectives. This means that potentially multiple nouns will be matched via multiple Said()s.
For review, we were previously parsing nouns (ParseNoun.sc) similar to this:
(if(Said('*/rock>')) = noun ROCK_OBJ ) (if(Said('*/window>')) = noun WINDOW_OBJ )
We'll need to change our technique to this:
(if(Said('*/rock[<small]>')) = nounSaid[++nounSaidCnt] SMALL_ROCK_OBJ ) (if(Said('*/window[<center]>')) = nounSaid[++nounSaidCnt] CENTER_WINDOW_OBJ )
First off, note that we have a new array 'nounSaid' (which is defined in the Main.sc) along with a counter of how many nouns we've matched (nounSaidCnt). I'd recommend making the array size roughly 10, which is an artificial maximum for the number of times a single noun can be duplicated in a game. By duplicated, I mean the same noun but differentiated with adjectives (e.g. gold key, silver key, copper key). You probably aren't going to have more than 10 different keys in a game, but I suppose you might. If you do, make it large enough to fit your needs.
Also see that we have now introduced adjectives & that they are optional. All of your nouns in your Said()s *must* include optional adjectives. If you do not specify an adjective, that means that any adjective input by the user will match, which is inappropriate. For example, without the '[<small]' portion of the said in the first example, you could type 'small rock', 'bright rock', 'dirty rock', 'ugly rock', etc. and the Said() would evaluate to true. This doesn't mean that the adjectives must be optional. You are free to make them mandatory by removing the brackets '' around them which would force the user to enter 'small' every time they wanted to refer to the 'rock'.
We've also changed our object constants slightly (SMALL_ROCK_OBJ, CENTER_WINDOW_OBJ). This is because we will have multiple 'rock' and 'window' objects.
Now that we've layed the groundwork, let's make things a bit more complicated:
(if(Said('*/rock[<small]>')) = nounSaid[++nounSaidCnt] SMALL_ROCK_OBJ ) (if(Said('*/rock[<large]>')) = nounSaid[++nounSaidCnt] LARGE_ROCK_OBJ ) (if(Said('*/rock[<giant]>')) = nounSaid[++nounSaidCnt] GIANT_ROCK_OBJ ) (if(Said('*/window[<right]>')) = nounSaid[++nounSaidCnt] RIGHT_WINDOW_OBJ ) (if(Said('*/window[<center]>')) = nounSaid[++nounSaidCnt] CENTER_WINDOW_OBJ ) (if(Said('*/window[<left]>')) = nounSaid[++nounSaidCnt] LEFT_WINDOW_OBJ )
So, now we have three rocks & three windows in our game. We will use the same pattern in the ParseSecond.sc script as well with references to an array & counter of 'secondSaid' and 'secondSaidCnt':
(if(Said('*/*/rock[<small]>')) = secondSaid[++secondSaidCnt] SMALL_ROCK_OBJ ) (if(Said('*/*/rock[<large]>')) = secondSaid[++secondSaidCnt] LARGE_ROCK_OBJ ) . . .
We'll need to update our game.sh to reflect the new objects:
// Nouns (define TREE_OBJ 0) (define SAW_OBJ 1) (define GROUND_OBJ 2) (define SHOVEL_OBJ 3) (define BIRD_OBJ 4) (define HOUSE_OBJ 5) (define DOOR_OBJ 6) (define KEY_OBJ 7) (define LEFT_WINDOW_OBJ 8) (define CENTER_WINDOW_OBJ 9) (define RIGHT_WINDOW_OBJ 10) (define SMALL_ROCK_OBJ 11) (define LARGE_ROCK_OBJ 12) (define GIANT_ROCK_OBJ 13)
We'll also need to update our 'Noun names' text resource:
/***************** Text.001 (noun names) *****************/ Num Text ------ ----- 0 tree 1 saw 2 ground 3 shovel 4 bird 5 house 6 door 7 key 8 rock 9 window
Don't forget to update your vocabulary with the new adjectives and nouns as well.
We will need to make some modifications to our RobustPragmaFail.sc script. Basically, we need to change the logic to be testing for the number of nouns that were matched (nounSaidCnt) since we are no longer setting the 'noun' variable (yet!). We've also changed the print statements to use a new global strings 'nounDescrip' and 'secondDescrip', which we will address next.
(procedure public (RobustPragmaFail) (if(== needNoun TRUE and == nounSaidCnt UNSET) FormatPrint("What do you want to %s?" VERB_NAMES Verb) return(TRUE) ) (if(== needSecond TRUE and == secondSaidCnt UNSET) FormatPrint("What do you want to %s the %s with?" VERB_NAMES Verb @nounDescrip) return(TRUE) ) (if(== unneededNoun TRUE and <> nounSaidCnt UNSET) FormatPrint("I understood only as far as you wanting to %s." VERB_NAMES Verb) return(TRUE) ) (if(== unneededSecond TRUE and <> secondSaidCnt UNSET) FormatPrint("I understood only as far as you wanting to %s the %s." VERB_NAMES Verb @secondDescrip) return(TRUE) ) return(FALSE) )
Before calling the RobustPragmaFail, we'll need to call a new procedure called GetObjectDescrip.sc. The purpose of this script is to extract *exactly* what the user inputted for the noun & second. We need to do this to support the following example: Let's say you have a 'rock' in your game. What happens when you enter an invalid adjective for your noun (perhaps 'cold rock'), but a valid adjective for some other noun in your game. Wouldn't it be nice to be able to provide a detailed error message back to them, something like: 'I don't see any ugly rock here.'
I'm not going to post the code here (you can get it from the new version of RobustParseDemo game) as it's fairly lengthly and unexciting. However, rest assured that it's only purpose is to take apart the user input and capture the noun & second as it was input into the nounDescrip and secondDescrip variables. In order to support this script, we need to create a new text resource that will be used to match adjectives. It should hold every adjective defined for your game (just like the noun names text resource).
/***************** Text.002 (adjective names) *****************/ Num Text ------ ----- 0 tall 1 rusty 2 hard 3 garden 4 song 5 brown 6 front 7 silver 8 left 9 center 10 right 11 small 12 large 13 giant
The remainder of our changes we are making is in the ValidateNouns.sc script.
First, we will determine which nouns that were referenced are actually here:
(for (=i 0) (<= i nounSaidCnt) (++i) // if the object is here, add it to our diambiguate string (if ( == objectLocations[nounSaid[i]] gRoomNumber or == objectLocations[nounSaid[i]] INVENTORY or == objectLocations[nounSaid[i]] EVERYWHERE) = nounsHere[nounsHereCnt] nounSaid[i] ++nounsHereCnt ) )
Then, we will determine if we need to ask the user if they need to be more specific:
(if(== nounsHereCnt 0) = noun UNSET )(else (if(> nounsHereCnt 1) (for (=i 0) (< i nounsHereCnt) (++i) (if(== i 0) StrCpy(@disambigStr "the ") ) (if(<> i 0 and <> i (- nounsHereCnt 1)) StrCat(@disambigStr ", the ") ) (if(== i (- nounsHereCnt 1)) StrCat(@disambigStr " or the ") ) Format(@fullNounDesc "%s" NOUN_FULLNAMES nounsHere[i]) StrCat(@disambigStr @fullNounDesc) ) FormatPrint("Which %s do you mean, %s?" @nounDescrip @disambigStr) return(FALSE) )(else = noun nounsHere // the only noun here ) )
At this point we've successfully determined which noun is being referenced, but we are still not sure if it's here in the room. Finally, we make sure that the noun is actually here:
(if (== noun UNSET and > StrLen(@nounDescrip) 0) (if ( <> objectLocations[noun] gRoomNumber and <> objectLocations[noun] INVENTORY and <> objectLocations[noun] EVERYWHERE) FormatPrint("I don't see any %s here." @nounDescrip) return(FALSE) ) )
When building our error output for disambiguating nouns, we need the full noun description (adjective + noun). For this, we will utilize a new text resource. This text resource needs to be correlated to the values that we assigned our object constants in game.sh.
/***************** Text.003 (full noun names) *****************/ Num Text ------ ----- 0 tall tree 1 rusty saw 2 hard ground 3 garden shovel 4 song bird 5 brown house 6 front door 7 silver key 8 left window 9 center window 10 right window 11 small rock 12 large rock 13 giant rock
Testing: Let's try it out. Start up the new RobustParseDemo game & try some things out:
While in the house room (there are three windows and one (small) rock there):
'look window' results in 'Which window do you mean, the left window, the center window or the right window?'
'look small rock' results in 'It's small. More like a pebble, actually'
'look rock' - same result as before. We don't need to specify 'small' in this case because it's the only one here.
'look brown rock' results in 'I don't see any brown rock here.' (bad adjective pairing)
'look large rock' results in 'I don't see any large rock here.' (good adjective pairing, but that object isn't in this room)
While in the tree room (there are two rocks there):
'look window' results in 'I don't see any window here'
'look rock' results in 'Which rock do you mean, the large rock or the giant rock?'
'look large rock' results in 'It's a large rock'
'look giant rock' results in 'It's a giant rock'
'cut down tree with rock' results in 'Which rock do you mean, the large rock or the giant rock?'
Multiple adjective handling: Everything we've done above will support this, just use the supported Said() syntax for multiple adjectives:
(if(Said('*/rock[<(small,tiny,miniature)]>')) = nounSaid[++nounSaidCnt] SMALL_ROCK_OBJ )
Notes: - If you have ambiguous objects as in these examples, it's courteous to the user to specify the valid adjectives when looking at the object. Examine the 'small rock' example above - when we typed 'look rock', it is indicated that it's small.
- Again, don't use synonyms for your adjectives. Instead use the method above of including multiple adjectives in our Said() strings.