Text Adventure help

Started by
10 comments, last by dvang 18 years, 1 month ago
Ok, I'm feeling dumb. <sigh> I should know this, but I am very rusty at C++. I am trying to make a 'simple' text adventure using classes (for rooms & objects). I cannot seem to get the rooms to work, or anything else for that matter. Could I get some help getting started please? I learn best by examining sample code, but I havent found any tutorials or open source that covers this. What I need to get me started is the basics of a room class and object class, with the ability to link rooms, move between them, and have a basic object able to be picked up and dropped. All help is greatly appreciated, thanks!
Advertisement
You're asking a lot. More than most of us are willing to spend time working on for the sake of answering your post (hint: it will take much more than 10 minutes...)

SO!

Start by taking a piece of paper and writing down:

The information and operations a Room needs to contain and provide.
The information and operations an Object needs to contain and provide.
The information and operations the Character needs to contain and provide.

For each piece of information, write down what operations and classes need to access it, how and why.
For each operation, write down write down what classes needs to access it, how and why.

Then come back here and we'll talk about converting that to C++.
"Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it." — Brian W. Kernighan
Edit: I just saw Fruny's comment. Don't read this unless you did what he told you to do. This is some sort of spoiler... :) [with colored hidden text]

Edit2: recolor :)

Are you sure that you are ready for nearly uncommented code ? :)

Let's assume the following:
  1. The rooms are representing a graph. They are linked each other using pointer (ie room A points to room B) or any other linking systme that might work (ie room A contains an id that is used to find room B in a list of rooms). A room can have multiple exits.
  2. Any object can be in the room;
  3. The player can pick/drop objects in the room or move to another room


Point 1 assume that you'll have some room creation function somewhere (in order to correctly set up the rooms).

The room class will look like
class Room{private:  // our 'list' of exits is a standard library map<> object  std::map<std::string, Room*> mExits;  std::string mName;  void realAddExit(const std::string& exitName, Room *room)  {    mExits[exitName] = room;  }public:  // room constructor  Room(const std::string& name) : mname(name) { }  // name accessor  std::string getName() const { return mName; }  // find if there is an exit with this name  Room *getExit(const std::string& exitName)  {    // find the exit in the list    std::map<std::string, Room*>::iterator it;    it = mExits.find(exitName);    if (it != mExits.end()) {      return it->second;    }    return NULL;  }  void addExit(Room *room)  {    // we must add the exit to our list of exits, but we    // must also add us to the list of exits of this new    // exit (am I clear enough ? :))    realAddExit(room->getName(), room);    room->realAddExit(mName, this);  }};/*// it is rather simple to create a room and to init it:Room *roomEntrance = new Room("entrance");Room *roomA = new Room("roomA");Room *roomB = new Room("roomB");Room *roomC = new Room("roomC");Room *roomD = new Room("roomD");Room *roomExit = new Room("exit");// create a sqaure with a diagonalroomEntrance->addExit(roomA);roomA->addExit(roomB);roomB->addExit(roomC);roomC->addExit(roomD);roomD->addExit(roomA);roomB->addExit(roomD);roomC->addExit(roomExit);*/


As for our Object class, let's say our object only have a name (it's simpler for the moment):
class Object{private:  std::string mName;public:  Object(const std::string& name) : mName(name) { }  std::string getName() const { return mName; }};


We can now add a list of object in our room:
// class Room == same as above + list of objectclass Room{private:  std::vector<Object*> mObject;  // ...public:  // object list management  void addObject(Object* o) { mObjects.push_back(o); }  Object *getObject(size_t i) { return mObjects; }  size_t getObjectCount() const { return mObjects.size(); }  // this function removes an object from the list but don't destroy it  void removeObject(size_t i) { mObjects.erase(mObjects.begin()+i); }  // we have to add a destructor in order to delete any remaining object  ~Room()  {    for (size_t i=0; i<mObjects.size(); i++) {      delete mObjects;    }    mObjects.clear();  }};


Now, let's have a look to the game code:
void handleRoom(Player *player, Room *beginRoom, Room *exitRoom){  Room *room = beginRoom;  while (room != exitRoom) {    std::cout << "you are in room " << room->getName() << std::endl;    std::string userAction;    std::cin >> userAction;    if (userAction == "pick") {      std::cout << "which object ?" << std::endl;      for (size_t i=0; i<room->getObjectCount(); i++) {        Object *o = room->getObject(i);        std::cout << "  - " << o->getName() << std::endl;      }      std::string pickObject;      std::cin >> pickObject;      if (pickObject != "none") {      for (size_t i=0; i<room->getObjectCount(); i++) {        Object *o = room->getObject(i);        if (o->getName() == pickObject) {          player->addObject(o);          room->removeObject(i);          break;        }      }    }  } else {    Room *exit = room->getExit(userAction);    if (!exit} {      std::cout << "I found no exit with this name :'(" << std::endl;    } else {      room = exit;    }  }}// callig this function is rather simple:// handleRoom(player, roomEntrance, roomExit);// all the magic will be done inside.


I believe I gave you enough code now :) Of course, you'll have to work a little (well, maybe more than just "a little") to have this running. You'll have to create the Player class and to correctly setup everywith (plus, there may be some compilation problems, since I didn't tell you what include files to get (so you'll have to do some research to understand what are these std::string, std::map and std::vector I used all over this code)).

Anyway, you're free to ask more questions here, we'll be glad to help you :)

Regards,

[Edited by - Emmanuel Deloget on March 10, 2006 12:15:41 PM]
Well, I guess Emmanuel isn't like most people here. [grin]
"Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it." — Brian W. Kernighan
Quote:Original post by Fruny
Well, I guess Emmanuel isn't like most people here. [grin]


Is it better now ? [smile]
dvang, you don't necessarily need very much. For each object you want to have in the game, start by typing one of these for each one:
class NameOfObject{};

Write these with the most basic items at the top, and the most complicated/involved items (those most likely to build on the more basic items) towards the bottom. In general, it's a safe practice to create an object for each type of "thing" you may need in the game world; in your case, one for a Room, one for an Object, likely one for the Player or a given Character, and possibly also one for a Connection (or whatever you want to call it -- maybe Door? I like Door) between Rooms.

Then, start adding to each class the elements it may need. Most things will probably want some kind of text description (so that when you look at them, you'll have something to display) -- many here would I'm sure recommend a std::string for storing this. Also, any given Room will need to have some lists of things -- probably a list of Objects and a list of Doors, to start. A good class to use for listing things is a std::vector<YourTypeHere>.

You'll want to start adding methods to things too, so you can interact with them. Running with the Door metaphor, you might give it an open() function, which would transport you to whatever's on the other end of a door...which you might indicate with some sort of reference to another Room -- perhaps a pointer, or an index into a big array of Rooms. However you want to do it is good.

As for building all this, you'll probably want to provide some calls for adding to Rooms and establishing Doors to other Rooms and so on. The coolest thing would be if you could build all this from a file or set of files, but for that you'd need a parser, which is some work, but which will be well-worth it as far as ease of content generation down the road goes.

I know this isn't a cookie-cutter solution, but I hope it gets you thinking. Let us know if you want more help once you've got more details ironed out.
- Hai, watashi no chichi no kuruma ga oishikatta desu!...or, in other words, "Yes, my dad's car was delicious!"
Thanks for the replies. I'm sorry if it seems I was asking too much, I am just frustrated that things that seem like they should be working, aren't.

The information and operations a Room needs to contain and provide.
The information and operations an Object needs to contain and provide.
The information and operations the Character needs to contain and provide.

- Room need exits, objects contained, a short name, and a long description.
- Objects, well at this point I'm only concerned about them being able to be picked up and dropped by a character
- The character needs to be able to view the objects he's carrying, look at a room/object, and move between rooms.

I managed to get a semblance of a room class working, but I'm not sure how to get input to move between rooms. Here's what I have (not fiddling with objects until I can get rooms working):

-----------
class Object
{

public:
string desc;
string name;
} ;

class Room : public Object
{
private:

vector<Room *> exits;
vector<char> nDirection;
bool entered;

public:
Room(string iname) {name = iname; entered = false; exits = vector<Room *>();}
void linkTo(Room &other, char direction);
void showExits(Room* place);
} ;


void Room::linkTo(Room &other, char direction) {

// Add a Room* to the vector which points at the input room.
// Add a direction character to the vector to tell what direction it is

exits.push_back(&other);
nDirection.push_back(direction);
}

void Room::showExits(Room* other) {

char dir = other->nDirection.at(0);
string exit = other->exits.at( 0 )->name;
cout << "\nThere are exits, and the first one is " << exit << ", which is to the " << other->nDirection.at(0) << endl;

}


This seems to work, or at least I can print out the exit names and directions. From here, I don't know what I would do to implement a movement function. I also can't seem to get a way to loop and display the entire contents of a vecto. I'm still processing Emmanuel's extensive help, but I thought I'd post what I had already. Thanks.
Quote:Original post by dvang
vector<Room *> exits;
vector<char> nDirection;


I'd use a map<char, Room*> or a vector<pair<char, Room> >, to keep the exit and the direction together, rather than in separate data structures.

Quote:
	 bool entered;


What is it for? It is indicating what room you're in? If so, I would rather store your current position as a pointer to the appropriate Room in the Character object.

Quote:
	  Room(string iname) {name = iname; entered = false; exits = vector<Room *>();}


Now, just a C++ thing: use constructor initialization lists:

Room(const string& iname) :    name(iname),    entered(false)       // vectors are always empty on startup{}



This seems to work, or at least I can print out the exit names and directions.
Quote:From here, I don't know what I would do to implement a movement function.


Given a character as the direction for your movement, you need to find if there is a match in the room's exits. If so, display a message, move to the new room by adjusting the 'current room' pointer then display the new room's description and content.

Quote:I also can't seem to get a way to loop and display the entire contents of a vecto.


There are essentially three ways of looping through a vector. First, you can just use indices:

vector<Foo> foo;for(int index=0; index!=foo.size(); ++index)   cout << foo[index] << endl;


Or you can use iterators (work for all containers):

vector<Foo> foo;vector<Foo>::iterator itor;for(itor = foo.begin(); itor != foo.end(); ++itor)   cout << *itor << endl;


Or you can use the appropriate STL algorithm.

vector<Foo> foo;for_each(foo.begin(), foo.end(), my_print_function);


There are other ways, but that's a good start.
"Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it." — Brian W. Kernighan
Quote:Original post by Fruny
I'd use a map<char, Room*> or a vector<pair<char, Room> >, to keep the exit and the direction together, rather than in separate data structures.


Personally, I'd recommend an Exit class in that map, containing information not only about the rooms it links, but also the state of the exit. Easier to implement doors, lit or unlit exits (think a dark tunnel that you can't see what's on the other side), and so forth.

Of course, I always was a bit grandiosely minded.
Allow me to completely recycle another post I made for someone. I actually wrote an entire little example about this (minus items and monsters in rooms)

I may expand it into a tutorial and host it on my site:


Here is an example of a text rpg like the one you are trying to do. I made it just for you as you said you were having difficulties with files and such. It isn't super well commented, so if anyone else is willing to go through and comment it or to package all the files in a zip, that would be great. I'm just providing you the working source code. This isn't meant to replace your effots, just to give you something that might help in learning. If you look at my code as a reference it might help in understanding some less hard-coded methods of going about level loading and such.

I do not profess this is paramount of coding expertise, I did it quickly and simply for you. I didn't use any of the more obscure stl stuff, I kept it to lists and strings. If you have any specific questions please ask. Again, if someone else could heavily comment this or fix up anything you feel like fixing while maintaining the simplicity (which is important) feel free to. Just keep my name at the top of the map header.

It's easy to add new rooms and you can have any direction you want link to another room. So here's what 45 minutes and a plan can bring ;):

main.cpp
#include <iostream>#include <string>#include "map.h"using namespace std;int main(){   map world;   int playerRoom = 0;   bool done = 0;   int moveTo;   string playerInput;   world.loadRoomsFromFile("world.txt");   while(!done){      world.displayRoom(playerRoom);      cout << endl << "Command: ";      cin >> playerInput;      moveTo = world.checkDirection(playerInput, playerRoom);      if(moveTo != -1){         playerRoom = moveTo;         cout << endl << endl << "You walk " << playerInput << " and find yourself in a new place." << endl << endl;      }else{         //handle input for other things.         if(playerInput == "exit" || playerInput == "quit"){            done = 1;         }      }   }   cout << endl << "bye!" << endl;   char c;   cin.get(c);   cin.get(c);   return 0;} 


map.h
/**************************************************\| This is a simple little text rpg type world and  || loader that I made to help guide new coders who  || are interested in making a text world.           || I hope that you can learn from this example and  || and go on to make your own very nifty text rpg.  ||                                                  || example code by Michael Hamilton                 || (maxmike@gmail.com | http://www.mutedvision.net) |\**************************************************/#ifndef __MAP_H__#define __MAP_H__#include <iostream>#include <fstream>#include <string>#include <list>int stoi(const char*d);struct roomExit{   std::string direction;   int id;};class room{public:   room(){id = -1; description = "NULL";}   ~room(){}     void setDescription(const std::string &desc){      description = desc;   }   std::string getDescription(){      return description;   }   void setId(const int &m_id){      id = m_id;   }   int getId(){      return id;   }   void addExit(const std::string &name, const int &m_id);   int getExitId(const std::string &name);   int getExitId(int num);   std::string getExitName(int num);   int getNumOfExits(){      return roomExits.size();   }   void clearExits(){      roomExits.clear();   }   void display(){      std::list<roomExit>::iterator cell;      std::cout << description;      std::cout << std::endl << "roomExits:"<<std::endl;      for(cell = roomExits.begin();cell!=roomExits.end();cell++){         if(cell!=roomExits.begin()){            std::cout << ", ";         }         std::cout << cell->direction;      }      std::cout << std::endl;   }private:   std::list<roomExit> roomExits;   std::string description;   int id;};class map{public:   void addRoom(const room &roomToAdd){      rooms.push_back(roomToAdd);   }   bool displayRoom(int id);   int checkDirection(const std::string &userInput, int roomId);   bool loadRoomsFromFile(std::string fileName);private:   std::list<room> rooms;};#endif 


map.cpp
#include "map.h"   int stoi(const char*d){int r;for(r=0;*d>='0'&&*d<='9';r=r*10+*(d++)-'0');return r;}//ROOM class functions   void room::addExit(const std::string &name, const int &m_id){      roomExit toAdd;      toAdd.direction = name;      toAdd.id = m_id;      roomExits.push_back(toAdd);   }   int room::getExitId(const std::string &name){      std::list<roomExit>::iterator cell;      for(cell = roomExits.begin();cell!=roomExits.end();cell++){         if(cell->direction==name){return cell->id;}      }      return -1; //this number reserved for invalid ids   }   int room::getExitId(int num){      std::list<roomExit>::iterator cell = roomExits.begin();      if(num == 0 && cell!=roomExits.end()){return cell->id;}      for(;cell!=roomExits.end() && num>=0;cell++,num--){         if(num==0){return cell->id;}      }      return -1;      }   std::string room::getExitName(int num){      std::list<roomExit>::iterator cell = roomExits.begin();      if(num == 0 && cell!=roomExits.end()){return cell->direction;}      for(;cell!=roomExits.end() && num>=0;cell++,num--){         if(num==0){return cell->direction;}      }      return "NULL";        }    //MAP class functions   bool map::loadRoomsFromFile(std::string fileName){      room roomToAdd;      std::ifstream worldFile(fileName.c_str());      if (!worldFile.is_open()){return 0;}      std::string totalString;      int number;      std::string line, tmpDirection;      while (! worldFile.eof() )      {         std::getline (worldFile,line);         totalString+=(line+"\n");      }      worldFile.close();      std::size_t cursor = 0;      while(cursor < totalString.size()){         switch(totalString[cursor]){            case '~':               line = "";               for(cursor++;totalString[cursor]!=':'&&cursor<totalString.size();cursor++){                  line+=totalString[cursor];               }               cursor++;               if(line == "room"){                  line = "";                  for(;totalString[cursor]!='|'&&cursor<totalString.size();cursor++){                     line+=totalString[cursor];                  }                  number = stoi(line.c_str());                  roomToAdd.setId(number);               }else if(line == "description"){                  line = "";                  for(;totalString[cursor]!='|'&&cursor<totalString.size();cursor++){                     line+=totalString[cursor];                  }                  roomToAdd.setDescription(line);               }else if(line == "exit"){                  line = "";                  for(;totalString[cursor]!=','&&cursor<totalString.size();cursor++){                     line+=totalString[cursor];                  }                  tmpDirection = line;                  line = "";                  for(cursor++;totalString[cursor]!='|'&&cursor<totalString.size();cursor++){                     line+=totalString[cursor];                  }                  number = stoi(line.c_str());                  roomToAdd.addExit(tmpDirection, number);               }            break;            case '_':               if(roomToAdd.getId()!=-1){                  rooms.push_back(roomToAdd);               }               roomToAdd.clearExits();               roomToAdd.setId(-1);            break;         }         cursor++;      }      return 1;   }   bool map::displayRoom(int id){      std::list<room>::iterator cell;      for(cell = rooms.begin();cell!=rooms.end();cell++){         if(cell->getId() == id){            cell->display();            return 1; //found and displayed the room         }      }      return 0; //failed to find the room   }   int map::checkDirection(const std::string &userInput, int roomId){      std::list<room>::iterator cell;      for(cell = rooms.begin();cell!=rooms.end()&&cell->getId()!=roomId;cell++){;}      if(cell!=rooms.end()){         return cell->getExitId(userInput);      }      return -1;   }


accompanying file, save it as world.txt:
world.txt
_~room:0|~description:You stand in a dark corridor.|~exit:North,1|~exit:South,2|_~room:1|~description:You're in a room with only one exit!|~exit:South,0|_~room:2|~description:You stand in a study, many books line the shelf.|~exit:Up,3|~exit:North,0|~exit:Down,4|_~room:3|~description:you are in the attic.|~exit:Down,2|_~room:4|~description:you are in the basement.|~exit:Up,2| _

_______________________"You're using a screwdriver to nail some glue to a ming vase. " -ToohrVyk

This topic is closed to new replies.

Advertisement