/*- worms.cpp -- A primitive game example of wiggly worms moving about (c) 2013 pmateti@wright.edu The file worms12-doc.html has a detailed commentary/tutorial that goes with this file. These two files are given to students asking them to critique it and improve it. E.g., make it (more) object oriented. Build it on Linux: 'g++ -o worms worms.cpp -lncurses' Run it: './worms 3' Build it on Windows: Visual Studio 2012 or later and Public Domain Curses (http://pdcurses.sourceforge.net/). These mods were contributed by salva.3@wright.edu 2013. */ /* TBD signals ^C */ #include#include #include #ifdef _WIN32 //c++11 headers #include #include #else #include #endif #define random(x) (rand() % x) #define isDigit(x) ('0' <= x && x <= '9') #define min(a, b) ((a) < (b)? (a) : (b)) #define ESC '\033' // the ESCape char #define CARROT '.' // a carrot is shown as a dot const char * say[] = { // worms carry these strings "do-not-optimize-too-soon*", "If it does not have to be correct, making the program efficient is easy.", "90% of code executes only 10% of the time!", "Is there a program, longer than say 1000 lines, that is correct?", "OS is not bug-free, the compiler is not bug-free, so is my program!", "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "98765432109876543210*", "Edsger-Dijkstra*", // legendary programmers ... "C-A-R-Hoare*", "Donald-Knuth*", "Richard-Stallman*", "Linux-Torvalds*", }; #define Nsay sizeof(say)/sizeof(char *) // #sayings we have typedef struct { int onec; // one character int attr; // its color } ASOG; // A Square On the Ground typedef enum { NORTH, NORTHEAST, EAST, SE, S, SW, W, NW } DIRECTION; // as numbers: 0 .. 7 typedef enum { VEGETARIAN, CANNIBAL, SCISSORHEAD } WORM_KIND; // 0, 1, 2 typedef struct { int color; int capacity; // stomach size int foodValue; } WORM_INFO; typedef enum { EATEN, DEAD, ALIVE } LIFE; typedef struct { int x, y; // coordinates of the segment char c; // the letter it carries } SEGMENT; #define MAXsegs 100 // max # segments per worm typedef struct { WORM_KIND type; // once set, type does not change int direction; // its (head's) direction int nsegs; // # of segs in this worm int stomach; // food value LIFE status; // EATEN, DEAD, or ALIVE SEGMENT body[MAXsegs]; // body parts } WORM; #define headSeg(wp) (wp->body + wp->nsegs) #define xOf(wp) (headSeg(wp)->x) #define yOf(wp) (headSeg(wp)->y) #define MAXworms 100 // max # worms in our program WORM worm[MAXworms]; // vars for the worms int hxWorms; // high water mark of the worm[] int xworms[3]; // counts of different worms #define nVegetarians xworms[0] #define nCannibals xworms[1] #define nScissors xworms[2] #define nAllWorms (nVegetarians + nCannibals + nScissors) #define MAXrow 100 #define MAXcol 100 ASOG passive[MAXrow * MAXcol]; // for carrots and dead worms ASOG screen[MAXrow * MAXcol]; // what gets displayed int asogRows, asogCols; // the actual size of 'worm area' #define asog(ptr, row, col) (ptr + row*asogCols + col) #ifdef _WIN32 #include #else #include #endif #define mvCursor(y, x) move(y, x) #define putChar(c) addch(c) #define putString(s) addstr(s) #define getChar() getch() #define screenFlush() refresh() #define screenClear() clear() #define EOLN "\n" // End Of LiNe string void endCurses() { if (!isendwin()) endwin(); } void startCurses() { initscr(); // ncurses init cbreak(); // unbuffered getChar noecho(); // no echoing of keys pressed // intrflush(stdscr, 0); // TBD nodelay(stdscr, TRUE); // get a char *if* available atexit(endCurses); start_color(); use_default_colors(); init_pair(1, COLOR_RED, -1); init_pair(2, COLOR_GREEN, -1); init_pair(3, COLOR_BLUE, -1); getmaxyx(stdscr, asogRows, asogCols); if (asogCols > MAXcol) asogCols = MAXcol; if (asogRows > MAXrow) asogRows = MAXrow; asogRows -= 6; // 6 lines for msgs, asogRows needs to be > 0 } int getOneChar() { nodelay(stdscr, FALSE); // wait until a char is typed int c = getChar(); nodelay(stdscr, TRUE); return c; } // ncurses code ends here void showScreen() { ASOG * sp = screen; for (int y = 0; y < asogRows; y++) { mvCursor(y, 0); for (int x = 0; x < asogCols; x++, sp++) putChar(sp->onec | sp->attr); } screenFlush(); } char msg[1024]; void showMsg(int y) { msg[1023] = '\0'; mvCursor(y, 0); putString(msg); screenFlush(); } void sprinkleCarrots() { for(ASOG *p = asog(passive, asogRows, 1); p-- > passive;) { p->attr = WA_DIM; p->onec = CARROT; } } WORM_INFO worm_info[] = { // indexed by WORM_KIND {COLOR_PAIR(1), 3, 3}, // from {COLOR_PAIR(2), 4, 5}, {COLOR_PAIR(3), 5, 4} }; /* Show a single worm. pre: wp != 0. post: It is drawn on passive[] if it is dead. If alive, it is drawn on screen[]. If eaten, not drawn. ; */ void showOneWorm(WORM * wp) { if (wp->status != EATEN) { ASOG *f = (wp->status == ALIVE ? screen : passive); SEGMENT *bp, *hp = headSeg(wp); for (bp = wp->body + 1; bp <= hp; bp++) { ASOG *g = asog(f, bp->y, bp->x); g->attr = worm_info[wp->type].color; g->onec = bp->c; } } } /* Display the carrots, the dead worms, and the current positions of one (pre: wp != 0) or all (pre: wp == 0) worms. globals: passive[] read-only. pre: none; post: ... ; */ void showWormsAndCarrots(WORM * wp) { ASOG *sp = screen, *se = asog(screen, asogRows, 1); for (ASOG *pp = passive; sp < se; ) *sp++ = *pp++; // copy the carrots + dead worms WORM * headWorm = worm + (wp? 1 : hxWorms); for (WORM * wp = worm; wp < headWorm; ) showOneWorm(wp++); showScreen(); } /* It is hungry if its tummy is at least 25% empty. pre: wp != 0; post: Return 1 if worm wp is hungry; otherwise return 0. */ 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); } /* pre: wp != 0 */ void gotEaten(WORM * wp) { if (wp->status == ALIVE) { wp->status = EATEN; xworms[wp->type] --; } } /* pre: wp->status == ALIVE; post: if stomach is 0 or less, mark it dead, and send its body to the cemetary. */ void checkStomach(WORM * wp) { if (wp->stomach <= 0) { wp->status = DEAD; xworms[wp->type] --; showOneWorm(wp); // mv the body to passive } } /* Eat a carrot, if available. pre: wp->status == ALIVE; post: ...; */ void eatACarrot(WORM * wp) { ASOG *sg = asog(passive, yOf(wp), xOf(wp)); if (CARROT == sg->onec) { wp->stomach += 2; // food value of one carrot sg->onec = ' '; } } /* Check if any segment of worm wp is at (col, row). Return this segment's number. Recall that body[0] is just an eraser-blank, not really a part of the worm. pre: wp != 0; post: ...; */ int isAt(WORM * wp, int row, int col) { SEGMENT *bp = headSeg(wp); for (int sn = wp->nsegs; sn > 0; sn--, bp--) { if ((bp->y == row) && (bp->x == col)) { return sn; } } return 0; //this is important as not all compileres return 0 by default } /* The following two vars should be 'out' parameters (var params), but for ease of understanding, let us declare these as globals. */ WORM *victimWormp; int victimSegNum; /* Find a worm that is on the same square as "my" head. pre: me != 0; post: victimSegNum > 0 => victimWormp != 0 and victimWormp's victimSegNum segment is at the same square as the head of me. */ void setVictimWorm(WORM * me) { WORM *headWorm = worm + hxWorms, *wp; victimWormp = 0; victimSegNum = 0; for (wp = worm; wp < headWorm; wp++) { if (wp != me && wp->status == ALIVE) { victimSegNum = isAt(wp, yOf(me), xOf(me)); if (victimSegNum > 0) { victimWormp = wp; return; // found the victim } } } } /* We have a new worm about to join. Find a slot for it in our worm[] array. pre: 0 <= hxWorms <= MAXworms; post: Return a ptr to a slot for the new work, or 0, if we do not succeed in finding a slot. */ WORM * findSlot() { WORM * zp = worm + hxWorms; if (hxWorms < MAXworms) { hxWorms++; return zp; } /* see if there are any dead/eaten worms */ for (WORM * wp = worm; wp < zp; wp++) if (wp->status != ALIVE) return wp; return 0; } /* Copy the body segments. pre: vp != 0 && 0 < vsn <= vp->nsegs; post: old body[vsn+1..end] should become new body[1..new-length]; body[0] remains as the eraser-blank */ void shiftBodyDown(WORM * vp, int vsn) { SEGMENT *dp = vp->body + 1; // dp destination, sp source SEGMENT *hp = headSeg(vp); for (SEGMENT *sp = vp->body + vsn + 1; sp <= hp; ) *dp++ = *sp++; int tsegs = vp->nsegs; vp->nsegs -= vsn; vp->stomach = vp->stomach / tsegs * vp->nsegs; checkStomach(vp); } /* A scissor-head or cannibal hit the victim at segment number vsn. Slice the victim into two. pre: vp != 0 && 0 < vsn <= vp->nsegs; post: The first one with vp->body[1..vsn-1] and the second one with vp->body[vsn+1..vp->nsegs] as its new body. */ WORM * sliceTheVictim(WORM * vp, int vsn) { int tsegs = vp->nsegs; WORM *wp = findSlot(); if (wp == 0) // could not find a slot return vp; xworms[vp->type]++; *wp = *vp; // bottom-half wp->nsegs = vsn - 1; wp->stomach = vp->stomach / tsegs * wp->nsegs; checkStomach(wp); shiftBodyDown(vp, vsn); // the top-half return wp; } /* Eat whatever you find at the wp's head position. If wp is a cannibal or scissor-head it will also eat a carrot, if available. Update the stomach contents. pre: isHungry(wp) && wp->status == ALIVE; post: ...; */ void eat(WORM * wp) { if (wp->type != VEGETARIAN) { setVictimWorm(wp); if (victimWormp != 0) { wp->stomach += worm_info[victimWormp->type].foodValue; WORM * zp = sliceTheVictim(victimWormp, victimSegNum); if (wp->type == CANNIBAL && zp->status == ALIVE) { wp->stomach += zp->nsegs * worm_info[zp->type].foodValue; gotEaten(zp); } } } eatACarrot(wp); } // dx/dy changes, indexed by DIRECTION; (+0 just to line up) const int dxa[] = { +0, +1, +1, +1, +0, -1, -1, -1 }; const int dya[] = { -1, -1, +0, +1, +1, +1, +0, -1 }; const int nextTurn[16] = { // experiment with other values 0, 0, 0, 0, 0, 0, 0, 0, // think: why 16? 1, 1, 1, 7, 7, 7, 2, 6 }; /* One moment in the life of a worm: crawl by one step using one unit of food, may change direction, eat if hungry, mark as dead if stomach is now empty. */ void live(WORM * wp) { SEGMENT *hp = headSeg(wp); for (SEGMENT * bp = wp->body; bp < hp; bp++) // < not <= bp->x = (bp + 1)->x, bp->y = (bp + 1)->y; // crawl int dir = wp->direction = (wp->direction + nextTurn[random(16)]) % 8; hp->y += dya[dir]; hp->x += dxa[dir]; if (hp->y < 0) hp->y = asogRows-1; if (hp->x < 0) hp->x = asogCols-1; if (hp->y >= asogRows) hp->y = 0; if (hp->x >= asogCols) hp->x = 0; wp->stomach--; if (isHungry(wp)) eat(wp); checkStomach(wp); } /* Create a worm. It should be just long enough to carry the phrase sy[]. It is going to crawl out of a random hole. pre: s != 0; post: ...; */ void createWorm(WORM_KIND type, const char * sy) { WORM *wp = findSlot(); if (wp == 0) return; // no room for a new worm int n = wp->nsegs = min(strlen(sy), MAXsegs - 1); wp->stomach = n * worm_info[type].capacity; wp->direction = NORTH; wp->status = ALIVE; wp->type = type; SEGMENT * bp = wp->body; int yy = bp->y = random(asogRows); // birth place of this worm int xx = bp->x = random(asogCols); bp->c = ' '; // works as an "eraser" SEGMENT * hp = headSeg(wp); for (bp++; bp <= hp; bp++) { bp->c = *sy++; // store the phrase s[] bp->x = xx; // worm hangs down in the z-axis bp->y = yy; } xworms[type] ++; } /* parameters for the 'graphical' (such as it is) display */ int slowness; // slowness number int paused = 0; // == 1 iff paused int idelay = 10; // delay increment int tdelay; // total delay yet to do int userKeyPress(int c) { switch (c) { case '+': slowness -= min(slowness, 100); break; case '-': slowness += 100; break; case 'f': slowness = 0; break; case 's': /* for you todo: highlight a worm */ break; case 'k': /* for you todo: kill the highlighted worm */ break; case 'w': createWorm((WORM_KIND)random(3), say[random(Nsay)]); break; case ' ': paused ^= 1; break; } return c; } /* User can control the speed, etc. The total delay required, tdelay, is doled out in increments of idelay so that keyboard interaction is more responsive. pre: none; post: returns ESC or '\0';; */ int userControl() { sprintf (msg, "SPC %s, ESC terminates, k kills-, w creates-, s shows-a-worm" EOLN "%2d Vegetarians,%2d Cannibals,%2d Scissor-heads,%2d hi-water-mark" EOLN "%04d slowness, - increases, + reduces, f full-speed" EOLN, (paused? "resumes" : "pauses "), nVegetarians, nCannibals, nScissors, hxWorms, slowness); showMsg(asogRows + 1); for (tdelay = slowness+1; tdelay > 0; tdelay -= idelay) { char c = userKeyPress(getChar()); // no-delay if (c == ESC) return ESC; if (paused) tdelay += idelay; if (idelay > 0) { #ifdef _WIN32 std::this_thread::sleep_for(std::chrono::microseconds(1000 * idelay)); #else usleep(1000 * idelay); #endif } } return '\0'; } int main(int argc, char * argv[]) { slowness = 10*(argc > 1? argv[1][0] - '0' : 1); if (slowness < 0) slowness = 0; srand(time(0)); // pseudo-random number generator seed for (int ch = 0; ch != ESC; ) { startCurses(); sprinkleCarrots(); hxWorms = nVegetarians = nCannibals = nScissors = 0; for (int i = random(6) + 3; i > 0; i--) createWorm((WORM_KIND) random(3), say[random(Nsay)]); while (ch != ESC && nAllWorms > 0) { for (WORM * wp = worm; wp < worm + hxWorms; wp++) // hxWorms may +- if (wp->status == ALIVE) live(wp); showWormsAndCarrots(0); ch = userControl(); } sprintf(msg, "press ESC to terminate, or any other key to re-run" EOLN EOLN EOLN EOLN); showMsg(asogRows + 1); ch = getOneChar(); endCurses(); } return 0; } /* -eof- */