Skip to content

Commit

Permalink
solver documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
jamis committed Dec 19, 2010
1 parent d417a0d commit f374ff8
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 16 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
html
2 changes: 1 addition & 1 deletion bin/theseus
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion lib/theseus/formatters/png.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 47 additions & 9 deletions lib/theseus/solvers/astar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
12 changes: 9 additions & 3 deletions lib/theseus/solvers/backtracker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
42 changes: 40 additions & 2 deletions lib/theseus/solvers/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 }
Expand All @@ -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

Expand All @@ -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.