これからはじめるTDD
第2章を終えたところから、これは正解を見ずに書いた方が勉強になるかも、と感じ、
テストの方だけを見て、実装の方はあまり見ずにやってみた。
また、minitest は使ったことなかったので、RSpec + Guard でテストを回していた。
第3章の時点で、これはFrameクラス有った方が良いなと思って追加したので、
最終結果の変数名とか違う部分が出てきた。
最終結果で参考にして直した部分は 3点。
- Frame を 最初に MAX フレーム数配列として 10 作っていたが、current を追加していく方式に変更
- destribute_bonus のほうは、select を使う発想がなく、ストライクでも MAX 2つ前までなので、2つ前まで見る、としていたのを修正
- (0..10).cover?(pins) という書き方
他に苦労した点とは、ボーリングのルールを知らない、という点だった。
コードを見て貰う機会がないので、jnchito さんに添削してもらいたいところ。
bowling_game.rb
require_relative "frame.rb" class BowlingGame def initialize @frame_status = [ Frame.new ] end def frame_score(frame_no) @frame_status[frame_no - 1].score end def record_shot(pins) @frame_status.last.add(pins) add_bonus(pins) return change_frame if @frame_status.last.finished? end def score @frame_status.inject(0) { |sum, frame| sum += frame.score } end private def add_bonus(pins) @frame_status[0..-2].select(&:need_bonus?).each do |frame| frame.add_bonus(pins) end end def change_frame @frame_status << Frame.new end end
bowling_game_spec.rb
describe BowlingGame do describe "#record_short" do before do @game = BowlingGame.new end subject do throw_count.times.each { @game.record_shot(pins) } @game.score end context "全ての投球がガターの場合" do let(:throw_count) { 20 } let(:pins) { 0 } it { expect(subject).to eq 0 } end context "全ての投球で1ピンだけ倒した" do let(:throw_count) { 20 } let(:pins) { 1 } it { expect(subject).to eq 20 } end context "スペアをとると次の投球のピン数を加算" do before do [3, 7, 4].each { |n| @game.record_shot(n) } end let(:throw_count) { 17 } let(:pins) { 0 } it { expect(subject).to eq 18 } it "一フレームの得点が加算されていること" do subject expect(@game.frame_score(1)).to eq 14 end end context "直前の投球と合計が10ピンでもフレーム違いはspareではない" do before do [2, 5, 5, 1].each { |n| @game.record_shot(n) } end let(:throw_count) { 16 } let(:pins) { 0 } it { expect(subject).to eq 13 } end context "ストライクをとると次の2投分のピン数を加算" do before do [10, 3, 3, 1].each { |n| @game.record_shot(n) } end let(:throw_count) { 15 } let(:pins) { 0 } it { expect(subject).to eq 23 } it "一フレームの得点が加算されていること" do subject expect(@game.frame_score(1)).to eq 16 end end context "連続ストライクすなわちダブル" do before do [10, 10, 3, 1].each { |n| @game.record_shot(n) } end let(:throw_count) { 14 } let(:pins) { 0 } it { expect(subject).to eq 41 } it "一フレームの得点が加算されていること" do subject expect(@game.frame_score(1)).to eq 23 end it "二フレームの得点が加算されていること" do subject expect(@game.frame_score(2)).to eq 14 end end context "3連続ストライクすなわちターキー" do before do [10, 10, 10, 3, 1].each { |n| @game.record_shot(n) } end let(:throw_count) { 12 } let(:pins) { 0 } it { expect(subject).to eq 71 } end context "ストライク後のスペア" do before do [10, 5, 5, 3].each { |n| @game.record_shot(n) } end let(:throw_count) { 15 } let(:pins) { 0 } it { expect(subject).to eq 36 } end context "ダブル後のスペア" do before do [10, 10, 5, 5, 3].each { |n| @game.record_shot(n) } end let(:throw_count) { 13 } let(:pins) { 0 } it { expect(subject).to eq 61 } end context "全ての投球が1ピンの場合" do let(:throw_count) { 20 } let(:pins) { 1 } it "全フレーム2点であること" do subject 10.times.with_index(1) do |_, i| expect(@game.frame_score(i)).to eq 2 end end end end end
frame.rb
class Frame PITCH_MAX = 2 SPARE_POWER = 1 STRIKE_POWER = 2 MAX_PINS = 10 def initialize @pitches = [] @bonus_count = 0 @bonus = 0 end def add(pins) check_pins(pins) @pitches << pins return @bonus_count = STRIKE_POWER if strike? @bonus_count = SPARE_POWER if spare? end def add_bonus(pins) return unless need_bonus? @bonus += pins @bonus_count -= 1 end def finished? pitch_score == Frame::MAX_PINS || @pitches.count == PITCH_MAX end def need_bonus? @bonus_count > 0 end def pitch_score @pitches.count == 0 ? 0 : @pitches.inject(&:+) end def score pitch_score + @bonus end def spare? @pitches.count == PITCH_MAX && @pitches.inject(&:+) == MAX_PINS end def strike? @pitches.count == 1 && @pitches.first == MAX_PINS end private def check_pins(pins) unless (0..10).cover?(pins) && pitch_score + pins <= MAX_PINS raise ArgumentError.new("bad num of pins: #{pins}") end end end
frame_spec.rb
describe Frame do before do @frame = Frame.new end describe "#add" do context "一投分を追加した場合" do it "一投分が保存されていること" do pins = 6 @frame.add(pins) expect(@frame.instance_variable_get("@pitches")).to match_array([pins]) end end context "二投分を追加した場合" do it "二投分が保存されていること" do pins_arr = [7, 2] pins_arr.each { |pins| @frame.add(pins) } expect( @frame.instance_variable_get("@pitches") ).to match_array(pins_arr) end end context "10ピン倒した状態になった場合" do it "スペアポイントが設定されていること" do pins_arr = [4, 6] pins_arr.each { |pins| @frame.add(pins) } expect( @frame.instance_variable_get("@bonus_count") ).to eq Frame::SPARE_POWER end end shared_examples "Raise ArgumentError" do it "ArgumentError 例外が発生すること" do expect { @frame.add(pins) }.to raise_error(ArgumentError) end end context "ピン数が0未満の場合" do let(:pins) { -1 } include_examples "Raise ArgumentError" end context "ピン数が11以上の場合" do let(:pins) { 11 } include_examples "Raise ArgumentError" end context "フレームの合計ピン数が11以上の場合" do before do @frame.add(5) end let(:pins) { 6 } include_examples "Raise ArgumentError" end end describe "#add_bonus" do subject { @frame.add_bonus(pins) } let(:pins) { 7 } context "ボーナスカウントがない場合" do it "ボーナスポイントは 0 のままであること" do subject expect(@frame.instance_variable_get("@bonus")).to eq 0 end end context "スペア分のカウントがある場合" do before do @frame.instance_variable_set("@bonus_count", Frame::SPARE_POWER) end it "ボーナスポイントがつくこと" do subject expect(@frame.instance_variable_get("@bonus")).to eq pins end it "ボーナスカウントが減ること" do subject expect(@frame.instance_variable_get("@bonus_count")).to eq 0 end end end describe "#finished?" do subject { @frame.finished? } context "一投終えた場合" do before do @frame.instance_variable_set("@pitches", [2]) end it { expect(subject).to be_falsey } end context "二投終えた場合" do before do @frame.instance_variable_set("@pitches", [1, 2]) end it { expect(subject).to be_truthy } end context "一投で10ピン倒した場合" do before do @frame.instance_variable_set("@pitches", [10]) end it { expect(subject).to be_truthy } end end describe "#need_bonus?" do subject { @frame.need_bonus? } context "オープンフレームの場合" do before do [5, 3].each { |pins| @frame.add(pins) } end it "ボーナスは不要" do expect(subject).to be_falsey end end context "スペアのボーナスは1投分で完了" do before do [5, 5].each { |pins| @frame.add(pins) } end it "ボーナス付与前はボーナスが必要" do expect(subject).to be_truthy end end end describe "#score" do subject { @frame.score } context "投球が一回もない場合" do it { expect(subject).to eq 0 } end context "投球が二回あった場合" do before do @frame.instance_variable_set("@pitches", [2, 3]) end it "投球の合計が返ること" do expect(subject).to eq 5 end end context "ボーナスポイントがある場合" do before do @frame.instance_variable_set("@pitches", [3, 4]) @frame.instance_variable_set("@bonus", 9) end it "投球の合計 + ボーナスポイントが返ること" do expect(subject).to eq 16 end end end describe "#spare?" do subject { @frame.spare? } context "投球してない場合" do it { expect(subject).to be_falsey } end context "一投の場合" do before do @frame.instance_variable_set("@pitches", [5]) end it { expect(subject).to be_falsey } end context "一投ので10ピン倒した場合" do before do @frame.instance_variable_set("@pitches", [10]) end it { expect(subject).to be_falsey } end context "二投の場合" do before do @frame.instance_variable_set("@pitches", [7, 2]) end it { expect(subject).to be_falsey } end context "二投で10ピン倒した場合" do before do @frame.instance_variable_set("@pitches", [7, 3]) end it { expect(subject).to be_truthy } end end describe "#strike?" do subject { @frame.strike? } context "投球してない場合" do it { expect(subject).to be_falsey } end context "一投の場合" do before do @frame.instance_variable_set("@pitches", [5]) end it { expect(subject).to be_falsey } end context "一投ので10ピン倒した場合" do before do @frame.instance_variable_set("@pitches", [10]) end it { expect(subject).to be_truthy } end context "二投の場合" do before do @frame.instance_variable_set("@pitches", [7, 2]) end it { expect(subject).to be_falsey } end context "二投で10ピン倒した場合" do before do @frame.instance_variable_set("@pitches", [7, 3]) end it { expect(subject).to be_falsey } end end end