C++ Text Adventure Design

Started by
8 comments, last by ICanC 6 years, 6 months ago

I've created a few text adventures in plain C, whereby every 'area' was a function with its own switch statement to get user input. Instead of traditional "commands" the user is given options e.g. "1. Go north 2. Go south 3. Examine note" etc. This resulted in great flexibitly and options within an area but serious code bloat and ugliness. I'm now trying it in C++ and really like OOP, I've built a class for characters and the areas which takes care of the displaying and world navigation aspects, but I'm now finding the whole approach really inflexible and don't know how to add all my extra content and options to each area. For instance in the test code below I've created 2 areas, once you look around in the plains area, you can see option 6. Examine paper - but how will I add this functionality in an elegant way without resorting back to my old functions for areas mess?

Main.CPP


#include "character.hpp"
#include "area.hpp"

void setupareas();

extern area* current;
 area* plains = new area;    area* town = new area; area* swamp = new area;
 area* chapel = new area;    area* mountain = new area;

void setupareas()
{
    std::vector<std::string> plainsmenu1 {"1. (Plains) Look Around\n\n", "2. Go North\n\n", "3. Go East\n\n", "4. Go South\n\n", "5. Go West\n\n\n> "};
    std::vector<std::string> plainsmenu2 {"1. (Plains) Look Around\n\n", "2. Go North\n\n", "3. Go East\n\n", "4. Go South\n\n", "5. Go West\n\n6. Examine paper\n\n\n> "};
    std::vector<std::string> chapelmenu1 {"1. (Chapel) Look Around\n\n", "2. Go North\n\n", "3. Go East\n\n", "4. Go South\n\n", "5. Go West\n\n\n> "};
    std::vector<std::string> chapelmenu2 {"1. (Chapel) Look Around\n\n", "2. Go North\n\n", "3. Go East\n\n", "4. Go South\n\n", "5. Go West\n\n6. Open door\n\n\n> "};
    plains->setdesc("You are standing in a wide plain\n\n1. Back\n\n\n> ");
    plains->setmenu1(plainsmenu1); plains->setmenu2(plainsmenu2);
    plains->setdoors(chapel, mountain, swamp, town);
    chapel->setdesc("You are outside a chapel\n\n1. Back\n\n\n> ");
    chapel->setmenu1(chapelmenu1); chapel->setmenu2(chapelmenu2);
    chapel->setdoors(NULL, NULL, plains, NULL);
}

int main(void)
{
    setupareas();
    current = plains;
    while (1) {
    current->getinput();
    }


    return 0;
}

area.HPP


class area {
public:
    area();
    ~area();
    std::vector<std::string> menu1;
    std::vector<std::string> menu2;
    std::string desc;
    short seen;
    area *north, *east, *south, *west;
    void setmenu1(std::vector<std::string>);
    void setmenu2(std::vector<std::string>);
    void setdesc(std::string);
    void display();
    void look();
    void getinput();
    void setdoors(area*,area*,area*,area*);
};

area* current;

area::area()
{
    this->seen = 0;
}

area::~area()
{

}

void area::setmenu1(std::vector<std::string> line)
{
    for (std::string s : line)
    {
        this->menu1.push_back(s);
    }
}

void area::setmenu2(std::vector<std::string> line)
{
    for (std::string s : line)
    {
        this->menu2.push_back(s);
    }
}

void area::display()
{
    if (this->seen == 0) {
    for (std::string s : this->menu1)
    {
        print(s);
    }
    }
    else {
    for (std::string s : this->menu2)
    {
        print(s);
    }
    }
}

void area::getinput()
{
    char choice;
    bool here = true;
    while (here) {
    system("cls"); std::cout << TITLE;
    this->display();
    std::cin>>choice;
    switch(choice)
    {
        case '1': this->look(); break;
        case '2': if (this->north != NULL) {current = this->north; here = false; break;} else {print("\n\nCan't go North"); Sleep(2500); break;}
        case '3': if (this->east != NULL) {current = this->east; here = false; break;} else {print("\n\nCan't go East"); Sleep(2500); break;}
        case '4': if (this->south != NULL) {current = this->south; here = false; break;} else {print("\n\nCan't go South"); Sleep(2500); break;}
        case '5': if (this->west != NULL) {current = this->west; here = false; break;} else {print("\n\nCan't go West"); Sleep(2500); break;}
    }
    }
}

void area::look()
{
    system("cls"); this->seen = 1; std::cout << TITLE;
    print(this->desc); char c; std::cin>>c;
}

void area::setdesc(std::string desc)
{
    this->desc = desc;
}

void area::setdoors(area* north, area* east, area* south, area* west)
{
    this->north = north;
    this->east = east;
    this->south = south;
    this->west = west;
}

 

Advertisement

I would recommend doing a more data driven approach, where you store your areas in files. This doesn't directly solve the problem with special functionality for each area, but it makes your code less messy.

For the actual problem at hand, you could define requirements for each option in terms of variables that need to be set. If you go for the data driven approach, you could have a map that stores the variables in code, and refer to the variables via strings in the level files. Not sure if this would work for exactly every special functionality, but it could be a good way to generalize some of it anyway.

Instead of hard-coding an action, make it an object (this fits in data-driven thinking). Each room has a list of actions that you can do.

You'll have several such lists, depending on what's around. The room itself can have one, each object in the room can have one, your inventory items can have one, a NPC can have one, etc.

 

Consider adding a Player class and assign some actions to it, and variables like HP, for fighting, and a pointer to the area the player is currently in.

Apart from learning about classes a bit more it is only logical - it is the player that does the looking and going, not the area. player.look() could call area.getDescription(). (Getters and setters are a Good Idea (tm).)

And while you're at it, don't have your areas defined as globals. Make a World class that holds the areas and the functionality to set them up nicely.

With that the way towards objects or monsters isn't far.

Thanks very much for the replies, I'm still struggling with this one. For the kind of actions I want (where they could be vastly different and load up various console displays), I feel like I need to have them as functions, somehow nicely wrapped in up an object. I've tried making an Actions class with all the functions, that take the player, current location, and int pressed as parameters (the int refers to what action number it is in a room) - or do away with the actions class and list them all in the character class (as I've tested at the bottom of the code below - obviously they won't all be just taking an item). But it doesn't seem orderly - Also, for all these objects to communicate with each other, it feels like I have to declare global pointers for the header files to see other objects I want them to be able to take parameters for in their functions?

 

What I also find challenging (probably the most), is having all the control flow from the area class switch statement which currently handles direction:

 


switch(choice)
    {
        case '1': this->look(); break;
        case '2': if (this->north != NULL) {current = this->north; here = false; break;} else {print("\n\nCan't go North"); Sleep(2500); break;}
        case '3': if (this->east != NULL) {current = this->east; here = false; break;} else {print("\n\nCan't go East"); Sleep(2500); break;}
        case '4': if (this->south != NULL) {current = this->south; here = false; break;} else {print("\n\nCan't go South"); Sleep(2500); break;}
        case '5': if (this->west != NULL) {current = this->west; here = false; break;} else {print("\n\nCan't go West"); Sleep(2500); break;}
        case '6': if (this->options >= 6) {} - somehow get action(function call for another object) by passing area, character and choice?????
    }

Character class


class character {
    private:
        short age;
        short health;
        short gold;
        std::vector<std::string> inventory;
    public:
        std::string name;
        character();
        ~character();
        void setname (std::string);
        void setage (short);
        void sethealth (short);
        void setgold (short);
        void buyitem (std::string, short);
        void sellitem (std::string, short);
        void takedamage (short);
        void display ();
        void additem (std::string);
        void removeitem (std::string);

        /* ACTIONS */

        void takegem();
        void takesword();
        void findaction();
};

character::character()
{
    this->name = "Unknown";
    this->age = 0;
    this->health = 100;
    this->gold = 0;
}

character::~character()
{

}

bool operator==(character& c1, character& c2)
{
    if (c1.name == c2.name)
    {
       std::cout << "\n\nName is the same.\n\n";
       return true;
    }
    else
    {
       std::cout << "\n\nName is different.\n\n";
       return false;
    }
}

void character::setname(std::string name)
{
    this->name = name;
}

void character::setage(short age)
{
    this->age = age;
}

void character::sethealth(short health)
{
    this->health = health;
}

void character::setgold(short gold)
{
    this->gold = gold;
}

void character::buyitem(std::string item, short price)
{
    if (price > this->gold)
    {
        print("\n\nYou do not have enough gold to buy "); std::cout << item; print("\n\n");
        print("1. Back\n\n> "); char c; std::cin>>c;
    }
    else
    {
        this->gold -= price;
        this->inventory.push_back(item);
        print("\n\nYou bought "); std::cout << item; print(" for "); std::cout<<price; print(" gold\n\n");
        print("1. Back\n\n> "); char c; std::cin>>c;
    }
}

void character::sellitem(std::string item, short price)
{
    short temp = this->gold;
    std::vector<std::string>::iterator it;
    for (it = this->inventory.begin(); it != this->inventory.end(); ++it)
    {
        if (*it == item)
        {
            this->inventory.erase(it);
            this->gold += price;
            print("\n\nYou sold "); std::cout << item; print(" for "); std::cout << price; print(" gold"); break;
        }
    }
    if (temp == this->gold)
    {
            print("\nYou do not have "); std::cout << item; print(" to sell.");
    }
    print("\n\n1. Back\n\n> "); char c; std::cin>>c;
}

void character::takedamage(short damage)
{
    this->health -= damage;
    if (this->health <= 0)
    {
        system("cls"); std::cout << TITLE;
        print("Your bones are swept clean by the desolate wind... your adventure is done.");
        print("\n\n1. Game Over\n\n> "); char c; std::cin>>c; exit (EXIT_SUCCESS);
    }
}

void character::display()
{
    system("cls"); std::cout <<  TITLE;
    print("\nName: "); std::cout << this->name;
    print("\nAge: "); std::cout << this->age;
    print("\nHealth: "); std::cout << this->health;
    print("\nGold: "); std::cout << this->gold << "\n\n";
    std::cout << this->name; print(" Inventory\n\n");
    for (std::string s : this->inventory)
    {
        std::cout << s << std::endl;
    }
    print("\n\n1. Back\n\n> "); char c; std::cin>>c;
}

void character::additem(std::string item)
{
    this->inventory.push_back(item);
    std::cout << "\nItem added";
}

void character::removeitem(std::string item)
{
    for (std::vector<std::string>::iterator it = this->inventory.begin(); it != this->inventory.end(); ++it)
    {
        if (*it == item)
        {
            this->inventory.erase(it);
            std::cout << "\nItem removed"; break;
        }
    }
}

// ACTIONS

/////////////////////////////
/////////////////////////////
/////////////////////////////

void character::takegem()
{
    system("cls");
    print("You take a rough gemstone from the rocks");
    char c; std::cin>>c;
    this->additem("Gemstone");
}

void character::takesword()
{
    system("cls");
    print("You pick up a sword.");
    char c; std::cin>>c;
    this->additem("Sword");
}

void character::findaction()
{
    if (current->name == "Plains")
    {
        this->takegem();
    }
}

 

Quote

I've built a class for characters and the areas which takes care of the displaying and world navigation aspects, but I'm now finding the whole approach really inflexible and don't know how to add all my extra content and options to each area

What do you mean with inflexible, can you make an example? 

Also what is the kind of extra contents or option you're trying to add? :)  (I probably can't help you, but maybe this extra informations can help someone else to answer)

EDIT: my bad, didn't notice there where more than 1 reply, so you probably already answer that

Quote

What I also find challenging (probably the most), is having all the control flow from the area class switch statement which currently handles direction:

I've never done something like it, but I would probably have every area object with inside of it a 


map<string,area*> Connections;

and then the World class would have a function that takes care of building  the network of areas, maybe reading it from a .txt or something.

So the player trying to go north would do


if(CurrentArea->Connections.find("north") != Connections.end())
{
	CurrentArea = Connections[north];
}
else
{
   cout << "can't go that way" << endl;
}

or you can have it taking it directly the user numbers instead of "north", so you don't even have to convert it, though you lose in readability

Though it's hard to give advice without more specific questions you can think of two major strategies for figuring out how to design code things.

First off, examine what it is your code needs to do, forget about the code itself and focus on the action. In your case you need to display a list of things that you can do when you enter a room, then you need to respond to input based on that list.

Second, look at what things have in common between them, that's an often not well explained thing in coding is that data driven behavior is all about -similar- behavior. In your case each rooms needs an action. Well what do actions have in common? They have a number for their action in the list, they have text that describes what they do, and then they have a function that they call when you select them.

Looking at it that way, you could make a structure or class that represents an action, and have rooms contain a list of actions, then when you enter a room you can have a function that simply prints them. Since the input also needs to deal with actions you might make part of the action data include a function to call to handle them when they are selected, or something. The details don't matter, the point is to think of it as an abstraction, you are trying to allow yourself to add an arbitrary amount of things that all do similar behavior, that one bundle of code can work with.

Thanks for the replies, and thanks Satharis, that makes a lot of sense. I feel a lot more confident now and will go and have a dabble this afternoon!

This topic is closed to new replies.

Advertisement