[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 the second part of a chapter that presents a relatively elaborate example of the use of class inheritance, polymorphism, dynamic binding etc. The example is a "Moria" ("Rogue") style game.


29: The Power of Inheritance and Polymorphism: continued

29.3 An implementation

The files used in the implementation, and their interdependencies are summarized in Figure 29. 8.
[29.8]

Figure 29.8 Module structure for Dungeon game example.

The files D.h and D.cp contain the DynamicArray code defined in Chapter 21. The Geom files have the definition of the simple Pt class. The WindowRep files contain WindowRep, Window and its subclasses. DItem.h and DItem.cp contain the declaration and definition of the classes in the DungeonItem hierarchy while the Dungeon files contain class Dungeon.

An outline for main() has already been given; function Terminate(), which prints an appropriate "you won" or "you lost" message, is trivial.


29.3.1 Windows classes

There are two aspects to class WindowRep: its "singleton" nature, and its interactions with a cursor addressable screen.

The constructor is private. WindowRep objects cannot be created by client code (we only want one, so we don't want to allow arbitrary creation). Clients (like the code of class Dungeon) always access the unique WindowRep object through the static member function Instance().

WindowRep *WindowRep::Instance() 
{
	if(sWindowRep == NULL)
		sWindowRep = new WindowRep;
	return sWindowRep;
}
WindowRep *WindowRep::sWindowRep = NULL;
The first time that it is called, function Instance() creates the WindowRep object; subsequent calls always return a pointer to the same object.

The constructor calls function Initialize() which performs any system dependent device initialization. It then sets up the image array, and clears the screen.

WindowRep::WindowRep()
{
	Initialize();
	for(int row = 0; row < CG_HEIGHT; row++)
		for(int col = 0; col< CG_WIDTH; col++)
			fImage[row][col] = ' ';
	Clear();
}

The implementation of functions like Initialize() involves the same system dependent calls as outlined in the "Cursor Graphics" examples in Chapter 12. Some example functions are:

void WindowRep::Initialize()
{
#if defined(SYMANTEC)
/*
Have to change the "mode" for the 'console' screen.
Putting it in C_CBREAK allows characters to be read one by 
one as they are typed
*/
	csetmode(C_CBREAK, stdin);
#else
/*
No special initializations are needed for Borland IDE
*/
#endif
}
void WindowRep::MoveCursor(int x, int y)
{
	if((x<1) || (x>CG_WIDTH)) return;
	if((y<1) || (y>CG_HEIGHT)) return;
	
#if defined(SYMANTEC)
	cgotoxy(x,y,stdout);
#else	
	gotoxy(x,y);	
#endif
}
void WindowRep::PutCharacter(char ch)
{
#if defined(SYMANTEC)
	fputc(ch, stdout);
	fflush(stdout);	
#elif	
	putch(ch);
#endif
}
Functions like WindowRep::Delay() and WindowRep::GetChar() similarly repackage code from "Cursor Graphics" example.

The WindowRep::Putcharacter() function only does the cursor movement and character output operations when necessary. This function also keeps the WindowRep object's image array consistent with the screen.

void WindowRep::PutCharacter(char ch, int x, int y)
{
	if((x<1) || (x>CG_WIDTH)) return;
	if((y<1) || (y>CG_HEIGHT)) return;

	if(ch != fImage[y-1][x-1]) {
		MoveCursor(x,y);
		PutCharacter(ch);
		fImage[y-1][x-1] = ch;
		}
}

The CloseDown() function clears the screen, performs any device specific termination, then after a short delay lets the WindowRep object self destruct.

void WindowRep::CloseDown()
{
	Clear();
#if defined(SYMANTEC)
	csetmode(C_ECHO, stdin);
#endif
	sWindowRep = NULL;
	Delay(2);
	delete this;
}
Window
The constructor for class Window initializes the simple data members like the width and height fields. The foreground and background arrays are created. They are vectors, each element of which represents an array of characters (one row of the image).
Window::Window(int x, int y, int width, int height, 
	char bkgd, int framed )
{
	fX = x-1;
	fY = y-1;
	fWidth = width;
	fHeight = height;
	fFramed = framed;
	
	fBkgd = new char* [height];
	fCurrentImg = new char* [height];
	for(int row = 0; row < height; row++) {
		fBkgd[row] = new char[width];
		fCurrentImg[row] = new char[width];
		for(int col = 0; col < width; col++)
			fBkgd[row][col] = bkgd;
		}
}
Naturally, the main task of the destructor is to get rid of the image arrays:
Window::~Window()
{
	for(int row = 0; row < fHeight; row++) {
		delete [] fCurrentImg[row];
		delete [] fBkgd[row];
		}
	delete [] fCurrentImg;
	delete [] fBkgd;
}

Functions like Clear(), and Set() rely on auxiliary routines Valid() and Change() to organize the real work. Function Valid() makes certain that the coordinates are within the window's bounds. Function Change() is given the coordinates, and the new character. It looks after details like making certain that the window frame is not overwritten (if this is a framed window), arranging for a request to the WindowRep object asking for the character to be displayed, and the updating of the array.

void Window::Clear(int x, int y)
{
	if(Valid(x,y))
		Change(x,y,Get(x,y,fBkgd));
}
void Window::Set(int x, int y, char ch)
{
	if(Valid(x,y))
		Change(x, y, ch);
}
(Function Change() has to adjust the x, y values from the 1-based scheme used for referring to screen positions to a 0-based scheme for C array subscripting.)
void Window::Change(int x, int y, char ch)
{
	if(fFramed)	{
		if((x == 1) || (x == fWidth)) return;
		if((y == 1) || (y == fHeight)) return;
		}
	
	WindowRep::Instance()->PutCharacter(ch, x + fX, y + fY);
	x--;
	y--;
	fCurrentImg[y][x] = ch;
}
Note the call to WindowRep::Instance(). This returns a WindowRep* pointer. The WindowRep referenced by this pointer is then asked to output the character at the specified point as offset by the origin of this window.

Function SetBkgd() simply validates the coordinate arguments and then sets a character in the background array. Function Get() returns the character at a particular point in either background of foreground array (an example of its use is in the statement Get(x, y, fBkgd) in Window::Clear()).

char Window::Get(int x, int y, char **img) const
{
	x--;
	y--;
	return img[y][x];
}

Function PrepareContent() loads the current image array from the background and, if appropriate, calls SetFrame() to add a frame.

void Window::PrepareContent()
{
	for(int row = 0; row < fHeight; row++)
		for(int col = 0; col < fWidth; col++)
			fCurrentImg[row][col] = fBkgd[row][col];
	if(fFramed)
		SetFrame();
}
void Window::SetFrame()
{
	for(int x=1; x < fWidth-1; x++) {
	 	fCurrentImg[0][x] = '-';
	 	fCurrentImg[fHeight-1][x] = '-';
		}
	for(int y=1; y < fHeight-1; y++) {
	 	fCurrentImg[y][0] = '|';
	 	fCurrentImg[y][fWidth-1] = '|';
		}
	fCurrentImg[0][0] = '+';
	fCurrentImg[0][fWidth-1] = '+';
	fCurrentImg[fHeight-1][0] = '+';
	fCurrentImg[fHeight-1][fWidth-1] = '+';
}
A Window object's frame uses its top and bottom rows and leftmost and rightmost columns. The content area, e.g. the map in the dungeon game, cannot use these perimeter points. (The input file for the map could define the perimeter as all "wall".)

The access functions like X(), Y(), Height() etc are all trivial, e.g.:

int Window::X() const
{
	return fX;
}

The functions ShowAll() and ShowContent() are similar. They have loops take characters from the current image and send the to the WindowRep object for display. The only difference between the functions is in the loop limits; function ShowContent() does not display the periphery of a framed window.

void Window::ShowAll() const
{
	for(int row=1;row<=fHeight; row++) 
		for(int col = 1; col <= fWidth; col++)
			WindowRep::Instance()->
				PutCharacter(
					fCurrentImg[row-1][col-1],
					fX+col, fY+row);
}
NumberItem and EditText
The only complications in class NumberItem involve making certain that the numeric value output does not overlap with the label. The constructor checks the length of the label given and essentially discards it if display of the label would use too much of the width of the NumberItem.
NumberItem::NumberItem(int x, int y, int width, char *label, 
	long initval) : Window(x, y, width, 3)
{
	fVal = initval;
	fLabelWidth = 0;
	int s = strlen(label);
	if((s > 0) && (s < (width-5))) 
		SetLabel(s, label);
	PrepareContent();
	ShowValue();
}
(Note how arguments are passed to the base class constructor.)

Function SetLabel() copies the label into the left portion of the background image. Function SetVal() simply changes the fVal data member then calls ShowValue().

void NumberItem::SetLabel(int s, char * l)
{
	fLabelWidth = s;
	for(int i=0; i< s; i++)
		fBkgd[1][i+1] = l[i];
}

Function ShowValue() starts by clearing the area used for number display. A loop is then used to generate the sequence of characters needed, these fill in the display area starting from the right. Finally, a sign is added. (If the number is too large to fit into the available display area, a set of hash marks are displayed.)

void NumberItem::ShowValue()
{
	int left = 2 + fLabelWidth;
	int pos = fWidth - 1;
	long val = fVal;
	for(int i = left; i<= pos; i++)
		fCurrentImg[1][i-1] = ' ';
	if(val<0) val = -val;
	if(val == 0) 
		fCurrentImg[1][pos-1] = '0';
	while(val > 0) {
		int d = val % 10;
		val = val / 10;
		char ch = d + '0';
		fCurrentImg[1][pos-1] = ch;
		pos--;
		if(pos <= left) break;
		}
	if(pos<=left)
		for(i=left; i < fWidth;i++)
			fCurrentImg[1][i-1] = '#';
	else
	if(fVal < 0)
		fCurrentImg[1][pos-1] = '-';
	ShowContent();
}

Class EditText adopts a similar approach to dealing with the label, it is not shown if it would use too large a part of the window's width. The contents of the buffer have to be cleared as part of the work of the constructor (it is sufficient just to put a null character in the first element of the buffer array).

EditText::EditText(int x, int y, int width, char *label, 
	short size) : Window(x, y, width, 3)
{
	fSize = size;
	fLabelWidth = 0;
	int s = strlen(label);
	if((s > 0) && (s < (width-8))) 
		SetLabel(s, label);
	PrepareContent();
	fBuf[0] = '\0';
	ShowValue();
}

The SetLabel() function is essentially the same as that of class NumberItem. The SetVal() function loads the buffer with the given string (taking care not to overfill the array).

void EditText::SetVal(char* val)
{
	int n = strlen(val);
	if(n>254) n = 254;
	strncpy(fBuf,val,n);
	fBuf[n] = '\0';
	ShowValue();
}

The ShowValue() function displays the contents of the buffer, or at least that portion of the buffer that fits into the window width.

void EditText::ShowValue()
{
	int left = 4 + fLabelWidth;
	int i,j;
	for(i=left; i< fWidth;i++)
			fCurrentImg[1][i-1] = ' ';
	for(i=left,j=0; i< fWidth; i++, j++) {
		char ch = fBuf[j];
		if(ch == '\0') break;
		fCurrentImg[1][i-1] = ch;
		}
	ShowContent();
}

Function GetInput() positions the cursor at the start of the data entry field then loops accepting input characters (obtained via the WindowRep object). The loop terminates when the required number of characters has been obtained, or when a character like a space or tab is entered.

char EditText::GetInput()
{
	int left = 4 + fLabelWidth;
	fEntry = 0;
	ShowValue();
	WindowRep::Instance()->MoveCursor(fX+left, fY+2);
	char ch = WindowRep::Instance()-> GetChar();
	while(isalnum(ch)) {
		fBuf[fEntry] = ch;
		fEntry++;
		if(fEntry == fSize) {
			ch = '\0';
			break;
			}
		ch = WindowRep::Instance()->GetChar();
		}
	fBuf[fEntry] = '\0';
	return ch;
}
The function does not prevent entry of long strings from overwriting parts of the screen outside of the supposed window area. You could have a more sophisticated implementation that "shifted existing text leftwards" so that display showed only the last few characters entered and text never went beyond the right margin.

29.3.2 class Dungeon

The constructor and destructor for class Dungeon are limited. The constructor will simply involve initializing pointer data members to NULL, while the destructor should delete "owned" objects like the main display window.

The Load() function will open the file, then use the auxiliary LoadMap() and PopulateDungeon() functions to read the data.

void Dungeon::Load(const char filename[])
{
	ifstream in(filename, ios::in | ios::nocreate);
	if(!in.good()) {
		cout << "File does not exist. Quitting." << endl;
		exit(1);
		}
	LoadMap(in);
	PopulateDungeon(in);
	in.close();
}

The LoadMap() function essentially reads "lines" of input. It will have to discard any characters that don't fit so will be making calls to ignore(). The argument END_OF_LINE_CHAR would normally be '\n' but some editors use '\r'.

const int END_OF_LINE_CHAR = '\r';

void Dungeon::LoadMap(ifstream& in)
{
	in >> fWidth >> fHeight;
	in.ignore(100, END_OF_LINE_CHAR);
	for(int row = 1; row <= fHeight; row++ ) {
		char ch;
		for(int col = 1; col <= fWidth; col++) {
			in.get(ch);
			if((row<=MAXHEIGHT) && (col <= MAXWIDTH))
				fDRep[row-1][col-1] = ch;
			}
		in.ignore(100, END_OF_LINE_CHAR);
		}
		
	if(!in.good()) {
		cout << "Sorry, problems reading that file.  "
				"Quitting." << endl;
		exit(1);
		}

	cout << "Dungeon map read OK" << endl;
	
	if((fWidth > MAXWIDTH) || (fHeight > MAXHEIGHT)) {
		cout << "Map too large for window, only using "
				"part of map." << endl;
		fWidth = (fWidth < MAXWIDTH) ? fWidth : MAXWIDTH;
		fHeight = (fHeight < MAXHEIGHT) ? 
				fHeight : MAXHEIGHT;
		}

}

The DungeonItem objects can appear in any order in the input file, but each starts with a character symbol followed by some integer data. The PopulateDungeon() function can use the character symbol to control a switch() statement in which objects of appropriate kinds are created and added to lists.

void Dungeon::PopulateDungeon(ifstream& in) 
{
	char ch;
	Monster *m;
	in >> ch;
	while(ch != 'q') {
		switch(ch) {
//Create Player object
case 'h':
		if(fPlayer != NULL) {
			cout << "Limit of one player "
				"violated." << endl;
			exit(1);
		}
		else {
			fPlayer = new Player(this);
			fPlayer->Read(in);
			}
		break;
//Create different specialized Monster objects
case 'w':
		m = new Wanderer(this);
		m->Read(in);
		fInhabitants.Append(m);
		break;
case 'g':
		m = new Ghost(this);
		m->Read(in);
		fInhabitants.Append(m);
		break;
case 'p':
		m = new Patrol(this);
		m->Read(in);
		fInhabitants.Append(m);
		break;
//Create Collectable items
case '*':
case '=':
case '$':
		Collectable *prop = new Collectable(this, ch);
		prop->Read(in);
		fProps.Append(prop);
		break;
default:
		cout << "Unrecognizable data in input file." 
				<< endl;
		cout << "Symbol " << ch << endl;
		exit(1);
		}
		in >> ch;
	}
	if(fPlayer == NULL) {
		cout << "No player!  No Game!" << endl;
		exit(1);
		}
	if(fProps.Length() == 0) {
		cout << "No items to collect!  No Game!" << endl;
		exit(1);
		}
	cout << "Dungeon population read" << endl;
}
The function verifies the requirements for exactly one Player object and at least one Collectable item.

The Run() function starts by creating the main map window and arranging for all objects to be drawn. The main while() loop shows the Collectable items, gets the Player move, then lets the Monsters have their turn.

int Dungeon::Run() 
{
	CreateWindow();
	int n = fInhabitants.Length();
	for(int i=1; i <= n; i++) {
		Monster *m = (Monster*) fInhabitants.Nth(i);
		m->Draw();
		}
	fPlayer->Draw();
	fPlayer->ShowStatus();
	WindowRep::Instance()->Delay(1);
	

	while(fPlayer->Alive()) {
		for(int j=1; j <= fProps.Length(); j++) {
			Collectable *pi = 
				(Collectable*) fProps.Nth(j);
			pi->Draw();
			}

		fPlayer->Run();
		if(fProps.Length() == 0)
			break;
			
		int n = fInhabitants.Length();
		for(i=1; i<= n; i++) {
			Monster *m = (Monster*) 
				fInhabitants.Nth(i);
			m->Run();
			}
		}
	return fPlayer->Alive();
}
(Note the need for type casts when getting members of the collections; the function DynamicArray::Nth() returns a void* pointer.)

The CreateWindow() function creates a Window object and sets its background from the map.

void Dungeon::CreateWindow()
{
	fDWindow = new Window(1, 1, fWidth, fHeight);
	for(int row = 1; row <= fHeight; row++)
		for(int col = 1; col <= fWidth; col++)
			fDWindow->SetBkgd(col, row,
				fDRep[row-1][col-1]);
	fDWindow->PrepareContent();
	fDWindow->ShowAll();
}

Class Dungeon has several trivial access functions:

int Dungeon::Accessible(Pt p) const
{
	return (' ' == fDRep[p.Y()-1][p.X()-1]);
}
Window *Dungeon::Display() { return fDWindow; }
Player *Dungeon::Human() { return fPlayer; }
int Dungeon::ValidPoint(Pt p) const
{
	int x = p.X();
	int y = p.Y();
	// check x range
	if((x <= 1) || (x >= fWidth)) return 0;
	// check y range
	if((y <= 1) || (y >= fHeight)) return 0;
	// and accessibility
	return Accessible(p);
}

There are similar pairs of functions M_at_Pt() and PI_at_Pt(), and RemoveM() and RemoveProp() that work with the fInhabitants list of Monsters and the fProps list of Collectables. Examples of the implementations are

Collectable *Dungeon::PI_at_Pt(Pt p) 
{
	int n = fProps.Length();
	for(int i=1; i<= n; i++) {
		Collectable *pi = (Collectable*) fProps.Nth(i);
		Pt w = pi->Where();
		if(w.Equals(p)) return pi;
		}
	return NULL;
}
void Dungeon::RemoveM(Monster *m)
{
	fInhabitants.Remove(m);
	m->Erase();
	delete m;
}

The ClearLineOfSight() function checks the coordinates of the Pt arguments to determine which of the various specialized auxiliary functions should be called:

int Dungeon::ClearLineOfSight(Pt p1, Pt p2, int max, Pt path[])
{
	if(p1.Equals(p2)) return 0;
	if(!ValidPoint(p1)) return 0;
	if(!ValidPoint(p2)) return 0;
	
	if(p1.Y() == p2.Y())
		return ClearRow(p1, p2, max, path);
	else
	if(p1.X() == p2.X())
		return ClearColumn(p1, p2, max, path);
	
	int dx = p1.X() - p2.X();
	int dy = p1.Y() - p2.Y();
	
	if(abs(dx) >= abs(dy))
		return ClearSemiHorizontal(p1, p2, max, path);
	else 
		return ClearSemiVertical(p1, p2, max, path);
}

The explanation of the algorithm given in the previous section dealt with cases involving rows or oblique lines that were more or less horizontal. The implementations given here illustrate the cases where the line is vertical or close to vertical.

int Dungeon::ClearColumn(Pt p1, Pt p2, int max, Pt path[])
{
	int delta = (p1.Y() < p2.Y()) ? 1 : -1;
	int x = p1.X();
	int y = p1.Y();
	for(int i = 0; i < max; i++) {
		y += delta;
		Pt p(x,y);
		if(!Accessible(p)) return 0;
		path[i] = p;
		if(p.Equals(p2)) return 1;
		}
	return 0;
}
int Dungeon::ClearSemiVertical(Pt p1, Pt p2, int max,
		Pt path[])
{
	int ychange = p2.Y() - p1.Y();
	if(abs(ychange) > max) return 0;
	int xchange = p2.X() - p1.X();
	
	int deltax = (xchange > 0) ? 1 : -1;
	int deltay = (ychange > 0) ? 1 : -1;
	
	float slope = ((float)xchange)/((float)ychange);
	float error = slope*deltay;
	
	int x = p1.X();
	int y = p1.Y();
	for(int i=0;i0.5) {
				x += deltax;
				error -= deltax;
				}
		error += slope*deltay;
		y += deltay;
		Pt p(x, y);
		if(!Accessible(p)) return 0;
		path[i] = p;
		if(p.Equals(p2)) return 1;
		}
	return 0;
}


29.3.3 DungeonItems

DungeonItem
Class DungeonItem implements a few basic behaviours shared by all variants. Its constructor sets the symbol used to represent the item and sets the link to the Dungeon object. The body of the destructor is empty as there are no separate resources defined in the DungeonItem class.
DungeonItem::DungeonItem(Dungeon *d, char sym)
{
	fSym = sym;
	fD = d;
}
DungeonItem::~DungeonItem() { }

The Erase() and Draw() functions operate on the Dungeon object's main map Window. The call fd->Display() returns a Window* pointer. The Window referenced by this pointer is asked to perform the required operation.

void DungeonItem::Erase()
{
	fD->Display()->Clear(fPos.X(), fPos.Y());
}
void DungeonItem::Draw()
{
	fD->Display()->Set( fPos.X(), fPos.Y(), fSym);
}

All DungeonItem objects must read their coordinates, and the data given as input must be checked. These operations are defined in DungeonItem::Read().

void DungeonItem::Read(ifstream& in)
{
	int x, y;
	in >> x >> y;
	if(!in.good()) {
		cout << "Problems reading coordinate data" << endl;
		exit(1);
		}
	if(!fD->ValidPoint(Pt(x,y))) {
		cout <<  "Invalid coords, out of range or"
				"already occupied" << endl;
		cout << "(" << x << ", " << y << ")" << endl;
		exit(1);
		}
	fPos.SetPt(x,y);
}
Collectable
The constructor for class Collectable passes the Dungeon* pointer and char arguments to the DungeonItem constructor:
Collectable::Collectable(Dungeon* d, char sym) :
		 DungeonItem(d, sym)
{
	fHval = fWval = fMval = 0;
}

Class Collectable's access functions (Wlth() etc) simply return the values of the required data members. Its Read() function extends DungeonItem::Read(). Note the call to DungeonItem::Read() at the start; this gets the coordinate data. Then the extra integer parameters can be input.

void Collectable::Read(ifstream& in)
{
//Invoke inherited Read function
	DungeonItem::Read(in);
	in >> fHval >> fWval >> fMval;
	if(!in.good()) {
		cout << "Problem reading a property" << endl;
		exit(1);
		}
}
ActiveItem
The constructor for class ActiveItem again just initializes some data members to zero after passing the given arguments to the DungeonItem constructor. Function ActiveItem::Read() is similar to Collectable::Read() in that it invokes the DungeonItem::Read() function then reads the extra data values (fHealth and fStrength).

There are a couple of trivial functions (GetHit() { fHealth -= damage; }; and Alive() { return fHealth > 0; }). The Move() operation involves calls to the (inherited) Erase() and Draw() functions. Function Step() works out the x, y offset (+1, 0, or -1) coordinates of a chosen neighboring Pt.

void ActiveItem::Move(const Pt& newpoint)
{
	Erase();
	fPos.SetPt(newpoint);
	Draw();
}
Pt ActiveItem::Step(int dir)
{
	Pt p;
	switch(dir) {
case 1: p.SetPt(-1,1);	break;
case 2: p.SetPt(0,1);	break;
case 3: p.SetPt(1,1);	break;
case 4: p.SetPt(-1,0); break;
case 6: p.SetPt(1,0);	break;
case 7: p.SetPt(-1,-1); break;
case 8: p.SetPt(0,-1); break;
case 9: p.SetPt(1,-1); break;
	}
	return p;
}
Player
The constructor for class Player passes its arguments to its parents constructor and then sets its data members to 0 (NULL for the pointer members). The Read() function is similar to Collectable::Read(); it invokes the inherited Dungeon Item::Read() and then gets the extra "manna" parameter.

The first call to ShowStatus() creates the NumberItem and EditText windows and arranges for their display. Subsequent calls update the contents of the NumberItem windows if there have been changes (the call to SetVal() results in execution of the NumberItem object's ShowContents() function so resulting in changes to the display).

void Player::ShowStatus()
{
	if(fWinH == NULL) {
	   fWinH = new NumberItem(2, 20, 20, "Health", fHealth);
	   fWinM = new NumberItem(30,20, 20, "Manna ", fManna);
	   fWinW = new NumberItem(58,20, 20, "Wealth", fWealth);
	   fWinE = new EditText(2, 22, 20, "Direction", 1);
	   fWinH->ShowAll();
	   fWinM->ShowAll();
	   fWinW->ShowAll();
	   fWinE->ShowAll();
	   }
	else {
	   if(fHealth != fWinH->GetVal()) fWinH->SetVal(fHealth);
	   if(fManna != fWinM->GetVal()) fWinM->SetVal(fManna);
	   if(fWealth != fWinW->GetVal()) fWinW->SetVal(fWealth);
	   }
}

The Run() function involves getting and performing a command followed by update of state and display.

void Player::Run()
{
	char ch = GetUserCommand();
	if(isdigit(ch)) PerformMovementCommand(ch);
	else PerformMagicCommand(ch);
	UpdateState();
	ShowStatus();
}
void Player::UpdateState()
{
	fMoveCount++;
	if(0 == (fMoveCount % 3)) fHealth++;
	if(0 == (fMoveCount % 7)) fManna++;
}

The function PeformMovementCommand() first identifies the neighboring point. There is then an interaction with the Dungeon object to determine whether there is a Collectable at that point (if so, it gets taken). A similar interaction determines whether there is a Monster (if so, it gets attacked, after which a return is made from this function). If the neighboring point is not occupied by a Monster, the Player object moves to that location.

void Player::PerformMovementCommand(char ch)
{
	int x = fPos.X();
	int y = fPos.Y();
	Pt p = Step(ch - '0');
	int newx = x + p.X();
	int newy = y + p.Y();
	
	Collectable *pi = fD->PI_at_Pt(Pt(newx, newy));
	if(pi != NULL)
		Take(pi);
	Monster *m = fD->M_at_Pt(Pt(newx, newy));
	if(m != NULL) {
		Attack(m);
		return;
		}
	TryMove(x + p.X(), y + p.Y());
}

The auxiliary functions, Take(), Attack(), and TryMove() are all simple. Function Take() updates the Player objects health and related attributes with data values from the Collectable item, and then arranges for the Dungeon to dispose of that item. Function Attack() reduces the Monster object's health (via a call to its GetHit() function) and, if appropriate, arranges for the Dungeon object to dispose of the Monster. Function TryMove() validates and then performs the appropriate movement.

The function GetUserCommand() arranges for the EditText window to input some text and then inspects the first character of the text entered.

char Player::GetUserCommand()
{
	fWinE->GetInput();
	char *str = fWinE->GetVal();
	return *str;
}

The function PerformMagicCommand() identifies the axis for the magic bolt. There is then a loop in which damage is inflicted (at a reducing rate) on any Monster objects found along a sequence of points in the given direction:

void Player::PerformMagicCommand(char ch)
{
	int dx, dy;
	switch (ch) {
case 'q': dx = -1; dy = -1; break;
case 'w': dx = 0; dy = -1; break;
case 'e': dx = 1; dy = -1; break;
case 'a': dx = -1; dy = 0; break; 
case 'd': dx = 1; dy = 0; break;
case 'z': dx = -1; dy = 1; break;
case 'x': dx = 0; dy = 1; break;
case 'c': dx = 1; dy = 1; break;
default:
		return;
	}
	int x = fPos.X();
	int y = fPos.Y();
	
	int power = 8;
	fManna -= power;
	if(fManna < 0) {
		fHealth += 2*fManna;
		fManna = 0;
		}
	while(power > 0) {
		x += dx;
		y += dy;
		if(!fD->ValidPoint(Pt(x,y))) return;
		Monster* m = fD->M_at_Pt(Pt(x,y));
		if(m != NULL) {
			m->GetHit(power);
			if(!m->Alive())
				fD->RemoveM(m);
			}
		power /= 2;
		}
}
Monster
The constructor and destructor functions of class Monster both have empty bodies for there is no work to be done; the constructor passes its arguments back to the constructor of its parent class (ActiveItem):
Monster::Monster(Dungeon *d, char sym) : ActiveItem(d, sym)
{
}

Function Monster::Run() was defined earlier. The default implementations of the auxiliary functions are:

int Monster::CanAttack()
{

Player *p = fD->Human(); Pt target = p->Where(); return fPos.Adjacent(target); }

void Monster::Attack()
{
	Player *p = fD->Human();
	p->GetHit(fStrength);
}

int Monster::CanDetect() { return 0; }

void Monster::Advance() { }
Ghost
The Ghost::CanDetect() function uses the Pt::Distance() member function to determine the distance to the Player (this function just takes the normal Euclidean distance between two points, rounded up to the next integral value).
int Ghost::CanDetect()
{
	Player *p = fD->Human();
	int range = fPos.Distance(p->Where());
	return (range < 7);
}

The Advance() function determines the change in x, y coords that will bring the Ghost closer to the Player.

void Ghost::Advance()
{
	Player *p = fD->Human();
	Pt p1 = p->Where();
	int dx, dy;
	dx = dy = 0;
	if(p1.X() > fPos.X()) dx = 1;
	else
	if(p1.X() < fPos.X()) dx = -1;
	if(p1.Y() > fPos.Y()) dy = 1;
	else
	if(p1.Y() < fPos.Y()) dy = -1;
	
	Move(Pt(fPos.X() + dx, fPos.Y() + dy));
}
Wanderer
The Wanderer::CanDetect() function uses the Dungeon::ClearLineOfSight() member function to determine whether the Player object is visible. This function call also fills in the array fPath with the points that will have to be crossed.
int Wanderer::CanDetect()
{
	Player *p = fD->Human();
	return
	  fD->ClearLineOfSight(fPos, p->Where(), 10, fPath);
}
The Advance() function moves one step along the path:
void Wanderer::Advance()
{
	Move(fPath[0]);
}

The NormalMove() function tries moving in the same direction as before. Directions are held by storing the delta-x and delta-y values in fLastX and fLastY data members (initialized to zero in the constructor). If movement in that general direction is blocked, a new direction is picked randomly.

void Wanderer::NormalMove()
{
	int x = fPos.X();
	int y = fPos.Y();
	// Try to keep going in much the same direction as last time
	if((fLastX != 0) || (fLastY != 0)) {
		int newx = x + fLastX;
		int newy = y + fLastY;
//Movement in same direction
		if(fD->Accessible(Pt(newx,newy))) { 
			Move(Pt(newx,newy)); 
			return; 
			}
		else
		if(fD->Accessible(Pt(newx,y))) {
//Movement in similar direction
			Move(Pt(newx,y)); fLastY = 0; 
			return; 
			}
		else
		if(fD->Accessible(Pt(x,newy))) { 
			Move(Pt(x,newy)); fLastX= 0; 
			return; }
		}
	int dir = rand();
	dir = dir % 9;
	dir++;
	Pt p = Step(dir);
//Pick new direction at random
	x += p.X();
	y += p.Y();
	if(fD->Accessible(Pt(x,y))) {
		fLastX = p.X();
		fLastY = p.Y();
		Move(Pt(x,y));
		}
}
Patrol
The patrol route has to be read, consequently the inherited Read() function must be extended. There are several possible errors in route definitions, so Patrol:: Read() involves many checks:
void Patrol::Read(ifstream& in)
{
	Monster::Read(in);
	fRoute[0] = fPos;
	fNdx = 0;
	fDelta = 1;
	in >> fRouteLen;
	for(int i=1; i<= fRouteLen; i++) {
		int x, y;
		in >> x >> y;
		Pt p(x, y);
		if(!fD->ValidPoint(p)) {
			cout << "Bad data in patrol route" << endl;
			cout << "(" << x << ", " << y << ")" << endl;
			exit(1);
			}
		if(!p.Adjacent(fRoute[i-1])) {
			cout << "Non adjacent points in patrol"
					"route" << endl;
			cout << "(" << x << ", " << y << ")" << endl;
			exit(1);
			}
		fRoute[i] = p;
		}
	if(!in.good()) {
		cout << "Problems reading patrol route" << endl;
		exit(1);
		}
}

The NormalMove() function causes a Patrol object to move up or down its route:

void Patrol::NormalMove()
{
//Reverse direction at start
	if((fNdx == 0) && (fDelta == -1)) {
		fDelta = 1;
		return;
		}
//Reverse direction at end
	if((fNdx == fRouteLen) && (fDelta == 1)) {
		fDelta = -1;
		return;
		}
//Move one step along route
	fNdx += fDelta;
	Move(fRoute[fNdx]);
}

The CanDetect() function is identical to Wanderer::CanDect(). However, instead of advancing one step along the path to the Player, a Patrol fires a projectile that moves along the complete path. When the projectile hits, it causes a small amount of damage:

void Patrol::Advance()
{
	Player *p = fD->Human();
	Pt target = p->Where();
	Pt arrow = fPath[0];
	int i = 1;
	while(!arrow.Equals(target)) {
		fD->Display()->Set( arrow.X(), arrow.Y(), ':');
		WindowRep::Instance()->Delay(1);
		fD->Display()->Clear( arrow.X(), arrow.Y());
		arrow = fPath[i];
		i++;
		}
	p->GetHit(2);
}


The code.

Exercises

  1. Complete and run the dungeon game program.
  2. This one is only for users of Borland's system.

    Why should the monsters wait while the user thinks? If they know what they want to do, they should be able to continue!

    The current program requires user input in each cycle of the game. If there is no input, the program stops and waits. The game is much more interesting if this wait is limited. If the user doesn't type any command within a second or so, the monsters should get their chance to run anyway.

    This is not too hard to arrange.

    First, the main while() loop in Dungeon::Run() should have a call WindowRep:: Instance()->Delay(1). This results in a 1 second pause in each cycle.

    The Player::Run() function only gets called if there have been some keystrokes. If there are no keystrokes waiting to be processed, the Dungeon::Run() function skips to the loop that lets each monster have a chance to run.

    All that is required is a system function, in the "console" library package, that allows a program to check whether input data are available (without "blocking" like a normal read function). The Borland conio library includes such a function.

    Using the on-line help system in the Borland environment, and other printed documentation, find how to check for input. Use this function in a reorganized version of the dungeon program.

    (You can achieve the same result in the Symantec system but only by utilising specialized system calls to the "Toolbox" component of the Macintosh operating system. It is all a little obscure and clumsy.)

  3. Add multiple levels to the dungeon.

    (There are various ways that this might be done. The easiest is probably to define a new class DungeonLevel . The Dungeon object owns the main window, the Player, and a list of DungeonLevel objects. Each DungeonLevel object owns a map, a list of collectables, and a list of monsters. You will need some way of allowing a user to go up or down levels. When you change level, the new DungeonLevel resets the background map in the main window and arranges for all data to be redrawn.)

  4. Add more challenging Monsters and "traps".

    (Use your own imagination.)



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