Program Questions

Started by
10 comments, last by Zahlman 17 years, 8 months ago
Alright, this is my second "game", actually it's more like a simulation. A hockey shootout between The Toronto Maple Leafs and the Carolina Hurricanes, and my two questions are, how is this thing actually randomizing, and how can I get it so the output stays on the line I want it to. ex.// Eric Stall, across the blueline, in on Raycroft, he shoots, he -> the output scores, or gets stopped goes here, but I want it up here ^. Here's the code for it:

#include <iostream>
#include <ctime>

using namespace std;

int main()
{
    int shot, tscore, cscore;
    tscore = 0;
    cscore = 0;
    
    cout << "Live From The Air Canada Center\n";
    cout << "Stanley Cup Finals - Game 7\n";
    cout << "Toronto-3  Carolina-3\n";
    cout << "Overtime - Shootout\n\n";
    cout << "Shootout lineups:\n";
    cout << "Toronto:\tCarolina:\n";
    cout << "Mats Sundin\tEric Stall\n";
    cout << "Eric Lindros\tCorey Stillman\n";
    cout << "Jeff O'neill\tDoug Weight\n\n";
    
    srand(time(NULL));
    //First shot
    shot = (rand() % 2);
    cout << "Toronto: " << tscore << "\tCarolina: " << cscore << endl;
    cout << "Eric Stall, across the blueline, in on Raycroft, he shoots, he";
    if (shot == 1){
                   cin.get();
                   cout << "scores!\n\n";
                   cscore++;
                   cin.get();
                   }
    else if (shot != 1){
                         cin.get();
                         cout << "gets stopped by Raycroft.\n\n";
                         cin.get();
                         }
    //Second shot
    cout << "Toronto: " << tscore << "\tCarolina: " << cscore << endl;
    shot = (rand() % 2);
    cout << "Mats Sundin, across the blueline, in on Ward, he shoots, he";
    if (shot == 1){
                  cin.get();
                  cout << "scores!\n\n";
                  tscore++;
                  }
    else if (shot != 1){
                         cin.get();
                         cout << "gets stopped by Ward.\n\n";
                         cin.get();
                         }
    //Third shot
    cout << "Toronto: " << tscore << "\tCarolina: " << cscore << endl;
    shot = (rand() % 2);
    cout << "Corey Stillman, across the blueline, in on Raycroft, he shoots, he";
    if (shot == 1){
                   cin.get();
                   cout << "scores!\n\n";
                   cscore++;
                   cin.get();                  
                   }
    else if (shot != 1){
                        cin.get();
                        cout << "gets stopped by raycroft.\n\n";
                        cin.get();
                        }
    //Forth shot
    cout << "Toronto: " << tscore << "\tCarolina: " << cscore << endl;
    shot = (rand() % 2);
    cout << "Eric Lindros, across the blueline, in on Ward, he shoots";
    if (shot == 1){
                   cin.get();
                   cout << "scores!\n\n";
                   tscore++;
                   cin.get();
                   }
    else if (shot != 1){
                        cin.get();
                        cout << "gets stopped by Ward.\n\n";
                        cin.get();
                        }
    //Fifth Shot
    cout << "Toronto: " << tscore << "\tCarolina: " << cscore << endl;
    shot = (rand() % 2);
    cout << "Doug Weight, across the blueline, in on Raycroft, he shoots, he";
    if (shot == 1){
                   cin.get();
                   cout << "scores!\n\n";
                   cscore++;
                   cin.get();
                   }    
    else if (shot != 1){
                        cin.get();
                        cout << "gets stopped by Ward.\n\n";
                        cin.get();
                        }
    //Sixth Shot
    cout << "Toronto: " << tscore << "\tCarolina: " << cscore << endl;
    shot = (rand() % 2);
    cout << "Jeff O'neal, across the blueline, in on Ward, he shoots, he";
    if (shot == 1){
                   cin.get();
                   cout << "scores!\n\n";
                   tscore++;
                   cin.get();
                   }
    else if (shot != 1){
                        cin.get();
                        cout << "gets stopped by Ward.\n\n";
                        cin.get();
                        }
    cout << "Toronto: " << tscore << " Carolina: " << cscore << endl;
    //Extra shots
    if (tscore == cscore){
                          cin.get();
                          cout << "The score is tied, we need extra shots.\n\n";
                          cin.get();
                          do {
                              //Extra shot 1
                              shot = (rand() % 2);
                              cout << "Toronto: " << tscore << "\tCarolina: " << cscore << endl;
                              cout << "Eric Stall, across the blueline, in on Raycroft, he shoots, he";
                              if (shot == 1){
                                             cin.get();
                                             cout << "scores!\n\n";
                                             cscore++;
                                             cin.get();
                                             }
                              else if (shot != 1){
                                                  cin.get();
                                                  cout << "gets stopped by Raycroft.\n\n";
                                                  cin.get();
                                                  }
                                                  //Extra shot 2
                                                  cout << "Toronto: " << tscore << "\tCarolina: " << cscore << endl;
                                                  shot = (rand() % 2);
                                                  cout << "Mats Sundin, across the blueline, in on Ward, he shoots, he";
                                   if (shot == 1){
                                                  cin.get();
                                                  cout << "scores!\n\n";
                                                  tscore++;
                                                  }
                                   else if (shot != 1){
                                                       cin.get();
                                                       cout << "gets stopped by Ward.\n\n";
                                                       cin.get();
                                                       }
                              }
                              while (tscore == cscore);
                          }
    
    if (tscore > cscore) {
                              cout << "The Toronto Maple Leafs have won the Stanley Cup!\n\n";
                              }
    else if (tscore < cscore) {
                              cout << "The Carolina Hurricanes have won the Stanley Cup!\n\n";
                              }
    system ("pause");
    return 0;
}

Also feel free to comment on my style of programming, I'm always looking to improve it.
Advertisement
The difficulty is that when you use cin.get(), the console waits for the user to press 'enter' and then adds that newline to the output. I've always used cin.get() for console pauses but never needed to use it in the middle of a sentence like you are. cin.ignore() might not exhibit this problem (but might have problems of its own), and I've heard of kbhit() but never used it myself; IIRC it's nonstandard but accepts any key. Stick around for someone more informed on these than myself.
It only takes one mistake to wake up dead the next morning.
If you don't mind a non-standard solution, your compiler PROBABLY has <conio.h> which defines int getch(); This will wait for a keypress then continue, without echoing the key to the screen. Replace cin.get() with this.

I'm not sure there is a STANDARD way to do this since the standard has to support too wide a range of potential input devices, including tele-type terminals (are they called that?) for which waiting for a keypress would make no sense.

I may be wrong about that, but I've never seen a standard way of doing it anyway.

If you are using windows, there are ways to use the API to wait for a keypress but they are not pretty in a console application.

If you want to do it with the API, the following may be of use:

#include <windows.h>HANDLE hIn;INPUT_RECORD InRec;DWORD NumRead;void WinGetchAcquire(){    hIn=GetStdHandle(STD_INPUT_HANDLE);}int WinGetch(){    while(true)        {        ReadConsoleInput(hIn,&InRec,1,&NumRead);        if(InRec.EventType==KEY_EVENT)            {            if(InRec.Event.KeyEvent.bKeyDown==0) continue;            return InRec.Event.KeyEvent.uChar.AsciiChar;            }        }    return 0; // supress warning}


That is standard as long as you are using windows. You need to call WinGetchAcquire() at the start of your program, then WinGetch() will wait for a key and return its ASCII as an int.

As well as not being non-standard, it has the advantage over <conio.h>'s getch() in that you can retrieve more information from the INPUT_RECORD structure, like shift keys pressed, extended keys etc.
Quote:Original post by Kenny77
how is this thing actually randomizing


the rand() function uses a PRNG (psuedo-random number generator), in which it takes a seed value, performs a series of operations on it, and ends up with a number different from the seed. It then uses this new value as its new seed, so that the next time you call rand(), it will have a different seed, and thus, have different (seemingly random) output.

I don't think the C++ standard specifies how rand() is to be implemented, but I believe it typically either uses a Linear congruential generator or the Mersenne twister algorithm.
It's one of the more annoying things about being a beginner at anything.

Q. How do I do X?
A. Please don't try yet.

The thing is, at its core C++ can't do that sort of thing; you need an appropriate library. conio.h is one that can do what you need, but isn't really in the standard set (although all compilers out there will have it realistically). C++ has a very limited idea of what's going on outside the program: a program using just iostream, for example, doesn't really know there *is* a console; rather, it knows about the cin and cout *streams*. It has an interface to extract data from cin and send it to cout, and doesn't know what happens from there (this is a good thing, design-wise; keeping components isolated makes it easier to fix problems, swap alternate solutions in and out etc.) At that point it's up to all kinds of OS calls to (for output; you can imagine similar processes for input, starting at the keyboard) translate the characters into patterns of pixels, figure out where the corresponding console window is and what windows might be on top of it, and ultimately write some data into video memory.

So really, you're quite limited in this interface - which is fine, because the *important* thing is learning how to deal with input, calculate stuff, and provide output. Once you have better programming practices, you can worry about fancy interfaces to the outside world.



Speaking of which. :)

The main problem with the code as it stands is the repetition. Every shot is "handled" the same way; the only thing that changes are the names. So, let's pull that out into a function, and make calls to the function.

(There will be several more posts doing additional "passes" on this code. I want to present the ideas in digestible chunks. :) )

For the first attempt, we need to identify the things that *change* for each shot. Those things will in turn indicate what parameters we need. If we look at the actual code, the differences are just in the text that is displayed. Specifically, we change:

- the shooter's name, when describing the shot
- the goalie's name, when describing the shot
- the goalie's name, when the goalie stops the shot.

Now, of course, the goalie's name doesn't change while the shot is taking place :) So there are really only two things we want to pass to the function. This highlights another redundancy, an internal one :)

I'll fix and comment on a couple of other minor things in here while implementing that. I'm going to define the function above main(), without a forward declaration. This is the style I recommend for free (i.e. non-member) functions. By trying to avoid forward declarations, you will be forced to use them exactly when you need them, i.e. when there is a circular dependency between functions. Therefore, doing things that way highlights the circular dependencies, and saves on another kind of redundancy (typing the function signature twice) :)

There's one other detail: the teams' scores need to be preserved between function calls, so it seems like they ought to stay in main, but we still need a way to change them. We could change the score directly if we passed it by reference, but instead I'm going to return a boolean value to indicate whether the shot was successful, and update the scores in the main loop.

#include <iostream>#include <ctime>// Neither of those actually provides the rand() function directly; // so we should also have:#include <cstdlib>using namespace std;// I'm reindenting stuff with my usual bracing style. Yours is just fine,// so don't worry about that. I'm just insecure like that :)// So here's our function to take a shot:bool handle_shot(const char* shooter_name, const char* goalie_name) {  // Any time you output something to cout, you should make sure the buffer  // is flushed. You can output 'flush', which just flushes the buffer, or  // 'endl', which outputs a newline and then flushes.  cout << shooter_name << ", across the blueline, in on "       << goalie_name << ", he shoots, he" << flush;  // We always cin.get() before and after, so I pull that out of the  // if-condition. You shouldn't pretend that it's conditional on something  // if it isn't :)  cin.get();  // Because the function will do something (cin.get()) between calculating  // calculating the result and returning, I will store the result in  // a variable. We'll calculate the result now, and use it to initialize  // the variable (good practice: when you know where the first value of a  // variable will come from, and it's possible to initialize that way, do so.)  bool result = (rand() % 2) == 1;  // Don't compare boolean values to 'true' or 'false'. Just use them as is.  // It sounds more natural this way.  if (result) {    cout << "scores!\n" << endl;    // In general, you shouldn't put an 'else if' where the condition is the    // opposite of the 'if'. Saying 'else' is easier and also makes it clear    // that you handle all cases.  } else {    cout << "gets stopped by " << goalie_name << ".\n" << endl;  }  cin.get();  return result;}int main() {  // Only tscore and score are left in main().  // We will declare them close to their first use, and initialize them to  // their initial zero values, rather than assigning that value right away.  cout << "Live From The Air Canada Center\n";  cout << "Stanley Cup Finals - Game 7\n";  cout << "Toronto-3  Carolina-3\n";  cout << "Overtime - Shootout\n\n";  cout << "Shootout lineups:\n";  cout << "Toronto:\tCarolina:\n";  cout << "Mats Sundin\tEric Stall\n";  cout << "Eric Lindros\tCorey Stillman\n";  cout << "Jeff O'neill\tDoug Weight\n\n";      srand(time(NULL));  int tscore = 0, cscore = 0;  //First shot  cout << "Toronto: " << tscore << "\tCarolina: " << cscore << endl;  if (handle_shot("Eric Stall", "Raycroft")) { cscore++; }  //Second shot  cout << "Toronto: " << tscore << "\tCarolina: " << cscore << endl;  if (handle_shot("Mats Sundin", "Ward")) { tscore++; }  //Third shot  cout << "Toronto: " << tscore << "\tCarolina: " << cscore << endl;  if (handle_shot("Corey Stillman", "Raycroft")) { cscore++; }   //Fourth shot  cout << "Toronto: " << tscore << "\tCarolina: " << cscore << endl;  if (handle_shot("Eric Lindros", "Ward")) { tscore++; }  //Fifth Shot  cout << "Toronto: " << tscore << "\tCarolina: " << cscore << endl;  if (handle_shot("Doug Weight", "Raycroft")) { cscore++; }  //Sixth Shot  cout << "Toronto: " << tscore << "\tCarolina: " << cscore << endl;  if (handle_shot("Jeff O'neal", "Ward")) { tscore++; }  cout << "Toronto: " << tscore << " Carolina: " << cscore << endl;  //Extra shots  if (tscore == cscore) {    cin.get();    cout << "The score is tied, we need extra shots.\n\n";    cin.get();    do {      //Extra shot for Carolina      cout << "Toronto: " << tscore << "\tCarolina: " << cscore << endl;      if (handle_shot("Eric Stall", "Raycroft")) { cscore++; }            //Extra shot for Toronto      cout << "Toronto: " << tscore << "\tCarolina: " << cscore << endl;      if (handle_shot("Mats Sundin", "Ward")) { tscore++; }    } while (tscore == cscore);  }  if (tscore > cscore) {    cout << "The Toronto Maple Leafs have won the Stanley Cup!\n\n";  } else {    // Again, if we didn't have tscore > cscore, then we must have     // tscore < cscore, otherwise we would have looped again. So just write the    // else. :)    cout << "The Carolina Hurricanes have won the Stanley Cup!\n\n";  }  // I strongly recommend not pausing your programs artificially at the end.  // Instead, make sure you run them properly from a command line. You can  // facilitate that with a simple batch script.  // As a special case, you don't need to return 0 at the end of main. In case  // you couldn't tell by now, I'm in favour of not typing things that aren't  // needed and also don't really tell you anything. :)}


Now, if we look at this from a distance, we can see there's still a lot of repetition going on. I'll be addressing that in subsequent passes. However, a big improvement has been made.

By eliminating redundancy from your code, you can:

- get shorter code that's less painful to look at - you don't have the feeling "I needed to write *all that* to do this simple task?" :)
- get shorter code that keeps relevant parts of the system closer together, rather than being spaced out by repetitive fluff.
- Avoid opportunities for cut-and-paste errors (or even retyping errors - yes, sometimes people retype huge sections of code to change some small thing, and end up with lots of other typos. Sometimes they'll be caught by the compiler; sometimes they're typos in output text that then becomes inconsistent; sometimes one variable name turns into another one and all hell breaks loose...).
- Express clearly what the "task" is (by extracting it and giving it a name), and the intent to perform it multiple times and/or make use of the result multiple times (by using an appropriate construct rather than repeating the stuff and making the user count it up). This is most obvious for the example of moving whole sections of code into a function, but it also applies to manual "common subexpression elimination" (looking for common bits of long foo[bar]->baz() type nonsense, and putting the common parts into temporaries with a meaningful name rather than typing out the weird stuff for each statement).

My experience, in fact, has been that seeking out and destroying all forms of rendundancy (along with just plain unnecessary stuff - that's a degenerate case of "you have X copies of something and you only need N", where N < X) is the single biggest driving force behind cleaning up code (refactoring, though I usually use the term more loosely than I ought to). Even when the problem is something like an abstraction that's missing from the system, usually it will produce some kind of *structural* redundancy as a symptom (see for example).

In subsequent posts I'll be showing how to remove the rest of the redundancy... [smile]
Quote:Original post by Driv3MeFar
I don't think the C++ standard specifies how rand() is to be implemented,


It doesn't, as far as I can tell. The only real requirement is that given the same seed, to the same implementation, the same stream of values will result.

Quote:but I believe it typically either uses a Linear congruential generator or the Mersenne twister algorithm.


I highly doubt (but would be pleasantly surprised) if any popular compiler vendors provided an MT-based rand(). However, incidentally, the Windows implementation of Python (which is de facto normative) does use the MT for its random module. :)
OK, time for pass 2. :)

Last time, we got rid of a lot of the content redundancy, by extracting the process of handling a shot into a function and calling the function several times. This lets us look at more of the task at once, highlighting the underlying *structural* redundancy. That is, we still make several calls to a function, each of which results in a similar-looking pair of lines: one to output the score, and one to call the function.

You might wonder, incidentally, why I didn't put the score-outputting into the handle_shot function. This was for two reasons. First, it's not logically part of the process of handling a shot, and removing redundancy isn't the only reason for doing this stuff - you also want to create meaningful abstractions that do what they sound like they do. And second, the score values "belong" to the main loop. While there are workarounds (and indeed, this time we will implement one of them), the communication of score values back to the main loop would complicate things.

Now then. Let's analyze how the calls are made. Normally, this is part of my design process, which results in writing better code the first time around :) However, you'll never get things right the first time regardless, so design time gets diminishing returns; and anyway, the process is almost the same after-the-fact of writing some code, as long as you do it reasonably soon. (On a large project, you should be thinking of doing it continually as you add functionality.)

We see that all of the calls are logically paired: a shot for Carolina, then a shot for Toronto. This process is repeated three times, once for each pair of shooters, and then done iteratively in the "extra shots" if needed. So we can imagine extracting a function to handle a "round" of the shootout. This needs to know which shooters to use, and since scores will change during each round, we will want to communicate the score changes back to the main loop.

Now, the handle_round will operate something like this:
- Display score.- Handle Carolina's shot.- Display score.- Handle Toronto's shot.

And well, the shots for the two teams work in the same way, so what we *really* want is:
- For each team:  - Display score.  - Handle *the current team's* shot.


The natural way to handle this is to make a for loop, and use the iteration variable to determine all of the things that need to change in order to distinguish a Toronto shot from a Carolina shot. These things are: the text arguments for the function call, and which score value to increment if a goal is scored. To "determine" what to use from the iteration variable - this is the key! - we will use the variable to index into an array of options.

So now we know we need to build several arrays. We'll make some constant arrays with char*'s to hold all the player and team names. There are two goalie names that fit in an array [2]; similarly the team "short" and "long" names (the latter being what we use in the final output when we announce the winner). The shooters' names can go in a 2-dimensional array: we'll index it first by team, then by "position" in the shootout. Finally, we'll replace the main function's tscore and cscore with an array of 2 scores. We'll pass that to the handle_round function (as a pointer), which will index into it and increment according to which team scored. For organizational purposes, we'll keep the same "sense" for each array: index 0 represents Toronto (and its players and goalies), and index 1 represents Carolina similarly.

From handle_round(), we need to call handle_shot() twice in a loop. We'll loop over the team indices (0 and 1), which identify the "goalie's team" each time (since Toronto receives a shot first). Each time, the *other* index needs to be calculated, representing the "shooter's team". We index into the shooters' names array by shooter's team and then shootout position (so that has to be a parameter to the function), and index into the goalies' names array by goalie's team, and that gives us the parameters for the handle_shot() call.

Meanwhile, we can decompose the score-reporting process in a similar way: within each shot-making loop, we loop to display two scores: Toronto's, and then Carolina's. For each of those, we can make use of the score array and the team name array. Note that previously we put a tab after Toronto's score (i.e. between the two scores) but not after Carolina's. We could use a simple if-condition to suppress the tab after Carolina's score in the new structure, but I will leave it alone, simply because the trailing tab isn't visible ;)

Finally, we can look at the rest of the code and make similar substitutions of array contents for pieces of what used to be constant strings. This presents more opportunities for looping. For example, when announcing the starting lineup, we can iterate over teams and shooters again, displaying them similarly to how we displayed the scores.

That gives us something like:
#include <iostream>#include <ctime>#include <cstdlib>using namespace std;// The "symbol tables".const char* teamnames[2] = {  "Toronto", "Carolina"};const char* team_full_names[2] = {  "Toronto Maple Leafs", "Carolina Hurricanes"};const char* goalie_names[2] = {  "Raycroft", "Ward"};const char* shooter_names[2][3] = {  { "Mats Sundin", "Eric Lindros", "Jeff O'Neill" },  { "Eric Stall", "Corey Stillman", "Doug Weight" }};  bool handle_shot(const char* shooter_name, const char* goalie_name) {  cout << shooter_name << ", across the blueline, in on "       << goalie_name << ", he shoots, he" << flush;  cin.get();  bool result = (rand() % 2) == 1;  if (result) {    cout << "scores!\n" << endl;  } else {    cout << "gets stopped by " << goalie_name << ".\n" << endl;  }  cin.get();  return result;}void handle_round(int shooter, int* scores) {  for (int goalie_team = 0; goalie_team < 2; ++goalie_team) {    // Whichever team is goaltending, the other team is shooting.    // There are a few ways to express this; I like this one    int shooter_team = goalie_team == 0 ? 1 : 0;    // Display the current score    for (int score_team = 0; score_team < 2; ++score_team) {      cout << teamnames[score_team] << ": " << scores[score_team] << "\t";    }    cout << endl;    // Handle the shot    if (handle_shot(shooter_names[shooter_team][shooter],                     goalie_names[goalie_team])) {       scores[shooter_team]++;     }  }}int main() {  cout << "Live From The Air Canada Center\n"       << "Stanley Cup Finals - Game 7\n"       << teamnames[0] << "-3 " << teamnames[1] << "-3\n"       << "Overtime - Shootout\n\n"       << "Shootout lineups:\n"       << teamnames[0] << ":\t" << teamnames[1] << ":\n";  for (int shooter = 0; shooter < 3; ++shooter) {    for (int team = 0; team < 2; ++team) {      cout << shooter_names[team][shooter] << "\t";    }    cout << "\n";  }  cout << endl; // last \n and flush.      srand(time(NULL));  int scores[2];  for (int shooter = 0; shooter < 3; ++shooter) {    handle_round(shooter, scores);  }  // Extra shots  if (scores[0] == scores[1]) {    cin.get();    cout << "The score is tied, we need extra shots.\n\n";    cin.get();    do {      // FIXME: do we really want the same shooter taking all extra shots?      // Notice how, with the new structure, it's much easier to change that :)      handle_round(0, scores);    } while (scores[0] == scores[1]);  }    // Report the winner  int winning_team = (scores[0] > scores[1]) ? 0 : 1;  cout << "The " << team_full_names[winning_team]        << " have won the Stanley Cup!\n" << endl;}




Some readers will have undoubtedly noticed that I'm using char*'s for text in the above and previous code exclusively, rather in violation of my own sig advice ;) The thing to note here is the limited use of the text in question: we don't try to modify the text, get properties of it or even index into it for non-modifying reasons: we just treat them as "symbols" that can be inserted into output streams. Also, we know the text (and its length in particular) ahead of time, so there's no need to worry about the buffer sizes and so on. The compiler will take care of that nonsense for us, by its usual processing of string literals.

The use of char*'s for these reasons is IMHO fine and in fact *preferred*, because by using them, you "contract" that you will be making limited use of them, and thus any invalid uses stick out. After all, noone would advocate that you automatically type std::string("foo") instead of just "foo" universally. :) The std::string adds memory overhead, but more importantly adds flexibility that you don't *want* (to say nothing of "need"). Just please, mark these things 'const': they *should* be const (you're not necessarily able to write to a string literal), but aren't automatically, for C backwards compatibility reasons.

However: now that the code's design is improving, the inflexibility is beginning to show. There's not much reason to hard-code these names, or even the number of shooters per team (although we probably should hard-code that there are two teams; otherwise we need some weird logic to figure out who receives whose shot :S ). After all, other sports do shootouts like this, too. Consider soccer (football ;) ) for example: there are five shots before extra shots, and eleven people to rotate to take shots, and you're not playing for the Stanley Cup ;), but the overall process is really the same.

In the next pass, I'll show how to read the relevant text in from files, *and* how to use C++'s object-orientation facilities to create a stronger "team" abstraction. After all, well-done code provides abstractions for "things" as well as for "actions". At this point, we *will* be using std::strings for text, because the text isn't known ahead of time and we don't want to impose artificial restrictions on length. But before we get to the code for that, we'll need a primer on file format design...
Okay, time for some file format work.

There are two things we need to think about there: what kind of things are "data" to our program (as opposed to the essentials of the "code", i.e. what the program *is*), and how to represent that data in a way that will be easy to handle in code.

For the first, what we need to do is think more generally about the concept of a "shootout". It has certain rules (teams alternate taking shots), but a lot of things are not specified (player names, or even what *sport* is being played: they do shootouts in soccer, too; and there's no good reason why we can't support such a thing with the same program). For the second, we need to know a little about the available tools.

First, deciding what info we need. The preamble text looks like a good candidate: it would change for any given shootout situation. Similarly, the player and team names. Notice that although in our case, the "short" team name is a part of the "long" team name - specifically at the beginning of it - that might not always be the case. Anyway, the pattern here is that what we want to extract is all of the *text items*. This is common enough for data files. Sometimes there are important numbers you'll want to extract and make "configurable" using the text file, but right now we have none of those. (We can always add stuff later if we discover a need.)

Indeed, we can pull out basically all of the string literals. We'll leave little things like a couple of "\n"s in place, but the rest has got to go into the file. Even things that we wouldn't expect to change, like "Shootout lineup:" - by pulling that out, we get the ability (at least theoretically) to translate to a different language by just changing the text file, and not having to recompile the program. (Of course, in practice, things are much more complicated than that when you are talking about translation :) But the general idea still holds for, say, changing the sport. We don't want FIFA to be handing out the Stanley Cup.)

So we extract the text and dump it into a 'sample' data file, which we'll reorganize later. Just to start out with, we'll put each logical "text item" on a separate line, so that we can see what we have. All the "code" stuff like 'cout' and the operator overloads and the double-quoting of strings (and escaping of quote characters) gets removed; we're working with actual text now. We take out \n's, assuming that they will be supplied by the code.

But there are still some problems here: some of the "text items" have stuff substituted into them at run-time - phrases and variables get inter-mixed by the output statements. We actually should not just make a separate item for each "phrase" in the line; instead, we will write the entire thing as one text item, using some kind of placeholder text for the things that need to be substituted. That means we will be implementing our own sort of escape sequences :)

I have chosen a simple scheme, representing the place-held items as $1, $2 etc. At runtime, we will look for these items in the string, and replace them with items 0, 1 etc. correspondingly of some sequence of substitution values. This scheme lets us put the same text in multiple times without making things any harder for the calling code (just repeat the placeholder), and is also an asset for localization (here that includes language translation, change-of-sport etc.) because we can re-order things in data rather than code.

What if we need to write an actual dollar amount in the file? Simple: we'll use $$ to represent an actual dollar sign (even though that functionality won't be used, I'll implement it). So for example, if we had a number converted to text that represented a player's salary, we might insert it into a string with "$$$1", the first $$ creating the actual dollar sign, and the next $1 standing for the interpolated value.

So, anyway, we get a list of strings like this:
TorontoCarolinaToronto Maple LeafsCarolina HurricanesRaycroftWardMats SundinEric LindrosJeff O'NeillEric StallCorey StillmanDoug Weight$1, across the blueline, in on $2, he shoots, hescores!gets stopped by $1.Live From The Air Canada CenterStanley Cup Finals - Game 7$1-3 $2-3Overtime - ShootoutShootout lineups:The score is tied, we need extra shots.The $1 have won the Stanley Cup!


Finally, we need to reorder this in a more convenient way. We note that the listing of the "preamble" text lost some information: we want to put an extra blank line before "Shootout lineups:", and don't want that hard-coded. Really, the preamble is all one "item", even though it spans multiple lines and might have blank lines in the middle.

Meanwhile, it would be nice to see the teams side-by-side. This has the additional benefit of highlighting any mismatch in the number of players on each team. We can do this by putting related items for each team on the same line, using tabs to separate them (so they line up). We didn't record anywhere how many shooters there are per team (and indeed we shouldn't hard-code that). If we can somehow determine where the list of players ends, though, then we can just count them up as we read them, and store them appropriately in a resizable container (just accept this for now; we'll get to implementation details in a bit). No player has a blank name, so we can just use a blank line to separate the "team information section" from the rest of the file.

Our data logically has three sections: the "team information", the "preamble", and miscellaneous one-liner text. The preamble needs to span several lines, and there's no good way of marking where it ends explicitly: it can contain blank lines, and we don't want to put other artificial restrictions on what it could contain. So we'll put that section last; then its end is marked implicitly by the end-of-file. We'll put the team information first, for aesthetic reasons. The remaining text items can go in the middle, and there is no problem handling them, since we know how many of them there are. To simplify the data format, we'll mandate the order in which they appear, and assume they are in that order when we read the file. All we need to do is put a blank line after the team information. We don't need one after the miscellaneous text because we know how many things will be there (and might want to start the preamble off with blank lines for some reason o_O).

So we have a revised file format:

Toronto			CarolinaToronto Maple Leafs	Carolina HurricanesRaycroft		WardMats Sundin		Eric StallEric Lindros		Corey StillmanJeff O'Neill		Doug Weight$1, across the blueline, in on $2, he shoots, hescores!gets stopped by $1.The score is tied, we need extra shots.The $1 have won the Stanley Cup!Live From The Air Canada CenterStanley Cup Finals - Game 7$1-3 $2-3Overtime - ShootoutShootout lineups:


Our format is basically specified by example.
Now comes the second part: verifying that we can deal with this data.

Our basic algorithm will have to do like this:
- Read short team names.- Read long team names.- Read goalie names.- Until a blank line is seen:  - Read pairs of player names.- Skip blank line.- Read next line as "shot in progress" text.- Read next line as "shot successful" text.- Read next line as "shot failed" text.- Read next line as "score still tied" text.- Read next line as "announce winner" text.- Read the rest of the file, and store it as the "preamble" text.


To read our text items, we will make use of the standard library class std::ifstream, storing our text in std::strings.
To read the lines with tabs on them, we can make use of the free function std::getline(), which reads from a stream into a string, until a delimiter (which defaults to newline, but we will use tab) is found. We might have several tabs in the middle of a line to make things line up, so if we just iteratively getline(), we'll see extra 'blank' entries on each line. However, we can just skip over these, knowing that there are no blank names. In order to handle the fact that lines end with a newline rather than tab, and to allow for a little better error checking, we will read actual lines into a string with the default std::getline, and then re-parse the line by constructing a std::stringstream from the string data. As we read player names, we'll append (push_back) them to std::vectors, which are resizable containers.
To read the other single lines, we can just use std::getline as before - nothing complicated.
Finally, to read "to the end of file" for the preamble, there are a few approaches. Most obviously, we could keep reading lines, and appending the text (along with intermediate newline characters, because getline() does not store the delimiter character into the string) to a buffer string. This is probably not the best way, but it will do for educational purposes.

We're about to add a lot of new stuff, so buckle your seatbelt. In the next post, I'll just present the revised code first, and then analyze it in detail. I n that post, there will be no implementation of the string-substitution stuff; we'll forget about the subbed-in values for a moment, and just output strings with the dollar-signs in them. This is so as not to overwhelm ;)
What the hell - Wow! Extract this and put it elsewhere. The origninal poster is gone. Lest all this have been as naught, convert this into somekind of tutorial where beginner code is evolved to accomplished. This would have an added benefit of an easily readable format, forums make for hard enlonged technical reading. As well, others may benefit from this. Elsewise this work will simply disappear buried and seen by but a handful, to be forgotten. A truly brilliant and noble but in the end, completely wasted effort.
Thanks. :) I have the thread tracked, and intend to save the posts locally for later reworkage when I'm done. :)

This topic is closed to new replies.

Advertisement