theseus 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|