awilliams/RTanque

View on GitHub
bin/rtanque

Summary

Maintainability
Test Coverage
#!/usr/bin/env ruby
require 'rubygems'
require 'thor'
require 'rtanque'
require 'rtanque/runner'
require 'octokit'

class RTanqueCLI < Thor
  include Thor::Actions
  BOT_DIR = 'bots'
  source_root(File.expand_path('../../', __FILE__))

  desc "start <path_to_brain> <path_to_brain>:x3 ...", "Starts match with given bots"
  long_desc <<-LONGDESC
Start an RTanque match. Provide as arguments paths to the brains.
Note that multiple of the same bot can be loaded by appending ':x<int>' to the path. Eg 'bot/my_bot:x2'

There exist a few sample bots to help get started:
\x5 * sample_bots/keyboard (special bot controlled with keyboard: a/s/d/f and arrow keys)
\x5 * sample_bots/seek_and_destroy
\x5 * sample_bots/camper
LONGDESC
  method_option :width, :aliases => '-w', :default => 1200, :type => :numeric, :banner => 'width of window'
  method_option :height, :aliases => '-h', :default => 700, :type => :numeric, :banner => 'height of window'
  method_option :max_ticks, :aliases => '-m', :default => Float::INFINITY, :type => :numeric, :banner => 'max ticks allowed per match'
  method_option :teams, :default => false, :type => :boolean, :banner => 'true to do a team based match based on tank names'
  method_option :gui, :default => true, :type => :boolean, :banner => 'false to run headless'
  method_option :gc, :default => true, :type => :boolean, :banner => 'disable GC (EXPERIMENTAL)'
  method_option :quiet, :aliases => '-q', :default => false, :type => :boolean, :banner => 'disable chatter'
  method_option :seed, :default => Kernel.srand, :type => :numeric, :banner => 'random number seed value'
  def start(*brain_paths)
    Kernel.srand(options[:seed])
    runner = RTanque::Runner.new(options[:width], options[:height], options[:max_ticks], options[:teams])
    brain_paths.each { |brain_path|
      begin
        runner.add_brain_path(brain_path)
      rescue RTanque::Runner::LoadError => e
        say e.message, :red
        exit false
      end
    }
    self.print_start_banner(runner) unless options[:quiet]
    self.set_gc(options[:gc]) { runner.start(options[:gui]) }
    self.print_runner_stats(runner) unless options[:quiet]
  end

  desc "new_bot <bot_name>", "Creates a new bot"
  long_desc <<-LONGDESC
Helper to create a basic brain template in the bots directory
LONGDESC
  def new_bot(bot_name)
    @bot_name = bot_name
    @bot_class_name = Thor::Util.camel_case(bot_name)
    template('templates/bot.erb', "#{BOT_DIR}/#{Thor::Util.snake_case(bot_name)}.rb")
  end

  desc "get_gist <gist_id> <gist_id> ...", "Downloads files from given gist ids into #{BOT_DIR} directory"
  long_desc <<-LONGDESC
Helper to download tanks from github gists for easier sharing. Gists can be both 'secret' and 'public'.
LONGDESC
  method_option :force, :aliases => '-f', :default => false, :type => :boolean, :banner => 'overwrite existing file without prompt'
  def get_gist(*gist_ids)
    gist_ids.each { |gist_id| self.download_gist(gist_id, options) }
  end

  protected

  def print_start_banner(runner)
    self.print_stats{ |table|
      # print options
      options.each { |opts| table << [set_color(opts[0], :yellow), opts[1].to_s] }
      # print bots
      table << [set_color('Bots', :green), runner.match.bots.map { |bot| bot.name }]
    }
  end

  def print_runner_stats(runner)
    say '='*30
    self.print_stats{ |table|
      table << [set_color('Ticks', :blue), runner.match.ticks.to_s]
      table << [set_color('Survivors', :green)] + runner.match.bots.map { |bot| "#{bot.name} [#{bot.health.round}]" }
    }
  end

  def set_gc(gc = true)
    if gc
      yield
    else
      GC.disable
      begin
        yield
      ensure
        GC.enable
      end
    end
  end

  def download_gist(gist_id, options)
    client = Octokit::Client.new
    begin
      gist = client.gist(gist_id)
    rescue Octokit::NotFound
      puts set_color("Error! Gist #{gist_id} not found. Please ensure the gist id is correct.", :red)
    else
      gist.files.attrs.each do |name, gist_file|
        gist_path = "#{BOT_DIR}/#{gist.owner.login}.#{gist_id}/#{name}"
        create_file(gist_path, gist_file.content, options)
      end
    end
  end

  def print_stats(indent = 2, &block)
    self.print_table([].tap(&block), :indent => indent)
  end
end

RTanqueCLI.start