So in the last tutorial we set up some structures. Now it’s time to use them. In today’s lesson we’re going to set up a simple parser, the tool that does all the decision making, to look at our environment, and move around in it.
Firstly we’re going to make a few refinements. The first one is to get rid of the for loop inside the gameloop that tests that we’re getting input from the player. We know it works so we don’t need to test it further. Feel free to comment it out or delete it entirely. I deleted it myself as we won’t be needing it any further and if I really do need it for debugging I can write it again quickly enough and it makes the code a bit cleaner.
So far we’ve been getting a few warnings telling us that we’ve been making comparisons between signed and unsigned ints. In our case it’s not really a big deal but why don’t we go ahead fix it anyway. It’s as simple as using the static_cast function in our loops and our if statements, which is where the warnings are arising. So the for statement will look like this now
for(int i = 0; i <= static_cast<signed int>(userInput.length()-1); i++)
Do this for all the places you have a comparison between and integer (like i or j) and a function like userInput.Length or words.size(). If your unsure your compiler should tell you what lines these warnings are being generated from.
The last refinement is how the section function handles empty strings. It’s a simple enough refinement. What we’re going to do, is have section do it’s thing if the string isn’t empty, but if it is empty, we’ll give it something to do of our own. In this case we’ll use the .empty() function on the user input. This function returns a boolean, true if (in this case) the string is empty, and false if it contains anything at all. So wrap your two for loops in an if statement, and we’ll use a handy shortcut for comparing bools. Putting ! in front of a bool is the same as saying != true in a comparison. So we have
if(!userInput.empty())
{
…
If the string contains anything, .empty() will return false, so by putting ! in front that makes our comparison true, therefore the if statement will run the code inside. Simply put, we’re saying if userInput is not empty. For good measure lets put in an else statement at the end of this to catch the string if it is empty and put something in there ourselves, so if the user accidentally hits return without entering anything, the program knows what to do, in this case we’ll have it enter the verb look, but don’t forget, we need to do it in upper case so the parser we’re about to write will recognise it. Add the code:
else
{
words.push_back(“LOOK”);
}
And we’re now done with refinements. Not the most interesting of work perhaps but they make the code more robust and will give us more options later if we want to change or add things.
It’s time to start working on the parser. For this we’re going to need a few variables for the parser to play with. Funnily enough we already have all these to hand. We’ll need the player’s location and access to all our words, verbs, directions and nouns. We might not have any nouns right now, but later that’s going to change so we might as well include it now. In our game loop, that’s the do loop we have in our main function we’ll add the following
parser(location, rooms, directions, verbs, words);
Nice and simple. And without that for loop our main function is looking pretty clean, so it’s easy to tell what’s going on, making it easier to hunt for bugs later if we have any. Now on to the meat of today’s tutorial and a huge part of the engine itself, the parser. So we’ll set up our parser function next.
We’re going to use pointers and deference operators so that we can access and more importantly manipulate data that is going to be outside the parser function itself. This is important as it means that any changes we make from the parser are maintained once the parser has done it’s thing.
We’ve used the deference operators before, the * in front of the arguments in the various structures we’ve set up. We’ll use them again so that they’ll all be pointing at the same information. The pointer works in a similar way but a little more directly and uses the & in front of the arguments, we used this in the section command. This is much better explained in the documentation, and is well worth a read.
The only pointer we’ll need is for the location as the variable is only once removed as it was initiated in the main function. For the structures we’ll use the deference operator as they are further removed, outside the main function. We don’t need to use a pointer for the words vector as we only need to access it, we don’t need to change it in any way. So we end up with this
bool parser (int &loc, room *rms, verb *dir, verb *vbs, vector<string> words)
{
…
}
Great, now we have a load of useful information coming into our parser that we can edit and use as we like. The first thing we’re going to want to do is go through the words we have and see if they’re any use to us. Since all we’re looking at today is verbs and movement we just need two more vectors, one to store the verbs and one to store the directions, I’m going to call them
vector<int> verbAction;
vector<int> route;
With that done it’s time to check our words, all we’re going to do here is go through the words one by one and see if they match any of the verbs we’ve defined in the verb structures. In this case it’ll be GO and LOOK. So it’s a simple double loop. Remember we gave each verb two properties, the code and the word, the word is what we’ll use to compare, and the code is what we’ll assign to the vector. Here’s what it looks like
for(int i = 0; i < static_cast<signed int>(words.size()); i++)
{
for(int j = 0; j < MAX_VERBS; j++)
{
if(words[i] == vbs[j].word)
{
verbAction.push_back(vbs[j].code);
}
}
Again our MAX_ comes in handy. Always good to look ahead when you can. And we’re going to quite simply do the same for the directions, so when we use the verb GO we’ll know where to!
for(int i = 0; i < static_cast<signed int>(words.size()); i++)
{
for(int j = 0; j < MAX_DIRS; j++)
{
if(words[i] == dir[j].word)
{
route.push_back(dir[j].code);
}
}
}
That’s all well and good of course, but what if the player entered a typo or something we haven’t accounted for, or just garbage, we’ll have to catch that using an escape. Well use the .empty() function again, but since we’re checking if it IS empty, we don’t need the ! symbol. And since we won’t be doing anything else with the parser if there’s nothing in the words that we can use we’ll return true.
if(verbAction.empty() && route.empty())
{
cout << “That didn’t make any sense.\n”;
return true;
}
The \n is a command that will add in a carriage return, so that the cursor will move down to the next line making things a little easier on the player.
Since we’ve used a vector for the verbs and directions we could queue up a load of commands but that’s just future proofing. For the purposes of this tutorial we’re going to assume that we’re getting one verb and one command, in this case the direction. We’ll tackle the easier of the two verbs first, LOOK. To make life a little easier on ourselves later as well we’ll write it as a function too. Inside the parser we’ll check our verb to see if it’s LOOK and then we’ll call the function. To know where to look we’ll need to variables of course, our location and the rooms structures.
if(verbAction[0] == LOOK)
{
look(loc, rms);
}
return true;
The return true is added to the end of the parser function as it’s a bool function and has to return something of course! In the look function we’ll quite simply print the description of the room at the players location
bool look(int loc, room *rms)
{
cout << “I’m in a ” + rms[loc].description + “.\n”;
return true;
}
If you want you can compile and run. type in look and it will tell you what the cell looks like, brilliant!
Moving around isn’t particularly difficult, but does require a little more thought. All it requires is to check that there is in fact somewhere for the player to go and then change the location variable to reflect that. If there’s nowhere to go, we tell the player, and if there is somewhere to go, we move the player and then tell them where they are! Above the final return true in the parser add
if(verbAction[0] == GO)
{
if(rms[loc].exits_to_room[route[0]] == NONE)
{
cout << “There is no exit that way.\n”;
}
if(rms[loc].exits_to_room[route[0]] != NONE)
{
loc = rms[loc].exits_to_room[route[0]];
look(loc, rms);
return true;
}
}
See, writing LOOK as a function saved us a bit of work. Since when we refine look later, it’ll be reflected in GO automatically without having to copy and paste code.
Now our little engine is really starting to look like something. You’ve got some structures built and you’ve got interactions going with the player. Well done, things are moving along nicely!
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 how the main.cpp looks with all the changes/updates:
#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)
{
cout << “I’m in a ” + rms[loc].description + “.\n”;
return true;
}
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;
//check for verb
for(int i = 0; i < static_cast<signed int>(words.size()); i++)
{
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 i = 0; i < static_cast<signed int>(words.size()); i++)
{
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())
{
cout << “That didn’t make any sense.\n”;
return true;
}
if(verbAction[0] == LOOK)
{
look(loc, rms);
}
if(verbAction[0] == GO)
{
if(rms[loc].exits_to_room[route[0]] == NONE)
{
cout << “There is no exit that way.\n”;
}
if(rms[loc].exits_to_room[route[0]] != NONE)
{
loc = rms[loc].exits_to_room[route[0]];
look(loc, rms);
}
}
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);
do
{
words.clear();
getline(cin, userInput);
section(userInput, words);
parser(location, rooms, directions, verbs, words);
}while(words[0] != “QUIT”);
}
Pingback: Text Adventure Tutorial: Multi-Routing | SeveralBytes