theseus 1.0.2 → 1.1.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.
@@ -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