class Theseus::CLI

Constants

ALGO_MAP
TYPE_MAP

Attributes

animate[RW]
delay[RW]
format[RW]
maze_opts[R]
output[RW]
png_opts[R]
solution[RW]
sparse[RW]
type[RW]
unicursal[RW]

Public Class Methods

new(*args) click to toggle source
# File lib/theseus/cli.rb, line 40
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
run(*args) click to toggle source
# File lib/theseus/cli.rb, line 24
def self.run(*args)
  new(*args).run
end

Public Instance Methods

run() click to toggle source
# File lib/theseus/cli.rb, line 66
def run
  if @animate
    run_animation
  else
    run_static
  end
end

Private Instance Methods

clear_screen() click to toggle source
# File lib/theseus/cli.rb, line 84
def clear_screen
  print "\e[2J"
end
cursor_home() click to toggle source
# File lib/theseus/cli.rb, line 88
def cursor_home
  print "\e[H"
end
normalize_settings!() click to toggle source
# File lib/theseus/cli.rb, line 162
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
option_parser() click to toggle source
# File lib/theseus/cli.rb, line 209
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
run_animation() click to toggle source
# File lib/theseus/cli.rb, line 76
def run_animation
  if format == :ascii
    run_ascii_animation
  else
    run_png_animation
  end
end
run_ascii_animation() click to toggle source
# File lib/theseus/cli.rb, line 97
def run_ascii_animation
  clear_screen

  @maze.generate! do
    show_maze
    sleep(@delay)
  end

  show_maze
end
run_png_animation() click to toggle source
# File lib/theseus/cli.rb, line 115
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
run_static() click to toggle source
# File lib/theseus/cli.rb, line 139
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
    @png_opts[:solution] = @solution
    File.open(@output + ".png", "w") { |io| io.write(@maze.to(:png, @png_opts)) }
    puts "maze written to #{@output}.png"
  end
end
setup_formatting_options(opts) click to toggle source
# File lib/theseus/cli.rb, line 319
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
setup_maze_options(opts) click to toggle source
# File lib/theseus/cli.rb, line 264
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
setup_misc_options(opts) click to toggle source
# File lib/theseus/cli.rb, line 356
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
setup_output_options(opts) click to toggle source
# File lib/theseus/cli.rb, line 239
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
setup_required_options(opts) click to toggle source
# File lib/theseus/cli.rb, line 219
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
show_maze() click to toggle source
# File lib/theseus/cli.rb, line 92
def show_maze
  cursor_home
  puts @maze.to_s(:mode => :unicode)
end
write_frame(step, options={}) click to toggle source
# File lib/theseus/cli.rb, line 108
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