Asteroids game using a Gameduino

Dependencies:   Gameduino mbed

A version of the classic asteroids game for the mbed using gameduino. The game currently runs far too fast and is unplayable.

Please see http://mbed.org/users/TheChrisyd/code/Gameduino/ for more information

main.cpp

Committer:
TheChrisyd
Date:
2012-12-21
Revision:
2:6c07ac67dbb2
Parent:
1:3907d67048f1

File content as of revision 2:6c07ac67dbb2:

#include "mbed.h"
#include "GD.h"
#include "shield.h"

GDClass GD(ARD_MOSI, ARD_MISO, ARD_SCK, ARD_D9, USBTX, USBRX) ;
SPI spimain(ARD_MOSI, ARD_MISO, ARD_SCK); // mosi, miso, sclk
DigitalIn down(ARD_A2);//0
DigitalIn up(ARD_A3);//1
DigitalIn left(ARD_A1);//2
DigitalIn right(ARD_A0);//3

// ----------------------------------------------------------------------
//     qrand: quick random numbers
// ----------------------------------------------------------------------

static uint16_t lfsr = 1;

static void qrandSeed(int seed) {
    if (seed) {
        lfsr = seed;
    } else {
        lfsr = 0x947;
    }
}

static byte qrand1() {  // a random bit
    lfsr = (lfsr >> 1) ^ (-(lfsr & 1) & 0xb400);
    return lfsr & 1;
}

static byte qrand(byte n) { // n random bits
    byte r = 0;
    while (n--)
        r = (r << 1) | qrand1();
    return r;
}

// ----------------------------------------------------------------------
//     controller: buttons on Arduino pins 3,4,5,6
// ----------------------------------------------------------------------

static void controller_init() {
    // Configure input pins with internal pullups

    down.mode(PullUp);
    up.mode(PullUp);
    left.mode(PullUp);
    right.mode(PullUp);

}

#define CONTROL_LEFT  1
#define CONTROL_RIGHT 2
#define CONTROL_UP    4
#define CONTROL_DOWN  8


static byte controller_sense(uint16_t clock) {
    byte r = 0;


    if (!down)
        r |= CONTROL_DOWN;
    if (!up)
        r |= CONTROL_UP;
    if (!left)
        r |= CONTROL_LEFT;
    if (!right)
        r |= CONTROL_RIGHT;

    return r;
}

// Swap color's red and blue channels
uint16_t swapRB(uint16_t color) {
    byte r = 31 & (color >> 10);
    byte g = 31 & (color >> 5);
    byte b = 31 & color;
    return (color & 0x8000) | (b << 10) | (g << 5) | r;
}

// Swap color's red and green channels
uint16_t swapRG(uint16_t color) {
    byte r = 31 & (color >> 10);
    byte g = 31 & (color >> 5);
    byte b = 31 & color;
    return (color & 0x8000) | (g << 10) | (r << 5) | b;
}

#include "asteroidgraphics.h"
#include "splitscreen.h"

static void update_score();

// [int(127 * math.sin(math.pi * 2 * i / 16)) for i in range(16)]
static PROGMEM signed char charsin[16] = {0, 48, 89, 117, 127, 117, 89, 48, 0, -48, -89, -117, -127, -117, -89, -48};
char qsin(int a) {
//har)pgm_read_byte_near(charsin + ((a) & 15))
    return charsin[(a & 15)];
}
#define qcos(a) qsin((a) + 4)

static char spr2obj[256];   // Maps sprites to owning objects

/*

The general idea is that an object table ``objects`` has an entry for
each drawn thing on screen (e.g. player, missile, rock, explosion).
Each class of object has a ``handler`` function that does the necessary
housekeeping and draws the actual sprites.

As part of the behavior, some classes need to know if they have collided
with anything. In particular the rocks need to know if they have collided
with the player or a missile.  The `collide` member points to the
colliding sprite.

*/

struct object {
    int x, y;
    byte handler, state;
    byte collide;
} objects[128];
#define COORD(c) ((c) << 4)

static signed char explosions = -1;
static signed char enemies = -1;
static signed char missiles = -1;

static void push(signed char *stk, byte i) {
    objects[i].state = *stk;
    *stk = i;
}

static char pop(signed char *stk) {
    signed char r = *stk;
    if (0 <= r) {
        *stk = objects[r].state;
    }
    return r;
}

byte rr[4] = { 0,3,6,5 };

static struct {
    byte boing, boom, pop;
    byte thrust;
    byte bass;
} sounds;

static int player_vx, player_vy;  // Player velocity
static int player_invincible, player_dying;
static byte lives;
static long score;
static byte level;

// Move object po by velocity (vx, vy), optionally keeping in
// player's frame.
// Returns true if the object wrapped screen edge
static bool move(struct object *po, char vx, char vy, byte playerframe = 1) {
    bool r = 0;
    if (playerframe) {
        po->x += (vx - player_vx);
        po->y += (vy - player_vy);
    } else {
        po->x += vx;
        po->y += vy;
    }

    if (po->x > COORD(416))
        r = 1, po->x -= COORD(432);
    else if (po->x < COORD(-16))
        r = 1, po->x += COORD(432);

    if (po->y > COORD(316))
        r = 1, po->y -= COORD(332);
    else if (po->y < COORD(-16))
        r = 1, po->y += COORD(332);
    return r;
}


#define HANDLE_NULL 0
#define HANDLE_ROCK0 1
#define HANDLE_ROCK1 2
#define HANDLE_BANG0 3
#define HANDLE_BANG1 4
#define HANDLE_PLAYER 5
#define HANDLE_MISSILE 6

// Expire object i, and return it to the free stk
static void expire(signed char *stk, byte i) {
    objects[i].handler = HANDLE_NULL;
    push(stk, i);
}

static void handle_null(byte i, byte state, uint16_t clock) {
}

static void handle_player(byte i, byte state, uint16_t clock) {
    struct object *po = &objects[i];
    byte angle = (po->state & 15);
    byte rot1 = (angle & 3);
    byte rot2 = rr[3 & (angle >> 2)];
    if (!player_dying && (player_invincible & 1) == 0)
        draw_player(200, 150, rot1, rot2);

    static byte prev_control;
    byte control = controller_sense(clock);

    char thrust_x, thrust_y;
    if (!player_dying && control & CONTROL_DOWN) { // thrust
        byte flame_angle = angle ^ 8;
        byte d;
        for (d = 9; d > 5; d--) {
            int flamex = 201 - (((d + (clock&3)) * qsin(flame_angle)) >> 5);
            int flamey = 150 - (((d + (clock&3)) * qcos(flame_angle)) >> 5);
            if ((player_invincible & 1) == 0)
                draw_sparkr(flamex, flamey, rot1, rot2, 1);   // collision class K
        }
        thrust_x = -qsin(angle);
        thrust_y = -qcos(angle);
        sounds.thrust = 1;
    } else {
        thrust_x = thrust_y = 0;
        sounds.thrust = 0;
    }

    player_vx = ((31 * player_vx) + thrust_x) / 32;
    player_vy = ((31 * player_vy) + thrust_y) / 32;

    po->x += player_vx;
    po->y += player_vy;

    if (clock & 1) {
        char rotate = (512) / 400;
        if (control & CONTROL_LEFT)
            rotate++;
        if (control & CONTROL_RIGHT)
            rotate--;
        po->state = ((angle + rotate) & 15);
    }

    if (!player_dying &&
            !(prev_control & CONTROL_UP) &&
            (control & CONTROL_UP)) { // shoot!
        signed char e = pop(&missiles);
        if (0 <= e) {
            objects[e].x = COORD(200);
            objects[e].y = COORD(150);
            objects[e].state = po->state;
            objects[e].handler = HANDLE_MISSILE;
        }
        sounds.boing = 1;
    }
    prev_control = control;
    if (player_invincible)
        --player_invincible;
    if (player_dying) {
        if (--player_dying == 0) {
            --lives;
            update_score();
            if (lives != 0) {
                player_invincible = 48;
            }
        }
    }
}

static void handle_missile(byte i, byte state, uint16_t clock) {
    struct object *po = &objects[i];
    byte angle = (po->state & 15);
    byte rot1 = (angle & 3);
    byte rot2 = rr[3 & (angle >> 2)];
    draw_sparkr(po->x >> 4, po->y >> 4, rot1, rot2);
    char vx = -qsin(po->state), vy = -qcos(po->state);
    if (move(po, vx, vy, 0)) {
        expire(&missiles, i);
    }
}

static void explodehere(struct object *po, byte handler, uint16_t clock) {
    signed char e = pop(&explosions);
    if (0 <= e) {
        objects[e].x = po->x;
        objects[e].y = po->y;
        objects[e].handler = handler;
        objects[e].state = clock;
    }
}

static void killplayer(uint16_t clock) {
    if (!player_invincible && !player_dying) {
        signed char e = pop(&explosions);
        if (0 <= e) {
            objects[e].x = COORD(200);
            objects[e].y = COORD(150);
            objects[e].handler = HANDLE_BANG1;
            objects[e].state = clock;
        }
        player_dying = 2 * 36;
        sounds.boom = 1;
        sounds.pop = 1;
    }
}

static byte commonrock(uint16_t clock, byte i, byte speed, void df(int x, int y, byte anim, byte rot, byte jk)) {
    struct object *po = &objects[i];

    byte move_angle = po->state >> 4;
    char vx = (speed * -qsin(move_angle)) >> 6, vy = (speed * -qcos(move_angle)) >> 6;
    move(po, vx, vy);

    byte angle = (clock * speed) >> 4;
    if (po->state & 1)
        angle = ~angle;
    byte rot1 = (angle & 3);
    byte rot2 = rr[3 & (angle >> 2)];
    df(po->x >> 4, po->y >> 4, rot1, rot2, 1);
    if (po->collide != 0xff) {
        struct object *other = &objects[po->collide];
        switch (other->handler) {
            case HANDLE_PLAYER:
                killplayer(clock);
                break;
            case HANDLE_MISSILE:
                expire(&missiles, po->collide);   // missile is dead
                expire(&enemies, i);
                return 1;
        }
    }
    return 0;
}

static void handle_rock0(byte i, byte state, uint16_t clock) {
    struct object *po = &objects[i];
    byte speed = 12 + (po->state & 7);
    if (commonrock(clock, i, speed, draw_rock0r)) {
        explodehere(po, HANDLE_BANG0, clock);
        score += 10;
        sounds.pop = 1;
    }
}

static void handle_rock1(byte i, byte state, uint16_t clock) {
    struct object *po = &objects[i];
    byte speed = 6 + (po->state & 3);
    if (commonrock(clock, i, speed, draw_rock1r)) {
        int j;
        for (j = 0; j < 4; j++) {
            char e = pop(&enemies);
            if (0 < e) {
                objects[e].x = po->x;
                objects[e].y = po->y;
                objects[e].handler = HANDLE_ROCK0;
                objects[e].state = (j << 6) + qrand(6);   // spread fragments across 4 quadrants
            }
        }
        explodehere(po, HANDLE_BANG1, clock);
        score += 30;
        sounds.boom = 1;
    }
}

static void handle_bang0(byte i, byte state, uint16_t clock) {
    struct object *po = &objects[i];
    move(po, 0, 0);
    byte anim = ((0xff & clock) - state) >> 1;
    if (anim < EXPLODE16_FRAMES)
        draw_explode16(po->x >> 4, po->y >> 4, anim, 0);
    else
        expire(&explosions, i);
}

static void handle_bang1(byte i, byte state, uint16_t clock) {
    struct object *po = &objects[i];
    move(po, 0, 0);
    byte anim = ((0xff & clock) - state) >> 1;
    byte rot = 7 & i;
    if (anim < EXPLODE32_FRAMES)
        draw_explode32(po->x >> 4, po->y >> 4, anim, rot);
    else
        expire(&explosions, i);
}

typedef void (*handler)(byte, byte, uint16_t);
static handler handlers[] = {
    handle_null,
    handle_rock0,
    handle_rock1,
    handle_bang0,
    handle_bang1,
    handle_player,
    handle_missile
};


// uncompress one line of the title banner into buffer dst
// title banner lines are run-length encoded
static void titlepaint(char *dst, byte src, byte mask) {
    if (src != 0xff) {
        PROGMEM prog_uchar *psrc = title_runs + 2 * src;
        byte a, b;
        do {
            a = *psrc++;
            b = *psrc++;
            while (a < (b & 0x7f))
                dst[a++] |= mask;
        } while (!(b & 0x80));
    }
}

// draw a title banner column src (0-511) to screen column dst (0-63).
static void column(byte dst, byte src) {
    static char scratch[76];
    memset(scratch, 0, sizeof(scratch));
    //PROGMEM prog_uchar* titleadd = title + 2 * src;
    prog_uchar line = title[2 * src];//*titleadd++;//pgm_read_byte_near(title + 2 * src);
    titlepaint(scratch, line, 1);
    line = title[2 * src+1];//*titleadd;//pgm_read_byte_near(title + 2 * src + 1);
    titlepaint(scratch, line, 2);

    byte j;
    for (j = 0; j < 38; j++) {
        GD.wr(dst + (j << 6),
              (((dst + j) & 15) << 4) +
              scratch[2 * j] +
              (scratch[2 * j + 1] << 2));
    }
}

static void setup_sprites() {
    GD.copy(PALETTE16A, palette16a, sizeof(palette16a));
    GD.copy(PALETTE4A, palette4a, sizeof(palette4a));
    GD.copy(PALETTE16B, palette16b, sizeof(palette16b));

    // Use the first two 256-color palettes as pseudo-16 color palettes
    int i;
    for (i = 0; i < 256; i++) {

        // palette 0 decodes low nibble, hence (i & 15)
        //PROGMEM prog_uchar* pallow = palette256a + ((i & 15) << 1);
        uint16_t rgb = palette256a[((i & 15) << 1)];
        GD.wr16(RAM_SPRPAL + (i << 1), rgb);

        // palette 1 decodes nigh nibble, hence (i >> 4)
        //PROGMEM prog_uchar* palhigh = palette256a + ((i >> 4) << 1);
        rgb = palette256a[((i >> 4) << 1)];
        GD.wr16(RAM_SPRPAL + 512 + (i << 1), rgb);
    }

    GD.uncompress(RAM_SPRIMG, asteroid_images_compressed);
    GD.wr(JK_MODE, 1);
}

// Run the object handlers, keeping track of sprite ownership in spr2obj
static void runobjects(uint16_t r) {
    int i;
    GD.__wstartspr(((r & 1) ? 256 : 0));  // write sprites to other frame
    for (i = 0; i < 128; i++) {
        struct object *po = &objects[i];
        handler h = (handler)handlers[po->handler];
        byte loSpr = GD.spr;
        (*h)(i, po->state, r);
        while (loSpr < GD.spr) {
            spr2obj[loSpr++] = i;
        }
    }
    // Hide all the remaining sprites
    do
        GD.xhide();
    while (GD.spr);
    GD.__end();
}

// ----------------------------------------------------------------------
//     map
// ----------------------------------------------------------------------

static byte loaded[8];
static byte scrap;

// copy a (w,h) rectangle from the source image (x,y) into picture RAM
static void rect(unsigned int dst, byte x, byte y, byte w, byte h) {
    PROGMEM prog_uchar *src = bg_pic + (16 * y) + x;
    while (h--) {
        GD.copy(dst, src, w);
        dst += 64;
        src += 16;
    }
}

static void map_draw(byte strip) {
    strip &= 63;              // Universe is finite but unbounded: 64 strips
    byte s8 = (strip & 7);    // Destination slot for this strip (0-7)
    if (loaded[s8] != strip) {
        qrandSeed(level ^ (strip * 77));    // strip number is the hash...

        // Random star pattern is made from characters 1-15
        GD.__wstart(s8 * (8 * 64));
        int i;
        for (i = 0; i < (8 * 64); i++) {
            byte r;
            if (qrand(3) == 0)
                r = qrand(4);
            else
                r = 0;
            spimain.write(r);
        }
        GD.__end();

        // Occasional planet, copied from the background char map
        if (qrand(2) == 0) {
            uint16_t dst = (qrand(3) * 8) + (s8 * (8 * 64));
            switch (qrand(2)) {
                case 0:
                    rect(dst, 0, 1, 6, 6);
                    break;
                case 1:
                    rect(dst, 7, 1, 6, 6);
                    break;
                case 2:
                    rect(dst, 0, 7, 8, 4);
                    break;
                case 3:
                    rect(dst, 8, 7, 5, 5);
                    break;
            }
        }
        loaded[s8] = strip;
    }
}

static void map_coldstart() {
    memset(loaded, 0xff, sizeof(loaded));
    scrap = 0xff;
    byte i;
    for (i = 0; i < 8; i++)
        map_draw(i);
}

static int atxy(int x, int y) {
    return (y << 6) + x;
}

static void update_score() {
    unsigned long s = score;
    uint16_t a = atxy(49, scrap << 3);
    byte i;
    //void* code;
    for (i = 0; i < 6; i++) {
        PROGMEM prog_uchar* digitcodes = (bg_pic + (16 * 30))+(s % 10);

        //code = &digitcodes + (s % 10);
        GD.wr(a--, *digitcodes);//pgm_read_byte_near(digitcodes + (s % 10)) ;
        s /= 10;
    }
    PROGMEM prog_uchar* digitcodes = bg_pic + (16 * 30) + lives;
    //code = &digitcodes + lives;
    GD.wr(atxy(0, scrap << 3), *digitcodes);// pgm_read_byte_near(digitcodes + lives));
}

static void map_refresh(byte strip) {
    byte i;
    byte newscrap = 7 & (strip + 7);
    if (scrap != newscrap) {
        scrap = newscrap;

        uint16_t scrapline = scrap << 6;
        GD.wr16(COMM+2, 0x8000 | scrapline);    // show scrapline at line 0
        GD.wr16(COMM+14, 0x8000 | (0x1ff & ((scrapline + 8) - 291))); // show scrapline+8 at line 291

        GD.fill(atxy(0, scrap << 3), 0, 50);
        update_score();

        GD.fill(atxy(0, 1 + (scrap << 3)), 0, 64);
        rect(atxy(0, 1 + (scrap << 3)), 0, 31, 16, 1);
        rect(atxy(32, 1 + (scrap << 3)), 0, 31, 16, 1);

        loaded[scrap] = 0xff;
    }
    wait_ms(1);   // wait for raster to pass the top line before overwriting it
    for (i = 0; i < 6; i++)
        map_draw(strip + i);
}

static void start_level() {
    int i;

    for (i = 0; i < 128; i++)
        objects[i].handler = 0;

    player_vx = 0;
    player_vy = 0;
    player_invincible = 0;
    player_dying = 0;

    objects[0].x = 0;
    objects[0].y = 0;
    objects[0].state = 0;
    objects[0].handler = HANDLE_PLAYER;

    // Set up the pools of objects for missiles, enemies, explosions
    missiles = 0;
    enemies = 0;
    explosions = 0;
    for (i = 1; i < 16; i++)
        push(&missiles, i);
    for (i = 16; i < 80; i++)
        push(&enemies, i);
    for (i = 80; i < 128; i++)
        push(&explosions, i);

    // Place asteroids in a ring around the edges of the screen
    int x;
    if ((level + 3) < 32) {
        x = level + 3;
    } else {
        x = 32;
    }
    for (i = 0; i < x; i++) {
        char e = pop(&enemies);
        if (rand()%2 == 0) {
            objects[e].x = rand()%2 ? COORD(32) : COORD(400-32);
            objects[e].y = rand()%COORD(300);
        } else {
            objects[e].x = rand()%COORD(400);
            objects[e].y = rand()%2 ? COORD(32) : COORD(300-32);
        }
        objects[e].handler = HANDLE_ROCK1;
        objects[e].state = qrand(8);
    }

    GD.copy(PALETTE16B, palette16b, sizeof(palette16b));
    for (i = 0; i < 16; i++) {
        uint16_t a = PALETTE16B + 2 * i;
        uint16_t c = GD.rd16(a);
        if (level & 1)
            c = swapRB(c);
        if (level & 2)
            c = swapRG(c);
        if (level & 4)
            c = swapRB(c);
        GD.wr16(a, c);
    }

    map_coldstart();
}

void setup() {

    GD.begin();
    controller_init();

}

static void title_banner() {
    GD.fill(VOICES, 0, 64 * 4);
    GD.wr(J1_RESET, 1);
    GD.wr(SPR_DISABLE, 1);
    GD.wr16(SCROLL_X, 0);
    GD.wr16(SCROLL_Y, 0);
    GD.fill(RAM_PIC, 0, 4096);
    setup_sprites();

    uint16_t i;
    uint16_t j;

    GD.__wstart(RAM_CHR);
    for (i = 0; i < 256; i++) {
        // bits control lit segments like this:
        //   0   1
        //   2   3
        byte a = (i & 1) ? 0x3f : 0;
        byte b = (i & 2) ? 0x3f : 0;
        byte c = (i & 4) ? 0x3f : 0;
        byte d = (i & 8) ? 0x3f : 0;
        for (j = 0; j < 3; j++) {
            spimain.write(a);
            spimain.write(b);
        }
        spimain.write(0);
        spimain.write(0);
        for (j = 0; j < 3; j++) {
            spimain.write(c);
            spimain.write(d);
        }
        spimain.write(0);
        spimain.write(0);
    }
   
    GD.__end();
    
    for (i = 0; i < 256; i++) {
        GD.setpal(4 * i + 0, RGB(0,0,0));
        //prog_uchar* colptr = title_ramp + 2 * (i >> 4);
        prog_uchar color = title_ramp[(i >> 4)];//pgm_read_word_near(title_ramp + 2 * (i >> 4));
        GD.setpal(4 * i + 3, color);
    }
    
    for (i = 0; i < 64; i++) {
        column(i, i);
    }

    for (i = 0; i < 128; i++) {
        objects[i].handler = 0;
        objects[i].collide = 0xff;
    }
    
    for (i = 0; i < 128; i++)
        push(&enemies, i);

    for (i = 0; i < 40; i++) {
        char e = pop(&enemies);
        objects[e].x = COORD(rand()%400);
        objects[e].y = COORD(rand()%300);
        objects[e].handler = qrand1() ? HANDLE_ROCK1 : HANDLE_ROCK0;
        objects[e].state = qrand(8);
    }

    byte startgame = 0;
    for (i = 0; startgame < 50; i++) {
        for (j = 0; j < 256; j++) {
            byte index = 15 & ((-i >> 2) + (j >> 4));
            prog_uchar* colour = title_ramp + 2 * index;
            prog_uchar color = *colour;// pgm_read_word_near(title_ramp + 2 * index);
            GD.setpal(4 * j + 3, color);
        }
        
        if (!startgame &&
                (controller_sense(i) || (i == (2048 - 400)))) {
            // explode all rocks!
            for (j = 0; j < 128; j++) {
                byte h = objects[j].handler;
                if ((h == HANDLE_ROCK0) || (h == HANDLE_ROCK1))
                    objects[j].handler = HANDLE_BANG1;
                objects[j].state = i;
            }
            startgame = 1;
        }
        
        if (startgame){
            startgame++;
            }
            
        //runobjects(i);
        
        GD.waitvblank();
        GD.wr(SPR_PAGE, (i & 1));
        GD.wr(SPR_DISABLE, 0);
        GD.wr16(SCROLL_X, i);
        if ((i & 7) == 0) {
            byte x = ((i >> 3) + 56);
            column(63 & x, 255 & x);
        }
    }

    for (i = 0; i < 32; i++) {
        for (j = 0; j < 256; j++) {
            uint16_t a = RAM_PAL + (8 * j) + 6;
            uint16_t pal = GD.rd16(a);
            byte r = 31 & (pal >> 10);
            byte g = 31 & (pal >> 5);
            byte b = 31 & pal;
            if (r) r--;
            if (g) g--;
            if (b) b--;
            pal = (r << 10) | (g << 5) | b;
            GD.wr16(a, pal);
        }
        GD.waitvblank();
        GD.waitvblank();
        
    }

    GD.fill(RAM_PIC, 0, 4096);
    
}

#define SOUNDCYCLE(state) ((state) = v ? ((state) + 1) : 0)

int main() {
   
    setup();
    while (1) {
     
        title_banner();

        GD.uncompress(RAM_CHR, bg_chr_compressed);
        GD.uncompress(RAM_PAL, bg_pal_compressed);

        GD.wr16(COMM+0, 0);
        GD.wr16(COMM+2, 0x8000);
        GD.wr16(COMM+4, 8);   // split at line 8
        GD.wr16(COMM+6, 177);
        GD.wr16(COMM+8, 166);
        GD.wr16(COMM+10, 291);   // split at line 291
        GD.wr16(COMM+12, 0);
        GD.wr16(COMM+14, 0x8000 | (0x1ff & (8 - 291))); // show line 8 at line 292
        GD.microcode(splitscreen_code, sizeof(splitscreen_code));

        setup_sprites();


        memset(&sounds, 0, sizeof(sounds));
        level = 0;
        score = 0;
        lives = 3;
        unsigned int r = 0;
        start_level();

        while (lives) {
            int i;//, j;

            runobjects(r);

            for (i = 0; i < 128; i++)
                objects[i].collide = 0xff;

            GD.waitvblank();
            // swap frames
            GD.wr(SPR_PAGE, (r & 1));
            int scrollx = objects[0].x >> 4;
            int scrolly = objects[0].y >> 4;
            GD.wr16(COMM+6, scrollx & 0x1ff);
            GD.wr16(COMM+8, scrolly & 0x1ff);
            map_refresh(scrolly >> 6);
            update_score();
            GD.wr16(COMM+12, r);    // horizontal scroll the bottom banner

            GD.waitvblank();

            GD.__start(COLLISION);
            for (i = 0; i < 256; i++) {
                byte c = spimain.write(0);   // c is the colliding sprite number
                if (c != 0xff) {
                    objects[spr2obj[i]].collide = spr2obj[c];
                }
            }
            GD.__end();

            if (sounds.boing) {
                byte x;
                if ((16 - (sounds.boing - 1) * 2) <0) {
                    x = 0;
                } else {
                    x = 16 - (sounds.boing - 1) * 2;
                }
                byte v = x; //max(0, 16 - (sounds.boing - 1) * 2);
                GD.voice(0, 0, 4 * 4000 - 700 * sounds.boing, v/2, v/2);
                GD.voice(1, 1, 1000 - 100 * sounds.boing, v, v);
                SOUNDCYCLE(sounds.boing);
            }
            if (sounds.boom) {
                byte x;
                if ((96 - (sounds.boom - 1) * 6) <0) {
                    x = 0;
                } else {
                    x = 96 - (sounds.boom - 1) * 6;
                }
                byte v = x;//max(0, 96 - (sounds.boom - 1) * 6);
                GD.voice(2, 0, 220, v, v);
                GD.voice(3, 1, 220/8, v/2, v/2);
                SOUNDCYCLE(sounds.boom);
            }
            if (sounds.pop) {
                byte x;
                if ((32 - (sounds.pop - 1) * 3) <0) {
                    x = 0;
                } else {
                    x = 32 - (sounds.pop - 1) * 3;
                }
                byte v = x;//max(0, 32 - (sounds.pop - 1) * 3);
                GD.voice(4, 0, 440, v, v);
                GD.voice(5, 1, 440/8, v/2, v/2);
                SOUNDCYCLE(sounds.pop);
            }
            GD.voice(6, 1, 40, sounds.thrust ? 10 : 0, sounds.thrust ? 10 : 0);

            static byte tune;
            if (sounds.bass) {
                byte v = sounds.bass < 9 ? 63 : 0;
                int f0 = tune ? 130: 163 ;
                byte partials[] = { 71, 32, 14, 75, 20, 40 };
                byte i;
                for (i = 0; i < 6; i++) {
                    byte a = (v * partials[i]) >> 8;
                    GD.voice(7 + i, 0, f0 * (i + 1), a, a);
                }
                SOUNDCYCLE(sounds.bass);
            }
            static byte rhythm;
            byte x;
            if ((24 - level) <10) {
                x = 10;
            } else {
                x = 24 - level;
            }
            if (++rhythm >= x) {
                sounds.bass = 1;
                rhythm = 0;
                tune = !tune;
            }

            byte nenemies = 64;
            byte pe = enemies;
            while (pe) {
                pe = objects[pe].state;
                nenemies--;
            }
            if (nenemies == 0) {
                level++;
                start_level();
            }

            r++;
        }
    }
}