■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に移行しちゃいます。
押忍
※このエントリはZDNETブロガーにより投稿されたものです。朝日インタラクティブ および ZDNET編集部の見解・意向を示すものではありません。