theseus 1.0.0

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