Skip to content
Browse files

refactor the cli so it can be maintained more easily

  • Loading branch information...
1 parent 47f6f1e commit 9d1f88b863fd68f55372ea26a68b0cb0efe75151 @jamis committed
Showing with 378 additions and 286 deletions.
  1. +2 −286 bin/theseus
  2. +376 −0 lib/theseus/cli.rb
View
288 bin/theseus
@@ -1,289 +1,5 @@
#!/usr/bin/env ruby
-require 'optparse'
-require 'theseus'
-require 'theseus/formatters/png'
+require 'theseus/cli'
-require 'theseus/algorithms/recursive_backtracker'
-require 'theseus/algorithms/kruskal'
-require 'theseus/algorithms/prim'
-
-type_map = {
- "ortho" => Theseus::OrthogonalMaze,
- "delta" => Theseus::DeltaMaze,
- "sigma" => Theseus::SigmaMaze,
- "upsilon" => Theseus::UpsilonMaze
-}
-
-algo_map = {
- "backtrack" => Theseus::Algorithms::RecursiveBacktracker,
- "kruskal" => Theseus::Algorithms::Kruskal,
- "prim" => Theseus::Algorithms::Prim,
-}
-
-animate = false
-delay = 50
-output = "maze"
-sparse = 0
-unicursal = false
-type = "ortho"
-format = :ascii
-
-png_opts = Theseus::Formatters::PNG::DEFAULTS.dup
-maze_opts = { mask: nil, width: nil, height: nil,
- randomness: 50, weave: 0, symmetry: :none, braid: 0, wrap: :none,
- entrance: nil, exit: nil, algorithm: algo_map["backtrack"] }
-
-OptionParser.new do |opts|
- opts.separator ""
- opts.separator "Required options:"
-
- opts.on("-w", "--width N", Integer, "width of the maze (default 20, or mask width)") do |w|
- maze_opts[:width] = w
- end
-
- opts.on("-H", "--height N", Integer, "height of the maze (default 20 or mask height)") do |h|
- maze_opts[:height] = h
- end
-
- opts.on("-m", "--mask FILE", "png file to use as mask") do |m|
- case m
- when /^triangle:(\d+)$/ then maze_opts[:mask] = Theseus::TriangleMask.new($1.to_i)
- else maze_opts[:mask] = Theseus::Mask.from_png(m)
- end
- end
-
- opts.separator ""
- opts.separator "Output options:"
-
- opts.on("-a", "--[no-]animate", "emit frames for each step") do |v|
- animate = v
- end
-
- opts.on("-D", "--delay N", Integer, "time to wait between animation frames, in ms, default is #{delay}") do |d|
- delay = d
- end
-
- opts.on("-o", "--output FILE", "where to save the file(s) (for png only)") do |f|
- output = f
- end
-
- opts.on("-f", "--format FMT", "png, ascii (default #{format})") do |f|
- format = f.to_sym
- end
-
- opts.on("-V", "--solve [METHOD]", "whether to display the solution of the maze.", "METHOD is either `backtracker' (the default) or `astar'") do |s|
- png_opts[:solution] = (s || :backtracker).to_sym
- end
-
- opts.separator ""
- opts.separator "Maze options:"
-
- opts.on("-s", "--seed N", Integer, "random seed to use") do |s|
- srand(s)
- end
-
- opts.on("-A", "--algorithm NAME", "the algorithm to use to generate the maze.",
- "may be any of #{algo_map.keys.sort.join(",")}.",
- "defaults to `backtrack'.") do |a|
- maze_opts[:algorithm] = algo_map[a] or abort "unknown algorithm `#{a}'"
- end
-
- opts.on("-t", "--type TYPE", "#{type_map.keys.sort.join(",")} (default: #{type})") do |t|
- type = t
- end
-
- opts.on("-u", "--[no-]unicursal", "generate a unicursal maze (results in 2x size)") do |u|
- unicursal = u
- end
-
- opts.on("-y", "--symmetry TYPE", "one of none,x,y,xy,radial (default is '#{maze_opts[:symmetry]}')") do |s|
- maze_opts[:symmetry] = s.to_sym
- end
-
- opts.on("-e", "--weave N", Integer, "0-100, chance of a passage to go over/under another (default #{maze_opts[:weave]})") do |v|
- maze_opts[:weave] = v
- end
-
- opts.on("-r", "--random N", Integer, "0-100, randomness of maze (default #{maze_opts[:randomness]})") do |r|
- maze_opts[:randomness] = r
- end
-
- opts.on("-S", "--sparse N", Integer, "how sparse to make the maze (default #{sparse})") do |s|
- sparse = s
- end
-
- opts.on("-d", "--braid N", Integer, "0-100, percentage of deadends to remove (default #{maze_opts[:braid]})") do |b|
- maze_opts[:braid] = b
- end
-
- opts.on("-R", "--wrap axis", "none,x,y,xy (default #{maze_opts[:wrap]})") do |w|
- maze_opts[:wrap] = w.to_sym
- end
-
- opts.on("-E", "--enter [X,Y]", "the entrance of the maze (default -1,0)") do |s|
- maze_opts[:entrance] = s.split(/,/).map { |v| v.to_i }
- end
-
- opts.on("-X", "--exit [X,Y]", "the exit of the maze (default width,height-1)") do |s|
- maze_opts[:exit] = s.split(/,/).map { |v| v.to_i }
- end
-
- opts.separator ""
- opts.separator "Formatting options:"
-
- opts.on("-B", "--background COLOR", "rgba hex background color for maze (default %08X)" % png_opts[:background]) do |c|
- png_opts[:background] = c
- end
-
- opts.on("-C", "--cellcolor COLOR", "rgba hex cell color for maze (default %08X)" % png_opts[:cell_color]) do |c|
- png_opts[:cell_color] = c
- end
-
- opts.on("-L", "--wallcolor COLOR", "rgba hex wall color for maze (default %08X)" % png_opts[:wall_color]) do |c|
- png_opts[:wall_color] = c
- end
-
- opts.on("-U", "--solutioncolor COLOR", "rgba hex color for the answer path (default %08X)" % png_opts[:solution_color]) do |c|
- png_opts[:solution_color] = c
- end
-
- opts.on("-c", "--cell N", Integer, "size of each cell (default #{png_opts[:cell_size]})") do |c|
- png_opts[:cell_size] = c
- end
-
- opts.on("-b", "--border N", Integer, "border padding around outside (default #{png_opts[:outer_padding]})") do |c|
- png_opts[:outer_padding] = c
- end
-
- opts.on("-p", "--padding N", Integer, "padding around cell (default #{png_opts[:cell_padding]})") do |c|
- png_opts[:cell_padding] = c
- end
-
- opts.on("-W", "--wall N", Integer, "thickness of walls (default #{png_opts[:wall_width]})") do |c|
- png_opts[:wall_width] = c
- end
-
- opts.separator ""
- opts.separator "Other options:"
-
- opts.on_tail("-v", "--version", "display the Theseus version and exit") do
- maze = Theseus::OrthogonalMaze.generate(width: 20, height: 4)
- s = maze.to_s(mode: :lines).strip
- print s.gsub(/^/, " ").sub(/^\s*/, "theseus --")
-
- require 'theseus/version'
- puts "--> v#{Theseus::Version::STRING}"
- puts "a maze generator, renderer, and solver by Jamis Buck <jamis@jamisbuck.org>"
- exit
- end
-
- opts.on_tail("-h", "--help", "this helpful list of options") do
- puts opts
- exit
- end
-end.parse!
-
-# default width to height, and vice-versa
-maze_opts[:width] ||= maze_opts[:height]
-maze_opts[:height] ||= maze_opts[:width]
-
-if maze_opts[:mask].nil? && (maze_opts[:width].nil? || maze_opts[:height].nil?)
- warn "You must specify either a mask (-m) or the maze dimensions(-w or -H)."
- abort "Try --help for a full list of options."
-end
-
-if animate
- abort "sparse cannot be used for animated mazes" if sparse > 0
- abort "cannot animate unicursal mazes" if unicursal
-
- png_opts[:background] = ChunkyPNG::Color.from_hex(png_opts[:background]) unless Fixnum === png_opts[:background]
- solution = png_opts.delete(:solution)
-
- if png_opts[:background] & 0xFF != 0xFF
- warn "if you intend to make a movie out of the frames from the animation,"
- warn "it is HIGHLY RECOMMENDED that you use a fully opaque background color."
- end
-
- # convert delay to a fraction of a second
- delay = delay / 1000.0
-end
-
-if unicursal
- unicursal_entrance = maze_opts.delete(:entrance)
- maze_opts[:entrance] = [0,0]
- maze_opts[:exit] = [0,0]
-end
-
-maze_opts[:mask] ||= Theseus::TransparentMask.new(maze_opts[:width], maze_opts[:height])
-maze_opts[:width] ||= maze_opts[:mask].width
-maze_opts[:height] ||= maze_opts[:mask].height
-maze = type_map[type].new(maze_opts)
-
-if unicursal && !maze.respond_to?(:to_unicursal)
- abort "#{type} mazes do not support the -u (unicursal) option"
-end
-
-if animate
- # clear the screen before animating in ASCII mode
- print "\e[2J" if format == :ascii
-
- step = 0
- maze.generate! do
- if format == :ascii
- print "\e[H" # move to upper-left
- puts maze.to_s(:mode => :unicode)
- sleep(delay)
- else
- f = "%s-%04d.png" % [output, step]
- step += 1
- File.open(f, "w") { |io| io.write(maze.to(:png, png_opts)) }
- print "."
- end
- end
-
- if format == :ascii
- print "\e[H" # move to upper-left
- puts maze.to_s(:mode => :unicode)
- else
- f = "%s-%04d.png" % [output, step]
- File.open(f, "w") { |io| io.write(maze.to(:png, png_opts)) }
- print "."
-
- if solution
- solver = maze.new_solver(type: solution)
-
- while solver.step
- path = solver.to_path(color: png_opts[:solution_color])
-
- step += 1
- f = "%s-%04d.png" % [output, step]
- File.open(f, "w") { |io| io.write(maze.to(:png, png_opts.merge(paths: [path]))) }
- print "."
- end
- end
-
- puts
- puts "done, %d frames written to %s-*.png" % [step+1, output]
- end
-else
- maze.generate!
- sparse.times { maze.sparsify! }
-
- if unicursal
- enter_at = unicursal_entrance || [-1,0]
- if enter_at[0] > 0 && enter_at[0] < width*2
- exit_at = [enter_at[0]+1, enter_at[1]]
- else
- exit_at = [enter_at[0], enter_at[1]+1]
- end
- maze = maze.to_unicursal(entrance: enter_at, exit: exit_at)
- end
-
- if format == :ascii
- puts maze.to_s(:mode => :unicode)
- else
- File.open(output + ".png", "w") { |io| io.write(maze.to(:png, png_opts)) }
- puts "maze written to #{output}.png"
- end
-end
+Theseus::CLI.run(ARGV)
View
376 lib/theseus/cli.rb
@@ -0,0 +1,376 @@
+require 'optparse'
+require 'theseus'
+require 'theseus/formatters/png'
+
+require 'theseus/algorithms/recursive_backtracker'
+require 'theseus/algorithms/kruskal'
+require 'theseus/algorithms/prim'
+
+module Theseus
+ class CLI
+ TYPE_MAP = {
+ "ortho" => Theseus::OrthogonalMaze,
+ "delta" => Theseus::DeltaMaze,
+ "sigma" => Theseus::SigmaMaze,
+ "upsilon" => Theseus::UpsilonMaze,
+ }
+
+ ALGO_MAP = {
+ "backtrack" => Theseus::Algorithms::RecursiveBacktracker,
+ "kruskal" => Theseus::Algorithms::Kruskal,
+ "prim" => Theseus::Algorithms::Prim,
+ }
+
+ def self.run(*args)
+ new(*args).run
+ end
+
+ attr_accessor :animate
+ attr_accessor :delay
+ attr_accessor :output
+ attr_accessor :sparse
+ attr_accessor :unicursal
+ attr_accessor :type
+ attr_accessor :format
+ attr_accessor :solution
+
+ attr_reader :maze_opts
+ attr_reader :png_opts
+
+ def initialize(*args)
+ args.flatten!
+
+ @animate = false
+ @delay = 50
+ @output = "maze"
+ @sparse = 0
+ @unicursal = false
+ @type = "ortho"
+ @format = :ascii
+ @solution = nil
+
+ @png_opts = Theseus::Formatters::PNG::DEFAULTS.dup
+ @maze_opts = { mask: nil, width: nil, height: nil,
+ randomness: 50, weave: 0, symmetry: :none, braid: 0, wrap: :none,
+ entrance: nil, exit: nil, algorithm: ALGO_MAP["backtrack"] }
+
+ option_parser.parse!(args)
+
+ if args.any?
+ abort "extra arguments detected: #{args.inspect}"
+ end
+
+ normalize_settings!
+ end
+
+ def run
+ if @animate
+ run_animation
+ else
+ run_static
+ end
+ end
+
+ private
+
+ def run_animation
+ if format == :ascii
+ run_ascii_animation
+ else
+ run_png_animation
+ end
+ end
+
+ def clear_screen
+ print "\e[2J"
+ end
+
+ def cursor_home
+ print "\e[H"
+ end
+
+ def show_maze
+ cursor_home
+ puts @maze.to_s(:mode => :unicode)
+ end
+
+ def run_ascii_animation
+ clear_screen
+
+ @maze.generate! do
+ show_maze
+ sleep(@delay)
+ end
+
+ show_maze
+ end
+
+ def write_frame(step, options={})
+ f = "%s-%04d.png" % [@output, step]
+ step += 1
+ File.open(f, "w") { |io| io.write(@maze.to(:png, @png_opts.merge(options))) }
+ print "."
+ end
+
+ def run_png_animation
+ step = 0
+ @maze.generate! do
+ write_frame(step)
+ step += 1
+ end
+
+ write_frame(step)
+ step += 1
+
+ if @solution
+ solver = @maze.new_solver(type: @solution)
+
+ while solver.step
+ path = solver.to_path(color: @png_opts[:solution_color])
+ write_frame(step, paths: [path])
+ step += 1
+ end
+ end
+
+ puts
+ puts "done, %d frames written to %s-*.png" % [step, @output]
+ end
+
+ def run_static
+ @maze.generate!
+ @sparse.times { @maze.sparsify! }
+
+ if @unicursal
+ enter_at = @unicursal_entrance || [-1,0]
+ if enter_at[0] > 0 && enter_at[0] < width*2
+ exit_at = [enter_at[0]+1, enter_at[1]]
+ else
+ exit_at = [enter_at[0], enter_at[1]+1]
+ end
+ @maze = @maze.to_unicursal(entrance: enter_at, exit: exit_at)
+ end
+
+ if @format == :ascii
+ puts @maze.to_s(:mode => :unicode)
+ else
+ File.open(@output + ".png", "w") { |io| io.write(@maze.to(:png, @png_opts)) }
+ puts "maze written to #{@output}.png"
+ end
+ end
+
+ def normalize_settings!
+ # default width to height, and vice-versa
+ @maze_opts[:width] ||= @maze_opts[:height]
+ @maze_opts[:height] ||= @maze_opts[:width]
+
+ if @maze_opts[:mask].nil? && (@maze_opts[:width].nil? || @maze_opts[:height].nil?)
+ warn "You must specify either a mask (-m) or the maze dimensions(-w or -H)."
+ abort "Try --help for a full list of options."
+ end
+
+ if @animate
+ abort "sparse cannot be used for animated mazes" if @sparse > 0
+ abort "cannot animate unicursal mazes" if @unicursal
+
+ if @format != :ascii
+ @png_opts[:background] = ChunkyPNG::Color.from_hex(@png_opts[:background]) unless Fixnum === @png_opts[:background]
+
+ if @png_opts[:background] & 0xFF != 0xFF
+ warn "if you intend to make a movie out of the frames from the animation,"
+ warn "it is HIGHLY RECOMMENDED that you use a fully opaque background color."
+ end
+ end
+
+ # convert delay to a fraction of a second
+ @delay = @delay / 1000.0
+ end
+
+ if @solution
+ abort "cannot display solution in ascii mode" if @format == :ascii
+ end
+
+ if @unicursal
+ @unicursal_entrance = @maze_opts.delete(:entrance)
+ @maze_opts[:entrance] = [0,0]
+ @maze_opts[:exit] = [0,0]
+ end
+
+ @maze_opts[:mask] ||= Theseus::TransparentMask.new(@maze_opts[:width], @maze_opts[:height])
+ @maze_opts[:width] ||= @maze_opts[:mask].width
+ @maze_opts[:height] ||= @maze_opts[:mask].height
+ @maze = TYPE_MAP[@type].new(@maze_opts)
+
+ if @unicursal && !@maze.respond_to?(:to_unicursal)
+ abort "#{@type} mazes do not support the -u (unicursal) option"
+ end
+ end
+
+ def option_parser
+ OptionParser.new do |opts|
+ setup_required_options(opts)
+ setup_output_options(opts)
+ setup_maze_options(opts)
+ setup_formatting_options(opts)
+ setup_misc_options(opts)
+ end
+ end
+
+ def setup_required_options(opts)
+ opts.separator ""
+ opts.separator "Required options:"
+
+ opts.on("-w", "--width N", Integer, "width of the maze (default 20, or mask width)") do |w|
+ @maze_opts[:width] = w
+ end
+
+ opts.on("-H", "--height N", Integer, "height of the maze (default 20 or mask height)") do |h|
+ @maze_opts[:height] = h
+ end
+
+ opts.on("-m", "--mask FILE", "png file to use as mask") do |m|
+ case m
+ when /^triangle:(\d+)$/ then @maze_opts[:mask] = Theseus::TriangleMask.new($1.to_i)
+ else @maze_opts[:mask] = Theseus::Mask.from_png(m)
+ end
+ end
+ end
+
+ def setup_output_options(opts)
+ opts.separator ""
+ opts.separator "Output options:"
+
+ opts.on("-a", "--[no-]animate", "emit frames for each step") do |v|
+ @animate = v
+ end
+
+ opts.on("-D", "--delay N", Integer, "time to wait between animation frames, in ms, default is #{@delay}") do |d|
+ @delay = d
+ end
+
+ opts.on("-o", "--output FILE", "where to save the file(s) (for png only)") do |f|
+ @output = f
+ end
+
+ opts.on("-f", "--format FMT", "png, ascii (default #{@format})") do |f|
+ @format = f.to_sym
+ end
+
+ opts.on("-V", "--solve [METHOD]", "whether to display the solution of the maze.", "METHOD is either `backtracker' (the default) or `astar'") do |s|
+ @solution = (s || :backtracker).to_sym
+ end
+ end
+
+ def setup_maze_options(opts)
+ opts.separator ""
+ opts.separator "Maze options:"
+
+ opts.on("-s", "--seed N", Integer, "random seed to use") do |s|
+ srand(s)
+ end
+
+ opts.on("-A", "--algorithm NAME", "the algorithm to use to generate the maze.",
+ "may be any of #{ALGO_MAP.keys.sort.join(",")}.",
+ "defaults to `backtrack'.") do |a|
+ @maze_opts[:algorithm] = ALGO_MAP[a] or abort "unknown algorithm `#{a}'"
+ end
+
+ opts.on("-t", "--type TYPE", "#{TYPE_MAP.keys.sort.join(",")} (default: #{@type})") do |t|
+ @type = t
+ end
+
+ opts.on("-u", "--[no-]unicursal", "generate a unicursal maze (results in 2x size)") do |u|
+ @unicursal = u
+ end
+
+ opts.on("-y", "--symmetry TYPE", "one of none,x,y,xy,radial (default is '#{@maze_opts[:symmetry]}')") do |s|
+ @maze_opts[:symmetry] = s.to_sym
+ end
+
+ opts.on("-e", "--weave N", Integer, "0-100, chance of a passage to go over/under another (default #{@maze_opts[:weave]})") do |v|
+ @maze_opts[:weave] = v
+ end
+
+ opts.on("-r", "--random N", Integer, "0-100, randomness of maze (default #{@maze_opts[:randomness]})") do |r|
+ @maze_opts[:randomness] = r
+ end
+
+ opts.on("-S", "--sparse N", Integer, "how sparse to make the maze (default #{@sparse})") do |s|
+ @sparse = s
+ end
+
+ opts.on("-d", "--braid N", Integer, "0-100, percentage of deadends to remove (default #{maze_opts[:braid]})") do |b|
+ @maze_opts[:braid] = b
+ end
+
+ opts.on("-R", "--wrap axis", "none,x,y,xy (default #{@maze_opts[:wrap]})") do |w|
+ @maze_opts[:wrap] = w.to_sym
+ end
+
+ opts.on("-E", "--enter [X,Y]", "the entrance of the maze (default -1,0)") do |s|
+ @maze_opts[:entrance] = s.split(/,/).map { |v| v.to_i }
+ end
+
+ opts.on("-X", "--exit [X,Y]", "the exit of the maze (default width,height-1)") do |s|
+ @maze_opts[:exit] = s.split(/,/).map { |v| v.to_i }
+ end
+ end
+
+ def setup_formatting_options(opts)
+ opts.separator ""
+ opts.separator "Formatting options:"
+
+ opts.on("-B", "--background COLOR", "rgba hex background color for maze (default %08X)" % @png_opts[:background]) do |c|
+ @png_opts[:background] = c
+ end
+
+ opts.on("-C", "--cellcolor COLOR", "rgba hex cell color for maze (default %08X)" % @png_opts[:cell_color]) do |c|
+ @png_opts[:cell_color] = c
+ end
+
+ opts.on("-L", "--wallcolor COLOR", "rgba hex wall color for maze (default %08X)" % @png_opts[:wall_color]) do |c|
+ @png_opts[:wall_color] = c
+ end
+
+ opts.on("-U", "--solutioncolor COLOR", "rgba hex color for the answer path (default %08X)" % @png_opts[:solution_color]) do |c|
+ @png_opts[:solution_color] = c
+ end
+
+ opts.on("-c", "--cell N", Integer, "size of each cell (default #{@png_opts[:cell_size]})") do |c|
+ @png_opts[:cell_size] = c
+ end
+
+ opts.on("-b", "--border N", Integer, "border padding around outside (default #{@png_opts[:outer_padding]})") do |c|
+ @png_opts[:outer_padding] = c
+ end
+
+ opts.on("-p", "--padding N", Integer, "padding around cell (default #{@png_opts[:cell_padding]})") do |c|
+ @png_opts[:cell_padding] = c
+ end
+
+ opts.on("-W", "--wall N", Integer, "thickness of walls (default #{@png_opts[:wall_width]})") do |c|
+ @png_opts[:wall_width] = c
+ end
+ end
+
+ def setup_misc_options(opts)
+ opts.separator ""
+ opts.separator "Other options:"
+
+ opts.on_tail("-v", "--version", "display the Theseus version and exit") do
+ maze = Theseus::OrthogonalMaze.generate(width: 20, height: 4)
+ s = maze.to_s(mode: :lines).strip
+ print s.gsub(/^/, " ").sub(/^\s*/, "theseus --")
+
+ require 'theseus/version'
+ puts "--> v#{Theseus::Version::STRING}"
+ puts "a maze generator, renderer, and solver by Jamis Buck <jamis@jamisbuck.org>"
+ exit
+ end
+
+ opts.on_tail("-h", "--help", "this helpful list of options") do
+ puts opts
+ exit
+ end
+ end
+ end
+end

0 comments on commit 9d1f88b

Please sign in to comment.
Something went wrong with that request. Please try again.