Skip to content

Instantly share code, notes, and snippets.

@anicholson
Created January 19, 2015 06:50
Show Gist options
  • Save anicholson/f0164c3948c96311cf40 to your computer and use it in GitHub Desktop.
Save anicholson/f0164c3948c96311cf40 to your computer and use it in GitHub Desktop.
Bowling Kata
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
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment