Today’s tutorial is quick and easy. We’re going to work on inventory management. All that means is picking up objects, dropping objects and checking our inventory to see what we have.
We’ll use the commands GET, DROP and INV, as inventory is painful to type out if you have to do it often (feedback I got from multiple sources who helped test The Willow Effect for me) and inv is much quicker anyway. The last piece of the puzzle is to have somewhere to put the items, we’ll simple create a room called POCKET to put everything
First thing we need to do is add GET, DROP and INV to the verbs enum as well as adding POCKET to the rooms enum found in properties.cpp.
enum en_ROOMS {CELL, DUNGEON, HALLWAY, KEEP, GATE, FREEDOM, POCKET, MAX_ROOMS};
enum en_VERBS {GO, LOOK, GET, DROP, INV, MAX_VERBS};
Since POCKET isn’t a physical space the player isn’t going to visit there’s no need to create a room struct for it, you can if you want to but I’m not going to here. In verbs.cpp don’t forget to add our new actions so we can use them!
vbs[LOOK].word = “LOOK”;
vbs[GET].code = GET;
vbs[GET].word = “GET”;
vbs[INV].code = INV;
vbs[INV].word = “INV”;
vbs[DROP].code = DROP;
vbs[DROP].word = “DROP”;
Back to main.cpp now and the parser to get everything working! The great thing about this is that we’ve pretty much already got our structure set up from the last tutorial covering the stepper method. So when we set up the bones of the GET function inside our parser it won’t be any surprise when it looks like this:
if(stepper[i] == GET)
{
//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)
{
}
}
Nothing new here, all we have to do is fill in the gaps. The easiest will be the last two if statement which indicate that no words were used after the command GET so we can just tell players that they didn’t tell us what to get.Easy peasy:
cout << “Get what?\n”;
If the next word is a direction we can be a bit clever and guess that maybe the player meant GO instead of GET. Remember that stepper is just full of integers so we still have to translate that into a word for the players. We do that by using the dir struct:
cout << “Did you mean GO ” + dir[stepper[i+j+1]].word + “?\n”;
That leaves us with the noun. Here is where the work is done. First thing we need to do is check that the object is capable of being carried by the player and if it isn’t, as usual we’ll let the player know. After all a text adventure is about telling the player everything that happens, and everything that doesn’t happen so they know they didn’t run into a bug or a glitch or something.
if(nns[stepper[i+j+1]].cancarry == true)
{
}
else
{
cout << “I can’t pick that up.\n”;
}
Great! Now we just need to make sure the object the player wants to pick up, is actually in the room with them. Again we can be a little clever and if they already have the object we can let the player know that too. Lastly if that object isn’t in the room we’ll tell the player.
if(nns[stepper[i+j+1]].location == loc)
{
nns[stepper[i+j+1]].location = POCKET;
cout << “I picked up ” + nns[stepper[i+j+1]].description + “.\n”;
}
else if(nns[stepper[i+j+1]].location == POCKET)
{
cout << “I already have ” + nns[stepper[i+j+1]].description + “.\n”;
}
else
{
cout << “There isn’t ” + nns[stepper[i+j+1]].description + ” here.\n”;
}
And that’s GET done already! Drop is almost exactly the same except in reverse and a little easier as we only need to check if the object is already in the pocket. Instead of changing the object location to POCKET, we’ll be chancing it to the players current location.
if(stepper[i] == DROP)
{
//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”)
{
cout << “Drop what?\n”;
cout << “Did you mean to GO ” + dir[stepper[i+j+1]].word + “?\n”;
}
//The next word is a noun
else if(commander[i+j+1] == “NOUN”)
{
if(nns[stepper[i+j+1]].location == POCKET)
{
nns[stepper[i+j+1]].location = loc;
cout << “I dropped ” + nns[stepper[i+j+1]].description + “.\n”;
}
else
{
cout << “I can’t drop something I don’t have.\n”;
}
}
//The next word is a verb and we haven’t done anything yet
else if(commander[i+j+1] == “VERB” && j == 0)
{
cout << “Drop what?\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 << “Drop what?\n”;
}
}
So we can pick stuff up (useful) and drop stuff (not really that useful, or is it! You could design a puzzle that requires the player to drop an item) but now we need a way for players to know what they have in their proverbial (and virtual) pockets. We’ll make it a function as we’ll need to call it more than once. The only argument we’ll need for this function is the noun nns. Inside the function we’ll loop through all the nouns and see if there are any in the players pocket, and tell them what they are. And also tell the player if we have nothing at all! We end up with:
bool inventory(noun *nns)
{
bool emptyPockets = true;
for(int i = 0; i < MAX_NOUNS; i++)
{
if(nns[i].location == POCKET)
{
cout << “I have a ” + nns[i].word + “.\n”;
emptyPockets = false;
}
}
if(emptyPockets)
{
cout << “I’m not carrying anything.\n”;
}
return true;
}
When the player uses INV they won’t need a second word. But again we can be a little clever. If they pick a direction we can as we have previously tell the player they may have missed a command. If they use a noun we can also check if the player has that object in their pocket and let them know one way or the other and also if there’s said item in the room as they may have meant to pick it up but forgot to. This is a great little feature as it tells the player that item is still in the room instead of doing nothing where the player might assume the game has a bug or a glitch and it hadn’t picked up an item they thought they did. The last two if statements will be if INV is used exactly as intended and we can just call the function and not worry about it.
if(stepper[i] == INV)
{
//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”)
{
inventory(nns);
cout << “Did you mean GO ” + dir[stepper[i+j+1]].word + “?\n”;
}
//The next word is a noun
else if(commander[i+j+1] == “NOUN”)
{
if(nns[stepper[i+j+1]].location == POCKET)
{
cout << “I have a ” + nns[stepper[i+j+1]].word + ” in my inventory.\n”;
}
else if(nns[stepper[i+j+1]].location == loc)
{
cout << “I don’t have a ” + nns[stepper[i+j+1]].word + ” but there’s one in the room.\n”;
}
else
{
cout << “I don’t have a ” + nns[stepper[i+j+1]].word + “.\n”;
}
}
//The next word is a verb and we haven’t done anything yet
else if(commander[i+j+1] == “VERB” && j == 0)
{
inventory(nns);
}
}
//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)
{
inventory(nns);
}
}
Things are starting to shape up for us now. The player can now pick up objects, drop them and check what objects they have in their inventory. These are core mechanics in a huge amount of games and in just about every text adventure. They give the player agency in the game world and will help draw them in.
Even seemingly useless functions like drop can help with player immersion as even though it doesn’t seem to do much, the player would, as with our world, expect to be able to drop objects they’ve collected.
As always, if you have 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
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 inventory(noun *nns)
{
bool emptyPockets = true;
for(int i = 0; i < MAX_NOUNS; i++)
{
if(nns[i].location == POCKET)
{
cout << “I have a ” + nns[i].word + “.\n”;
emptyPockets = false;
}
}
if(emptyPockets)
{
cout << “I’m not carrying anything.\n”;
}
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”;
}
}
if(stepper[i] == GET)
{
//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”)
{
cout << “Did you mean GO ” + dir[stepper[i+j+1]].word + “?\n”;
}
//The next word is a noun
else if(commander[i+j+1] == “NOUN”)
{
if(nns[stepper[i+j+1]].cancarry == true)
{
if(nns[stepper[i+j+1]].location == loc)
{
nns[stepper[i+j+1]].location = POCKET;
cout << “I picked up ” + nns[stepper[i+j+1]].description + “.\n”;
}
else if(nns[stepper[i+j+1]].location == POCKET)
{
cout << “I already have ” + nns[stepper[i+j+1]].description + “.\n”;
}
else
{
cout << “There isn’t ” + nns[stepper[i+j+1]].description + ” here.\n”;
}
}
else
{
cout << “I can’t pick that up.\n”;
}
}
//The next word is a verb and we haven’t done anything yet
else if(commander[i+j+1] == “VERB” && j == 0)
{
cout << “Get what?\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 << “Get what?\n”;
}
}
if(stepper[i] == DROP)
{
//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”)
{
cout << “Drop what?\n”;
cout << “Did you mean to GO ” + dir[stepper[i+j+1]].word + “?\n”;
}
//The next word is a noun
else if(commander[i+j+1] == “NOUN”)
{
if(nns[stepper[i+j+1]].location == POCKET)
{
nns[stepper[i+j+1]].location = loc;
cout << “I dropped ” + nns[stepper[i+j+1]].description + “.\n”;
}
else
{
cout << “I can’t drop something I don’t have.\n”;
}
}
//The next word is a verb and we haven’t done anything yet
else if(commander[i+j+1] == “VERB” && j == 0)
{
cout << “Drop what?\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 << “Drop what?\n”;
}
}
if(stepper[i] == INV)
{
//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”)
{
inventory(nns);
cout << “Did you mean GO ” + dir[stepper[i+j+1]].word + “?\n”;
}
//The next word is a noun
else if(commander[i+j+1] == “NOUN”)
{
if(nns[stepper[i+j+1]].location == POCKET)
{
cout << “I have a ” + nns[stepper[i+j+1]].word + ” in my inventory.\n”;
}
else if(nns[stepper[i+j+1]].location == loc)
{
cout << “I don’t have a ” + nns[stepper[i+j+1]].word + ” but there’s one in the room.\n”;
}
else
{
cout << “I don’t have a ” + nns[stepper[i+j+1]].word + “.\n”;
}
}
//The next word is a verb and we haven’t done anything yet
else if(commander[i+j+1] == “VERB” && j == 0)
{
inventory(nns);
}
}
//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)
{
inventory(nns);
}
}
//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”);
}
properties.cpp
#ifndef __PROPERTIES_H_INCLUDED___
#define __PROPERTIES_H_INCLUDED___
#include <string>
#include <vector>
using namespace std;
enum en_DIRS {NORTH, SOUTH, EAST, WEST, MAX_DIRS};
enum en_ROOMS {CELL, DUNGEON, HALLWAY, KEEP, GATE, FREEDOM, POCKET, MAX_ROOMS};
enum en_VERBS {GO, LOOK, GET, DROP, INV, MAX_VERBS};
enum en_NOUNS {STICK, STRING, WIRE, KEY, MAX_NOUNS};
const int NONE = -1;
struct verb
{
string word;
vector<string> synonyms;
int code;
};
struct noun
{
string word;
string description;
string on_examination;
int code;
int location;
bool can_carry;
};
struct room
{
string description;
int exits_to_room[MAX_DIRS];
};
#endif //__PROPERTIES_H_INCLUDED___
verbs.cpp
#ifndef __VERBS_H_INCLUDED__
#define __VERBS_H_INCLUDED__
#include “properties.cpp”
inline void set_directions(verb *dir)
{
dir[NORTH].code = NORTH;
dir[NORTH].word = “NORTH”;
dir[SOUTH].code = SOUTH;
dir[SOUTH].word = “SOUTH”;
dir[EAST].code = EAST;
dir[EAST].word = “EAST”;
dir[WEST].code = WEST;
dir[WEST].word = “WEST”;
}
inline void set_verbs(verb *vbs)
{
vbs[GO].code = GO;
vbs[GO].word = “GO”;
vbs[LOOK].code = LOOK;
vbs[LOOK].word = “LOOK”;
vbs[GET].code = GET;
vbs[GET].word = “GET”;
vbs[INV].code = INV;
vbs[INV].word = “INV”;
vbs[DROP].code = DROP;
vbs[DROP].word = “DROP”;
}
#endif // __VERBS_H_INCLUDED__