Shackers, shoesers, and shoobs,

This one should work on all three platforms.

Here's a lil' game of 'roids to satiate all of your Shoes-related
space-flying and gun-blasting needs. Don't play too safe,
suicide attacks can sometimes come in handy.

Give it a spin. When (or if) the asteroids crush you,
press 'N' to get a new game.

_why: The keypress event that Shoes gives you isn't so
hot for flying a spaceship. Slam the throttle hard left
(press the left arrow) and you'll get one jerk left, a pause,
and then a rapid torrent of lefts. Not so smooth. For the
same reason, you can't shoot and steer at the same time.
So while the keypress event is well and good for key
sniffing and delegation, maybe we could get something else?

How about a method on the Shoes app:
key_pressed?(:left)
Would return true or false.

G'night.
— Jeremy


# Omygawshkenas. Jan 2008.
# 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
    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
    100.times do |i|
      direction = ((2 * Math::PI) * i/100)
      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 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)
    @x, @y = x, y
    @vel_x = vel_x + (rand - 0.5)*2.5
    @vel_y = vel_y + (rand - 0.5)*2.5
    @size = size
    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($app.rgb(0.2, 0.2, 0.5, 0.5))
    $app.oval(@x, @y, @size, @size)
  end
  
  def explode(index)
    Roid.remove_roid(index)
    case @size
    when LARGE
      2.times { Roid.new(center_x - MEDIUM/2, center_y - MEDIUM/2, @vel_x, @vel_y, MEDIUM) }
    when MEDIUM
      2.times { Roid.new(center_x - SMALL/2, center_y - SMALL/2, @vel_x, @vel_y, SMALL) }
    when SMALL
      Asteroids.maybe_start_new_level
    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 Ship < CelestialBody
  UNIT = 30
  
  def initialize(x, y)
    @x, @y = x, y
    @vel_x, @vel_y = 0, 0
    @direction = 0
    @size = UNIT
  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)
  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
      @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
  @rays = []
  attr_accessor :x, :y, :size
  
  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)
    @x, @y, @vel_x, @vel_y, @direction = x, y, vel_x, vel_y, direction
    @vel_x += 8 * Math.cos(@direction)
    @vel_y += 8 * Math.sin(@direction)
    @size = 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
  
  def center_x
    @x
  end
  
  def center_y
    @y
  end
end

Shoes.app :width => Asteroids::WIDTH, :height => Asteroids::HEIGHT, :title => "Asteroids", :resizable => false do
  $app = self
  Asteroids.new_game
  animate(60) do
    Asteroids.draw
  end
  keypress do |k|
    Asteroids.keypress(k)
  end
end

Reply via email to