Another Parser Modification Question

Hello.

The game I am porting has parser errors like so:

GET MACGUFFIN

I don't know the word "macguffin".

FOO LAMP

I don't know the word "foo".

GET SOUTH

You used the word "south" in a way I don't understand.

(In this example, the foobar exists, but is not in scope.)
GET FOOBAR

You can't see any foobar here!

I got the first two working fairly easily. I think I can do the last one with little thought required, but that third one is stumping me.

GET SOUTH

You used the word "south" in a way that I don't understand.

GET GET

You used the word "get" in a way that I don't understand.

Oh, and if lamp is an object anywhere in the game:

LAMP

There was no verb in that sentence!

LAMP LAMP

There was no verb in that sentence!

All in all, it looks like I need to:

  1. check if the first word would return an object if the parser tried to match it to an object in order to:

    • return "You can't see any object here!"
    • return "There was no verb in that sentence!"
  2. make sure an unresolved object is not a command's verb or a direction in order to:

    • return 'You used the word "text" in a way that I don't understand.'

I'm going to take a 30 minute break before messing with this, and I'll come back to see if anyone already had any quick fixes for anything before getting back to work on it.


The code so far:

  <function name="HandleSingleCommand" parameters="command"><![CDATA[
    if (LCase(command) = "again" or LCase(command) = "g") {
      // First handle AGAIN
      if (not game.pov.currentcommand = null) {
        HandleSingleCommand (game.pov.currentcommand)
      }
      else {
        msg ("There is nothing to repeat.")
      }
    }
    else {
      // Check through all commands for any that match
      candidates = NewObjectList()
      foreach (cmd, ScopeCommands()) {
        if (IsRegexMatch(cmd.pattern, command, cmd.name)) {
          list add (candidates, cmd)
        }
      }
      maxstrength = -1
      thiscommand = null
      // Pick the best match
      foreach (candidate, candidates) {
        strength = GetMatchStrength(candidate.pattern, command, candidate.name)
        // favour commands defined later, so an author can override a library command...
        if (strength >= maxstrength) {
          // ... except if the command defined later (candidate) has no parent, and the current best
          // match (thiscommand) does have a parent. We want to favour any commands defined in rooms
          // over global candidates.
          skip = false
          if (thiscommand <> null) {
            if (thiscommand.parent <> null and candidate.parent = null) {
              skip = true
            }
          }
          if (not skip) {
            thiscommand = candidate
            maxstrength = strength
          }
        }
      }
      if (thiscommand = null) {
        if (HasScript(game, "unresolvedcommandhandler")) {
          params = NewDictionary()
          dictionary add (params, "command", command)
          do (game, "unresolvedcommandhandler", params)
        }
        else {
          // Infocom style mod
          if (game.pov.parent.dark) {
            msg ("It's too dark to see!")
          }
          else {
            // msg ("HandleSingleCommand")
            handled = ObjectExists(command)
            if (handled) {
              // Make sure there is actually no verb, not just the first word!
              commandArray = Split(command, " ")
              verbExists = false
              foreach (cmd, commandArray) {
                verbExists = ActionExists(cmd)
              }
              if (verbExists) {
                msg ("That sentence isn't one I recognise.")
              }
              else {
                msg ("There was no verb in that sentence!")
              }
            }
            else {
              // Replacing old Infocom text:
              // msg ("I don't know the word \"" + Split(command," ")[0] + "\".")
              // ...with...
              msg ("I don't know how to \"" + Split(command," ")[0] + "\".")
            }
          }
          // msg (Template("UnrecognisedCommand"))
        }
        HandleNextCommandQueueItem
      }
      else {
        varlist = Populate(thiscommand.pattern, command, thiscommand.name)
        HandleSingleCommandPattern (command, thiscommand, varlist)
      }
    }
  ]]></function>
  <function name="UnresolvedCommand" parameters="objectname, varname"><![CDATA[
    // TO DO: Update names below, we don't need these two variables
    unresolvedobject = objectname
    unresolvedkey = varname
    if (HasString(game.pov.currentcommandpattern, "unresolved")) {
      if (ListCount(game.pov.currentcommandvarlist) > 1) {
        msg (game.pov.currentcommandpattern.unresolved + " (" + unresolvedobject + ")")
      }
      else {
        msg (game.pov.currentcommandpattern.unresolved)
      }
    }
    else if (HasScript(game.pov.currentcommandpattern, "unresolved")) {
      do (game.pov.currentcommandpattern, "unresolved", QuickParams("object", unresolvedobject, "key", unresolvedkey))
    }
    else {
      // Infocom style mod
      if (Instr(unresolvedobject, " ") > 0) {
        unresolvedobject = Split(unresolvedobject, " ")[0]
      }
      if (game.pov.parent.dark) {
        msg ("It's too dark to see!")
      }
      else {
        handled = ObjectExists(unresolvedobject)
        if (handled) {
          msg ("You can't see any " + unresolvedobject + " here!")
        }
        else {
          // sort through command patterns' verbs AND directions
          // msg ("UnresolvedCommand")
          handled = ActionExists(unresolvedobject)
          if (handled) {
            msg ("You used the word \"" + unresolvedobject + "\" in a way I don't understand.")
          }
          else {
            msg ("I don't know the word \"" + unresolvedobject + "\".")
          }
        }
      }
    }
    game.unresolvedcommand = game.pov.currentcommandpattern
    game.unresolvedcommandvarlist = game.pov.currentcommandvarlist
    game.unresolvedcommandkey = unresolvedkey
  ]]></function>
  <function name="ObjectExists" parameters="o" type="boolean">
    handled = false
    foreach (obj, AllObjects()) {
      if (Left(LCase(obj.name),6) = Left(LCase(o),6)) {
        handled = true
      }
      if (not handled and HasAttribute(obj, "alt")) {
        foreach (altname, obj.alt) {
          if (Left(LCase(altname),6) = Left(LCase(o),6)) {
            handled = true
          }
        }
      }
    }
    return (handled)
  </function>
  <function name="ActionExists" parameters="s" type="boolean"><![CDATA[
    exists = false
    // foreach (cmd, AllCommands()) {
      // if (IsRegExMatch(cmd.pattern, s)) {
        // exists = true
        // }
      // }
    foreach (cmd, AllCommands()) {
      pattern = Left(cmd.pattern, Instr(cmd.pattern, " "))
      pattern = Trim(pattern)
      if (EndsWith(pattern,"(")) {
        // msg ("WINNER")
        // msg(LengthOf(pattern))
        pattern = Left(pattern,LengthOf(pattern)-1)
      }
      // msg (pattern)
      if (LengthOf(pattern)>0 and IsRegExMatch(pattern, s)) {
        exists = true
        // msg ("<code>"+pattern+"</code>")
      }
      if (not exists) {
        // Check directions (not in, out, up,or down)
        pattern = "^(north|east|south|west|northeast|northwest|southeast|southwest|n|e|s|w|ne|nw|se|sw)$"
        exists = IsRegExMatch(pattern,s)
      }
    }
    return (exists)
  ]]></function>

TODO

I seem to have everything handled, except...

In the original game, if (say) there is a ball and a bucket:

PUT BALL IN BUCKET

BALL PUT IN BUCKET

Both of those will put the ball in the bucket.

(I wonder if the parser is rearranging text, or if the PUT IN command has a pattern to match PUT <OBJECT1> IN <OBJECT2> as well as <OBJECT1> PUT IN <OBJECT2>. Probably the latter, but I don't know.)


BALL PUT

This will ask "What do you want to put the ball in?"


BALL BUCKET PUT

This will say "That sentence isn't one I recognise."
(I've got this one handled already.)


Out of curiosity, how does it respond to "put ball bucket"? Or "put in bucket ball"? "in bucket put ball"? "ball in bucket"?

Looking at the examples you gave, I'm wondering if it's using some kind of grammar system rather than just patterns; in which case it might be simpler to rewrite HandleSingleCommand to split the command into a list of words, identify each of those as nouns or verbs, and compare the number of nouns/prepositions to the command's requirements.


All four of those were met with:

That sentence isn't one I recognise.

I was thinking the same thing about the grammar system, though. (In fact, I'm still thinking it.)


Found the syntax stuff for PUT. It seems the patterns all use the word "put", then all the synonyms for "put" are listed separately -- much like an object and its aliases. Pretty smart.

<SYNTAX PUT OBJECT (HELD MANY) IN OBJECT = V-PUT PRE-PUT>
<SYNTAX PUT OBJECT (HELD MANY) AT OBJECT = V-PUT-IN-FRONT PRE-GIVE>
<SYNTAX PUT OBJECT (HELD MANY) BEFORE OBJECT = V-PUT-IN-FRONT PRE-GIVE>
<SYNTAX PUT OBJECT (HELD MANY) DOWN OBJECT = V-PUT-ON PRE-PUT>
<SYNTAX PUT OBJECT (HELD MANY) ON OBJECT = V-PUT-ON PRE-PUT>
<SYNTAX PUT OBJECT (HELD MANY) AROUND OBJECT = V-PUT-ON PRE-PUT>
<SYNTAX PUT OBJECT (HELD MANY) OVER OBJECT = V-PUT-ON PRE-PUT>
<SYNTAX PUT OBJECT (HELD MANY) ACROSS OBJECT = V-PUT-ON PRE-PUT>
<SYNTAX PUT DOWN OBJECT (HELD MANY HAVE) = V-DROP PRE-DROP>
<SYNTAX PUT OBJECT UNDER OBJECT = V-PUT-UNDER> 
<SYNTAX PUT ON OBJECT (FIND WEARBIT) (HAVE) = V-WEAR>
<SYNTAX PUT OBJECT BEHIND OBJECT = V-PUT-BEHIND>
<SYNONYM PUT STUFF INSERT PLACE LAY>

Looks like the parser searches the lists of synonyms from the commands' syntax to find the VERBs in the player's input. Then it finds the object(s).

So, my guess was wrong. (Good thing I didn't gamble any extra time on it. Ha ha!)


EDIT

Also, I'm feeling kind of like a jerk.

I've been trying to find ways for one function to check through all the command patterns for verbs. I thought of just adding an attribute to each command which was a list comprised of the verb and its synonyms, but thought, 'nah. Too much work.'

Well, now I see that the people who wrote the classics took the time to include the list of synonyms for each action. So, gosh darn it, I'm gonna do that , too!


Hmm… in those cases, I would probably build 3 lists of words (objects, verbs, other) or a dictionary mapping words onto "verb", "noun", "other"

Then HandleSingleCommand would split the command into words, and look them up. If it finds a verb, it makes note of it. If it finds an unknown word, it can report [I don't know the word "foo".] and finish. No need for a load of regex comparisons.

After the loop finishes, if a verb hasn't been found, it can report [There was no verb in that sentence!] and end - again, saving a lot of string comparisons.

If you have a command that has a verb in it and no unknown words, you can look it up as normal, with the message "That sentence isn't one I recognise." if it doesn't match.

Then ResolveName can also use the same lists. It looks up the word it's been given in the list first to see what type it is.

  • noun → Look up the object as normal. If not found, report "You can't see any foobar here!"
  • verb/other → Report [You used the word "get" in a way that I don't understand.]

Actually… a more efficient way to do it might be having a dictionary of objects, dictionary of verbs, dictionary of verbs, and list of other words ('in', 'the', 'it', etc). Most of these could be assembled by looping over AllObjects, AllExits, and AllCommands on startup.

The dictionary keys could be the first six letters of the word, and the value an objectlist. That way, HandleSingleCommand has a list of commands which use that verb (for example, "look" could point to look, lookat, and lookdir), so it doesn't need to try the pattern for every command in the game. Saves time by making an index. And ResolveName doesn't need to do all the complex matching; it just looks at the objects in the list, and checks if any of them are in scope.


I was thinking about adding a synonyms string list attribute to each verb/command. A dictionary definitely sounds a little easier than that.


Oh, I found some unexpected behavior.

I have a thing object. SYNONYMS: thing, gift, aunt's

I can EXAMINE AUNT'S BUTT, and it examines the thing.

In the real game, however:
>examine aunt's butt
I don't know the word "butt".

I tried to break it down by splitting at spaces (if they existed) and running each word back through CompareNames with a foreach, but that made the game freeze up.

Hrmm...


I don't know the word "butt".

That's the response I would expect for the method I described. Do you want it to ignore the extra word? I assumed that the existence of an "I don't know the word" message implies that unknown words will cause an error.


I must have missed something. I'll look at it again.

The way I've got it, it seems to replace " " with "", which combines all the words. So, aunt's butt:

aunt'sbutt
123456789

// This becomes:

aunt's


Even aunt's zzzz zz z zzz would end up being read as aunt's the way I've got it.

I started off listening to Black Sabbath, see, and I was coding well then.

...but, ever since The Doors came on, I've been just kinda starin' at the screen, ya' know?

I think I'll take a break and come back and reread your posts before I get back into it.


I hadn't noticed your code; we must have been typing at the same time earlier.

First thing I notice:

              verbExists = false
              foreach (cmd, commandArray) {
                verbExists = ActionExists(cmd)
              }

This tests if the last word is a verb.
You probably want either:

              verbExists = false
              foreach (cmd, commandArray) {
                verbExists = verbExists or ActionExists(cmd)
              }

or:

              verbExists = false
              foreach (cmd, commandArray) {
                if (ActionExists(cmd)) {
                  verbExists = true
                }
              }

Here's a rough guess at how I'd do this.

I'm assuming game.object_words, game.verb_words, and game.exit_words are dictionaries. The key is the lowercased first 6 letters of the word; the value is an objectlist containing matching objects. Meanwhile, game.other_words is just a stringlist of words like "again", "him", "it", "the", "to", "and", etc. Words that are neither objects nor verbs, but may appear in a command.

For those objects, I'm assuming that we treat each word separately. So an object with a two-word name appears on both lists.

HandleSingleCommand:

commands = NewObjectList()
valid = true
foreach (word, Split(LCase(command), " ")) {
  if (valid) {
    token = Left(word, 6)
    while (DictionaryContains (game.synonyms, token)) {
      token = DictionaryItem (game.synonyms, token)
    }
    if (DictionaryContains (game.verb_words, token)) {
      nonefound = true
      foreach (cmd, DictionaryItem (game.verb_words, token)) {
        if (cmd.parent = null or cmd.parent = game.pov.parent) {
          list add (commands, cmd)
          nonefound = false
        }
      }
      if (nonefound) {
        msg ("I cannot "+word+" here.")
        valid = false
      }
    }
    else if (not (DictionaryContains (game.object_words, token) or DictionaryContains (game.exit_words, token) or ListContains (game.other_words, token))) {
      msg ("I don't know the word \"" + word + "\".")
      valid = false
    }
  }
}
if (valid and ListCount (commands) = 0) {
  msg ("There was no verb in that sentence!")
  valid = false
}
if (valid) {
  // Do the usual HandleSingleCommand stuff here
  //   but… we can use the objectlist 'commands' created above
  //   instead of ScopeCommands(), which should be more efficient
  // If we can't resolve the command, we should display:
  //     msg ("That sentence isn't one I recognise.")
  // as the other options would have prevented us reaching this point.
}

And then ResolveNameFromList can be a bit simpler.

  • Look up the value it was given in object_words or exit_words as appropriate
    • If it isn't there, msg ("You used the word \"" + value + "\" in a way I don't understand.")
    • If value is multiple words, loop over them removing from consideration any objects that don't match all of them.
  • We have a list of valid objects. Remove any that aren't in scope.
  • If there's none left, it's time to say msg ("You can't see any " + value + " here.")
  • If there's more than one object, do the disambiguation thing.

I wasn't thinking about everything correctly.

I (guess I) was thinking CompareNames would stop the process if a value didn't match anything, but I was being utterly foolish.

I realized this whilst reading that last code you posted.

With CompareNames, it starts false. If it finds one match (just one), it becomes true.

Dur!

You're starting off true and if one word isn't matched, it kicks it back as false, but it will search through all the aliases and synonyms listed, leaving it false is nothing is found in the end.

Well played, sir. At least one of us was thinking clearly. :)


Well, I've decided against using all the code I've added to make it match the first six characters.

That was a limitation back then (as we were already aware), and I just found an interview with the man who wrote the game I'm porting in which he complains about that particular limitation. It pissed him off. He would have liked to have been able to have a smart-ass response for every word entered by the player -- not just the first 6 letters of each.

So, I'm losing all the bits concerning 6 characters. It has to mach a full word (or phrase) or no match. It still doesn't search each word within a phrase to match just one word.

Also, I adjusted "I don't know the word ____." Now, if there is a space in the string, it will print "I don't know the phrase ___."


I was having fun porting the game to Quest 5 up until I started messing with all this.


Here's a question: When porting something old to Quest, is it more fitting to leave Quest's basic functionality (like the parser) intact so it behaves as Quest users expect, or to modify nearly everything to make the game behave as it originally did?

EDIT

...because, if I'm truly replicating this, X will not be understood; the player will have to type out EXAMINE or LOOK AT. I don't think anyone would appreciate that very much.


Here's a question: When porting something old to Quest, is it more fitting to leave Quest's basic functionality (like the parser) intact so it behaves as Quest users expect, or to modify nearly everything to make the game behave as it originally did?

I think that someone who's used to the original game should be able to get through it in the same way; but someone who's used to Quest shouldn't find features missing. So… add features to match the original, but don't take anything away.

Like for my Circus and The Time Machine remakes, I added alternatives to some of the commands (so "put rock in lever" is a synonym for "wedge lever", "give fish to dolphin" is a synonym for "feed dolphin", and "use cable on terminals" is a synonym for "short terminals". And other examples where it felt a bit guess-the-verb, I added alternatives like "switch off" and "turn on" so that operating a flashlight doesn't require you to think of the verbs "illuminate" and "extinguish". More controversially, I decided it was appropriate to add descriptions in case you want to examine an object,

I think it's nice being able to play a game in the way it was originally; but in some cases I think it's better for the game to work in an intuitive way. So I always made sure that if you treat the game like its 13KB original, it will behave like you expect; but if you play it assuming Quest conventions, it will still do what you expect

(One more example: "You can see a panel." What kind of panel is it? Is it a control panel? A piece of a wall? It turns out to be an access panel that you could force open to enter the lab, but the only way to discover this is by typing "lever panel" while holding a crowbar. I think that's something that should have been changed, because it's a clue that would have been obvious to the character in the game, but in the original it was hidden from the player. I don't like "idiot plots" in games, when the memory or resolution issues that game rise to them no longer apply)


Dave Lebling has referred to this more than once:

flowchart-image


I think that someone who's used to the original game should be able to get through it in the same way; but someone who's used to Quest shouldn't find features missing. So… add features to match the original, but don't take anything away.

That's a good goal.


This is not unlike what I'm doing.

https://github.com/historicalsource/hitchhikersguide

Specifically:

https://github.com/historicalsource/hitchhikersguide/blob/master/parser.zil

and:

https://github.com/historicalsource/hitchhikersguide/blob/master/syntax.zil

...and maybe this too:

https://github.com/historicalsource/hitchhikersguide/blob/master/verbs.zil


With the various games I have "ported/translated" from the original, I allow all the current functions/facilites that are used in the APP I am using while ensuring that all verb-noun combinations in the original will work in the new version.


UPDATE

Rejoice!

I wanted to port the SOLID-GOLD EDITION in the beginning, but couldn't find that source code.

...but now I've found it! And guess what? The solid-gold edition doesn't allow the player to input just the first 6 characters. Whole words are required (just like I prefer it, and now I see why).

So, ha ha! Hoo hoo! I don't have to mess with that part now.

The solid-gold editions are the definitive releases, after all.


I allow all the current functions/facilites that are used in the APP I am using while ensuring that all verb-noun combinations in the original will work in the new version.

Same approach suggested by mrangel.

If both of you suggest the same thing, it sounds like it's definitely the way to go!


@mrangel

https://textadventures.co.uk/forum/quest/topic/bcumxvm5ous9xl0uz8lvrq/another-parser-modification-question#a0a9a2fa-cf8c-4da5-923a-8df66d999bb3

That's sort of what I want to do.

Take the "attack" command, for instance. I usually make it so the following words all attack: attack,hit,kick,slap,kill,push,shove

So, for my attack command, I'll have a stringlist of synonyms including all those words. Each command has a synonyms attribute with a string list of its synonyms. Each synonym is a single word (no prepositions).

The attack command still has a normal pattern attribute.

That's a bad example.

How about "go". I'd say we should include: go, walk, proceed, run, step

Those are the synonyms.

The pattern: ^(go|walk|proceed|run|step)( (to|towards)|) (?<exit>.*)$

I'd also add a prepositions string list attribute to this command: to, towards

WALK TOWARDS THE NORTH

Quest handles the "THE" already, but I'd have another string list attribute somewhere including all the articles.

With those additions, Quest will know every word that's in its vocabulary. (The object names and altnames are already handled.)


@Artoo

With the various games I have "ported/translated" from the original, I allow all the current functions/facilites that are used in the APP I am using while ensuring that all verb-noun combinations in the original will work in the new version.

What about misspellings and such? Leave 'em or fix 'em?


Makes sense.
With the code I'd shown above, I'm assuming global dictionaries for each word type. But there's no reason you couldn't have a startup script to collate them:

game.object_words = NewDictionary()
game.verb_words = NewDictionary()
game.exit_words = NewDictionary()
game.other_words = Split("the;a;an;of;in;")
foreach (cmd, AllCommands()) {
  if (HasAttribute (cmd, "synonyms")) {
    foreach (word, cmd.synonyms) {
      if (not DictionaryContains (game.verb_words, verb)) {
        dictionary add (game.verb_words, verb, NewObjectList())
      }
      list = DictionaryItem (game.verb_words, verb)
      if (not ListContains (list, cmd)) {
        list add (list, cmd)
      }
    }
  }
  if (HasAttribute (cmd, "prepositions")) {
    foreach (word, cmd.preopsitions) {
      if (not ListContains (game.other_words, word)) {
        list add (game.other_words, word)
      }
    }
  }
}
foreach (obj, AllObjects()) {
  words = Split (LCase (GetDisplayAlias (obj)), " ")
  if (HasAttribute (obj, "alt")) {
    foreach (alias, obj.alt) {
      foreach (word, Split (LCase (alias), " ")) {
        if (not ListContains (words, word)) {
          list add (words, word)
        }
      }
    }
  }
  foreach (word, words) {
    if (not DictionaryContains (game.object_words, word)) {
      dictionary add (game.object_words, word, NewObjectList())
    }
    list = DictionaryItem (game.object_words, word)
    if (not ListContains (list, obj)) {
      list add (list, obj)
    }
  }
}
foreach (obj, AllExits()) {
  words = Split (LCase (GetDisplayAlias (obj)), " ")
  if (HasAttribute (obj, "alt")) {
    foreach (alias, obj.alt) {
      foreach (word, Split (LCase (alias), " ")) {
        if (not ListContains (words, word)) {
          list add (words, word)
        }
      }
    }
  }
  foreach (word, words) {
    if (not DictionaryContains (game.exit_words, verb)) {
      dictionary add (game.exit_words, verb, NewObjectList())
    }
    list = DictionaryItem (game.exit_words, word)
    if (not ListContains (list, obj)) {
      list add (list, obj)
    }
  }
}

So that it's easy for parser to look up a list of all commands that contain this particular verb-word. Means that rather than comparing the entered command against the pattern of every command, we have a pre-generated list of commands that it could match. Smaller number of comparisons, so more efficient (if using a little more memory to store the lookup table). And if it doesn't match a regexp, we know that it's a case of the words being in the wrong order, not an unknown word.


I usually fix spelling / grammar unless it is germane to the story. (ie. a pirate speaking or a gangster)


@mrangel

That looks pretty good!

Thanks!


@R2

Cool.

Also, just to be sure everyone agrees:

til - until
'til - until
till - an action not unlike digging

Just making sure "till" isn't an acceptable form of "until".


It might also be useful having synonym-words and verb-words as separate things. Verb words are recognised by each command's pattern; synonym words are stored in a dictionary so that the parser will try Replaceing if necessary to get a match. For commands with quite complex patterns, it could be useful to have a separate substitution dictionary as the first step.


til is short for until but both mean till. Until is usually used to begin a sentence.

till can be a verb - to till the ground
a noun - He put the customer's money in the till
or a preposition or conjunction - as late as or up to a time. - till the time when...
These definitions are from the Collins English Dictionary and the Oxford Dictionary.

Isn't English wonderful!


Yeah, it seems that until can be shortened to either "til" or "till". Not sure if that's a regional variation.


This topic is now closed. Topics are closed after 60 days of inactivity.

Support

Forums