Saturday, 12 January 2013

Text Adventure Games C++ Part 4

Hello again!

I finally get back to my C++ Programming blog, to continue a little with this fascinating business of making my own demo adventure game. This time I am going to continue a little with interacting with some NOUNS using some VERBS. Previously, I implemented the usage of a simple verb (LOOK) in the parser. In this installment I will endeavor to OPEN a DOOR. Now, coding these adventure games is a good old fashioned programming exercise in recognizing conditions and executing the program based on the results of those conditions. Which program isn't? However, here the program is so basic that there is little need to resort to Classes and other, newer programming techniques. The one exception I have made, to make life easier with my limited time availability, is the use of a couple of STL features. Doubtless, if one wants to make a living (lol) off writing text adventure games, then an effective API for it is well within reach of being written in C++. Right here, I am just going to "plod on" the old fashioned way.

To business, verbs and nouns...
Nouns are objects. When using nouns in a text adventure game, they simply MUST be preceded by a verb. However a verb does not necessarily need to be followed by a noun, as was proved in the last post. LOOK, for example, can be a perfectly valid command when used on its own. But when you want to open a door, it is only logical that the command be "OPEN DOOR" and not just "OPEN", in order to avoid ambiguity. For this to happen, the parser now has to handle both the words thrown out by the command line interpreter. So, that is the first objective. To simplify reading the changes in the code, I have included comments to identify the modifications that I have made to the original listing.

Here are the objectives for this post...

1. Implement the NOUN trap and acheive the objective of opening the door.
2. Making the door visible and interactive in two different rooms (modification to the LOOK command, which will make the other nouns visible at their locations, too).
3. Making sure the door does what it is supposed to do, as in stopping access to the adjoining room when it is closed.

So, first off, some enumerated types again to identify my nouns, and a constant integer for the number of NOUNS. At the top of the code...

enum en_NOUNS {STORE_DOOR, MAGNET, METER, ROULETTE, MONEY, FISHROD};
const int NOUNS = 6;

Now, create a structure for nouns. Nouns are going to be a little different from verbs and directions, so I cannot really reuse the previous word structure. For a start, because nouns are objects, they will need a location. Because they can be examined and seen, they also need a description (like rooms). Also, some objects can be carried, and others cannot (too heavy an object, for example), so a way to determine this must be implemented. Here is what I came up with...

struct noun
{
    string word;
    string description;
    int code;
    int location;
    bool can_carry;
};

Now, that is just a guide. More complex adventures (better said, adventures with more complex objects) might require some more features for their nouns inside this structure.

Then, I need the function to set values of the members of the structure...

void set_nouns(noun *nns)
{
    nns[STORE_DOOR].word = "DOOR";
    nns[STORE_DOOR].code = STORE_DOOR;
    nns[STORE_DOOR].description = "a store room door";
    nns[STORE_DOOR].can_carry = false;
    nns[STORE_DOOR].location = CORRIDOR;
    nns[MAGNET].word = "MAGNET";
    nns[MAGNET].code = MAGNET;
    nns[MAGNET].description = "a magnet";
    nns[MAGNET].can_carry = true;
    nns[MAGNET].location = NONE;
    // See complete code at the end of this post for the rest of this...

Then there is the declaration of the array of the structure and the call to that set_nouns function inside main()...

    noun nouns[NOUNS];
    set_nouns(nouns);

And finally the addition of a NOUN argument to both the parser function and the call from within main()...

// In the declaration of the parser parameters, add the noun *nns pointer.

bool parser(int &loc, string wd1, string wd2, word *dir, word *vbs, room *rms, noun *nns)
{
    // ..... parser code ....
}


// And, where the parser is called from within main(), pass the array of the noun structure...


if(word_1 != "QUIT")
{
   parser(location, word_1, word_2, directions, verbs, rooms, nouns);
}

The change is highlighted in dark orange. That should make the code compilable again, ableit without any functionality for the nouns yet. For that I will need to implement my NOUN trap, very similar to the previously used VERB trap. Almost all the work from here on is going to be done inside the parser function. So, remember this, the VERB trap?

int VERB_ACTION = NONE;


for(i = 0; i < VERBS; i++)
{
    if(wd1 == vbs[i].word)
    {
        VERB_ACTION = vbs[i].code;
        break;
    }
}

The NOUN trap is going to look like this...

int NOUN_MATCH = NONE;


if(wd2 != "")
{
    for(i = 0; i < NOUNS; i++)
    {
        if(wd2 == nns[i].word)
        {
            NOUN_MATCH = nns[i].code;
            break;
        }
    }
}


Except that here the contents of variable word_2 (here in its function passed form, wd2) is examined to see if it is empty. That is to say, the parser will always expect a verb (or a direction), even if on its own, but the inclusion of nouns is optional.

SUPPLEMENTARY NOTE:
The vbs[i].code and nns[i].code members of the verb and noun structures are a bit superfluous, at this point, for both verbs and nouns. Their value would only become apparent when using synonyms for verbs and nouns, and that only if it is done in a particular way. In future blogs I might do a demo of a couple these "synonym techniques" that I have played about with. For the moment, one could dispense completely with that "x.code" member and simply have the trap work like this, for example;

if(wd2 == nns[i].word)
{
    NOUN_MATCH = i;
    break;
}
...and I suspect it would still do the trick...

Anyway, back to the noun trap. This will be located inside the parser function, right under the verb trap. The NOUN_MATCH variable will be initialized as a value of NONE (-1). After that, if the variable word_2 is not empty, the loop will scan through its list of nns.word members. If it finds a match, it will assign the respective nns.code member value to the NOUN_MATCH variable, and break out of the for-loop. Otherwise (if it does not find a match or there was no content to word_2) the value NONE of NOUN_MATCH will remain for the rest of the parser's function.

Now, how to use that "trapped" noun? Well, this is where the "coding slog" comes in. First, a valid verb must have preceded the noun. The combination should also make sense. Whether it does or not is up to the programmer, really. For example, a condition for the command "OPEN MONEY" could be programmed, but it is not very logical. For these cases, a simple default answer from the parser, for each verb, should be implemented. Something like "That cannot be opened". With that, the parser returns to the main loop waiting for the next command.

If the condition is coded, however, a result of the action should occur. For example, if access from one room to another is barred by a door, then the command "OPEN DOOR" should then permit access to the adjoining room. That is the idea, now I will start to implement that. Here is a bit of code that opens a door. It goes after the VERB_ACTION == LOOK segment...

if(VERB_ACTION == OPEN)
{
    if(NOUN_MATCH == STORE_DOOR)
    {
        if(loc == CORRIDOR || loc == STOREROOM)
        {
            if(door_state == false)
            {
                door_state = true;
                cout << "I have opened the door." << endl;
                return true;
            }
            else if(door_state == true)
            {
                cout << "The door is already open." << endl;
                return true;
            }
        }
        else
        {
            cout << "There is no door to open here." << endl;
            return true;
        }
    }
    else
    {
        cout << "Opening that is not possible." << endl;
        return true;
    }
}

Before I say anything about this piece of code, I want to draw attention to a variable that occurs in there. It is the door_state variable. This is a "flag", and is ideally suited to being a bool type variable. The door is either open (true) or closed (false). A look at the code will make this pretty self evident, I think, if the parser response is used as a guide to what happens. This flag I have chosen to declare as a function level static bool variable. Its declaration is at the beginning of the parser code, and looks like this...

static bool door_state = false;

The section of code is not actually complete. Just setting the door_state variable to true or false does not automatically open and close the door. Some more work needs to be done elsewhere in the code. However, in effect at least, we are now able to open the door and see that change reflected in the feedback from the running program. The program could at this stage be compiled and tried again. Here is an output....


There is also one other thing to notice in that code. Doors tend to be in both the rooms that they interconnect, unless the aim is to make a game in which there is a one way door into another dimension, of course. So, the player should be able to see the door in the corridor and in the store room. The player should also be able to interact with the door from both rooms. Therefore the line in the code....

if(loc == CORRIDOR || loc == STOREROOM)


That is, "if the player's location is the corridor or the storeroom". However, the noun structure only caters for one location. So the door noun itself can only either be in the corridor or in the store room at any one time. The solution is pretty simple. When the player is in either the corridor or in the store room, change the location of the door noun to the player's current location.

This part is very easy, though it does contain something to consider regarding the LOOK command and nouns. The first thing to do is to make nouns visible to the LOOK command. This will require a modification to the LOOK function itself. It is so simple that just showing a before and after sample of the code will make it all plainly obvious...

BEFORE:
void look_around(int loc, room *rms, word *dir)
{
    int i;
    cout << "I am in a " << rms[loc].description << "." << endl;

    for(i = 0; i < DIRS; i++)
    {
        if(rms[loc].exits_to_room[i] != NONE)
        {
            cout << "There is an exit " << dir[i].word << " to a " << rms[rms[loc].exits_to_room[i]].description << "." << endl;
        }
    }
}

AFTER:
void look_around(int loc, room *rms, word *dir, noun *nns)
{

    int i;
    cout << "I am in a " << rms[loc].description << "." << endl;


    for(i = 0; i < DIRS; i++)
    {
        if(rms[loc].exits_to_room[i] != NONE)
        {
            cout << "There is an exit " << dir[i].word << " to a " << rms[rms[loc].exits_to_room[i]].description << "." << endl;
        }
    }
    for(i = 0; i < NOUNS; i++)
    {
        if(nns[i].location == loc)
        {
            cout << "I see " << nns[i].description << "." << endl;
        }
    }
}
Yeah, that is it. Add a for-loop that scans through all the nouns. If the noun happens to be at the same location as the player (nns[i].location == loc), then inform the player that it can be seen with a cout output. Note the new noun *nns argument in the parameters.

The call to the LOOK command also needs to be modified to include the new noun argument, passing it from within the parser.

    if(VERB_ACTION == LOOK)
    {
        look_around(loc, rms, dir, nns);
        return true;
    }

Now, let us look at the consideration about LOOK command and nouns: This command is going to need further modification later on when I implement the GET and DROP commands. If an object (noun) is carried, then it is obviously(?) going to be in the same room as the player, but I will not want the LOOK command to list objects that are in the player's INVENTORY. It would be tedious, seeing as the INVENTORY command is specifically going to serve that function. In fact, there are two solutions. 1. A separate, inaccessible "pseudo-room" could be created, for the sole purpose of, say, being the players "pocket", eliminating the need for the LOOK command to filter out "carried objects". Or 2. just as simple, actually do the filtering, with an INVENTORY array that contains "slots" for the objects, which the LOOK command skips over. That, therefore, is a primer of a future issue.

Finally, to wrap up this post, I want to get the door actually doing what a door should do: stopping access when it is closed and permitting it when open. The easiest way to do this is to write code that modifies the room exits depending on the door position. As the door starts in the game closed, the exits east from CORRIDOR and west from STOREROOM should be assigned a value of NONE. That is, the rooms are NOT interconnected, initially. I will therefore modify the set_rooms function.

Where they looked like this...

void set_rooms(room *rms)
{
    // .....
    rms[CORRIDOR].description.assign("corridor");
    rms[CORRIDOR].exits_to_room[NORTH] = LOBBY;
    rms[CORRIDOR].exits_to_room[EAST] = STOREROOM;
    rms[CORRIDOR].exits_to_room[SOUTH] = GARDEN;
    rms[CORRIDOR].exits_to_room[WEST] = NONE;


    rms[STOREROOM].description.assign("store room");
    rms[STOREROOM].exits_to_room[NORTH] = NONE;
    rms[STOREROOM].exits_to_room[EAST] = NONE;
    rms[STOREROOM].exits_to_room[SOUTH] = NONE;
    rms[STOREROOM].exits_to_room[WEST] = CORRIDOR;
    // .....
}

I will rewrite that to look like this...

void set_rooms(room *rms)
{
    // .....
    rms[CORRIDOR].description.assign("corridor");
    rms[CORRIDOR].exits_to_room[NORTH] = LOBBY;
    //rms[CORRIDOR].exits_to_room[EAST] = STOREROOM;
    rms[CORRIDOR].exits_to_room[EAST] = NONE;
    rms[CORRIDOR].exits_to_room[SOUTH] = GARDEN;
    rms[CORRIDOR].exits_to_room[WEST] = NONE;
    rms[STOREROOM].description.assign("store room");
    rms[STOREROOM].exits_to_room[NORTH] = NONE;
    rms[STOREROOM].exits_to_room[EAST] = NONE;
    rms[STOREROOM].exits_to_room[SOUTH] = NONE;
    //rms[STOREROOM].exits_to_room[WEST] = CORRIDOR;
    rms[STOREROOM].exits_to_room[WEST] = NONE;
    // .....
}

Now I will zip along to the parser function and zero in on the condition for the verb OPEN, specifically this part...

if(loc == CORRIDOR || loc == STOREROOM)
{
    if(door_state == false)
    {
        door_state = true;
        cout << "I have opened the door." << endl;
        return true;
    }
    else if(door_state == true)
    {
        cout << "The door is already open." << endl;
        return true;
    }
}

If the door state is changed from false to true, I will want to enable the passage between the CORRIDOR and the STOREROOM, so I will add this bit of code (just two lines), beneath the door_state = true;

if(door_state == false)
{
    door_state = true;
    rms[CORRIDOR].exits_to_room[EAST] = STOREROOM;
    rms[STOREROOM].exits_to_room[WEST] = CORRIDOR;
    cout << "I have opened the door." << endl;
    return true;
}

And the passage is enabled. When the CLOSE verb is implemented, the reverse can be done to shut the door.

It would be nice to have the LOOK command report whether the door is open or closed, incidentally. There are two ways I could do this. First, I could pass the door_state variable to the look_around function, and do a conditional cout. Something like this, in the function...

if(loc == CORRIDOR || loc == STOREROOM)
{
    if(door_state == false)
    {
        cout << "The door is closed" << endl;
    }
    else
    {
        cout << "The door is open" << endl;
    }
}

Or I could do it the cheap and nasty way, altering the nns[STORE_DOOR].description in the parser, like this...

if(door_state == false)
{
    door_state = true;
    rms[CORRIDOR].exits_to_room[EAST] = STOREROOM;
    rms[STOREROOM].exits_to_room[WEST] = CORRIDOR;
    nns[STORE_DOOR].description.clear();
    nns[STORE_DOOR].description.assign("an open store room door.");
    cout << "I have opened the door." << endl;
    return true;
}

Anyway, that is the beginning of integrating a verb/noun parser, and some interaction with the nouns. I now have a door in the game that actually works. Next time, I will play around with picking up an object and putting it in the inventory, and maybe even using it! However, this would be just about it for this post, really. The code can now be compiled and tested.

Here is the code...

And here is another sample output...



And here is a chunk of code that can be added to the parser, right after the end of OPEN command conditions, to enable the closing of the door...

if(VERB_ACTION == CLOSE)
{
    if(NOUN_MATCH == STORE_DOOR)
    {
        if(loc == CORRIDOR || loc == STOREROOM)
        {
            if(door_state == true)
            {
                door_state = false;
                rms[CORRIDOR].exits_to_room[EAST] = NONE;
                rms[STOREROOM].exits_to_room[WEST] = NONE;
                nns[STORE_DOOR].description.clear();
                nns[STORE_DOOR].description.assign("a closed store room door");
                cout << "I have closed the door." << endl;
                return true;
            }
            else if(door_state == true)
            {
                cout << "The door is already closed." << endl;
                return true;
            }
        }
        else
        {
            cout << "There is no door to close here." << endl;
            return true;
        }
    }
    else
    {
        cout << "Closing that is not possible." << endl;
        return true;
    }
}

Bye now.