Worms -- OOP Redo
2016-01
Worms-Redo Project as a learning vehicle.
|
This file was a "notes to myself" that I edited a bit for use by others. It is about worms.cpp, a solution to a problem in CS1 courses. Like all documentations of real programs, the source code may have been updated without a corresponding update here.
We are using the word worm as in an earth worm. Our worms travel on a rectangular (x,y)-grid plane. They are born with their head positioned just below this plane (i.e., their body hangs on z-axis vertically down). We have three kinds of worms: vegetarian (eat only carrots), cannibal (eat everything), scissorhead (cut the others).
Is this file at the level expected of CS1? Well, not quite. There are several items in the code that I implemented in a particular way so as to be able to pedagogically show style and technique, and raise some questions.
nworms = nVegetarians = nCannibals = nScissors = 0;
Such multiple assignments are very suitable in initializations.
typedef struct { int row, col; } POSITION;
typedef signed char COORD; /* coordinate type */ /* dx/dy changes, indexed by DIRECTION */ COORD dxa[] = {+0, +1, +1, +1, +0, -1, -1, -1}; COORD dya[] = {-1, -1, +0, +1, +1, +1, +0, -1};
So COORD is an integer in the range -128..127. Plus-zero in the above could be written without the sign. I added the + just to line up the values.
typedef struct { LIFE status; WORM_KIND type; DIRECTION direction; /* its (head's) direction */ unsigned char nsegs; /* actual # of segs in this worm */ SEGMENT body[MAXsegs]; /* body parts */ unsigned int stomach; } WORM;
nsegs is guaranteed to be no more than MAXsegs. Suppose MAXsegs == 64; so, 8-bits of an unsigned char will do.
unsigned int stomach;
is at the end just to show that an array field does not have to be the last of a struct. However, it is useful to have an array that needs to be of varying length at the end. You will see this in more advance programming.
int nworms; /* high water mark of the worm[] */
0 <= nowrms <= MAXworms, always. The only place it gets incremented is in findSlot(). Gets initialized in
nworms = nVegetarians = nCannibals = nScissors = 0;
of main(). Never gets decremented. Why??
int xworms[3]; /* counts of different worms */ #define nVegetarians *xworms #define nCannibals *(xworms + 1) #define nScissors *(xworms + 2) #define nAllWorms (nVegetarians + nCannibals + nScissors) #define incWorms(t) *(xworms+t) += 1 #define decWorms(t) *(xworms+t) -= 1
This illustrates a good use of macros. xworms[] is an array so that we could increment/decrement using the worm-type as an index. nCannibals etc #defines retain the readability.
Note that *(xworms+t)++
is not the same as *(xworms+t) += 1. To understand this, consider *p++.
typedef struct { /* A Square On the Ground */ char onec; /* one character */ char attr; /* its color */ } ASOG;
ASOG passive[MAXrow * MAXcol]; /* for Carrots and Dead Worms */ ASOG screenImage[MAXrow * MAXcol]; /* what gets displayed */ #define asog(ptr, row, col) (ptr + (row - 1)*screenCols + col - 1)
These two (passive and screenImage) together is what you see on the monitor. Note that they are 1-D arrays. The macro takes what in 2-D-array-index notation would be a[i][j] and turns it into a 1-D ptr-based left-value.
Why -1? Because, the screen coordinates start from 1 not 0, but C array indices begin at 0.
Why are we not multiplying with MAXcol in #define asog(ptr, row, col) (ptr + (row - 1)*screenCols + col - 1)
Because, even though we declared the two 1-D arrays passive and screenImage to be sufficiently long to be considered MAXrow-by-MAXcol 2-D arrays, we actually are using them as screenRows-by-screenCols arrays.
We display the carrots and worms by first copying the entire passive[] into screenImage[]. Then we draw the images of the live worms onto screenImage[]. We then blast the entire array screenImage to the monitor screen via the puttext().
void drawAWorm(WORM * wp) { if (wp->status != EATEN) { ASOG *f = (wp->status == ALIVE ? screenImage : passive); SEGMENT *bp, *lp = lastSeg(wp); for (bp = wp->body + 1; bp <= lp; bp++) drawSegment(wp, bp, f); } }
Even in plain C, you can declare at the beginning of every brace.
By now, you have gotten used to conditional expressions.
But, why are we skipping the eraser-blank? Should that be drawn also. Why isn't the worm leaving its droppings behind?
A number of routines have pre: wp != NULL
. Why? Because, we almost immediately do wp->... which crashes otherwise.
int isHungry(WORM * wp) { int m = wp->stomach; int n = wp->nsegs * worm_info[wp->type].capacity;
return (wp->status == ALIVE && 4 * m < 3 * n); }
Of course, it does not make sense that dead worms are hungry. Then why are we testing for it? To make this routine more robust. When we make a worm die, we simply change its status. We do not change its stomach value. So, just in case you call this routine with such a way ...
POSITION posOfHead(WORM * wp) { return lastSeg(wp)->p; }
Is a one-liner routine worth the hassle? Yes. For the abstraction it provides. Just as the lastSeg() macro was worth it.
The design of userControl() satisfies a critical goal that while paused, keys that change the speed etc. are still active. A simple solution such as
default: getch(); /* wait until the next key press */ ...
wont do that.
To Be Done: Do we do anything with Cannibals? crawling over itself, or each other: prevent/do-better
improve efficiency, e.g., identify repeated showing of the same msgs etc.
-eof-
Copyright © 2013 Prabhaker Mateti
</body> </body> </html>