Permalink
Browse files

solver documentation

  • Loading branch information...
jamis committed Dec 19, 2010
1 parent d417a0d commit f374ff8e5cb8659872d8a7f6be4ed9cbdca6f1e2
Showing with 99 additions and 16 deletions.
  1. +1 −0 .gitignore
  2. +1 −1 bin/theseus
  3. +1 −1 lib/theseus/formatters/png.rb
  4. +47 −9 lib/theseus/solvers/astar.rb
  5. +9 −3 lib/theseus/solvers/backtracker.rb
  6. +40 −2 lib/theseus/solvers/base.rb
View
@@ -0,0 +1 @@
+html
View
@@ -216,7 +216,7 @@ if animate
solver = maze.new_solver(type: solution)
while solver.step
- path = solver.path(color: png_opts[:solution_color])
+ path = solver.to_path(color: png_opts[:solution_color])
step += 1
f = "%s-%04d.png" % [output, step]
@@ -76,7 +76,7 @@ def initialize(maze, options)
@paths = @options[:paths] || []
if @options[:solution]
- path = maze.new_solver(type: @options[:solution]).solve.path(color: @options[:solution_color])
+ path = maze.new_solver(type: @options[:solution]).solve.to_path(color: @options[:solution_color])
@paths = [path, *@paths]
end
end
@@ -2,37 +2,75 @@
module Theseus
module Solvers
+ # An implementation of the A* search algorithm. Although this can be used to
+ # search "perfect" mazes (those without loops), the recursive backtracker is
+ # more efficient in that case.
+ #
+ # The A* algorithm really shines, though, with multiply-connected mazes
+ # (those with non-zero braid values, or some symmetrical mazes). In this case,
+ # it is guaranteed to return the shortest path through the maze between the
+ # two points.
class Astar < Base
+
+ # This is the data structure used by the Astar solver to keep track of the
+ # current cost of each examined cell and its associated history (path back
+ # to the start).
+ #
+ # Although you will rarely need to use this class, it is documented because
+ # applications that wish to visualize the A* algorithm can use the open set
+ # of Node instances to draw paths through the maze as the algorithm runs.
class Node
include Comparable
- attr_accessor :point, :under, :path_cost, :estimate, :cost, :next
+ # The point in the maze associated with this node.
+ attr_accessor :point
+
+ # Whether the node is on the primary plane (+false+) or the under plane (+true+)
+ attr_accessor :under
+
+ # The path cost of this node (the distance from the start to this cell,
+ # through the maze)
+ attr_accessor :path_cost
+
+ # The (optimistic) estimate for how much further the exit is from this node.
+ attr_accessor :estimate
+
+ # The total cost associated with this node (path_cost + estimate)
+ attr_accessor :cost
+
+ # The next node in the linked list for the set that this node belongs to.
+ attr_accessor :next
+
+ # The array of points leading from the starting point, to this node.
attr_reader :history
- def initialize(point, under, path_cost, estimate, history)
+ def initialize(point, under, path_cost, estimate, history) #:nodoc:
@point, @under, @path_cost, @estimate = point, under, path_cost, estimate
@history = history
@cost = path_cost + estimate
end
- def <=>(node)
+ def <=>(node) #:nodoc:
cost <=> node.cost
end
end
+ # The open set. This is a linked list of Node instances, used by the A*
+ # algorithm to determine which nodes remain to be considered. It is always
+ # in sorted order, with the most likely candidate at the head of the list.
attr_reader :open
- def initialize(maze, a=maze.start, b=maze.finish)
+ def initialize(maze, a=maze.start, b=maze.finish) #:nodoc:
super
@open = Node.new(@a, false, 0, estimate(@a), [])
@visits = Array.new(@maze.height) { Array.new(@maze.width, 0) }
end
- def current_solution
+ def current_solution #:nodoc:
@open.history + [@open.point]
end
- def step
+ def step #:nodoc:
return false unless @open
current = @open
@@ -64,11 +102,11 @@ def step
private
- def estimate(pt)
+ def estimate(pt) #:nodoc:
Math.sqrt((@b[0] - pt[0])**2 + (@b[1] - pt[1])**2)
end
- def add_node(pt, under, path_cost, history)
+ def add_node(pt, under, path_cost, history) #:nodoc:
return if @visits[pt[1]][pt[0]] & (under ? 2 : 1) != 0
node = Node.new(pt, under, path_cost, estimate(pt), history)
@@ -98,7 +136,7 @@ def add_node(pt, under, path_cost, history)
end
end
- def move(pt, direction)
+ def move(pt, direction) #:nodoc:
[pt[0] + @maze.dx(direction), pt[1] + @maze.dy(direction)]
end
end
@@ -2,20 +2,26 @@
module Theseus
module Solvers
+ # An implementation of a recursive backtracker for solving a maze. Although it will
+ # work (eventually) for multiply-connected mazes, it will almost certainly not
+ # return an optimal solution in that case. Thus, this solver is best suited only
+ # for "perfect" mazes (those with no loops).
+ #
+ # For mazes that contain loops, see the Theseus::Solvers::Astar class.
class Backtracker < Base
- def initialize(maze, a=maze.start, b=maze.finish)
+ def initialize(maze, a=maze.start, b=maze.finish) #:nodoc:
super
@visits = Array.new(@maze.height) { Array.new(@maze.width, 0) }
@stack = []
end
VISIT_MASK = { false => 1, true => 2 }
- def current_solution
+ def current_solution #:nodoc:
@stack[1..-1].map { |item| item[0] }
end
- def step
+ def step #:nodoc:
if @stack == [:fail]
return false
elsif @stack.empty?
@@ -2,25 +2,43 @@
module Theseus
module Solvers
+ # The abstract superclass for solver implementations. It simply provides
+ # some helper methods that implementations would otherwise have to duplicate.
class Base
- attr_reader :maze, :a, :b
+ # The maze object that this solver will provide a solution for.
+ attr_reader :maze
+ # The point (2-tuple array) at which the solution path should begin.
+ attr_reader :a
+
+ # The point (2-tuple array) at which the solution path should end.
+ attr_reader :b
+
+ # Create a new solver instance for the given maze, using the given
+ # start (+a+) and finish (+b+) points. The solution will not be immediately
+ # generated; to do so, use the #step or #solve methods.
def initialize(maze, a=maze.start, b=maze.finish)
@maze = maze
@a = a
@b = b
@solution = nil
end
+ # Returns +true+ if the solution has been generated.
def solved?
@solution != nil
end
+ # Returns the solution path as an array of 2-tuples, beginning with #a and
+ # ending with #b. If the solution has not yet been generated, this will
+ # generate the solution first, and then return it.
def solution
solve unless solved?
@solution
end
+ # Generates the solution to the maze, and returns +self+. If the solution
+ # has already been generated, this does nothing.
def solve
while !solved?
step
@@ -29,6 +47,10 @@ def solve
self
end
+ # If the maze is solved, this yields each point in the solution, in order.
+ #
+ # If the maze has not yet been solved, this yields the result of calling
+ # #step, until the maze has been solved.
def each
if solved?
solution.each { |s| yield s }
@@ -37,7 +59,9 @@ def each
end
end
- def path(options={})
+ # Returns the solution (or, if the solution is not yet fully generated,
+ # the current_solution) as a Theseus::Path object.
+ def to_path(options={})
path = @maze.new_path(options)
prev = @maze.entrance
@@ -52,6 +76,20 @@ def path(options={})
path
end
+
+ # Returns the current (potentially partial) solution to the maze. This
+ # is for use while the algorithm is running, so that the current best-solution
+ # may be inspected (or displayed).
+ def current_solution
+ raise NotImplementedError, "solver subclasses must implement #current_solution"
+ end
+
+ # Runs a single iteration of the solution algorithm. Returns +false+ if the
+ # algorithm has completed, and non-nil otherwise. The return value is
+ # algorithm-dependent.
+ def step
+ raise NotImplementedError, "solver subclasses must implement #step"
+ end
end
end
end

0 comments on commit f374ff8

Please sign in to comment.