Sunday 4 November 2012

Text Adventure Games C++ Part 1


So, it has been a while again. Oh! How the demands of my job keep me from my beloved hobby!

In the continuing exploration of C++, though short of time lately to really get into more intricate subjects, I decided to revert to something simpler and, surprisingly, never tried before by yours truly. Text adventures. Why this is surprising is two-fold. One: The first ever game I played (on someone's Commodore Vic 20, lent to me for one afternoon sometime back in 1981 or so), and indeed my first ever experience of computers, was a text adventure (and it was to be my only experience of computers until my own Spectrum in 1986) . Two: It turns out to be quite easy to write a simple console style text adventure with the power of C++. Everyone should try it; it is fun!

Warning: I decided to tackle the challenge completely unprimed; that is to say, without reference to any books or tutorials about the subject, in order that it be a test of my "problem solving abilities". Therefore, the results on this post may not be "standard", if such a thing exists, for text adventures.

First Steps:
As far as I was able to deduce, text adventures are initially about two things, from a programming point of view. First, handling input text and second, moving around a map. Things like getting, dropping and using objects, and the occurrence of events in the adventure, will come later.

Though I can see now that handling text strings could have been a little bit of a chore previously (in the days when text adventures were first popular), with the tools available in C++, it is pretty much a simple task. The idea goes something like this;

1. The player inputs a line of instructions.
2. The program divides the line into individual words.
3. The program interprets the words and checks if the instruction makes any sense.
4. The program executes the command, if it makes sense, or by default informs the player that the command made no sense.
5. The program loops back to asking for a command to be input by the player.

The first item is very easy, using C++'s standard iostream and string.

#include "iostream"
#include "string"

using namespace std;

int main()
{
    string command;
   
    while(1 == 1) // Temporary condition for now...
    {
        command.clear();
        cout << "What shall I do? ";
        getline(cin, command);
        cout << "Your raw command was " << command << endl;
    }
    return 0;
}

I do not recommend compiling and running this fragment because of the infinite loop in the while statement, but this is as simple as simple gets for inputting a command. Now there are a few considerations, as I am about to move into the second item of the list; dividing the line into separate words. The first consideration is; how many words of the command do I want to be actually interpretted? For this example I want to keep it as simple as possible. By that I mean that I only want to interpret one or two words of the command, which restricts the player to either inputting a direction or a verb and a noun. For example, the player can input at the comand prompt...

What shall I do? north

or...

What shall I do? get keys

So, if the player inputs something like...

What shall I do? go get a club and hit yourself over the head with it

...will result in the program only interpretting the words "go" and "get", which will be an invalid command input. Straight off the bat we can see that some ground rules are established. With that in mind, I will continue to expand the above snippet. I need to make a function that will section the raw command line into one or two words. So, first the expansion of main()...

#include "iostream"
#include "string"
#include "vector" // For the command handling function.
#include "cctype" // Will be used to eliminate case sensitivity problems.

using namespace std;

int main()
{
    string command;
    string word_1;
    string word_2;
   
    while(word_1 != "QUIT") 
// I have provided an escape condition from the loop here
    {
        command.clear();
        cout << "What shall I do? ";
        getline(cin, command);
        cout << "Your raw command was " << command << endl;
        word_1.clear();
        word_2.clear();
        // Call the function that handles the command line format.
        section_command(command, word_1, word_2);
       
        // For test purposes, output the command after formatting by the function.
        cout << word_1 << " " << word_2 << endl;
       
    }
    return 0;
}

I have added vector and cctype headers, and two more strings (word_1 and word_2). These strings will hold the two words I want from the sectioned command line, for later parsing. In the while loop I also call a function (section_command) and pass it three arguments (command, word_1 and word_2). Here is that function, which does as its name implies; it sections the command.

void section_command(string Cmd, string &wd1, string &wd2)
{
    string sub_str;
    vector words;
    char search = ' ';
    size_t i, j;
    // Split Command into vector
    for(i = 0; i < Cmd.size(); i++)
    {
        if(Cmd.at(i) != search)
        {
            sub_str.insert(sub_str.end(), Cmd.at(i));
        }
        if(i == Cmd.size() - 1)
        {
            words.push_back(sub_str);
            sub_str.clear();
        }
        if(Cmd.at(i) == search)
        {
            words.push_back(sub_str);
            sub_str.clear();
        }
    }
    // Clear out any blanks
    // I work backwords through the vectors here as a cheat not to invalidate the iterator
    for(i = words.size() - 1; i > 0; i--)
    {
        if(words.at(i) == "")
        {
            words.erase(words.begin() + i);
        }
    }
    // Make words upper case
    // Right here is where the functions from cctype are used
    for(i = 0; i < words.size(); i++)
    {
        for(j = 0; j < words.at(i).size(); j++)
        {
            if(islower(words.at(i).at(j)))
            {
                words.at(i).at(j) = toupper(words.at(i).at(j));
            }
        }
    }
    // Very simple. For the moment I only want the first to words at most (verb / noun).
    if(words.size() == 0)
    {
        cout << "No command given" << endl;
    }
    if(words.size() == 1)
    {
        wd1 = words.at(0);
    }
    if(words.size() == 2)
    {
        wd1 = words.at(0);
        wd2 = words.at(1);
    }
    if(words.size() > 2)
    {
        cout << "Command too long. Only type one or two words (direction or verb and noun)" << endl;
    }
}

This function uses both vector and cctype functions. The cctype function toupper() is used to turn any lower case letters in the command line into upper case. This process is to simplify the internal workings of the program. All text commands in the program will be interpreted in upper case, but the player is still free to input lower case.

I want to go through that function bit by bit. I am going to use a space character to split the words of the command line, as it can be (reasonably) safe to assume that the command line will be input with spaces between words. A local substring will be used to "collect" the letters of each word in the command line, while no space occurs, and when a space occurs, it will push the substring (containing the word) into the local string vector. The substring is cleared, and the process is repeated for the next series of letters, either up to the next space, or to the end of the command line string, whichever happens first. By the end of the process, I will have a local string vector loaded with the individual words of the command line.

That said, there is at least one possible and plausible problem. If the player, by typo mistake, entered more than one space between words, the above method will "collect" and pushback a blank word into the vector. In a rudimentary effort at error trapping, I will endevour to eliminate the occurence of these blank words. I search through the vector strings, from end to beginning (ie; backwards) looking for such blank words. If one is found, it is erased with an iterator reference from the beginning of the vector (note, I do not actually use an iterator, just the reference that is normally used to set an iterator). Yes, a bit crude, as there are other ways to do this, but doing it this way exempts me from having to reinitialize the vector iterator every time the vector changes size because a blank word was eliminated.

Next, the remaining words in the vector need to be converted to upper case (as explained why above, and will become clearer later). The loop goes through each character in each vector string looking for the occurence of lower case characters and converting them to upper case.

Finally, the first two words of the string vector only are returned to the main loop by the function (wd1 and wd2). The rest of what was written on the command line is, basically, wasted time by the player. So, why do I go through the trouble of "collecting" all the words in the command line? Future expansion! One day, I might want to create a text adventure that accepts more complex commands, like for example "open door using crowbar", instead of just "use crowbar". The section command function is already up to meeting that challenge as it is now.

So, the program may be compiled and run now. I will have a console application that prompts me for a command and, after entering it, shows me the first two words of that command in upper case. Not very exciting, as yet, but more is coming on this somewhat archaic but none-the-less fun subject.

Click here for some code of what has been done on this post.

Here are some references to the headers used in this post...
cctype
vector
string
iostream
more on vector

And here is a sample output of the program so far...


Next time, navigating a game world map, where the enum types really come into their own!

That's all, for now!


6 comments:

  1. When I compile this in Visual Studio 2012 it gives me errors C2208: 'word' : no members defined using this type, referencing the word structure. Is there anyway around this or should I just switch compilers?

    ReplyDelete
    Replies
    1. I don't know if he is using a different compiler or not but where he wrote " vector words;" it should be written as "vector words;" that should get rid of some of the errors.
      Hope this helps!

      Delete
    2. Just applied that small fix and everything works just fine!

      Delete
  2. Thank you for your very helpful tutorial. I'm making progress and taking names!

    ReplyDelete
  3. what if there is a stop word between two words commands, how to remove that?
    for example, get the key.

    ReplyDelete
    Replies
    1. That is an article. There are only three, really "the", "a" and "an". You can make a list of them, and have your parser filter them out. So, if you type "get the key", the parser will recognize "the", discard it, and only see "get key". It is a good idea to do this, actually!

      Delete