Saturday, 10 November 2012

Text Adventure Games C++ Part 2


So, moving on (quite literally for this post) I am picking up where I left off from my last post (entering commands), and am here am going to design a game map and move around inside it with some basic commands. Before actually diving in and trying to program the map, it is a good idea to draw out a map of my world, something like this...


It should go without saying, at this point, that anyone writing an "adventure game" should already have an engrossing "adventure story" in mind.

I will keep it simple. Exits from a room will be only north, east, south and west. I will not do any northeast or up or down type of thing here (though there is no impediment for doing so in more complex maps). First I will number my rooms, from 0 to 10 (that is, eleven rooms). I will also consider my exits to be from 0 to 3, going around clockwise from north (0). Now the problem with using numbers as identifiers is that I will need to write down what each one "equates to" on a sheet of paper and keep track of it all the time I am programming. For example;

0 is a SPORTSHOP
1 is a CASINO... and so on,

...for the rooms, and for the directions...

0 is NORTH,
1 is EAST... and so on.

This is the perfect sitaution for using enumerated types. So, first thing; at the top of my code, just after the #includes, I will declare some enumerated types globally for the directions and the rooms...

enum en_DIRS {NORTH, EAST, SOUTH, WEST};
enum en_ROOMS {SPORTSHOP, CASINO, CARPARK, LOBBY, RESTAURANT, CORRIDOR, STOREROOM, POOL, GARDEN, POND, PUMPROOM};

const int NONE = -1;
const int DIRS = 4;
const int ROOMS = 11;

I will also want an identifier for NO EXIT; check out the map, not all rooms have exits in all directions. I will use a constant integer -1, above declared as NONE. Also, for the purpose of looping through the DIRECTIONS and ROOMS, which will probably be required later on, I will set their number with another couple of constants, DIRS and ROOMS.

Now the programming will be instantly easier to understand. In pseudo code...

if direction commanded is 1 and location is 2 then
    location = 3

...is certainly more difficult to follow than...

if direction commanded is EAST and location is CARPARK then
    location = LOBBY

The Map
But that is just an example. For now, it would be convenient to make a global structure for holding information about each room. And while I am at it, a global structure for words, which will be a multipurpose structure to hold both directions and verbs (ie; basically commands). So, under those constant integers, I will code this;

struct word
{
    string word;
    int code;
};

struct room
{
    string description;
    int exits_to_room[DIRS];
};

That is a basic minimum for each of those structures. I will come back to the word structure in a minute. For now, I want to focus on the room structure. First it has a description. That is just a line of text that offers a description of the room itself, for example "car park". It will be displayed in the game for the player to know where he (or she*) is. The exits_to_room variable, on the other hand is an array, and is the size of the number of directions that the game handles (ie; four of them). Each room has four exits. It does not matter that some of these exits in the game do not exist for specific rooms (for example, you cannot go south or west from the carpark). That is what the NONE constant is for. The exit is still there, it just cannot be used in the game.

Just as a note, exits_to_room should be considered as "room interconnectivity". That is to say (normally) if room 0 exits south to room 2, then room 2 should exit north to room 0. I know deeper consideration of this can lead to ideas of all sorts of weird maps that have exits leading to unexpected rooms, but I sincerely think that it is generally NOT a good idea to deliberately confuse the player like this. The adventure should be the challenge, not the map, unless there is a very good reason why part of the map should be a challenge (for example, the player dies but the game continues with him/her in a strange Labyrinth of the Underworld, trying to get back to life. Heh! Heh! A "serving suggestion").

With that clarified, I will continue and "set" all the rooms for the game. This first requires an array of the structure room to be created in main(). So, somewhere in the beginning of the main() block, before the while() loop, I create;

room rooms[ROOMS];
set_rooms(rooms);

That second line in there is a call to a separate function that is yet to be made, and which will set my rooms characteristics (that is, its description and its exits). Here is that function, in part (see the full listing at the end of this post for the complete version).

void set_rooms(room *rms)
{
    rms[SPORTSHOP].description.assign("sports shop");
    rms[SPORTSHOP].exits_to_room[NORTH] = NONE;
    rms[SPORTSHOP].exits_to_room[EAST] = NONE;
    rms[SPORTSHOP].exits_to_room[SOUTH] = CARPARK;
    rms[SPORTSHOP].exits_to_room[WEST] = NONE;

    rms[CASINO].description.assign("bustling casino");
    rms[CASINO].exits_to_room[NORTH] = NONE;
    rms[CASINO].exits_to_room[EAST] = NONE;
    rms[CASINO].exits_to_room[SOUTH] = LOBBY;
    rms[CASINO].exits_to_room[WEST] = NONE;


    // ...and so on untill all rooms and their exits are completed.
}

Take care doing this. The structure array is passed to the function, and the number of rooms described in the function simply MUST not exceed the size of the array (const int ROOMS), or bad things will happen to the program when it is run. When adding or removing rooms to the map (say, if I want to expand the game) I must first change the value of the global ROOMS, then edit the enumerated type en_ROOMS to include (or suppress) rooms, and finally edit the rooms and their exits in this set_rooms function.

So great, I have my map now, but it would be nice to move around it, too. Here is where I will take the first step in bringing together the command interface from the previous post with what I have done so far on this post. For this, I will start to create that thing which is the heart of an adventure game, from a programming point of view. The Parser.

The Parser (Rudimentary)
So. What is the parser? It is a block of code (I am going to create it as a separate function) that takes a command entered by the player, interprets it, and then executes the command. It can be very lengthy. Efforts can be made to reduce its core size by "proceduralizing" it somewhat (other, smaller sub functions), but I will not worry about that for now. The parser must recognize "words", the structure for which has already been created above. In this case, the parser will receive either one or two words (a direction or a verb and a noun). There will be much more about the parser in future posts; this time around? Well, it stays simple. It will receive direction commands only, so that I may at least navigate my map.

A good place to start, therefore, would be setting the "words" (directions, in this case). So, inside main(), I declare the array of the structure word as directions.

word directions[DIRS];
set_directions(directions);

...and I then write the function that sets the directions...

void set_directions(word *dir)
{
    dir[NORTH].code = NORTH;
    dir[NORTH].word = "NORTH";
    dir[EAST].code = EAST;
    dir[EAST].word = "EAST";

    // ...and so on, through SOUTH and up to WEST.
}

Now that I have set the directions that I will use, I need the way for the player to actually employ them through text commands. Basically, this means that when the player types "east" and presses enter, the program will take the word and check the current room for exits, and if there is an exit to the east, it will transport the player's location to that room. For this to happen, I am first going to need a variable that actually holds the players current position. I will call it the "location" variable, which makes it instantly recognizable. It gets declared and initialized at the top of the main() function...

int location = CARPARK; // using the enumerated type identifier, of course.

Further down in the main() function, inside the while loop, below the section_command() function call, I will call the parser() function, like this...

parser(location, word_1, word_2, directions, rooms);


Now I will actually write the rudimentary parser function itself...

bool parser(int &loc, string wd1, string wd2, word *dir, room *rms)

{
    int i;
    for(i = 0; i < DIRS; i++)
    {
        if(wd1 == dir[i].word)
        {
            if(rms[loc].exits_to_room[dir[i].code] != NONE)
            {
                loc = rms[loc].exits_to_room[dir[i].code];
                cout << "I am now in a " << rms[loc].description << "." << endl;
                return true;
            }
            else
            {
                cout << "No exit that way." << endl;
                return true;
            }
        }
    }
    cout << "No valid command entered." << endl;
    return false;
}

Taken from the top. I create the parser as a bool function. At the moment that is not a very useful thing to do, but when that parser grows, returning a boolean may well be a useful method for error trapping later on. It does no harm at present.

I then declare a local integer (i) to enable me to loop through commands. In this case, I start by looping through the directions. What I am doing is causing the program to compare what the player entered (and was passed to the parser as word_1) with the data for the member word in the structure directions. If word_1 coincides with a word member in the structure, then we have a hit, and the parser then checks if there actually is an exit form the current location in that direction (it checks the exits_to_room member of the room structure). If the result is something other than NONE, then there is an exit there, and the location variable (passed referrenced so that the function can permanently modify it) is moved to the room in that direction, using the directon code. It offers a description of the new room, and breaks out of the function, returning true. If there is no exit in the entered direction, the parser simply states so, and returns. And finally, if nothing that the player entered is recognized by the parser, then it returns false, telling the player that no valid command was entered.

Though not apparent here, this method of using the word structure "code" instead of the "word" itself can considerably shorten the coding of the parser. Eventually, it will permit the use of synonyms, expanding the possibilities of what the player may enter as an acceptable text command. For example, a command like "GO" may also be expressed as "WALK", but both words will have the code GO, making the parser do the same thing for two different words that mean the same thing.

The program can now be compiled and tested. I can now move around my map, entering simple direction commands like north, or east. Hooray! I am going places!

Here is the code of what was done for this post.

Here is a sample out put of what happens when the program is run.



Next time, that parser will be expanded a little, and introduce verbs. As yet, it is only using word_1 of the entered text, and that word must be a direction for the present parser to accept it.

That is all, for this post!

3 comments:

  1. helpful i got it to work, thanks

    ReplyDelete
  2. Hi I keep getting error "stray '\240' in progress

    ReplyDelete
  3. Here is a question. I am using VS 2017 and the code i'm following along with has several errors. Not sure if i'm just messing up the syntax here, but i'm curious if it was intentional. If so....touche. Certainly giving my brain some exercise trying to fix them all.

    ReplyDelete