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