../../

/*-
  worms.cpp -- A primitive game example of wiggly worms moving about

  (c) 2013 pmateti@wright.edu 

  Build it: 'g++ -o worms worms.cpp -lncurses'
  Run   it: './worms 3'
*/

// TBD  signals ^C

#include 
#include 
#include 
#include 

#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)

#include 
#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;
}

/* 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)
      usleep(1000 * idelay);
  }
  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- */