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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7cab672f9d3d677b4e11d56540a0050cc6f1f26f
4
+ data.tar.gz: 29094f4def583c3371b6c47ce93370012256def8
5
+ SHA512:
6
+ metadata.gz: 731e708c8c4fadd6f0f934bb42ee65aea8b686fb45ecbe437fa4023550ae6693c5d6579806dcf3b37c7949fecb5082023607a0d3ed5138a2dae8d15a020791a0
7
+ data.tar.gz: 307256f22616f13b05e4763657b96fe638af856c2bfa08beba59f5509059270a4837a80494395ee61e9102f4e0b2ce62e42f7271985a7577ec3c2d741276e0e9
data/Rakefile CHANGED
@@ -1,10 +1,7 @@
1
1
  require 'rake'
2
- require 'rake/gempackagetask'
3
- require 'rake/rdoctask'
2
+ require 'rdoc/task'
4
3
  require 'rake/testtask'
5
4
 
6
- require './lib/theseus/version'
7
-
8
5
  task :default => :test
9
6
 
10
7
  Rake::TestTask.new do |t|
@@ -12,26 +9,6 @@ Rake::TestTask.new do |t|
12
9
  t.verbose = true
13
10
  end
14
11
 
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
12
  Rake::RDocTask.new do |rd|
36
13
  rd.main = "README.rdoc"
37
14
  rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
@@ -1,262 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'optparse'
4
- require 'theseus'
5
- require 'theseus/formatters/png'
3
+ require 'theseus/cli'
6
4
 
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 => :unicode)
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 => :unicode)
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 => :unicode)
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
5
+ Theseus::CLI.run(ARGV)
@@ -0,0 +1,31 @@
1
+ module Theseus
2
+ module Algorithms
3
+ # A minimal abstract superclass for maze algorithms to descend
4
+ # from, mostly as a helper to provide some basic, common
5
+ # functionality.
6
+ class Base
7
+ # The maze object that the algorithm will operate on.
8
+ attr_reader :maze
9
+
10
+ # Create a new algorithm object that will operate on the
11
+ # given maze.
12
+ def initialize(maze, options={})
13
+ @maze = maze
14
+ @pending = true
15
+ end
16
+
17
+ # Returns true if the algorithm has not yet completed.
18
+ def pending?
19
+ @pending
20
+ end
21
+
22
+ # Execute a single step of the algorithm. Return true
23
+ # if the algorithm is still pending, or false if it has
24
+ # completed.
25
+ def step
26
+ return false unless pending?
27
+ do_step
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,81 @@
1
+ require 'theseus/algorithms/base'
2
+
3
+ module Theseus
4
+ module Algorithms
5
+ # Kruskal's algorithm is a means of finding a minimum spanning tree for a
6
+ # weighted graph. By changing how edges are selected, it becomes suitable
7
+ # for use as a maze generator.
8
+ #
9
+ # The mazes it generates tend to have a lot of short cul-de-sacs, which
10
+ # on the one hand makes the maze look "spiky", but on the other hand
11
+ # can potentially make the maze harder to solve.
12
+ #
13
+ # This implementation of Kruskal's algorithm does not support weave
14
+ # mazes.
15
+ class Kruskal < Base
16
+ class TreeSet #:nodoc:
17
+ attr_accessor :parent
18
+
19
+ def initialize
20
+ @parent = nil
21
+ end
22
+
23
+ def root
24
+ @parent ? @parent.root : self
25
+ end
26
+
27
+ def connected?(tree)
28
+ root == tree.root
29
+ end
30
+
31
+ def connect(tree)
32
+ tree.root.parent = self
33
+ end
34
+ end
35
+
36
+ def initialize(maze, options={}) #:nodoc:
37
+ super
38
+
39
+ if @maze.weave > 0
40
+ raise ArgumentError, "weave mazes cannot be generated with kruskal's algorithm"
41
+ end
42
+
43
+ @sets = Array.new(@maze.height) { Array.new(@maze.width) { TreeSet.new } }
44
+ @edges = []
45
+
46
+ maze.height.times do |y|
47
+ maze.row_length(y).times do |x|
48
+ next unless @maze.valid?(x, y)
49
+ @maze.potential_exits_at(x, y).each do |dir|
50
+ dx, dy = @maze.dx(dir), @maze.dy(dir)
51
+ if (dx < 0 || dy < 0) && @maze.valid?(x+dx, y+dy)
52
+ weight = rand(100) < @maze.randomness ? 0.5 + rand : 1
53
+ @edges << [x, y, dir, weight]
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ @edges = @edges.sort_by { |e| e.last }
60
+ end
61
+
62
+ def do_step #:nodoc:
63
+ until @edges.empty?
64
+ x, y, direction, _ = @edges.pop
65
+ nx, ny = x + @maze.dx(direction), y + @maze.dy(direction)
66
+
67
+ set1, set2 = @sets[y][x], @sets[ny][nx]
68
+ unless set1.connected?(set2)
69
+ set1.connect(set2)
70
+ @maze.apply_move_at(x, y, direction)
71
+ @maze.apply_move_at(nx, ny, @maze.opposite(direction))
72
+ return true
73
+ end
74
+ end
75
+
76
+ @pending = false
77
+ return false
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,81 @@
1
+ require 'theseus/algorithms/base'
2
+
3
+ module Theseus
4
+ module Algorithms
5
+ class Prim < Base
6
+ IN = 0x10000 # indicate that a cell, though blank, is part of the IN set
7
+ FRONTIER = 0x20000 # indicate that a cell is part of the frontier set
8
+
9
+ def initialize(maze, options={}) #:nodoc:
10
+ super
11
+
12
+ if @maze.weave > 0
13
+ raise ArgumentError, "weave mazes cannot be generated with prim's algorithm"
14
+ end
15
+
16
+ @frontier = []
17
+
18
+ loop do
19
+ y = rand(@maze.height)
20
+ x = rand(@maze.row_length(y))
21
+ next unless @maze.valid?(x, y)
22
+
23
+ mark_cell(x, y)
24
+ break
25
+ end
26
+ end
27
+
28
+ # Iterates over each cell in the frontier space, yielding the coordinates
29
+ # of each one.
30
+ def each_frontier
31
+ @frontier.each do |x, y|
32
+ yield x, y
33
+ end
34
+ end
35
+
36
+ def do_step #:nodoc:
37
+ if rand(100) < @maze.randomness
38
+ x, y = @frontier.delete_at(rand(@frontier.length))
39
+ else
40
+ x, y = @frontier.pop
41
+ end
42
+
43
+ neighbors = find_neighbors_of(x, y)
44
+ direction, nx, ny = neighbors[rand(neighbors.length)]
45
+
46
+ @maze.apply_move_at(x, y, direction)
47
+ @maze.apply_move_at(nx, ny, @maze.opposite(direction))
48
+
49
+ mark_cell(x, y)
50
+
51
+ @pending = @frontier.any?
52
+ end
53
+
54
+ private
55
+
56
+ def mark_cell(x, y) #:nodoc:
57
+ @maze[x, y] |= IN
58
+ @maze[x, y] &= ~FRONTIER
59
+
60
+ @maze.potential_exits_at(x, y).each do |dir|
61
+ nx, ny = x + @maze.dx(dir), y + @maze.dy(dir)
62
+ if @maze.valid?(nx, ny) && @maze[nx, ny] == 0
63
+ @maze[nx, ny] |= FRONTIER
64
+ @frontier << [nx, ny]
65
+ end
66
+ end
67
+ end
68
+
69
+ def find_neighbors_of(x, y) #:nodoc:
70
+ list = []
71
+
72
+ @maze.potential_exits_at(x, y).each do |dir|
73
+ nx, ny = x + @maze.dx(dir), y + @maze.dy(dir)
74
+ list << [dir, nx, ny] if @maze.valid?(nx,ny) && @maze[nx, ny] & IN != 0
75
+ end
76
+
77
+ return list
78
+ end
79
+ end
80
+ end
81
+ end