Hi,

Here is my solution - I haven't done much more than implement what was
asked for in the quiz. It produces this path on the large map:

0,0  1,1  2,2  3,3  4,4  5,5  6,6  7,7  8,8  9,9  10,9  11,10  12,11
12,12  13,13  14,14  15,15  15,16  15,17  16,18  17,18  18,19  19,20
20,21  21,22  22,22  23,23  22,24  22,25  23,26  24,27  25,28  26,29
27,29  28,29  29,30  30,31  30,32  31,33  30,34  30,35  31,36  32,37
33,38  34,39  34,40  35,41  36,42  37,43  38,43  39,44  40,44  41,45
42,46  43,46  44,47  45,48  46,48  47,48  48,49  49,49

I've used Brian Schröäer's PriorityQueue implementation, so to run
this you'll need to do gem install priorityqueue first.

In response to Martin about the quiz being "spoiled" - I don't come
from a Computer Science background and finding out about algorithms /
data structures which might be obvious to someone with a more technical
bent is one of the things I like getting out of ruby quiz. I think
there is room for a mix of quizes, some with more vague goals than
others.

Cheers,
Roland Swingler

My solution:

require 'rubygems'
require 'priority_queue'

class String
  # Convenience method so we can iterate over characters easily

  def to_chars
    a = []
    each_byte do |b|
      a << b.chr
    end
    a
  end
end

module TileMap
  # Although start is a plains, its cost is 0

  START  = 0
  PLAINS = 1
  FOREST = 2
  MOUNTAIN = 3
  WATER = nil

  class TilePath < Array
    def initialize(map)
      @map = map
      super([@map.start])
    end

    def cost
      inject(0) {|sum, c| sum + @map.tile(*c) }
    end
  end

  class Map
    attr_reader :start, :goal

    # parse a string contining a tile map into a nested array

    def initialize(map_str)
      @tiles = []
      map_str.each do |line|
        @tiles << []
        line.chomp.to_chars.each do |c|
          @tiles.last << case c
                         when "@"
                           START
                         when ".", "X"
                           PLAINS
                         when "*"
                           FOREST
                         when "^"
                           MOUNTAIN
                         when "~"
                           WATER
                         else
                           raise "Invalid tile type"
                         end
          if '@' == c
            @start = [@tiles.last.length - 1, @tiles.length - 1]
          elsif 'X' == c
            @goal = [@tiles.last.length - 1, @tiles.length - 1]
          end
        end
      end
      unless @start && @goal
        raise "Either position or goal tile are not set"
      end
    end

    def tile(x, y)
      @tiles[y][x]
    end

    def move_choices(x, y)
      if tile(x, y) == WATER
        raise "Illegal tile"
      end
      choices = []
      (-1..1).each do |i|
        ypos = y + i
        if ypos >= 0 && ypos < @tiles.length
          (-1..1).each do |j|
            xpos = x + j
            if xpos >= 0 && xpos < @tiles[i].length
              new_position = [xpos, ypos]
              if new_position != [x, y] && tile(*new_position) != WATER
                choices << new_position
              end
            end
          end
        end
      end
      choices
    end

    def self.manhattan(point1, point2)
      ((point2[0] - point1[0]) + (point2[1] - point1[1])).abs
    end
  end

  def self.a_star_search(map)
    # To store points we have already visited, so we don't repeat
ourselves
    closed = []
    open = PriorityQueue.new
    # Start off the queue with one path, which will contain the start
position
    open.push TilePath.new(map), 0
    while ! open.empty?
      # Get the path with the best cost to expand

      current_path = open.delete_min_return_key
      pos = current_path.last
      unless closed.include?(pos)
        if pos == map.goal
          return current_path
        end
        closed << pos
        # Create new paths and add them to the priority queue

        map.move_choices(*pos).each do |p|
          heuristic = Map.manhattan(p, map.goal)
          new_path = current_path.clone << p
          open.push(new_path, new_path.cost + heuristic)
        end
      end
    end
    raise "Cannot be solved"
  end
end

@m = TileMap::Map.new(File.read('large_map.txt'))
results = TileMap.a_star_search(@m)
puts results.map! {|pos| pos.join(",") }.join("  ")