#include <SPI.h>
#include <GD.h>

#define CHEAT_INVINCIBLE    0   // means Willy unaffected by nasties
#define START_LEVEL         0   // level to start on, 0-18
#define CHEAT_OPEN_PORTAL   0   // Portal always open

// Game has three controls: LEFT, RIGHT, JUMP.  You can map them
// to any pins by changing the definitions of PIN_L, PIN_R, PIN_J
// below.

static int ss;  // JCB
#if 1 // SPARKFUN_JOYSTICK
#define PIN_L  6
#define PIN_R  3
#define PIN_J  4
#else
#define PIN_L  A2
#define PIN_R  A3
#define PIN_J  A5
#endif

#define CONTROL_LEFT  1
#define CONTROL_RIGHT 2
#define CONTROL_JUMP  4

static byte setup_control()
{
  pinMode(PIN_L, INPUT);
  digitalWrite(PIN_L, HIGH);
  pinMode(PIN_R, INPUT);
  digitalWrite(PIN_R, HIGH);
  pinMode(PIN_J, INPUT);
  digitalWrite(PIN_J, HIGH);
}

static byte control()
{
  byte r = 0;
  if (!digitalRead(PIN_J))
    r |= CONTROL_JUMP;
  if (!digitalRead(PIN_L))
    r |= CONTROL_LEFT;
  if (!digitalRead(PIN_R))
    r |= CONTROL_RIGHT;
  return r;
}

#define BLACK   RGB(0,0,0)
#define RED     RGB(255,0,0)
#define YELLOW  RGB(255,255,0)
#define CYAN    RGB(0,255,255)
#define GREEN   RGB(0,255,0)
#define MAGENTA RGB(255,0,255)
#define WHITE   RGB(255,255,255)

#define CHR_BORDER    31    // hw char used for all of the border
#define CHR_AIR       128   // AIR line

                            // assigned sprite images and sprite numbers
#define IMG_ITEM      0     // items 0-4
#define IMG_PORTAL    5     // room exit
#define IMG_SWITCH1   6     // the switches for the Kong Beast levels
#define IMG_SWITCH2   7     
#define IMG_GUARD     8     // All guardians, 8 max
#define IMG_NASTY1    16    // Nasty blocks
#define IMG_NASTY2    17
#define IMG_WILLYC    56
#define IMG_WILLY     63

#define ELEM_AIR      0
#define ELEM_FLOOR    1
#define ELEM_CRUMBLE  2
#define ELEM_WALL     3
#define ELEM_CONVEYOR 4
#define ELEM_NASTY1   5
#define ELEM_NASTY2   6

struct level {
  byte name[32];
  prog_uchar *background;
  byte border;
  byte bgchars[64];
  byte bgattr[8];
  byte item[8];
  struct { byte x, y; } items[5];
  byte portal[32];
  byte air;
  byte conveyordir;
  byte portalattr, portalx, portaly;
  byte guardian[8 * 32];
  struct { byte a, x, y, d, x0, x1; } hguard[8];
  byte wx, wy, wd, wf;
  byte bidir;
};
#include "manicminer.h"
#include "spectrum.h"
#include "spectrum_data.h"    // title screen

#define COLOR(i)  (pgm_read_word_near(specpal + (2 * (i))))
#define BRIGHT(x) (((x) & 64) >> 3)
#define PAPER(a)  ((((a) >> 3) & 7) | BRIGHT(a))
#define INK(a)    (((a) & 7) | BRIGHT(a))

// screen coordinate - Spectrum 256x192 screen is centered in Gameduino screen
static uint16_t atxy(byte x, byte y)
{
  return RAM_PIC + 64 * (y + 6) + (x + 9);
}

// unpack 8 monochrome pixels into Gameduino character RAM
void unpack8(byte b)
{
  static byte stretch[16] = {
    0x00, 0x03, 0x0c, 0x0f,
    0x30, 0x33, 0x3c, 0x3f,
    0xc0, 0xc3, 0xcc, 0xcf,
    0xf0, 0xf3, 0xfc, 0xff
  };
  SPI.transfer(stretch[b >> 4]);
  SPI.transfer(stretch[b & 15]);
}

// unpack monochrome bitmap from flash to Gameduino character RAM at dst
void unpack8xn(uint16_t dst, PROGMEM prog_uchar *src, uint16_t size)
{
  GD.__wstart(dst);
  while (size--)
    unpack8(pgm_read_byte_near(src++));
  GD.__end();
}

// Load 8x8 1-bit sprite from src into slot (0-63)
// zero and one are the two colors

void send8(byte b, byte zero, byte one)
{
    for (byte j = 8; j; j--) {
      SPI.transfer((b & 0x80) ? one : zero);
      b <<= 1;
    }
}

// load an 8x8 into a hw 256-color sprite, padding with transparent (color 255)
void loadspr8(byte slot, PROGMEM prog_uchar *src, byte zero, byte one)
{
  GD.__wstart(RAM_SPRIMG + (slot << 8));
  for (byte i = 8; i; i--) {
    send8(pgm_read_byte_near(src++), zero, one);
    send8(0, 255, 255);
  }
  for (byte i = 128; i; i--)
    SPI.transfer(255);
  GD.__end();
}

// load an 16x16 into a hw 256-color sprite
void loadspr16(byte slot, PROGMEM prog_uchar *src, byte zero, byte one)
{
  GD.__wstart(RAM_SPRIMG + (slot << 8));
  for (byte i = 32; i; i--)
    send8(pgm_read_byte_near(src++), zero, one);
  GD.__end();
}


static void sprite(byte spr, byte x, byte y, byte img, byte rot = 0)
{
  GD.sprite(spr, x + 9*8, y + 6*8, img, 0, rot);
}

static void hide(byte spr)
{
  GD.sprite(spr, 400, 400, 0, 0, 0);
}

struct guardian {
  byte a;
  byte x, y;
  signed char d;
  byte x0, x1;
  byte f;
} guards[8];

#define MAXRAY 40

struct state_t {
  byte level;
  byte lives;
  uint32_t score;
  uint32_t hiscore;

  byte bidir;
  byte air;
  byte conveyordir;
  byte portalattr;
  byte nitems;
  byte wx, wy;    // Willy x,y
  byte wd, wf;    // Willy dir and frame
  byte lastdx;    // Willy last x movement
  byte convey;    // Willy caught on conveyor
  byte jumping;
  signed char wyv;
  byte conveyor[2];
  PROGMEM prog_uchar *guardian;
  uint16_t prevray[MAXRAY];
  byte switch1, switch2;
} state;

// All this is taken from
// http://jswremakes.emuunlim.com/Mmt/Manic%20Miner%20Room%20Format.htm

static void plot_air()
{
  // Starting at CHR_AIR+4, draw a line of n/8 solid, n%8, then 27-(n/8)
  uint16_t addr = RAM_CHR + (16 * (CHR_AIR + 4)); // line 2 of (CHR_AIR+4)
  for (byte i = 0; i < 28; i++) {
    byte v;
    if (i < (state.air >> 3))
      v = 8;
    else if (i == (state.air >> 3))
      v = state.air & 7;
    else
      v = 0;
    unpack8xn(addr + i * 16, airs + (v << 3), 8);
  }
}

static void plot_score()
{
  uint32_t n = state.score;

  GD.__wstart(atxy(26, 19));
  SPI.transfer('0' + (n / 100000) % 10);
  SPI.transfer('0' + (n / 10000) % 10);
  SPI.transfer('0' + (n / 1000) % 10);
  SPI.transfer('0' + (n / 100) % 10);
  SPI.transfer('0' + (n / 10) % 10);
  SPI.transfer('0' + n % 10);
  GD.__end();
}

static void bump_score(byte n)
{
  if ((state.score < 10000) && (10000 <= (state.score + n )))
    state.lives++;
  state.score += n;
  plot_score();
}

static void pause(byte n)
{
  while (n--)
    GD.waitvblank();
}

static void clear_screen(byte yclear = 16)
{
  // blank out top 16 lines
  GD.fill(RAM_CHR, 0, 16);
  GD.wr16(RAM_PAL, BLACK);
  for (byte y = 0; y < yclear; y++)
    GD.fill(atxy(0, y), 0, 32);
  for (int i = 0; i < 256; i++)
    hide(i);
  for (int i = 0; i < 64; i++)
    GD.voice(i, 0, 0, 0, 0);
  pause(6);
}

static void loadlevel(PROGMEM struct level *l)
{
  clear_screen();

  // border
  GD.wr16(RAM_PAL + 8 * CHR_BORDER, COLOR(pgm_read_byte_near(&l->border)));

  // background picture: uncompress into scratch then copy to screen
  prog_uchar *background = (prog_uchar*)pgm_read_word_near(&l->background);
  uint16_t scratch = 4096 - 512;  // offscreen scratch
  GD.uncompress(scratch, background);
  for (byte y = 0; y < 16; y++)
    for (byte x = 0; x < 32; x++)
      GD.wr(atxy(x, y), GD.rd(scratch + (y << 5) + x));
  for (byte y = 16; y < 24; y++)
    GD.fill(atxy(0, y), ' ', 32);

  // bg characters
  unpack8xn(RAM_CHR, l->bgchars, sizeof(l->bgchars));
  state.conveyor[0] = pgm_read_byte_near(&l->bgchars[8 * 4 + 0]);
  state.conveyor[1] = pgm_read_byte_near(&l->bgchars[8 * 4 + 3]);

  // bg character palettes
  for (byte c = 0; c < 8; c++) {
    byte attr = pgm_read_byte_near(l->bgattr + c);
    GD.wr16(RAM_PAL + 8 * c, COLOR(PAPER(attr)));
    GD.wr16(RAM_PAL + 8 * c + 6, COLOR(INK(attr)));
  }

  // block 2 is the crumbling floor
  // put the 7 anims of crumbling floor in chars 8-14
  GD.fill(RAM_CHR + 16 * 8, 0, 16 * 7);
  prog_uchar *src = l->bgchars + 2 * 8;
  for (byte i = 0; i < 7; i++) {
    byte ch = 8 + i;
    unpack8xn(RAM_CHR + 16 * ch + 2 * (i + 1), src, 7 - i);
    byte attr = pgm_read_byte_near(l->bgattr + 2);
    GD.wr16(RAM_PAL + 8 * ch, COLOR(PAPER(attr)));
    GD.wr16(RAM_PAL + 8 * ch + 6, COLOR(INK(attr)));
  }

  // level name and score line
  for (byte x = 0; x < 32; x++) {
    char scoreline[] = "High Score 000000   Score 000000";
    GD.wr(atxy(x, 16), 0x80 + pgm_read_byte_near(l->name + x));
    GD.wr(atxy(x, 19), scoreline[x]);
  }
  plot_score();

  // AIR line characters
  for (byte i = 0; i < 32; i++)
    GD.wr(atxy(i, 17), CHR_AIR + i);

  // the items are 5 sprites, using colors 16 up
  state.nitems = 0;
  for (byte i = 0; i < 5; i++) {
    byte x = pgm_read_byte_near(&l->items[i].x);
    byte y = pgm_read_byte_near(&l->items[i].y);
    if (x || y) {
      loadspr8(IMG_ITEM + i, l->item, 255, 16 + i);   // items have color 16 up
      sprite(IMG_ITEM + i, x, y, IMG_ITEM + i);
      state.nitems++;
    } else
      hide(IMG_ITEM + i);
  }

  // the portal is a sprite, using colors 32,33
  state.portalattr = pgm_read_byte_near(&l->portalattr);
  loadspr16(IMG_PORTAL, l->portal, 32, 33);
  byte portalx = pgm_read_byte_near(&l->portalx);
  byte portaly = pgm_read_byte_near(&l->portaly);
  sprite(IMG_PORTAL, portalx, portaly, IMG_PORTAL);

  // use sprites for blocks NASTY1 and NASTY2
  byte nc1 = pgm_read_byte_near(l->bgattr + ELEM_NASTY1);
  byte nc2 = pgm_read_byte_near(l->bgattr + ELEM_NASTY2);
  loadspr8(IMG_NASTY1, l->bgchars + ELEM_NASTY1 * 8, 255, INK(nc1));
  loadspr8(IMG_NASTY2, l->bgchars + ELEM_NASTY2 * 8, 255, INK(nc2));
  // now paint sprites over all the NASTY blocks
  {
    byte spr = IMG_NASTY1;
    for (byte y = 0; y < 16; y++)
      for (byte x = 0; x < 32; x++) {
        byte blk = GD.rd(atxy(x, y));
        if (blk == ELEM_NASTY1)
          sprite(spr++, x << 3, y << 3, IMG_NASTY1);
        if (blk == ELEM_NASTY2)
          sprite(spr++, x << 3, y << 3, IMG_NASTY2);
      }
  }

  // air
  state.air = pgm_read_byte_near(&l->air);
  plot_air();

  // Conveyor direction
  state.conveyordir = pgm_read_byte_near(&l->conveyordir);

  // the hguardians
  state.bidir = pgm_read_byte_near(&l->bidir);
  state.guardian = l->guardian;
  guardian *pg;
  for (byte i = 0; i < 8; i++) {
    byte a = pgm_read_byte_near(&l->hguard[i].a);
    guards[i].a = a;
    if (a) {
      byte x = pgm_read_byte_near(&l->hguard[i].x);
      byte y = pgm_read_byte_near(&l->hguard[i].y);
      guards[i].x = x;
      guards[i].y = y;
      guards[i].d = pgm_read_byte_near(&l->hguard[i].d);
      guards[i].x0 = pgm_read_byte_near(&l->hguard[i].x0);
      guards[i].x1 = pgm_read_byte_near(&l->hguard[i].x1);
      guards[i].f = 0;
    } else {
      hide(6 + i);
    }
  }

  pg = &guards[4];  // Use slot 4 for special guardians
  switch (state.level) { // Special level handling
  case 4: // Eugene's lair
    pg->a = 0;  // prevent normal guardian logic
    pg->x = 120;
    pg->y = 0;
    pg->d = -1;
    pg->x0 = 0;
    pg->x1 = 88;
    loadspr16(IMG_GUARD + 4, eugene, 255, 15);
    break;
  case 7:   // Miner Willy meets the Kong Beast
  case 11:  // Return of the Alien Kong Beast
    pg->a = 0;
    pg->x = 120;
    pg->y = 0;
    state.switch1 = 0;
    loadspr8(IMG_SWITCH1, lightswitch, 0, 6);
    sprite(IMG_SWITCH1, 48, 0, IMG_SWITCH1);
    state.switch2 = 0;
    loadspr8(IMG_SWITCH2, lightswitch, 0, 6);
    sprite(IMG_SWITCH2, 144, 0, IMG_SWITCH2);
    break;
  }

  for (byte i = 0; i < MAXRAY; i++)
    state.prevray[i] = 4095;

  // Willy
  state.wx = pgm_read_byte_near(&l->wx);
  state.wy = pgm_read_byte_near(&l->wy);
  state.wf = pgm_read_byte_near(&l->wf);
  state.wd = pgm_read_byte_near(&l->wd);
  state.jumping = 0;
  state.convey = 0;
  state.wyv = 0;
}

static byte rol8(byte b, byte d)  // 8-bit byte rotate left
{
  d &= 7;
  return ((b << d) | (b >> (8 - d))) & 0xff;
}


void setup()
{
  GD.begin();
  setup_control();
  Serial.begin(1000000);    // JCB
}

void game_graphics()
{
  // Load the Spectrum font: regular in 32-127 reverse in 160-255
  unpack8xn(RAM_CHR + 16 * ' ', specfont, sizeof(specfont));
  unpack8xn(RAM_CHR + 16 * (128 + ' '), specfont, sizeof(specfont));
  for (byte i = 32; i < 128; i++) {
    GD.setpal(4 * i,              BLACK);
    GD.setpal(4 * i + 3,          YELLOW);
    GD.setpal(4 * (128 + i),      COLOR(6));    // dark yellow
    GD.setpal(4 * (128 + i) + 3,  BLACK);
  }

  // AIR display in 32 chars starting at CHR_AIR
  unpack8xn(RAM_CHR + 16 * CHR_AIR, specfont + 8 * ('A' - ' '), 8);
  unpack8xn(RAM_CHR + 16 * (CHR_AIR+1), specfont + 8 * ('I' - ' '), 8);
  unpack8xn(RAM_CHR + 16 * (CHR_AIR+2), specfont + 8 * ('R' - ' '), 8);
  for (byte i = 0; i < 32; i++) {
    uint16_t color = (i < 10) ? RED : GREEN;
    GD.setpal(4 * (CHR_AIR + i), color);
    GD.setpal(4 * (CHR_AIR + i) + 3, WHITE);
  }

  // Load the Spectrum's palette into PALETTE256A
  GD.copy(RAM_SPRPAL, specpal, sizeof(specpal));
  GD.wr16(RAM_SPRPAL + 2 * 255, TRANSPARENT);  // color 255 is transparent

  // fill screen with char CHR_BORDER
  GD.fill(RAM_CHR + 16 * CHR_BORDER, 0, 16);
  GD.fill(RAM_PIC, CHR_BORDER, 64 * 64);

  // Load Willy in bright cyan and white
  for (byte i = 0; i < 4; i++) {
    loadspr16(IMG_WILLYC + i, willy + (i << 5), 255, 8 + 5);
  }
}

// midi frequency table
static PROGMEM prog_uint16_t midifreq[128] = {
32,34,36,38,41,43,46,48,51,55,58,61,65,69,73,77,82,87,92,97,103,110,116,123,130,138,146,155,164,174,184,195,207,220,233,246,261,277,293,311,329,349,369,391,415,440,466,493,523,554,587,622,659,698,739,783,830,880,932,987,1046,1108,1174,1244,1318,1396,1479,1567,1661,1760,1864,1975,2093,2217,2349,2489,2637,2793,2959,3135,3322,3520,3729,3951,4186,4434,4698,4978,5274,5587,5919,6271,6644,7040,7458,7902,8372,8869,9397,9956,10548,11175,11839,12543,13289,14080,14917,15804,16744,17739,18794,19912,21096,22350,23679,25087,26579,28160,29834,31608,33488,35479,37589,39824,42192,44701,47359,50175
};
#define MIDI(n) pgm_read_word(midifreq + (n))

static void squarewave(byte voice, uint16_t f0, byte amp)
{
  GD.voice(8*voice + 0, 0, f0,     amp,    amp);
  GD.voice(8*voice + 1, 0, 3 * f0, amp/3,  amp/3);
  GD.voice(8*voice + 2, 0, 5 * f0, amp/5,  amp/5);
  GD.voice(8*voice + 3, 0, 7 * f0, amp/7,  amp/7);
  GD.voice(8*voice + 4, 0, 9 * f0, amp/9,  amp/9);
  GD.voice(8*voice + 5, 0, 11 * f0, amp/11,  amp/11);
}

static void drive_level(byte t);

static int title_screen() // returns 1 if player pressed something
{
  for (;;) {
display:
    clear_screen();
    GD.fill(0x4000, 0, 6144);   // clear emulated Spectrum video RAM
    GD.uncompress(0x7000, spectrum_tables);

    // fill screen with char 0x80 for border
    GD.setpal(4 * 0x80, COLOR(2));
    GD.fill(RAM_CHR, 0, 4096);
    GD.fill(RAM_PIC, 0x80, 4096);

    // paint the 256x192 window as alternating lines of
    // chars 0-31 and 32-63
    for (byte y = 0; y < 24; y += 2) {
      for (byte x = 0; x < 32; x++) {
        GD.wr(atxy(x, y), x);
        GD.wr(atxy(x, y + 1), 32 + x);
      }
    }
    GD.microcode(spectrum_code, sizeof(spectrum_code));
    GD.uncompress(0x4000, screen_mm1);

    for (prog_uint32_t *tune = blue_danube;
         (size_t)tune < ((size_t)blue_danube + sizeof(blue_danube));
         tune++) {
      uint32_t cmd = pgm_read_dword_near(tune);
      for (int t = 60 * (cmd >> 28); t; t--) {
        delay(1);
        if (control())
          goto escape;
      }
      byte n0 = 127 & (cmd >> 21);
      byte a0 = 127 & (cmd >> 14);
      byte n1 = 127 & (cmd >> 7);
      byte a1 = 127 & cmd;
      squarewave(0, MIDI(n0), a0 >> 1);
      squarewave(1, MIDI(n1), a1 >> 1);
      GD.setpal(4 * 0x80, COLOR((n1 ^ n0) & 7));
    }
    GD.setpal(4 * 0x80, COLOR(7));
    // GD.screenshot(ss++);    // JCB

    for (byte i = 0; i < ((sizeof(message) - 1) - 32); i++) {
      prog_uchar *src = message + i;
      for (byte j = 0; j < 32; j++) {
        byte ch = pgm_read_byte_near(src + j);
        prog_uchar *glyph = specfont + ((ch - ' ') << 3);
        for (byte k = 0; k < 8; k++)
          GD.wr(0x5060 + j + (k << 8), pgm_read_byte_near(glyph++));
      }
      for (byte t = 80; t; t--) {
        delay(1);
        if (control())
          goto escape;
      }
    }

    GD.wr(J1_RESET, 1); // stop coprocessor
    clear_screen(24);
    game_graphics();

    for (state.level = 0; state.level < 19; state.level++) {
      loadlevel(&levels[state.level]);
      for (byte t = 0; t < 128; t++) {
        drive_level(t);
        GD.waitvblank();
        if (control())
          goto display;
      }
    }
  }

escape:
  GD.wr(J1_RESET, 1); // stop coprocessor
  clear_screen(24);
  game_graphics();
}

static void game_over()
{
  clear_screen();
  // Willy
  loadspr16(0, willy, 255, 8 + 7);
  sprite(0, 124, 128 - 32, 0);

  // plinth
  loadspr16(2, plinth, 255, 8 + 7);
  sprite(2, 120, 128 - 16, 2);

  // Boot
  loadspr16(1, boot, 255, 8 + 7);
  loadspr16(3, boot, 255, 8 + 7);   // sprite 3 is top 2 lines of boot
  // erase the bottom 14 lines of boot
  GD.fill(RAM_SPRIMG + 3 * 256 + 2 * 16, 255, 14 * 16);

  for (byte i = 0; i <= 48; i++) {
    sprite(1, 120, 2 * i, 1);         // boot in sprite 1
    sprite(3 + i, 120, 2 * i, 3);     // leg in sprites 3,4, etc
    if (41 <= i) // erase Willy as boot covers him
      GD.fill(RAM_SPRIMG + 32 * (i - 41), 255, 32);
    if ((i & 1) == 0)
      GD.wr16(RAM_PAL, COLOR(random(7)));
    squarewave(0, 400 + 4 * i, 150);
    pause(2);
  }
  GD.wr16(RAM_PAL, BLACK);
  squarewave(0, 0, 0);

  // "Game Over" text in sprite images 16-23, color 16-23
  char msg[] = "GameOver";
  for (byte i = 0; i < 8; i++)
    loadspr8(16 + i, specfont + (msg[i] - 32) * 8, 255, 16 + i);
  for (byte i = 0; i < 4; i++) {
    sprite(100 + i, 80 + i * 8, 48, 16 + i);
    sprite(104 + i, 140 + i * 8, 48, 20 + i);
  }
  for (byte t = 120; t; t--) {
    for (byte i = 0; i < 8; i++)
      GD.wr16(RAM_SPRPAL + (16 + i) * 2, COLOR(9 + random(7)));
    pause(2);
  }
  clear_screen();
}

// crumble block at s, which is the sequence
// 2->8->9->10->11->12->13->14->0

static void crumble(uint16_t s)
{
  byte r = GD.rd(s);
  signed char nexts[] = {
    -1, -1,  8, -1, -1, -1, -1, -1,
    9,  10, 11, 12, 13, 14, 0 };
  if (r < sizeof(nexts) && nexts[r] != -1)
    GD.wr(s, nexts[r]);
}

// is it legal for willy to be at (x,y)
static int canbe(byte x, byte y)
{
  uint16_t addr = atxy(x / 8, y / 8);
  return
    (GD.rd(addr) != ELEM_WALL) &&
    (GD.rd(addr+1) != ELEM_WALL) &&
    (GD.rd(addr+64) != ELEM_WALL) &&
    (GD.rd(addr+65) != ELEM_WALL);
}

// is Willy standing in a solar ray?
static int inray(byte x, byte y)
{
  uint16_t addr = atxy(x / 8, y / 8);
  return
    (GD.rd(addr) > 0x80 ) ||
    (GD.rd(addr+1) > 0x80) ||
    (GD.rd(addr+64) > 0x80) ||
    (GD.rd(addr+65) > 0x80);
}

static void drive_level(byte t)
{
  uint16_t freq = 133955L / pgm_read_byte_near(music1 + ((t >> 1) & 63));
  GD.waitvblank();
  squarewave(0, freq, 128);
  GD.waitvblank();
  squarewave(0, freq, 0);
  GD.waitvblank();
  GD.waitvblank();

  guardian *pg;
  for (byte i = 0; i < 8; i++) {
    pg = &guards[i];
    if (pg->a) {
      byte vertical = (i >= 4);
      byte frame;
      switch (state.level) {
      case 13:  // Skylabs
        if (pg->y != pg->x1) {
          pg->f = 0;
          pg->y += pg->d;
        } else {
          pg->f++;
        }
        if (pg->f == 8) {
          pg->f = 0;
          pg->x += 64;
          pg->y = pg->x0;
        }
        loadspr16(IMG_GUARD + i, state.guardian + (pg->f << 5), 255, INK(pg->a));
        sprite(IMG_GUARD + i, pg->x, pg->y, IMG_GUARD + i);
        break;
      default:
        if (state.bidir)
          frame = (t & 3) ^ (pg->d ? 7 : 0);
        else
          if (vertical)
            frame = t & 3;
          else
            frame = 4 + (t & 3) ^ (pg->d ? 3 : 0);
        loadspr16(IMG_GUARD + i, state.guardian + (frame << 5), 255, INK(pg->a));
        sprite(IMG_GUARD + i, pg->x, pg->y, IMG_GUARD + i);
        if (!vertical) {
          if ((t & 3) == 3) {
            if (pg->x == pg->x0 && pg->d)
              pg->d = 0;
            else if (pg->x == pg->x1 && !pg->d)
              pg->d = 1;
            else
              pg->x += pg->d ? -8 : 8;
          }
        } else {
          if (pg->y <= pg->x0 && pg->d < 0)
            pg->d = -pg->d;
          else if (pg->y >= pg->x1 && pg->d > 0)
            pg->d = -pg->d;
          else
            pg->y += pg->d;
        }
      }
    }
  }
  pg = &guards[4];
  switch (state.level) { // Special level handling
  case 4: // Eugene's lair
    sprite(IMG_GUARD + 4, pg->x, pg->y, IMG_GUARD + 4);
    if (pg->y == pg->x0 && pg->d < 0)
      pg->d = 1;
    if (pg->y == pg->x1 && pg->d > 0)
      pg->d = -1;
    if (state.nitems == 0) {  // all collected -> descend and camp
      if (pg->d == -1)
        pg->d = 1;
      if (pg->y == pg->x1)
        pg->d = 0;
    }
    pg->y += pg->d;
    break;
  case 7: // Miner Willy meets the Kong Beast
  case 11: //  Return of the Alien Kong Beast
    byte frame, color;
    if (!state.switch2) {
      frame = (t >> 3) & 1;
      color = 8 + 4;
    } else {
      frame = 2 + ((t >> 1) & 1);
      color = 8 + 6;
      if (pg->y < 104) {
        pg->y += 4;
        bump_score(100);
      }
    }
    if (pg->y != 104) {
      loadspr16(IMG_GUARD + 4, state.guardian + (frame << 5), 255, color);
      sprite(IMG_GUARD + 4, pg->x, pg->y, IMG_GUARD + 4);
    } else {
      hide(IMG_GUARD + 4);
    }
  }

  // Animate conveyors: 
  signed char convt = state.conveyordir ? t : -t;
  GD.__wstart(RAM_CHR + 4 * 16 + 2 * 0);  // character 4 line 0
  unpack8(rol8(state.conveyor[0], convt));
  GD.__end();
  GD.__wstart(RAM_CHR + 4 * 16 + 2 * 3);  // character 4 line 3
  unpack8(rol8(state.conveyor[0], -convt));
  GD.__end();

  // Solar rays
  if (state.level == 18) {
    // Draw new ray, turning 0x00 to 0x80
    byte sx = 23;
    byte sy = 0;
    byte sd = 0; // down
    byte ri;     // ray index
    for (ri = 0; ri < MAXRAY; ri++) {
      uint16_t a = atxy(sx, sy);
      if (state.prevray[ri] != a) {
        GD.wr(state.prevray[ri], 0);
        if (GD.rd(a) != 0)
          break;  // absorbed
        GD.wr(a, 0x80 + ' ');
        state.prevray[ri] = a;
      }
      for (byte i = 0; i < 8; i++)
        if (guards[i].a && (guards[i].x >> 3) == sx && (guards[i].y >> 3) == sy)
          sd ^= 1;
      if (sd == 0)
        sy++;
      else
        sx--;
    }
    while (ri < MAXRAY) {
      GD.wr(state.prevray[ri], 0);
      state.prevray[ri++] = 4095;
    }
  }

  // animate item colors starting at 16
  uint16_t colors[4] = { MAGENTA, YELLOW, CYAN, GREEN };
  for (byte i = 0; i < 5; i++)
    GD.wr16(RAM_SPRPAL + 2 * (i + 16), colors[(i + t) & 3]);

  // when all items collected,
  // flash portal in colors 32,33 - the thing the Spectrum *can* do in hardware
  byte flip = (t >> 3) & (CHEAT_OPEN_PORTAL || state.nitems == 0);
  GD.wr16(RAM_SPRPAL + 2 * (32 ^ flip), COLOR(PAPER(state.portalattr)));
  GD.wr16(RAM_SPRPAL + 2 * (33 ^ flip), COLOR(INK(state.portalattr)));

  // animated lives count, draw up to seven
  for (byte i = 0; i < 7; i++)
    if (i < (state.lives - 1))
      sprite(IMG_WILLYC + i, i << 4, 168, IMG_WILLYC + ((t >> 2) & 3));
    else
      hide(IMG_WILLYC + i);

  GD.waitvblank();
}

// play a complete game
void loop()
{
  title_screen();
  game_graphics();

  state.level = START_LEVEL;
  state.score = 0;
  for (state.lives = 3; state.lives; state.lives--) {
    loadlevel(&levels[state.level]);
    // state.wy = 16;
    byte alive = 1;
    byte t = 0;
    while (alive) {
      drive_level(t);

      // Willy draw
      byte frame = state.wf ^ (state.wd ? 7 : 0);
      loadspr16(IMG_WILLY, willy + (frame << 5), 255, 8 + 7);
      sprite(IMG_WILLY, state.wx, state.wy, IMG_WILLY);

      // Willy movement
      // See http://www.seasip.demon.co.uk/Jsw/manic.mac
      // and http://jetsetwilly.jodi.org/poke.html

      byte con = control();
      byte ychanged = 0;  // if Y block changes must check for landing later
      if (state.jumping) {
#define JUMP_APEX 9   
#define JUMP_FALL 11  //  1   2   3   4   5   6   7   8   9  10 11
        signed char moves[] = {  -4, -4, -3, -3, -2, -2, -1, -1, 0, 0, 1, 1, 2, 2, 3, 3, 4 };
        byte index = min(sizeof(moves) - 1, state.jumping - 1);
        byte newy = state.wy + moves[index];
        state.jumping++;
        if (canbe(state.wx, newy)) {
          ychanged = (state.wy >> 3) != (newy >> 3);
          state.wy = newy;
        } else {
          state.jumping = max(state.jumping, JUMP_FALL);   // halt ascent
          ychanged = 1;
        }
      }
      uint16_t feet_addr = atxy(state.wx >> 3, (state.wy + 16) >> 3);
      byte elem0 = GD.rd(feet_addr);
      byte elem1 = GD.rd(feet_addr + 1);
      byte elem = ((1 <= elem0) && (elem0 <= 31)) ? elem0 : elem1;

      byte dx = 0xff;
      byte first_jump = (con & CONTROL_JUMP) && state.lastdx == 0xff;
      if (state.jumping) {
        dx = state.lastdx;
      } else if (!first_jump && (elem == ELEM_CONVEYOR) && (state.wd == state.conveyordir)) {
        dx = state.conveyordir;
      } else {
        if (con & CONTROL_RIGHT) {
          if (state.wd != 0) {
            state.wf ^= 3;
            state.wd = 0;
          } else {
            dx = 0;
          }
        } else if (con & CONTROL_LEFT) {
          if (state.wd == 0) {
            state.wf ^= 3;
            state.wd = 1;
          } else {
            dx = 1;
          }
        }
      }
      if (dx != 0xff) {
        if (state.wf != 3)
          state.wf++;
        else {
          byte newx = state.wx + (dx ? -8 : 8);
          if (canbe(newx, state.wy)) {
            state.wf = 0;
            state.wx = newx;
          }
        }
      }
      state.lastdx = dx;

      if ((elem == ELEM_CONVEYOR) && (dx == 0xff) && !state.jumping)
        state.wd = state.conveyordir;

      if (!state.jumping && (con & CONTROL_JUMP)) {
        if (canbe(state.wx, state.wy - 3)) {
          state.jumping = 1;
          state.wy -= 0;
        }
      }

      byte onground = ((1 <= elem) && (elem <= 4)) || ((7 <= elem) && (elem <= 16));
      if (state.jumping) {
        if ((JUMP_APEX <= state.jumping) && ychanged && onground) {
          state.jumping = 0;
          state.wy &= ~7;
        }
        if (state.jumping >= 16)    // stop x movement late in the jump
          state.lastdx = 0xff;
      } else {
        if (!onground) {    // nothing to stand on, start falling
          state.jumping = JUMP_FALL;
          state.lastdx = 0xff;
        }
      }
      if (!state.jumping) {
        crumble(feet_addr);
        crumble(feet_addr + 1);
      }

      if (((t & 7) == 0) ||
         ((state.level == 18) && inray(state.wx, state.wy))) {
        state.air--;
        plot_air();
        if (state.air == 0) {
          alive = 0;
        }
      }
      GD.waitvblank();    // let the screen display, then check for collisions
      byte coll = GD.rd(COLLISION + IMG_WILLY);
      if (coll <= (IMG_ITEM + 4)) {
        bump_score(100);
        hide(coll - IMG_ITEM);
        --state.nitems;
      } else if (coll == IMG_PORTAL) {
        if (CHEAT_OPEN_PORTAL || state.nitems == 0) {
          while (state.air) {
            squarewave(0, 800 + 2 * state.air, 100);
            state.air--;
            plot_air();
            bump_score(7);
            GD.waitvblank();
          }
          state.level = (state.level + 1) % 18;
          loadlevel(&levels[state.level]);
        }
      } else if (coll == IMG_SWITCH1) {
        if (!state.switch1) {
          state.switch1 = 1;
          sprite(IMG_SWITCH1, 48 - 8, 0, IMG_SWITCH1, 2);
          GD.wr(atxy(17, 11), 0);
          GD.wr(atxy(17, 12), 0);
        }
      } else if (coll == IMG_SWITCH2) {
        if (coll == IMG_SWITCH2) {
          state.switch2 = 1;
          sprite(IMG_SWITCH2, 144 - 8, 0, IMG_SWITCH2, 2);
          GD.wr(atxy(15, 2), 0);
          GD.wr(atxy(16, 2), 0);
        }
      } else if (coll != 0xff && !CHEAT_INVINCIBLE) {
        alive = 0;
      }
      t++;
    }
    // player died
    clear_screen();
  }
  game_over();
}
