require 'robot'

class << Module.new # anonymous container

  module RobotMath

    def weighted_linear_regression(data)
      w,x,y = [],[],[]

      data.each do |(xp,yp,wp)|
        w << wp.to_f
        x << xp.to_f
        y << yp.to_f
      end

      x2 = x.map{|e|e*e}
      xy = x.zip(y).map{|(a,b)|a*b}

      xs = x.zip(w).inject(0){|a,(b,c)|a+b*c}
      ys = y.zip(w).inject(0){|a,(b,c)|a+b*c}
      x2s = x2.zip(w).inject(0){|a,(b,c)|a+b*c}
      xys = xy.zip(w).inject(0){|a,(b,c)|a+b*c}
      one = w.inject(0){|a,b|a+b}

      div = xs*xs - one * x2s

      a = (xs * ys - one * xys)/div
      b = (xs * xys - x2s * ys)/div
      return a,b
    end
    
    def zero_fixed_linear_regression(data)
      x,y = [],[]

      data.each do |(xp,yp)|
        x << xp.to_f
        y << yp.to_f
      end

      x2 = x.map{|e|e*e}.inject(0){|a,b|a+b}
      xy = x.zip(y).map{|(a,b)|a*b}.inject(0){|a,b|a+b}

      return xy/x2
    end


    def offset(heading_a,heading_b = 0)
      my_offset = (heading_a-heading_b) % 360
      my_offset = my_offset -360 if my_offset > 180
      my_offset
    end

  end


  module Turnarounder
    def initialize

      @wanted_heading = 0

      @wanted_gun_heading = 0

      @wanted_radar_heading = 0
      
      @last_radar_heading = 0
      
      super 
    end

    def head_to deg
      @wanted_heading = deg
    end

    def head_gun_to deg
      @wanted_gun_heading = deg
    end

    def head_radar_to deg
      @wanted_radar_heading = deg
    end

    def next_delta_heading
      [-10,[offset(@wanted_heading,heading),10].min].max
    end

    alias turn_amount next_delta_heading
    
    def ready?
      offset(next_heading - @wanted_heading).abs < 2
    end

    def next_heading
      heading + next_delta_heading
    end

    def turn_gun_amount
      [-30,[offset(@wanted_gun_heading,gun_heading+next_delta_heading),30].min].max
    end
    
    def gun_ready?
      offset(next_gun_heading - @wanted_gun_heading).abs < 2
    end

    def next_delta_gun_heading
      next_delta_heading+turn_gun_amount
    end

    def next_gun_heading
      heading + next_delta_gun_heading
    end
    
    def turn_radar_amount
      [-60,[offset(@wanted_radar_heading,radar_heading+next_delta_gun_heading),60].min].max
    end

    def radar_ready?
      

      offset(next_radar_heading - @wanted_radar_heading).abs < 2

    end

    def next_delta_radar_heading
      next_delta_gun_heading+turn_radar_amount
    end

    def next_radar_heading
      radar_heading + next_delta_radar_heading
    end

    def final_turn
      proxy_turn turn_amount
      proxy_turn_gun turn_gun_amount
      proxy_turn_radar turn_radar_amount

      @last_radar_heading = radar_heading

    end
    
    def turn x
      @wanted_heading += x
    end
    
    def turn_gun x
      @wanted_gun_heading += x
    end
    
    def turn_radar x
      @wanted_radar_heading += x
    end

    def mid_radar_heading
      turnc = offset(radar_heading,@last_radar_heading)
      
      radar_heading-(turnc/2.0)    
    end

  end

  module Pointanizer
    include Math
    include RobotMath
    include Turnarounder

    def move_to x,y
      @move_x = x
      @move_y = y
      @move_mode = :to
    end

    def move mode,x,y
      @move_x,@move_y = x,y
      @move_mode = mode
    end

    def halt
      @move_mode = false
    end

    def moving?
       @move_mode 
    end

    def on_wall?
      xcor <= size*3 or ycor <= size*3 or battlefield_width - xcor <= size*3 or battlefield_height - ycor <= size*3
    end

    def final_point

        yc = ycor-@move_y rescue 0
        xc = @move_x-xcor rescue 0

        if hypot(yc,xc) < size/3
          @move_mode = false
        end

        acc = true

        case @move_mode
        when :to
          head_to atan2(yc,xc).to_deg
        when :away
          head_to atan2(yc,xc).to_deg+180
        when :side_a
          head_to atan2(yc,xc).to_deg+60
        when :side_b
          head_to atan2(yc,xc).to_deg-60
        when nil,false
          acc = false
        else
          raise "Unknown move mode!"
        end

        accelerate(8) if acc

    end

    def rad_to_xy(r,d)
      return xcor + cos(r.to_rad)*d, \
             ycor - sin(r.to_rad)*d
    end

  end
  
  class Brain
    include Math
    include RobotMath
    include Turnarounder
    include Pointanizer
    
    BULLET_SPEED = 30.0
    TRACK_TIMEOUT = 5
    HISTORY_SIZE = 7
    MIN_POINTS = 4
    HISTORY_TIMEOUT = 20

    SCAN_SWITCH = 2.8
    SCAN_SWITCH2 = 1.2
    
    TRACK_RANGE = 1150.0
    
    #movement
    
    INFACTOR = 9
    OUTFACTOR = 10
    RANDOMIZE = 7
    TIMEOUT = 20
    DIFF = 5.3
    HITAWAY = 5
    SDIFF = 60*DIFF
    
    RANDTURN = 0.11
    
    attr_accessor :predx, :predy
    
    def initialize(robot)
      @robot = robot
      super()

      @points = []
       
      @last_seen_time = -TRACK_TIMEOUT
      
      @radar_speed = 1
      @track_mul = 1

      @searching =0
      @seeking =0
      
      #movement
      @move_direction = 1
      @lasthitcount = 0
      @lasthitcount2 = false
      @lastchange = -TIMEOUT
    end

    def old_predict ptime
      if @points.size < MIN_POINTS
        return rand(battlefield_width),rand(battlefield_height)
      end

      ltime = @points.last.last
      ftime = ltime - HISTORY_TIMEOUT
      xint = []
      yint = []
      @points.each do |(x,y,ktime)|
        r = ((ktime-ftime)/HISTORY_TIMEOUT.to_f)
        r = (10**r)**2
        xint << [ktime,x,r]
        yint << [ktime,y,r]
      end
    
      xa,xb = weighted_linear_regression(xint)
      ya,yb = weighted_linear_regression(yint)

      return xa*ptime+xb,ya*ptime+yb
    
    end

    def predict ptime
      if @points.size < MIN_POINTS
        return rand(battlefield_width),rand(battlefield_height)
      end
      
      ltime = (@points.last.last+@points[-2].last)/2.0
      lx = (@points.last[0]+@points[-2][0])/2.0
      ly = (@points.last[1]+@points[-2][1])/2.0
      
      xint = @points.map{|x,y,xtime| [xtime-ltime,x-lx]}
      yint = @points.map{|x,y,xtime| [xtime-ltime,y-ly]}
      
      
      xa = zero_fixed_linear_regression xint
      ya = zero_fixed_linear_regression yint
      
      q = (ptime-ltime)
      
      x,y =  q*xa+lx,q*ya+ly
      
      x2,y2 = old_predict ptime
      
      return (x+x2)/2,(y+y2)/2
      
    end

    def predcurrent
      @predx,@predy = predict time unless @predx
    end

    def tick events
      
      fire 0.1
      
      #event processing

      if event = events['robot_scanned'].pop
        dist = event.first
        
        x,y = rad_to_xy(mid_radar_heading,dist)
      
      
        @points << [x,y,time]
      
        if @points.size > HISTORY_SIZE or time - @points.first.last >= HISTORY_TIMEOUT
          @points.shift
        end
        
        @last_seen_time = time
        
        @radar_speed = 1
        
        @track_mul = 1
        
        head_radar_to mid_radar_heading
        
        @direction = 1
        
      end
      
      #moving
      
      @predx,@predy = nil,nil
      
      if events['got_hit'].pop
        @lasthitcount +=1
      elsif @lasthitcount2
        @lasthitcount2 = false
        @lasthitcount = 0
      else 
        @lasthitcount2 = true
      end

      if ((on_wall? or (tmp = @lasthitcount >= HITAWAY) or rand < RANDTURN) and time - @lastchange > TIMEOUT) or @lasthitcount >= HITAWAY * 4
        @lasthitcount2 = false
        @lasthitcount = 0
        @lastchange = time
        @move_direction *= -1
      end
      halt
      accelerate 8
      predcurrent

      yc = ycor-predy rescue 0
      xc = predx-xcor rescue 0

      deg = atan2(yc,xc).to_deg+90*@move_direction

      hyp = hypot(yc,xc)

      if hyp < SDIFF
        deg += OUTFACTOR*@move_direction
      elsif hyp > SDIFF+size
        deg -= INFACTOR*@move_direction
      end 

      deg += rand(RANDOMIZE)
      deg -= rand(RANDOMIZE)

      head_to deg
      
      #aiming
       
      if @points.size >= MIN_POINTS
        predcurrent
        
        hyp = hypot(@predx-xcor,@predy-ycor)
        
        steps = (hyp-20) / BULLET_SPEED 
      
        fx,fy = predict(time+steps+1)
        
        gh = atan2(ycor-fy,fx-xcor).to_deg+rand(7)-3
        
        head_gun_to gh
      end  
      
      #scanning

      if time-@last_seen_time >= TRACK_TIMEOUT or @points.size < MIN_POINTS
        say "Searching,"
   
        if radar_ready?
          turn_radar @radar_speed
          @radar_speed *= -SCAN_SWITCH
        end
      
      else
        say "Seek and Destroy!"

        if radar_ready?
          predcurrent
          
          yc = ycor-@predy rescue 0
          xc = @predx-xcor rescue 0
        
          deg = atan2(yc,xc).to_deg
        
          dist = hypot(yc,xc)
          
          sign = [-0.5,-1.5,-0.5,0.5,1.5,0.5][@direction % 6]

          @direction += 1
        
          deg +=  ( TRACK_RANGE * @track_mul * sign)/dist
        
          @track_mul *= SCAN_SWITCH2
        
          head_radar_to deg
        
        end
      end

      final_point
      final_turn
      
    end

    def method_missing(*args,&block)
      @robot.relay(*args,&block)
    end

  end

  class Proxy
    include ::Robot

    def initialize
      @brain = Brain.new(self)
    end

    EXPORT_MAP = Hash.new{|h,k|k}

    EXPORT_MAP['xcor'] = 'x'
    EXPORT_MAP['ycor'] = 'y'
    EXPORT_MAP['proxy_turn'] = 'turn'
    EXPORT_MAP['proxy_turn_gun'] = 'turn_gun'
    EXPORT_MAP['proxy_turn_radar'] = 'turn_radar'
    
    def relay(method,*args,&block)
      self.send(EXPORT_MAP[method.to_s],*args,&block)
    end

    def tick events
      @brain.tick events
    end

  end


  classname = "Ente"
  unless Object.const_defined?(classname)
    Object.const_set(classname,Class.new(Proxy))
  end
end
