Sign in to follow this  
musasabi

(more) text-based navigation

Recommended Posts

okay~, so. designing everything from scratch, trying to force myself to become familiar with C++ and get creative about my problems. so heres what i came up with for navigating a text-based world. a "floor" is a vector of "rooms", each room contains a vector of "directions". each direction class (north, south, east, west, up, down) contains a counter of how many portals are available to travel through in that direction. each portal contains an index number for the appropriately corresponding room on that floor. read the code below if you want to see the gory details of how im doing it. otherwise, heres my question: its easy to go room-to-room with this setup. however, its time to expand it to floor-to-floor and later building-to-building and so on. but, of course, i have a problem. i cant use simple index numbers to reference some member of another vector. the hierarchy is daunting. so, how would _you_ adapt what ive got (or would you rewrite it?) to facilitate switching floors? and then hopefully the same theory can be applied to switching buildings/cities/whatever? thanks as always. the definitions in the header:
enum DIRECTION{N, S, E, W, U, D}; //north, south, east, west, up, down

int wander();

class Direction
{
	public:
		Direction(int whichDirection, int whichRoom);
		Direction(){};
		~Direction(){};

		int getDestination(int whichPortal) const {return destination[whichPortal];}
		int getNumberOfPortals() const {return numberOfPortals;}

	private:
		int numberOfPortals;
		vector <int> destination;
};

class Room
{
	public:
		Room(int whichRoom);
		~Room(){};

		string getName() const {return name;}
		string getDescription() const {return description;}
		int getDestination(int whichDirection, int whichPortal) const {return direction[whichDirection].getDestination(whichPortal);}

		Direction direction[6];

	private:
		string name;
		string description;
};

class Floor
{
	public:
		Floor();
		~Floor(){};

		const string getName(){return name;}
		const int getNumberOfRooms(){return numberOfRooms;}

		vector<Room> rooms;

	private:
		string name;
		int numberOfRooms;
};
the code from the cpp file:
#include "main.h"
#include "nav.h"

Direction::Direction(int whichDirection, int whichRoom)
{
	switch(whichRoom)
	{
		case 0:	//front porch
		{
			switch(whichDirection)
			{	case S:
					numberOfPortals = 1;
					destination.push_back(2); //to main hallway
					break;
				default:
					numberOfPortals = 0;
					break;
			}
		}
		break;

		case 1:	//wine cellar
		{
			switch(whichDirection)
			{
				case S:
					numberOfPortals = 1;
					destination.push_back(4); //to lateral hallway
					break;
				default:
					numberOfPortals = 0;
					break;
			}
		}
		break;

		case 2:	//main hallway
		{
			switch(whichDirection)
			{
				case N:
					numberOfPortals = 1;
					destination.push_back(0); //to front porch
				case S:
					numberOfPortals = 1;
					destination.push_back(4); //to lateral hallway
					break;
				default:
					numberOfPortals = 0;
					break;
			}
		}
		break;

		case 3:	//ballroom
		{
			switch(whichDirection)
			{
				case S:
					numberOfPortals = 1;
					destination.push_back(7); //to server room
					break;
				case E:
					numberOfPortals = 1;
					destination.push_back(8); //to arena
					break;
				default:
					numberOfPortals = 0;
					break;
			}
		}
		break;

		case 4:	//lateral hallway
		{
			switch(whichDirection)
			{
				case N:
					numberOfPortals = 2;
					destination.push_back(1); //to wine cellar
					destination.push_back(2); //to main hallway
					break;
				case S:
					numberOfPortals = 2;
					destination.push_back(5); //to bathroom
					destination.push_back(6); //to kitchen
					break;
				case E:
					numberOfPortals = 1;
					destination.push_back(3); //to ballroom
					break;
				default:
					numberOfPortals = 0;
					break;
			}
		}
		break;

		case 5:	//bathroom
		{
			switch(whichDirection)
			{
				case N:
					numberOfPortals = 1;
					destination.push_back(4); //to lateral hallway
					break;

				default:
					numberOfPortals = 0;
					break;
			}
		}
		break;

		case 6:	//kitchen
		{
			switch(whichDirection)
			{
				case N:
					numberOfPortals = 1;
					destination.push_back(4); //to lateral hallway
					break;

				default:
					numberOfPortals = 0;
					break;
			}
		}
		break;

		case 7:	//sever room
		{
			switch(whichDirection)
			{
				case N:
					numberOfPortals = 1;
					destination.push_back(3); //to lateral hallway
					break;

				default:
					numberOfPortals = 0;
					break;
			}
		}
		break;

		case 8:	//arena
		{
			switch(whichDirection)
			{
				case W:
					numberOfPortals = 1;
					destination.push_back(3); //to ballroom
					break;

				default:
					numberOfPortals = 0;
					break;
			}
		}
		break;
	}
}

Room::Room(int whichRoom)
{
	//assign room text
	switch(whichRoom)
	{
		case 0:
			name = "Front Porch";
			description = "can go south";
			break;
		case 1:
			name = "Wine Cellar";
			description = "can go south";
			break;
		case 2:
			name = "Main Hallway";
			description = "can go south";
			break;
		case 3:
			name = "Ballroom";
			description = "can go south, east";
			break;
		case 4:
			name = "Lateral Hallway";
			description = "can go north 2, south 2, east";
			break;
		case 5:
			name = "Bathroom";
			description = "can go north";
			break;
		case 6:
			name = "Kitchen";
			description = "can go north";
			break;
		case 7:
			name = "Server Room";
			description = "can go north";
			break;
		case 8:
			name = "ARENA";
			description = "can go west";
			break;
	}

	//use the direction class constructor to assign the destinations per direction
	for(int i = 0; i < 6; i++)
				direction[i] = Direction(i, whichRoom);
}

Floor::Floor()
{
	numberOfRooms = 9;

	for(int i = 0; i < numberOfRooms; i++)
		rooms.push_back(Room(i));
}

int wander()
{
	Floor floor;
	vector<Room>::iterator itrCurrentRoom = floor.rooms.begin();
	int menuChoice = -1;
	string sMenuChoice;
	vector <string> menuChoiceToken;
	bool actionFailed = true;
	
	while(menuChoice != 0 && actionFailed == true)
	{
		cout << endl << itrCurrentRoom->getName() << endl << "---\n";
		cout << itrCurrentRoom->getDescription() << endl;

		sMenuChoice.clear();
		menuChoiceToken.clear();
		do
		{
			if(cin.fail())
			{
				cin.clear();
				cin.ignore(numeric_limits<streamsize>::max(), '\n');
			}
			cout << ">>";
			getline(cin, sMenuChoice);
			fflush(stdin);

			tokenize(sMenuChoice, menuChoiceToken, " ");
		}
		while((menuChoiceToken[0] != "go" && menuChoiceToken[0] != "0") || cin.fail());

		if(menuChoiceToken[0] == "0")
			return PLAYER_QUIT;

		if(menuChoiceToken[0] == "go")
		{
			if(menuChoiceToken[1] == "north")
			{
				if(itrCurrentRoom->direction[N].getNumberOfPortals() == 0)
				{
					actionFailed = true;
					cout << "There is no way to travel northward.\n";
					getchar();
					fflush(stdin);
				}
				else
					itrCurrentRoom = (floor.rooms.begin() + itrCurrentRoom->getDestination(N, 0));
			}

			else if(menuChoiceToken[1] == "south")
			{
				if(itrCurrentRoom->direction[S].getNumberOfPortals() == 0)
				{
					actionFailed = true;
					cout << "There is no way to travel southward.\n";
					getchar();
					fflush(stdin);
				}
				else
					itrCurrentRoom = (floor.rooms.begin() + itrCurrentRoom->getDestination(S, 0));
			}
			else if(menuChoiceToken[1] == "east")
			{
				if(itrCurrentRoom->direction[E].getNumberOfPortals() == 0)
				{
					actionFailed = true;
					cout << "There is no way to travel eastward.\n";
					getchar();
					fflush(stdin);
				}
				else
					itrCurrentRoom = (floor.rooms.begin() + itrCurrentRoom->getDestination(E, 0));
			}
			else if(menuChoiceToken[1] == "west")
			{
				if(itrCurrentRoom->direction[W].getNumberOfPortals() == 0)
				{
					actionFailed = true;
					cout << "There is no way to travel westward.\n";
					getchar();
					fflush(stdin);
				}
				else
					itrCurrentRoom = (floor.rooms.begin() + itrCurrentRoom->getDestination(W, 0));
			}
			else if(menuChoiceToken[1] == "up")
			{
				if(itrCurrentRoom->direction[U].getNumberOfPortals() == 0)
				{
					actionFailed = true;
					cout << "There is no way to travel upward.\n";
					getchar();
					fflush(stdin);
				}
				else
					itrCurrentRoom = (floor.rooms.begin() + itrCurrentRoom->getDestination(U, 0));
			}
			else if(menuChoiceToken[1] == "down")
			{
				if(itrCurrentRoom->direction[D].getNumberOfPortals() == 0)
				{
					actionFailed = true;
					cout << "There is no way to travel downward.\n";
					getchar();
					fflush(stdin);
				}
				else
					itrCurrentRoom = (floor.rooms.begin() + itrCurrentRoom->getDestination(D, 0));
			}
			else
			{
				cout << "You can't go " << menuChoiceToken[1] << "!\n";
				getchar();
				fflush(stdin);
			}
		}
	}
}

Share this post


Link to post
Share on other sites
I'm running on about three hours of sleep, so I apologize beforehand if I say anything... awkward.

First, I'd get rid of the hardcoded room instances. The notion of the code itself detailing what room a particular room's exits will lead to is a big no-no, at least for a comfortable design.

As a rough example, consider changing Direction from referring to hard exit numbers to instead referring to Rooms themselves.
class Direction
{
public:
Direction( const Room&, const Room& );

bool OnTravel( Player& );

private:
Room& firstRoom;
Room& secondRoom;
};

class Room
{
public:
Room();

bool OnEnter( Player& );
bool OnExit( Player& );

private:
std::map< std::string, Direction* > exits;
};

class RoomManager
{
public:
RoomManager( const std::string& RoomFile );

private:
std::vector< Room > rooms;
};

Instead of trying to have a Direction worry about what the Rooms have, it only has to worry about itself. I'd personally use the name RoomExit or something similar at this point, rather than Direction, but the idea remains the same.

When a player tries to leave, the interpreter matches which direction the player specifies with the directions that are listed in the room, namely the key to the exits member. It then grabs the Direction that the key refers to and attempts to use the OnTravel member. Since you can only go between two Rooms with one Direction, I'd have it check which one the player is in and choose the other. It returns bool to specify whether or not the player was actually allowed to use the exit. You don't have to have this, but the idea could be expanded to included locked exits and the like.

Same with the return values for OnEnter and OnExit. The reason for having them in addition to Direction's OnTravel is so that you could have special logic on the room -- say it takes off ten health for leaving, or some such.

RoomManager isn't fleshed out, but it's there to point out that, rather than having the Rooms care about what other Rooms are doing, it'd be best to have a manager that's keeping track of them. It would also handle the loading and unloading of rooms. This way you could instead define rooms and their exits in a text file -- custom, XML, INI-style, whatever means you want to use. It also allows you to hide the means of generating and using a RoomID behind the manager, rather than the rooms themselves caring about such a thing.

There may be quirks with this particular design, I haven't put a whole lot of thought behind it, but the gist of the idea remains the same. Room definitions are data, not code. Keep them as such.

[Edited by - tychon on March 7, 2008 6:37:00 AM]

Share this post


Link to post
Share on other sites
Also, I'd point out that the notion of floors, depending on what you plan on doing with it, is more a human concept than something that needs to be worked out in code. You could easily have a single room per "floor" that has an up direction, leading to another series of rooms that has one room with an up.

If you used the notion of a manager, though, you could define a new floor in a different room file, load that region up, and create a special link -- read, exit between two particular rooms -- between the two regions. The idea of the physical regions matching up is more a problem of how you lay the exits out and describe the rooms. As far as the code is concerned they could just as well be floating tesseracts as much as they could be floors of a building.

Again, depends on how you handle things, but there's no reason the code has to explicitly match how things are in real life, so long as the data presents it.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this