theseus 1.0.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,80 @@
1
+ require 'theseus/algorithms/base'
2
+
3
+ module Theseus
4
+ module Algorithms
5
+ # The recursive backtracking algorithm is a quick, flexible algorithm
6
+ # for generating mazes. It tends to produce mazes with fewer dead-ends
7
+ # than algorithms like Kruskal's or Prim's.
8
+ class RecursiveBacktracker < Base
9
+ # The x-coordinate that the generation algorithm will consider next.
10
+ attr_reader :x
11
+
12
+ # The y-coordinate that the generation algorithm will consider next.
13
+ attr_reader :y
14
+
15
+ def initialize(maze, options={}) #:nodoc:
16
+ super
17
+
18
+ loop do
19
+ @y = rand(@maze.height)
20
+ @x = rand(@maze.row_length(@y))
21
+ break if @maze.valid?(@x, @y)
22
+ end
23
+
24
+ @tries = @maze.potential_exits_at(@x, @y).shuffle
25
+ @stack = []
26
+ end
27
+
28
+ def do_step #:nodoc:
29
+ direction = next_direction or return false
30
+ nx, ny = @maze.move(@x, @y, direction)
31
+
32
+ @maze.apply_move_at(@x, @y, direction)
33
+
34
+ # if (nx,ny) is already visited, then we're weaving (moving either over
35
+ # or under the existing passage).
36
+ nx, ny, direction = @maze.perform_weave(@x, @y, nx, ny, direction) if @maze[nx, ny] != 0
37
+
38
+ @maze.apply_move_at(nx, ny, @maze.opposite(direction))
39
+
40
+ @stack.push([@x, @y, @tries])
41
+ @tries = @maze.potential_exits_at(nx, ny).shuffle
42
+ @tries.push direction if @tries.include?(direction) unless rand(100) < @maze.randomness
43
+ @x, @y = nx, ny
44
+
45
+ return true
46
+ end
47
+
48
+ private
49
+
50
+ # Returns the next direction that ought to be attempted by the recursive
51
+ # backtracker. This will also handle the backtracking. If there are no
52
+ # more directions to attempt, and the stack is empty, this will return +nil+.
53
+ def next_direction #:nodoc:
54
+ loop do
55
+ direction = @tries.pop
56
+ nx, ny = @maze.move(@x, @y, direction)
57
+
58
+ if @maze.valid?(nx, ny) && (@maze[@x, @y] & (direction | (direction << Maze::UNDER_SHIFT)) == 0)
59
+ if @maze[nx, ny] == 0
60
+ return direction
61
+ elsif !@maze.dead?(@maze[nx, ny]) && @maze.weave > 0 && rand(100) < @maze.weave
62
+ # see if we can weave over/under the cell at (nx,ny)
63
+ return direction if @maze.weave_allowed?(@x, @y, nx, ny, direction)
64
+ end
65
+ end
66
+
67
+ while @tries.empty?
68
+ if @stack.empty?
69
+ @pending = false
70
+ return nil
71
+ else
72
+ @x, @y, @tries = @stack.pop
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,377 @@
1
+ require 'optparse'
2
+ require 'theseus'
3
+ require 'theseus/formatters/png'
4
+
5
+ require 'theseus/algorithms/recursive_backtracker'
6
+ require 'theseus/algorithms/kruskal'
7
+ require 'theseus/algorithms/prim'
8
+
9
+ module Theseus
10
+ class CLI
11
+ TYPE_MAP = {
12
+ "ortho" => Theseus::OrthogonalMaze,
13
+ "delta" => Theseus::DeltaMaze,
14
+ "sigma" => Theseus::SigmaMaze,
15
+ "upsilon" => Theseus::UpsilonMaze,
16
+ }
17
+
18
+ ALGO_MAP = {
19
+ "backtrack" => Theseus::Algorithms::RecursiveBacktracker,
20
+ "kruskal" => Theseus::Algorithms::Kruskal,
21
+ "prim" => Theseus::Algorithms::Prim,
22
+ }
23
+
24
+ def self.run(*args)
25
+ new(*args).run
26
+ end
27
+
28
+ attr_accessor :animate
29
+ attr_accessor :delay
30
+ attr_accessor :output
31
+ attr_accessor :sparse
32
+ attr_accessor :unicursal
33
+ attr_accessor :type
34
+ attr_accessor :format
35
+ attr_accessor :solution
36
+
37
+ attr_reader :maze_opts
38
+ attr_reader :png_opts
39
+
40
+ def initialize(*args)
41
+ args.flatten!
42
+
43
+ @animate = false
44
+ @delay = 50
45
+ @output = "maze"
46
+ @sparse = 0
47
+ @unicursal = false
48
+ @type = "ortho"
49
+ @format = :ascii
50
+ @solution = nil
51
+
52
+ @png_opts = Theseus::Formatters::PNG::DEFAULTS.dup
53
+ @maze_opts = { mask: nil, width: nil, height: nil,
54
+ randomness: 50, weave: 0, symmetry: :none, braid: 0, wrap: :none,
55
+ entrance: nil, exit: nil, algorithm: ALGO_MAP["backtrack"] }
56
+
57
+ option_parser.parse!(args)
58
+
59
+ if args.any?
60
+ abort "extra arguments detected: #{args.inspect}"
61
+ end
62
+
63
+ normalize_settings!
64
+ end
65
+
66
+ def run
67
+ if @animate
68
+ run_animation
69
+ else
70
+ run_static
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def run_animation
77
+ if format == :ascii
78
+ run_ascii_animation
79
+ else
80
+ run_png_animation
81
+ end
82
+ end
83
+
84
+ def clear_screen
85
+ print "\e[2J"
86
+ end
87
+
88
+ def cursor_home
89
+ print "\e[H"
90
+ end
91
+
92
+ def show_maze
93
+ cursor_home
94
+ puts @maze.to_s(:mode => :unicode)
95
+ end
96
+
97
+ def run_ascii_animation
98
+ clear_screen
99
+
100
+ @maze.generate! do
101
+ show_maze
102
+ sleep(@delay)
103
+ end
104
+
105
+ show_maze
106
+ end
107
+
108
+ def write_frame(step, options={})
109
+ f = "%s-%04d.png" % [@output, step]
110
+ step += 1
111
+ File.open(f, "w") { |io| io.write(@maze.to(:png, @png_opts.merge(options))) }
112
+ print "."
113
+ end
114
+
115
+ def run_png_animation
116
+ step = 0
117
+ @maze.generate! do
118
+ write_frame(step)
119
+ step += 1
120
+ end
121
+
122
+ write_frame(step)
123
+ step += 1
124
+
125
+ if @solution
126
+ solver = @maze.new_solver(type: @solution)
127
+
128
+ while solver.step
129
+ path = solver.to_path(color: @png_opts[:solution_color])
130
+ write_frame(step, paths: [path])
131
+ step += 1
132
+ end
133
+ end
134
+
135
+ puts
136
+ puts "done, %d frames written to %s-*.png" % [step, @output]
137
+ end
138
+
139
+ def run_static
140
+ @maze.generate!
141
+ @sparse.times { @maze.sparsify! }
142
+
143
+ if @unicursal
144
+ enter_at = @unicursal_entrance || [-1,0]
145
+ if enter_at[0] > 0 && enter_at[0] < width*2
146
+ exit_at = [enter_at[0]+1, enter_at[1]]
147
+ else
148
+ exit_at = [enter_at[0], enter_at[1]+1]
149
+ end
150
+ @maze = @maze.to_unicursal(entrance: enter_at, exit: exit_at)
151
+ end
152
+
153
+ if @format == :ascii
154
+ puts @maze.to_s(:mode => :unicode)
155
+ else
156
+ @png_opts[:solution] = @solution
157
+ File.open(@output + ".png", "w") { |io| io.write(@maze.to(:png, @png_opts)) }
158
+ puts "maze written to #{@output}.png"
159
+ end
160
+ end
161
+
162
+ def normalize_settings!
163
+ # default width to height, and vice-versa
164
+ @maze_opts[:width] ||= @maze_opts[:height]
165
+ @maze_opts[:height] ||= @maze_opts[:width]
166
+
167
+ if @maze_opts[:mask].nil? && (@maze_opts[:width].nil? || @maze_opts[:height].nil?)
168
+ warn "You must specify either a mask (-m) or the maze dimensions(-w or -H)."
169
+ abort "Try --help for a full list of options."
170
+ end
171
+
172
+ if @animate
173
+ abort "sparse cannot be used for animated mazes" if @sparse > 0
174
+ abort "cannot animate unicursal mazes" if @unicursal
175
+
176
+ if @format != :ascii
177
+ @png_opts[:background] = ChunkyPNG::Color.from_hex(@png_opts[:background]) unless Fixnum === @png_opts[:background]
178
+
179
+ if @png_opts[:background] & 0xFF != 0xFF
180
+ warn "if you intend to make a movie out of the frames from the animation,"
181
+ warn "it is HIGHLY RECOMMENDED that you use a fully opaque background color."
182
+ end
183
+ end
184
+
185
+ # convert delay to a fraction of a second
186
+ @delay = @delay / 1000.0
187
+ end
188
+
189
+ if @solution
190
+ abort "cannot display solution in ascii mode" if @format == :ascii
191
+ end
192
+
193
+ if @unicursal
194
+ @unicursal_entrance = @maze_opts.delete(:entrance)
195
+ @maze_opts[:entrance] = [0,0]
196
+ @maze_opts[:exit] = [0,0]
197
+ end
198
+
199
+ @maze_opts[:mask] ||= Theseus::TransparentMask.new(@maze_opts[:width], @maze_opts[:height])
200
+ @maze_opts[:width] ||= @maze_opts[:mask].width
201
+ @maze_opts[:height] ||= @maze_opts[:mask].height
202
+ @maze = TYPE_MAP[@type].new(@maze_opts)
203
+
204
+ if @unicursal && !@maze.respond_to?(:to_unicursal)
205
+ abort "#{@type} mazes do not support the -u (unicursal) option"
206
+ end
207
+ end
208
+
209
+ def option_parser
210
+ OptionParser.new do |opts|
211
+ setup_required_options(opts)
212
+ setup_output_options(opts)
213
+ setup_maze_options(opts)
214
+ setup_formatting_options(opts)
215
+ setup_misc_options(opts)
216
+ end
217
+ end
218
+
219
+ def setup_required_options(opts)
220
+ opts.separator ""
221
+ opts.separator "Required options:"
222
+
223
+ opts.on("-w", "--width N", Integer, "width of the maze (default 20, or mask width)") do |w|
224
+ @maze_opts[:width] = w
225
+ end
226
+
227
+ opts.on("-H", "--height N", Integer, "height of the maze (default 20 or mask height)") do |h|
228
+ @maze_opts[:height] = h
229
+ end
230
+
231
+ opts.on("-m", "--mask FILE", "png file to use as mask") do |m|
232
+ case m
233
+ when /^triangle:(\d+)$/ then @maze_opts[:mask] = Theseus::TriangleMask.new($1.to_i)
234
+ else @maze_opts[:mask] = Theseus::Mask.from_png(m)
235
+ end
236
+ end
237
+ end
238
+
239
+ def setup_output_options(opts)
240
+ opts.separator ""
241
+ opts.separator "Output options:"
242
+
243
+ opts.on("-a", "--[no-]animate", "emit frames for each step") do |v|
244
+ @animate = v
245
+ end
246
+
247
+ opts.on("-D", "--delay N", Integer, "time to wait between animation frames, in ms, default is #{@delay}") do |d|
248
+ @delay = d
249
+ end
250
+
251
+ opts.on("-o", "--output FILE", "where to save the file(s) (for png only)") do |f|
252
+ @output = f
253
+ end
254
+
255
+ opts.on("-f", "--format FMT", "png, ascii (default #{@format})") do |f|
256
+ @format = f.to_sym
257
+ end
258
+
259
+ opts.on("-V", "--solve [METHOD]", "whether to display the solution of the maze.", "METHOD is either `backtracker' (the default) or `astar'") do |s|
260
+ @solution = (s || :backtracker).to_sym
261
+ end
262
+ end
263
+
264
+ def setup_maze_options(opts)
265
+ opts.separator ""
266
+ opts.separator "Maze options:"
267
+
268
+ opts.on("-s", "--seed N", Integer, "random seed to use") do |s|
269
+ srand(s)
270
+ end
271
+
272
+ opts.on("-A", "--algorithm NAME", "the algorithm to use to generate the maze.",
273
+ "may be any of #{ALGO_MAP.keys.sort.join(",")}.",
274
+ "defaults to `backtrack'.") do |a|
275
+ @maze_opts[:algorithm] = ALGO_MAP[a] or abort "unknown algorithm `#{a}'"
276
+ end
277
+
278
+ opts.on("-t", "--type TYPE", "#{TYPE_MAP.keys.sort.join(",")} (default: #{@type})") do |t|
279
+ @type = t
280
+ end
281
+
282
+ opts.on("-u", "--[no-]unicursal", "generate a unicursal maze (results in 2x size)") do |u|
283
+ @unicursal = u
284
+ end
285
+
286
+ opts.on("-y", "--symmetry TYPE", "one of none,x,y,xy,radial (default is '#{@maze_opts[:symmetry]}')") do |s|
287
+ @maze_opts[:symmetry] = s.to_sym
288
+ end
289
+
290
+ opts.on("-e", "--weave N", Integer, "0-100, chance of a passage to go over/under another (default #{@maze_opts[:weave]})") do |v|
291
+ @maze_opts[:weave] = v
292
+ end
293
+
294
+ opts.on("-r", "--random N", Integer, "0-100, randomness of maze (default #{@maze_opts[:randomness]})") do |r|
295
+ @maze_opts[:randomness] = r
296
+ end
297
+
298
+ opts.on("-S", "--sparse N", Integer, "how sparse to make the maze (default #{@sparse})") do |s|
299
+ @sparse = s
300
+ end
301
+
302
+ opts.on("-d", "--braid N", Integer, "0-100, percentage of deadends to remove (default #{maze_opts[:braid]})") do |b|
303
+ @maze_opts[:braid] = b
304
+ end
305
+
306
+ opts.on("-R", "--wrap axis", "none,x,y,xy (default #{@maze_opts[:wrap]})") do |w|
307
+ @maze_opts[:wrap] = w.to_sym
308
+ end
309
+
310
+ opts.on("-E", "--enter [X,Y]", "the entrance of the maze (default -1,0)") do |s|
311
+ @maze_opts[:entrance] = s.split(/,/).map { |v| v.to_i }
312
+ end
313
+
314
+ opts.on("-X", "--exit [X,Y]", "the exit of the maze (default width,height-1)") do |s|
315
+ @maze_opts[:exit] = s.split(/,/).map { |v| v.to_i }
316
+ end
317
+ end
318
+
319
+ def setup_formatting_options(opts)
320
+ opts.separator ""
321
+ opts.separator "Formatting options:"
322
+
323
+ opts.on("-B", "--background COLOR", "rgba hex background color for maze (default %08X)" % @png_opts[:background]) do |c|
324
+ @png_opts[:background] = c
325
+ end
326
+
327
+ opts.on("-C", "--cellcolor COLOR", "rgba hex cell color for maze (default %08X)" % @png_opts[:cell_color]) do |c|
328
+ @png_opts[:cell_color] = c
329
+ end
330
+
331
+ opts.on("-L", "--wallcolor COLOR", "rgba hex wall color for maze (default %08X)" % @png_opts[:wall_color]) do |c|
332
+ @png_opts[:wall_color] = c
333
+ end
334
+
335
+ opts.on("-U", "--solutioncolor COLOR", "rgba hex color for the answer path (default %08X)" % @png_opts[:solution_color]) do |c|
336
+ @png_opts[:solution_color] = c
337
+ end
338
+
339
+ opts.on("-c", "--cell N", Integer, "size of each cell (default #{@png_opts[:cell_size]})") do |c|
340
+ @png_opts[:cell_size] = c
341
+ end
342
+
343
+ opts.on("-b", "--border N", Integer, "border padding around outside (default #{@png_opts[:outer_padding]})") do |c|
344
+ @png_opts[:outer_padding] = c
345
+ end
346
+
347
+ opts.on("-p", "--padding N", Integer, "padding around cell (default #{@png_opts[:cell_padding]})") do |c|
348
+ @png_opts[:cell_padding] = c
349
+ end
350
+
351
+ opts.on("-W", "--wall N", Integer, "thickness of walls (default #{@png_opts[:wall_width]})") do |c|
352
+ @png_opts[:wall_width] = c
353
+ end
354
+ end
355
+
356
+ def setup_misc_options(opts)
357
+ opts.separator ""
358
+ opts.separator "Other options:"
359
+
360
+ opts.on_tail("-v", "--version", "display the Theseus version and exit") do
361
+ maze = Theseus::OrthogonalMaze.generate(width: 20, height: 4)
362
+ s = maze.to_s(mode: :lines).strip
363
+ print s.gsub(/^/, " ").sub(/^\s*/, "theseus --")
364
+
365
+ require 'theseus/version'
366
+ puts "--> v#{Theseus::Version::STRING}"
367
+ puts "a maze generator, renderer, and solver by Jamis Buck <jamis@jamisbuck.org>"
368
+ exit
369
+ end
370
+
371
+ opts.on_tail("-h", "--help", "this helpful list of options") do
372
+ puts opts
373
+ exit
374
+ end
375
+ end
376
+ end
377
+ end