This particular page contains one of the examples from a chapter that reinforces material on functions and arrays by providing a number of relatively sophisticated examples. This example is a "hangman" game. The game utilizes a "curses" style (cursor addressable) screen display that works on both Borland (Intel) and Symantec (Macintosh) development environments.
In this version, the program takes the role of player one. The program contains a large word list from which it selects a word for the current round. It generates a display showing the number of letters, then loops processing letters entered by the user until either the word is guessed or the cartoon is complete. This version of the program is to use the curses functions, and to produce a display like that shown in Figure 12.6.
+----------------------------------------------------------------------------+ | | | | | gavotte | | ======== | | # / | | | # / | | | #/ |@ | | # --H--- | | # - H | | # H | | # H | | # | | | | # | | | | # | | | | # | | # | | ##### | | | +----------------------------------------------------------------------------+ You lost?
The words used by the program might be held in a data file. The program would open and read this file at each round, picking "at random" one of the words in the file. However, that scheme has a couple of disadvantage. The program and vocabulary file have to be kept together. When you think how programs get shuffled around different disks, you will see that depending on a data file in the same directory isn't wise. Also, the scheme makes it rather difficult to "pick at random"; until you've read the file you don't know how many choices you've got. So, you can't pick a random number and read that many words from the file and then take the next as the word to use in a round. The words would have to be read into an array, and then one could be picked randomly from the array.
If the words are all going to have to be in an array anyway, we might as well have them as an initialized array that gets "compiled" and linked in with the code. This way avoids problems about the word file getting lost. It seems best to keep this separate from the code, it is easier to edit small files and a long word list would get in the way. So we could have one file "vocab.cp" that contains little more than an initialized array with some common English words, and a compiler set count defining the number of entries in the array.
What are "words"? They small character arrays; like the UT_Word typedef in the last example. In fact we might as well use the UT.h header file with the definition.
Thus the program is going to be built of a number of parts:
This file will contain the main() function and all the other functions defined for the program.
The file with the words.
The header file with the typedef defining a "word" as a character array.
These files contain the curses cursor graphics package.
initialize random number generator and cursor graphics loop pick a word show word as "****" giving indication of length loop get user to guess character check word, where character matches; change displayed word to show matches e.g. "**a*" if didn't get any match draw one more part of cartoon check for loop termination report final win/loss status ask if another game, tidy upGiven the relative complexity of the code, this is going to require breaking down into many separate functions. Further, the implementation should be phased with parts of the code made to work while before other parts get implemented.
The main line shown above is too complex. It should be simplified by "abstracting out" all details of the inner loop that plays the game. This reduces it to:
main() Initialize() do PlayGame() while AnotherGame() TidyUp()
This structure should be elaborated with a "reduced" version of PlayGame(). The reduced version would just pick a random word, show it a "***", wait for a few second, and then show the actual word. This reduced version allows the basic framework of the program to be completed and tested.
Following these decisions, the preliminary design for the code becomes:
AnotherGame() prompt user for a yes/no reply return true if user enters 'y' PlayGame() get curses window displayed pick random word display word as "****" at some point in window !!!delay !!!display actual wordThe two steps marked as !!! are only for this partial implementation and will be replaced later.
Function AnotherGame() can be coded using the CG functions like CG_Prompt() and CG_GetChar(), so it doesn't require further decomposition into simpler functions. However, a number of additional functions will be required to implement even the partial PlayGame().
These functions obviously include a "display word" function and a "pick random word" function. There is also the issue of how the program should record the details of the word to be guessed and the current state of the users guess.
One approach would be to use two "words" - gGuess and gWord, and a couple of integer counters. The "pick random word" function could select a word from the vocabulary, record its length in one of the integer counters, copy the chose word into gWord and fill gGuess with the right number of '*'s. The second integer counter would record the number of characters matched. This could be used later when implementing code to check whether the word has been guessed completely.
So, we seem to have:
PickRandomWord pick random number i in range 0 ... N-1 where N is number of word in vocab record length of vocab[i] in length copy vocab[i] into gWord fill gGuess with '*'s ShowGuess move cursor to suitable point on screen copy characters from gGuess to screen ShowWord move cursor to suitable point on screen copy characters from gWord to screen
The functions identified at this point are sufficiently simple that coding should be straightforward. So, the design of this part can be finished off by resolving outstanding issues of data organization and deciding on function prototypes.
The data consist of the four variables (two words and two integer counters) that can be made globals. If these data are global, then the prototypes for the functions identified so far are:
int AnotherGame(void); void Initialize(void); void PickWord(void); void PlayGame(void); void ShowGuess(void); void ShowWord(void); int main()
The code in the main file Hangm.cp needs to reference the vocabulary array and word count that are defined in the separate vocab.cp file. This is achieved by having "external declarations" in Hangm.cp. An external declaration specifies the type and name of a variable; it is basically a statement to the compiler "This variable is defined in some other file. Generate code using it. Leave it to the linking loader to find the variables and fill in the correct addresses in the generated code."
The file Hangm.cp will start with the #includes of the standard header files and definitions of global variables. The program needs to use random numbers; stdlib provides the rand() and srand() functions. As explained in 10.10, a sensible way of "seeding" the random number generator is to use a value from the system's clock. The clock functions vary between IDE's. On Symantec, the easiest function to use is TickCount() whose prototype is in events.h; in the Borland system, either the function time() or clock() might be used, their prototypes are in time.h. The header ctype is #included although it isn't required in the simplified program.
#includeHere are the extern declarations naming the variables defined in the separate vocab.cp file:#include #include // change events.h to time.h for Borland #include #include "CG.h" #include "UT.h"
extern UT_Word vocab[]; extern int numwords;and these are the global variables:
UT_Word gGuess; UT_Word gWord; int gLength; int gMatched;
The functions Initialize(), AnotherGame(), ShowGuess() and ShowWord() are all simple:
void Initialize(void) { CG_Initialize(); // change to srand(time(NULL)); on Borland srand(TickCount()); } int AnotherGame(void) { CG_Prompt("Another Game?"); char ch = CG_GetChar(); CG_Prompt(" "); return ((ch == 'y') || (ch == 'Y')); } void ShowGuess(void) { const xGuess = 6; const yGuess = 4; CG_MoveCursor(xGuess, yGuess); for(int i=0;i< gLength; i++) CG_PutCharacter(gGuess[i]); } void ShowWord(void) { const xGuess = 6; const yGuess = 4; CG_MoveCursor(xGuess, yGuess); for(int i=0;i< gLength; i++) CG_PutCharacter(gWord[i]); }
Function PickWord() uses the random number generator to pick a number that is converted to an appropriate range by taking its value modulo the number of possible words. The chosen word is then copied and the guess word is filled with '*'s as required.
void PickWord(void) { int choice = rand() % numwords; gLength = strlen(vocab[choice]); for(int i = 0; i < gLength; i++) { gWord[i] = vocab[choice][i]; gGuess[i] = '*'; } gWord[gLength] = gGuess[gLength] = '\0'; gMatched = 0; }
Function PlayGame() is supposed to look after a complete game. Of course in this simplified version, it just refreshes the curses window, picks a word, shows it as stars, waits, and shows the word:
void PlayGame(void) { CG_FrameWindow(); PickWord(); ShowGuess(); CG_Delay(2); ShowWord(); return; }
Function main() is complete, it shouldn't need to be changed later. The "Tidy()" routine postulated in the original design has collapsed into the call to reset the curses system, there didn't seem to be any other tidying to do so no need for a separate function.
int main() { Initialize(); do { PlayGame(); } while( AnotherGame()); CG_Reset(); return 0; }
The other source file is vocab.cp. This should have definitions of the words and the number of words. This file needs to #include UT.h to get the declaration of type UT_Word. The array vocab[] should contain a reasonable number of common words from a standard dictionary such as the Oxford English Dictionary:
#include "UT.h" UT_Word vocab[] = { "vireo", "inoculum", "ossuary", ... "thurible", "jellaba", "whimbrel", "gavotte", "clearcole", "theandric" }; int numwords = sizeof(vocab)/ sizeof(UT_Word);Although the vocabulary and number of words are effectively constant, they should not be defined as const. In C++, const carries the implication of filescope and the linking loader might not be able to match these names with the external declarations in the Hangm.cp file.
The code as shown should be executable. It allows testing of the basic structure and verifies that words are being picked randomly.
The PlayGame() function needs to be expanded to contain a loop in which the user enters a character, the character is checked to determine whether any additional letters have been matched, and counts and displays are updated.
This loop should terminate when all letters have been guessed. After the loop finishes a "You won" message can be displayed.
The '*'s in the gGuess word can be changed to appropriate characters as they are matched. This will make it easy to display a partially matched word as the ShowGuess() function can be called after each change is complete.
Some additional functions are needed. Function GetGuessedCharacter() should prompt the user for a character and get input. If the character is a letter, it should be converted to lower case and returned. Otherwise GetGuessedCharacter() should just loop, repeating the prompt. Function CheckCharacter() should compare the character with each letter in the gWord word; if they match, the character should overwrite the '*' in gGuess and a count of matches should be incremented. There will also be a ShowResult() function that can display a message and cause a short pause.
GetGuessed charater loop prompt for character get input if letter convert to lower case and return CheckCharacter ch count = 0; for each letter in gWord if letter == ch count++ set character in gGuess Show Result display message delay a couple of seconds PlayGame() CG_FrameWindow() PickWord() ShowGuess() gameover = false while not gameover Get guessed character matched = CheckCharacter if(matched > 0) ShowGuess gMatched += matched gameover = (gMatched == gLength) Show Result "You win"
The prototypes for the additional functions are:
int CheckChar(char ch); char GetGuessedChar(); void ShowResult(const char msg[]);
char GetGuessedChar() { CG_Prompt(" "); CG_Prompt(">"); char ch; do ch = CG_GetChar(); while (!isalpha(ch)); ch = tolower(ch); return ch; } int CheckChar(char ch) { int count = 0; for(int i = 0; i < gLength; i++) if((gWord[i] == ch) && (gGuess[i] == '*')) { gGuess[i] = ch; count++; } return count; } void ShowResult(const char msg[]) { CG_Prompt(msg); CG_Delay(5); }
The while loop in PlayGame() terminates when gameOverMan is true. Currently, there is only one way that the game will be over - all the letters will have been guessed. But we know that the implementation of the next phase will add other conditions that could mean that the game was over.
void PlayGame(void) { CG_FrameWindow(); PickWord(); ShowGuess(); int count = 0; int gameOverMan = 0; while(!gameOverMan) { char ch = GetGuessedChar(); int matched = CheckChar(ch); if(matched > 0) { ShowGuess(); gMatched += matched; } gameOverMan = (gMatched == gLength); } ShowResult("You won"); return; }
Again, this code is executable and a slightly larger part of the program can be tested.
The cartoon is made up out of parts: the frame, the horizontal beam, a support, the rope, a head, a body, left and right arms, and left and right legs. Each of these ten parts is to be drawn in turn.
The PlayGame() function can keep track of the number of incorrect guesses; the same information identifies the next part to be drawn. Selection of the parts can be left to an auxiliary routine - "show cartoon part".
Each part of the cartoon is made up of a number of squares that must be filled in with a particular character. The simplest approach would appear to be to have a little "fill squares" function that gets passed arrays with x, y coordinates, the number of points and the character. This routine could loop using CG_PutCharacter() to display a character at each of the positions defined in the arrays given as arguments.
Details of the individual parts are best looked after by separate routines that have their own local arrays defining coordinate data etc.
Thus, this phase of the development will have to deal with the following functions:
Fill squares loop move cursor to position of next point defined by x, y argument arrays output character Show Frame call Fill squares passing that function arrays defining the points that make up the frame Show Head call Fill squares passing that function arrays defining the points that make up the head ... Show Cartoon part (partnumber) switch on partnumber call show frame, or show beam, or ... as appropriate
Function PlayGame() requires some further extension. If a character didn't match, the next cartoon part should be drawn and the count of errors should be increased. There is an additional loop termination condition - error count exceeding limit. The final display of the result should distinguish wins from losses.
PlayGame() CG_FrameWindow() PickWord() ShowGuess() gameover = false count = 0; while not gameover Get guessed character matched = CheckCharacter if(matched > 0) ShowGuess gMatched += matched else ShowCartoonPart count++ gameover = (gMatched == gLength) || (count > limit) if (gMatched== gLength)Show Result "You win" else ShowWord Show Result "You lost"
void FillSquares(char fill, int num, int x[], int y[]) { for(int i = 0; i < num; i++) { CG_MoveCursor(x[i],y[i]); CG_PutCharacter(fill); } } void ShowBeam(void) { int x[] = { 51, 52, 53, 54, 55, 56, 57, 58 }; int y[] = { 5, 5, 5, 5, 5, 5, 5, 5 }; int n = sizeof(x) / sizeof(int); FillSquares('=', n, x, y); } void ShowHead(void) { int x[] = { 59 }; int y[] = { 8 }; int n = sizeof(x) / sizeof(int); FillSquares('@', n, x, y); } void ShowCartoonPart(int partnum) { switch(partnum) { case 0: ShowFrame(); break; case 1: ShowBeam(); break; case 2: ShowSupport(); break; ... ... case 9: ShowRightLeg(); break; } }
The final version of PlayGame() is:
void PlayGame(void) { CG_FrameWindow(); PickWord(); ShowGuess(); int count = 0; int gameOverMan = 0; while(!gameOverMan) { char ch = GetGuessedChar(); int matched = CheckChar(ch); if(matched > 0) { ShowGuess(); gMatched += matched; } else { ShowCartoonPart(count); count++; } gameOverMan = (count >= kNMOVES) || (gMatched == gLength); } if(gMatched==gLength) ShowResult("You won"); else { ShowWord(); ShowResult("You lost"); } return; }(The constant kNMOVES = 10 gets added to the other constants at the start of the file.) Phased implementation strategy
You will find that most of the larger programs that you must write will need to be implemented in phases as was done in this example.