[N.A.B.G. picture] [ABC cover]


This page contains text from the draft version of a book "A Beginners' C++"; this text is intended for "CS1, CS2" introductory Computer Science courses that use C++ as an implementation language.

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.


12: Examples with Functions and Arrays

12.5 Hangman

You must know the rules of this one. One player selects a word and indicates the number of letters, the other player guesses letters. When the second player guesses a letter that is in the word, the first player indicates all occurrences of the letter. If the second player guesses wrongly, the first player adds a little more to a cartoon of a corpse hung from a gibbet. The second player wins if all letters in the word are guessed. The first player wins if the cartoon is completed while some letters are still not matched.

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?

Figure 12.6The curses based implementation of the Hangman game

Specification
Implement the hangman program; use the curses package for display and interaction.
Design
This program involves some higher level design decisions than earlier examples where we could start by thinking how the code might work. Here, we have to make some other decisions first; decisions about how to organize the collection of words used by the program, and how to organize the program into files. (This is partly a consequence of the looser specification "implement hangman"; but, generally, as programs get more elaborate you do tend to have to consider more general issues in the design process.)

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:

First iteration through design of code
The program will be something like:
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 up
Given 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.

Second iteration, design of a simplified program

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 word
The 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
Final design iteration for simplified program

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()
External declarations

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."

Implementation of simplified program
The project has include the files Hangm.cp (main program etc), vocab.cp (array with words), and CG.cp (the curses functions); the header files CG.h and UT.h are also needed in the same directory (UT.h is only being used for the definition of the UT_Word type).

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.

#include 
#include 
#include 
// change events.h to time.h for Borland
#include 
#include "CG.h"
#include "UT.h"
Here are the extern declarations naming the variables defined in the separate vocab.cp file:
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.

Designing the code to handle the user's guesses
The next step of a phased implementation would be to handle the user's guesses (without exacting any penalties for erroneous guesses).

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[]);
Implementation of code for user input
The code implementing these additional and modified function functions is straight-forward. Function GetGuessedCharacter() uses a do ... while loop to get an alphabetic character. Function CheckChar() uses a for loop to check the match of letters.
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.

Designing the code to handle incorrect guesses
The final phase of the implementation would deal with the programs handling of incorrect guesses. Successive incorrect guesses result in display of successive components of the cartoon of the hung man. The game ends if the cartoon is completed.

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"
Implementation of final part of code
The extra routines that get the cartoon parts drawn are all simple, only a couple of representatives are shown:
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.


Last modified March 1996. Please email questions to [email protected]