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.
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.
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::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::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.
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; }
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::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); } }
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; }
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::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() { }
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)); }
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)); } }
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); }
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.)
(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.)
(Use your own imagination.)