継続を試す

先日のRuby勉強会の演習のソースを少し修正しました。せっかく「irb の演習」がお題でしたので、calccによる「継続」の練習です。ホントは、Marshal使ってセーブまでしようと思ったのですが、ContinuationオブジェクトがMarshalできない、ということでしたので、そちらはやめました。たぶん、1.8/1.9の両方で動くはずです。
あと、NetBeansって、プロジェクトごとにエンコーディングを変えられるんですね。もちろん、windows-31j に変更です。

#! ruby -Ks
# -*- encoding: Shift_JIS -*-

require 'enumerator'
require 'readline'
require 'continuation' unless defined? Continuation

class Array
  # ruby 1.8用
  def choise_one
    self[rand(size)]
  end
end

module CalcHundred
  # 全問題数
  MAX_N_QUESTIONS = 100
  # 10問ずつ区切る
  N_FRACTION = 10
  # かんたんモード
  MODE_EASY = 1
  # むずかしいモード
  MODE_HARD = 2
  # モードの一覧
  MODES = [ MODE_EASY, MODE_HARD ]
  # モードの表示内容
  MODE_NAMES = { MODE_EASY => "#{MODE_EASY}:かんたん",
                 MODE_HARD => "#{MODE_HARD}:むずかしい" }
  # 演算子の集合  
  OPS ={ MODE_EASY => %w(+ - *), MODE_HARD => %w(+ - * /) }
  # オペランドの最大値
  MAX_VALUE = { MODE_EASY => 10, MODE_HARD => 100 }
  
  class ExitException < StandardError; end

  class Controller
    def initialize(max_n_q = nil)
      @n_q = max_n_q || MAX_N_QUESTIONS
    end
    
    def setup
      @elapsed = 0
      @n_correct = 0
      @n_wrong = 0
    end
    
    # mode を呼び出し時に設定することを可能にしています。
    def select_mode(mode = nil)
      puts "計算 #{@n_q} 開始"
      @mode = mode ? mode : get_mode
      puts
    end

    def get_mode
      case Readline.readline "難易度を選択してください。 #{MODE_NAMES.values.sort.join(' ')} > "
      when /\A\s*([#{MODES}])\s*\Z/
        $1.to_i
      when nil
        raise ExitException, "EOFが入力されました。"
      else
        get_mode
      end
    end
    
    def start
      setup
      Readline.readline("#{MODE_NAMES[@mode]} を開始します。 Press Enter > ")
      @time_start = Time.now
      srand(@time_start.to_i)
      (1..@n_q).each_slice(N_FRACTION) { |poses_q|
        poses_q.each do
          q = Question.new(@mode, OPS[@mode].choise_one)
          $DEBUG ? sample_answer(q) : answer_by_user(q)
        end
        puts "#{poses_q.last} 問突破" if poses_q.last.modulo(N_FRACTION) == 0
      }
      @elapsed += Time.now - @time_start
      puts '', "#{@n_q} 問終了しました。"
      callcc { |cc| @cc = cc }
    end
    
    def sample_answer(q)
      print q
      sleep 0.2
      rand(2) == 0 ? (puts q.answer; correct) : (puts; incorrect)
    end
    
    def answer_by_user(q)
      callcc { |cc| @cc = cc }
      ans = Readline.readline(q.to_s)
      suspend('中断します') if ans.nil?
      q.check_answer(ans) ? correct :  incorrect
    end
    
    def correct
      puts "正解"
      @n_correct += 1      
    end
    
    def incorrect
      puts "不正解"
      @n_wrong += 1    
    end
    
    def summary
      s = []
      s << "正解  : %d 問" % @n_correct
      s << "不正解: %d 問" % @n_wrong
      s << "タイム: %s" % elapsed_time
      puts s
    end
    
    def elapsed_time
      "%d 分 %d 秒" % @elapsed.divmod(60)
    end
    
    def suspend(mes)
      puts mes
      @elapsed += Time.now - @time_start
      summary
      if /y/i =~ Readline.readline("再開しますか? [yN]: ")
        resume
      else
        raise ExitException, "irbの場合、 ``_.resume'' で復帰できます"
      end
    end
    
    def resume
      @time_start = Time.now
      @cc.call
    end
    
    def exit(mes=nil)
      puts( mes ||= "終了します" )
    end
    
    def main
      select_mode
      start
      summary
    rescue ExitException => e
      exit(e)
    ensure
      return self
    end
  end
  
  class Question
    def initialize(mode, op)
      @mode = mode
      @op = op
      mk_operands
    end
    
    def mk_operands
      @v1, @v2 = Array.new(2).map{ rand(MAX_VALUE[@mode]) }
      mk_operands if @op == '/' and (@v2 == 0 or @v1.modulo(@v2) != 0)
    end
    
    def to_s
      "%d %s %d = " % [@v1, @op, @v2]
    end
    
    def check_answer(ans)
      return false if /\A\s*\Z/ =~ ans.to_s
      ans.to_i == answer
    end
    
    def answer
      @ans ||= eval("#{@v1} #{@op} #{@v2}")
    end
  end
  
  def self.main(max = nil)
    max = nil if max.to_i <= 0
    Controller.new(max).main
  end
end

if $0 == __FILE__
  max = ARGV.shift.to_i
  CalcHundred.main(max)
end

実行結果はこんな感じです。

irb(main):002:0> require'calc_hundred'
=> true
irb(main):003:0> CalcHundred.main 15     <- 15問を選択
計算 15 開始
難易度を選択してください。 1:かんたん 2:むずかしい > 1

1:かんたん を開始します。 Press Enter >
9 + 9 = 18
正解
5 - 6 =
不正解
8 * 9 = 72
正解
5 + 3 =
不正解
9 * 1 = 9
正解
3 * 3 = 中断します              <- C-d入力
正解  : 3 問
不正解: 2 問
タイム: 0 分 6 秒
再開しますか? [yN]: y          <- 継続する
3 * 3 = 9
正解
1 + 0 = 1
正解
0 + 8 = 8
正解
4 + 8 = 12
正解
3 + 2 = 5
正解
10 問突破
2 + 1 = 3
正解
5 + 0 = 中断します
正解  : 9 問
不正解: 2 問
タイム: 0 分 15 秒
再開しますか? [yN]: n          <- 継続しない
irbの場合、 ``_.resume'' で復帰できます
=> #<CalcHundred::Controller:0x1b60278 @n_q=15, @mode=1, @elapsed=15.692, 
@n_correct=9, @n_wrong=2, @time_start=2008-08-03 15:48:18 +0900, @cc=#<Continuation:0x1bfddfc>>


irb(main):004:0> _.resume       <- 復旧する
5 + 0 = 5
正解
9 + 4 = 13
正解
0 + 8 =
不正解
1 + 4 = 5
正解

15 問終了しました。
正解  : 12 問
不正解: 3 問
タイム: 0 分 20 秒
=> #<CalcHundred::Controller:0x1b60278 @n_q=15, @mode=1, @elapsed=20.036,
@n_correct=12, @n_wrong=3, @time_start=2008-08-03 15:48:32 +0900, @cc=#<Continuation:0x1bfb494>>