In the martial art of Tai Chi there is a set of exercises called Chi Kung (or sometimes Qigong) which are often described as standing still to be fit. Sometimes game dev is a bit like that when you realise something you’ve designed could be done a lot better, so you go back and change it. And while it seems like you’ve gone nowhere, since there’s no outward change, your code is now much more fit for purpose. Today is one of those days!
After having a think about our previous multi command parser I came to the conclusion that it was wholly inadequate. It was hugely prone to bugs and was not at all useful when it came to processing a multi command input structure.
Fortunately I’ve come up with a system that will overcome these problems with ease! I’m not taking down the previous post however as it does contain useful housekeeping elements. I will edit it to direct viewers attention to this post as well though.
Of course first of all, as always, there’s a little housekeeping to do! The look function works well but it has a couple little issues surrounding the sentence structure created when there was only one object in a room, or non. And there’s a very easy fix for that.
Inside the look function add a new integer called numObjects and set it to zero. We’ll use this to decide what to do if there’s no objects, or one or more objects. What we need to do it move the start of the sentence “I see ” inside of the for loop so that if there are no objects, it doesn’t get added to the structure. So when we find an object for the first time, we’ll add it to the structure there. And when we find an object, we’ll also need to increment out numObject counter. so the first if statement will look like this now:
for(int i = 0; i < MAX_NOUNS; i++)
{
if(nns[i].location == loc)
{
if(numObjects == 0)
{
str = str + “I see “;
}
numObjects++;
str = str + nns[i].description;
}
As you can see the addition of “I see ” will only happen the first time the loop comes across and object. The second thing you need to do is make sure that ” and ” is only added when there is more than one object in the room, so all we have to do is add that to the third if statement so it looks like this:
if(ismore == 1 && !andadded && numObjects > 1)
So along with the other conditions it will only get added if there’s two or more objects present. The last thing we need to do is only add the “. “, that finishes the noun section of the look function, if we have any objects at all, so just surround it in that condition.
if(numObjects > 0)
{
str = str + “. “;
}
Now the look function works much better where nouns are concerned! Time to sort out the parser. You can comment out pretty much the whole inside of the parser as we won’t be using any of it now. I never delete old code till I’m sure the new code is up and running, just in case. Half working code is better than no working code, at least you have a jumping off point to fix it if you still have it.
Before I get into the nitty gritty of the code I’ll walk you through how it’s going to work. We’re going to use a stepper technique (I don’t know if there’s a real name for what I’m doing here so I named it myself, feel free to call it what you want!). Simply put, it works by stepping through each word of the players input and executing each command in sequence, step by step. It’s a more more natural approach to parsing the player input and gives us the opportunity to decide what to do in cases where not enough commands where given in one section of the input while still being able to execute the rest of the input.
There’s always the chance the if the player makes a mistake early in the input it will effect the outcome and mess up the end of the input, but that’s something we’re going to let the player deal with. After all if the player can’t make a mistake then the puzzles aren’t really puzzles right?
To achieve what we want to do we’ll only need two vectors which will be run together to make sense of the input. The first is the stepper itself that will contain all the commands themselves and the second is a commander that will record what those words actually are so we know how to use them. Otherwise we’d have to run a loop every time we wanted to know what each work was and that’s just messy. Other than that all we’ll need is out counters i and j.
vector<int> stepper;
vector<string> commander;
int i = 0;
int j = 0;
Just like last time, our first job is to fill those vectors with useful information gleaned from the players input we’ve conveniently stored int he words vector. This works almost exactly the same as it did in the earlier version of the parser expect we’re just putting all the words into the one vector and their type in another so it will look like the following:
for(i = 0; i < static_cast<signed int>(words.size()); i++)
{
//check for verb
for(j = 0; j < MAX_VERBS; j++)
{
if(words[i] == vbs[j].word)
{
stepper.push_back(vbs[j].code);
commander.push_back(“VERB”);
}
}
//check for route command
for(j = 0; j < MAX_DIRS; j++)
{
if(words[i] == dir[j].word)
{
stepper.push_back(dir[j].code);
commander.push_back(“DIR”);
}
}
//check for nouns
for(j = 0; j < MAX_NOUNS; j++)
{
if(words[i] == nns[j].word)
{
stepper.push_back(nns[j].code);
commander.push_back(“NOUN”);
}
}
}
Easy. Of course we want to tell the user if they’ve not given us anything useful we can use so they know the game isn’t just ignoring them, again it’s almost the same as the previous parser:
if(stepper.empty() && words[0] != “QUIT”)
{
cout << “That didn’t make any sense.\n”;
return true;
}
Now we have the basics out of the way it’s time to get into the meat of the parser. This is where things look very different and become much more effective. Since we’re going to go through the words step by step we won’t need to use a while loop, a simple for loop will get us to the end very effectively. What we’re going to do inside that loop is check if the word is a verb, if it’s a very we’re going to check the next word and based on that information we’re going to do something. That may be go on to the next verb, or do something with the current verb. We start simply:
for(i = 0; i < static_cast<signed int>(stepper.size()); i++)
{
j = 0;
if(commander[i] == “VERB”)
{
It will become clearer why we set j at the start of each loop when we close the loop later. We will use it to move on smoothly to the next word if there is no verb at i in the first loop. Once we’ve picked up a verb we will want to start another loop to go through the following words if any before the next verb, which starts at the same position as we are currently in so we’re not back tracking:
for(j = 0; j+i < static_cast<signed int>(stepper.size()); j++)
{
we could have set j = i instead of using j+i in the next part of the statement to get the same effect, however we want j to start at 0 so we can use it more effectively as a counter inside the loop, and move on to the correct next word when we’re finished. It’s at this stage that we’re going to start examining exactly what it is we want to do with the words. Since we have updated the look function why don’t we start there.
What we want to do is check the current word in stepper to see if it’s LOOK, same as before. Then we want to check if there’s another word available in that list and if so what type of word is it. If it’s a direction we can use that to look that direction to see what’s there, if it’s a noun we can look at the object and if it’s another verb we’ll just look at the room around us, and also, if there isn’t another word at all, we can also just look at the room around us. The main body will look like this:
if(stepper[i] == LOOK)
{
//check if there’s another word in the stepper to use
if((i+j+1) < static_cast<signed int>(stepper.size()))
{
//The next word is a direction
if(commander[i+j+1] == “DIR”)
{
}
//The next word is a noun
else if(commander[i+j+1] == “NOUN”)
{
}
//The next word is a verb and we haven’t done anything yet
else if(commander[i+j+1] == “VERB” && j == 0)
{
}
}
//If there’s no more words in stepper to use and we haven’t already done something
else if((i+j+1) == static_cast<signed int>(commander.size()) && j == 0)
{
}
}
We’ll be pretty much using this exact structure for any verb we want, they’ll all follow the same logic. What we do with them will be vastly different but these checks will always be the same.
In the case of look if there is no more words, or the next word is a verb we’re just going to look around us so we’ll simply call the look function in those instances. In the last two else if’s add:
look(loc, rms, nns, dir);
If there’s a direction command following the verb well why don’t we describe what we find there, if there is something to describe there, so first thing we’ll do is check that:
//Check there’s something to look at
if(rms[loc].exits_to_room[stepper[i+j+1]] != NONE)
{
}
else
{
}
What we’re saying here isn’t to dissimilar what we were saying before. We’re checking if the room at our location, at the exit of the next word in the stepper (That we know for sure is a direction because we checked) exists (Not none, double negative!). In the else we can just tell the player that there is nothing in that direction. We might as well tell reflect back to them when direction that is in case they entered a long string and it isn’t obvious. Always make things as easy and transparent as you can for the player when it comes to the command structure. So in the else section we can add:
cout << “There is nothing to the ” + dir[stepper[i+j+1]].word + “.\n”;
We use the dir structure we created way back when to convert the code in stepper into a word the player will recognise. If something does exist we need to tell the player, and it’s a little trickier to get your head around the logic but bear with me. In the if statement itself we have rms[loc].exits_to_room[stepper[i+j+1]] which itself a location when it’s not NONE. So we’ll use that in place of loc and then call the description from there. Looks messy but is very useful:
cout << “I see a ” + rms[rms[loc].exits_to_room[stepper[i+j+1]]].description + “.\n”;
Once you’ve wrapped your head around that, doing the same for the nouns becomes a very simple, if slightly different matter. The only main difference is that we want to make sure the object is actually in the same location as the player so that they can actually see it!
//Check the object is actually in the same location
if(nns[stepper[i+j+1]].location == loc)
{
cout << “I see ” + nns[stepper[i+j+1]].description + “.\n”;
}
else
{
cout << “I don’t see one of those here”;
}
That’s look sorted out, next up is the go verb so we can move around our map. As you can see it’s uses the very same structure as we used for look:
if(stepper[i] == GO)
{
//Check if there’s another word in the stepper to use
if((i+j+1) < static_cast<signed int>(stepper.size()))
{
//The next work is a direction
if(commander[i+j+1] == “DIR”)
{
}
//The next work is a noun
else if(commander[i+j+1] == “NOUN”)
{
}
//The next work is a verb and we haven’t done anything yet
else if(commander[i+j+1] == “VERB” && j == 0)
{
}
}
//if there’s no more words in stepper to use and we haven’t already done something
else if((i+j+1) == static_cast<signed int>(commander.size()) && j == 0)
{
}
}
Last things first, easiest part here again is telling the player their input can’t be used in this case, simple add “Go where?” to the last two else if’s:
cout << “Go where?\n”;
Since technically a noun can be a place which potentially you could go to we’ll tell the player they can’t, we might be bursting their bubble but we can only do so much right? Just add this to solve those problems:
cout << “You cannot go there.\n”;
And if the next word is a direction, we deal with it pretty much exactly the same as with the previous parser by changing the players location and giving them a description of their new location if there was in fact an exit that way:
//Check there’s somewhere to actually go
if(rms[loc].exits_to_room[stepper[i+j+1]] != NONE)
{
loc = rms[loc].exits_to_room[stepper[i+j+1]];
look(loc, rms, nns, dir);
}
else
{
cout << “There is no exit that way.\n”;
}
Now we just need to stop looping through the inner loop if the next word is a verb or in fact we have no more words, so we can check the next verb without going to the end of the loop and missing them and so that if we get to the end of the stepper we can end gracefully instead of crashing out:
//stop inner loop if the next word is a verb or you hit the end of stepper
if(i+j+1 < static_cast<signed int>(stepper.size()))
{
if(commander[i+j+1] == “VERB”)
{
break;
}
}
if(i+j+1 >= static_cast<signed int>(stepper.size()))
{
break;
}
and before we end the outer loop we need to update i so we start at the next verb and not not necessarily just the next word since we may have used that already. So while we have too loops going we still only loop through stepper once. Very economical. We finish with:
}
}
i = i + j;
}
And that’s it, once you’ve compiled and tested the new parser to make sure you’ve worked out any little bugs that may have gone in due to human error and such, you can delete the old code as we won’t be using it any more.
While it may seem that not a lot has really happened this time around we’ve hugely improved how our parser works and going forward we shouldn’t have to do to much more to it as it accepts and processes everything we can throw at it in whatever order we throw it. Sometimes you just have to do the under the hood work, and that’s ok, it’s still progress even if the player can’t see it.
As always any question or queries feel free to post a comment or reach out to me on twitter (@SeveralBytes). Thanks for reading and I hope you get something from this!
Here is all the code for the parser after today’s additions to main.cpp:
#include <iostream>
#include <string>
#include <vector>
#include “rooms.cpp”
#include “nouns.cpp”
#include “verbs.cpp”
using namespace std;
bool section(string userInput, vector<string> &words)
{
string subString;
if(!userInput.empty())
{
//Make everything upper case for easier handling
for(int i = 0; i <= static_cast<signed int>(userInput.length()-1); i++)
{
userInput[i] = toupper(userInput[i]);
}
//Split userInput into a string vector for even easier handling later
for(int i = 0; i <= static_cast<signed int>(userInput.length()-1); i++)
{
if(userInput[i] != ‘ ‘ && i <= static_cast<signed int>(userInput.length()-1))
{
subString += userInput[i];
}
if(userInput[i] == ‘ ‘ || i == static_cast<signed int>(userInput.length()-1))
{
words.push_back(subString);
subString.clear();
}
}
}
else
{
words.push_back(“LOOK”);
}
return true;
}
bool look(int loc, room *rms, noun *nns, verb *dir)
{
string str = “I’m in a ” + rms[loc].description + “. “;
bool andadded = false;
int numObjects = 0;
for(int i = 0; i < MAX_NOUNS; i++)
{
if(nns[i].location == loc)
{
if(numObjects == 0)
{
str = str + “I see “;
}
numObjects++;
str = str + nns[i].description;
}
int ismore = 0;
int j;
for(j = i+1; j < MAX_NOUNS; j++)
{
if(nns[j].location == loc)
{
ismore++;
}
}
if(ismore == 1 && !andadded && numObjects > 1)
{
str = str + ” and “;
andadded = true;
}
if(ismore > 1)
{
str = str + “, “;
}
}
if(numObjects > 0)
{
str = str + “. “;
}
for(int i = 0; i < MAX_DIRS; i++)
{
if(rms[loc].exits_to_room[i] != NONE)
{
str = str + “To the ” + dir[i].word + ” is a ” + rms[rms[loc].exits_to_room[i]].description + “. “;
}
}
str = str + “\n”;
cout << str;
return true;
}
bool parser (int &loc, room *rms, verb *dir, verb *vbs, noun *nns, vector<string> words)
{
//By using a vector we can have multiple actions in one input
vector<int> stepper;
vector<string> commander;
int i = 0;
int j = 0;
for(i = 0; i < static_cast<signed int>(words.size()); i++)
{
//check for verb
for(j = 0; j < MAX_VERBS; j++)
{
if(words[i] == vbs[j].word)
{
stepper.push_back(vbs[j].code);
commander.push_back(“VERB”);
}
}
//check for route command
for(j = 0; j < MAX_DIRS; j++)
{
if(words[i] == dir[j].word)
{
stepper.push_back(dir[j].code);
commander.push_back(“DIR”);
}
}
//check for nouns
for(j = 0; j < MAX_NOUNS; j++)
{
if(words[i] == nns[j].word)
{
stepper.push_back(nns[j].code);
commander.push_back(“NOUN”);
}
}
}
if(stepper.empty() && words[0] != “QUIT”)
{
cout << “That didn’t make any sense.\n”;
return true;
}
for(i = 0; i < static_cast<signed int>(stepper.size()); i++)
{
j = 0;
if(commander[i] == “VERB”)
{
for(j = 0; j+i < static_cast<signed int>(stepper.size()); j++)
{
if(stepper[i] == LOOK)
{
//check if there’s another word in the stepper to use
if((i+j+1) < static_cast<signed int>(stepper.size()))
{
//The next word is a direction
if(commander[i+j+1] == “DIR”)
{
//Check there’s something to look at
if(rms[loc].exits_to_room[stepper[i+j+1]] != NONE)
{
cout << “I see a ” + rms[rms[loc].exits_to_room[stepper[i+j+1]]].description + “.\n”;
}
else
{
cout << “There is nothing to the ” + dir[stepper[i+j+1]].word + “.\n”;
}
}
//The next word is a noun
else if(commander[i+j+1] == “NOUN”)
{
//Check the object is actually in the same location
if(nns[stepper[i+j+1]].location == loc)
{
cout << “I see ” + nns[stepper[i+j+1]].description + “.\n”;
}
else
{
cout << “I don’t see one of those here”;
}
}
//The next word is a verb and we haven’t done anything yet
else if(commander[i+j+1] == “VERB” && j == 0)
{
look(loc, rms, nns, dir);
}
}
//If there’s no more words in stepper to use and we haven’t already done something
else if((i+j+1) == static_cast<signed int>(commander.size()) && j == 0)
{
look(loc, rms, nns, dir);
}
}
if(stepper[i] == GO)
{
//Check if there’s another word in the stepper to use
if((i+j+1) < static_cast<signed int>(stepper.size()))
{
//The next word is a direction
if(commander[i+j+1] == “DIR”)
{
//Check there’s somewhere to actually go
if(rms[loc].exits_to_room[stepper[i+j+1]] != NONE)
{
loc = rms[loc].exits_to_room[stepper[i+j+1]];
look(loc, rms, nns, dir);
}
else
{
cout << “There is no exit that way.\n”;
}
}
//The next word is a noun
else if(commander[i+j+1] == “NOUN”)
{
cout << “You cannot go there.\n”;
}
//The next word is a verb and we haven’t done anything yet
else if(commander[i+j+1] == “VERB” && j == 0)
{
cout << “Go where?\n”;
}
}
//if there’s no more words in stepper to use and we haven’t already done something
else if((i+j+1) == static_cast<signed int>(commander.size()) && j == 0)
{
cout << “Go where?\n”;
}
}
//stop inner loop if the next word is a verb or you hit the end of stepper
if(i+j+1 < static_cast<signed int>(stepper.size()))
{
if(commander[i+j+1] == “VERB”)
{
break;
}
}
if(i+j+1 >= static_cast<signed int>(stepper.size()))
{
break;
}
}
}
i = i + j;
}
return true;
}
int main()
{
string userInput;
vector<string> words;
int location = CELL;
verb directions[MAX_DIRS];
set_directions(directions);
verb verbs[MAX_VERBS];
set_verbs(verbs);
room rooms[MAX_ROOMS];
set_rooms(rooms);
noun nouns[MAX_NOUNS];
set_nouns(nouns);
do
{
words.clear();
getline(cin, userInput);
section(userInput, words);
parser(location, rooms, directions, verbs, nouns, words);
}while(words[0] != “QUIT”);
}
Pingback: Text Adventure Tutorial: Multi-Routing | SeveralBytes
Pingback: Text Adventure Tutorial: Inventory Management | SeveralBytes