これからはじめるTDD

tatsu-zine.com

第2章を終えたところから、これは正解を見ずに書いた方が勉強になるかも、と感じ、
テストの方だけを見て、実装の方はあまり見ずにやってみた。
また、minitest は使ったことなかったので、RSpec + Guard でテストを回していた。

第3章の時点で、これはFrameクラス有った方が良いなと思って追加したので、
最終結果の変数名とか違う部分が出てきた。

最終結果で参考にして直した部分は 3点。

  1. Frame を 最初に MAX フレーム数配列として 10 作っていたが、current を追加していく方式に変更
  2. destribute_bonus のほうは、select を使う発想がなく、ストライクでも MAX 2つ前までなので、2つ前まで見る、としていたのを修正
  3. (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