theseus 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +137 -0
- data/Rakefile +42 -0
- data/bin/theseus +262 -0
- data/examples/a-star-search.rb +106 -0
- data/lib/theseus.rb +6 -0
- data/lib/theseus/delta_maze.rb +45 -0
- data/lib/theseus/formatters/ascii.rb +41 -0
- data/lib/theseus/formatters/ascii/delta.rb +79 -0
- data/lib/theseus/formatters/ascii/orthogonal.rb +156 -0
- data/lib/theseus/formatters/ascii/sigma.rb +57 -0
- data/lib/theseus/formatters/ascii/upsilon.rb +67 -0
- data/lib/theseus/formatters/png.rb +183 -0
- data/lib/theseus/formatters/png/delta.rb +85 -0
- data/lib/theseus/formatters/png/orthogonal.rb +87 -0
- data/lib/theseus/formatters/png/sigma.rb +105 -0
- data/lib/theseus/formatters/png/upsilon.rb +137 -0
- data/lib/theseus/mask.rb +113 -0
- data/lib/theseus/maze.rb +855 -0
- data/lib/theseus/orthogonal_maze.rb +195 -0
- data/lib/theseus/path.rb +91 -0
- data/lib/theseus/sigma_maze.rb +107 -0
- data/lib/theseus/solvers/astar.rb +144 -0
- data/lib/theseus/solvers/backtracker.rb +79 -0
- data/lib/theseus/solvers/base.rb +95 -0
- data/lib/theseus/upsilon_maze.rb +37 -0
- data/lib/theseus/version.rb +10 -0
- data/test/maze_test.rb +193 -0
- metadata +104 -0
@@ -0,0 +1,195 @@
|
|
1
|
+
require 'theseus/maze'
|
2
|
+
|
3
|
+
module Theseus
|
4
|
+
# An orthogonal maze is one in which the field is tesselated into squares. This is
|
5
|
+
# probably the type of maze that most people think of, when they think of mazes.
|
6
|
+
#
|
7
|
+
# The orthogonal maze implementation in Theseus is the most complete, supporting
|
8
|
+
# weaving as well as all four symmetry types. You can even convert any "perfect"
|
9
|
+
# (no loops) orthogonal maze to a "unicursal" maze. (Unicursal means "one course",
|
10
|
+
# and refers to a maze that has no junctions, only a single path that takes you
|
11
|
+
# through every cell in the maze exactly once.)
|
12
|
+
#
|
13
|
+
# maze = Theseus::OrthogonalMaze.generate(width: 10)
|
14
|
+
# puts maze
|
15
|
+
class OrthogonalMaze < Maze
|
16
|
+
def potential_exits_at(x, y) #:nodoc:
|
17
|
+
[N, S, E, W]
|
18
|
+
end
|
19
|
+
|
20
|
+
# Extends Maze#finish! to make sure symmetrical mazes are properly closed.
|
21
|
+
#--
|
22
|
+
# Eventually, this would be good to generalize somehow, and make available to
|
23
|
+
# the other maze types.
|
24
|
+
#++
|
25
|
+
def finish! #:nodoc:
|
26
|
+
# for symmetrical mazes, if the size of the maze in the direction of reflection is
|
27
|
+
# even, then we have two distinct halves that need to be joined in order for the
|
28
|
+
# maze to be fully connected.
|
29
|
+
|
30
|
+
available_width, available_height = @width, @height
|
31
|
+
|
32
|
+
case @symmetry
|
33
|
+
when :x then
|
34
|
+
available_width = available_width / 2
|
35
|
+
when :y then
|
36
|
+
available_height = available_height / 2
|
37
|
+
when :xy, :radial then
|
38
|
+
available_width = available_width / 2
|
39
|
+
available_height = available_height / 2
|
40
|
+
end
|
41
|
+
|
42
|
+
connector = lambda do |x, y, ix, iy, dir|
|
43
|
+
start_x, start_y = x, y
|
44
|
+
while @cells[y][x] == 0
|
45
|
+
y = (y + iy) % available_height
|
46
|
+
x = (x + ix) % available_width
|
47
|
+
break if start_x == x || start_y == y
|
48
|
+
end
|
49
|
+
|
50
|
+
if @cells[y][x] == 0
|
51
|
+
warn "maze cannot be fully connected"
|
52
|
+
nil
|
53
|
+
else
|
54
|
+
@cells[y][x] |= dir
|
55
|
+
nx, ny = move(x, y, dir)
|
56
|
+
@cells[ny][nx] |= opposite(dir)
|
57
|
+
[x,y]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
even = lambda { |x| x % 2 == 0 }
|
62
|
+
|
63
|
+
case @symmetry
|
64
|
+
when :x then
|
65
|
+
connector[available_width-1, rand(available_height), 0, 1, E] if even[@width]
|
66
|
+
when :y then
|
67
|
+
connector[rand(available_width), available_height-1, 1, 0, S] if even[@height]
|
68
|
+
when :xy then
|
69
|
+
if even[@width]
|
70
|
+
x, y = connector[available_width-1, rand(available_height), 0, 1, E]
|
71
|
+
@cells[@height-y-1][x] |= E
|
72
|
+
@cells[@height-y-1][x+1] |= W
|
73
|
+
end
|
74
|
+
|
75
|
+
if even[@height]
|
76
|
+
x, y = connector[rand(available_width), available_height-1, 1, 0, S]
|
77
|
+
@cells[y][@width-x-1] |= S
|
78
|
+
@cells[y+1][@width-x-1] |= N
|
79
|
+
end
|
80
|
+
when :radial then
|
81
|
+
if even[@width]
|
82
|
+
@cells[available_height-1][available_width-1] |= E | S
|
83
|
+
@cells[available_height-1][available_width] |= W | S
|
84
|
+
@cells[available_height][available_width-1] |= E | N
|
85
|
+
@cells[available_height][available_width] |= W | N
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
super
|
90
|
+
end
|
91
|
+
|
92
|
+
# Takes the current orthogonal maze and converts it into a unicursal maze. A unicursal
|
93
|
+
# maze is one with only a single path, and no dead-ends or junctions. Such mazes are
|
94
|
+
# more properly called "labyrinths". Note that although this method will always return
|
95
|
+
# a new OrthogonalMaze instance, it is not guaranteed to be a valid maze unless the
|
96
|
+
# current maze is "perfect" (not braided, containing no loops).
|
97
|
+
#
|
98
|
+
# The resulting unicursal maze will be twice as wide and twice as high as the original
|
99
|
+
# maze.
|
100
|
+
#
|
101
|
+
# The +options+ hash can be used to specify the <code>:entrance</code> and
|
102
|
+
# <code>:exit</code> points for the resulting maze. Currently, both the entrance and
|
103
|
+
# the exit must be adjacent.
|
104
|
+
#
|
105
|
+
# The process of converting an orthogonal maze to a unicursal maze is straightforward;
|
106
|
+
# take the maze, and divide all passages in half down the middle, making two passages.
|
107
|
+
# Dead-ends become a u-turn, etc. This is why the maze increases in size.
|
108
|
+
def to_unicursal(options={})
|
109
|
+
unicursal = OrthogonalMaze.new(options.merge(width: @width*2, height: @height*2, prebuilt: true))
|
110
|
+
|
111
|
+
set = lambda do |x, y, direction, *recip|
|
112
|
+
nx, ny = move(x, y, direction)
|
113
|
+
unicursal[x,y] |= direction
|
114
|
+
unicursal[nx, ny] |= opposite(direction) if recip[0]
|
115
|
+
end
|
116
|
+
|
117
|
+
@cells.each_with_index do |row, y|
|
118
|
+
row.each_with_index do |cell, x|
|
119
|
+
x2 = x * 2
|
120
|
+
y2 = y * 2
|
121
|
+
|
122
|
+
if cell & N != 0
|
123
|
+
set[x2, y2, N]
|
124
|
+
set[x2+1, y2, N]
|
125
|
+
set[x2, y2+1, N, true] if cell & W == 0
|
126
|
+
set[x2+1, y2+1, N, true] if cell & E == 0
|
127
|
+
set[x2, y2+1, E, true] if (cell & PRIMARY) == N
|
128
|
+
end
|
129
|
+
|
130
|
+
if cell & S != 0
|
131
|
+
set[x2, y2+1, S]
|
132
|
+
set[x2+1, y2+1, S]
|
133
|
+
set[x2, y2, S, true] if cell & W == 0
|
134
|
+
set[x2+1, y2, S, true] if cell & E == 0
|
135
|
+
set[x2, y2, E, true] if (cell & PRIMARY) == S
|
136
|
+
end
|
137
|
+
|
138
|
+
if cell & W != 0
|
139
|
+
set[x2, y2, W]
|
140
|
+
set[x2, y2+1, W]
|
141
|
+
set[x2+1, y2, W, true] if cell & N == 0
|
142
|
+
set[x2+1, y2+1, W, true] if cell & S == 0
|
143
|
+
set[x2+1, y2, S, true] if (cell & PRIMARY) == W
|
144
|
+
end
|
145
|
+
|
146
|
+
if cell & E != 0
|
147
|
+
set[x2+1, y2, E]
|
148
|
+
set[x2+1, y2+1, E]
|
149
|
+
set[x2, y2, E, true] if cell & N == 0
|
150
|
+
set[x2, y2+1, E, true] if cell & S == 0
|
151
|
+
set[x2, y2, S, true] if (cell & PRIMARY) == E
|
152
|
+
end
|
153
|
+
|
154
|
+
if cell & (N << UNDER_SHIFT) != 0
|
155
|
+
unicursal[x2, y2] |= (N | S) << UNDER_SHIFT
|
156
|
+
unicursal[x2+1, y2] |= (N | S) << UNDER_SHIFT
|
157
|
+
unicursal[x2, y2+1] |= (N | S) << UNDER_SHIFT
|
158
|
+
unicursal[x2+1, y2+1] |= (N | S) << UNDER_SHIFT
|
159
|
+
elsif cell & (W << UNDER_SHIFT) != 0
|
160
|
+
unicursal[x2, y2] |= (E | W) << UNDER_SHIFT
|
161
|
+
unicursal[x2+1, y2] |= (E | W) << UNDER_SHIFT
|
162
|
+
unicursal[x2, y2+1] |= (E | W) << UNDER_SHIFT
|
163
|
+
unicursal[x2+1, y2+1] |= (E | W) << UNDER_SHIFT
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
enter_at = unicursal.adjacent_point(unicursal.entrance)
|
169
|
+
exit_at = unicursal.adjacent_point(unicursal.exit)
|
170
|
+
|
171
|
+
if enter_at && exit_at
|
172
|
+
unicursal.add_opening_from(unicursal.entrance)
|
173
|
+
unicursal.add_opening_from(unicursal.exit)
|
174
|
+
|
175
|
+
if enter_at[0] < exit_at[0]
|
176
|
+
unicursal[enter_at[0], enter_at[1]] &= ~E
|
177
|
+
unicursal[enter_at[0]+1, enter_at[1]] &= ~W
|
178
|
+
elsif enter_at[1] < exit_at[1]
|
179
|
+
unicursal[enter_at[0], enter_at[1]] &= ~S
|
180
|
+
unicursal[enter_at[0], enter_at[1]+1] &= ~N
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
return unicursal
|
185
|
+
end
|
186
|
+
|
187
|
+
private
|
188
|
+
|
189
|
+
def configure_symmetry #:nodoc:
|
190
|
+
if @symmetry == :radial && @width != @height
|
191
|
+
raise ArgumentError, "radial symmetrial is only possible for mazes where width == height"
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
data/lib/theseus/path.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
module Theseus
|
2
|
+
# The Path class is used to represent paths (and, generally, regions) within
|
3
|
+
# a maze. Arbitrary metadata can be associated with these paths, as well.
|
4
|
+
#
|
5
|
+
# Although a Path can be instantiated directly, it is generally more convenient
|
6
|
+
# (and less error-prone) to instantiate them via Maze#new_path.
|
7
|
+
class Path
|
8
|
+
# Represents the exit paths from each cell in the Path. This is a Hash of bitfields,
|
9
|
+
# and should be treated as read-only.
|
10
|
+
attr_reader :paths
|
11
|
+
|
12
|
+
# Represents the cells within the Path. This is a Hash of bitfields, with bit 1
|
13
|
+
# meaning the primary plane for the cell is set for this Path, and bit 2 meaning
|
14
|
+
# the under plane for the cell is set.
|
15
|
+
attr_reader :cells
|
16
|
+
|
17
|
+
# Instantiates a new plane for the given +maze+ instance, and with the given +meta+
|
18
|
+
# data. Initially, the path is empty.
|
19
|
+
def initialize(maze, meta={})
|
20
|
+
@maze = maze
|
21
|
+
@paths = Hash.new(0)
|
22
|
+
@cells = Hash.new(0)
|
23
|
+
@meta = meta
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns the metadata for the given +key+.
|
27
|
+
def [](key)
|
28
|
+
@meta[key]
|
29
|
+
end
|
30
|
+
|
31
|
+
# Marks the given +point+ as occupied in this path. If +how+ is +:over+, the
|
32
|
+
# point is set in the primary plane. Otherwise, it is set in the under plane.
|
33
|
+
#
|
34
|
+
# The +how+ parameter is usually used in conjunction with the return value of
|
35
|
+
# the #link method:
|
36
|
+
#
|
37
|
+
# how = path.link(from, to)
|
38
|
+
# path.set(to, how)
|
39
|
+
def set(point, how=:over)
|
40
|
+
@cells[point] |= (how == :over ? 1 : 2)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Creates a link between the two given points. The points must be adjacent.
|
44
|
+
# If the corresponding passage in the maze moves into the under plane as it
|
45
|
+
# enters +to+, this method returns +:under+. Otherwise, it returns +:over+.
|
46
|
+
#
|
47
|
+
# If the two points are not adjacent, no link is created.
|
48
|
+
def link(from, to)
|
49
|
+
if (direction = @maze.relative_direction(from, to))
|
50
|
+
opposite = @maze.opposite(direction)
|
51
|
+
|
52
|
+
if @maze.valid?(from[0], from[1])
|
53
|
+
direction <<= Maze::UNDER_SHIFT if @maze[from[0], from[1]] & direction == 0
|
54
|
+
@paths[from] |= direction
|
55
|
+
end
|
56
|
+
|
57
|
+
opposite <<= Maze::UNDER_SHIFT if @maze[to[0], to[1]] & opposite == 0
|
58
|
+
@paths[to] |= opposite
|
59
|
+
|
60
|
+
return (opposite & Maze::UNDER == 0) ? :over : :under
|
61
|
+
end
|
62
|
+
|
63
|
+
return :over
|
64
|
+
end
|
65
|
+
|
66
|
+
# Adds all path and cell information from the parameter (which must be a
|
67
|
+
# Path instance) to the current Path object. The metadata from the parameter
|
68
|
+
# is not copied.
|
69
|
+
def add_path(path)
|
70
|
+
path.paths.each do |pt, value|
|
71
|
+
@paths[pt] |= value
|
72
|
+
end
|
73
|
+
|
74
|
+
path.cells.each do |pt, value|
|
75
|
+
@cells[pt] |= value
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns true if the given point is occuped in the path, for the given plane.
|
80
|
+
# If +how+ is +:over+, the primary plane is queried. Otherwise, the under
|
81
|
+
# plane is queried.
|
82
|
+
def set?(point, how=:over)
|
83
|
+
@cells[point] & (how == :over ? 1 : 2) != 0
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns true if there is a path from the given point, in the given direction.
|
87
|
+
def path?(point, direction)
|
88
|
+
@paths[point] & direction != 0
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'theseus/maze'
|
2
|
+
|
3
|
+
module Theseus
|
4
|
+
# A "sigma" maze is one in which the field is tesselated into hexagons.
|
5
|
+
# Trying to map such a field onto a two-dimensional grid is a little tricky;
|
6
|
+
# Theseus does so by treating a single row as the hexagon in the first
|
7
|
+
# column, then the hexagon below and to the right, then the next hexagon
|
8
|
+
# above and to the right (on a line with the first hexagon), and so forth.
|
9
|
+
# For example, the following grid consists of two rows of 8 cells each:
|
10
|
+
#
|
11
|
+
# _ _ _ _
|
12
|
+
# / \_/ \_/ \_/ \_
|
13
|
+
# \_/ \_/ \_/ \_/ \
|
14
|
+
# / \_/ \_/ \_/ \_/
|
15
|
+
# \_/ \_/ \_/ \_/ \
|
16
|
+
# \_/ \_/ \_/ \_/
|
17
|
+
#
|
18
|
+
# SigmaMaze supports weaving, but not symmetry (yet).
|
19
|
+
#
|
20
|
+
# maze = Theseus::SigmaMaze.generate(width: 10)
|
21
|
+
# puts maze
|
22
|
+
class SigmaMaze < Maze
|
23
|
+
|
24
|
+
# Because of how the cells are positioned relative to other cells in
|
25
|
+
# the same row, the definition of the diagonal walls changes depending
|
26
|
+
# on whether a cell is "shifted" (e.g. moved down a half-row) or not.
|
27
|
+
#
|
28
|
+
# ____ ____
|
29
|
+
# / N \ /
|
30
|
+
# /NW NE\____/
|
31
|
+
# \W E/ N \
|
32
|
+
# \_S__/W E\____
|
33
|
+
# \SW SE/
|
34
|
+
# \_S__/
|
35
|
+
#
|
36
|
+
# Thus, if a cell is shifted, W/E are in the upper diagonals, otherwise
|
37
|
+
# they are in the lower diagonals. It is important that W/E always point
|
38
|
+
# to cells in the same row, so that the #dx and #dy methods do not need
|
39
|
+
# to be overridden.
|
40
|
+
#
|
41
|
+
# This change actually makes it fairly easy to generalize the other
|
42
|
+
# operations, although weaving needs special attention (see #weave_allowed?
|
43
|
+
# and #perform_weave).
|
44
|
+
def potential_exits_at(x, y) #:nodoc:
|
45
|
+
[N, S, E, W] +
|
46
|
+
((x % 2 == 0) ? [NW, NE] : [SW, SE])
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
# This maps which axis the directions share, depending on whether a cell
|
52
|
+
# is shifted (+true+) or not (+false+). For example, in a non-shifted cell,
|
53
|
+
# E is on a line with NW, so AXIS_MAP[false][E] returns NW (and vice versa).
|
54
|
+
# This is used in the weaving algorithms to determine which direction an
|
55
|
+
# UNDER passage moves as it passes under a cell.
|
56
|
+
AXIS_MAP = {
|
57
|
+
false => {
|
58
|
+
N => S,
|
59
|
+
S => N,
|
60
|
+
E => NW,
|
61
|
+
NW => E,
|
62
|
+
W => NE,
|
63
|
+
NE => W
|
64
|
+
},
|
65
|
+
|
66
|
+
true => {
|
67
|
+
N => S,
|
68
|
+
S => N,
|
69
|
+
W => SE,
|
70
|
+
SE => W,
|
71
|
+
E => SW,
|
72
|
+
SW => E
|
73
|
+
}
|
74
|
+
}
|
75
|
+
|
76
|
+
# given a path entering in +entrance_direction+, returns the side of the
|
77
|
+
# cell that it would exit if it passed in a straight line through the cell.
|
78
|
+
def exit_wound(entrance_direction, shifted) #:nodoc:
|
79
|
+
# if moving W into the cell, then entrance_direction == W. To determine
|
80
|
+
# the axis within the new cell, we reverse it to find the wall within the
|
81
|
+
# cell that was penetrated (opposite(W) == E), and then
|
82
|
+
# look it up in the AXIS_MAP (E<->NW or E<->SW, depending on the cell position)
|
83
|
+
entrance_wall = opposite(entrance_direction)
|
84
|
+
AXIS_MAP[shifted][entrance_wall]
|
85
|
+
end
|
86
|
+
|
87
|
+
def weave_allowed?(from_x, from_y, thru_x, thru_y, direction) #:nodoc:
|
88
|
+
# disallow a weave if there is already a weave at this cell
|
89
|
+
return false if @cells[thru_y][thru_x] & UNDER != 0
|
90
|
+
|
91
|
+
pass_thru = exit_wound(direction, thru_x % 2 != 0)
|
92
|
+
out_x, out_y = move(thru_x, thru_y, pass_thru)
|
93
|
+
return valid?(out_x, out_y) && @cells[out_y][out_x] == 0
|
94
|
+
end
|
95
|
+
|
96
|
+
def perform_weave(from_x, from_y, to_x, to_y, direction) #:nodoc:
|
97
|
+
shifted = to_x % 2 != 0
|
98
|
+
pass_thru = exit_wound(direction, shifted)
|
99
|
+
|
100
|
+
apply_move_at(to_x, to_y, pass_thru << UNDER_SHIFT)
|
101
|
+
apply_move_at(to_x, to_y, AXIS_MAP[shifted][pass_thru] << UNDER_SHIFT)
|
102
|
+
|
103
|
+
nx, ny = move(to_x, to_y, pass_thru)
|
104
|
+
[nx, ny, pass_thru]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'theseus/solvers/base'
|
2
|
+
|
3
|
+
module Theseus
|
4
|
+
module Solvers
|
5
|
+
# An implementation of the A* search algorithm. Although this can be used to
|
6
|
+
# search "perfect" mazes (those without loops), the recursive backtracker is
|
7
|
+
# more efficient in that case.
|
8
|
+
#
|
9
|
+
# The A* algorithm really shines, though, with multiply-connected mazes
|
10
|
+
# (those with non-zero braid values, or some symmetrical mazes). In this case,
|
11
|
+
# it is guaranteed to return the shortest path through the maze between the
|
12
|
+
# two points.
|
13
|
+
class Astar < Base
|
14
|
+
|
15
|
+
# This is the data structure used by the Astar solver to keep track of the
|
16
|
+
# current cost of each examined cell and its associated history (path back
|
17
|
+
# to the start).
|
18
|
+
#
|
19
|
+
# Although you will rarely need to use this class, it is documented because
|
20
|
+
# applications that wish to visualize the A* algorithm can use the open set
|
21
|
+
# of Node instances to draw paths through the maze as the algorithm runs.
|
22
|
+
class Node
|
23
|
+
include Comparable
|
24
|
+
|
25
|
+
# The point in the maze associated with this node.
|
26
|
+
attr_accessor :point
|
27
|
+
|
28
|
+
# Whether the node is on the primary plane (+false+) or the under plane (+true+)
|
29
|
+
attr_accessor :under
|
30
|
+
|
31
|
+
# The path cost of this node (the distance from the start to this cell,
|
32
|
+
# through the maze)
|
33
|
+
attr_accessor :path_cost
|
34
|
+
|
35
|
+
# The (optimistic) estimate for how much further the exit is from this node.
|
36
|
+
attr_accessor :estimate
|
37
|
+
|
38
|
+
# The total cost associated with this node (path_cost + estimate)
|
39
|
+
attr_accessor :cost
|
40
|
+
|
41
|
+
# The next node in the linked list for the set that this node belongs to.
|
42
|
+
attr_accessor :next
|
43
|
+
|
44
|
+
# The array of points leading from the starting point, to this node.
|
45
|
+
attr_reader :history
|
46
|
+
|
47
|
+
def initialize(point, under, path_cost, estimate, history) #:nodoc:
|
48
|
+
@point, @under, @path_cost, @estimate = point, under, path_cost, estimate
|
49
|
+
@history = history
|
50
|
+
@cost = path_cost + estimate
|
51
|
+
end
|
52
|
+
|
53
|
+
def <=>(node) #:nodoc:
|
54
|
+
cost <=> node.cost
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# The open set. This is a linked list of Node instances, used by the A*
|
59
|
+
# algorithm to determine which nodes remain to be considered. It is always
|
60
|
+
# in sorted order, with the most likely candidate at the head of the list.
|
61
|
+
attr_reader :open
|
62
|
+
|
63
|
+
def initialize(maze, a=maze.start, b=maze.finish) #:nodoc:
|
64
|
+
super
|
65
|
+
@open = Node.new(@a, false, 0, estimate(@a), [])
|
66
|
+
@visits = Array.new(@maze.height) { Array.new(@maze.width, 0) }
|
67
|
+
end
|
68
|
+
|
69
|
+
def current_solution #:nodoc:
|
70
|
+
@open.history + [@open.point]
|
71
|
+
end
|
72
|
+
|
73
|
+
def step #:nodoc:
|
74
|
+
return false unless @open
|
75
|
+
|
76
|
+
current = @open
|
77
|
+
|
78
|
+
if current.point == @b
|
79
|
+
@open = nil
|
80
|
+
@solution = current.history + [@b]
|
81
|
+
else
|
82
|
+
@open = @open.next
|
83
|
+
|
84
|
+
@visits[current.point[1]][current.point[0]] |= current.under ? 2 : 1
|
85
|
+
|
86
|
+
cell = @maze[current.point[0], current.point[1]]
|
87
|
+
|
88
|
+
directions = @maze.potential_exits_at(current.point[0], current.point[1])
|
89
|
+
directions.each do |dir|
|
90
|
+
try = current.under ? (dir << Theseus::Maze::UNDER_SHIFT) : dir
|
91
|
+
if cell & try != 0
|
92
|
+
point = move(current.point, dir)
|
93
|
+
next unless @maze.valid?(point[0], point[1])
|
94
|
+
under = ((@maze[point[0], point[1]] >> Theseus::Maze::UNDER_SHIFT) & @maze.opposite(dir) != 0)
|
95
|
+
add_node(point, under, current.path_cost+1, current.history + [current.point])
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
return current
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def estimate(pt) #:nodoc:
|
106
|
+
Math.sqrt((@b[0] - pt[0])**2 + (@b[1] - pt[1])**2)
|
107
|
+
end
|
108
|
+
|
109
|
+
def add_node(pt, under, path_cost, history) #:nodoc:
|
110
|
+
return if @visits[pt[1]][pt[0]] & (under ? 2 : 1) != 0
|
111
|
+
|
112
|
+
node = Node.new(pt, under, path_cost, estimate(pt), history)
|
113
|
+
|
114
|
+
if @open
|
115
|
+
p, n = nil, @open
|
116
|
+
|
117
|
+
while n && n < node
|
118
|
+
p = n
|
119
|
+
n = n.next
|
120
|
+
end
|
121
|
+
|
122
|
+
if p.nil?
|
123
|
+
node.next = @open
|
124
|
+
@open = node
|
125
|
+
else
|
126
|
+
node.next = n
|
127
|
+
p.next = node
|
128
|
+
end
|
129
|
+
|
130
|
+
# remove duplicates
|
131
|
+
while node.next && node.next.point == node.point
|
132
|
+
node.next = node.next.next
|
133
|
+
end
|
134
|
+
else
|
135
|
+
@open = node
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def move(pt, direction) #:nodoc:
|
140
|
+
[pt[0] + @maze.dx(direction), pt[1] + @maze.dy(direction)]
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|