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 +137 -0
- data/Rakefile +42 -0
- data/bin/theseus +262 -0
- data/examples/a-star-search.rb +106 -0
- data/lib/theseus.rb +6 -0
- data/lib/theseus/delta_maze.rb +45 -0
- data/lib/theseus/formatters/ascii.rb +41 -0
- data/lib/theseus/formatters/ascii/delta.rb +79 -0
- data/lib/theseus/formatters/ascii/orthogonal.rb +156 -0
- data/lib/theseus/formatters/ascii/sigma.rb +57 -0
- data/lib/theseus/formatters/ascii/upsilon.rb +67 -0
- data/lib/theseus/formatters/png.rb +183 -0
- data/lib/theseus/formatters/png/delta.rb +85 -0
- data/lib/theseus/formatters/png/orthogonal.rb +87 -0
- data/lib/theseus/formatters/png/sigma.rb +105 -0
- data/lib/theseus/formatters/png/upsilon.rb +137 -0
- data/lib/theseus/mask.rb +113 -0
- data/lib/theseus/maze.rb +855 -0
- data/lib/theseus/orthogonal_maze.rb +195 -0
- data/lib/theseus/path.rb +91 -0
- data/lib/theseus/sigma_maze.rb +107 -0
- data/lib/theseus/solvers/astar.rb +144 -0
- data/lib/theseus/solvers/backtracker.rb +79 -0
- data/lib/theseus/solvers/base.rb +95 -0
- data/lib/theseus/upsilon_maze.rb +37 -0
- data/lib/theseus/version.rb +10 -0
- data/test/maze_test.rb +193 -0
- metadata +104 -0
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
|