Here's a revised version of Midnight 'Roids. It's got engine glow,
and asteroid explosions. And some cleaned-up code, so that it
tips the scales at 299 lines.
(left, right, up, space to play)
(N for a new game)
# Midnight 'Roids Rev. 2
# This code is hereby released into the public domain.
class Asteroids
WIDTH = 700
HEIGHT = 600
def self.new_game
@lives = 3
@level = 0
@text = nil
@ship = Ship.new(WIDTH/2, HEIGHT/2)
@roids_count = 3
self.start_new_level
end
def self.start_new_level
@level += 1
unsafe_to_start = true
while unsafe_to_start do
Roid.reset
@roids_count.times {|i| Roid.new(rand(WIDTH), rand(HEIGHT), (rand - 0.5)*2.5, (rand - 0.5)*2.5, Roid::LARGE) }
unsafe_to_start = Roid.check_for_collision_with(@ship, Ship::UNIT*5)
end
end
def self.game_over
@text = "Game Over"
end
def self.draw
@ship.check_for_collision_with_roids if @ship
Ray.check_for_collision_with_roids
$app.clear do
$app.background $app.black
$app.para("Lives left: [EMAIL PROTECTED]", :top => 5, :left => 5, :stroke => $app.gray(0.5), :font => "13px")
$app.para("Level: [EMAIL PROTECTED]", :top => 5, :left => WIDTH - 75, :stroke => $app.gray(0.5), :font => "13px")
$app.title(@text, :top => 240, :left => 80, :stroke => $app.gray(0.1), :font => "bold 100px") if @text
@ship.draw if @ship
Ray.draw_all
Roid.draw_all
Explosion.draw_all
end
end
def self.ship_explodes
center_x, center_y = @ship.center_x, @ship.center_y
if @lives > 0
@lives -= 1
unsafe_to_start = true
@ship = Ship.new(WIDTH/2, HEIGHT/2)
while unsafe_to_start do
unsafe_to_start = Roid.check_for_collision_with(@ship, Ship::UNIT*5)
@ship = Ship.new(rand(WIDTH), rand(HEIGHT)) if unsafe_to_start
end
else
@ship = nil
self.game_over
end
50.times do |i|
direction = ((2 * Math::PI) * i/50)
Ray.new(center_x, center_y, 10*Math.cos(direction), 10*Math.sin(direction), direction)
end
end
def self.maybe_start_new_level
if Roid.roids_left == 0
@roids_count += 1
self.start_new_level
end
end
def self.keypress(key)
@ship.move(key) if [:left, :right, :up].include?(key) && @ship
@ship.shoot if (key == ' ') && @ship
self.new_game if key == 'n'
end
end
class CelestialBody
attr_accessor :x, :y, :size
def center_x
@x + @size/2
end
def center_y
@y + @size/2
end
def initialize(x, y, vel_x, vel_y, size)
@x, @y, @vel_x, @vel_y, @size = x, y, vel_x, vel_y, size
end
def intersects_with?(thing, margin = 0)
return true if Math.sqrt((center_x - thing.center_x)**2 + (center_y - thing.center_y)**2) < (@size/2 + ((thing.size + margin) / 2))
return false
end
end
class Roid < CelestialBody
LARGE = 80
MEDIUM = 45
SMALL = 25
def self.add_roid(roid)
@roids << roid
end
def self.remove_roid(roid)
@roids.delete_at(roid)
end
def self.draw_all
@roids.each {|roid| roid.draw }
end
def self.roids_left
@roids.length
end
def self.reset
@roids = []
end
def initialize(x, y, vel_x, vel_y, size)
super(x, y, vel_x + (rand - 0.5)*2.5, vel_y + (rand - 0.5)*2.5, size)
@color = $app.rgb((rand-0.5)*0.2 + 0.15, (rand-0.5)*0.2 + 0.15, (rand-0.5)*0.3+ 0.45, 0.5)
Roid.add_roid(self)
end
def draw
@x += @vel_x; @y += @vel_y
@x = @x % Asteroids::WIDTH; @y = @y % Asteroids::HEIGHT
$app.stroke($app.gray(1.0, 0.5))
$app.fill(@color)
$app.oval(@x, @y, @size, @size)
end
def explode(index)
Roid.remove_roid(index)
explode_factor = 0
case @size
when LARGE
explode_factor = rand(10) + 15
2.times { Roid.new(center_x - MEDIUM/2, center_y - MEDIUM/2, @vel_x, @vel_y, MEDIUM) }
when MEDIUM
explode_factor = rand(5) + 5
2.times { Roid.new(center_x - SMALL/2, center_y - SMALL/2, @vel_x, @vel_y, SMALL) }
when SMALL
explode_factor = rand(3) + 2
Asteroids.maybe_start_new_level
end
explode_factor.times do |i|
direction = ((2 * Math::PI) * i/explode_factor)
Explosion.new(center_x, center_y, 4*Math.cos(direction), 4*Math.sin(direction))
end
end
def self.check_for_collision_with(thing, margin = 0)
@roids.each_with_index do |roid, index|
return [roid, index] if roid.intersects_with?(thing, margin)
end
return false
end
end
class Explosion < CelestialBody
@explosions = []
def self.add_explosion(explosion)
@explosions << explosion
end
def self.remove(index)
@explosions.delete_at(index)
end
def self.draw_all
@explosions.each_with_index {|explosion, i| explosion.draw(i) }
end
def initialize(x, y, vel_x, vel_y)
super( x, y, vel_x + (rand-0.5)*4, vel_y + (rand-0.5)*4, 10)
@transparency = 1.0
Explosion.add_explosion(self)
end
def draw(index)
@x += @vel_x; @y += @vel_y
$app.stroke($app.gray(1.0, @transparency))
color = [(rand-0.5)*0.7 + 0.7, (rand-0.5)*0.7 + 0.7, (rand-0.5)*0.1 + 0.3, @transparency]
$app.fill($app.rgb(*color))
$app.oval(@x, @y, @size, @size)
@transparency -= 0.065
Explosion.remove(index) if @transparency <= 0
end
end
class Ship < CelestialBody
UNIT = 30
def initialize(x, y)
super(x, y, 0, 0, UNIT)
@direction = 0
@glow = 0
end
def draw
@x += @vel_x; @y += @vel_y
@x = @x % Asteroids::WIDTH; @y = @y % Asteroids::HEIGHT
$app.stroke($app.gray(1.0, 0.5))
$app.fill($app.rgb(0.7, 0.2, 0.2, 0.5))
$app.oval(@x, @y, UNIT, UNIT)
$app.oval(@x + 20*Math.cos(@direction) + 10, @y + 20*Math.sin(@direction) + 10, UNIT/3, UNIT/3)
$app.fill($app.rgb(1.0, 1.0, 0.8, 0.5))
$app.oval(@x + 5*Math.cos(@direction) + 12.5, @y + 5*Math.sin(@direction) + 12.5, UNIT/5, UNIT/5)
if @glow > 0
$app.stroke($app.gray(1.0, @glow + 0.2))
$app.fill($app.gray(1.0, @glow))
$app.oval(@x + 15*Math.cos(@direction - Math::PI + 0.35) + 11, @y + 15*Math.sin(@direction - Math::PI + 0.35) + 11, UNIT/4, UNIT/4)
$app.oval(@x + 15*Math.cos(@direction - Math::PI - 0.35) + 11, @y + 15*Math.sin(@direction - Math::PI - 0.35) + 11, UNIT/4, UNIT/4)
@glow -= 0.06
end
end
def shoot
Ray.new(@x + UNIT/2, @y + UNIT/2, @vel_x, @vel_y, @direction)
end
def move(way)
case way
when :left
@direction -= 0.25
when :right
@direction += 0.25
when :up
@glow = 0.8
@vel_x += 1.5*Math.cos(@direction); @vel_y += 1.5*Math.sin(@direction)
["@vel_x", "@vel_y"].each do |vel|
instance_variable_set(vel, 10) if instance_variable_get(vel) > 10
instance_variable_set(vel, -10) if instance_variable_get(vel) < -10
end
end
end
def check_for_collision_with_roids
if roid = Roid.check_for_collision_with(self)
Asteroids.ship_explodes
end
end
end
class Ray < CelestialBody
@rays = []
def self.add_ray(ray)
@rays << ray
end
def self.draw_all
@rays.each_with_index {|ray, i| ray.draw(i) }
end
def self.remove_ray(index)
@rays.delete_at(index)
end
def self.check_for_collision_with_roids
@rays.each_with_index do |ray, index|
if roid_list = Roid.check_for_collision_with(ray)
Ray.remove_ray(index)
roid_list[0].explode(roid_list[1])
end
end
end
def initialize(x, y, vel_x, vel_y, direction)
@direction = direction
super(x, y, vel_x + 8 * Math.cos(@direction), vel_y + 8 * Math.sin(@direction), 5)
Ray.add_ray(self)
end
def draw(index)
@x += @vel_x; @y += @vel_y
if @x > Asteroids::WIDTH || @x < 0 || @y > Asteroids::HEIGHT || @y < 0
Ray.remove_ray(index)
else
$app.stroke($app.rgb(1.0, 1.0, 0.8, 0.8))
$app.line(@x, @y, @x + @vel_x, @y + @vel_y)
end
end
end
Shoes.app :width => Asteroids::WIDTH, :height => Asteroids::HEIGHT, :title => "Asteroids", :resizable => false do
$app = self
Asteroids.new_game
animate(60) { Asteroids.draw }
keypress {|k| Asteroids.keypress(k) }
end
That's all,
— Jeremy