Searching Within an Input File

Started by
5 comments, last by Zahlman 18 years, 8 months ago
I am working on the very beginning stages of a C++ text-based game (classic Infocom-type). I created a few basic functions, one to print the location information (i.e. You are in Room A, welcome to room a there is a light switch here) and another to read the input (I haven't exactly written the parser yet [embarrass]). Anyway, I am trying to get all my game info from a file called simpletext.gam. I am able to read all of the lines for the first location and print them (gamedata.getline(desc,100);), but I can't figure out a way to search the file for a specific location name and then start the read buffer there (for title, description, etc.). Anyway, here's the code I have so far, I know its very basic but well here goes.
#include <fstream>
#include <iostream>

using namespace std;

bool ingame=true;

void printloc(char roomn[100]) {
	ifstream gamedata("simpletext.gam");
	char name[100];
	char title[100];
	char desc[100];
	gamedata.ignore(0, 'roomn');
	gamedata.getline(name,100);
	gamedata.getline(title,100);
	gamedata.getline(desc,100);
	cout << title << endl;
	cout << " " << desc << endl;
	ingame=false;
}

void readinput() {
	cout << "\n> ";
	char input[100];
	cin.getline(input,100);
}
	

int main() {
    while(ingame==true) {
		char room[100] = "roomB";
		printloc(room);
		readinput();
	}	
}
Here is the format of my simpletext.gam file:
roomA
room A
Welcome to room A.

roomB
room B
Welcome to room B.
I plan to add more information to it later. Anyway, as I said above I'm trying to get the buffer to search for "roomb" and start the read buffer there. Thanks for all your help guys.
EBrennan
Advertisement
First, your "ignore" usage is wonky: .ignore() skips stuff for the specified number of characters, or until the first appearance of the specified single character, whichever comes first. The second argument is a char argument, i.e. something specified in single quotes which is supposed to be a single character - your compiler should at least warn you for putting 'roomn' like that. Anyway, as it is, it will do nothing, because it will succeed in ignoring 0 characters right away, so that "comes first". But as it stands, you don't *want* to .ignore() anything at that point anyway, so the best thing is to just take that line out.

Also, you seem to have a fundamental misunderstanding of how C++ variables work. The variable names are not data of any sort, and do not exist in the compiled code. Thus, you cannot say 'roomn' and expect it to refer to the contents of a variable named roomn; it refers to that text instead (notwithstanding the single-quote issue).

Anyway, what you should do is: First, get acquainted with std::string; it will save you a hell of a lot of trouble down the road. It won't limit your text length, won't cause any memory management issues, and won't mess up when you try to pass it back from a function, the way a character array would.

Second, what you need to do is read the file as a series of entries. Each time, look at the 'name', and if it matches, then we can proceed.

Third, don't use globals when you can easily handle the problem by passing the value back as a return value. Here, "ingame" can be local to main(), and we can set it to a return value that we get from printloc().

#include <fstream>#include <iostream>#include <string> // for std::stringusing namespace std;// As I said, we can use the return value to set 'ingame'.// However, it's best not to mix the finding and output tasks together.// So, let's build a string that represents what we need to output, and// return that. If we can't find the room name, we'll return an empty string.string readDescription(const string& desiredRoom) {  ifstream gamedata("simpletext.gam");  string name, title, desc;  // Keep reading until the name matches the desired room name.  while(name != desiredRoom && !gamedata.eof()) {    // std::string lets us compare strings like that. Character arrays don't.    // To read a whole line into a std::string, we must use this free function    // instead (rather than the ifstream member function):    getline(gamedata, name);    getline(gamedata, title);    getline(gamedata, desc);    // And now we will skip over the blank line that separates entries:    gamedata >> ws; // assumes it will be blank. There are other ways, too.  }  // When we get here, either the name matches the desired room name (and  // the title and desc have already been read for that room), or the file  // reached the end (name not found).  if (gamedata.eof()) return ""; // The "" makes a char*, but it will be  // implicitly converted by assignment to the return value.  // Otherwise, we have the data we need.  // We can now build up a string consisting of the title and desc.  return title + "\n " + desc;  // That's another thing you can't do with character arrays.}// Having the input as a local variable here doesn't really help; we want to// communicate it back so we can use it.string readinput() {  cout << "\n> ";  string input;  getline(cin, input);  return input;}int main() {  string roomDescription = "";  string userInput;  // Previously, we ended the loop unconditionally, because printloc() would  // always set ingame to false. Now I'm doing it so that we end the loop almost  // every time; i.e. as long as we are able to read the room entry (such that  // roomDescription becomes non-empty, falsifying the loop condition). You  // probably want it to work the other way around, though, eventually.  while(roomDescription == "") {    // Try to find and show the data for roomB    roomDescription = readDescription("roomB");    cout << roomDescription;    userInput = readinput();  }	}


There are still lots of problems (in particular, you really don't want to re-read the whole file, every time through) and lots of work to do, but this should get you on the right track. :)
Thank you very much Zahlman [wink]!

Yes, re-reading several sections of the tutorial I'm working with (guess I was a little too eager to get started) I see now that I misunderstand the function of .ignore(). Also, I originally tried to use strings but wasn't sure if you could use that as a function type (return a string) so I just played it safe and went with a character array, however now I realize (and now I've gotten farther along into the tutorial) the functionality strings give you. I really appreciate your response and especially the example source code you posted. Just one question, you said at the end of your post that you wouldn't want to re-read the file everytime you had to print a location name or description. I was wondering how would you avoid that? Would you preload some of the information into memory to make the access time quicker? Thanks again for the response.
EBrennan
Just so you know, for some reason I had to remove your if statement afte the while loop in the readDescription function.

if (gamedata.eof()) return "";

I'm not sure why, the code made sense to me, however with the line the only output I got was a blank "> " prompt. Anyway, works now. Thanks again.
EBrennan
Typically you create a structure (or class - in C++ you can treat the two more or less equivalently) that represents a "room", and read the whole file and create all the Rooms that are represented by the file data. You could store these for example in an array, or a std::vector. The latter is a lot easier here since we don't know ahead of time how many Rooms there will be:

// We must also include <vector> for the std::vector object.// It is a templated type: just plain 'vector' doesn't exist, but rather we// must specify what it is that we want a vector of... keep reading :)// First, let's make that Roomstruct Room {  string name;  string title;  string desc;}; // This semicolon is important! And I usually forget it :(// Now we'll make a function to read in the file and return a vector of Rooms.// Instead of hardcoding the file name here, we'll pass it in.vector<Room> loadRooms(const string& filename) {//    ^^^^^^ <-- specifies "vector of Rooms".  // When we store things into a vector, it will copy the object into  // some space that the vector allocates for you. Our strategy here will be  // to set up a temporary Room object, and iteratively change its data and then  // append a copy of it to the "end" of the vector.  ifstream gamedata(filename);  Room currentRoom;  vector<Room> result; // where we store what we'll return.  // For as long as it it possible to read in a name,  while (getline(gamedata, currentRoom.name)) {    // read in the rest,    getline(gamedata, currentRoom.title);    getline(gamedata, currentRoom.desc);    // and copy the result into the vector.    result.push_back(currentRoom);  }  // Done.  return result;}


As for the other problem, I didn't quite think things through - when the Room to be found is the last one in the file, of course you will reach the end of the file right after successfully reading it. Doh. :)

And of course, as the game coding progresses, you will find that this setup doesn't quite accomodate your needs... but I don't want to do too much for you at once (at all, even... better to try for yourself) :)
Thanks again Zahlman. Before I started coding I originally thought of setting up the program by creating a location class where I could store all the rooms, an object class where I could store all the objects, etc. However, I thought it would get a little impractical since I would have to define each and every location and object in the code itself rather than in some data file. i.e.
loc roomA, roomB, roomC;obj objA, objB, objC;
However the way that you demonstrated it it actually seems more efficient and cleaner. I think I'll give it a try, rewriting the code I already have using struct and vector and then decide which I'll go with. Just one quick question, though, I'm really not too clear on the difference between structs and classes. I understand that they can basically be used to do the same things, but are they intended for different purposes? Thanks again for all your help.
EBrennan
Oh, I wasn't sure if you'd already heard of them, and was building towards it :)

In C++, there is really no difference; as far as the compiler is concerned, it just provides some syntactic sugar. In particular, a struct defaults to public access for data members, and public inheritance from its bases, and a class defaults to private. However, responsible C++ programmers make use of the two keywords as a bit of documentation: the expectation is that a "struct" is just a binding together of a couple bits of data to make a new type to hold stuff and pass it around (without concern for proper OO style) whereas a "class" will try to respect normal OO design considerations like enforcing invariants, providing some encapsulation, etc.

So if you want a Location class rather than a struct, you might for example provide a constructor that reads the data from the stream, instead of default-constructing one and then doing the reading yourself - that's messy and it's expecting calling code to much around with the class internals (and don't kid yourself: accessor and mutator functions don't really change that).

class Location { // was Room  // other room stuff here - now it's private  public:  Location(istream& dataSource) {    getline(dataSource, name);    getline(dataSource, title);    getline(dataSource, desc);  }}vector<Location> locations;istream gamedata("foo.gam");// Note: this assumes no blank line at the end after the last Location.// The problem is that .eof() doesn't return true until after you've already// tried and failed to read past the end, so if there's a blank line, it will// try one more time and produce an extra bogus Location.// There are a few ways around this; read some iostream documentation and// think about it :)while(!foo.eof()) {  locations.push_back(Location(gamedata));  gamedata.ignore(std::numeric_limits<streamsize>::max(), '\n');}

This topic is closed to new replies.

Advertisement