ASTEROIDS video game [HANDMADE]

Some time ago I wanted to improve my Python programming skills and I thought it would be more fun to learn it while programming small games. So I followed the online Coursera course 'An Introduction to Interactive Programming in Python' offered by the Rice university. The last game that we had to program was a modified version of the Atari game ASTEROIDS from the 1980s. In this game the player controls a spaceship in an asteroid field. The objective is to shoot and destroy asteroids without colliding with those.

I still didn't modify and convert the code into an executable file. But to play the game you simply go to www.codeskulptor.org, clear the left window and paste the code below. Click the PLAY button and there you go. A window pops up and you see the start page of the game. Simply navigate the spaceship with the arrow keys UP, LEFT or RIGHT and shoot with the SPACE key.

# CODE for ASTEROIDS
import simplegui
import math
import random

# globals for user interface
WIDTH = 800
HEIGHT = 600
score = 0
lives = 3
time = 0.5
ANG_VEL = 0.1
THRUST_ACC = 0.1
FRICTION = 0.01
MISSILE_FOR = 5
started = False

class ImageInfo:
    def __init__(self, center, size, radius = 0, lifespan = None, animated = False):
        self.center = center
        self.size = size
        self.radius = radius
        if lifespan:
            self.lifespan = lifespan
        else:
            self.lifespan = float('inf')
        self.animated = animated

    def get_center(self):
        return self.center

    def get_size(self):
        return self.size

    def get_radius(self):
        return self.radius

    def get_lifespan(self):
        return self.lifespan

    def get_animated(self):
        return self.animated
 
# art assets created by Kim Lathrop, may be freely re-used in non-commercial projects, please credit Kim
 
# debris images - debris1_brown.png, debris2_brown.png, debris3_brown.png, debris4_brown.png
#                 debris1_blue.png, debris2_blue.png, debris3_blue.png, debris4_blue.png, debris_blend.png
debris_info = ImageInfo([320, 240], [640, 480])
debris_image = simplegui.load_image("http://commondatastorage.googleapis.com/codeskulptor-assets/lathrop/debris2_blue.png")

# nebula images - nebula_brown.png, nebula_blue.png
nebula_info = ImageInfo([400, 300], [800, 600])
nebula_image = simplegui.load_image("http://commondatastorage.googleapis.com/codeskulptor-assets/lathrop/nebula_blue.png")

# splash image
splash_info = ImageInfo([200, 150], [400, 300])
splash_image = simplegui.load_image("http://commondatastorage.googleapis.com/codeskulptor-assets/lathrop/splash.png")

# ship image
ship_info = ImageInfo([45, 45], [90, 90], 35)
ship_image = simplegui.load_image("http://commondatastorage.googleapis.com/codeskulptor-assets/lathrop/double_ship.png")

# missile image - shot1.png, shot2.png, shot3.png
missile_info = ImageInfo([5,5], [10, 10], 3, 50)
missile_image = simplegui.load_image("http://commondatastorage.googleapis.com/codeskulptor-assets/lathrop/shot2.png")

# asteroid images - asteroid_blue.png, asteroid_brown.png, asteroid_blend.png
asteroid_info = ImageInfo([45, 45], [90, 90], 40)
asteroid_image = simplegui.load_image("http://commondatastorage.googleapis.com/codeskulptor-assets/lathrop/asteroid_blue.png")

# animated explosion - explosion_orange.png, explosion_blue.png, explosion_blue2.png, explosion_alpha.png
explosion_info = ImageInfo([64, 64], [128, 128], 17, 24, True)
explosion_image = simplegui.load_image("http://commondatastorage.googleapis.com/codeskulptor-assets/lathrop/explosion_alpha.png")

# sound assets purchased from sounddogs.com, please do not redistribute
soundtrack = simplegui.load_sound("http://commondatastorage.googleapis.com/codeskulptor-assets/sounddogs/soundtrack.mp3")
missile_sound = simplegui.load_sound("http://commondatastorage.googleapis.com/codeskulptor-assets/sounddogs/missile.mp3")
missile_sound.set_volume(.5)
ship_thrust_sound = simplegui.load_sound("http://commondatastorage.googleapis.com/codeskulptor-assets/sounddogs/thrust.mp3")
explosion_sound = simplegui.load_sound("http://commondatastorage.googleapis.com/codeskulptor-assets/sounddogs/explosion.mp3")

# helper functions to handle transformations
def angle_to_vector(ang):
    return [math.cos(ang), math.sin(ang)]

def dist(p,q):
    return math.sqrt((p[0] - q[0]) ** 2+(p[1] - q[1]) ** 2)

# Ship class
class Ship:
    def __init__(self, pos, vel, angle, image, info):
        self.pos = [pos[0],pos[1]]
        self.vel = [vel[0],vel[1]]
        self.thrust = False
        self.angle = angle
        self.angle_vel = 0
        self.image = image
        self.image_center = info.get_center()
        self.image_size = info.get_size()
        self.radius = info.get_radius()
     
    def draw(self,canvas):
        canvas.draw_image(ship_image, self.image_center, self.image_size, self.pos, self.image_size, self.angle)
     
    def update(self):
        forward = angle_to_vector(self.angle)
        self.vel[0] *= 1 - FRICTION
        self.vel[1] *= 1 - FRICTION
        if self.thrust == True:
            self.vel[0] += THRUST_ACC * forward[0]
            self.vel[1] += THRUST_ACC * forward[1]
        self.pos[0] = (self.pos[0] + self.vel[0]) % WIDTH
        self.pos[1] = (self.pos[1] + self.vel[1]) % HEIGHT
        self.angle += self.angle_vel
 
    def increase_angle_vel(self):
        self.angle_vel += ANG_VEL
     
    def decrease_angle_vel(self):
        self.angle_vel -= ANG_VEL
     
    def thruster_on(self):      
        if self.thrust == False:
            self.image_center[0] += 90
            self.thrust = True
            ship_thrust_sound.set_volume(1)
            ship_thrust_sound.rewind()
            ship_thrust_sound.play()

    def thruster_off(self):
        if self.thrust == True:
            self.image_center[0] -= 90
            self.thrust = False
            ship_thrust_sound.pause()        
         
    def shoot(self):
        global a_missile, missile_group
        forward = angle_to_vector(self.angle)
        missile_x = self.pos[0] + forward[0] * self.image_size[0]/2
        missile_y = self.pos[1] + forward[1] * self.image_size[0]/2
        missile_vel_x = self.vel[0] + MISSILE_FOR * forward[0]
        missile_vel_y = self.vel[1] + MISSILE_FOR * forward[1]  
        a_missile = Sprite([missile_x, missile_y], [missile_vel_x,missile_vel_y], 0, 0, missile_image, missile_info, missile_sound)
        missile_group.add(a_missile)
     
    def get_radius(self):
        return self.radius
 
    def get_center(self):
        return self.pos
 
# Sprite class
class Sprite:
    def __init__(self, pos, vel, ang, ang_vel, image, info, sound = None):
        self.pos = [pos[0],pos[1]]
        self.vel = [vel[0],vel[1]]
        self.angle = ang
        self.angle_vel = ang_vel
        self.image = image
        self.image_center = info.get_center()
        self.image_size = info.get_size()
        self.radius = info.get_radius()
        self.lifespan = info.get_lifespan()
        self.animated = info.get_animated()
        self.age = 0
        if sound:
            sound.set_volume(1)
            sound.rewind()
            sound.play()
 
    def draw(self, canvas):
        if self.animated == True:
            EXPLOSION_CENTER = self.image_center
            EXPLOSION_SIZE = self.image_size
            EXPLOSION_DIM = 24
            explosion_index = self.age % EXPLOSION_DIM
            canvas.draw_image(self.image,[EXPLOSION_CENTER[0] + explosion_index * EXPLOSION_SIZE[0],
                     EXPLOSION_CENTER[1]],EXPLOSION_SIZE, self.pos, EXPLOSION_SIZE)
        else:
            canvas.draw_image(self.image, self.image_center, self.image_size, self.pos, self.image_size, self.angle)
         
    def update(self):
        self.pos[0] = (self.pos[0] + self.vel[0]) % WIDTH
        self.pos[1] = (self.pos[1] + self.vel[1]) % HEIGHT
        self.angle += self.angle_vel
        self.age += 1
        if self.age >= self.lifespan:
            return False
        else:
            return True
     
    def get_radius(self):
        return self.radius
 
    def get_center(self):
        return self.pos
     
    def collide(self, other_object):
        other_object_radius = other_object.get_radius()
        other_object_center = other_object.get_center()
        center_distance = dist(other_object_center, self.pos)
        distance = center_distance - self.radius - other_object_radius
        if distance <= 0:          
            return True
        else:
            return False

# helper function - sprite set
def process_sprite_group(sprite_group, canvas):
    group = set(sprite_group)  
    for sprite in group:
        if sprite.update() == False:
            sprite_group.discard(sprite)
        else:
            sprite.draw(canvas)

# helper function - group collide
def group_collide(sprite_group, other_object):
    global explosion_group
    group = set(sprite_group)
    for sprite in group:
        if sprite.collide(other_object):
            sprite_group.discard(sprite)
            explosion = Sprite(sprite.pos,sprite.vel, 0, 0, explosion_image, explosion_info, explosion_sound)
            explosion_group.add(explosion)
    return len(group) - len(sprite_group)

# helper function - group-group collide
def group_group_collide(sprite_group, other_sprite_group):
    group = set(sprite_group)
    for sprite in group:
        if group_collide(other_sprite_group, sprite) > 0:
            sprite_group.discard(sprite)
    return len(group) - len(sprite_group)

# helper function to stop sounds
def exit_all():
    soundtrack.pause()  
    missile_sound.pause()
    ship_thrust_sound.pause()
    explosion_sound.pause()  
 
# key handlers to control ship
def keydown(key):
    global thrust
    if key == simplegui.KEY_MAP["left"]:
        my_ship.decrease_angle_vel()
    elif key == simplegui.KEY_MAP["right"]:
        my_ship.increase_angle_vel()
    elif key == simplegui.KEY_MAP["up"]:
        my_ship.thruster_on()
    elif key == simplegui.KEY_MAP["space"]:
        my_ship.shoot()

def keyup(key):
    global thrust
    if key == simplegui.KEY_MAP["left"]:
        my_ship.increase_angle_vel()
    elif key == simplegui.KEY_MAP["right"]:
        my_ship.decrease_angle_vel()
    elif key == simplegui.KEY_MAP["up"]:
        my_ship.thruster_off()      

# mouseclick handlers that reset UI and conditions whether splash image is drawn
def click(pos):
    global started, lives, score
    center = [WIDTH / 2, HEIGHT / 2]
    size = splash_info.get_size()
    inwidth = (center[0] - size[0] / 2) < pos[0] < (center[0] + size[0] / 2)
    inheight = (center[1] - size[1] / 2) < pos[1] < (center[1] + size[1] / 2)
    if (not started) and inwidth and inheight:
        started = True
        lives = 3
        score = 0
        soundtrack.rewind()
        soundtrack.play()

def draw(canvas):
    global time, a_group
 
    # animiate background
    time += 1
    center = debris_info.get_center()
    size = debris_info.get_size()
    wtime = (time / 8) % center[0]
    canvas.draw_image(nebula_image, nebula_info.get_center(), nebula_info.get_size(), [WIDTH / 2, HEIGHT / 2], [WIDTH, HEIGHT])
    canvas.draw_image(debris_image, [center[0] - wtime, center[1]], [size[0] - 2 * wtime, size[1]],
                                [WIDTH / 2 + 1.25 * wtime, HEIGHT / 2], [WIDTH - 2.5 * wtime, HEIGHT])
    canvas.draw_image(debris_image, [size[0] - wtime, center[1]], [2 * wtime, size[1]],
                                [1.25 * wtime, HEIGHT / 2], [2.5 * wtime, HEIGHT])

    # draw ship and sprites
    my_ship.draw(canvas)
 
    # draw sprite_group
    process_sprite_group(a_group, canvas)
    process_sprite_group(missile_group, canvas)
    process_sprite_group(explosion_group, canvas)
 
    # call collide method
    global lives, started
    if group_collide(a_group, my_ship) > 0:
        lives -= 1
        if lives == 0:
            started = False
            a_group = set()
            soundtrack.pause()        
 
    # call group-group collide method
    global score
    score += group_group_collide(a_group, missile_group)
 
    # update ship
    my_ship.update()

    # draw score and lives
    canvas.draw_text("Score: " + str(score), (WIDTH-160, 50), 25, "White", "monospace")
#    canvas.draw_text("Lives: " + str(lives), (50, 50), 25, "White", "monospace")
    canvas.draw_image(ship_image, (45,45), ship_info.get_size(), (55, 45), (50,50), -math.pi/2)
    canvas.draw_text( "x " + str(lives), (85, 50), 25, "White", "monospace")

    # draw splash screen if not started
    if not started:
        canvas.draw_image(splash_image, splash_info.get_center(),
                          splash_info.get_size(), [WIDTH / 2, HEIGHT / 2],
                          splash_info.get_size())

# timer handler that spawns a rock  
def rock_spawner():
    global a_rock, a_group
    rock_pos = [random.randrange(0,WIDTH), random.randrange(0,HEIGHT)]
    rock_vel = [random.random() * .1*score - .05*score, random.random() * .1*score - .05*score]
    rock_avel = random.random() * .2 - .1
    a_rock = Sprite(rock_pos, rock_vel, 0, rock_avel, asteroid_image, asteroid_info)
    distance = dist(my_ship.get_center(), a_rock.get_center())
    if distance > (my_ship.get_radius() + a_rock.get_radius() + 10) and len(a_group) < 12 and started == True:
        a_group.add(a_rock)
    if frame.get_canvas_textwidth("test",50) < 10:      
        exit_all()
     
# initialize frame
frame = simplegui.create_frame('Asteroids', WIDTH, HEIGHT)

# initialize ship and sprite groups
my_ship = Ship([WIDTH / 2, HEIGHT / 2], [1, 1], 0, ship_image, ship_info)
a_group = set()
missile_group = set()
explosion_group = set()

# register handlers
frame.set_keyup_handler(keyup)
frame.set_keydown_handler(keydown)
frame.set_mouseclick_handler(click)
frame.set_draw_handler(draw)

timer = simplegui.create_timer(1000.0, rock_spawner)

# get things rolling
timer.start()
frame.start()