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