Trivial Solutions
                        Happy Programmers Make Us Happy

The LoseThos 64-bit PC Operating System
File:/LT/Apps/Tanks/Tanks.CPZ
This, and all LoseThos files, are public domain.  Do whatever you like.
God is watching; God is just; God is never out-done in generosity.  He talks to me!

LoseThos code compiles with the 64-bit LoseThos compiler/assembler I wrote.
Boot the LoseThos CD and you can compile it.  Not in Kansas anymore!

Click here for preliminary compiler information you need.

/*
Alteration Ideas:
  * Add another fire phase
  * Add opportunity fire
  * Add overruns
  * Add close assaults
  * Add more unit types
  * Add artillery
  * Add aircraft
  * Add troop transport
  * Add land mines
  * Add buildings
  * Adjust terrain cost
  * Adjust terrain defense bonuses
  * Add bridges
  * Add amphibious scenareos
  * Add vehicle wreckage
  * Add scoring
  * Add game turn limit
*/

#define MAP_WIDTH               603
#define MAP_HEIGHT              452
#define MAX_UNITS               32
#define ODDS                    1.0
#define FRIENDLY_ARMOR_PERCENT  50
#define ENEMY_ARMOR_PERCENT     50

/*Constants are often faster when compiled.
This is an advantage of just-in-time compilation.
You can't change them when you start a
game over, however.  A down-side of this
is you can't use the CTRL-C auto-indent feature.
*/

#exe {
  TaskStruct *task;
  PopUpFile("/LT/Demo/Graphics/RotateTank.CPZ",FALSE,NULL,&task);
  F64 d;
  if (!PopUpNoYes("Tournament Settings")) {
    d=PopUpRangeF64Log(300,2000,20,"%4f","Map Width In Pixels\r\n");
    StreamPrintF("#define MAP_WIDTH %f\r\n",d);
    StreamPrintF("#define MAP_HEIGHT %f\r\n",480.0/640.0*d);
    d=PopUpRangeF64Exp(8,512,2,"%3f","Enemy Units\r\n");
    StreamPrintF("#define MAX_UNITS %f\r\n",d);
    d=PopUpRangeF64(0.2,1.01,0.05,"%4.2f to 1.00","Odds\r\n");
    StreamPrintF("#define ODDS %4.2f\r\n",d);
    d=PopUpRangeF64(0,100,10,"%3f% %%","Friendly Armor Percent\r\n");
    StreamPrintF("#define FRIENDLY_ARMOR_PERCENT %f\r\n",d);
    d=PopUpRangeF64(0,100,10,"%3f% %%","Enemy Armor Percent\r\n");
    StreamPrintF("#define ENEMY_ARMOR_PERCENT %f\r\n",d);
  }
  d=PopUpRangeF64(0,100,25,"%3f% %%","Animation Delay\r\n");
  StreamPrintF("#define ANIMATION_DELAY %5.3f\r\n",d/100.0);
  XTalk(task," ");
  TaskWaitIdle(task);
  Kill(task);
};

#define HEX_SIDE        11
F64 dc=HEX_SIDE*Cos(60.0/180*pi),
       ds=HEX_SIDE*Sin(60.0/180*pi),
       hex_radius=ds+0.01; //slop

I64 map_cols=(MAP_WIDTH-dc)/(2*HEX_SIDE+2*dc),
   map_rows=ToI64((MAP_HEIGHT-ds)/ds)&~1,
   map_width=map_cols*(2*HEX_SIDE+2*dc)+dc,
   map_height=map_rows*ds+ds+1;

GrBitMap *map;
U8 terrain[map_rows][map_cols];


//Centers of hexes
class Point
{
  F64 x,y;
};
Point hex_centers[map_rows][map_cols];

I64 show_visible_row,show_visible_col;
BoolI8 roads[map_rows][map_cols],
       rivers[map_rows][map_cols],
       visible_map[map_rows][map_cols];

//Other options for PLAINS are WHITE or YELLOW
#define PLAINS          LTGREEN
#define TREES           GREEN
#define MOUNTAINS       DKGRAY

U8 movement_costs[16];
movement_costs[PLAINS]=2;
movement_costs[TREES]=6;
movement_costs[MOUNTAINS]=10;

//These are used to display a range circle when they player
//is firing.
F64 fire_radius,fire_radius_x,fire_radius_y;

//These display "phase", "turn" and "game over".
U8 msg_buf[80];
U64 msg_off_timeout; //Goes away after a time.


/*I got tricky by not defining a color
right away in these GrElems so they can
work for both players by setting base->color
before drawing them.  I actually made these
graphics by defining a color in the CTRL-R
menu, drawing the unit and deleting the color.

I had to leave a gap between the tank tread
and body because of how it is rendered when rotated.
*/




<1>/* Graphics Not Rendered in HTML */


<2>/* Graphics Not Rendered in HTML */


//This is an infantry.

<3>/* Graphics Not Rendered in HTML */


class Unit
{
  U8 *img;
  I64 num,row,col,
     armored_attack,unarmored_attack,armor;
  U8 type,player,facing,movement,life,
     range,remaining_movement,accuracy;
  BoolI8 visible[2],fired,infantry;
};

Unit units[2][MAX_UNITS];

// Bt(visible_unit_bitmap,p1+p0*MAX_UNITS)
U8 visible_unit_bitmap[2][MAX_UNITS*MAX_UNITS>>3];

#define PHASE_START     0
#define PHASE_MOVE      0
#define PHASE_MOVE0     0
#define PHASE_MOVE1     1
#define PHASE_FIRE      2
#define PHASE_FIRE0     2
#define PHASE_FIRE1     3
#define PHASE_END       4

I64 phase,cur_player,enemy_player,view_player,turn,cursor_row,cursor_col,alive_cnt[2],
   move_routines[2],fire_routines[2];


U0 Toward(I64 &row,I64 &col,I64 direction)
{

//We want this "atomic" in a multitasking sense.
  BoolI8 old_preempt=Preempt(OFF);

  switch (direction) {
    case 0:
      row-=2;
      break;
    case 1:
      if (row&1)
        col++;
      row--;
      break;
    case 2:
      if (row&1)
        col++;
      row++;
      break;
    case 3:
      row+=2;
      break;
    case 4:
      if (!(row&1))
        col--;
      row++;
      break;
    case 5:
      if (!(row&1))
        col--;
      row--;
      break;
  }
  Preempt(old_preempt);
}

I64 FacingChg(I64 f1,I64 f2)
{
  I64 result=(f1+6-f2)%6;
  if (result>=3)
    return 6-result;
  else
    return result;
}

U0 RowColToXY(F64 &x,F64 &y,I64 row,I64 col)
{
  Point *c;
  row=LimitI64(row,0,map_rows);
  col=LimitI64(col,0,map_cols);
  c=&hex_centers[row][col];
  x=c->x;
  y=c->y;
}

U0 XYToRowCol(I64 &row,I64 &col,F64 x,F64 y)
{
  col=(x-dc/2)/(HEX_SIDE+dc);
  if (col&1)
    row=ToI64((y-ds)/(2*ds))*2+1;
  else
    row=ToI64(y/(2*ds))*2;
  col>>=1;
  row=LimitI64(row,0,map_rows-1);
  col=LimitI64(col,0,map_cols-1);
}

Unit *FindUnit(I64 row,I64 col)
{//Finds unit in a hexagon.
  I64 i,j;
  for (j=0;j<2;j++)
    for (i=0;i<MAX_UNITS;i++)
      if (units[j][i].life>0 &&
          units[j][i].row==row &&
          units[j][i].col==col)
        return &units[j][i];
  return NULL;
}

BoolI8 CursorInWindow(TaskStruct *task,I64 x,I64 y)
{
  if (0<=x+task->win_scroll_x<task->win_pixel_width &&
      0<=y+task->win_scroll_y<task->win_pixel_height)
    return TRUE;
  else
    return FALSE;
}

U0 UpdateCursor(TaskStruct *task,I64 x,I64 y)
{

//We want this "atomic" in a multitasking sense.
  BoolI8 old_preempt=Preempt(OFF);

  if (CursorInWindow(task,x,y))
    XYToRowCol(cursor_row,cursor_col,x+task->horz_scroll.pos,y+task->vert_scroll.pos);
  Preempt(old_preempt);
}

class LOSCtrl
{
  I64 r1,c1,r2,c2,distance;
};

BoolI64 LOSPlot(LOSCtrl *l,I64 x,I64 y,I64 z)
{ //We got tricky and used z as the distance from the start of the line.
  I64 row,col;
  XYToRowCol(row,col,x,y);
  if ((row!=l->r1 || col!=l->c1) &&
      (row!=l->r2 || col!=l->c2) &&
      terrain[row][col]!=PLAINS) {
    if (terrain[l->r1][l->c1]==MOUNTAINS) {
      if (terrain[row][col]==MOUNTAINS || z>l->distance>>1)
        return FALSE;
    } else if (terrain[l->r2][l->c2]==MOUNTAINS) {
      if (terrain[row][col]==MOUNTAINS || z<=l->distance>>1)
        return FALSE;
    } else
      return FALSE;
  }
  return TRUE;
}

BoolI64 LOS(I64 r1,I64 c1,I64 r2,I64 c2)
{
  F64 x1,y1,x2,y2;
  LOSCtrl l;
  RowColToXY(x1,y1,r1,c1);
  RowColToXY(x2,y2,r2,c2);
  l.r1=r1; l.c1=c1;
  l.r2=r2; l.c2=c2;
  l.distance=Sqrt(SqrI64(x1-x2)+SqrI64(y1-y2));
  return Line(&l,x1,y1,0,x2,y2,l.distance,&LOSPlot);
}

#define RV_ONE_FRIENDLY_UNIT    0
#define RV_UPDATE_FRIENDLY_UNIT 1
#define RV_FRIENDLY_UNIT_DIED   3
#define RV_ONE_ENEMY_UNIT       4
#define RV_ALL_UNITS            5

class MPCtrl1
{
  I64 mode,lo,hi;
  Unit *tempu;
};

U0 RVSetUp(I64 player)
{
  I64 i;
  Unit *ut0,*ut1;
  ut0=&units[player][0];
  ut1=&units[player^1][0];
  for (i=0;i<MAX_UNITS;i++,ut0++,ut1++) {
    LBtr(&ut1->visible[player],0);
    LBEqu(&ut0->visible[player],0,ut0->life>0);
  }
}

U0 RVMerge(I64 player)
{
  I64 i,j;
  Unit *ut1;
  U8 *dst,*src,*mask=CAlloc(MAX_UNITS>>3);
  src=&visible_unit_bitmap[player];
  for (j=0;j<MAX_UNITS;j++) { //p0
    dst=mask;
    for (i=0;i<MAX_UNITS>>3;i++) //p1
      *dst++|=*src++;
  }
  ut1=&units[player^1][0];
  for (j=0;j<MAX_UNITS;j++,ut1++)
    LBEqu(&ut1->visible[player],0,Bt(mask,j) && ut1->life>0);
  Free(mask);
}


BoolI64 MPRecalcVisible(MPCtrl1 *job)
{
  BoolI8 result=FALSE,seen;
  I64 i,j,row,col;
  F64 x1,y1,x2,y2,d,range;
  Unit *ut0,*ut1;
  ut0=&units[cur_player][job->lo];
  ut1=&units[enemy_player][job->lo];
  if (job->tempu) {
    row=job->tempu->row;
    col=job->tempu->col;
    range=job->tempu->range*2*hex_radius;
    range*=range;
  }
  switch (job->mode) {
    case RV_UPDATE_FRIENDLY_UNIT:
    case RV_ONE_FRIENDLY_UNIT:
      if (job->mode==RV_UPDATE_FRIENDLY_UNIT)
        range=MAX_F64;
      RowColToXY(x1,y1,row,col);
      for (i=job->lo;i<job->hi;i++,ut1++) {
        seen=FALSE;
        if (ut1->life>0 &&
            LOS(row,col,ut1->row,ut1->col)) {
            RowColToXY(x2,y2,ut1->row,ut1->col);
          d=Sqr(x2-x1)+Sqr(y2-y1);
          if (d<range) {
            seen=TRUE;
            LBts(&ut1->visible[cur_player],0);
          }
        }
        if (job->mode==RV_UPDATE_FRIENDLY_UNIT)
          LBEqu(&visible_unit_bitmap[cur_player],i+job->tempu->num*MAX_UNITS,seen);
      }
      break;
    case RV_ONE_ENEMY_UNIT:
      RowColToXY(x1,y1,row,col);
      for (i=job->lo;i<job->hi;i++,ut1++)
        if (ut1->life>0 &&
            LOS(row,col,ut1->row,ut1->col)) {
          LBts(&visible_unit_bitmap[enemy_player],job->tempu->num+i*MAX_UNITS);
          result=TRUE;
        } else
          LBtr(&visible_unit_bitmap[enemy_player],job->tempu->num+i*MAX_UNITS);
      break;
    case RV_ALL_UNITS:
      ut0=&units[cur_player][0];
      for (i=0;i<MAX_UNITS;i++,ut0++)
        if (ut0->life>0) {
          RowColToXY(x1,y1,ut0->row,ut0->col);
          ut1=&units[enemy_player][job->lo];
          for (j=job->lo;j<job->hi;j++,ut1++) {
            if (ut1->life>0 &&
              LOS(ut0->row,ut0->col,ut1->row,ut1->col)) {
              LBts(&ut1->visible[cur_player],0);
              LBts(&visible_unit_bitmap[cur_player],j+i*MAX_UNITS);
            } else
              LBtr(&visible_unit_bitmap[cur_player],j+i*MAX_UNITS);
          }
        } else
          for (j=job->lo;j<job->hi;j++)
            LBtr(&visible_unit_bitmap[cur_player],j+i*MAX_UNITS);
      ut0=&units[enemy_player][0];
      for (i=0;i<MAX_UNITS;i++,ut0++)
        if (ut0->life>0) {
          RowColToXY(x1,y1,ut0->row,ut0->col);
          ut1=&units[cur_player][job->lo];
          for (j=job->lo;j<job->hi;j++,ut1++) {
            if (ut1->life>0 &&
              LOS(ut0->row,ut0->col,ut1->row,ut1->col)) {
              LBts(&ut1->visible[enemy_player],0);
              LBts(&visible_unit_bitmap[enemy_player],j+i*MAX_UNITS);
            } else
              LBtr(&visible_unit_bitmap[enemy_player],j+i*MAX_UNITS);
          }
        } else
          for (j=job->lo;j<job->hi;j++)
            LBtr(&visible_unit_bitmap[enemy_player],j+i*MAX_UNITS);
      break;
  }
  return result;
}

BoolI64 RecalcVisible(I64 mode,Unit *tempu=NULL)
{
  I64 i,hi,k,cnt;
  BoolI64 result;

/*The compiler doesn't go out of it's way
to know if something is constant.;-)  This
just compiles with the val at compile
time, an advantage of just-in-time over
static binaries.  LoseThos has a limited
stk size, so don't get in the habit.
MAlloc() would probably be the better choice.
*/
  MPCtrl1 job[mp_cnt];
  MPCmdStruct *cmd[mp_cnt];


  if (mode==RV_FRIENDLY_UNIT_DIED) {
    MemSet(&visible_unit_bitmap[enemy_player]+tempu->num*MAX_UNITS>>3,0,MAX_UNITS>>3);
    RVMerge(enemy_player);
    return DONT_CARE;
  }

  cnt=mp_cnt; //cores
  hi=MAX_UNITS;
  if (mode==RV_ONE_ENEMY_UNIT) {
    for (hi--;hi>=0;hi--)
      if (units[enemy_player][hi].life>0)
        break;
    hi++;
  }
  k=hi;
  if (hi/mp_cnt<2)
    cnt=1;
  for (i=0;i<cnt;i++) {
    job[i].mode=mode;
    job[i].tempu=tempu;
    job[i].hi=k;
    k-=hi/cnt;
    if (k<0) k=0;
    if (i==cnt-1) k=0;
    job[i].lo=k;
  }


  for (i=cnt-1;i>0;i--)
    cmd[i]=MPJob(&MPRecalcVisible,&job[i],0,1<<i);
  result=MPRecalcVisible(&job[0]);
  for (i=cnt-1;i>0;i--)
    if (MPJobResult(cmd[i]))
      result=TRUE;
  if (mode==RV_UPDATE_FRIENDLY_UNIT)
    RVMerge(cur_player);
  return result;
}

class MPCtrl2
{
  I64 lo,hi,row,col;
};

U0 MPRecalcVisibleMap(MPCtrl2 *job)
{
  I64 i,j;
  for (j=job->lo;j<job->hi;j++)
    for (i=0;i<map_cols;i++)
      if (LOS(job->row,job->col,j,i))
        visible_map[j][i]=TRUE;
      else
        visible_map[j][i]=FALSE;
}

U0 RecalcVisibleMap(I64 row,I64 col)
{
  I64 i,hi,k,cnt;
  MPCtrl2 job[mp_cnt];
  MPCmdStruct *cmd[mp_cnt];

  cnt=mp_cnt; //cores
  hi=map_rows;
  k=hi;
  if (hi/mp_cnt<2)
    cnt=1;
  for (i=0;i<cnt;i++) {
    job[i].row=row;
    job[i].col=col;
    job[i].hi=k;
    k-=hi/cnt;
    if (k<0) k=0;
    if (i==cnt-1) k=0;
    job[i].lo=k;
  }
  for (i=cnt-1;i>0;i--)
    cmd[i]=MPJob(&MPRecalcVisibleMap,&job[i],0,1<<i);
  MPRecalcVisibleMap(&job[0]);
  for (i=cnt-1;i>0;i--)
    MPJobResult(cmd[i]);
}

I64 MoveCost(Unit *tempu,I64 r,I64 c,I64 facing)
{
  I64 result;
  if (tempu->infantry)
    result=0;
  else {
    result=FacingChg(facing,tempu->facing);
    if (result>0) result--;
  }
  if (roads[r][c] && roads[tempu->row][tempu->col])
    result+=1;
  else {
    if (tempu->infantry)
      result+=2;
    else {
      result+=movement_costs[terrain[r][c]];
      if (rivers[r][c])
        result=tempu->movement;
    }
  }
  return result;
}

I64 MoveOneHex(I64 &row,I64 &col,F64 x,F64 y)
{
  I64 direction,best_direction=-1,r,c;
  F64 d,best_d,x1,y1;
  RowColToXY(x1,y1,row,col);
  best_d=Sqr(x1-x)+Sqr(y1-y);
  for (direction=0;direction<6;direction++) {
    r=row; c=col;
    Toward(r,c,direction);
    RowColToXY(x1,y1,r,c);
    d=Sqr(x1-x)+Sqr(y1-y);
    if (0<=r<map_rows &&
        0<=c<map_cols &&
        d<best_d) {
      best_d=d;
      best_direction=direction;
    }
  }
  if (best_direction>=0) {
    Toward(row,col,best_direction);
    return best_direction;
  } else
    return -1;
}

BoolI8 moving=FALSE;
I64 move_x,move_y;
F64 move_facing;
Unit *moving_unit;

BoolI64 MovePlot(U0 aux,I64 x,I64 y,I64 z)
{
  nounusedwarn z,aux;
  move_x=x; move_y=y;
  Sleep(5*ANIMATION_DELAY);
  return TRUE;
}

U0 MoveUnitAnimation(Unit *tempu,I64 r,I64 c,I64 facing)
{
  F64 x1,y1,x2,y2,f=facing*60.0*pi/180.0;
  moving_unit=tempu;
  RowColToXY(x1,y1,tempu->row,tempu->col);
  move_x=x1; move_y=y1;
  moving=TRUE;
  if (tempu->infantry)
    Snd(300);
  else {
    move_facing=tempu->facing*60.0*pi/180.0;
    Snd(150);
    while (Unwrap(f-move_facing,-pi)<=0) {
      move_facing-=0.03;
      Sleep(5*ANIMATION_DELAY);
    }
    while (Unwrap(f-move_facing,-pi)>0) {
      move_facing+=0.03;
      Sleep(5*ANIMATION_DELAY);
    }
    Snd(100);
  }
  move_facing=f;
  RowColToXY(x2,y2,r,c);
  Line(NULL,x1,y1,0,x2,y2,0,&MovePlot);
  Snd(0);
  moving_unit=NULL;
  moving=FALSE;
}
 
BoolI64 MoveUnit(Unit *tempu,I64 x,I64 y)
{
  I64 r,c,r0=tempu->row,c0=tempu->col,i,facing;
  while (tempu->remaining_movement>0) {
    r=tempu->row;
    c=tempu->col;
    if ((facing=MoveOneHex(r,c,x,y))<0)
      break;
    else {
      i=MoveCost(tempu,r,c,facing);
      if (i>tempu->movement)
        i=tempu->movement;
      if (tempu->remaining_movement>=i && !FindUnit(r,c)) {
        MoveUnitAnimation(tempu,r,c,facing);
        tempu->facing=facing;
        tempu->remaining_movement-=i;
        tempu->row=r;
        tempu->col=c;
        RecalcVisible(RV_UPDATE_FRIENDLY_UNIT,tempu);
        LBEqu(&tempu->visible[enemy_player],0,RecalcVisible(RV_ONE_ENEMY_UNIT,tempu));
      } else
        break;
    }
  }
  if (tempu->row!=r0 || tempu->col!=c0)
    return TRUE;
  else
    return FALSE;
}

BoolI8 firing=FALSE;
I64 fire_x,fire_y;

BoolI64 FirePlot(U0 aux,I64 x,I64 y,I64 z)
{
  nounusedwarn z,aux;
  fire_x=x; fire_y=y;
  firing=TRUE;
  Sleep(3*ANIMATION_DELAY);
  return TRUE;
}

U0 FireShot(Unit *tempu,Unit *target)
{
  I64 r,c,facing,add_modifier,
    t1=terrain[tempu->row][tempu->col],
    t2=terrain[target->row][target->col];
  F64 x1,y1,x2,y2,d,a,dammage=0,range_factor;
  BoolI8 hit;

  if (tempu->life<=0 || target->life<=0 ||
      tempu->range<=0 || tempu->accuracy<=0)
    return;
  RowColToXY(x1,y1,tempu->row,tempu->col);
  RowColToXY(x2,y2,target->row,target->col);

  range_factor=Sqrt(Sqr(x2-x1)+Sqr(y2-y1))/(tempu->range*ds);

  add_modifier=0;
  if (t2==TREES)
    add_modifier+=30;
  if (t1==MOUNTAINS && t2!=MOUNTAINS)
    add_modifier-=30;

  d=(100.0*range_factor/2.0*RandU16/MAX_U16+add_modifier)/tempu->accuracy-1.0;
  if (d<0)
    hit=TRUE;
  else
    hit=FALSE;

  if (hit)
    Noise(500*ANIMATION_DELAY,100,150);
  else
    Noise(1000*ANIMATION_DELAY,750,1000);

  if (hit)
    Line(NULL,x1,y1,0,x2,y2,0,&FirePlot);
  else {
    a=pi*2*RandU16/MAX_U16;
    d=(d+0.5)*HEX_SIDE;
    Line(NULL,x1,y1,0,x2+d*Cos(a),y2+d*Sin(a),0,&FirePlot);
  }
  firing=FALSE;
  tempu->fired=TRUE;

  if (hit) {
    r=target->row; c=target->col;
    if ((facing=MoveOneHex(r,c,x1,y1))>=0)
      facing=FacingChg(facing,target->facing);
    else
      facing=0;
    dammage=200.0*RandU16/MAX_U16;
    if (target->armor) {
      d=target->armor/100.0*(5.0-facing)/5.0;
      if (d>=0)
        dammage*=(tempu->armored_attack/100.0)/d;
      else
        dammage=0;
    } else {
      d=2.0-range_factor;
      if (d>=0)
        dammage*=(tempu->unarmored_attack/100.0)*d;
      else
        dammage=0;
    }
    dammage=Round(dammage);
    if (dammage>0) {
      if (dammage>=target->life) {
        Noise(1000*ANIMATION_DELAY,1000,4000);
        target->life=0;
        RecalcVisible(RV_FRIENDLY_UNIT_DIED,target);
        alive_cnt[target->player]--;
      } else {
        if (target->armor) {
          if (dammage>0.6*target->life)
            target->movement=0;
        } else
          target->life-=dammage;
      }
    }
  }
  while (snd_freq) //see Snd()
    Yield;
}

U0 DrawHexes()
{
  F64 dx=2*HEX_SIDE+2*dc,dy=2*ds,
      x,y,x1,y1,x2,y2;
  I64 i,j;
  map->color=WHITE;
  GrRect(map,0,0,map->width,map->height);
  map->color=BLACK;
  y=0;
  for (j=0;j<map_rows;j+=2) {
    x=dc;
    GrLine(map,x,y,x-dc,y+ds);
    GrLine(map,x-dc,y+ds,x,y+2*ds);
    for (i=0;i<map_cols;i++) {
      x1=x; y1=y;
      x2=x1+HEX_SIDE; y2=y1;
      GrLine(map,x1,y1,x2,y2);
      x1=x2; y1=y2;
      x2+=dc; y2+=ds;
      GrLine(map,x1,y1,x2,y2);
      GrLine(map,x2,y2,x2-dc,y2+ds);
      x1=x2; y1=y2;
      x2+=HEX_SIDE;
      GrLine(map,x1,y1,x2,y2);
      GrLine(map,x2,y2,x2+dc,y2+ds);
      x1=x2; y1=y2;
      x2+=dc; y2-=ds;
      if (j || i<map_cols-1)
        GrLine(map,x1,y1,x2,y2);
      x+=dx;
    }
    y+=dy;
  }
  x=dc;
  for (i=0;i<map_cols;i++) {
    x1=x; y1=y;
    x2=x1+HEX_SIDE; y2=y1;
    GrLine(map,x1,y1,x2,y2);
    x1=x2; y1=y2;
    x2+=dc; y2+=ds;
    GrLine(map,x1,y1,x2,y2);
    x1=x2; y1=y2;
    x2+=HEX_SIDE;
    GrLine(map,x1,y1,x2,y2);
    x1=x2; y1=y2;
    x2+=dc; y2-=ds;
    GrLine(map,x1,y1,x2,y2);
    x+=dx;
  }
}

U0 MakeTerrain(U8 color,I64 cnt,I64 cluster_lo,I64 cluster_hi)
{
  I64 i,j,l,row,col;
  for (i=0;i<cnt;i++) {
    col=RandU32%map_cols;
    row=RandU32%map_rows;
    l=cluster_lo+RandU16%(cluster_hi-cluster_lo+1);
    for (j=0;j<l;j++) {
      terrain[row][col]=color;
      Toward(row,col,RandU16%6);
      col=LimitI64(col,0,map_cols-1);
      row=LimitI64(row,0,map_rows-1);
    }
  }
}

U0 MakeRivers()
{
  I64 i,row,col,direction;
  for (i=0;i<4;i++) {
    row=RandU32%map_rows;
    col=RandU32%map_cols;
    direction=RandU16%6;
    while (TRUE) {
      rivers[row][col]=TRUE;
      Toward(row,col,direction);
      if (!(0<=row<map_rows && 0<=col<map_cols))
        break;
      if (!(RandU16%4))
        direction=(direction+(7-RandU16%3))%6;
    }
  }
}

U0 MakeRoads()
{
  I64 i,row,col,direction;
  for (i=0;i<5;i++) {
    row=RandU32%map_rows;
    col=RandU32%map_cols;
    direction=RandU16%6;
    while (TRUE) {
      roads[row][col]=TRUE;
      Toward(row,col,direction);
      if (!(0<=row<map_rows && 0<=col<map_cols))
        break;
      if (!(RandU16%3))
        direction=(direction+(7-RandU16%3))%6;
    }
  }
}

U0 DrawTerrain()
{
  I64 i,j;
  F64 x,y;
  for (j=0;j<map_rows;j++)
    for (i=0;i<map_cols;i++) {
      map->color=terrain[j][i];
      RowColToXY(x,y,j,i);
      GrFloodFill(map,x,y);
    }
}

U0 DrawRivers()
{
  I64 i,j,k,r,c;
  F64 x1,y1,x2,y2;
  for (j=0;j<map_rows;j++)
    for (i=0;i<map_cols;i++) {
      if (rivers[j][i]) {
        RowColToXY(x1,y1,j,i);
        for (k=0;k<6;k++) {
          r=j;c=i;
          Toward(r,c,k);
          if (0<=r<map_rows && 0<=c<map_cols &&
            rivers[r][c]) {
            RowColToXY(x2,y2,r,c);
            map->color=LTBLUE;
            map->pen_width=4;
            GrLine3(map,x1,y1,0,x2,y2,0);
            map->color=BLUE;
            map->pen_width=2;
            GrLine3(map,x1,y1,0,x2,y2,0);
          }
        }
      }
    }
}

U0 DrawRoads()
{
  I64 i,j,k,r,c;
  F64 x1,y1,x2,y2;
  map->color=RED;
  map->pen_width=3;
  for (j=0;j<map_rows;j++)
    for (i=0;i<map_cols;i++) {
      if (roads[j][i]) {
        RowColToXY(x1,y1,j,i);
        for (k=0;k<6;k++) {
          r=j;c=i;
          Toward(r,c,k);
          if (0<=r<map_rows && 0<=c<map_cols &&
            roads[r][c]) {
            RowColToXY(x2,y2,r,c);
            GrLine3(map,x1,y1,0,x2,y2,0);
          }
        }
      }
    }
}


U0 CalcHexCenters()
{
  I64 i,j;
  F64 x,y;
  for (j=0;j<map_rows;j++)
    for (i=0;i<map_cols;i++) {
      x=(2*HEX_SIDE+2*dc)*i+HEX_SIDE/2+dc;
      if (j&1)
        x+=HEX_SIDE+dc;
      y=ds*(j+1);
      hex_centers[j][i].x=x;
      hex_centers[j][i].y=y;
    }
}

U0 DrawDots()
{
  I64 i,j;
  F64 x,y;
  map->color=BLACK;
  for (j=0;j<map_rows;j++)
    for (i=0;i<map_cols;i++) {
      RowColToXY(x,y,j,i);
      GrPlot(map,x,y);
    }
}

U0 InitMap()
{
  CalcHexCenters;
  DrawHexes;
  MemSet(terrain,PLAINS,sizeof(terrain));
  MemSet(roads,FALSE,sizeof(roads));
  MemSet(rivers,FALSE,sizeof(rivers));
  MemSet(visible_map,FALSE,sizeof(visible_map));
  MakeTerrain(MOUNTAINS,0.03*map_cols*map_cols,5,35);
  MakeTerrain(TREES,0.03*map_cols*map_cols,5,35);
  DrawTerrain;
  MakeRivers;
  DrawRivers;
  MakeRoads;
  DrawRoads;
  DrawDots;
}

U0 InitUnits()
{
  I64 i,j,row,col,type;
  MemSet(units,0,sizeof(units));
  alive_cnt[0]=Round(MAX_UNITS*ODDS);
  alive_cnt[1]=MAX_UNITS;
  for (j=0;j<2;j++)
    for (i=0;i<alive_cnt[j];i++) {
      units[j][i].player=j;
      units[j][i].num=i;
      units[j][i].life=100;
      units[j][i].facing=RandU16%6;
      if (!j) {
        if (i>=Round(MAX_UNITS*ODDS*FRIENDLY_ARMOR_PERCENT/100.0))
          type=2;
        else if (i>=Round(0.5*MAX_UNITS*ODDS*FRIENDLY_ARMOR_PERCENT/100.0))
          type=1;
        else
          type=0;
      } else {
        if (i>=Round(MAX_UNITS*ENEMY_ARMOR_PERCENT/100.0))
          type=2;
        else if (i>=Round(0.5*MAX_UNITS*ENEMY_ARMOR_PERCENT/100.0))
          type=1;
        else
          type=0;
      }
      units[j][i].type=type;
      switch (type) {
        case 0: //Light Tank
          units[j][i].infantry=FALSE;
          units[j][i].armor   =30;
          units[j][i].armored_attack  =40;
          units[j][i].unarmored_attack=30;
          units[j][i].accuracy=25;
          units[j][i].range   =8;
          units[j][i].movement=24;
          units[j][i].img     =<2>;
          break;
        case 1: //Medium Tank
          units[j][i].infantry=FALSE;
          units[j][i].armor   =60;
          units[j][i].armored_attack  =60;
          units[j][i].unarmored_attack=40;
          units[j][i].accuracy=25;
          units[j][i].range   =12;
          units[j][i].movement=16;
          units[j][i].img     =<1>;
          break;
        case 2: //Standard Rifle Platoon (with bazooka)
          units[j][i].infantry=TRUE;
          units[j][i].armor   =0;
          units[j][i].armored_attack  =15;
          units[j][i].unarmored_attack=90;
          units[j][i].accuracy=45;
          units[j][i].range   =5;
          units[j][i].movement=4;
          units[j][i].img     =<3>;
          break;
      }
      do {
        row=RandU32%map_rows;
        col=RandU32%(map_cols/3);
        if (j)
          col+=2*map_cols/3;
      } while (FindUnit(row,col));
      units[j][i].row=row;
      units[j][i].col=col;
      LBts(&units[j][i].visible[cur_player],0);
    }
}

U0 NewTurn()
{
  I64 i,j;
  for (j=0;j<2;j++)
    for (i=0;i<MAX_UNITS;i++) {
      units[j][i].remaining_movement=units[j][i].movement;
      units[j][i].fired=FALSE;
    }
  phase=PHASE_START;
  moving_unit=NULL;

  SleepUntil(msg_off_timeout);
  msg_off_timeout=sys_jiffies+JIFFY_FREQ*2*ANIMATION_DELAY+1;
  Snd(1000);
  SPrintF(msg_buf,"Turn %d",++turn);
  RVSetUp(0);
  RVSetUp(1);
  RecalcVisible(RV_ALL_UNITS);
  cur_player=(turn&1)^1;
  enemy_player=cur_player^1;
}


U0 NewPhase()
{
  cur_player^=1;
  enemy_player=cur_player^1;
  if (++phase>=PHASE_END)
    NewTurn;
 
  if (phase&~1==PHASE_MOVE)
    Fs->border_attr=WHITE<<4+GREEN;
  else
    Fs->border_attr=WHITE<<4+RED;
  SleepUntil(msg_off_timeout);
  msg_off_timeout=sys_jiffies+JIFFY_FREQ*2*ANIMATION_DELAY+1;
  Snd(1000);
  switch (phase) {
    case PHASE_MOVE0:
    case PHASE_MOVE1:
      SPrintF(msg_buf,"Player %d Move",cur_player+1);
      break;
    case PHASE_FIRE0:
    case PHASE_FIRE1:
      SPrintF(msg_buf,"Player %d Fire",cur_player+1);
      break;
  }
}

U0 SetViewPlayer(I8 p)
{
  MenuEntry *tempse;
  view_player=p;
  if (tempse=MenuEntryFind(Fs->cur_menu,"View/Player1"))
    tempse->checked= view_player==0;
  if (tempse=MenuEntryFind(Fs->cur_menu,"View/Player2"))
    tempse->checked= view_player==1;
}

U0 Init()
{
  moving_unit=NULL;
  InitMap;
  SetViewPlayer(cur_player=0);
  enemy_player=1;
  Fs->horz_scroll.pos=0;
  Fs->vert_scroll.pos=0;
  InitUnits;
  turn=0;
  fire_radius=0;
  show_visible_row=-1;
  show_visible_col=-1;
  *msg_buf=0;
  msg_off_timeout=0;
  phase=PHASE_END;
}


#define T_TURN_OVER     0
#define T_GAME_OVER     1
#define T_NEW_GAME      2
#define T_EXIT_GAME     3

U0 CheckUser()
{
  if (!alive_cnt[0] || !alive_cnt[1])
    throw (EXCEPT_LOCAL,T_GAME_OVER);
  switch (ScanChar) {
    case CH_ESC:
    case CH_CTRLQ:
      throw (EXCEPT_LOCAL,T_EXIT_GAME);
    case CH_SPACE:
      throw (EXCEPT_LOCAL,T_TURN_OVER);
    case CH_CR:
      throw (EXCEPT_LOCAL,T_NEW_GAME);
    case '1':
      SetViewPlayer(0);
      break;
    case '2':
      SetViewPlayer(1);
      break;
  }
}

U0 PickAI(U8 *dirname,I64 player)
{
  I64 i=0;
  U8 *st;
  LTDirEntry *tempm,*tempm1,*tempm2;
  Ltf *l=LtfNew;
  BoolI8 *old_silent=Silent(ON);
  st=MSPrintF("%s/*.CPZ",dirname);
  tempm=FilesFind(st);
  Free(st);
  tempm2=FilesFind("HOME/Tanks/*.CPZ");
  tempm1=tempm;
  Silent(old_silent);

  LtfPrintF(l,"Player %d Type\r\n\r\n",player+1);
  while (tempm1) {
    if (!(i++%4))
      LtfPutS(l,"\r\n");
    st=StrNew(tempm1->name);
    StrLastRem(st,".");
    tempm1->user_data=LtfPrintF(l,"$MU-UL,\"%-10ts\",%d$ ",st,tempm1);
    Free(st);
    tempm1=tempm1->next;
  }
  tempm1=tempm2;
  while (tempm1) {
    if (!(i++%4))
      LtfPutS(l,"\r\n");
    st=StrNew(tempm1->name);
    StrLastRem(st,".");
    tempm1->user_data=LtfPrintF(l,"$MU-UL,\"%-10ts\",%d$ ",st,tempm1);
    Free(st);
    tempm1=tempm1->next;
  }
  LtfPutS(l,"\r\n\r\n\r\nCreate your own AI in HOME/Tanks.");
  while ((tempm1=PopUpMenu(l))<0);
  ExeFile(tempm1->full_name);
  LtfDel(l);
  LTDirListDel(tempm);
  LTDirListDel(tempm2);
  ExePrintF("move_routines[%d]=&TanksMove;fire_routines[%d]=&TanksFire;",player,player);
}


U0 DrawUnit(TaskStruct *task,GrBitMap *base,Unit *tempu,I64 x,I64 y,F64 f)
{
  if (tempu->infantry)
    GrElemsPlot3(base,x-task->horz_scroll.pos,y-task->vert_scroll.pos,0,tempu->img);
  else
    GrElemsPlotRotZ3b(base,x-task->horz_scroll.pos,y-task->vert_scroll.pos,0,tempu->img,f);
}

U0 DrawUnits(TaskStruct *task,GrBitMap *base)
{
  I64 i,j;
  F64 x,y;
  Unit *tempu;
  for (j=0;j<2;j++) {
    if (j)
      base->color=LTPURPLE;
    else
      base->color=LTCYAN;
    for (i=0;i<MAX_UNITS;i++) {
      tempu=&units[j][i];
      if (tempu->life>0 && Bt(&tempu->visible[view_player],0) && tempu!=moving_unit) {
        RowColToXY(x,y,tempu->row,tempu->col);
        if (phase&~1==PHASE_MOVE && tempu->remaining_movement ||
            phase&~1==PHASE_FIRE && !tempu->fired ||
            Blink(5,tP(task)))
          DrawUnit(task,base,tempu,x,y,tempu->facing*60.0*pi/180.0);
      }
    }
  }
}

U0 UpdateWin(TaskStruct *task)
{
  F64 x,y;
  I64 h,v;
  I64 i,j;
  BoolI8 old_preempt=IsPreempt;
  GrBitMap *base=GrAlias(gr_refreshed_base,task);
  U8 buf[80];

  task->horz_scroll.min=0;
  task->horz_scroll.max=map_width-task->win_pixel_width;
  task->vert_scroll.min=-FONT_HEIGHT;
  task->vert_scroll.max=map_height-task->win_pixel_height;
  TaskDerivedValsUpdate(task);
  h=task->horz_scroll.pos;
  v=task->vert_scroll.pos;

  GrBlot(base,-h,-v,map);

  //We want this "atomic" in a multitasking sense.
  //ipx,ipy are the current mouse x,y in screen coordinates.
  Preempt(OFF);
  i=ipx-task->win_pixel_left-task->win_scroll_x;
  j=ipy-task->win_pixel_top -task->win_scroll_y;
  if (CursorInWindow(task,i,j))
    UpdateCursor(task,i,j);
  RowColToXY(x,y,cursor_row,cursor_col);
  Preempt(old_preempt);

//Roads require multiple cursor fills
  base->color=YELLOW;
  GrFloodFill(base,x+(HEX_SIDE+dc)/2-h,y-v);
  GrFloodFill(base,x-(HEX_SIDE+dc)/2-h,y-v);
  GrFloodFill(base,x+HEX_SIDE/2-h,y+(HEX_SIDE+dc)/2-v);
  GrFloodFill(base,x+HEX_SIDE/2-h,y-(HEX_SIDE+dc)/2-v);
  GrFloodFill(base,x-HEX_SIDE/2-h,y+(HEX_SIDE+dc)/2-v);
  GrFloodFill(base,x-HEX_SIDE/2-h,y-(HEX_SIDE+dc)/2-v);

  DrawUnits(task,base);
  if (firing) {
    base->color=BLACK;
    GrCircle(base,fire_x-h,fire_y-v,2);
  }
  if (moving_unit && moving_unit->visible[view_player]) {
    base->color=YELLOW;
    DrawUnit(task,base,moving_unit,move_x,move_y,move_facing);
  }
  progress4=progress4_max=progress1=progress1_max=0;
  if (moving_unit) {
    if (ipy<GR_HEIGHT/2) {
      progress4_max=moving_unit->movement;
      progress4=moving_unit->remaining_movement;
    } else {
      progress1_max=moving_unit->movement;
      progress1=moving_unit->remaining_movement;
    }
  }
  if (fire_radius) {
    base->color=YELLOW;
    GrCircle(base,fire_radius_x-h,fire_radius_y-v,fire_radius-1);
    GrCircle(base,fire_radius_x-h,fire_radius_y-v,fire_radius+1);
    base->color=RED;
    GrCircle(base,fire_radius_x-h,fire_radius_y-v,fire_radius);
  }

  if (Bt(key_down_bitmap,SC_SHIFT)) {

    //We want this "atomic" in a multitasking sense.
    Preempt(OFF);
    if (show_visible_row!=cursor_row || show_visible_col!=cursor_col) {
      show_visible_row=cursor_row;
      show_visible_col=cursor_col;
      Preempt(old_preempt);
      RecalcVisibleMap(show_visible_row,show_visible_col);
    } else
      Preempt(old_preempt);

    base->color=LTGRAY;
    for (j=0;j<map_rows;j++)
      for (i=0;i<map_cols;i++)
        if (!visible_map[j][i]) {
          RowColToXY(x,y,j,i);
          GrLine(base,x-6-h,y-6-v,x+6-h,y+6-v);
          GrLine(base,x+6-h,y-6-v,x-6-h,y+6-v);
          GrLine(base,x-h,y-6-v,x-h,y+6-v);
          GrLine(base,x+6-h,y-v,x-6-h,y-v);
        }
  }
  if (i=StrLen(msg_buf)*FONT_WIDTH) {
    base->color=BLACK;
    GrRect(base,(task->win_pixel_width-i)>>1-10-task->win_scroll_x,
               (task->win_pixel_height-FONT_HEIGHT)>>1-10-task->win_scroll_y,
               i+20,FONT_HEIGHT+20);

    base->color=YELLOW;
    GrRect(base,(task->win_pixel_width-i)>>1-7-task->win_scroll_x,
               (task->win_pixel_height-FONT_HEIGHT)>>1-7-task->win_scroll_y,
               i+14,FONT_HEIGHT+14);

    base->color=RED;
    GrPutS(base,(task->win_pixel_width-i)>>1-task->win_scroll_x,
                (task->win_pixel_height-FONT_HEIGHT)>>1-task->win_scroll_y,
      msg_buf);
    if (msg_off_timeout) {
      if (msg_off_timeout-sys_jiffies<3*JIFFY_FREQ/2*ANIMATION_DELAY)
        Snd(0);
      if (sys_jiffies>msg_off_timeout)
        *msg_buf=0;
    }
  }

  SPrintF(buf,"Turn:%2d Player 1:%3d Player 2:%3d",turn,alive_cnt[0],alive_cnt[1]);
  base->color=WHITE;
  GrRect(base,-task->win_scroll_x,-task->win_scroll_y,StrLen(buf)*FONT_WIDTH,FONT_HEIGHT);
  base->color=BLACK;
  GrPutS(base,-task->win_scroll_x,-task->win_scroll_y,buf);

  GrDel(base);
}

U0 TaskEndCB()
{
  Snd(0);
  progress4=progress4_max=progress1=progress1_max=0;
  Exit;
}

U0 Tanks()
{
  U64 result,ch;
  map=GrNew(BMT_COLOR4,MAP_WIDTH,MAP_HEIGHT);

  SettingsPush; //See SettingsPush
  Fs->win_inhibit|=WIF_DBL_CLICK|WIF_IP_L|WIF_IP_R;

  MenuPush(
"File {"
"  Abort(,CH_CTRLQ);"
"  Exit(,CH_ESC);"
"}"
"Play {"
"  EndPhase(,CH_SPACE);"
"  Restart(,CH_CR);"
"}"
"View {"
"  Player1(,'1');"
"  Player2(,'2');"
"  LOS(,0,SCF_SHIFT);"
"}"
);

  WinMax;
  WordStat(OFF);
  WinBorder(ON);
  Init;
  PickAI("/LT/Apps/Tanks/AIs",0);
  PickAI("/LT/Apps/Tanks/AIs",1);

  PopUpOk("Left-click to move or fire units.\r\n"
          "$FG,GREEN$SPACE$FG$ or right-click to end phase.\r\n"
          "$FG,GREEN$SHIFT$FG$ to show line-of-sight.\r\n"
          "$FG,GREEN$ENTER$FG$ to start new game.\r\n"
          "$FG,GREEN$  1  $FG$ Player 1 View.\r\n"
          "$FG,GREEN$  2  $FG$ Player 2 View.");
  Fs->task_end_cb=&TaskEndCB; //CTRL-ALT-X
  Fs->update_win=&UpdateWin;
  LtfClear;

  NewPhase;
  try {
    do {
      try {
        result=T_EXIT_GAME;
        if (phase&~1==PHASE_MOVE)
          CallInd(move_routines[cur_player]);
        else
          CallInd(fire_routines[cur_player]);
      } catch {
        if (Fs->except_argc==2 && Fs->except_argv[0]==EXCEPT_LOCAL) {
          Fs->catch_except=TRUE;
          result=Fs->except_argv[1];
        }
      }
      switch (result) {
        case T_TURN_OVER:
          NewPhase;
          break;
        case T_GAME_OVER:
          while (TRUE) {
            msg_off_timeout=0;
            StrCpy(msg_buf,"Game Over");
            Snd(0);
            ch=GetChar(NULL,FALSE);
            if (ch==CH_CR) {
              Init;
              NewPhase;
              break;
            } else if (ch==CH_ESC || ch==CH_CTRLQ) {
              result=T_EXIT_GAME;
              break;
            } else if (ch=='1')
              SetViewPlayer(0);
            else if (ch=='2')
              SetViewPlayer(1);
          }
          break;
        case T_NEW_GAME:
          Init;
          NewPhase;
          break;
      }
    } while (result!=T_EXIT_GAME);
  } catch
    Fs->catch_except=TRUE;
  progress4=progress4_max=progress1=progress1_max=0;

  SettingsPop;
  MenuPop;
  GrDel(map);
}