Kata: Bowling Description Accurately model a game of ten-pin bowling. Inspired by a mini-Code Retreat I attended years ago, and the front page of rspec.info. Tests require 'bowling' describe Bowling do describe '#hit' do it { is_expected.to respond_to :hit } it 'rejects non-numeric args' do expect { subject.hit('aha') }.to raise_exception end it 'rejects negative numbers' do expect { subject.hit(-1) }.to raise_exception end it 'rejects higher scores than there are pins to hit' do expect { subject.hit(11) }.to raise_exception end end describe '#score' do it { is_expected.to respond_to :score } it 'scores 0 for an all-gutter game' do 20.times { subject.hit 0 } expect(subject.score).to eq(0) end it 'scores the sum of pins for a game with no strikes or spares' do attempts = (1..20).map {|_| rand(5) } attempts.each {|attempt| subject.hit attempt } puts attempts expect(subject.score).to eq(attempts.reduce(0, &:+)) end it 'adds the next score on when a spare is scored' do subject.hit 6 subject.hit 4 # spare subject.hit 8 subject.hit 0 expect(subject.score).to eq(26) end it 'only recognises scores on frame boundaries' do subject.hit 6 subject.hit 3 subject.hit 7 # not a spare subject.hit 1 expect(subject.score).to eq(17) end it 'adds the next two scores on when a strike is scored' do subject.hit 10 # Strike! subject.hit 3 subject.hit 6 expect(subject.score).to eq(28) end it 'handles turkeys' do 3.times { subject.hit 10 } subject.hit 0 subject.hit 9 expect(subject.score).to eq(78) end it 'handles strikes after spares' do subject.hit 7 subject.hit 3 # spare subject.hit 10 # Strike! expect(subject.score).to eq(30) end it 'handles spares after strikes' do subject.hit 10 # Strike! subject.hit 5 subject.hit 5 # spare expect(subject.score).to eq(30) end it 'scores 270 for a 9-frame perfect' do 9.times { subject.hit 10 } subject.hit 0 subject.hit 0 expect(subject.score).to eq(240) end it 'scores 300 for a perfect game' do 12.times { subject.hit 10 } expect(subject.score).to eq(300) end end end Code class Bowling class EmptyFrame def pin_total 0 end def scores [0,0] end def strike?; false; end def spare?; false; end def last?; true; end end class Frame def initialize(last = false) @scores = [] @last = last end def last? @last end def full? if last? if (scores.first == 10) || (scores.count >= 2 && (scores[0] + scores[1] == 10)) scores.count == 3 else scores.count == 2 end else strike? || spare? || (@scores.count == 2) end end def record(pins) @scores << pins end def spare? @scores.count == 2 && pin_total == 10 end def strike? @scores.count == 1 && pin_total == 10 end def pin_total @scores.reduce(0, &:+) end def scores @scores end def next @next_frame end def next=(frame) @next_frame = frame end end def initialize @score_history = [] @frames = [] @current_frame = Frame.new end def hit(pins) raise ArgumentError unless pins.is_a? Integer raise ArgumentError unless pins >= 0 && pins <= 10 @score_history << pins @current_frame.record(pins) if @current_frame.full? @frames << @current_frame @current_frame.next = Frame.new(last_frame?) @current_frame = @current_frame.next end end def last_frame? @frames.count == 9 end def score score = 0 @frames.each_with_index do |frame, idx| next_frame = @frames[idx + 1] || EmptyFrame.new the_frame_after_that = @frames[idx + 2] || EmptyFrame.new next_score = if frame.strike? if frame.last? frame.pin_total else if next_frame.strike? 20 + the_frame_after_that.scores.first elsif next_frame.last? 10 + next_frame.scores[0] + next_frame.scores[1] else 10 + next_frame.pin_total end end elsif frame.spare? if frame.last? frame.pin_total + frame.scores.last else 10 + next_frame.scores.first end else frame.pin_total end score += next_score puts "Frame #{idx}: #{next_score} (#{score})" end score end private end Jan 19th, 2015 kata