theseus 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,137 @@
1
+ = Theseus
2
+
3
+ Theseus is a library for generating and solving mazes. It also includes
4
+ routines for rendering mazes (and their solutions) to both ASCII art,
5
+ and to PNG image files.
6
+
7
+ There is also an included utility for generating mazes from the command-line.
8
+
9
+ Note that Theseus requires Ruby 1.9.2 or higher.
10
+
11
+ == Overview
12
+
13
+ Theseus supports the following types of mazes:
14
+
15
+ * *Orthogonal*. This is the traditional maze layout of rectangular passages.
16
+ * *Delta*. This maze type tesselates the field into triangles.
17
+ * *Sigma*. The field is tesselated into hexagons.
18
+ * *Upsilon*. The maze field consists of tiled octogons and squares.
19
+
20
+ Mazes may be generated using any of the following features:
21
+
22
+ * *Symmetry*. The maze may be reflected in x, y, x and y, or radially. (Not
23
+ all maze types support symmetry yet.)
24
+ * *Randomness*. A maze with low randomness will result in many long, straight
25
+ corridors. Higher randomness gives a maze with more twists and turns.
26
+ * *Weave*. Mazes with high weave will frequently pass over or under existing
27
+ passages. Low weave mazes will prefer to remain on the same plane.
28
+ * *Braid*. Mazes with high braid will trade dead-ends for circular loops in
29
+ the maze. Thus, braided mazes will tend to have multiple possible solutions.
30
+ * *Wrap*. Mazes may wrap in x, y, or x and y together. A maze that wraps in
31
+ any of its dimensions will allow the passages to go from one side
32
+ of the maze to the other, by moving beyond the far edge of the
33
+ maze. Another way to think of it is that a maze that wraps in one
34
+ dimension may be mapped onto a cylinder, and a maze that wraps in
35
+ both dimensions may be mapped onto a torus.
36
+ * *Masks*. Mazes may be constrained with masks, which are basically boolean
37
+ grids that define where a passage is allowed to exist. With masks,
38
+ you can create mazes that fit pre-defined geometry, or wrap around text.
39
+
40
+ Theseus supports the following output types:
41
+
42
+ * *ASCII*. Using the ASCII output, you can simply print a maze to the console
43
+ to see what it looks like. Not all features can be displayed well
44
+ in ASCII mode, but it works well enough to see what the maze will be like.
45
+ * *PNG*. Mazes that are rendered to PNG may be highly customized, and even
46
+ allow you to specify custom paths to be rendered.
47
+
48
+ Theseus supports the following solution algorithms:
49
+
50
+ * <b>Recursive Backtracking</b>. This is a fast, efficient algorithm for solving
51
+ mazes that have no circular loops (e.g. unbraided mazes).
52
+ * <b>A* Search</b>. The A* search algorithm really shines with mazes that
53
+ are highly braided, and is guaranteed to provide you with the shortest
54
+ path through the maze.
55
+
56
+ Orthogonal mazes may be converted to their _unicursal_ equivalent. A unicursal
57
+ maze is one which has only a single path that covers every cell in the field
58
+ exactly once. This style is maze is often called a "labyrinth". See
59
+ Theseus::OrthogonalMaze#to_unicursal for more information.
60
+
61
+ Theseus is also designed to allow you to step through both the generation of
62
+ the maze, as well as the computation of the solution. This lets you (for instance)
63
+ animate the construction (and solution) of the maze by drawing individual PNG
64
+ frames for each step! And since Theseus includes an implementation of A* Search,
65
+ this gives you an interesting way to visualize (among other things) how that
66
+ algorithm works.
67
+
68
+ Lastly, Theseus can be used to manually build mazes (or any other grid-based
69
+ structure) by hand. See Theseus::Maze for more information.
70
+
71
+ == Usage
72
+
73
+ Theseus is designed to be super simple to use. Here are some examples:
74
+
75
+ require 'theseus'
76
+
77
+ # generate a 10x10 orthogonal maze and print it to the console
78
+ maze = Theseus::OrthogonalMaze.generate(width: 10)
79
+ puts maze
80
+
81
+ # render a triangular delta maze to a PNG file
82
+ maze = Theseus::DeltaMaze.generate(mask: Theseus::TriangularMask.new(10))
83
+ File.open("triangle.png", "w") { |f| f.write(maze.to(:png)) }
84
+
85
+ # render a highly braided, hexagonally-tiled maze, with its solution
86
+ maze = Theseus::SigmaMaze.generate(width: 20, height: 20, braid: 100)
87
+ File.open("sigma.png", "w") do |f|
88
+ f.write(maze.to(:png, solution: true))
89
+ end
90
+
91
+ # poor-man's animation of the generation of an octogon/square-tiled maze
92
+ maze = Theseus::UpsilonMaze.new(width: 10)
93
+ while maze.step
94
+ puts maze
95
+ sleep 0.05
96
+ end
97
+ puts maze
98
+
99
+ # emit animation frames showing the solution of a maze
100
+ maze = Theseus::OrthogonalMaze.generate(width: 10)
101
+ solver = maze.new_solver
102
+ step = 0
103
+ solver.each do
104
+ File.open("frame-%04d.png" % step, "w") do |f|
105
+ path = solver.to_path(color: 0xff0000ff)
106
+ f.write(maze.to(:png, paths:[path]))
107
+ end
108
+ step += 1
109
+ end
110
+
111
+ See the documentation for Theseus::Maze for information on all of these
112
+ features.
113
+
114
+ To use the command-line utility, simply pass the -h flag to the "theseus"
115
+ utility on the command-line:
116
+
117
+ $ theseus -h
118
+
119
+ == Requirements
120
+
121
+ Theseus requires Ruby 1.9.2, and the ChunkyPNG library.
122
+
123
+ == Installation
124
+
125
+ To install, simply type:
126
+
127
+ gem install theseus
128
+
129
+ This will install both the library, as well as the command-line "theseus"
130
+ tool.
131
+
132
+ == License
133
+
134
+ Theseus is created by Jamis Buck. It is made available in the public domain,
135
+ completely unencumbered by rules, restrictions, or any other nonsense.
136
+
137
+ Please prefer good over evil.
data/Rakefile ADDED
@@ -0,0 +1,42 @@
1
+ require 'rake'
2
+ require 'rake/gempackagetask'
3
+ require 'rake/rdoctask'
4
+ require 'rake/testtask'
5
+
6
+ require './lib/theseus/version'
7
+
8
+ task :default => :test
9
+
10
+ Rake::TestTask.new do |t|
11
+ t.test_files = FileList["test/*.rb"]
12
+ t.verbose = true
13
+ end
14
+
15
+ spec = Gem::Specification.new do |s|
16
+ s.platform = Gem::Platform::RUBY
17
+ s.summary = "Maze generator for Ruby"
18
+ s.name = 'theseus'
19
+ s.version = Theseus::Version::STRING
20
+ s.files = FileList["README.rdoc", "Rakefile", "lib/**/*.rb", "examples/**/*.rb", "bin/*", "test/**/*.rb"].to_a
21
+ s.executables << "theseus"
22
+ s.add_dependency "chunky_png", "~> 0.12.0"
23
+ s.requirements << "Ruby 1.9"
24
+ s.description = "Theseus is a library for building random mazes."
25
+ s.author = "Jamis Buck"
26
+ s.email = "jamis@jamisbuck.org"
27
+ s.homepage = "http://github.com/jamis/theseus"
28
+ end
29
+
30
+ Rake::GemPackageTask.new(spec) do |pkg|
31
+ pkg.need_zip = true
32
+ pkg.need_tar = true
33
+ end
34
+
35
+ Rake::RDocTask.new do |rd|
36
+ rd.main = "README.rdoc"
37
+ rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
38
+ end
39
+
40
+ task :clean do
41
+ rm_rf ["html", "pkg"]
42
+ end
data/bin/theseus ADDED
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'theseus'
5
+ require 'theseus/formatters/png'
6
+
7
+ type_map = {
8
+ "ortho" => Theseus::OrthogonalMaze,
9
+ "delta" => Theseus::DeltaMaze,
10
+ "sigma" => Theseus::SigmaMaze,
11
+ "upsilon" => Theseus::UpsilonMaze
12
+ }
13
+
14
+ animate = false
15
+ output = "maze"
16
+ sparse = 0
17
+ unicursal = false
18
+ type = "ortho"
19
+ format = :ascii
20
+
21
+ png_opts = Theseus::Formatters::PNG::DEFAULTS.dup
22
+ maze_opts = { mask: nil, width: nil, height: nil,
23
+ randomness: 50, weave: 0, symmetry: :none, braid: 0, wrap: :none,
24
+ entrance: nil, exit: nil }
25
+
26
+ OptionParser.new do |opts|
27
+ opts.separator ""
28
+ opts.separator "Required options:"
29
+
30
+ opts.on("-w", "--width N", Integer, "width of the maze (default 20, or mask width)") do |w|
31
+ maze_opts[:width] = w
32
+ end
33
+
34
+ opts.on("-H", "--height N", Integer, "height of the maze (default 20 or mask height)") do |h|
35
+ maze_opts[:height] = h
36
+ end
37
+
38
+ opts.on("-m", "--mask FILE", "png file to use as mask") do |m|
39
+ case m
40
+ when /^triangle:(\d+)$/ then maze_opts[:mask] = Theseus::TriangleMask.new($1.to_i)
41
+ else maze_opts[:mask] = Theseus::Mask.from_png(m)
42
+ end
43
+ end
44
+
45
+ opts.separator ""
46
+ opts.separator "Output options:"
47
+
48
+ opts.on("-a", "--[no-]animate", "emit frames for each step") do |v|
49
+ animate = v
50
+ end
51
+
52
+ opts.on("-o", "--output FILE", "where to save the file(s) (for png only)") do |f|
53
+ output = f
54
+ end
55
+
56
+ opts.on("-f", "--format FMT", "png, ascii (default #{format})") do |f|
57
+ format = f.to_sym
58
+ end
59
+
60
+ opts.on("-V", "--solve [METHOD]", "whether to display the solution of the maze.", "METHOD is either `backtracker' (the default) or `astar'") do |s|
61
+ png_opts[:solution] = (s || :backtracker).to_sym
62
+ end
63
+
64
+ opts.separator ""
65
+ opts.separator "Maze options:"
66
+
67
+ opts.on("-s", "--seed N", Integer, "random seed to use") do |s|
68
+ srand(s)
69
+ end
70
+
71
+ opts.on("-t", "--type TYPE", "#{type_map.keys.sort.join(",")} (default: #{type})") do |t|
72
+ type = t
73
+ end
74
+
75
+ opts.on("-u", "--[no-]unicursal", "generate a unicursal maze (results in 2x size)") do |u|
76
+ unicursal = u
77
+ end
78
+
79
+ opts.on("-y", "--symmetry TYPE", "one of none,x,y,xy,radial (default is '#{maze_opts[:symmetry]}')") do |s|
80
+ maze_opts[:symmetry] = s.to_sym
81
+ end
82
+
83
+ opts.on("-e", "--weave N", Integer, "0-100, chance of a passage to go over/under another (default #{maze_opts[:weave]})") do |v|
84
+ maze_opts[:weave] = v
85
+ end
86
+
87
+ opts.on("-r", "--random N", Integer, "0-100, randomness of maze (default #{maze_opts[:randomness]})") do |r|
88
+ maze_opts[:randomness] = r
89
+ end
90
+
91
+ opts.on("-S", "--sparse N", Integer, "how sparse to make the maze (default #{sparse})") do |s|
92
+ sparse = s
93
+ end
94
+
95
+ opts.on("-d", "--braid N", Integer, "0-100, percentage of deadends to remove (default #{maze_opts[:braid]})") do |b|
96
+ maze_opts[:braid] = b
97
+ end
98
+
99
+ opts.on("-R", "--wrap axis", "none,x,y,xy (default #{maze_opts[:wrap]})") do |w|
100
+ maze_opts[:wrap] = w.to_sym
101
+ end
102
+
103
+ opts.on("-E", "--enter [X,Y]", "the entrance of the maze (default -1,0)") do |s|
104
+ maze_opts[:entrance] = s.split(/,/).map { |v| v.to_i }
105
+ end
106
+
107
+ opts.on("-X", "--exit [X,Y]", "the exit of the maze (default width,height-1)") do |s|
108
+ maze_opts[:exit] = s.split(/,/).map { |v| v.to_i }
109
+ end
110
+
111
+ opts.separator ""
112
+ opts.separator "Formatting options:"
113
+
114
+ opts.on("-B", "--background COLOR", "rgba hex background color for maze (default %08X)" % png_opts[:background]) do |c|
115
+ png_opts[:background] = c
116
+ end
117
+
118
+ opts.on("-C", "--cellcolor COLOR", "rgba hex cell color for maze (default %08X)" % png_opts[:cell_color]) do |c|
119
+ png_opts[:cell_color] = c
120
+ end
121
+
122
+ opts.on("-L", "--wallcolor COLOR", "rgba hex wall color for maze (default %08X)" % png_opts[:wall_color]) do |c|
123
+ png_opts[:wall_color] = c
124
+ end
125
+
126
+ opts.on("-U", "--solutioncolor COLOR", "rgba hex color for the answer path (default %08X)" % png_opts[:solution_color]) do |c|
127
+ png_opts[:solution_color] = c
128
+ end
129
+
130
+ opts.on("-c", "--cell N", Integer, "size of each cell (default #{png_opts[:cell_size]})") do |c|
131
+ png_opts[:cell_size] = c
132
+ end
133
+
134
+ opts.on("-b", "--border N", Integer, "border padding around outside (default #{png_opts[:outer_padding]})") do |c|
135
+ png_opts[:outer_padding] = c
136
+ end
137
+
138
+ opts.on("-p", "--padding N", Integer, "padding around cell (default #{png_opts[:cell_padding]})") do |c|
139
+ png_opts[:cell_padding] = c
140
+ end
141
+
142
+ opts.on("-W", "--wall N", Integer, "thickness of walls (default #{png_opts[:wall_width]})") do |c|
143
+ png_opts[:wall_width] = c
144
+ end
145
+
146
+ opts.separator ""
147
+ opts.separator "Other options:"
148
+
149
+ opts.on_tail("-v", "--version", "display the Theseus version and exit") do
150
+ maze = Theseus::OrthogonalMaze.generate(width: 20, height: 4)
151
+ s = maze.to_s(mode: :lines).strip
152
+ print s.gsub(/^/, " ").sub(/^\s*/, "theseus --")
153
+
154
+ require 'theseus/version'
155
+ puts "--> v#{Theseus::Version::STRING}"
156
+ puts "a maze generator, renderer, and solver by Jamis Buck <jamis@jamisbuck.org>"
157
+ exit
158
+ end
159
+
160
+ opts.on_tail("-h", "--help", "this helpful list of options") do
161
+ puts opts
162
+ exit
163
+ end
164
+ end.parse!
165
+
166
+ # default width to height, and vice-versa
167
+ maze_opts[:width] ||= maze_opts[:height]
168
+ maze_opts[:height] ||= maze_opts[:width]
169
+
170
+ if maze_opts[:mask].nil? && (maze_opts[:width].nil? || maze_opts[:height].nil?)
171
+ warn "You must specify either a mask (-m) or the maze dimensions(-w or -H)."
172
+ abort "Try --help for a full list of options."
173
+ end
174
+
175
+ if animate
176
+ abort "sparse cannot be used for animated mazes" if sparse > 0
177
+ abort "cannot animate unicursal mazes" if unicursal
178
+
179
+ png_opts[:background] = ChunkyPNG::Color.from_hex(png_opts[:background]) unless Fixnum === png_opts[:background]
180
+ solution = png_opts.delete(:solution)
181
+
182
+ if png_opts[:background] & 0xFF != 0xFF
183
+ warn "if you intend to make a movie out of the frames from the animation,"
184
+ warn "it is HIGHLY RECOMMENDED that you use a fully opaque background color."
185
+ end
186
+ end
187
+
188
+ if unicursal
189
+ unicursal_entrance = maze_opts.delete(:entrance)
190
+ maze_opts[:entrance] = [0,0]
191
+ maze_opts[:exit] = [0,0]
192
+ end
193
+
194
+ maze_opts[:mask] ||= Theseus::TransparentMask.new(maze_opts[:width], maze_opts[:height])
195
+ maze_opts[:width] ||= maze_opts[:mask].width
196
+ maze_opts[:height] ||= maze_opts[:mask].height
197
+ maze = type_map[type].new(maze_opts)
198
+
199
+ if unicursal && !maze.respond_to?(:to_unicursal)
200
+ abort "#{type} mazes do not support the -u (unicursal) option"
201
+ end
202
+
203
+ if animate
204
+ step = 0
205
+ maze.generate! do
206
+ if format == :ascii
207
+ system "clear"
208
+ puts maze.to_s(:mode => :utf8_halls)
209
+ sleep 0.05
210
+ else
211
+ f = "%s-%04d.png" % [output, step]
212
+ step += 1
213
+ File.open(f, "w") { |io| io.write(maze.to(:png, png_opts)) }
214
+ print "."
215
+ end
216
+ end
217
+
218
+ if format == :ascii
219
+ system "clear"
220
+ puts maze.to_s(:mode => :utf8_halls)
221
+ else
222
+ f = "%s-%04d.png" % [output, step]
223
+ File.open(f, "w") { |io| io.write(maze.to(:png, png_opts)) }
224
+ print "."
225
+
226
+ if solution
227
+ solver = maze.new_solver(type: solution)
228
+
229
+ while solver.step
230
+ path = solver.to_path(color: png_opts[:solution_color])
231
+
232
+ step += 1
233
+ f = "%s-%04d.png" % [output, step]
234
+ File.open(f, "w") { |io| io.write(maze.to(:png, png_opts.merge(paths: [path]))) }
235
+ print "."
236
+ end
237
+ end
238
+
239
+ puts
240
+ puts "done, %d frames written to %s-*.png" % [step+1, output]
241
+ end
242
+ else
243
+ maze.generate!
244
+ sparse.times { maze.sparsify! }
245
+
246
+ if unicursal
247
+ enter_at = unicursal_entrance || [-1,0]
248
+ if enter_at[0] > 0 && enter_at[0] < width*2
249
+ exit_at = [enter_at[0]+1, enter_at[1]]
250
+ else
251
+ exit_at = [enter_at[0], enter_at[1]+1]
252
+ end
253
+ maze = maze.to_unicursal(entrance: enter_at, exit: exit_at)
254
+ end
255
+
256
+ if format == :ascii
257
+ puts maze.to_s(:mode => :utf8_halls)
258
+ else
259
+ File.open(output + ".png", "w") { |io| io.write(maze.to(:png, png_opts)) }
260
+ puts "maze written to #{output}.png"
261
+ end
262
+ end
@@ -0,0 +1,106 @@
1
+ # Demonstrates the following features of Theseus:
2
+ #
3
+ # * the A* solver algorithm
4
+ # * using Theseus::Path objects to customize the render
5
+ # * stepping through the solution process in order to animate it
6
+ # (by spitting out a new frame for each step)
7
+
8
+ require 'theseus'
9
+
10
+ # use 100% braid, to give a completely multiple-connected maze
11
+ # which will show the A* search to best effect (returning not
12
+ # just any path through the maze, but the SHORTEST path)
13
+ maze = Theseus::OrthogonalMaze.new(width: 15, height: 15, braid: 100)
14
+
15
+ puts "generating the maze..."
16
+ maze.generate!
17
+
18
+ # get a new solver object using the A* search algorithm.
19
+ solver = maze.new_solver(type: :astar)
20
+ puts "solving the maze..."
21
+
22
+ step = 0
23
+ renderings = 0
24
+
25
+ # use a path object to record every attempted route. This is how we'll
26
+ # show "stale" paths that the algorithm determined were ineffecient.
27
+ stale_paths = maze.new_path(color: 0x9f9f9fff)
28
+
29
+ while solver.step
30
+ # the open_set path will show all points in the "open set", the sorted
31
+ # set of points that A* uses to determine where to search next.
32
+ open_set = maze.new_path(color: 0xaaffaaff)
33
+
34
+ # the histories path shows the routes leading up to each point in the
35
+ # open set.
36
+ histories = maze.new_path(color: 0xaaaaffff)
37
+
38
+ # the "best" path is the path that the algorithm currently considers
39
+ # the most promising lead.
40
+ best = maze.new_path(color: 0xffaaaaff)
41
+
42
+ # begin with the first node in the open set
43
+ n = solver.open
44
+
45
+ while n
46
+ # add the point itself to the open_set path
47
+ open_set.set(n.point)
48
+
49
+ # iterate over the node's history and add add the appropriate
50
+ # connections to the "histories" path
51
+ prev = maze.entrance
52
+ n.history.each do |pt|
53
+ how = histories.link(prev, pt)
54
+ histories.set(pt, how)
55
+ prev = pt
56
+ end
57
+ how = histories.link(prev, n.point)
58
+ histories.set(n.point, how)
59
+ n = n.next
60
+ end
61
+
62
+ if solver.open
63
+ prev = maze.entrance
64
+ solver.open.history.each do |pt|
65
+ how = best.link(prev, pt)
66
+ best.set(pt, how)
67
+ prev = pt
68
+ end
69
+ best.link(prev, solver.open.point)
70
+ elsif solver.solved?
71
+ prev = maze.entrance
72
+ solver.solution.each do |pt|
73
+ how = best.link(prev, pt)
74
+ best.set(pt, how)
75
+ prev = pt
76
+ end
77
+ best.link(prev, maze.exit)
78
+ end
79
+
80
+ # add all previously examined histories to the stale paths.
81
+ stale_paths.add_path(histories)
82
+
83
+ # try to keep at least 6 frames animating in the background, to speed
84
+ # things along.
85
+
86
+ while renderings > 6
87
+ Process.wait
88
+ renderings -= 1
89
+ end
90
+
91
+ renderings += 1
92
+
93
+ fork do
94
+ File.open("step-%04d.png" % step, "w" ) do |f|
95
+ f.write(maze.to(:png, cell_size: 20, background: 0x2f2f2fff, paths: [best, open_set, histories, stale_paths]))
96
+ end
97
+ end
98
+
99
+ puts "%d..." % step
100
+ step += 1
101
+ end
102
+
103
+ while renderings > 0
104
+ Process.wait
105
+ renderings -= 1
106
+ end