Hi _why,

On Nov 17, 2008, at 12:13 PM, _why wrote:
Hasty. This turns out to be 0.r1091, folks.
_why

Looking good! Alas, my new-and-improved tankspank seems to run even *slower* now:

# Tankspank version 5.3, 25-July-2008
#
# This code is hereby released into the public domain
# http://creativecommons.org/licenses/publicdomain/
#
# Originally by kevin conner <[EMAIL PROTECTED]>
#
# Updated by Ernest Prabhakar <[EMAIL PROTECTED]>

# Notable Features:
# * Abstract classes "Thing" for static objects, "Sprite" for moving objects
# * Damage, drag both calculated dynamically based on size
# * Converts between "game" and "screen" coordinates using moving Camera
# * Use isometric projection (draw_box) for 2.5D rendering
# * Miniature "Radar" display to help locate enemy tanks
# * AppTest proxy object allows sanity-checking outside Shoes runtime

require 'matrix'
require 'logger'

#
#  Parameters 
#

$log = Logger.new(STDERR)
#$log.level = Logger::DEBUG
$log.level = Logger::INFO

X=0
Y=1

N_ENEMIES = 5 #5
FRAMES_PER_SECOND=20
IMPULSE = 10.0/FRAMES_PER_SECOND
TIGHTNESS = 0.1

#
# Extensions to built-in Ruby classes
#


class TankVector < Vector
  
  def self.a2s(array)
    array.inject("[") {|p,q| p += sprintf(" %.1f",q)} + "]"
  end
  
  def self.[](*array); super; end

  def self.area(v); 4*v[X]*v[Y]; end
  
  def self.within?(v, size)
    v.each2(size) { |self_i, size_i| return false if self_i.abs > size_i }
    true
  end
  
  def self.rotation(angle)
		cos_a, sin_a = Math.cos(angle), Math.sin(angle)
		Matrix[ [cos_a, -sin_a], [sin_a, cos_a] ]
  end

  def self.reflect(v); TankVector[v[X], -v[Y]]; end

  def -(v); super; end

  def rotate_by radians; TankVector.rotation(radians) * self; end

  def reflect_by radians; TankVector.reflect(self).rotate_by(radians); end
  
  def ratio; @elements[Y] / @elements[X]; end

  def to_s; sprintf("[ %.1f, %.1f]", @elements[X], @elements[Y])  end

end


#
# CONSTANTS
#


BOUNDS = TankVector[1500.0, 1250.0]
SCREEN_SIZE = TankVector[700.0, 500.0]
SCREEN_ORIGIN = SCREEN_SIZE * 0.5
Z_SCALE = 600.0
RADAR_SCALE = 0.1
EXPLODE_RATE = 3.0/FRAMES_PER_SECOND
DRAG_MAX = 0.20

SHELL_SPEED = 20
SHELL_SIZE = TankVector[2.0,2.0]
TANK_SIZE = TankVector[10.0,15.0]
MUZZLE_SIZE = TankVector[25.0,0.0]
AIM_RADIUS = 10
DEST_RADIUS = 20
HEADLIGHT_RADIUS = 3
BOUNCE_DISTANCE = 60.0


#
# Mixin Modules
#

module Drawable
  
  def self.rand_color
    color = (1..3).collect { 0.2 + 0.4 * rand }
		color << 1.0
	  $app.rgb *color
  end

	def draw_oval; $app.oval *base_extent; end

	def draw_rect; $app.rect *base_extent; end

	def draw_rect_with_corners screen_corners
	  base = screen_corners[2]
	  extent = screen_corners[0] - screen_corners[2]
	  $app.rect *(base.to_a + extent.to_a)
  end

  def draw_poly(points)
	  points.inject(points[-1]) do |p, q|
	    $app.line *(p.to_a + q.to_a); q
    end
  end

	def draw_outline
	  points = screen_corners()
	  draw_poly(points)
  end
  
	def draw_along(vector)
	  p = screen_coord()
	  q = p + vector
	  $app.line *(p.to_a + q.to_a)
  end
  
  def draw_box
    bottom = screen_corners()
		top = screen_corners(@height)
    $log.debug "#{$camera}"
	  $log.debug "\nbottom| #{bottom.to_s}\n   top| #{top.to_s}"
 		draw_outline
		4.times { |i| $app.line *(bottom[i].to_a + top[i].to_a) }
		draw_rect_with_corners top
	end

	def draw
	  $log.debug "\nDrawing #{self}"
	  $app.stroke @stroke_color
		$app.fill @fill_color
		draw_content
  end
  
  def draw_content; draw_rect; end

end

module Hurtable
	attr_reader :health

	def reset_health; @health = mass(); end

	def strength; @health / mass(); end

	def dead?; @health <= 0; end

	def hurt damage; @health -= damage; end
	
  def mass; TankVector.area(@size); end

  def energy; 0; end # speed is zero

	def encounter obstacle # Sprites only
	  damage = [energy(), obstacle.energy()].max
	  hurt damage
	  obstacle.hurt damage
	  collide(obstacle) if mass < obstacle.mass
  end
	
end

#
# Static "Things"
#

class Thing
  include Drawable
	include Hurtable
  attr_accessor :center, :size, :stroke_color, :fill_color
  
  def initialize(center, size)
    @center = center.clone()
    @size = size
    @height = @size.r * (0.25 + rand * 1.5)
    @dz = 1 + (2/Math::PI) * Math.atan(@height / Z_SCALE)
		@stroke_color = @fill_color = Drawable::rand_color
		reset_health()
  end
  
  def to_s
    s = "#{self.class}:[EMAIL PROTECTED]@size.to_s}"
    s += sprintf(" x %.0f | ", @height)
    s += "fill: [EMAIL PROTECTED] stroke: [EMAIL PROTECTED]"
  end
  
  def front; @height * @size[X]; end
  
  def volume; @height * area(@size); end

  
  def screen_coord(dz=1); SCREEN_ORIGIN + (@center - $camera.center) * dz; end

	def base_extent; (screen_coord - @size).to_a + (@size * 2).to_a; end

	def native_base_extent; (@center - @size).to_a + (@size * 2).to_a; end

  # Center +-  sizes/ reflected size
  def cornerize(ctr, s, sR); [ctr+s, ctr-sR, ctr-s, ctr+sR]; end

	def corners; cornerize(@center, @size, TankVector.reflect(@size)); end

	def front_corners; c = corners(); [c[-1],c[0]]; end
	
	def faces; cornerize(@center, [EMAIL PROTECTED],0], TankVector[0,@size[Y]]); end

	def screen_corners(height = 0)
	  dz = (0 == height) ? 1 : @dz
	  ctr =  screen_coord(dz)
	  psize = @size * dz
	  cornerize(ctr, psize, TankVector.reflect(psize))  
  end

	# rectangular
	def contain? point; TankVector.within?(point - @center, @size); end
	
	def intersect? shape
	  self.corners.any? { |point| shape.contain? point } or
	  shape.corners.any? { |point| self.contain? point } 
	end
  
  def angle_to shape
    Math.atan2(@center[Y] - shape.center[Y], @center[X] - shape.center[X])
  end

end


class Building < Thing
	
	def initialize(west, east, north, south)
	  center = TankVector[(east+west)/2.0, (south+north)/2.0]
    size = TankVector[(east-west)/2.0, (south-north)/2.0]
		super(center, size)
	end
	
	def draw_content; draw_box; end
  
end

class Boundary < Building
	def initialize(west, east, north, south)
	  super
	  @fill_color = $app.black
		@stroke_color = $app.silver
		@height = Z_SCALE/2
  end  
end


class Circle < Thing
	def initialize(center, radius, color)
    size = TankVector[radius, radius]
		super(center, size)
		@stroke_color = color
		@fill_color = color
	end
	
	def draw_content; draw_oval; end
  
  def corners; faces(); end

  def contain? point; (point - @center).r < @size.r; end
  
  def approach pos; @center += (pos - @center) * TIGHTNESS; end

  def self.bound(x, high)
    return -high if x < -high
    return high if x > high
    x
  end

  def self.angle_bound(t)
    return t + 2*Math::PI if t < -Math::PI
    return t - 2*Math::PI if t > Math::PI
    t
  end

  def angle_toward p, t
    angle = angle_to(p)
    $log.debug "Turning by angle: #{angle}"
    Circle.angle_bound(angle - t)
  end

end

#
# Moving "Sprites"
#

class Sprite < Thing
  attr_reader :facing, :goal
  
  def turn_radius; 2.0 * @size[Y]; end

  def drag; [DRAG_MAX, @size[Y]/100].min; end
  
  def initialize(center, size)    
    super
		@last = @center
    @facing = @acceleration = @speed = 0.0
    @turn_max = 1.0/turn_radius
    @decay = 1 - drag
    @goal = nil
    @goal_color = $app.green
  end
  
  def set_goal pos, radius
    @goal = Circle.new pos, radius, @goal_color
    @goal.stroke_color = @stroke_color
  end

  def energy; @speed**2; end

  def turn
    return if @goal.nil?
    turn_by = @goal.angle_toward(self, @facing)
    @facing += Circle.bound(turn_by, @turn_max)
    @acceleration = Math.cos(turn_by) * IMPULSE * strength()
  end
  
  def move
    @speed += @acceleration 
    @speed *= @decay
		@last = @center
    @center += TankVector[1,0].rotate_by(@facing) * @speed
  end
  
	def arrive
    @acceleration = 0
		@goal = nil
	end
	
	def collide obstacle
		arrive 
		@center = @last
		@speed = 0
	end

	def at_goal?; not @goal.nil? and self.intersect?(@goal); end
	
  def update; turn(); move(); arrive if at_goal?; nil; end

  def draw_content; draw_oval; end
  
end

class Camera < Sprite
  
  def self.rand_within(x); rand(2*x) - x; end

  # Game coords go from -BOUNDS to +BOUNDS
  def self.random_location
    TankVector[rand_within(BOUNDS[X]), rand_within(BOUNDS[Y])]
  end

	def initialize tank
	  super tank.center, SCREEN_ORIGIN
	  @goal = tank
	  $follow_mouse = false
  end
  
  def update x,y
    new_center = $follow_mouse ? (game_coord(x,y) + @goal.center) * 0.5 : @goal.center
    @center += (new_center - @center) * TIGHTNESS
    nil
  end
  
  def game_coord(x, y); TankVector[x,y] - SCREEN_ORIGIN + @center; end
  
  # Screen coords go from 0 to SCREEN_SIZE
  def nearby
    game_coord rand(SCREEN_SIZE[X]), rand(SCREEN_SIZE[Y])
  end

end

class Explosion < Sprite
  
	def initialize shape
	  super(shape.center, shape.size*2)
	  @r_min = @size.r / 4.0
	  @fill_color = $app.orange
	  @stroke_color = shape.stroke_color
	end

	def update
	  @size = @size * (1-EXPLODE_RATE)
	  nil
	end
    
  def dead?; @size.r < @r_min ; end

end

class Shell < Sprite
  
	def initialize position, angle
	  position += MUZZLE_SIZE.rotate_by(angle)
	  super(position, SHELL_SIZE)
	  @facing = angle
	  @speed = SHELL_SPEED
	  @fill_color = @stroke_color = $app.red
	end
	
	def dead?; @speed < 1; end
  
end

class Turret < Sprite

  def reset_goal; set_goal(Vector[0,0], AIM_RADIUS); end
  
  def initialize(center, size)
    super
    @goal_color = $app.yellow
    reset_goal
  end
  
  def update_goal pos
    reset_goal if @goal.nil?
    @goal.center = pos
    @goal.stroke_color = @stroke_color
  end
  
  def alpha= level; nil; end
	
  def draw_content
    super
    muzzle = MUZZLE_SIZE.rotate_by(@facing)
    draw_along(muzzle)
  end
	
end

class TankCore < Sprite
	
	def initialize(center, color)
	  super(center, TANK_SIZE)
		@turret = Turret.new(center, TANK_SIZE*0.5)
		@stroke_color = @turret.stroke_color = color
		@headlights = front_corners().inject([]) do |l,c| 
		  l << Circle.new(center, HEADLIGHT_RADIUS, color)
	  end
    @will_fire = false
    @timer = @firing_cycle = FRAMES_PER_SECOND
	end
  
  def move_to pos; set_goal(pos, DEST_RADIUS); end

  def aim_at pos; @turret.update_goal pos; end

#  def move_toward pos; @goal.approach(pos) if @goal; end

#  def aim_toward pos; @turret.goal.approach(pos) if @turret.goal; end
  
  def health_color; :silver; end
  
	def hurt damage
	  super
	  @turret.fill_color = $app.send(health_color(), strength())
  end
	
  def update
    super
    @turret.turn()
    @turret.center = @center
    front_corners().each_with_index {|fc, i| @headlights[i].center = fc}

    if @will_fire and @timer <= 0
      @timer = @firing_cycle
      @will_fire = false
      return Shell.new(@center, @turret.facing)
    else
      @timer -= 1
      return nil
    end
  end

  def corners
	  cornerize(@center, @size.rotate_by(@facing), @size.reflect_by(@facing))
  end

  def screen_corners
	  cornerize(screen_coord(), @size.rotate_by(@facing), @size.reflect_by(@facing))
  end
	
	def draw_content
    draw_outline
    @turret.draw
    @headlights.each {|b| b.draw}
	end

end

class TankPlayer < TankCore

  def initialize center
    super center, $app.blue
    hurt 0
  end

  def health_color; :green; end

  def fire; @will_fire = true; end

	def draw_content
	  super
	  @goal.draw if not @goal.nil?
	  @turret.goal.draw if not @turret.goal.nil?
  end

end

class TankBot < TankCore

  def initialize center
    super center, Drawable::rand_color
    @firing_cycle = FRAMES_PER_SECOND * N_ENEMIES
    aim_at $camera.nearby()
    hurt 0
  end
  
  def health_color; :red; end
  	
  def update
    move_to $camera.nearby() if @goal.nil?
    aim_at $camera.center()
    if @timer <= 0
      move_to $camera.center()
      $log.debug "[EMAIL PROTECTED]: @[EMAIL PROTECTED] -> [EMAIL PROTECTED]"
    end
    @will_fire = true
    super
  end

# bounce!
	def collide obstacle
    super
    @facing = angle_to(obstacle)
    move_to(@center + TankVector[BOUNCE_DISTANCE,0].rotate_by(@facing))
    @speed = SHELL_SPEED
	end

end

#
# Game Management
#

class Radar < Thing
  def initialize(tanks)
    size = SCREEN_SIZE * RADAR_SCALE
    offset = SCREEN_SIZE - size
    super offset, size
    @stroke_color = $app.white
    @fill_color = $app.black
    @tanks = tanks
    @extent = [2,2]
    @scale = RADAR_SCALE * SCREEN_SIZE.r / BOUNDS.r
  end
  
  def radar_coord pos;  @center + pos * @scale; end
  
  def draw_content
    $app.rect *native_base_extent
    @tanks.each do |tank|
      radar_color = tank.dead? ? $app.brown : tank.stroke_color
      $app.stroke radar_color
      $app.oval *(radar_coord(tank.center).to_a + @extent)
    end
  end
end

class TankSpank
  attr :time
  
	def initialize
		@time = 0
		@boundary = Boundary.new(-BOUNDS[X], BOUNDS[X], -BOUNDS[Y], BOUNDS[Y])
		@shapes = [
			[-1000, -750, -750, -250],
			[-500, 250, -750, -250],
			[500, 1000, -750, -500],
			[750, 1250, -250, 0],
			[750, 1250, 250, 750],
			[250, 500, 0, 750],
			[-250, 0, 0, 500],
			[-500, 0, 750, 1000],
			[-1000, -500, 0, 500],
			[400, 600, -350, -150]
		].collect { |p| Building.new *p }
		@tank = TankPlayer.new(SCREEN_ORIGIN + TankVector[-150,-350])
    
		@shapes << @tank
		$camera = Camera.new(@tank)
		N_ENEMIES.times do
		  p = Camera::random_location()
		  redo if @shapes.any? {|s| s.contain? p}
		  @shapes << TankBot.new(p)
	  end
	  @radar = Radar.new @shapes.select {|s| s.is_a? TankCore}
	end
	
	def enemies; sprites = @shapes.select {|s| s.is_a? TankBot}; end
	
	def playing?; not @tank.dead? and enemies.size > 0; end

	def status
	  s = ""
	  s += sprintf "Time: %5.2f ", @time*1.0/FRAMES_PER_SECOND
	  s += sprintf "• Strength: %.0f%% ", @tank.strength * 100
	  s += sprintf "• Enemies: %d ", enemies.size
	  s += sprintf "• Position: %s ", TankVector.a2s(@tank.center.to_a)
	end
	
	def fire; @tank.fire; end

	def follow x,y;	@tank.move_to $camera.game_coord(x,y); end
	
	def check shape
    if shape.dead? and not shape.is_a? Explosion
      @shapes.delete shape
      @shapes << Explosion.new(shape)
      $log.debug "Exploded: #{shape}"
    end
	end
	
	def update x,y
		$camera.update x,y
	  @tank.aim_at $camera.game_coord(x,y)
	  @time += 1
		sprites = @shapes.select {|s| s.is_a? Sprite}
		visible_objects = @shapes.select {|s| s.intersect? $camera}
		sprites.each do |sprite| 
		  result = sprite.update
		  @shapes << result if not result.nil?
		  obstacles = @shapes.find_all {|s| s.intersect? sprite and s != sprite }
		  obstacles.each do |obstacle|
		    $log.debug "#{sprite} vs.\n\t#{obstacle}\n"
  		  sprite.encounter obstacle
  		  check sprite
  		  check obstacle
  	  end
      @shapes.delete sprite if sprite.dead? # in case died of natural causes
	  end
  end

  def draw
    $log.debug "[EMAIL PROTECTED] @ #{$camera}"
		$app.clear do
			@boundary.draw
			@shapes.each { |s| s.draw if s.is_a? Thing and $camera.intersect? s }
		end
		@radar.draw
	end
	
end

#
# Test Helpers for when NOT running inside Shoes
#

class AppTest
  def self.app(param); yield; end
  def self.method_missing sym, *args
    s = args.collect{|x|sprintf(" %.0f",x)} 
    $log << "\t#{sym}: #{s}\n"
  end
  def self.black (*args); [1]; end
  def self.red (*args); [1]; end
  def self.green (*args); [1]; end
  def self.blue (*args); [1]; end
  def self.cyan (*args); [1]; end
  def self.yellow (*args); [1]; end
  def self.rgb (*args); args; end
  def self.stroke (*args); end
  def self.fill (*args); end
  def self.clear; yield; end
end


def stack; end;
def para x; $log << x; end
def banner x; $log << x; end; def caption x; $log << x; end; 
def mouse(); [1] + SCREEN_ORIGIN.to_a; end

def keypress; yield "x"; end;
def click; yield mouse(); end;
def animate(n); 3.times {yield}; end

#
# User Interaction

APP = Shoes rescue AppTest
APP.app :width => SCREEN_SIZE[X], :height => SCREEN_SIZE[Y] do
	$app = self
  $app = APP if $app.class == Object

	@game = TankSpank.new
	stack do
	  para "Game Starting...", :stroke => blue
  end
  @paused = false
  
	keypress do |key|
	  case key
      when "1", "z"
        button, x, y = mouse()
		  	@game.follow x,y if @game.playing? 
      when "2", "x", " "
		  	@game.fire if @game.playing? 
		  when "p"
		    @paused = ! @paused
		  when "m"
		    $follow_mouse = true
		  when "t"
		    $follow_mouse = false
		  when "n"
      	@game = TankSpank.new
  			stack do
  				banner "New Game", :stroke => white, :margin => 10
  			end
		end
	end
	
	click do |button, x, y|
		if 1 == button
	  	@game.follow x, y # if @game.playing? 
		else
	  	@game.fire if @game.playing? 
		end
	end
	
	animate(FRAMES_PER_SECOND) do
    button, x, y = mouse()		
	  if not @paused
	    @game.update x,y
	    @game.draw
    end
		if @game.playing? 
			stack do
 			para @game.status, :stroke => orange, :margin => 10
  			para "Mouse: aim, Click: move, Space: fire, p: pause, m/t: follow mouse/tank, n: new\n", :stroke => silver, :margin => 10
				para "Paused", :stroke => yellow if @paused
  		end
		else
			stack do
				banner "Game Over", :stroke => white, :margin => 10
				if @game.enemies.size > 0
				  caption "Learn to drive!", :stroke => white, :margin => 20
			  else
				  caption "Congratulations!You totally r00l!", :stroke => white, :margin => 20
		    end
			end
		end # else
	end # animate
end # app


Any suggestions for how to redo the drawing? Cached images? Less alpha? Fewer bullets?

-- Ernie P.

Reply via email to