Space invaders with a nRF2401A wireless joypad

Dependencies:   Gameduino mbed nRF2401A

Fork of Gameduino_Invaders_game by Chris Dick

Gameduino and an nRF2401A hooked up to an mbed on an mbeduino:

/media/uploads/TheChrisyd/2014-03-08_22.53.54.jpg

Revision:
0:8a7c58553b44
Child:
1:f44175dd69fd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/game.cpp	Thu Jun 21 19:13:34 2012 +0000
@@ -0,0 +1,1095 @@
+#include "game.h"
+
+
+SPI spigame(ARD_MOSI, ARD_MISO, ARD_SCK); // mosi, miso, sclk
+/*---------------------------------------------
+  Trivia: There is NO random number generator
+  anywhere in Space Invaders....
+---------------------------------------------*/
+
+
+/*---------------------------------------------
+  Global definitions
+---------------------------------------------*/
+#define invaderRows 5
+#define invadersPerRow 11
+#define numInvaders (invaderRows*invadersPerRow)
+
+// Positions of things on screen
+// nb. Space Invaders screen is 256x224 pixels
+#define screenTop 24
+#define screenLeft 88
+#define screenWidth 224
+#define screenHeight 256
+// Player
+#define playerMinLeft 18
+#define playerMaxRight 188
+#define playerYpos 216
+#define playerSpeed 1
+// Bullet
+#define bulletHeight 4
+#define bulletSpeed 4
+#define bulletTop 35
+// Invaders
+#define invaderAppearX 26
+#define invaderAppearY 64
+#define invaderXspacing 16
+#define invaderYspacing 16
+#define invaderXstep 2
+#define invaderYstep 8
+#define invaderXmin 10
+#define invaderXmax 202
+// Saucer
+#define saucerYpos 42
+#define saucerSpeed 1
+#define saucerXmin 0
+#define saucerXmax (screenWidth-16)
+#define saucerSkip 3
+#define saucerFrequency (25*72)
+// Shields
+#define numShields 4
+#define shieldXpos 32
+#define shieldYpos 192 
+#define shieldXstep 45
+// Bombs
+#define bombSpeed 1
+#define bombYmax 230
+
+/*-------------------------------------------------------
+  Sprite allocation list
+
+  nb. Sprite order is important for collision detection
+-------------------------------------------------------*/
+enum sprite_id {
+  SP_PLAYER,
+  SP_FIRST_SHIELD,
+  SP_LAST_SHIELD    = SP_FIRST_SHIELD+(2*numShields)-1,
+  // Invader bombs (can hit shields and player)
+  SP_BOMB1,  // nb. There's only three bombs in Space invaders...
+  SP_BOMB2,
+  SP_BOMB3,
+  // Invaders (can't be hit by their own bombs)
+  SP_FIRST_INVADER,
+  SP_LAST_INVADER   = SP_FIRST_INVADER+numInvaders-1,
+  // Flying saucer (needs two sprites because it's very wide...)
+  SP_SAUCER1,
+  SP_SAUCER2,
+  // Bullet (last ... because it can hit anything)
+  SP_BULLET
+};
+
+
+/*---------------------------------------------
+  Global vars
+---------------------------------------------*/
+// Joystick object
+Joystick joystick;
+
+// This increments once per frame
+static unsigned int frameCounter;
+
+// The current wave of invaders [0..n]
+static unsigned int invaderWave;
+
+// Number of lives the player has left...
+static byte numLives;
+
+// Player's score...
+static unsigned int playerScore;
+
+// High score
+static unsigned int highScore;
+
+// Number of living space invaders
+static unsigned int remainingInvaders;
+
+// Timer for the background heartbeat sound
+static int beatCounter;
+
+/*---------------------------------------------
+  General functions
+---------------------------------------------*/
+static PROGMEM prog_char scoreMsg[] = "Score";
+static PROGMEM prog_char hiScoreMsg[] = "Hi-Score";
+static unsigned int previousPlayerScore, previousHighScore;
+void redrawScores()
+{
+  previousPlayerScore = previousHighScore = 0xffff;
+}
+
+unsigned int putDigit(unsigned int s, unsigned int d)
+{
+  byte c = '0';
+  while (s >= d) {
+    ++c;
+    s -= d;
+  }
+  spigame.write(c);
+  return s;
+}
+void printScore(int8 x, const prog_char *m, unsigned int s, int8 xoff)
+{
+  x += screenLeft/8;
+  int y = screenTop/8;
+  unsigned int addr = (y*64)+x;
+  GD.__wstart(addr);
+  char c = *m;
+  while (c != 0) {
+    spigame.write(c);
+    c = *m++;
+  }
+  GD.__end();
+  addr += (2*64)+xoff;
+  GD.__wstart(addr);
+  s = putDigit(s,10000);
+  s = putDigit(s,1000);
+  s = putDigit(s,100);
+  s = putDigit(s,10);
+  spigame.write(s+'0');
+  GD.__end();
+}
+void updateScore()
+{
+  if (playerScore != previousPlayerScore) {
+    printScore(0,scoreMsg,playerScore,0);
+    previousPlayerScore = playerScore;
+    if (playerScore > highScore) {
+      highScore = playerScore;
+    }
+  }
+  if (highScore != previousHighScore) {
+    printScore(20,hiScoreMsg,highScore,3);
+    previousHighScore = highScore;
+  }
+}
+
+static unsigned short int prevLives;
+static void redrawBases()
+{
+  prevLives = 0xffff;
+}
+void updateRemainingBases()
+{
+  if (numLives != prevLives) {
+    prevLives = numLives;
+    GD.__wstart((64*((screenTop+240)>>3))+(screenLeft>>3));
+    spigame.write(numLives+'0');
+    spigame.write(0);
+    for (byte i=1; i<numLives; ++i) {
+      spigame.write(CH_PLAYERL);
+      spigame.write(CH_PLAYERR);
+    }
+    spigame.write(0);
+    spigame.write(0);
+    GD.__end();
+  }
+}
+
+/*---------------------------------------------
+  A generic object in the game
+---------------------------------------------*/
+enum object_status {
+  S_WAITING,
+  S_ALIVE,
+  S_DYING,
+  S_DEAD
+};
+
+struct GameObject {
+  byte sprite;     // Which sprite to use for my graphic (see "sprite_id")
+  byte status;     // What I'm doing at the moment
+  int xpos,ypos;   // Position on screen
+  // State of objects in the game
+  void initialize(byte s, object_status t=S_WAITING, int x=400, int y=0) {
+    sprite = s;
+    status = t;
+    xpos = x;
+    ypos = y;
+    updateSprite(0,0);
+  }
+  void updateSprite(byte img, byte frame) {
+    GD.sprite(sprite,xpos+screenLeft,ypos+screenTop,img,8+(frame<<1),0,0);
+  }
+  void doubleSprite(byte img1, byte frame1, byte img2, byte frame2, int8 xoff) {
+    int x = xpos+screenLeft+xoff;
+    int y = ypos+screenTop;
+    GD.sprite(sprite,  x,   y,img1,8+(frame1<<1),0,0);
+    GD.sprite(sprite+1,x+16,y,img2,8+(frame2<<1),0,0);
+  }
+  byte collision() {
+    return GD.rd(0x2900+sprite);
+  }
+};
+
+/*---------------------------------------------
+ Player's bullet
+---------------------------------------------*/
+// Forward references to functions
+bool killInvader(byte spriteNumber);
+void shootShield(byte spriteNumber, int bulletX);
+void shootSaucer();
+void shootBomb(byte spriteNumber);
+void incSaucerCounter();
+
+class BulletObject : GameObject {
+  byte timer;
+  bool visibleDeath;
+  void die(bool v) {
+    visibleDeath = v;
+    status = S_DYING;
+    timer = 12;
+  }
+public:
+  void reset() {
+    initialize(SP_BULLET);
+    updateSprite(GR_BULLET,3);
+    timer = 0;
+  }
+  void fire(GameObject& p) {
+    if (status == S_WAITING){
+      status = S_ALIVE;
+      xpos = p.xpos;
+      ypos = p.ypos+bulletSpeed-bulletHeight;
+      playerShootSound = true;
+    }
+  }
+  void update() {
+    int frame = 3;
+    switch (status) {
+      case S_ALIVE:  ypos -= bulletSpeed;
+                     if (ypos <= bulletTop) {
+                       ypos = bulletTop;
+                       die(true);
+                       frame = 1;
+                     }
+                     else {
+                       frame = 0;
+                     }
+                     break;
+      case S_DYING:  if (!--timer) {
+                       status = S_WAITING;
+                       incSaucerCounter();
+                     }
+                     else if (visibleDeath) {
+                       frame = 1;
+                     }
+                     break;
+    }
+    updateSprite(GR_BULLET,frame);
+  }
+  void setY(int y) {
+    if (status == S_DYING) {
+      ypos = y;
+      updateSprite(GR_BULLET,1);
+      //GD.wr16(SCROLL_Y,GD.rd16(SCROLL_Y)+1);
+    }
+  }
+  // See if the bullet hit anything
+  void collide() {
+    if (status == S_ALIVE) {
+      byte b = collision();
+      if (b != 0xff) {
+        if ((b >= SP_FIRST_INVADER) and (b <= SP_LAST_INVADER)) {
+          if (killInvader(b)) {
+            die(false);
+          }
+        }
+        if ((b >= SP_FIRST_SHIELD) and (b <= SP_LAST_SHIELD)) {
+          shootShield(b,xpos);
+          die(true);
+        }
+        if ((b >= SP_SAUCER1) and (b <= SP_SAUCER2)) {
+          shootSaucer();
+          die(false);
+        }
+        if ((b >= SP_BOMB1) and (b <= SP_BOMB3)) {
+          shootBomb(b);
+          die(false);
+        }
+      }
+    }
+  }
+} bullet;
+
+/*---------------------------------------------
+ The player
+---------------------------------------------*/
+class Player : public GameObject {
+  byte timer;
+public:
+  void reset() {
+    timer = 2*numInvaders;
+    initialize(SP_PLAYER,S_WAITING,playerMinLeft,playerYpos);
+    updateSprite(GR_PLAYER,3);
+  }
+
+  void update() {
+    int frame = 3;
+    switch (status) {
+      case S_WAITING: xpos = playerMinLeft;
+                      ypos = playerYpos;
+                      if (!--timer) {
+                        status = S_ALIVE;
+                      }
+                      break;
+      case S_ALIVE:   if (joystick.left()) {
+                        xpos -= playerSpeed;
+                        if (xpos < playerMinLeft) {
+                          xpos = playerMinLeft;
+                        }
+                      }
+                      if (joystick.right()) {
+                        xpos += playerSpeed;
+                        if (xpos > playerMaxRight) {
+                          xpos = playerMaxRight;
+                        }
+                      }
+                      { byte n = Joystick::buttonA|Joystick::buttonB;
+                        if (joystick.isPressed(n) and joystick.changed(n)) {
+                          bullet.fire(*this);
+                        }
+                      }
+                      frame = 0;
+                      break;
+      case S_DYING:   if (!--timer) {
+                        timer = 3*remainingInvaders;
+                        status = (--numLives>0)? S_WAITING: S_DEAD;
+                      }
+                      else {
+                        frame = ((frameCounter&4)==0)? 1:2;
+                      }
+                      break;
+    }
+    updateSprite(GR_PLAYER,frame);
+  }
+  void kill() {
+    if (status == S_ALIVE) {
+      status = S_DYING;
+      timer = 50;
+      playerDeathSound = true;
+    }
+  }
+  bool isAlive() {
+    return (status==S_ALIVE);
+  }
+  bool isDying() {
+    return (status==S_DYING);
+  }
+  bool isDead() {
+    return (status==S_DEAD);
+  }
+  void wakeUp() {
+  }
+} player;
+
+/*---------------------------------------------
+  "Shields" for the player to hide behind
+---------------------------------------------*/
+class Shields {
+  struct BlastInfo {
+    byte sprite;
+    int xpos;
+    void reset() {
+      sprite = 255;
+    }
+    bool hasBlast() const {
+      return (sprite!=255);
+    }
+    void blast(byte s, int x) {
+      sprite = s;
+      xpos = x;
+    }
+  };
+  BlastInfo bulletBlast, bombBlast[3];
+  void blastShield(BlastInfo& n, bool asBullet) {
+    if (n.hasBlast()) {
+      byte s = (n.sprite-SP_FIRST_SHIELD)>>1;
+      int8 x = int8(n.xpos-(shieldXpos+(s*shieldXstep)));
+      //int8 y = zapShield(s,x,asBullet);
+      if (asBullet) {
+        //bullet.setY(shieldYpos+y);
+      }
+      n.reset();
+    }
+  }
+public:
+  void reset() {
+    remakeShields();
+    int x = shieldXpos;
+    byte s = SP_FIRST_SHIELD;
+    for (int i=0; i<numShields; ++i) {
+      GD.sprite(s,  x+screenLeft,   shieldYpos+screenTop,GR_SHIELD1+i,8+0,0,0);
+      GD.sprite(s+1,x+screenLeft+16,shieldYpos+screenTop,GR_SHIELD1+i,8+2,0,0);
+      x += shieldXstep;
+      s += 2;
+    }
+    bulletBlast.reset();
+    for (int8 i=0; i<3; ++i) {
+      bombBlast[i].reset();
+    }
+  }
+  void update() {
+    blastShield(bulletBlast,true);
+    for (int8 i=0; i<3; ++i) {
+      blastShield(bombBlast[i],false);
+    }    
+  }
+  // Zap them in various ways
+  // nb. We defer the action because updating the sprites
+  // might be slow and we want to make sure all collision
+  // detection happens in the vertical blank
+  void shoot(byte s, int x) {
+    bulletBlast.blast(s,x);
+  }
+  void bomb(byte s, int x) {
+    for (int8 i=0; i<3; ++i) {
+      BlastInfo& b = bombBlast[i];
+      if (!b.hasBlast()) {
+        b.blast(s,x);
+        break;
+      }
+    }
+  }
+} shields;
+void shootShield(byte sprite, int bulletX)
+{
+  shields.shoot(sprite,bulletX);
+}
+
+/*---------------------------------------------
+  Flying saucer
+  
+  The score for the saucer depends on how
+  many bullets you've fired. If you want
+  a good score hit it with bullet 22 and
+  every 15th bullet after that.
+  
+  The direction of the saucer also depends
+  on the bullet count. If you're counting
+  bullets then note the the saucer will
+  appear on alternate sides and you can
+  be ready for it.
+ 
+  Repeat after me: There are NO random
+  numbers in Space Invaders.
+---------------------------------------------*/
+static PROGMEM prog_uchar saucerScores[15] = {
+  // nb. There's only one '300' here...
+  10,5,10,15,10,10,5,30,10,10,10,5,15,10,5
+};
+class Saucer : GameObject {
+  byte timer, scoreTimer;
+  byte score;
+  byte bulletCounter;
+  unsigned int timeUntilNextSaucer;
+  bool leftRight,goingRight,showingScore;
+  void startWaiting() {
+    status = S_WAITING;
+    timeUntilNextSaucer = saucerFrequency;
+  }
+public:
+  void reset() {
+    initialize(SP_SAUCER1);
+    timer = 1;
+    ypos = saucerYpos;
+    showingScore = false;
+    bulletCounter = 0;
+    leftRight = true;
+    timeUntilNextSaucer = saucerFrequency;
+  }
+  void update() {
+    int xoff=0;
+    byte gr1=GR_SAUCER, gr2=gr1;
+    byte fr1=3, fr2=fr1;  // Blank sprite
+    switch (status) {
+      case S_WAITING: if ((remainingInvaders>7) and !--timeUntilNextSaucer) {
+                        status = S_ALIVE;
+                        timer = saucerSkip;
+                        goingRight = leftRight;
+                        if (goingRight) {
+                          xpos = saucerXmin-saucerSpeed;
+                        }
+                        else {
+                          xpos = saucerXmax+saucerSpeed;
+                        }
+                        saucerSound = true;
+                      }
+                      else {
+                        stopSaucerSnd = true;
+                      }
+                      break;
+      case S_ALIVE:   if (!--timer) {
+                        // The player has to go faster then the saucer so we skip frames...
+                        timer = saucerSkip;
+                      }
+                      else {
+                        if (goingRight) {
+                          xpos += saucerSpeed;
+                          if (xpos > saucerXmax) {
+                            startWaiting();
+                          }
+                        }
+                        else {
+                          xpos -= saucerSpeed;
+                          if (xpos < saucerXmin) {
+                            startWaiting();
+                          }
+                        }
+                      }
+                      fr1 = 0;    // Normal saucer
+                      break;
+      case S_DYING:   if (!--timer) {
+                        if (showingScore) {
+                          startWaiting();
+                        }
+                        else {
+                          timer = 60;
+                          showingScore = true;
+                          playerScore += score*10;
+                        }
+                      }
+                      else {
+                        if (showingScore) {
+                          xoff = -5;
+                          gr1 = GR_SAUCER_SCORE;
+                          gr2 = GR_BULLET;    fr2 = 2;
+                          if (score == 5) { fr1=0; xoff-=4;}
+                          else if (score == 10) { fr1 = 1; }
+                          else if (score == 15) { fr1 = 2; }
+                          else if (score == 30) { fr1 = 3; }
+                        }
+                        else {
+                          fr1 = 1;    // Explosion left
+                          fr2 = 2;    // Explosion right
+                          xoff = -5;  // Move it a bit to the left
+                        }
+                      }
+                      break;
+    }
+    // Saucer sometimes needs two sprites...
+    doubleSprite(gr1,fr1,gr2,fr2,xoff);
+  }
+  void incCounter() {
+    if (++bulletCounter == 15) {
+      bulletCounter = 0;
+    }
+    leftRight = !leftRight;
+  }
+  void kill() {
+    status = S_DYING;
+    timer = 36;
+    saucerDieSound = true;
+    showingScore = false;
+    score = *saucerScores+bulletCounter;
+  }
+} saucer;
+
+void incSaucerCounter()
+{
+  saucer.incCounter();
+}
+void shootSaucer()
+{
+  saucer.kill();
+}
+/*---------------------------------------------
+  A space invader...
+---------------------------------------------*/
+enum invader_type {
+  INVADER_T,    // Top-row invader
+  INVADER_M,    // Middle-row invader
+  INVADER_B,    // Bottom-row invader
+  NUM_INVADER_TYPES
+};
+static PROGMEM prog_uchar invaderGraphic[NUM_INVADER_TYPES] = {
+  GR_INVADER_T, GR_INVADER_M, GR_INVADER_B
+};
+
+static PROGMEM prog_uchar invaderScore[NUM_INVADER_TYPES] = {
+  30, 20, 10
+};
+
+class Invader : public GameObject {
+  // Bitmasks for my vars
+  enum var_bits {
+    TYPEMASK = 0x0003,    // Type of invader, 0=top row, 1=middle row, 2=bottom row
+    ANIM     = 0x0010,    // Flip-flop for animation frame
+    GO_RIGHT = 0x0020,    // Horizontal direction
+    GO_DOWN  = 0x0040,    // If I should go downwards next time
+  };
+  byte vars;      // All my vars, packed together
+
+  byte readTable(const prog_uchar *t) {
+    return (*t + (vars&TYPEMASK));
+  }
+  void updateTheSprite() {
+    byte img = readTable(invaderGraphic);
+    byte fr = 3;    // Invisible...
+    switch (status) {
+      case S_ALIVE:   fr = (vars&ANIM)? 0:1;  // Two frame animation
+                      break;
+      case S_DYING:   fr = 2;                 // Explosion graphic
+                      break;
+    }
+    updateSprite(img,fr);
+  }
+public:
+  
+  bool isAlive() const {
+    return ((status==S_WAITING) or (status==S_ALIVE));
+  }
+  void goDown() {
+    vars |= GO_DOWN;
+  }
+
+  // Put me on screen at (x,y), set my type and sprite number.
+  // I will be invisible and appear next frame (ie. when you call "update()")
+  void reset(byte sp, int x, int y, invader_type t) {
+    initialize(sp,S_WAITING,x,y);
+    vars = t|GO_RIGHT;
+    updateTheSprite();
+  }
+
+  // Update me, return "true" if I reach the edge of the screen
+  bool update() {
+    bool hitTheEdge = false;
+    switch (status) {
+      case S_WAITING: status = S_ALIVE;
+                      break;
+      case S_ALIVE:   if (vars&GO_DOWN) {
+                        ypos += invaderYstep;
+                        vars &= ~GO_DOWN;
+                        vars ^= GO_RIGHT;
+                      }
+                      else {
+                        if (vars&GO_RIGHT) {
+                          xpos += invaderXstep;
+                          hitTheEdge = (xpos >= invaderXmax);
+                        }
+                        else {
+                          xpos -= invaderXstep;
+                          hitTheEdge = (xpos <= invaderXmin);
+                        }
+                      }
+                      vars = vars^ANIM;  // Animation flipflop
+                      break;
+    }
+    updateTheSprite();
+    return hitTheEdge;
+  }
+  bool die() {
+    bool result = (status==S_ALIVE);
+    if (result) {
+      status = S_DYING;
+      updateTheSprite();
+      playerScore += readTable(invaderScore);
+      alienDeathSound = true;
+    }
+    return result;
+  }
+  void kill() {
+    status = S_DEAD;
+    updateTheSprite();
+    --remainingInvaders;
+  }
+};
+
+/*---------------------------------------------
+  The array of invaders
+---------------------------------------------*/
+// Table for starting height of invaders on each level
+static PROGMEM prog_char invaderHeightTable[] = {
+  1,2,3,3,3,4,4,4
+};
+
+class InvaderList {
+  byte nextInvader;              // The invader to update on the next frame
+  int dyingInvader;             // Which invader is currently dying
+  int8 deathTimer;               // COuntdown during death phase
+  bool anInvaderHitTheEdge;      // When "true" the invaders should go down a line and change direction
+  bool anInvaderReachedTheBottom;// When "true" an invader has landed... Game Over!
+  Invader invader[numInvaders];  // The invaders
+  
+  bool findNextLivingInvader() {
+    // Find next living invader in the array
+    bool foundOne = false;
+    for (int8 i=0; i<numInvaders; ++i) {
+      if (++nextInvader == numInvaders) {
+        // Actions taken after all the invaders have moved
+        nextInvader = 0;
+        if (anInvaderHitTheEdge) {
+          for (int8 j=0; j<numInvaders; ++j) {
+            invader[j].goDown();
+          }
+          anInvaderHitTheEdge = false;
+        }
+      }
+      if (invader[nextInvader].isAlive()) {
+        foundOne = true;
+        break;
+      }
+    }
+    return foundOne;
+  }
+public:
+  void reset(int8 level) {
+    int y = invaderAppearY+(invaderRows*invaderYspacing);
+    if (invaderWave > 0) {
+      char w = (*invaderHeightTable+((invaderWave-1)&7));
+      y += w*invaderYstep;
+    }
+    for (int8 row=0; row<invaderRows; ++row) {
+      int x = invaderAppearX;
+      for (int8 col=0; col<invadersPerRow; ++col) {
+        const int8 index = (row*invadersPerRow)+col;
+        Invader& n = invader[index];
+        invader_type t = INVADER_B;
+        if (row > 1) {  t = INVADER_M;   }
+        if (row > 3) {  t = INVADER_T;   }
+        n.reset(SP_FIRST_INVADER+index,x,y,t);
+        x += invaderXspacing;
+      }
+      y -= invaderYspacing;
+    }
+    remainingInvaders = numInvaders;
+    nextInvader = 0;    // Start updating them here...
+    dyingInvader = -1;
+    deathTimer = 0;
+    anInvaderHitTheEdge = false;
+    anInvaderReachedTheBottom = false;
+  }
+  void update() {
+    if (dyingInvader != -1) {
+      // We stop marching when an invader dies
+      if (!--deathTimer) {
+        invader[dyingInvader].kill();
+        dyingInvader = -1;
+      }
+    }
+    else if (!player.isDying() and (remainingInvaders>0)) {
+      // Update an invader
+      Invader& n = invader[nextInvader];
+      if (n.isAlive()) {
+        // Move the invader
+        if (n.update()) {
+          anInvaderHitTheEdge = true;
+        }
+        if ((n.ypos+8) > player.ypos) {
+          anInvaderReachedTheBottom = true;
+        }
+      }
+      findNextLivingInvader();
+    }
+  }
+  // Kill the invader with sprite 'n'
+  bool kill(byte n) {
+    n -= SP_FIRST_INVADER;
+    bool result = invader[n].die();
+    if (result) {
+      if (dyingInvader != -1) {
+        invader[dyingInvader].kill();
+      }
+      dyingInvader = n;
+      deathTimer = 16;
+    }
+    return result;
+  }
+  int nearestColumnToPlayer() {
+    Invader& n = invader[nextInvader];  // We know this invader is alive so use it as a reference
+    int r = nextInvader%invadersPerRow; // The column this invader is in
+    int left = n.xpos-(r*invaderXspacing);
+    int c = (((player.xpos-left)+(invaderXspacing/2))/invaderXspacing);
+    if ((c>=0) and (c<invadersPerRow)) {
+      return c;
+    }
+    return -1;
+  }
+  const Invader *getColumn(int c) {
+    while ((c>=0) and (c<numInvaders)) {
+      const Invader *v = invader+c;
+      if (v->isAlive()) {
+        return v;
+      }
+      c += invadersPerRow;
+    }
+    return 0;
+  }
+  bool haveLanded() {
+    return anInvaderReachedTheBottom;
+  }
+} invaders;
+
+bool killInvader(byte n)
+{
+  return invaders.kill(n);
+}
+
+/*---------------------------------------------------------
+  Space invader bombs
+  
+  There's three bombs in Space Invaders. Two of them
+  follow a pattern of columns, the other one always
+  appears right above the player (to stop you getting
+  bored...!)
+  
+  Mantra: There are NO random numbers in Space Invaders...
+
+  nb. Column 1 is the most dangerous and column 5
+      isn't in either table... :-)
+---------------------------------------------------------*/
+// Column table for the 'zigzag' bomb
+static prog_char zigzagBombColumns[] = {
+  11,1,6,3,1,1,11,9,2,8,2,11,4,7,10,-1
+};
+// Column table for the bomb with horizontal bars across it
+static prog_char barBombColumns[] = {
+  1,7,1,1,1,4,11,1,6,3,1,1,11,9,2,8,-1
+};
+byte bombTimer;    // Countdown until next bomb can be dropped
+void resetBombTimer()
+{
+  if (!player.isAlive()) {
+    bombTimer = 60;    // We don't drop for this long after you reanimate
+  }
+  else {
+    // You get more bombs as the game progresses :-)
+    if (playerScore < 200)       { bombTimer = 48;  }
+    else if (playerScore < 1000) { bombTimer = 16;  }
+    else if (playerScore < 2000) { bombTimer = 11;  }
+    else if (playerScore < 3000) { bombTimer = 8;   }
+    else                         { bombTimer = 7;   }
+  }
+}
+class Bomb : public GameObject {
+  byte graphic;
+  byte timer;
+  byte cycle;
+  prog_char *columnTable, *tablePtr;
+  bool readyToDrop() {
+    return (bombTimer==0);
+  }
+  int8 getNextColumn() {
+    int c = *tablePtr;
+    if (c == -1) {
+      tablePtr = columnTable;
+      c = *tablePtr;
+    }
+    else {
+      ++tablePtr;
+    }
+    return c-1;
+  }
+public:
+  Bomb() {
+    tablePtr = 0;
+  }
+  bool isAlive() {
+    return (status!=S_WAITING);
+  }
+  void die() {
+    status = S_DYING;
+    timer = 12;
+  }
+  void reset(byte sprite, byte gr, prog_char *ct) {
+    initialize(sprite);
+    graphic = gr;
+    columnTable = ct;
+    if (!tablePtr) {
+      tablePtr = ct;  // Only set this the first time...
+    }
+    cycle = timer = 0;
+    updateSprite(GR_BOMB_OTHER,3);
+  }
+  void update() {
+    byte gr = GR_BOMB_OTHER;
+    byte frame = 3;
+    switch (status) {
+      case S_WAITING: if (bombTimer == 0) {
+                        int c = -1;
+                        if (columnTable) {
+                          // Follow sequence of columns
+                          c = getNextColumn();
+                        }
+                        else {
+                          // Drop me above the player
+                          c = invaders.nearestColumnToPlayer();
+                        }
+                        const Invader *v = invaders.getColumn(c);
+                        if (v) {
+                          status = S_ALIVE;
+                          xpos = v->xpos;
+                          ypos = v->ypos+8;
+                          resetBombTimer();
+                        }
+                      }
+                      break;
+      case S_ALIVE:   ypos += bombSpeed;
+                      if (ypos > bombYmax) {
+                        ypos = bombYmax;
+                        die();
+                      }
+                      gr = graphic;
+                      if (++timer==2) {
+                        ++cycle;
+                        timer = 0;
+                      }
+                      frame = cycle&3;
+                      break;
+      case S_DYING:   if (!--timer) {
+                        status = S_WAITING;
+                      }
+                      else {
+                        frame = 0;  // Bomb blast graphic
+                      }
+                      break;
+    }
+    updateSprite(gr,frame);
+  }
+  void collide() {
+    if (status==S_ALIVE) {
+      byte b = collision();
+      if (b == SP_PLAYER) {
+        player.kill();
+        status = S_DYING;
+      }
+      if ((b>=SP_FIRST_SHIELD) and (b<=SP_LAST_SHIELD)) {
+        shields.bomb(b,xpos);
+        die();
+      }
+    }
+  }
+};
+
+class Bombs {
+  Bomb zigzag,bar,diag;
+public:
+  void reset() {
+    resetBombTimer();
+    prog_char* bombptr = zigzagBombColumns;
+    zigzag.reset(SP_BOMB1, GR_BOMB_ZIGZAG, bombptr);
+    bombptr = barBombColumns;
+    bar   .reset(SP_BOMB2, GR_BOMB_BARS,   bombptr);
+    diag  .reset(SP_BOMB3, GR_BOMB_DIAG,  0);
+  }
+  void update() {
+    if (player.isAlive()) {
+      if (bombTimer > 0) {
+        --bombTimer;
+      }
+      zigzag.update();
+      bar   .update();
+      diag  .update();
+    }
+  }
+  void collide() {
+    zigzag.collide();
+    bar   .collide();
+    diag  .collide();
+  }
+  void shoot(byte s) {
+    if (zigzag.sprite==s) zigzag.die();
+    if (bar.sprite   ==s) bar.die();
+    if (diag.sprite  ==s) diag.die();
+  }
+} bombs;
+
+void shootBomb(byte s)
+{
+  bombs.shoot(s);
+}
+/*---------------------------------------------
+  Start next wave of invaders
+---------------------------------------------*/
+void startNextWave()
+{
+  beatCounter = 0;
+  player.reset();
+  bullet.reset();
+  saucer.reset();
+  bombs.reset();
+  shields.reset();
+  invaders.reset(invaderWave);
+  if (++invaderWave == 0) {
+    invaderWave = 1;
+  }
+}
+
+/*---------------------------------------------
+  Reset the game
+---------------------------------------------*/
+void resetGame()
+{
+  numLives = 3;
+  playerScore = 0;
+  invaderWave = 0;
+  startNextWave();
+  redrawScores();
+  redrawBases();
+  GD.fill((64*((screenTop+239)>>3))+(screenLeft>>3),CH_FLOOR,screenWidth>>3);
+}
+
+/*---------------------------------------------
+  Update the game - called from "loop()"
+---------------------------------------------*/
+void updateGame()
+{
+  ++frameCounter;
+  // Collision detection first (we have to do it all during vertical blanking!)
+  bullet.collide();
+  bombs.collide();
+  // The rest of the game logic
+  joystick.read();
+  player.update();
+  bullet.update();
+  saucer.update();
+  bombs.update();
+  shields.update();
+  invaders.update();
+  if (!remainingInvaders) {
+    startNextWave();
+  }
+  if (player.isDying()) {
+    bombs.reset();
+    bullet.reset();
+  }
+  if (player.isDead()) {
+    resetGame();
+  }
+  if (invaders.haveLanded()) {
+    numLives = 1;
+    player.kill();
+  }
+  updateScore();
+  updateRemainingBases();
+  if (--beatCounter < 0) {
+    alienBeatSound = true;
+    beatCounter = remainingInvaders+4;
+  }
+}
+
+/*---------------------------------------------
+  This is called once from "setup()"
+---------------------------------------------*/
+void initGame()
+{
+  joystick.recalibrate();
+  // Use a copperlist to simulate the colored plastic
+  // screen overlay...
+  CopperlistBuilder cp;
+  cp.begin(0x3700);
+  // White at the top
+  cp.write16(PALETTE4A+2,0x7fff);
+  // Red for the saucer
+  cp.wait(screenTop+bulletTop);
+  cp.write16(PALETTE4A+2,0x7c00);
+  // Back to white again
+  cp.wait(screenTop+invaderAppearY);
+  cp.write16(PALETTE4A+2,0x7fff);
+  // Green for the shields/player
+  cp.wait(screenTop+shieldYpos);
+  cp.write16(PALETTE4A+2,0x03e0);
+  cp.end();
+  highScore = 0;
+  resetGame();
+}
+