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,137 @@
1
+ require 'theseus/formatters/png'
2
+
3
+ module Theseus
4
+ module Formatters
5
+ class PNG
6
+ # Renders a UpsilonMaze to a PNG canvas. Does not currently support the
7
+ # +:wall_width+ option.
8
+ #
9
+ # You will almost never access this class directly. Instead, use
10
+ # UpsilonMaze#to(:png, options) to return the raw PNG data directly.
11
+ class Upsilon < PNG
12
+ # Create and return a fully initialized PNG::Upsilon object, with the
13
+ # maze rendered. To get the maze data, call #to_blob.
14
+ #
15
+ # See Theseus::Formatters::PNG for a list of all supported options.
16
+ def initialize(maze, options={})
17
+ super
18
+
19
+ width = @options[:outer_padding] * 2 + (3 * maze.width + 1) * @options[:cell_size] / 4
20
+ height = @options[:outer_padding] * 2 + (3 * maze.height + 1) * @options[:cell_size] / 4
21
+
22
+ canvas = ChunkyPNG::Image.new(width, height, @options[:background])
23
+
24
+ metrics = { size: @options[:cell_size] - @options[:cell_padding] * 2 }
25
+ metrics[:s4] = metrics[:size] / 4.0
26
+ metrics[:inc] = 3 * @options[:cell_size] / 4.0
27
+
28
+ maze.height.times do |y|
29
+ py = @options[:outer_padding] + y * metrics[:inc]
30
+ maze.row_length(y).times do |x|
31
+ cell = maze[x, y]
32
+ next if cell == 0
33
+
34
+ px = @options[:outer_padding] + x * metrics[:inc]
35
+
36
+ if (y + x) % 2 == 0
37
+ draw_octogon_cell(canvas, [x, y], px, py, cell, metrics)
38
+ else
39
+ draw_square_cell(canvas, [x, y], px, py, cell, metrics)
40
+ end
41
+ end
42
+ end
43
+
44
+ @blob = canvas.to_blob
45
+ end
46
+
47
+ private
48
+
49
+ def draw_octogon_cell(canvas, point, x, y, cell, metrics) #:nodoc:
50
+ p1 = [x + options[:cell_padding] + metrics[:s4], y + options[:cell_padding]]
51
+ p2 = [x + options[:cell_size] - options[:cell_padding] - metrics[:s4], p1[1]]
52
+ p3 = [x + options[:cell_size] - options[:cell_padding], y + options[:cell_padding] + metrics[:s4]]
53
+ p4 = [p3[0], y + options[:cell_size] - options[:cell_padding] - metrics[:s4]]
54
+ p5 = [p2[0], y + options[:cell_size] - options[:cell_padding]]
55
+ p6 = [p1[0], p5[1]]
56
+ p7 = [x + options[:cell_padding], p4[1]]
57
+ p8 = [p7[0], p3[1]]
58
+
59
+ fill_poly(canvas, [p1, p2, p3, p4, p5, p6, p7, p8], color_at(point))
60
+
61
+ any = proc { |x| x | (x << Maze::UNDER_SHIFT) }
62
+
63
+ if cell & any[Maze::NE] != 0
64
+ far_p6 = move(p6, metrics[:inc], -metrics[:inc])
65
+ far_p7 = move(p7, metrics[:inc], -metrics[:inc])
66
+ fill_poly(canvas, [p2, far_p7, far_p6, p3], color_at(point, any[Maze::NE]))
67
+ line(canvas, p2, far_p7, options[:wall_color])
68
+ line(canvas, p3, far_p6, options[:wall_color])
69
+ end
70
+
71
+ if cell & any[Maze::E] != 0
72
+ edge = (x + options[:cell_size] + options[:cell_padding] > canvas.width)
73
+ r1, r2 = p3, edge ? move(p4, options[:cell_padding], 0) : move(p7, options[:cell_size], 0)
74
+ fill_rect(canvas, r1[0], r1[1], r2[0], r2[1], color_at(point, any[Maze::E]))
75
+ line(canvas, r1, [r2[0], r1[1]], options[:wall_color])
76
+ line(canvas, r2, [r1[0], r2[1]], options[:wall_color])
77
+ end
78
+
79
+ if cell & any[Maze::SE] != 0
80
+ far_p1 = move(p1, metrics[:inc], metrics[:inc])
81
+ far_p8 = move(p8, metrics[:inc], metrics[:inc])
82
+ fill_poly(canvas, [p4, far_p1, far_p8, p5], color_at(point, any[Maze::SE]))
83
+ line(canvas, p4, far_p1, options[:wall_color])
84
+ line(canvas, p5, far_p8, options[:wall_color])
85
+ end
86
+
87
+ if cell & any[Maze::S] != 0
88
+ r1, r2 = p6, move(p2, 0, options[:cell_size])
89
+ fill_rect(canvas, r1[0], r1[1], r2[0], r2[1], color_at(point, any[Maze::S]))
90
+ line(canvas, r1, [r1[0], r2[1]], options[:wall_color])
91
+ line(canvas, r2, [r2[0], r1[1]], options[:wall_color])
92
+ end
93
+
94
+ line(canvas, p1, p2, options[:wall_color]) if cell & Maze::N == 0
95
+ line(canvas, p2, p3, options[:wall_color]) if cell & Maze::NE == 0
96
+ line(canvas, p3, p4, options[:wall_color]) if cell & Maze::E == 0
97
+ line(canvas, p4, p5, options[:wall_color]) if cell & Maze::SE == 0
98
+ line(canvas, p5, p6, options[:wall_color]) if cell & Maze::S == 0
99
+ line(canvas, p6, p7, options[:wall_color]) if cell & Maze::SW == 0
100
+ line(canvas, p7, p8, options[:wall_color]) if cell & Maze::W == 0
101
+ line(canvas, p8, p1, options[:wall_color]) if cell & Maze::NW == 0
102
+ end
103
+
104
+ def draw_square_cell(canvas, point, x, y, cell, metrics) #:nodoc:
105
+ v = options[:cell_padding] + metrics[:s4]
106
+ p1 = [x + v, y + v]
107
+ p2 = [x + options[:cell_size] - v, y + options[:cell_size] - v]
108
+
109
+ fill_rect(canvas, p1[0], p1[1], p2[0], p2[1], color_at(point))
110
+
111
+ any = proc { |x| x | (x << Maze::UNDER_SHIFT) }
112
+
113
+ if cell & any[Maze::E] != 0
114
+ r1 = [p2[0], p1[1]]
115
+ r2 = [x + metrics[:inc] + v, p2[1]]
116
+ fill_rect(canvas, r1[0], r1[1], r2[0], r2[1], color_at(point, any[Maze::E]))
117
+ line(canvas, r1, [r2[0], r1[1]], options[:wall_color])
118
+ line(canvas, [r1[0], r2[1]], r2, options[:wall_color])
119
+ end
120
+
121
+ if cell & any[Maze::S] != 0
122
+ r1 = [p1[0], p2[1]]
123
+ r2 = [p2[0], y + metrics[:inc] + v]
124
+ fill_rect(canvas, r1[0], r1[1], r2[0], r2[1], color_at(point, any[Maze::S]))
125
+ line(canvas, r1, [r1[0], r2[1]], options[:wall_color])
126
+ line(canvas, [r2[0], r1[1]], r2, options[:wall_color])
127
+ end
128
+
129
+ line(canvas, p1, [p2[0], p1[1]], options[:wall_color]) if cell & Maze::N == 0
130
+ line(canvas, [p2[0], p1[1]], p2, options[:wall_color]) if cell & Maze::E == 0
131
+ line(canvas, [p1[0], p2[1]], p2, options[:wall_color]) if cell & Maze::S == 0
132
+ line(canvas, p1, [p1[0], p2[1]], options[:wall_color]) if cell & Maze::W == 0
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,113 @@
1
+ require 'chunky_png'
2
+
3
+ module Theseus
4
+ # A "mask" is, conceptually, a grid of true/false values that corresponds,
5
+ # one-to-one, with the cells of a maze object. For every mask cell that is true,
6
+ # the corresponding cell in a maze may contain passages. For every mask cell that
7
+ # is false, the corresponding maze cell must be blank.
8
+ #
9
+ # Any object may be used as a mask as long as it responds to #height, #width, and
10
+ # #[].
11
+ class Mask
12
+ # Given a string, treat each line as rows and each character as a cell. Every
13
+ # period character (".") will be mapped to +true+, and everything else to +false+.
14
+ # This lets you define simple masks as ASCII art:
15
+ #
16
+ # mask_string = <<MASK
17
+ # ..........
18
+ # .X....XXX.
19
+ # ..X....XX.
20
+ # ...X....X.
21
+ # ....X.....
22
+ # .....X....
23
+ # .X....X...
24
+ # .XX....X..
25
+ # .XXX....X.
26
+ # ..........
27
+ # MASK
28
+ #
29
+ # mask = Theseus::Mask.from_text(mask_string)
30
+ #
31
+ def self.from_text(text)
32
+ new(text.strip.split(/\n/).map { |line| line.split(//).map { |c| c == '.' } })
33
+ end
34
+
35
+ # Given a PNG file with the given +file_name+, read the file and create a new
36
+ # mask where transparent pixels will be considered +true+, and all others +false+.
37
+ # Note that a pixel with any transparency at all will be considered +true+.
38
+ #
39
+ # The resulting mask will have the same dimensions as the image file.
40
+ def self.from_png(file_name)
41
+ image = ChunkyPNG::Image.from_file(file_name)
42
+ grid = Array.new(image.height) { |y| Array.new(image.width) { |x| (image[x, y] & 0xff) == 0 } }
43
+ new(grid)
44
+ end
45
+
46
+ # The number of rows in the mask.
47
+ attr_reader :height
48
+
49
+ # the length of the longest row in the mask.
50
+ attr_reader :width
51
+
52
+ # Instantiate a new mask from the given grid, which must be an Array of rows, and each
53
+ # row must be an Array of true/false values for each column in the row.
54
+ def initialize(grid)
55
+ @grid = grid
56
+ @height = @grid.length
57
+ @width = @grid.map { |row| row.length }.max
58
+ end
59
+
60
+ # Returns the +true+/+false+ value for the corresponding cell in the grid.
61
+ def [](x,y)
62
+ @grid[y][x]
63
+ end
64
+ end
65
+
66
+ # This is a specialized mask, intended for use with DeltaMaze instances (although
67
+ # it will work with any maze). This lets you easily create triangular delta mazes.
68
+ #
69
+ # mask = Theseus::TriangleMask.new(10)
70
+ # maze = Theseus::DeltaMaze.generate(mask: mask)
71
+ class TriangleMask
72
+ attr_reader :height, :width
73
+
74
+ # Returns a new TriangleMask instance with the given height. The width will
75
+ # always be <code>2h+1</code> (where +h+ is the height).
76
+ def initialize(height)
77
+ @height = height
78
+ @width = @height * 2 + 1
79
+ @grid = Array.new(@height) do |y|
80
+ run = y * 2 + 1
81
+ from = @height - y
82
+ to = from + run - 1
83
+ Array.new(@width) do |x|
84
+ (x >= from && x <= to) ? true : false
85
+ end
86
+ end
87
+ end
88
+
89
+ # Returns the +true+/+false+ value for the corresponding cell in the grid.
90
+ def [](x,y)
91
+ @grid[y][x]
92
+ end
93
+ end
94
+
95
+ # This is the default mask used by a maze when an explicit mask is not given.
96
+ # It simply reports every cell as available.
97
+ #
98
+ # mask = Theseus::TransparentMask.new(20, 20)
99
+ # maze = Theseus::OrthogonalMaze.new(mask: mask)
100
+ class TransparentMask
101
+ attr_reader :width, :height
102
+
103
+ def initialize(width=0, height=0)
104
+ @width = width
105
+ @height = height
106
+ end
107
+
108
+ # Always returns +true+.
109
+ def [](x,y)
110
+ true
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,855 @@
1
+ require 'theseus/mask'
2
+ require 'theseus/path'
3
+
4
+ module Theseus
5
+ # Theseus::Maze is an abstract class, intended to act solely as a superclass
6
+ # for specific maze types. Subclasses include OrthogonalMaze, DeltaMaze,
7
+ # SigmaMaze, and UpsilonMaze.
8
+ #
9
+ # Each cell in the maze is a bitfield. The bits that are set indicate which
10
+ # passages exist leading AWAY from this cell. Bits in the low byte (corresponding
11
+ # to the PRIMARY bitmask) represent passages on the normal plane. Bits
12
+ # in the high byte (corresponding to the UNDER bitmask) represent passages
13
+ # that are passing under this cell. (Under/over passages are controlled via the
14
+ # #weave setting, and are not supported by all maze types.)
15
+ #
16
+ # Mazes are generated using the recursive backtracking algorithm, which is fast,
17
+ # quite customizable, easily generalized to a variety of different maze types,
18
+ # and gives generally good results. On the down side, the current implementation
19
+ # will not regularly generate very challenging mazes, compared to human-built
20
+ # mazes of the same size.
21
+ class Maze
22
+ N = 0x01 # North
23
+ S = 0x02 # South
24
+ E = 0x04 # East
25
+ W = 0x08 # West
26
+ NW = 0x10 # Northwest
27
+ NE = 0x20 # Northeast
28
+ SW = 0x40 # Southwest
29
+ SE = 0x80 # Southeast
30
+
31
+ # bitmask identifying directional bits on the primary plane
32
+ PRIMARY = 0x00FF
33
+
34
+ # bitmask identifying directional bits under the primary plane
35
+ UNDER = 0xFF00
36
+
37
+ # The size of the PRIMARY bitmask (e.g. how far to the left the
38
+ # UNDER bitmask is shifted).
39
+ UNDER_SHIFT = 8
40
+
41
+ # The width of the maze (number of columns).
42
+ #
43
+ # In general, it is safest to use the #row_length method for a particular
44
+ # row, since it is theoretically possible for a maze subclass to describe
45
+ # a different width for each row.
46
+ attr_reader :width
47
+
48
+ # The height of the maze (number of rows).
49
+ attr_reader :height
50
+
51
+ # An integer between 0 and 100 (inclusive). 0 means passages will only
52
+ # change direction when they encounter a barrier they cannot move through
53
+ # (or under). 100 means that as passages are built, a new direction will
54
+ # always be randomly chosen for each step of the algorithm.
55
+ attr_reader :randomness
56
+
57
+ # An integer between 0 and 100 (inclusive). 0 means passages will never
58
+ # move over or under existing passages. 100 means whenever possible,
59
+ # passages will move over or under existing passages. Note that not all
60
+ # maze types support weaving.
61
+ attr_reader :weave
62
+
63
+ # An integer between 0 and 100 (inclusive), signifying the percentage
64
+ # of deadends in the maze that will be extended in some direction until
65
+ # they join with an existing passage. This will create loops in the
66
+ # graph. Thus, 0 is a "perfect" maze (with no loops), and 100 is a
67
+ # maze that is totally multiply-connected, with no dead-ends.
68
+ attr_reader :braid
69
+
70
+ # One of :none, :x, :y, or :xy, indicating which boundaries the maze
71
+ # should wrap around. The default is :none, indicating no wrapping.
72
+ # If :x, the maze will wrap around the left and right edges. If
73
+ # :y, the maze will wrap around the top and bottom edges. If :xy, the
74
+ # maze will wrap around both edges.
75
+ #
76
+ # A maze that wraps in a single direction may be mapped onto a cylinder.
77
+ # A maze that wraps in both x and y may be mapped onto a torus.
78
+ attr_reader :wrap
79
+
80
+ # A Theseus::Mask (or similar) instance, that is used by the algorithm to
81
+ # determine which cells in the space are allowed. This lets you create
82
+ # mazes that fill shapes, or flow around patterns.
83
+ attr_reader :mask
84
+
85
+ # One of :none, :x, :y, :xy, or :radial. Note that not all maze types
86
+ # support symmetry. The :x symmetry means the maze will be mirrored
87
+ # across the x axis. Similarly, :y symmetry means the maze will be
88
+ # mirrored across the y axis. :xy symmetry causes the maze to be
89
+ # mirrored across both axes, and :radial symmetry causes the maze to
90
+ # be mirrored radially about the center of the maze.
91
+ attr_reader :symmetry
92
+
93
+ # A 2-tuple (array) indicating the x and y coordinates where the maze
94
+ # should be entered. This is used primarly when generating the solution
95
+ # to the maze, and generally defaults to the upper-left corner.
96
+ attr_reader :entrance
97
+
98
+ # A 2-tuple (array) indicating the x and y coordinates where the maze
99
+ # should be exited. This is used primarly when generating the solution
100
+ # to the maze, and generally defaults to the lower-right corner.
101
+ attr_reader :exit
102
+
103
+ # The x-coordinate that the generation algorithm will consider next.
104
+ # This value is meaningless once a maze has been generated.
105
+ attr_reader :x
106
+
107
+ # The y-coordinate that the generation algorithm will consider next.
108
+ # This value is meaningless once a maze has been generated.
109
+ attr_reader :y
110
+
111
+ # A short-hand method for creating a new maze object and causing it to
112
+ # be generated, in one step. Returns the newly generated maze.
113
+ def self.generate(options={})
114
+ new(options).generate!
115
+ end
116
+
117
+ # Creates and returns a new maze object. Note that the maze will _not_
118
+ # be generated; the maze is initially blank.
119
+ #
120
+ # Many options are supported:
121
+ #
122
+ # [:width] The number of columns in the maze. Note that different
123
+ # maze types count columns and rows differently; you'll
124
+ # want to see individual maze types for more info.
125
+ # [:height] The number of rows in the maze.
126
+ # [:symmetry] The symmetry to be used when generating the maze. This
127
+ # defaults to +:none+, but may also be +:x+ (to have the
128
+ # maze mirrored across the x-axis), +:y+ (to mirror the
129
+ # maze across the y-axis), +:xy+ (to mirror across both
130
+ # axes simultaneously), and +:radial+ (to mirror the maze
131
+ # radially about the center). Some symmetry types may
132
+ # result in loops being added to the maze, regardless of
133
+ # the braid value (see the +:braid+ parameter).
134
+ # (NOTE: not all maze types support symmetry equally.)
135
+ # [:randomness] An integer between 0 and 100 (inclusive) indicating how
136
+ # randomly the maze is generated. A 0 means that the maze
137
+ # passages will prefer to go straight whenever possible.
138
+ # A 100 means the passages will choose random directions
139
+ # as often as possible.
140
+ # [:mask] An instance of Theseus::Mask (or something that acts
141
+ # similarly). This can be used to constrain the maze so that
142
+ # it fills or avoids specific areas, so that shapes and
143
+ # patterns can be made.
144
+ # [:weave] An integer between 0 and 100 (inclusive) indicating how
145
+ # frequently passages move under or over other passages.
146
+ # A 0 means the passages will never move over/under other
147
+ # passages, while a 100 means they will do so as often
148
+ # as possible. (NOTE: not all maze types support weaving.)
149
+ # [:braid] An integer between 0 and 100 (inclusive) representing
150
+ # the percentage of dead-ends that should be removed after
151
+ # the maze has been generated. Dead-ends are removed by
152
+ # extending them in some direction until they join with
153
+ # another passage. This will introduce loops into the maze,
154
+ # making it "multiply-connected". A braid value of 0 will
155
+ # always result in a "perfect" maze (with no loops), while
156
+ # a value of 100 will result in a maze with no dead-ends.
157
+ # [:wrap] Indicates which edges of the maze should wrap around.
158
+ # +:x+ will cause the left and right edges to wrap, and
159
+ # +:y+ will cause the top and bottom edges to wrap. You
160
+ # can specify +:xy+ to wrap both left-to-right and
161
+ # top-to-bottom. The default is +:none+ (for no wrapping).
162
+ # [:entrance] A 2-tuple indicating from where the maze is entered.
163
+ # By default, the maze's entrance will be the upper-left-most
164
+ # point. Note that it may lie outside the bounds of the maze
165
+ # by one cell (e.g. [-1,0]), indicating that the entrance
166
+ # is on the very edge of the maze.
167
+ # [:exit] A 2-tuple indicating from where the maze is exited.
168
+ # By default, the maze's entrance will be the lower-right-most
169
+ # point. Note that it may lie outside the bounds of the maze
170
+ # by one cell (e.g. [width,height-1]), indicating that the
171
+ # exit is on the very edge of the maze.
172
+ # [:prebuilt] Sometimes, you may want the new maze to be considered to be
173
+ # generated, but not actually have anything generated into it.
174
+ # You can set the +:prebuilt+ parameter to +true+ in this case,
175
+ # allowing you to then set the contents of the maze by hand,
176
+ # using the #[]= method.
177
+ def initialize(options={})
178
+ @width = (options[:width] || 10).to_i
179
+ @height = (options[:height] || 10).to_i
180
+
181
+ @symmetry = (options[:symmetry] || :none).to_sym
182
+ configure_symmetry
183
+
184
+ @randomness = options[:randomness] || 100
185
+ @mask = options[:mask] || TransparentMask.new
186
+ @weave = options[:weave].to_i
187
+ @braid = options[:braid].to_i
188
+ @wrap = options[:wrap] || :none
189
+
190
+ @cells = setup_grid or raise "expected #setup_grid to return the new grid"
191
+
192
+ @entrance = options[:entrance] || default_entrance
193
+ @exit = options[:exit] || default_exit
194
+
195
+ loop do
196
+ @y = rand(@cells.length)
197
+ @x = rand(@cells[@y].length)
198
+ break if valid?(@x, @y)
199
+ end
200
+
201
+ @tries = potential_exits_at(@x, @y).sort_by { rand }
202
+ @stack = []
203
+
204
+ @generated = options[:prebuilt]
205
+ end
206
+
207
+ # Generates the maze if it has not already been generated. This is
208
+ # essentially the same as calling #step repeatedly. If a block is given,
209
+ # it will be called after each step.
210
+ def generate!
211
+ yield if block_given? while step unless generated?
212
+ self
213
+ end
214
+
215
+ # Creates a new Theseus::Path object based on this maze instance. This can
216
+ # be used to (for instance) create special areas of the maze or routes through
217
+ # the maze that you want to color specially. The following demonstrates setting
218
+ # a particular cell in the maze to a light-purple color:
219
+ #
220
+ # path = maze.new_path(color: 0xff7fffff)
221
+ # path.set([5,5])
222
+ # maze.to(:png, paths: [path])
223
+ def new_path(meta={})
224
+ Path.new(self, meta)
225
+ end
226
+
227
+ # Instantiates and returns a new solver instance which encapsulates a
228
+ # solution algorithm. The options may contain the following keys:
229
+ #
230
+ # [:type] This defaults to +:backtracker+ (for the Theseus::Solvers::Backtracker
231
+ # solver), but may also be set to +:astar+ (for the Theseus::Solvers::Astar
232
+ # solver).
233
+ # [:a] A 2-tuple (defaulting to #start) that says where in the maze the
234
+ # solution should begin.
235
+ # [:b] A 2-tuple (defaulting to #finish) that says where in the maze the
236
+ # solution should finish.
237
+ #
238
+ # The returned solver will not yet have generated the solution. Use
239
+ # Theseus::Solvers::Base#solve or Theseus::Solvers::Base#step to generate the
240
+ # solution.
241
+ def new_solver(options={})
242
+ type = options[:type] || :backtracker
243
+
244
+ require "theseus/solvers/#{type}"
245
+ klass = Theseus::Solvers.const_get(type.to_s.capitalize)
246
+
247
+ a = options[:a] || start
248
+ b = options[:b] || finish
249
+
250
+ klass.new(self, a, b)
251
+ end
252
+
253
+ # Returns the solution for the maze as an array of 2-tuples, each indicating
254
+ # a cell (in sequence) leading from the start to the finish.
255
+ #
256
+ # See #new_solver for a description of the supported options.
257
+ def solve(options={})
258
+ new_solver(options).solution
259
+ end
260
+
261
+ # Returns the bitfield for the cell at the given (+x+,+y+) coordinate.
262
+ def [](x,y)
263
+ @cells[y][x]
264
+ end
265
+
266
+ # Sets the bitfield for the cell at the given (+x+,+y+) coordinate.
267
+ def []=(x,y,value)
268
+ @cells[y][x] = value
269
+ end
270
+
271
+ # Completes a single iteration of the maze generation algorithm. Returns
272
+ # +false+ if the method should not be called again (e.g., the maze has
273
+ # been completed), and +true+ otherwise.
274
+ def step
275
+ return false if @generated
276
+
277
+ if @deadends && @deadends.any?
278
+ dead_end = @deadends.pop
279
+ braid(dead_end[0], dead_end[1])
280
+
281
+ @generated = @deadends.empty?
282
+ return !@generated
283
+ end
284
+
285
+ direction = next_direction or return !@generated
286
+ nx, ny = move(@x, @y, direction)
287
+
288
+ apply_move_at(@x, @y, direction)
289
+
290
+ # if (nx,ny) is already visited, then we're weaving (moving either over
291
+ # or under the existing passage).
292
+ nx, ny, direction = perform_weave(@x, @y, nx, ny, direction) if @cells[ny][nx] != 0
293
+
294
+ apply_move_at(nx, ny, opposite(direction))
295
+
296
+ @stack.push([@x, @y, @tries])
297
+ @tries = potential_exits_at(nx, ny).sort_by { rand }
298
+ @tries.push direction if @tries.include?(direction) unless rand(100) < @randomness
299
+ @x, @y = nx, ny
300
+
301
+ return true
302
+ end
303
+
304
+ # Returns +true+ if the maze has been generated.
305
+ def generated?
306
+ @generated
307
+ end
308
+
309
+ # Since #entrance may be external to the maze, #start returns the cell adjacent to
310
+ # #entrance that lies within the maze. If #entrance is already internal to the
311
+ # maze, this method returns #entrance. If #entrance is _not_ adjacent to any
312
+ # internal cell, this method returns +nil+.
313
+ def start
314
+ adjacent_point(@entrance)
315
+ end
316
+
317
+ # Since #exit may be external to the maze, #finish returns the cell adjacent to
318
+ # #exit that lies within the maze. If #exit is already internal to the
319
+ # maze, this method returns #exit. If #exit is _not_ adjacent to any
320
+ # internal cell, this method returns +nil+.
321
+ def finish
322
+ adjacent_point(@exit)
323
+ end
324
+
325
+ # Returns an array of the possible exits for the cell at the given coordinates.
326
+ # Note that this does not take into account boundary conditions: a move in any
327
+ # of the returned directions may not actually be valid, and should be verified
328
+ # before being applied.
329
+ #
330
+ # This is used primarily by subclasses to allow for different shaped cells
331
+ # (e.g. hexagonal cells for SigmaMaze, octagonal cells for UpsilonMaze).
332
+ def potential_exits_at(x, y)
333
+ raise NotImplementedError, "subclasses must implement #potential_exits_at"
334
+ end
335
+
336
+ # Returns true if the maze may be wrapped in the x direction (left-to-right).
337
+ def wrap_x?
338
+ @wrap == :x || @wrap == :xy
339
+ end
340
+
341
+ # Returns true if the maze may be wrapped in the y direction (top-to-bottom).
342
+ def wrap_y?
343
+ @wrap == :y || @wrap == :xy
344
+ end
345
+
346
+ # Returns true if the given coordinates are valid within the maze. This will
347
+ # be the case if:
348
+ #
349
+ # 1. The coordinates lie within the maze's bounds, and
350
+ # 2. The current mask for the maze does not restrict the location.
351
+ #
352
+ # If the maze wraps in x, the x coordinate is unconstrained and will be
353
+ # mapped (via modulo) to the bounds. Similarly, if the maze wraps in y,
354
+ # the y coordinate will be unconstrained.
355
+ def valid?(x, y)
356
+ return false if !wrap_y? && (y < 0 || y >= height)
357
+ y %= height
358
+ return false if !wrap_x? && (x < 0 || x >= row_length(y))
359
+ x %= row_length(y)
360
+ return @mask[x, y]
361
+ end
362
+
363
+ # Moves the given (+x+,+y+) coordinates a single step in the given
364
+ # +direction+. If wrapping in either x or y is active, the result will
365
+ # be mapped to the maze's current bounds via modulo arithmetic. The
366
+ # resulting coordinates are returned as a 2-tuple.
367
+ #
368
+ # Example:
369
+ #
370
+ # x2, y2 = maze.move(x, y, Maze::W)
371
+ def move(x, y, direction)
372
+ nx, ny = x + dx(direction), y + dy(direction)
373
+
374
+ ny %= height if wrap_y?
375
+ nx %= row_length(ny) if wrap_x? && ny > 0 && ny < height
376
+
377
+ [nx, ny]
378
+ end
379
+
380
+ # Returns a array of all dead-ends in the maze. Each element of the array
381
+ # is a 2-tuple containing the coordinates of a dead-end.
382
+ def dead_ends
383
+ dead_ends = []
384
+
385
+ @cells.each_with_index do |row, y|
386
+ row.each_with_index do |cell, x|
387
+ dead_ends << [x, y] if dead?(cell)
388
+ end
389
+ end
390
+
391
+ dead_ends
392
+ end
393
+
394
+ # Removes one cell from all dead-ends in the maze. Each call to this method
395
+ # removes another level of dead-ends, making the maze increasingly sparse.
396
+ def sparsify!
397
+ dead_ends.each do |(x, y)|
398
+ cell = @cells[y][x]
399
+ direction = cell & PRIMARY
400
+ nx, ny = move(x, y, direction)
401
+
402
+ # if the cell includes UNDER codes, shifting it all UNDER_SHIFT bits to the right
403
+ # will convert those UNDER codes to PRIMARY codes. Otherwise, it will
404
+ # simply zero the cell, resulting in a blank spot.
405
+ @cells[y][x] >>= UNDER_SHIFT
406
+
407
+ # if it's a weave cell (that moves over or under another corridor),
408
+ # nix it and move back one more, so we don't wind up with dead-ends
409
+ # underneath another corridor.
410
+ if @cells[ny][nx] & (opposite(direction) << UNDER_SHIFT) != 0
411
+ @cells[ny][nx] &= ~((direction | opposite(direction)) << UNDER_SHIFT)
412
+ nx, ny = move(nx, ny, direction)
413
+ end
414
+
415
+ @cells[ny][nx] &= ~opposite(direction)
416
+ end
417
+ end
418
+
419
+ # Returns the direction opposite to the given +direction+. This will work
420
+ # even if the +direction+ value is in the UNDER bitmask.
421
+ def opposite(direction)
422
+ if direction & UNDER != 0
423
+ opposite(direction >> UNDER_SHIFT) << UNDER_SHIFT
424
+ else
425
+ case direction
426
+ when N then S
427
+ when S then N
428
+ when E then W
429
+ when W then E
430
+ when NE then SW
431
+ when NW then SE
432
+ when SE then NW
433
+ when SW then NE
434
+ end
435
+ end
436
+ end
437
+
438
+ # Returns the direction that is the horizontal mirror to the given +direction+.
439
+ # This will work even if the +direction+ value is in the UNDER bitmask.
440
+ def hmirror(direction)
441
+ if direction & UNDER != 0
442
+ hmirror(direction >> UNDER_SHIFT) << UNDER_SHIFT
443
+ else
444
+ case direction
445
+ when E then W
446
+ when W then E
447
+ when NW then NE
448
+ when NE then NW
449
+ when SW then SE
450
+ when SE then SW
451
+ else direction
452
+ end
453
+ end
454
+ end
455
+
456
+ # Returns the direction that is the vertical mirror to the given +direction+.
457
+ # This will work even if the +direction+ value is in the UNDER bitmask.
458
+ def vmirror(direction)
459
+ if direction & UNDER != 0
460
+ vmirror(direction >> UNDER_SHIFT) << UNDER_SHIFT
461
+ else
462
+ case direction
463
+ when N then S
464
+ when S then N
465
+ when NE then SE
466
+ when NW then SW
467
+ when SE then NE
468
+ when SW then NW
469
+ else direction
470
+ end
471
+ end
472
+ end
473
+
474
+ # Returns the direction that results by rotating the given +direction+
475
+ # 90 degrees in the clockwise direction. This will work even if the +direction+
476
+ # value is in the UNDER bitmask.
477
+ def clockwise(direction)
478
+ if direction & UNDER != 0
479
+ clockwise(direction >> UNDER_SHIFT) << UNDER_SHIFT
480
+ else
481
+ case direction
482
+ when N then E
483
+ when E then S
484
+ when S then W
485
+ when W then N
486
+ when NW then NE
487
+ when NE then SE
488
+ when SE then SW
489
+ when SW then NW
490
+ end
491
+ end
492
+ end
493
+
494
+ # Returns the direction that results by rotating the given +direction+
495
+ # 90 degrees in the counter-clockwise direction. This will work even if
496
+ # the +direction+ value is in the UNDER bitmask.
497
+ def counter_clockwise(direction)
498
+ if direction & UNDER != 0
499
+ counter_clockwise(direction >> UNDER_SHIFT) << UNDER_SHIFT
500
+ else
501
+ case direction
502
+ when N then W
503
+ when W then S
504
+ when S then E
505
+ when E then N
506
+ when NW then SW
507
+ when SW then SE
508
+ when SE then NE
509
+ when NE then NW
510
+ end
511
+ end
512
+ end
513
+
514
+ # Returns the change in x implied by the given +direction+.
515
+ def dx(direction)
516
+ case direction
517
+ when E, NE, SE then 1
518
+ when W, NW, SW then -1
519
+ else 0
520
+ end
521
+ end
522
+
523
+ # Returns the change in y implied by the given +direction+.
524
+ def dy(direction)
525
+ case direction
526
+ when S, SE, SW then 1
527
+ when N, NE, NW then -1
528
+ else 0
529
+ end
530
+ end
531
+
532
+ # Returns the number of cells in the given row. This is generally safer
533
+ # than relying the #width method, since it is theoretically possible for
534
+ # a maze to have a different number of cells for each of its rows.
535
+ def row_length(row)
536
+ @cells[row].length
537
+ end
538
+
539
+ # Returns +true+ if the given cell is a dead-end. This considers only
540
+ # passages on the PRIMARY plane (the UNDER bits are ignored, because the
541
+ # current algorithm for generating mazes will never result in a dead-end
542
+ # that is underneath another passage).
543
+ def dead?(cell)
544
+ raw = cell & PRIMARY
545
+ raw == N || raw == S || raw == E || raw == W ||
546
+ raw == NE || raw == NW || raw == SE || raw == SW
547
+ end
548
+
549
+ # If +point+ is already located at a valid point within the maze, this
550
+ # does nothing. Otherwise, it examines the potential exits from the
551
+ # given point and looks for the first one that leads immediately to a
552
+ # valid point internal to the maze. When it finds one, it adds a passage
553
+ # to that cell leading to +point+. If no such adjacent cell exists, this
554
+ # method silently does nothing.
555
+ def add_opening_from(point)
556
+ x, y = point
557
+ if valid?(x, y)
558
+ # nothing to be done
559
+ else
560
+ potential_exits_at(x, y).each do |direction|
561
+ nx, ny = move(x, y, direction)
562
+ if valid?(nx, ny)
563
+ @cells[ny][nx] |= opposite(direction)
564
+ return
565
+ end
566
+ end
567
+ end
568
+ end
569
+
570
+ # If +point+ is already located at a valid point withint he maze, this
571
+ # simply returns +point+. Otherwise, it examines the potential exits
572
+ # from the given point and looks for the first one that leads immediately
573
+ # to a valid point internal to the maze. When it finds one, it returns
574
+ # that point. If no such point exists, it returns +nil+.
575
+ def adjacent_point(point)
576
+ x, y = point
577
+ if valid?(x, y)
578
+ point
579
+ else
580
+ potential_exits_at(x, y).each do |direction|
581
+ nx, ny = move(x, y, direction)
582
+ return [nx, ny] if valid?(nx, ny)
583
+ end
584
+ end
585
+ end
586
+
587
+ # Returns the direction of +to+ relative to +from+. +to+ and +from+
588
+ # are both points (2-tuples).
589
+ def relative_direction(from, to)
590
+ if from[0] < to[0]
591
+ if from[1] < to[1]
592
+ SE
593
+ elsif from[1] > to[1]
594
+ NE
595
+ else
596
+ E
597
+ end
598
+ elsif from[0] > to[0]
599
+ if from[1] < to[1]
600
+ SW
601
+ elsif from[1] > to[1]
602
+ NW
603
+ else
604
+ W
605
+ end
606
+ elsif from[1] < to[1]
607
+ S
608
+ elsif from[1] > to[1]
609
+ N
610
+ else
611
+ # same point!
612
+ nil
613
+ end
614
+ end
615
+
616
+ # Applies a move in the given direction to the cell at (x,y). The +direction+
617
+ # parameter may also be :under, in which case the cell is left-shifted so as
618
+ # to move the existing passages to the UNDER plane.
619
+ #
620
+ # This method also handles the application of symmetrical moves, in the case
621
+ # where #symmetry has been specified.
622
+ #
623
+ # You'll generally never call this method directly, except to construct grids
624
+ # yourself.
625
+ def apply_move_at(x, y, direction)
626
+ if direction == :under
627
+ @cells[y][x] <<= UNDER_SHIFT
628
+ else
629
+ @cells[y][x] |= direction
630
+ end
631
+
632
+ case @symmetry
633
+ when :x then move_symmetrically_in_x(x, y, direction)
634
+ when :y then move_symmetrically_in_y(x, y, direction)
635
+ when :xy then move_symmetrically_in_xy(x, y, direction)
636
+ when :radial then move_symmetrically_radially(x, y, direction)
637
+ end
638
+ end
639
+
640
+ # Returns the type of the maze as a string. OrthogonalMaze, for
641
+ # instance, is reported as "orthogonal".
642
+ def type
643
+ self.class.name[/::(.*?)Maze$/, 1]
644
+ end
645
+
646
+ # Returns the maze rendered to a particular format. Supported
647
+ # formats are currently :ascii and :png. The +options+ hash is passed
648
+ # through to the formatter.
649
+ def to(format, options={})
650
+ case format
651
+ when :ascii then
652
+ require "theseus/formatters/ascii/#{type.downcase}"
653
+ Formatters::ASCII.const_get(type).new(self, options)
654
+ when :png then
655
+ require "theseus/formatters/png/#{type.downcase}"
656
+ Formatters::PNG.const_get(type).new(self, options).to_blob
657
+ else
658
+ raise ArgumentError, "unknown format: #{format.inspect}"
659
+ end
660
+ end
661
+
662
+ # Returns the maze rendered to a string.
663
+ def to_s(options={})
664
+ to(:ascii, options).to_s
665
+ end
666
+
667
+ def inspect # :nodoc:
668
+ "#<#{self.class.name}:0x%X %dx%d %s>" % [
669
+ object_id, @width, @height,
670
+ generated? ? "generated" : "not generated"]
671
+ end
672
+
673
+ private
674
+
675
+ # Not all maze types support symmetry. If a subclass supports any of the
676
+ # symmetry types (or wants to implement its own), it should override this
677
+ # method.
678
+ def configure_symmetry #:nodoc:
679
+ if @symmetry != :none
680
+ raise NotImplementedError, "only :none symmetry is implemented by default"
681
+ end
682
+ end
683
+
684
+ # The default grid should suffice for most maze types, but if a subclass
685
+ # wants a custom grid, it must override this method. Note that the method
686
+ # MUST always return an Array of rows, with each row being an Array of cells.
687
+ def setup_grid #:nodoc:
688
+ Array.new(height) { Array.new(width, 0) }
689
+ end
690
+
691
+ # Returns an array of deadends that ought to be braided (removed), based on
692
+ # the value of the #braid setting.
693
+ def deadends_to_braid #:nodoc:
694
+ return [] if @braid.zero?
695
+
696
+ ends = dead_ends
697
+
698
+ count = ends.length * @braid / 100
699
+ count = 1 if count < 1
700
+
701
+ ends.sort_by { rand }[0,count]
702
+ end
703
+
704
+ # Calculate the default entrance, by looking for the upper-leftmost point.
705
+ def default_entrance #:nodoc:
706
+ @cells.each_with_index do |row, y|
707
+ row.each_with_index do |cell, x|
708
+ return [x-1, y] if @mask[x, y]
709
+ end
710
+ end
711
+ [0, 0] # if every cell is masked, then 0,0 is as good as any!
712
+ end
713
+
714
+ # Calculate the default exit, by looking for the lower-rightmost point.
715
+ def default_exit #:nodoc:
716
+ @cells.reverse.each_with_index do |row, y|
717
+ ry = @cells.length - y - 1
718
+ row.reverse.each_with_index do |cell, x|
719
+ rx = row.length - x - 1
720
+ return [rx+1, ry] if @mask[rx, ry]
721
+ end
722
+ end
723
+ [0, 0] # if every cell is masked, then 0,0 is as good as any!
724
+ end
725
+
726
+ # Returns the next direction that ought to be attempted by the recursive
727
+ # backtracker. This will also handle the backtracking. If there are no
728
+ # more directions to attempt, and the stack is empty, this will return +nil+.
729
+ def next_direction #:nodoc:
730
+ loop do
731
+ direction = @tries.pop
732
+ nx, ny = move(@x, @y, direction)
733
+
734
+ if valid?(nx, ny) && (@cells[@y][@x] & (direction | (direction << UNDER_SHIFT)) == 0)
735
+ if @cells[ny][nx] == 0
736
+ return direction
737
+ elsif !dead?(@cells[ny][nx]) && @weave > 0 && rand(100) < @weave
738
+ # see if we can weave over/under the cell at (nx,ny)
739
+ return direction if weave_allowed?(@x, @y, nx, ny, direction)
740
+ end
741
+ end
742
+
743
+ while @tries.empty?
744
+ if @stack.empty?
745
+ finish!
746
+ return nil
747
+ else
748
+ @x, @y, @tries = @stack.pop
749
+ end
750
+ end
751
+ end
752
+ end
753
+
754
+ def move_symmetrically_in_x(x, y, direction) #:nodoc:
755
+ row_width = @cells[y].length
756
+ if direction == :under
757
+ @cells[y][row_width - x - 1] <<= UNDER_SHIFT
758
+ else
759
+ @cells[y][row_width - x - 1] |= hmirror(direction)
760
+ end
761
+ end
762
+
763
+ def move_symmetrically_in_y(x, y, direction) #:nodoc:
764
+ if direction == :under
765
+ @cells[@cells.length - y - 1][x] <<= UNDER_SHIFT
766
+ else
767
+ @cells[@cells.length - y - 1][x] |= vmirror(direction)
768
+ end
769
+ end
770
+
771
+ def move_symmetrically_in_xy(x, y, direction) #:nodoc:
772
+ row_width = @cells[y].length
773
+ if direction == :under
774
+ @cells[y][row_width - x - 1] <<= UNDER_SHIFT
775
+ @cells[@cells.length - y - 1][x] <<= UNDER_SHIFT
776
+ @cells[@cells.length - y - 1][row_width - x - 1] <<= UNDER_SHIFT
777
+ else
778
+ @cells[y][row_width - x - 1] |= hmirror(direction)
779
+ @cells[@cells.length - y - 1][x] |= vmirror(direction)
780
+ @cells[@cells.length - y - 1][row_width - x - 1] |= opposite(direction)
781
+ end
782
+ end
783
+
784
+ def move_symmetrically_radially(x, y, direction) #:nodoc:
785
+ row_width = @cells[y].length
786
+ if direction == :under
787
+ @cells[@cells.length - x - 1][y] <<= UNDER_SHIFT
788
+ @cells[x][row_width - y - 1] <<= UNDER_SHIFT
789
+ @cells[@cells.length - y - 1][row_width - x - 1] <<= UNDER_SHIFT
790
+ else
791
+ @cells[@cells.length - x - 1][y] |= counter_clockwise(direction)
792
+ @cells[x][row_width - y - 1] |= clockwise(direction)
793
+ @cells[@cells.length - y - 1][row_width - x - 1] |= opposite(direction)
794
+ end
795
+ end
796
+
797
+ # Finishes the generation of the maze by adding openings for the entrance
798
+ # and exit, and determing which dead-ends to braid (if any).
799
+ def finish! #:nodoc:
800
+ add_opening_from(@entrance)
801
+ add_opening_from(@exit)
802
+
803
+ @deadends = deadends_to_braid
804
+ @generated = @deadends.empty?
805
+ end
806
+
807
+ # If (x,y) is not a dead-end, this does nothing. Otherwise, it extends the
808
+ # dead-end in some direction until it joins with another passage.
809
+ #
810
+ # TODO: look for the direction that results in the longest loop.
811
+ # might be kind of spendy, but worth trying, at least.
812
+ def braid(x, y) #:nodoc:
813
+ return unless dead?(@cells[y][x])
814
+ tries = potential_exits_at(x, y)
815
+ [opposite(@cells[y][x]), *tries].each do |try|
816
+ next if try == @cells[y][x]
817
+ nx, ny = move(x, y, try)
818
+ if valid?(nx, ny)
819
+ opp = opposite(try)
820
+ next if @cells[ny][nx] & (opp << UNDER_SHIFT) != 0
821
+ @cells[y][x] |= try
822
+ @cells[ny][nx] |= opp
823
+ return
824
+ end
825
+ end
826
+ end
827
+
828
+ # Returns +true+ if a weave may be applied at (thru_x,thru_y) when moving
829
+ # from (from_x,from_y) in +direction+. This will be true if the thru cell
830
+ # does not already have anything in its UNDER plane, and if the cell
831
+ # on the far side of thru is valid and blank.
832
+ #
833
+ # Subclasses may need to override this method if special interpretations
834
+ # for +direction+ need to be considered (see SigmaMaze).
835
+ def weave_allowed?(from_x, from_y, thru_x, thru_y, direction) #:nodoc:
836
+ nx2, ny2 = move(thru_x, thru_y, direction)
837
+ return (@cells[thru_y][thru_x] & UNDER == 0) && valid?(nx2, ny2) && @cells[ny2][nx2] == 0
838
+ end
839
+
840
+ def perform_weave(from_x, from_y, to_x, to_y, direction) #:nodoc:
841
+ if rand(2) == 0 # move under existing passage
842
+ apply_move_at(to_x, to_y, direction << UNDER_SHIFT)
843
+ apply_move_at(to_x, to_y, opposite(direction) << UNDER_SHIFT)
844
+ else # move over existing passage
845
+ apply_move_at(to_x, to_y, :under)
846
+ apply_move_at(to_x, to_y, direction)
847
+ apply_move_at(to_x, to_y, opposite(direction))
848
+ end
849
+
850
+ nx, ny = move(to_x, to_y, direction)
851
+ [nx, ny, direction]
852
+ end
853
+
854
+ end
855
+ end