Hi,
Here is my first submission to a ruby quiz :)
I tried to make the A* algorithm as re-usable as possible (class
A_star).
As a parameter it gets an instance of the class Problem that need to
implement :
- neighbors(node) that returns a list of the neighbor nodes and their
distance to that node
- heuristic(node) that give to heuristical distance to the goal
- end_node?(node) that returns true if node is the goal
Thus the A_star class doesn't need to know anything about the problem
(it doesn't even care about what are nodes)
To run it just call the script with the map in stdio; it will print the
solution.
The implementation is rather simple and not optimized (the heuristic
and A_star#add_to_open are critical), and not very nice.
However, here is the code :
class Problem
attr_reader :start, :goal
def initialize
@data = []
STDIN.readlines.each do |line|
@data << line.chomp
start = line =~ /@/
@start = [start, @data.length-1] if start != nil
goal = line =~ /X/
@goal = [goal, @data.length-1] if goal != nil
end
end
def cost(x,y)
if x < 0 || x >= @data.length || y < 0 || y >= @data[0].length
nil
elsif @data[x][y..y].match(/[@\.X]/)
1
elsif @data[x][y..y] == '*'
2
elsif @data[x][y..y] == '^'
3
else
nil
end
end
# Returns the list of all the neighbors
def neighbors node
neighbors_list = []
x,y = node
for i in -1..1 do
for j in -1..1 do
if i != 0 || j != 0
cost = cost(x+i, y+j)
neighbors_list << [[x+i, y+j],cost] unless cost == nil
end
end
end
neighbors_list
end
def heuristic node
x, y = node
gx, gy = @goal
(gx-x)**2 + (gy-y)**2
end
def end_node? node; node == @goal; end
def print_solution path
data = @data
path.each{|x,y| data[x][y] = '#'}
data.each{|line| puts line}
end
end
class A_star
attr_reader :closed
def initialize problem
@problem = problem
@open = [problem.start]
@closed = []
@f = {problem.start => 0} # Estimated cost
@g = {problem.start => 0} # Cost so far
end
def run
while @open != []
node = @open.pop
@closed << node
return @closed if @problem.end_node? node
@problem.neighbors(node).each do |n|
neighbor, cost = n
add_to_open(neighbor, @g[node] + cost)
end
end
return nil
end
def add_to_open(node, cost)
unless @closed.include? node
if @open.include? node
@g[node] = cost if cost < @g[node]
else
@open << node
@g[node] = cost
end
@f[node] = @g[node] + @problem.heuristic(node)
@open.sort! {|a,b| @f[b] <=> @f[a]}
end
end
end
pb = Problem.new
test = A_star.new pb
pb.print_solution test.run
Ruby Quiz wrote:
> The three rules of Ruby Quiz:
>
> 1. Please do not post any solutions or spoiler discussion for this quiz until
> 48 hours have passed from the time on this message.
>
> 2. Support Ruby Quiz by submitting ideas as often as you can:
>
> http://www.rubyquiz.com/
>
> 3. Enjoy!
>
> Suggestion: A [QUIZ] in the subject of emails about the problem helps everyone
> on Ruby Talk follow the discussion. Please reply to the original quiz message,
> if you can.
>
> -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
>
> by Steven Davidovitz
>
> The A* (A-Star) search algorithm is a path-finding algorithm with many uses,
> including Artificial Intelligence for games. The search builds the route from
> tile to tile until it reaches the goal. To help choose the best possible tile
> the algorithm will use an equation that takes into account the distance from the
> tile to the goal and the cost of moving to that tile.
>
> Let's work through an example, so you can see how this comes together.
>
> Movement Cost for Terrain:
> Non-walkable:
> N/A = Water (~)
> Walkable:
> 1 = Flatlands (. or @ or X)
> 2 = Forest (*)
> 3 = Mountain (^)
>
> Test Map:
> @*^^^ @ = User start
> ~~*~. X = The goal tile
> **...
> ^..*~
> ~~*~X
>
> Step 1: Search the surrounding tiles for walkable choices.
>
> The only possible tile for the fist step is to the right: a forest (*) tile.
>
> Step 2: Go through that list finding the cost of movement for each of those
> choice tiles. The cost of movement is the path cost so far, plus the cost to
> move to the tile being considered.
>
> Our cost so far is 0, since we just started. The cost of a forest is 2 so the
> total cost of this move is 2 (0 + 2).
>
> Step 3: Determine the distance from the choice tiles to the goal and add that
> to the cost from Step 2 to find the score for each tile.
>
> You can calculate the distance from the tile to the goal using Manhattan
> distance formula |x1 - x2| + |y1 - y2|. For our example that is:
>
> |1 - 4| + |0 - 4| = |-3| + |-4| = 7
>
> Knowing that we can produce the final score for this move:
>
> score = cost (2) + distance to goal (7) = 9
>
> Step 4: Choose the best tile to move to by comparing the score of the
> surrounding tiles and choosing the lowest.
>
> Step 6: Repeat Steps 1-4 until you reach the goal tile. Be careful to prevent
> checking tiles twice and/or circling back.
>
> Test Map Solution:
> ##^^^ # = Best path
> ~~#~.
> **.#.
> ^..#~
> ~~*~#
>
> When you have a working solution, try it out on this move involved map:
>
> http://www.rubyquiz.com/large_map.txt