In text-based games, also known as Interactive Fiction or IF, the game describes a room to the player who then types in a command and the game responds.
If you’re familiar with Zork, Enchanter, Anchorhead, or even Colossal Cave Adventure, you already know what I’m talking about. If not, below is a snippet of a portion of a made-up game:
Your home office was never clean by any stretch of the imagination, but it’s been worse since the accident. You tell yourself that there’s a method to the madness and you know where to find anything of importance, but even you have to admit that when you need to keep the dog out of a room for fear of him ripping up bank paperwork that you have issues.
Your desktop computer is here on the desk. It looks to be busy installing important system updates at the moment.
A set of glass double doors leads east to your hallway.
Look at the computer screen
The computer displays an animated progress indicator with no signs of getting anywhere close to complete.
Unplug the computer
Not right now! It’s busy installing important updates!
Hopefully you get the idea. The game describes a situation and the player types a command to interact with the environment in a way the designer hopefully expected and has a meaningful response available for.
Whenever I learn a new language, almost invariably I’ll write a text-based game in that language. It’s how I learned architecture, design, and nuances of various languages as a kid and there’s a certain nostalgia to it.
Angular IF uses a custom variant of Angular Material and Materialize CSS, but at its core, it’s the same concept as the old black and white text adventures.
The user types a sentence into the
UserInputComponent which is then sent to the
InputService which interprets the input and updates the story, emitting an event that the
StoryViewComponent receives and displays to the user.
So how does the game make sense of what the player types?
Parsing Text Input
At a high level, Angular IF does the following things with user input:
- Tokenize the input text into individual word ‘tokens’ that can be interpreted
- Use a Lexer to get contextual information on parts of speech for the individual tokens
- Parse the tokens into a web of dependencies, making grammatical sense of the sentence
- Interpret the objects in the sentence with objects in the local environment where possible
- Execute the interpreted command by sending it to the appropriate verb handler
I’ll break these out in more detail with a sample sentence in the following sections.
A Sample Scenario
Take a look at the following room description with added emphasis on declared objects:
The walls of this small room were clearly once lined with hooks, though now only one remains. The exit is a door to the east.
We’re going to parse the following short sentence:
Put my cloak on the small hook
Before we look at how this breaks down, it should be noted that AngularIF requires sentences structured like this. Specifically it requires an imperative sentence starting with a verb. This greatly restricts the types of things users can type and makes the problem much more manageable.
Tokenizing and Lexing
We’re going to talk about Tokenizing and Lexing in tandem because both of these tasks are accomplished in AngularIF via a call to the Compromise NLP library.
In AngularIF, tokenizing looks like this:
Before we call compromise, we do some standard string replacement to clear up any ambiguous or compound words that Compromise has demonstrated potential to get confused by:
After this replacement is complete, we call
getTokensForSentence on a
NaturalLanguageProcessor class I wrote that wraps around the compromise library. This method calls to a few other methods that ultimately chains down to this call:
nlp is the instance of compromise.
So, back to the example, the phrase
put my cloak on the small hook would parse out the token
put as the following:
So here we see that Compromise thinks that
put is a verb that can be used in the past-tense or as part of a verb phrase, but Compromise’s best guess is that
put is a verb. It’s correct.
And so we see that with a simple call to Compromise, we get a lot of information on parts of speech that didn’t require any custom definitions at all.
If I give Compromise a word it has no idea about, it tells me what it does know about it. For example, the input
Madeupword gets interpreted as follows:
So here, it interprets it into a Noun as its best guess and tells me that it appears singular based on the end of the word and it’s in title case. Defaulting to a noun is a very good decision in our case, because new nouns are much more likely than new verbs with a fairly limited set of actions supported by most text-based games.
Now that we have a set of parsed terms, we can start to make sense of the ordering. Right now we have the following:
- put (Verb)
- my (Adjective)
- cloak (Noun)
- on (Preposition)
- the (Determiner)
- small (Adjective)
- hook (Noun)
AngularIF looks at that and immediately notes that it doesn’t start with a Subject, so the game implicitly adds I (Noun) to the beginning of the sentence. With a bit of styling from AngularIF’s debugging view, our sentence can now be displayed in the following way:
Here the color coding and relative sizing of the elements helps us start to make sense of the sentence. We really care about a verb and a sequence of objects that can be fed into the verb handler. The verb and objects are easy, but let’s look at the other words.
The adjective my applies to the noun cloak, so it becomes attached to that.
The preposition on and the determiner the both similarly apply to the noun hook.
Given these modifiers, we can represent our sentence as
I put cloak hook. The on preposition is actually important as many verb handlers need to know if you’re trying to do something under, above, inside of, on, etc. but for the simplicity of sentence parsing, our main functions are the nouns and verb.
The sentence can then be represented as follows:
Now it’s becoming a lot more clear what the user is actually saying, due to the structure of imperative sentences and the information Compromise is providing.
The next step is to interpret the meaning of these nouns.
Here we take a look at all of the nouns listed in the command and we try to map them to objects registered in the current room or attached to the player. We also need to match against some constant things such as cardinal directions.
This is fairly easy to do based on matching registered adjectives and synonyms for objects present in the room, so I’ll spare that code and focus more on the parser.
After interpreting our input it looks like the following:
Here we are able to make sense of what the user is talking about in the environment and have something concrete to hand off to the engine to execute.
If the user tried to refer to something that wasn’t coded as existing in the room, the interpreter could be unable to resolve some of the nouns and you’d get something like the following:
Here, the word bug was recognized as a noun, but was not mapped to any known game concept, so the game engine would respond back:
You don’t see a bug here.
If only all responses were as bug-free.
Now that a completed graph of the user’s intent is available, the system looks for a handler registered for the verb the user entered. For example, with the put verb, the system knows about it and invokes it, passing in the sentence graph. The handler looks at the objects in the sentence and it knows that the first object will be what we’re putting and the second will be where we’re putting it (and how, if there’s a preposition such as under).
If a verb handler doesn’t have all the info it needs or is confused, it can spit back a custom tailored response to the user.
If the user tries a verb that doesn’t have a handler, the system can say back something like:
You won’t need to use the verb
eatto win the game. For a complete list of verbs available, type
What can I do?
Fortunately, putting the cloak on the hook is perfectly valid and the system spits back:
Your score has just gone up by 1 point.
You hang the black velvet cloak on the small brass hook.
While this is a high-level overview of sentence parsing using Compromise NLP, I’m hopeful that this article gets you thinking about the things the library can help you achieve. I strongly recommend you look over the compromise website for a wide variety of examples and next steps.
If you’re curious about my own code for AngularIF, the code is available on GitHub. I should warn you that it is in Angular 4 still and has a significant number of vulnerabilities and bugs, so I recommend you update dependencies if possible. Still, the code should be instructive to anyone interested in learning more about parsing imperative sentences.
If you do something cool with either compromise or AngularIF, please let me know; I’d love to hear about it.