theseus 1.0.0

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