■A Game Is Afoot
ついにゲームとして遊ぶためのインタフェースに着手
クラス名はMastermind::Game
このクラスのコンストラクタは、第一引数にInteractorクラスのインスタンス、第二引数にRandomGeneratorクラスのオブジェクトを受け取り、Game#playメソッドにてゲームを開始する。
specs/game_spec.rb
require File.dirname(__FILE__) + '/../mastermind'
describe "ゲームをする時" do
before do
@random = mock("random")
@random.should_receive(:next).exactly(4).times.and_return 0, 0, 0, 0
@interactor = mock("interactor", :null_object => true)
@game = Mastermind::Game.new(@interactor, @random)
end
it "秘密のコードを生成する" do
@game.play
end
it "予想を入力するよう促す" do
@interactor.should_receive(:write).once.with("Enter your guess: ")
@game.play
end
end
@interactorに代入しているmockオブジェクトの生成に:null_object => trueとある。これは定義されていないスタブメソッドが呼ばれた場合、mockオブジェクト自身を返すようにするオプション。つまり、一つ目のexampleで@interactorにwriteメソッドを作ってないけれど、エラーにはならないよということ。これを通すMastermind::Gameクラスは次の様になる。
src/game.rb
module Mastermind
class Game
def initialize(interactor, random_generator)
@interactor = interactor
@random_generator = random_generator
end
def play
@code = Code.generate_using(@random_generator)
@interactor.write("Enter your guess: ")
end
end
end
ここで、playメソッドでコードを初期化して、予想の入力を促しているけど、予想の入力は複数回あるんだから分けた方がいい。なので、入力部分をtake_turnメソッドとして分け、そこで入力を促すようにするようにspecを書き換える。
require File.dirname(__FILE__) + '/../mastermind'
describe "ゲームをする時" do
before do
@random = mock("random", :null_object => true)
@interactor = mock("interactor", :null_object => true)
@game = Mastermind::Game.new(@interactor, @random)
end
it "秘密のコードを生成する" do
@random.should_receive(:next).exactly(4).times.and_return 0, 0, 0, 0
@game.play
end
it "予想を入力するよう促す" do
@interactor.should_receive(:write).once.with("Enter your guess: ")
@game.take_turn(1, Mastermind::Code.new(:white, :white, :white, :white))
end
end
Mastermind::Game#playはこんな感じになる。
def play
@code = Code.generate_using(@random_generator)
take_turn(1,@code)
end
次は実際にユーザーからの入力があった場合のexampleを書く。入力を受け付けるメソッドはInteractor#readline。それぞれの色の頭文字を4つ続けて入力してもらう。全部白ならwwww
it "ユーザーの予想入力を受け付ける" do
@interactor.should_receive(:readline).once.and_return 'wwww'
@game.take_turn(1, Mastermind::Code.new(:white, :white, :white, :white))
end
Mastermind::Game#take_turnでは、@interactor.readlineを一回呼んでやるようにする。
def take_turn(turn_number, secret_code)
@interactor.write("Enter your guess: ")
@guess_string = @interactor.readline
end
驚くべきは、ここまで@interactorに入るであろう、Interactorクラスを全く書いてない点。
ここまでテストが分離できると、モジュールごとの分担作業もやりやすそうだ。
そして予想があたったら、あんたの勝ちよって表示してあげないとね
it "一回目の予想で全部当たった" do
@random.should_receive(:next).exactly(4).times.and_return 0
@interactor.should_receive(:readline).once.and_return 'bbbb'
@interactor.should_receive(:writeline).once.with('You won in 1 turn.')
@game.play
end
単にテスト通すだけなら、playメソッドの中で、@interactor.writeline('You won in 1 turn.')を実行してあげればいいだけ。
でもこれじゃあ寂しいので、take_turnの中で入力をCodeオブジェクトに変換して、markメソッドで評価したい。ユーザーの入力をCodeオブジェクトにするのはCode#from_stringメソッド。
Mastermind::Game#take_turn
def take_turn(turn_number, secret_code)
@interactor.write("Enter your guess: ")
guess_string = @interactor.readline
guess = Code.from_string(guess_string)
@score = secret_code.mark(guess)
end
Codeクラスには適当なCodeオブジェクトを返すfrom_stringメソッドを定義してあげたところで、ここまでのspecは全て通った。じゃあ、from_stringをちゃんと実装しましょうと、specから書く。
specs/code_creation_spec.rb
require File.dirname(__FILE__) + '/../mastermind'
describe "文字列からコードを生成する時" do
it "wwwwが全部白のピンとい状態のCodeを返す" do
code = Mastermind::Code.new(:white, :white, :white, :white)
code.mark(Mastermind::Code.from_string('wwww')).should be_win
end
end
まずは単にMastermind::Code.new(:white, :white, :white, :white)が帰ってくるように、Mastermind::Code#from_stringを実装して、とりあえずテストを通す。その後、ちゃんと実装。結果こんなんなります
Mastermind::Code
@@colour_map = {
'b'=>:black,
'c'=>:cyan,
'g'=>:green,
'r'=>:red,
'y'=>:yellow,
'w'=>:white
}
def self.from_string(code_string)
pegs = code_string.split(//).collect {|each| @@colour_map[each]}
new(*pegs)
end
これでテストが通ってることを確認したら、全部白の時だけじゃなくて、いろんな色が混ざった場合のexampleもいくつか書いておきましょう。
■敗北を知りたい
specs/game_spec.rbに新しいexampleを追加する。予想があたった時に勝ちなのはいいが、何をもって負けとするのか。ここでは10回の予想が全部外れた場合に負けとする。
it "10回予想してもあたらなかったら負け" do
@random.should_receive(:next).exactly(4).times.and_return 0
@interactor.should_receive(:readline).any_number_of_times.and_return 'wwww'
@interactor.should_receive(:writeline).once.with('You lose after 10 turns.')
@game.play
end
ここでもplayメソッドの中で@interactor.writeline('You lose after 10 turns.')を呼んであげればいいんだけど、ちゃんと実装したい気分。
Mastermind::Game
def play
@code = Code.generate_using(@random_generator)
turns = 0
while turns < 10 && (not won?)
turns += 1
take_turn(turns, @code)
end
@interactor.writeline('You lose after 10 turns.') if turns == 10
@interactor.writeline("You won in 1 turn.") if won?
end
def won?
(not @score.nil?) && @score.win?
end
ふぅ、結構長いね、このチュートリアル。でもあと少し。毎ターンごとにヒットの数とブローの数を表示してあげないと成立しない。
specs/game_spec.rb
it "全部がヒット(black)の時の結果表示" do
@interactor.should_receive(:readline).once.and_return 'wwww'
@interactor.should_receive(:writeline).once.with 'Score: 4 black'
@game.take_turn(1, Mastermind::Code.new(:white, :white, :white, :white))
end
it "全部がブロー(white)の時の結果表示" do
@interactor.should_receive(:readline).once.and_return 'cgrb'
@interactor.should_receive(:writeline).once.with 'Score: 4 white'
@game.take_turn(1, Mastermind::Code.new(:black, :cyan, :green, :red))
end
it "blackとwhiteがまじってる時の結果表示" do
@interactor.should_receive(:readline).once.and_return 'byrc'
@interactor.should_receive(:writeline).once.with 'Score: 1 black, 2 white'
@game.take_turn(1, Mastermind::Code.new(:black, :cyan, :green, :red))
end
これを全部通して、これまでmockを使ってきたinteractorを実装しておしまい。
記念すべき1回目は8ターンで正解。偶然に頼りすぎた
これで大体RSpecのこと覚えたかな。まぁ習うより慣れろで実戦投入です。rails2.0でTest::Unitがバグってるらしいですし、明日移行の新規プロジェクトではRAWHIDE.のテストは全部rspecに移行しちゃいます。
押忍