AOJToolkit

公開しよう公開しようと思ってうっかり忘れていたスクリプトをついに公開する運びとなりました。


ネトゲ廃人の皆様におかれましては、東京大会を目前に控えた今日この頃、ますます廃プレイに磨きが掛かっているものとお喜び申し上げます。
さて、廃プレイをしていると、「submitするの面倒臭ぇ」とか思ったりすることはないでしょうか。コピーしてブラウザ開いてペしてsubmitだなんてまどろっこしいと思わないでしょうか。
思わない人は、このエントリを読むのを止めても問題ありません。

そんなわけで、コマンドラインからAOJにsubmitするためのスクリプトを作成しました。どうぞご活用ください。

概要

  • Rubyで書かれたスクリプトです。
    • ruby 1.8.7で開発されていますが、より新しいバージョンでも動くと思います。

ソースコード

#! /usr/bin/ruby

require 'net/http'
require 'uri'

class AOJResultPrinter

  RETRY_COUNT = 5

  def self.print_result(http, path, username)
    wait_time = 2
    RETRY_COUNT.times {
      sleep(wait_time)
      response = http.get(path)
      result, need_retry = parse_result(response.body, username)
      puts result
      if(!need_retry)
        break
      end
      wait_time += 1
    }
  end
  
  def self.parse_result(text, submitter_username)
    str = text.gsub(/\s/, " ")
    submission_regexp = /<status>(.*?<status>.*?<\/status>.*?)<\/status>/
    str.scan(submission_regexp){ |matches|
      match = matches[0]

      id       = extract(match, "run_id").to_i
      username = extract(match, "user_id")
      problem  = extract(match, "problem_id")
      time     = Time.at(extract(match, "submission_date").to_i / 1000)
      status   = extract(match, "status")
      language = extract(match, "language")
      runtime  = extract(match, "cpu_time").to_i
      memory   = extract(match, "memory").to_i
      length   = extract(match, "code_size").to_i
      
      if(username == submitter_username && Time.now < time + 60)
        tokens = []
        tokens.push('')
        tokens.push("Result:      " + status)
        tokens.push("Run ID:      " + id.to_s)
        if(status != "Runtime Error" && status != "Compile Error")
          tokens.push("Time:        " + sprintf("%.02f sec", runtime.to_f/100))
          tokens.push("Memory:      " + memory.to_s + " KB")
        end
        tokens.push("Code Length: " + length.to_s + " bytes")
        return [tokens.join("\n"), false]
      end
    }
    return ["in judge queue...", true]
  end
  
  def self.extract(text, target)
    return text.scan(/<#{target}>(.*?)<\/#{target}>/)[0][0].strip
  end
end

class Submitter
  
  MAP_EXTNAME_LANGUAGE = {
    '.c'    => :c,
    '.cpp'  => :cpp,
    '.cc'   => :cpp,
    '.C'    => :cpp,
    '.java' => :java,
  }

  JUDGE_SETTING = {
    
    :aoj => {
      :name => "Aizu Online Judge",
      
      :uri         => 'rose.u-aizu.ac.jp',
      :path_submit => '/onlinejudge/servlet/Submit',
      :path_result => '/onlinejudge/webservice/status_log',

      :map_form_name => {
        :username  => 'userID',
        :password  => 'password',
        :problemID => 'problemNO',
        :language  => 'language',
        :program   => 'sourceCode'
      },

      :map_form_value_language => {
        :c    => 'C',
        :cpp  => 'C++',
        :java => 'JAVA',
      },

      :result_printer => AOJResultPrinter,
    }
  }
  
  USER_SETTING = {
    :aoj => {
      :username => '',  # <= PUT YOUR USERNAME HERE
      :password => '',  # <= PUT YOUR PASSWORD HERE
    },
  }

  def main()
    set_params()
    submit()
  end
  
  def set_params()
    @filename  = nil
    @judge     = nil

    while(!ARGV.empty? && ARGV.first[0..0] == '-')
      option   = ARGV[0].dup
      argument = ARGV[1].dup
      ARGV.shift()
      ARGV.shift()

      if(!(option && argument))
        raise "Invalid option specified."
      end

      case(option)
      when '-judge', '-j'
        @judge     = argument.downcase.intern
      when '-problemID', '-p'
        @problemID = argument
      when '-language', '-lang', '-l'
        @language  = argument.downcase.intern
      else
        raise "Option '#{option}' is not defined."
      end
    end

    raise "Judge is not specified."     unless @judge
    raise "Filename is not specified."  unless ARGV[0]
    
    @filename = ARGV[0]
    @language  = get_language(@filename)  unless @language
    @problemID = get_problemID(@filename) unless @problemID
  end

  def get_language(filename)
    return MAP_EXTNAME_LANGUAGE[File.extname(filename)]
  end

  def get_problemID(filename)
    return File.basename(filename).split(/[^A-Z0-9]/).first
  end

  def submit()
    judge        = JUDGE_SETTING[@judge]
    uri          = judge[:uri]
    path_submit  = judge[:path_submit]
    path_result  = judge[:path_result]
    data         = create_data()
    
    print_log()

    # --------

    Net::HTTP.start(uri) { |http|
      # dryrun
      if(true)
        response = http.post(path_submit, data)
        print response.code, ' ', response.message, "\n" 
      else
        puts "Dryrun..."
      end
      
      resp = judge[:result_printer]
      
      if(path_result && judge[:result_printer])
        judge[:result_printer].print_result(http, path_result, USER_SETTING[@judge][:username])
      end
    }
  end

  def print_log()
    STDERR << "Submitting...\n"

    STDERR << "Judge:    " << JUDGE_SETTING[@judge][:name] << "\n"
    STDERR << "Problem:  " << @problemID << "\n"
    STDERR << "Language: " << @language.to_s.capitalize << "\n"
    STDERR << "Filename: " << @filename << "\n"    
  end

  def create_data()    
    judge = JUDGE_SETTING[@judge]
    user  = USER_SETTING[@judge]
    
    params = {
      :username => user[:username],
      :password => user[:password],
      
      :problemID => @problemID,
      :program   => File.open(@filename).read(),
      :language  => judge[:map_form_value_language][@language],
    }

    form_name = judge[:map_form_name]

    return params.map{ |key, value|
      enc(form_name[key]) + '=' + enc(value)
    }.join('&')
  end

  def enc(str)
    return URI.encode(str, /./n)
  end
end

Submitter.new.main()

使い方

  • 上のスクリプトを適当なファイル (submit.rbとします) に保存します。
  • スクリプト内に、あなたのユーザ名とパスワードを記述します。
    • PUT YOUR USERNAME/PASSWORD HERE とコメントされているところです
  • 以下のようにして実行します。


% ruby submit.rb -judge aoj -problemID __problem_id__ -language __language__ __filename_of_your_code__

  • しばらく待つと、結果も表示されます。

オプションの詳細説明

  • -judge, -j
    • ジャッジ名を指定します。今後の拡張 (UVaとかPKUとか) のために用意されていますが、今のところ aoj 以外を指定しても意味がありません。
    • 省略不可能です。
  • -problemID, -p
    • 問題番号を指定します。AOJでは Volume 0 などの問題のIDにleading zeroesがあることに注意してください。
    • 省略可能です。省略した場合、送信するファイル名からてきとーに判断されます。
  • -language, -lang, -l
    • 言語を指定します。c, cpp, java (大文字小文字を区別しない) が受理されます。
    • 省略可能です。省略した場合、送信するファイルの拡張子からてきとーに判断されます。

使用上の注意

  • 簡単にsubmitできるようになると、うっかり手元で実行するのと同じ感覚でsubmitしてしまうようになります。
    • WAとかTLEとか増える恐れがありますのでご注意ください。
    • また、AOJにあんまり負担を掛けないよう、用法用量を守って楽しくお使いください。

既知の問題

  • 結果が正しく表示されない (in judge queue...とだけ表示されるが、AOJのstatusを見るとジャッジされている)
    • お使いのPCの時刻が正しく設定されているか確認してください。これは、このスクリプトが「過去60秒以内に送った最新のsubmitの結果を表示する」という内部仕様になっていることに起因します。
    • PCの時刻が正しいならば、AOJの時刻が正しく設定されているか確認してください。ずれていた場合、AOJの中の人にお願いするか、あきらめてください。

ライセンス

  • NYSL Version 0.9982で公開します。
    • もし改良して公開いただける方がいましたら、嬉々として使いにいきますのでよろしくお願いします。

おまけ

#! /bin/bash

for filename in $*
do
	~/bin/submit.rb -judge aoj $filename
done


% aoj 10000.cpp

  • 複数ファイルを一度に送りたいシチュエーションなんてまずないわけですが。