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