RubyのRSpecでリボルバーを作ってロシアンルーレットしようよ!

今までコードを書くとき
正直あまりまじめにテストをしてこなかったよ
でもいつまでもそういう訳にはいかないだろうから
以下の記事を参考にしてRSpecをやってみたんだ


RSpec の入門とその一歩先へ - t-wadaの日記
Rubyist Magazine - スはスペックのス 【第 1 回】 RSpec の概要と、RSpec on Rails (モデル編)


そうしたら次のようなことが分かったんだよ

  1. RSpecは設計図
  2. RSpecはなんかたのしい


でこの感覚をみんなと共有できればと思ったので
RSpec入門者の僕がRSpecを使って
ここで何か作ってみるよ


さて 何を作ろうか..


何か実用的で楽しいものがいいよね
そうだ折角だからなかなか手に入らないものがいいね
なかなか手に入らないものといったら..


もちろん輸入禁制品だよね!


そんなわけで..


RubyRSpecを使ってけん銃を作るよ
で最高にイカしてるけん銃と言ったらリボルバーだから
リボルバーを作ることにするよ
銃が完成したら
君とロシアンルーレットを楽しみたいと思うんだ


最初に断っておくと
このポストは君がエディタとターミナルを開いて
僕と同じことをすることを期待したもの
つまりチュートリアルの形式になっているよ
だから相当長いポストになることを覚悟してほしいよ


結果だけみたい人は以下にコードを張ったから
それにざっと目を通してそれから
ロシアンルーレットで遊ぶ」の項に飛んでくれるとうれしいよ


Revolver for Russian Roulette ― Gist

リボルバーの作り方

うれしいことにRubyには
ロシアンルーレットのための部品が既に用意されてるよ
Array#rotate! #shuffle #cycle は
まさにその目的のために作られたんだよ
これらのメソッドがあれば作業がかなり捗るよね


さて 簡単な方針だけ決めてRSpecを書いていくよ
けん銃にもいろいろあるからここでは
いきなりRevolverクラスを作るんじゃなくて
ベースとなるGunクラスを作って
Revolverクラスはそれを継承するようにしよう

 #gun_spec.rb
 require "rspec"
 require_relative "gun"
 
 describe Gun do
   
 end
 
 describe Revolver do
   
 end

Gunクラス

けん銃は少なくとも弾を込めて
発砲できなくちゃいけないから
これをGunクラスの機能として持たせるよ
薬室(弾を込める場所) 装弾 発砲をそれぞれ
chamber set_cartridge triggerとするよ
弾はカートリッジ(cartridge)と言うよ


ちなみに銃に詳しくない人はここに目を通すといいよ


SCC-Gun

 describe Gun do
   context "chamber" do
     
   end
 
   context "set_cartridge" do
     
   end
 
   context "trigger" do
     
   end
 end


ここで一度rspecを走らせてみるよ

 % rspec -fs -c gun_spec.rb
 % gun_spec.rb:4:in `<top (required)>': uninitialized constant Object::Gun (NameError)


Gunクラスがないって文句を言われたからこれを作ろう
Revolverもね

 #gun.rb
 class Gun
   
 end
 
 class Revolver < Gun
   
 end


さあもう一度テストするよ

 % rspec -fs -c gun_spec.rb
 No examples found.
 
 Finished in 0.00005 seconds
 0 examples, 0 failures


でchamberは最初は空でset_cartridgeすると
弾がそこに入って撃てるようになる
そういう設計に沿ってexampleを書くよ

 require "rspec"
 require_relative "gun"
 
 describe Gun do
   before do
     @gun = Gun.new
   end
 
   context "chamber" do
     it "should be empty at default" do
       @gun.chamber.should be_empty
     end
     
   end
 
   context "set_cartridge" do
     it "should set a cartridge to the chamber" do
       @gun.set_cartridge
       @gun.chamber.should == [Cartridge.new]
     end
   end
 
   context "trigger" do
     
   end
 end
 
 describe Revolver do
   
 end


そしてテストするよ

 % rspec -fs -c gun_spec.rb 
 Gun
   chamber
     should be empty at default (FAILED - 1)
   set_cartridge
     should set a cartridge to the chamber (FAILED - 2)
 Failures:
 
   1) Gun chamber should be empty at default
      Failure/Error: @gun.chamber.should be_empty
      NoMethodError:
        undefined method `chamber' for #<Gun:0x00000100a1e248>
      # ./gun_spec.rb:11:in `block (3 levels) in <top (required)>'
 
   2) Gun set_cartridge should set a cartridge to the chamber
      Failure/Error: @gun.set_cartridge
      NoMethodError:
        undefined method `set_cartridge' for #<Gun:0x00000100a1cda8>
      # ./gun_spec.rb:18:in `block (3 levels) in <top (required)>'
 
 Finished in 0.00092 seconds
 2 examples, 2 failures
 Failed examples:
 
 rspec ./gun_spec.rb:10 # Gun chamber should be empty at default
 rspec ./gun_spec.rb:17 # Gun set_cartridge should set a cartridge to the chamber


chamberメソッドもset_cartridgeメソッドも
無いって言われたから作るよ

 class Gun
   attr_reader :chamber
   def initialize
     @chamber = []
   end
 
   def set_cartridge
     @chamber << Cartridge.new
   end
 end


もう一度テストするよ

 % rspec -fs -c gun_spec.rb 
 Gun
   chamber
     should be empty at default
   set_cartridge
     should set a cartridge to the chamber (FAILED - 1)
 Failures:
 
   1) Gun set_cartridge should set a cartridge to the chamber
      Failure/Error: @gun.set_cartridge
      NameError:
        uninitialized constant Gun::Cartridge
      # ./gun.rb:8:in `set_cartridge'
      # ./gun_spec.rb:18:in `block (3 levels) in <top (required)>'
 
 Finished in 0.00328 seconds
 2 examples, 1 failure

1つ目はパスしたけど
2つ目はまだ弾を作ってなかったからフェイルしたよ
弾を作るよ

 class Cartridge
   
 end


テストね

 % rspec -fs -c gun_spec.rb 
 Gun
   chamber
     should be empty at default
   set_cartridge
     should set a cartridge to the chamber (FAILED - 1)
 Failures:
 
   1) Gun set_cartridge should set a cartridge to the chamber
      Failure/Error: @gun.chamber.should == [Cartridge.new]
        expected: [#<Cartridge:0x00000100a992b8>]
             got: [#<Cartridge:0x00000100a99420>] (using ==)
        Diff:
        @@ -1,2 +1,2 @@
        -[#<Cartridge:0x00000100a992b8>]
        +[#<Cartridge:0x00000100a99420>]
      # ./gun_spec.rb:19:in `block (3 levels) in <top (required)>'
 Finished in 0.00145 seconds
 2 examples, 1 failure


今度は弾が違うって言われたよ
弾の同値性をオブジェクトで判断するからだね
じゃあ同値性をクラスで判断して弾の個性を消すよ

 class Cartridge
   include Comparable
   def <=>(other)
     self.class <=> other.class
   end
 end


さあもう一度

 % rspec -fs -c gun_spec.rb 
 Gun
   chamber
     should be empty at default
   set_cartridge
     should set a cartridge to the chamber
 Finished in 0.00088 seconds
 2 examples, 0 failures

うまくいったよ!


さて次にchamberに弾が入ってたらもう詰められないから
そのときはエラーがでるようにしないと
exampleを書くよ

  context "set_cartridge" do
    it "should set a cartridge to the chamber" do
      @gun.set_cartridge
      @gun.chamber.should == [Cartridge.new]
    end

    it "should be error when the chamber has a cartridge" do
      ->{ 2.times { @gun.set_cartridge } }.should raise_error
    end
  end

raise_errorを捕捉するにはset_cartridgeのリターンを
Procオブジェクト化しないといけないよ
ちょっとイケてないけどねー


テストするよ

 % rspec -fs -c gun_spec.rb 
 Gun
   chamber
     should be empty at default
   set_cartridge
     should set a cartridge to the chamber
     should be error when the chamber has a cartridge (FAILED - 1)
 
 Failures:
 
   1) Gun set_cartridge should be error when the chamber has a cartridge
      Failure/Error: ->{ 2.times { @gun.set_cartridge } }.should raise_error
        expected Exception but nothing was raised
      # ./gun_spec.rb:23:in `block (3 levels) in <top (required)>'
 
 Finished in 0.00122 seconds
 3 examples, 1 failure


エラーを期待したのに
エラーにならないって言われたので対応するよ
ErrorクラスはChamberErrorにしようか

 class Gun
   class ChamberError < StandardError; end
   
   attr_reader :chamber
   def initialize
     @chamber = []
   end
 
   def set_cartridge
     raise ChamberError, 'The chamber is full' unless @chamber.empty?
     @chamber << Cartridge.new
   end
 end


テストするよ

 % rspec -fs -c gun_spec.rb 
 Gun
   chamber
     should be empty at default
   set_cartridge
     should set a cartridge to the chamber
     should be error when the chamber has a cartridge
 
 Finished in 0.00137 seconds
 3 examples, 0 failures

パスしたよ


さて次はtrigger周りを作ろう
もちろん発砲したら'Bang!'ってならなきゃ

  context "trigger" do
    it "should return 'Bang!'" do
      @gun.trigger.should == 'Bang!'
    end
  end


実装もしちゃうよ

 class Gun
 
   def trigger
     'Bang!'
   end
 end


テストするよ

 % rspec -fs -c gun_spec.rb 
 Gun
   chamber
     should be empty at default
   set_cartridge
     should set a cartridge to the chamber
     should be error when the chamber has a cartridge
   trigger
     should return 'Bang!'
 
 Finished in 0.00148 seconds
 4 examples, 0 failures

いいね!


さて今度は弾がないときの対応も取らないと

  context "trigger" do
    it "should return 'Bang!'" do
      @gun.trigger.should == 'Bang!'
    end

    it "should be nil when the chamber is empty" do
      @gun.chamber.should be_empty
      @gun.trigger.should be_nil 
    end
  end


テストするよ

 % rspec -fs -c gun_spec.rb 
 Gun
   chamber
     should be empty at default
   set_cartridge
     should set a cartridge to the chamber
     should be error when the chamber has a cartridge
   trigger
     should return 'Bang!'
     should be nil when the chamber is empty (FAILED - 1)
 Failures:
 
   1) Gun trigger should be nil when the chamber is empty
      Failure/Error: @gun.trigger.should be_nil
        expected: nil
             got: "Bang!"
      # ./gun_spec.rb:34:in `block (3 levels) in <top (required)>'
 Finished in 0.00234 seconds
 5 examples, 1 failure


弾がなくても発砲しちゃうって..
対応するよ

  def trigger
    return nil if @chamber.empty?
    'Bang!'
  end


テストするよ

 % rspec -fs -c gun_spec.rb 
 Gun
   chamber
     should be empty at default
   set_cartridge
     should set a cartridge to the chamber
     should be error when the chamber has a cartridge
   trigger
     should return 'Bang!' (FAILED - 1)
     should be nil when the chamber is empty
 Failures:
 
   1) Gun trigger should return 'Bang!'
      Failure/Error: @gun.trigger.should == 'Bang!'
        expected: "Bang!"
             got: nil (using ==)
      # ./gun_spec.rb:29:in `block (3 levels) in <top (required)>'
 Finished in 0.00218 seconds
 5 examples, 1 failure


今度はさっきパスしたexampleがフェイルしてる
そうだよ弾をセットしてないからね
テストが間違ってたんだ
テストを直してもう一度

  context "trigger" do
    it "should return 'Bang!'" do
      @gun.set_cartridge
      @gun.trigger.should == 'Bang!'
    end

    it "should be nil when the chamber is empty" do
      @gun.chamber.should be_empty
      @gun.trigger.should be_nil 
    end
  end


テストする

 % rspec -fs -c gun_spec.rb 
 Gun
   chamber
     should be empty at default
   set_cartridge
     should set a cartridge to the chamber
     should be error when the chamber has a cartridge
   trigger
     should return 'Bang!'
     should be nil when the chamber is empty
 Finished in 0.0023 seconds
 5 examples, 0 failures

いいね!


さてそれから弾を撃ったら
chamberは空にならなきゃいけないね
追加しよう

  context "chamber" do
    it "should be empty at default" do
      @gun.chamber.should be_empty
    end

    it "should be empty after triggering" do
      @gun.set_cartridge
      @gun.trigger
      @gun.chamber.should be_empty
    end
  end


テストするよ

 % rspec -fs -c gun_spec.rb 
 
 Gun
   chamber
     should be empty at default
     should be empty after triggering (FAILED - 1)
   set_cartridge
     should set a cartridge to the chamber
     should be error when the chamber has a cartridge
   trigger
     should return 'Bang!'
     should be nil when the chamber is empty
 
 Failures:
 
   1) Gun chamber should be empty after triggering
      Failure/Error: @gun.chamber.should be_empty
        expected empty? to return true, got false
      # ./gun_spec.rb:17:in `block (3 levels) in <top (required)>'
 Finished in 0.00227 seconds
 6 examples, 1 failure


フェイルするから実装するよ

  def trigger
    return nil if @chamber.empty?
    @chamber.clear
    'Bang!'
  end


もう一度テスト

 % rspec -fs -c gun_spec.rb 
 
 Gun
   chamber
     should be empty at default
     should be empty after triggering
   set_cartridge
     should set a cartridge to the chamber
     should be error when the chamber has a cartridge
   trigger
     should return 'Bang!'
     should be nil when the chamber is empty
 
 Finished in 0.00215 seconds
 6 examples, 0 failures

いいね!


さてGunクラスは一応これで完成として
今度はRevolverクラスを作っていくよ

Revolverクラス

リボルバーは6弾程の弾を保持する
回転式シリンダーという独特の機構を持っていて
一発撃つ毎にシリンダーが一つ回転して
次の弾がチャンバーにアラインされて
発砲の準備が整うよ


早速Revolverクラスを設計しよう
Gunクラスでは弾を直接chamberにセットしたけど
Revolverではcylinderにセットすることになるよね
だからset_cartridgeとchamberの再設計が必要だよ
ここではcylinderの0位置に弾がセットされたら
chamberにも弾があることにするよ

 describe Revolver do
   before do
     @rev = Revolver.new
   end
 
   context "set_cartridge" do
     it "should set a cartridge to the cylinder pos 0" do
       @rev.set_cartridge
       @rev.cylinder[0].should == Cartridge.new
     end
   end
 end


テストしてみるよ

 % rspec -fs -c gun_spec.rb 
 
 Revolver
   set_cartridge
     should set a cartridge to the cylinder pos 0 (FAILED - 1)
 
 Failures:
 
   1) Revolver set_cartridge should set a cartridge to the cylinder pos 0
     Failure/Error: @rev.cylinder[0].should == Cartridge.new
      NoMethodError:
       undefined method `cylinder' for #<Revolver:0x00000100a5af90>
      # ./gun_spec.rb:53:in `block (3 levels) in <top (required)>'
 
 Finished in 0.00296 seconds
 7 examples, 1 failure


cylinderがないって言われたので作るよ

 class Revolver < Gun
   CYLINDER_SIZE = 6
   attr_reader :cylinder
   def initialize
     @cylinder = Array.new(CYLINDER_SIZE)
     super
   end
 end


再テストするよ

 % rspec -fs -c gun_spec.rb 
 
 Revolver
   set_cartridge
     should set a cartridge to the cylinder pos 0 (FAILED - 1)
 
 Failures:
 
   1) Revolver set_cartridge should set a cartridge to the cylinder pos 0
     Failure/Error: @rev.cylinder[0].should == Cartridge.new
        expected: #<Cartridge:0x00000100a6a3a0>
        got: nil (using ==)
      # ./gun_spec.rb:53:in `block (3 levels) in <top (required)>'
 Finished in 0.00281 seconds
 7 examples, 1 failure


今度は設計と違う結果(nil)が返ってきたよ
Revolver用のset_cartridgeを実装しないとね
最初は0ポジション空いてなければ隣に装弾する方式にしよう
先を急ぐから例外処理もここに書いちゃうよ

 class Revolver < Gun
   class CylinderError < StandardError; end
   
   CYLINDER_SIZE = 6
   attr_reader :cylinder
   def initialize
     @cylinder = Array.new(CYLINDER_SIZE)
     super
   end
 
   def set_cartridge
     pos = @cylinder.index(nil)
     raise CylinderError, 'Cylinder is full' unless pos
     @cylinder[pos] = Cartridge.new
   end
 end


もう2つほどexampleを追加してからテストしてみよう

  context "set_cartridge" do
    it "should set a cartridge to the cylinder pos 0" do
      @rev.set_cartridge
      @rev.cylinder[0].should == Cartridge.new
    end

    it "should set 3 cartridges to the cylinder pos 0-3" do
      3.times { @rev.set_cartridge }
      @rev.cylinder.should == [Cartridge.new, Cartridge.new, Cartridge.new, nil, nil, nil]
    end

    it "should be error when it called more than the cylinder rooms" do
      Revolver::CYLINDER_SIZE.times { @rev.set_cartridge }
      ->{ @rev.set_cartridge }.should raise_error(Revolver::CylinderError)
    end
  end


テストだよ

 % rspec -fs -c gun_spec.rb 
 
 Revolver
   set_cartridge
     should set a cartridge to the cylinder pos 0
     should set 3 cartridges to the cylinder pos 0-3
     should be error when it called more than the cylinder rooms
 
 Finished in 0.00315 seconds
 9 examples, 0 failures

OKだね!
ほんとうはcylinderの自由な位置に
カートリッジを装填できるべきだけど
ここでは割愛するよ


さてtriggerにいこう
まずはちゃんとBangするように

  context "trigger" do
    it "should return 'Bang!'" do
      @rev.set_cartridge
      @rev.trigger.should == 'Bang!'
    end
  end
 % rspec -fs -c gun_spec.rb 
 
 Revolver
   set_cartridge
     should set a cartridge to the cylinder pos 0
     should set 3 cartridges to the cylinder pos 0-3
     should be error when it called more than the cylinder rooms
   trigger
     should return 'Bang!' (FAILED - 1)
 Failures:
   1) Revolver trigger should return 'Bang!'
      Failure/Error: @rev.trigger.should == 'Bang!'
        expected: "Bang!"
             got: nil (using ==)
      # ./gun_spec.rb:70:in `block (3 levels) in <top (required)>'
 
 Finished in 0.00469 seconds
 10 examples, 1 failure


フェイルしちゃったよ
あーchamberに弾が無いからね
cylinder[0]に弾があれば
chamberにも弾があるって設計だったよね

  def chamber
    [ @cylinder[0] ].compact
  end
 % rspec -fs -c gun_spec.rb 
 
 Revolver
   set_cartridge
     should set a cartridge to the cylinder pos 0
     should set 3 cartridges to the cylinder pos 0-3
     should be error when it called more than the cylinder rooms
   trigger
     should return 'Bang!' (FAILED - 1)
 Failures:
   1) Revolver trigger should return 'Bang!'
      Failure/Error: @rev.trigger.should == 'Bang!'
        expected: "Bang!"
             got: nil (using ==)
      # ./gun_spec.rb:70:in `block (3 levels) in <top (required)>'
 
 Finished in 0.00338 seconds
 10 examples, 1 failure


まだフェイルする..
そうかこれはGun#triggerで
直接@chamberを参照してるから起きてるんだ
ここを直そう

 class Gun
 
    def trigger
 -    return nil if @chamber.empty?
 +    return nil if chamber.empty?
 -    @chamber.clear
 +    reset_chamber
     'Bang!'
    end
 
 +  private
 +  def reset_chamber
 +    @chamber.clear
 +  end
 end
 
 
 class Revolver
 
 + def reset_chamber
 +   @cylinder[0] = nil
 + end
 + private :reset_chamber
 end


テストだよ

 % rspec -fs -c gun_spec.rb 
 
 Gun
   chamber
     should be empty at default
     should be empty after triggering
   set_cartridge
     should set a cartridge to the chamber
     should be error when the chamber has a cartridge  trigger
     should return 'Bang!'
     should be nil when the chamber is empty
 Revolver
   set_cartridge
     should set a cartridge to the cylinder pos 0
     should set 3 cartridges to the cylinder pos 0-3
     should be error when it called more than the cylinder rooms
   trigger
     should return 'Bang!'
 Finished in 0.00373 seconds
 10 examples, 0 failures

OKだね


考えてみたらロシアンルーレットってみんな
毎回ハンマー(撃鉄)を手で起こしてるよね
このタイプのリボルバーはシングルアクションというそうだよ
是非ともこの機構(cocking)を実装したいよ
cockingしないと発砲できないようにしよう
それから発砲した後はhammerが戻るようにしよう

  context "trigger" do
    it "should return 'Bang!'" do
      @rev.set_cartridge
      @rev.cocking
      @rev.trigger.should == 'Bang!'
    end

    it "should be nil without cocking" do
      @rev.set_cartridge
      @rev.trigger.should be_nil
    end
  end

  context "hammer" do
    it "should be false after triggering" do
      @rev.set_cartridge
      @rev.cocking
      @rev.trigger
      @rev.hammer.should be_false
    end
  end


当然hammerやcockingが無いって怒られるから
それを確認した上で実装しよう

 class Revolver < Gun
   attr_reader :cylinder, :hammer
   def initialize
     @cylinder = Array.new(CYLINDER_SIZE)
     @hammer = false
     super
   end
 
   def cocking
     @hammer = true
   end
 
   def trigger
     return nil unless @hammer
     @hammer = false
     super
   end
 end


テストするよ

 % rspec -fs -c gun_spec.rb 
 
 Revolver
   set_cartridge
     should set a cartridge to the cylinder pos 0
     should set 3 cartridges to the cylinder pos 0-3
     should be error when it called more than the cylinder rooms
   trigger
     should return 'Bang!'
     should be nil without cocking
   hammer
     should be false after triggering
 Finished in 0.0042 seconds
 12 examples, 0 failures

いいみたいだね


あ それとcockingしたら
cylinderが一つ回転するようにしないといけなかったよ
回転しないと連続発砲ができないからね

  context "cylinder" do
    it "should rotate for next when cocking" do
      @rev.set_cartridge
      @rev.cocking
      @rev.cylinder.should == [nil, nil, nil, nil, nil, Cartridge.new]
    end
  end


テストするよ

 % rspec -fs -c gun_spec.rb 
 Revolver
   set_cartridge
     should set a cartridge to the cylinder pos 0
     should set 3 cartridges to the cylinder pos 0-3
     should be error when it called more than the cylinder rooms
   cylinder
     should rotate for next when cocking (FAILED - 1)
   trigger
     should return 'Bang!'
     should be nil without cocking
   hammer
     should be false after triggering
 
 Failures:
 
   1) Revolver cylinder should rotate for next when cocking
      Failure/Error: @rev.cylinder.should == [nil, Cartridge.new, nil, nil, nil, nil]
        expected: [nil, #<Cartridge:0x000001009cc1c8>, nil, nil, nil, nil]
             got: [#<Cartridge:0x000001009cc330>, nil, nil, nil, nil, nil] (using ==)
        Diff:
        @@ -1,2 +1,2 @@
        -[nil, #<Cartridge:0x000001009cc1c8>, nil, nil, nil, nil]
        +[#<Cartridge:0x000001009cc330>, nil, nil, nil, nil, nil]
      # ./gun_spec.rb:71:in `block (3 levels) in <top (required)>'
 
 Finished in 0.00549 seconds
 13 examples, 1 failure


フェイルするから直すよ

  def cocking
    @cylinder.rotate!
    @hammer = true
  end


テストだよ

 % rspec -fs -c gun_spec.rb 
 
 Revolver
   set_cartridge
     should set a cartridge to the cylinder pos 0
     should set 3 cartridges to the cylinder pos 0-3
     should be error when it called more than the cylinder rooms
   cylinder
     should rotate for next when cocking
   trigger
     should return 'Bang!' (FAILED - 1)
     should be nil without cocking
   hammer
     should be false after triggering
 
 Failures:
 
   1) Revolver trigger should return 'Bang!'
      Failure/Error: @rev.trigger.should == 'Bang!'
        expected: "Bang!"
             got: nil (using ==)
      # ./gun_spec.rb:79:in `block (3 levels) in <top (required)>'
 
 Finished in 0.00466 seconds
 13 examples, 1 failure


今度はtriggerのexampleでエラーが出たよ
cockingで弾がpos 0にいなくなったからだね
じゃあset_cartridgeでpos 0じゃなくpos 1に
弾をセットするように変えて対応するよ

   context "set_cartridge" do
 -    it "should set a cartridge to the cylinder pos 0" do
 +    it "should set a cartridge to the cylinder pos 1" do
       @rev.set_cartridge
 -      @rev.cylinder[0].should == Cartridge.new
 +      @rev.cylinder[1].should == Cartridge.new
     end
 
 -    it "should set 3 cartridges to the cylinder pos 0-3" do
 +    it "should set 3 cartridges to the cylinder pos 1-4" do
       3.times { @rev.set_cartridge }
 -      @rev.cylinder.should == [Cartridge.new, Cartridge.new, Cartridge.new, nil, nil, nil]
 +      @rev.cylinder.should == [nil, Cartridge.new, Cartridge.new, Cartridge.new, nil, nil]
     end
 
     it "should be error when it called more than the cylinder rooms" do
       Revolver::CYLINDER_SIZE.times { @rev.set_cartridge }
       ->{ @rev.set_cartridge }.should raise_error(Revolver::CylinderError)
     end
   end
 
   context "cylinder" do
     it "should rotate for next when cocking" do
       @rev.set_cartridge
       @rev.cocking
 -      @rev.cylinder.should == [nil, nil, nil, nil, nil, Cartridge.new]
 +      @rev.cylinder.should == [Cartridge.new, nil, nil, nil, nil, nil]
     end
   end  


フェイルするから実装するよ

  def set_cartridge
    pos = @cylinder.rotate.index(nil)
    raise CylinderError, 'Cylinder is full' unless pos
    pos = (pos + 1) % CYLINDER_SIZE
    @cylinder[pos] = Cartridge.new
  end


テストだよ

 % rspec -fs -c gun_spec.rb 
 
 Revolver
   set_cartridge
     should set a cartridge to the cylinder pos 1
     should set 3 cartridges to the cylinder pos 1-4
     should be error when it called more than the cylinder rooms
   cylinder
     should rotate for next when cocking
   trigger
     should return 'Bang!'
     should be nil without cocking
   hammer
     should be false after triggering
 
 Finished in 0.00461 seconds
 13 examples, 0 failures

うまくいったよ


じゃあ連続発砲ができるか試すよ

  context "trigger" do
    it "should work sequentially" do
      6.times { @rev.set_cartridge }
      @rev.cocking
      8.times.map { @rev.trigger.tap{ @rev.cocking } }.should == ["Bang!", "Bang!", "Bang!", "Bang!", "Bang!", "Bang!", nil, nil]
    end
  end


テストするよ

 % rspec -fs -c gun_spec.rb 
 
 Revolver
   set_cartridge
     should set a cartridge to the cylinder pos 1
     should set 3 cartridges to the cylinder pos 1-4
     should be error when it called more than the cylinder rooms
   cylinder    should rotate for next when cocking
   trigger
     should return 'Bang!'
     should be nil without cocking
     should work sequentially
   hammer
     should be false after triggering
 
 Finished in 0.00705 seconds
 14 examples, 0 failures

うまくいってるようだね


さて最後に
ロシアンルーレットに必須の機能とも言うべき
spin_cylinderを実装しよう

  context "spin_cylinder" do
    it "should rotate the cylinder line randomly(fail sometimes)" do
      @rev.set_cartridge
      before_spin = @rev.cylinder.dup
      @rev.spin_cylinder
      @rev.cylinder.should_not == before_spin
    end
  end

あまり良いテストじゃないよね..
こういうランダムな結果をテストするのは
どうすればいいんだろうね
わからないから今回はこれでよしとして先に進むよ


さてもちろんテストに通らないので
spin_cylinderを実装するよ

  def spin_cylinder
    @cylinder.rotate!(rand CYLINDER_SIZE)
  end


さあテストだよ

 % rspec -fs -c gun_spec.rb 
 
 Gun
   chamber
     should be empty at default
     should be empty after triggering
  set_cartridge
     should set a cartridge to the chamber
     should be error when the chamber has a cartridge
   trigger
     should return 'Bang!'
     should be nil when the chamber is empty
 Revolver
   set_cartridge
     should set a cartridge to the cylinder pos 1
     should set 3 cartridges to the cylinder pos 1-4
     should be error when it called more than the cylinder rooms
   cylinder
     should rotate for next when cocking
   trigger
     should return 'Bang!'
     should be nil without cocking
     should work sequentially
   hammer
     should be false after triggering
   spin_cylinder
     should rotate the cylinder line randomly(fail sometimes)
 
 Finished in 0.00705 seconds
 15 examples, 0 failures

いいね!


さあこれでようやくRevolverが完成したよ!

ロシアンルーレットで遊ぶ

さあ銃が用意できたから
早速ロシアンルーレットできるか試してみるよ
まずは舞台(russian_roulette.rb)を作ろう

 require "term/ansicolor"
 require_relative "gun"
 
 String.send(:include, Term::ANSIColor)
 
 def russian_roulette(fighters)
   print "--- Welcome to Russian Roulette ---\n".green
   print "Today's fighters are: "
   print fighters.map { |f| f.magenta.underline }.join(", ")
   print "\nLet's go!\n\n"
 
   rev = Revolver.new
   rev.set_cartridge
   sleep 2
 
   fighters.shuffle.cycle do |fighter|
     print "#{fighter}'s turn:\n".cyan
     rev.cocking
     rev.spin_cylinder
     sleep 2
     unless result = rev.trigger
       print "  Nothing happened..\n\n".yellow
       sleep 1
     else
       print "  #{result}  ".yellow.on_red.blink
       print "  #{fighter} is dead.\n".blue
       print "\n--- Game is over ---\n".green
       exit
     end
   end
 end
 
 russian_roulette(ARGV)

term-ansicolorで色を付けたから
gem install term-ansicolorしてね


さあ試してみるよ..

 % ruby russian_roulette.rb Charlie Fox Henry
 --- Welcome to Russian Roulette ---
 Today's fighters are: Charlie, Fox, Henry
 Let's go!
 
 Fox's turn:
   Nothing happened..
 
 Charlie's turn:
   Nothing happened..
 
 Henry's turn:
   Nothing happened..
 
 Fox's turn:
   Nothing happened..
 
 Charlie's turn:
   Bang!    Charlie is dead.
 
 --- Game is over ---
 



あー
チャーリーが死んじゃった..


じゃあ今度は君と僕とで勝負しようよ!
まずは僕から

 % ruby russian_roulette.rb me you
 --- Welcome to Russian Roulette ---
 Today's fighters are: you, me
 Let's go!
 
 me's turn:
   Nothing happened..

ふー セーフだったよ!
じゃあ今度は君

 you's turn:
   Nothing happened..


おー よかったね!
次は僕の番

 me's turn:
   Bang!    me is dead.
 
 --- Game is over ---