[Edit: Sorry about the formatting, still working on/learning WordPress!]
I had originally planned to look at nouns for this weeks tutorial but I’ve been thinking about multi command sentences a lot, and considering it would be easier to implement now when the parser is quite small rather than at the end when it’s full of all kinds of commands, it makes sense to tackle it this week. Don’t worry, nouns are coming soon!
A little bit of house keeping, I made a mistake in the last entry of the tutorial, parsing thoughts (which I’ve now edited to fix), which was causing havoc in my multi command parsing code (I wish there was an easier way to say that). In the for loops where you check verbs and routes you need to change the condition from ‘j <= MAX_’ to just ‘j < MAX_’ so you have these:
for(int j = 0; j < MAX_VERBS; j++)
{…
for(int j = 0; j < MAX_DIRS; j++)
{…
I also refined it slightly to streamline the code, instead of keeping the j loops in separate i loops, since the i loops are identical I put them both in the same i loop which looks like:
for(int i = 0; i < static_cast<signed int>(words.size()); i++)
{
//check for verb
for(int j = 0; j < MAX_VERBS; j++)
{
if(words[i] == vbs[j].word)
{
verbAction.push_back(vbs[j].code);
}
}
//check for route command
for(int j = 0; j < MAX_DIRS; j++)
{
if(words[i] == dir[j].word)
{
route.push_back(dir[j].code);
}
}
}
In the last bit of housekeeping for today I noticed that when you type quit to leave the game, the parser quit rightly doesn’t recognise the word as a verb or direction and the escape gives outputs “That didn’t make any sense.” which in itself is kinda wrong. All you need to do is add a little conditional to over that, add && words[0] != “QUIT” and now the escape won’t activate if the player is leaving the game.
EDIT The below code is no longer in use and has been replaced in the post: Proper Multi-Parsing. Please go there for an effective multi command system
Everything we do today is going to be inside the parser, and the first thing we’re going to need is of course a few new variables. Because we’re going to need to track the indexes of verbAction and route separately we’ll need one each for those and another to help define them. We’ll also need a bool to tell when we’re finished looping through all the verbActions and routes. So in the parser below where we’ve initiated our vectors add
int v = 0; //verbAction index
int r = 0; //route index
int k = 0; //index setter
bool done = false;
What we’re going to do is wrap out commands ‘GO’ and ‘LOOK’ in a loop so we can execute them as many times as necessary. We don’t need to include the loops where we fill up VerbAction and route with commands as it only needs to be done once, and the same goes for the escape. We’ll keeping looping until we’re done, so the conditional will look like this:
while(!done)
{…
We’re going to be using k as out index setter, so that’s the only index that’s going to be incremented each time we loop. We also have to set the done bool to true so we can stop the loop when we need to. We’ll do that by checking when k is greater than the number of entries of both verbAction and route. Simple really, and don’t forget to static cast to avoid warnings! Add the code to the end of the loop
k++;
if(k >= static_cast<signed int>(verbAction.size()) && k >= static_cast<signed int>(route.size()))
{
done = true;
}
And before we get into how we’re going to set the indexes we’ll need to make sure they’re being used, so anywhere you’ve got something like verbAction[0] or route[0] we’ll need to replace the with verbAction[v] and route[r].
Now we’re ready so set those indexes.There are a few situations we’re going to have to handle, when both verbAction and route have no entries, when they have the same number of entries (easiest to handle) and when on has more than the other (slightly more complicated but not too much). First two are easy and look like this:
if(verbAction.size() == 0 && route.size() == 0)
{
break;
}
if(verbAction.size() == route.size())
{
v = k;
r = k;
}
It seems like it might make sense to put in done = true; instead of break; but if we do that the rest of the loop tries to execute and when the commands try to access and empty vector the program crashes, so instead we use break; to stop the loop in its tracks. In the second part where both vectors have the same number of entries we can just set both of their indexes using the index setter, then loop through them as necessary.
But we could do a little better with the first part. The player might input a route but not a verb so lets account for that:
if(verbAction.size() == 0)
{
if(route.size() == 0)
{
break;
}
else
{
cout << “No actions given.\n”;
break;
}
}
if(verbAction.size() == route.size())
{
v = k;
r = k;
}
That leaves us with just one more problem area in regards to zero vector entries, and that’s when we do have a verbAction and don’t have a route. We don’t want to account for that here as we have commands that don’t require a route (or in the future a noun) such as ‘LOOK’, so using break; to cover that here would be counter productive. We’ll have to take account for that from within the commands themselves. The only one we need look at is ‘GO’, in the future we’ll build in these escapes as we go.
What we want from ‘GO’ is that when there is no route, it doesn’t try to access the route vector as this would cause a crash, so if there is no route we simply break out of the loop
if(verbAction[v] == GO)
{
if(route.size() > 0)
{
if(rms[loc].exits_to_room[route[r]] == NONE)
{
cout << “There is no exit that way.\n”;
}
if(rms[loc].exits_to_room[route[r]] != NONE)
{
loc = rms[loc].exits_to_room[route[r]];
look(loc, rms);
}
}
else
{
cout << “Go where?\n”;
break;
}
}
Where one vector has more entries than the other is where things gets a little bit more complicated for setting the indexes. You can only increase the indexes for both for as long as there are entries in both, when you reach the end of the smaller vector we’re going to stop increasing that index and continue to increase the index of the other vector until we reach the end of that, and the loop finishes. That’s why we made sure to check both vectors against the index setter instead of just one at the end. So we get:
if(verbAction.size() > route.size())
{
if(k >= static_cast<signed int>(route.size()))
{
v = k;
r = route.size()-1;
}
else if(k < static_cast<signed int>(route.size()))
{
v = k;
r = k;
}
}
if(verbAction.size() < route.size())
{
if(k >= static_cast<signed int>(verbAction.size()))
{
v = verbAction.size()-1;
r = k;
}
else if(k < static_cast<signed int>(verbAction.size()))
{
v = k;
r = k;
}
}
As you can see both of these are essentially a mirror of the other so you really only have to do the work once. You can see that once while there are entries in both vectors we keep using the index setter on both but when we reach the end of the smaller vector we just keep assigning it it’s own size -1 (as indexes start from 0 not 1) but continue to assign the index setter to the index of the bigger vector.
That’s it for today, it may not have seem like we’ve made a whole lot of progress as no real game content has gone in but think about how much more solid the engine is now. You can input multiple commands, and input multiple arguments to single commands, give it a try, type in go south west and watch as you now take two movement for one typed command. And now that it’s in place we shouldn’t have to worry to much about it as we go forward beyond the odd but if refinement and housekeeping. If someone knew your game inside out, the could potentially finish it in one (long) input. How cool is that? (Very).
Next time we’re going to look at adding nouns to the game, which add the content you need to add puzzles and story to your game and make the environments more real.
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:
bool parser (int &loc, room *rms, verb *dir, verb *vbs, vector<string> words)
{
//By using a vector we can have multiple actions in one input
vector<int> verbAction;
vector<int> route;
int v = 0; //verbAction index
int r = 0; //route index
int k = 0; //index setter
bool done = false;
for(int i = 0; i < static_cast<signed int>(words.size()); i++)
{
//check for verb
for(int j = 0; j < MAX_VERBS; j++)
{
if(words[i] == vbs[j].word)
{
verbAction.push_back(vbs[j].code);
}
}
//check for route command
for(int j = 0; j < MAX_DIRS; j++)
{
if(words[i] == dir[j].word)
{
route.push_back(dir[j].code);
}
}
}
if(verbAction.empty() && route.empty() && words[0] != “QUIT”)
{
cout << “That didn’t make any sense.\n”;
return true;
}
while(!done)
{
if(verbAction.size() == 0)
{
if(route.size() == 0)
{
break;
}
else
{
cout << “No actions given.\n”;
break;
}
}
if(verbAction.size() == route.size())
{
v = k;
r = k;
}
if(verbAction.size() > route.size())
{
if(k >= static_cast<signed int>(route.size()))
{
v = k;
r = route.size()-1;
}
else if(k < static_cast<signed int>(route.size()))
{
v = k;
r = k;
}
}
if(verbAction.size() < route.size())
{
if(k >= static_cast<signed int>(verbAction.size()))
{
v = verbAction.size()-1;
r = k;
}
else if(k < static_cast<signed int>(verbAction.size()))
{
v = k;
r = k;
}
}
if(verbAction[v] == LOOK)
{
look(loc, rms);
}
if(verbAction[v] == GO)
{
if(route.size() > 0)
{
if(rms[loc].exits_to_room[route[r]] == NONE)
{
cout << “There is no exit that way.\n”;
}
if(rms[loc].exits_to_room[route[r]] != NONE)
{
loc = rms[loc].exits_to_room[route[r]];
look(loc, rms);
}
}
else
{
cout << “Go where?\n”;
break;
}
}
k++;
if(k >= static_cast<signed int>(verbAction.size()) && k >= static_cast<signed int>(route.size()))
{
done = true;
}
}
return true;
}
Pingback: Text Adventure Tutorial: Proper Multi-Parsing | SeveralBytes