A recent topic in the QuestJS/QuestKit/Quest6 forum got me thinking about parser design; and how regular expressions aren't quite flexible enough for some of the things we might want of them. It's something I'd thought about before; but I came to the conclusion that the system I'd like to see would be way too tough to implement in Quest simply due to the awkward string-processing functions.
Now I'm thinking about it again. And I don't think this is Quest-specific; I'm pretty much talking about an arbitrary text adventure here.
How do you go about handling parameters for a command that the player could enter? I've got a system in mind now, and thought I'd share it in case anyone else has feedback. Is there anything that you'd want to do that my concept doesn't allow?
Let's start with a simple command:
// "TAKE #object#"
take.pattern = [[
["get", "take", "pick up"],
{
argument: 'objects',
multiple: true,
scope: GetVisibleNotHeld
}
]];
So that's an array containing an array (I'll explain that later). The first element is an array containing "get", "take", and "pick up" - different ways the player could refer to the command. The second is a dictionary (or whatever your language calls it) representing the value we want to capture.
The fields in this dictionary could include:
argument
- the name by which this argument will be passed to the command's scriptmultiple
- false by default. If true, the argument will be returned as an arrayexclusive
- if more than one parameter has the same 'argument' field, only match one of them. Default: same value as 'multiple'. If not exclusive and not multiple, the one with the highest priority will be returned.scope
- by default, all visible objects. Either an array of possible values, a regex, or a function which returns an array or regex. In this case I've used the name of a Quest function as a placeholder, as it's pretty obvious how that would work.scopes
- backup scopes. By default this is the array [scope, visible_objects, all_objects] if the scope contains objects. It's an array of arraysoptional
- if true, this argument is optional. Allowing too many optional or multiple parameters may make the parser really slowpriority
- If multiple commands match what the player typed, the one with the highest priority goes first. By default, the priority value is higher if the argument matches an object in its primary scope (so you can have commands with the same name but different scopes, and one will be chosen based on the objects entered). This can be a function, whose parameter is the matched object(s)disambiguate
- a function which can be called to choose between multiple matching objects. Its arguments will be a player-entered string, and a dictionary mapping aliases onto objects. This function can modify the dictionary.min
/max
- minimum and maximum number of objects for a multiple parameterpreposition
- This parameter may be preceded by a preposition. This can be a string, array, or dictionary (exactly as if it were another parameter). If the preposition doesn't have its optional field explicitly specified, it will be the inverse of the parent parameter's optional (this makes sense; trust me)reorder
- If true, allow this parameter to be moved later in the order if necessary to create a match. If the value is the name of another parameter, this one can't be moved past it. If this field is the same as the preposition, allow reordering only if the preposition is present.everything
- If the player enters "all" or "everything" for a multiple parameter, what should be taken? Default will be all objects in scope which have not been entered for another parameter (so "put all in bag" won't try to put the bag in itself). Setting this to false will make "put all in bag" include the bag as part of all. Setting it as a function will use that function instead of scope.pronoun
- will this parameter be remembered so the player can refer to it as "he", "it", etc in future commands?return
- value to be returned for this argument; if undefined
, don't return anything. Default: the object matchedSome of these may seem a bit weird. So here's some more examples:
// "GIVE #objects# TO #npc#"
// "GIVE TO #npc# #objects#"
// "GIVE #npc# #objects#"
giveto.pattern = [[
["give", "offer"],
{
argument: 'npc',
scope: GetVisibleNPCs,
preposition: "to",
reorder: "to"
},
{
argument: 'objects',
scope: GetReachableInventory,
multiple: true
}
]];
// "DROP #object#"
// "DROP #object# IN #container#"
// "PUT #object# IN #container#"
// "PUT DOWN #object#"
// "PUT #object# DOWN"
// "TOSS AWAY #object#"
// "TOSS #object# AWAY"
drop.pattern = [[
{
argument: 'verb',
scope: ["drop", "discard", "put", "throw"]
},
{
scope: (matched) => ({put: ['down'], throw: ['away']}[matched['verb']] || /^(?!.*?)/),
reorder: true,
optional: true
},
{
argument: 'objects',
multiple: true,
scope: GetReachableInventory
},
{
argument: 'destination',
scope: GetReachableContainers,
preposition: /^(in|on)(to| to|)$/,
optional: true
}
]];
Yeah, I put too much thought into this. I think it's pretty basic, easy to understand the easy stuff. Then lets you add the weirder options if you need them.
Oh, the nested arrays.
OK. What a pattern matches depends on its type:
Obviously, an array matching "any of these items", it would be meaningless to have another array inside it meaning the same thing.
An array within an array matches all of the items in the array, in order. (like the words in a longer command)
And an array inside an array inside an array matches any one of its items again (like the ["get", "take", "pick up"]
example)
Got to admit, Quest 6 parser is pretty much a development of the Quest 5 parser, and I have since become away there are other ways to do it.
With regards to the fields, reorder makes a lot of sense; might also be useful for translating to other languages with different word order. Not so sure about min/max, preposition or disambiguate....
When you say:
OK. What a pattern matches depends on its type:
Are you matching the specific object or the whole command? I assume the former? But then how do you match to a plain object, other than by string or regex?
Can you give an example of when a function might be used? I appreciate this is the ultimate in flexible, but I am not seeing when it would actually be used.
Not so sure about min/max, preposition or disambiguate....
Not sure about min/max; but experience on the forum suggests that someone is going to ask for them.
preposition
I added because it seems the easiest way to do some commands; notably the "give to" example. The preposition "to" indicates which object you're trying to give things to if it isn't the first. (Theoretically, reorder
could try every possible guess until the scope matches, which would probably work. But allowing words like "to", "with", "for" to be used as hints to the parser for which object goes into which argument gives quite a lot of flexibility.
disambiguate
I wouldn't have thought of, but it's basically an easy hook. There have been quite a few posts on the Quest forum about how to change the behaviour of disambiguation responses. Including "if objects A and B both match what the player typed, assume they mean A", or "prefer whole words" or "prefer no unmatched words"
One example I can think of might be directions. If a command expects the user to enter a compass direction, typing "sout" almost certainly means "south", not "southwest", even if both are options.
Are you matching the specific object or the whole command?
A pattern matches a whole string. So simple patterns like "wait"
or /^i(nv(entory)?)?$/
match the command.
If there's an array at the outer level, an array will try to match each of its members against the whole command. Such as ["rest", "sleep"]
.
If there's an array inside an odd number of nested arrays, it will attempt to match its first member against the beginning of the command, and its remaining members successively against what remains of the string.
I can't find an easy way to describe this, but it should look pretty intuitive if you look at the examples.
But then how do you match to a plain object, other than by string or regex?
Don't think I understand the question. A plain object in the JS sense? A plain object in the pattern can match a string, a regex, an object from a list, or whatever else is in it's scope
field. It just lets you specify other options like assigning the result to a named variable or similar.
(I should have said 'dictionary' rather than 'plain object' because I was trying to describe the structure in a language-agnostic way, and JS terminology 'object' could be confused with an in-game object, but I hope you know what I meant)
Can you give an example of when a function might be used?
I can't think of one. That's basically an option for people who want to rewrite part of the parser to work in a different way without completely starting over. If it's implemented in JS or Perl (or most modern languages with flexible types), this will be trivial to implement, and basically allows the user to do "anything the developer didn't think of".
I was already thinking that most of the fields could have a function reference, so the user's code can dynamically change the pattern at runtime.