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()
