# = Wig-Wug
# == Quest for the Treasured RubyŽ
#
# Matz has lost the Treasured Ruby!  He has put together Operation Wig-Wug to find it again, and he is
# offering the best Treasured Ruby hunter cold hard cash to help him find it.  The goal of Wig-Wug is
# to beat your opponent's digger to the Treasured Ruby.  You will implement a class much like the Digger
# class below, which will play the game for you.  The goal is to use the information provided by the
# game master to reach the Treasured Ruby in less turns that it takes your opponent.  It is played in
# a grid board, so you will know your coordinates and distances will be measured and presented as coordinate
# pairs.
#
# === Rules for the board
# Here are a few rules of the board for Wig-Wug
#
#   * Boards are arbitrary widths and heights.  They could be 12x12 or 2x320.
#   * There are two types of critters roaming about: fleegols and geegols
#     * Encountering (landng on a space with) a fleegol will cause a short circuit
#       and stop your digger for one turn
#     * Encountering geegols will destroy your digger and end the game
#   * Geegols and fleegols are much like venus fly traps and not able to move
#   * Diggers are always placed equal distances from the Treasured Ruby but may have
#     different configurations of fleegol and geegol placement
#
# === Game flow
#
# Each digger will be given a chance to move one space each turn.
# To move, your digger must return which direction to move from the +move!+
# method.  So for example, to move up, you must return "up" or +:up+.  The
# only valid directions are "up", "down", "left", and "right".
#
# Each time you move, the game master will call +move!+ on your class, passing
# in how far away you are from the Treasured Ruby as a coordinate array as the first argument
# and a matrix of the surrounding spaces as the second argument.  For example,
# if the surrounding spaces looked like this...
#
#   O O F
#   G P O
#   O O O
#
#   O = open space
#   G = geegol
#   F = fleegol
#   P = player position
#
# ...the game master code will call like this...
#
#   digger.move!([3, 13],
#           [
#             ["O", "O", "F"],
#             ["G", "P", "O"],
#             ["O", "O", "O"],
#           ]
#         )
#
# This signifies there is a fleegol to the NE, a geegol to the west, and you are 3 either
# east or west and 13 north or south of the Treasured Ruby.
#
# * If you are at the game board edge, spaces are indicated by "E".  Touching one of these ends your game.
# * The Treasured Ruby is signified by an "R".  You want to move on to it.
# * You will not be able to see other players on the board, so don't worry!
#
# === A typical game turn
#
# 1. Game master calls move!([0,2], [["O", "O", "O"], ["O", "P", "F"], ["E", "E", "E"]])
# 2. Digger class returns "up"
# 3. Game master calls move!([2,0], [["E", "E", "E"], ["O", "P", "O"], ["O", "O", "G"]]) on opposing digger
# 4. Opposing digger returns "left"
# 5. Game master calls move!([0,1], [["O", "R", "O"], ["O", "P", "O"], ["O", "O", "F"]])
# 6. First digger returns "up"; first digger wins!
#
# Of course, this is a REALLY arbitrary game, but you get the point.
#
#
# == StarDigger by martin.rehfeld@glnetworks.de
#
# StarDigger was announced winner of the Wig-Wug challenge by the Quizmaster at 6th January 2009.
# It uses the A* pathfinding algorithm in a continuous brute force approach being optimistic
# about unknown terrain. The approach is based on research published by Tony Stentz from the
# Robotics Institute at Carnegie Mellon University [aaai96].

class StarDigger

  # provide accessor to digger name -- enable if needed
  # attr_reader :name

  # debug = true will enable printout of known map and projected path after each move!
  def initialize(debug = false)
    @debug = debug
    @player = [0,0]
    @map = DiggerMap.new(@player)
    @name = "Martin Rehfeld's StarDigger"

    # obsolete as return value of initialize gets ignored, but adheres to the
    # interface spec of this competition ;-)
    return @name
  end

  def move!(distance, matrix)
    distance.map! {|c| c.abs } # adjust distance measurement to work with JEG2's Wig-Wug simulator
                               # (it will report negative distances, which I consider a bug)
                               # see http://github.com/JEG2/wig-wug/tree
                               # a pull request was sent to JEG2 for this fix
                               # http://github.com/martinrehfeld/wig-wug/commit/92b867b8ee92bd9827296f16126ae3f241ec1262
    @target_position_initialized ||= initialize_target_position(distance)
    update_player_position!

    s = Surrounding.new(matrix)
    determine_treasure_position(s,distance)

    @map.add_surrounding!(@player,s)
    @map.add_treasure!(@target) unless @target.any?{|c| c.fuzzy? }

    puts @map if @debug

    path = @map.path_to(@target)
    raise "No path to target available with matrix = #{matrix.inspect} and known map\n#{@map}" if path.nil?
    move = delta_to_move([path[1][0]-path[0][0], path[1][1]-path[0][1]])

    @last_distance = distance
    @last_move = move

    move.to_s # return chosen move
  end

private

  def initialize_target_position(distance)
    @target ||= (0..1).collect{|i| FuzzyValue.new(@player[i]+distance[i],@player[i]-distance[i]) }
  end

  def update_player_position!
    (0..1).each {|i| @player[i] += move_delta(@last_move)[i] } if @last_move
  end

  def determine_treasure_position(s,distance)
    # seeing the treasure fully discloses its position, doesn't it ;-)
    if s.treasure_visible?
      (0..1).each {|i| @target[i].determine_value(@player[i] + s.treasure[i])}
      return
    end

    # being on the same axis with the treasure discloses one coordinate
    (0..1).each {|i| @target[i].determine_value(@player[i]) if @target[i].fuzzy? && distance[i] == 0 }

    # a visible edge also discloses at least one coordinate of the treasure
    if s.edge_visible?
      @target[0].determine_value(@player[0] + distance[0]) if @target[0].fuzzy? && s.edge_left?
      @target[0].determine_value(@player[0] - distance[0]) if @target[0].fuzzy? && s.edge_right?
      @target[1].determine_value(@player[1] + distance[1]) if @target[1].fuzzy? && s.edge_up?
      @target[1].determine_value(@player[1] - distance[1]) if @target[1].fuzzy? && s.edge_down?
    end

    # once we moved we can determine one coordinate of the treasure by observing the change in distance
    if @last_distance
      direction = DiggerMap.manhattan_distance([0,0],distance) < DiggerMap.manhattan_distance([0,0],@last_distance) ? :towards : :away
      if move_delta(@last_move)[0] != 0
        @target[0].determine_value(@player[0] + distance[0]*move_delta(@last_move)[0]*(direction == :towards ? +1 : -1))
      elsif move_delta(@last_move)[1] != 0
        @target[1].determine_value(@player[1] + distance[1]*move_delta(@last_move)[1]*(direction == :towards ? +1 : -1))
      end
    end
  end

  def move_delta(move)
    { :left  => [-1,  0],
      :right => [+1,  0],
      :up    => [ 0, -1],
      :down  => [ 0, +1] }[move.to_sym]
  end

  def delta_to_move(delta)
    if delta[0] < 0
      :left
    elsif delta[0] > 0
      :right
    elsif delta[1] < 0
      :up
    elsif delta[1] > 0
      :down
    else
      raise "No move available to travel given delta of #{delta.inspect}"
    end
  end

  # A object that can hold multiple values at once, returning a random one
  # of these when used
  class FuzzyValue
    def initialize(*values)
      @values = Array(values).uniq
    end

    def determine_value(value)
      @values = [value]
    end

    def fuzzy?
      @values.size > 1
    end

    # delegate anything else to a random member of @values
    undef to_s # to_s shall also be called on delegate object
    def method_missing(key, *args)
      @values[rand(@values.size)].send key, *args
    end
  end

  # the immediate surrounding of the digger
  class Surrounding

    def initialize(matrix)
      @matrix = matrix
    end

    def treasure_visible?
      @matrix.flatten.any?{|cell| cell.to_sym == :R }
    end

    def treasure
      [treasure_x, treasure_y]
    end

    def edge_visible?
      @matrix.flatten.any?{|cell| cell.to_sym == :E }
    end

    def edge_left?
      @matrix[1][0].to_sym == :E
    end

    def edge_right?
      @matrix[1][2].to_sym == :E
    end

    def edge_up?
      @matrix[0][1].to_sym == :E
    end

    def edge_down?
      @matrix[2][1].to_sym == :E
    end

    def covered_positions
      positions = []
      @matrix.each_with_index do |line,y|
        line.each_with_index do |cell,x|
          positions << [x-1,y-1]
        end
      end
      positions
    end

    def terrain(delta)
      @matrix[delta[1]+1][delta[0]+1]
    end

  private

    def treasure_x
      @matrix.each do |line|
        line.each_with_index do |cell,index|
          return index-1 if cell.to_sym == :R
        end
      end
      nil
    end

    def treasure_y
      @matrix.each_with_index do |line,index|
        return index-1 if line.any?{|cell| cell.to_sym == :R }
      end
      nil
    end
  end

  # A* pathfinding algorithm
  # courtesy of http://branch14.org/snippets/a_star_in_ruby.html
  class AStar

    def initialize(adjacency_func, cost_func, distance_func)
      @adjacency = adjacency_func
      @cost      = cost_func
      @distance  = distance_func
    end

    def find_path(start, goal)
      been_there = {}
      pqueue = PriorityQueue.new
      pqueue << [1, [start, [], 0]]

      while !pqueue.empty?
        spot, path_so_far, cost_so_far = pqueue.next
        next if been_there[spot]

        newpath = path_so_far + [spot]
        return newpath if (spot == goal)

        been_there[spot] = 1

        @adjacency.call(spot).each do |newspot|
          next if been_there[newspot]

          tcost = @cost.call(spot, newspot)
          next unless tcost
          newcost = cost_so_far + tcost
          pqueue << [newcost + @distance.call(goal, newspot),
                     [newspot, newpath, newcost]]
        end
      end

      return nil
    end

    # a simple priority queue used internally by A*
    class PriorityQueue
      def initialize
        @list = []
      end

      def add(priority, item)
        @list << [priority, @list.length, item]
        @list.sort!
        self
      end

      def <<(pritem)
        add(*pritem)
      end

      def next
        @list.shift[2]
      end

      def empty?
        @list.empty?
      end
    end

  end

  # the map of the currently known "world", also encapsulates routing to target
  class DiggerMap

    attr_reader :treasure_position

    def initialize(origin)
      @current_player_position = origin
      @player_path = [] << @current_player_position
      @map = {}
    end

    def add_surrounding!(player_pos,s)
      @current_player_position = player_pos.dup
      @player_path << @current_player_position
      s.covered_positions.each do |delta|
        position = [player_pos[0]+delta[0],player_pos[1]+delta[1]]
        @map[position] ||= {}
        @map[position][:terrain] = s.terrain(delta).to_sym if @map[position][:terrain] == :P || @map[position][:terrain].nil?
      end
    end

    def add_treasure!(treasure_pos)
      @treasure_position = treasure_pos.collect{|c| c.to_i }
      @map[@treasure_position] ||= {}
      @map[@treasure_position][:terrain] = :R
    end

    def self.manhattan_distance(p1,p2)
      distance(p1,p2).inject(0){|sum,coord| sum + coord.abs}
    end

    def path_to(goal)
      return nil unless @current_player_position

      a_star = AStar.new(
        # adjacency function
        Proc.new {|position| DiggerMap.adjacent_positions(position) },
        # cost function (be optimistic about unknown terrain)
        Proc.new {|old_position, position| terrain_cost_at(position) },
        # distance function
        Proc.new {|target, position| DiggerMap.manhattan_distance(position,target) }
      )

      @further_path = a_star.find_path(@current_player_position, goal.map{|c| c.to_i})
    end

    def to_s
      x_coordinates = known_positions.collect{|position| position[0]}
      y_coordinates = known_positions.collect{|position| position[1]}
      (y_coordinates.min..y_coordinates.max).each do |y|
        (x_coordinates.min..x_coordinates.max).each do |x|
          print_cell [x,y]
        end
        print "\n"
      end
    end

  private

    def self.distance(p1, p2)
      [p2[0]-p1[0], p2[1]-p1[1]]
    end

    def self.adjacent_positions(position)
      [[-1,0], [1,0], [0,-1], [0,1]].collect{|delta| [position[0]+delta[0],position[1]+delta[1]] }
    end

    def self.terrain_cost(terrain)
      case terrain.to_sym
        when :O then 1
        when :F then 2
        when :R then 1
        else nil # non-walkable!
      end
    end

    def terrain_cost_at(position)
      DiggerMap.terrain_cost((@map[position] || {:terrain => :O})[:terrain])
    end

    def on_player_path?(position)
      @player_path.include? position
    end

    def on_further_path?(position)
      @further_path.include? position rescue false
    end

    def known_positions
      @map.keys
    end

    def red(&block)
      print "\e[31m"
      yield if block
      print "\e[0m"
    end

    def green(&block)
      print "\e[32m"
      yield if block
      print "\e[0m"
    end

    def cell_symbol(position)
      cell = @map[position]
      if    cell                       then position == @player_path.first ? '@' : cell[:terrain] || "?"
      elsif on_further_path?(position) then "."
      else                                  " " end
    end

    def print_cell(position)
      if    on_player_path?(position)  then green { print cell_symbol(position) }
      elsif on_further_path?(position) then red   { print cell_symbol(position) }
      else                                  print cell_symbol(position) end
    end
  end

end
