This article will be my attempt at the Bowling Kata. This is a step up in difficulty from most of the other katas because there is state involved, but the process should be the same.

I wanted to make it more of a genuine challenge by not actually seeing the kata being performed. After completing the kata I have added commentary to each step afterwards.

I’m also going to add some more flair to it, treating it more like a complete solution rather than just a golden path. I believe this also trains you to think in terms of how the software will be used rather than just testing if you can solve the problem.

Python is my language is choice with pytest for the unit tests.

Understanding the Problem

Before I start any coding, including a kata, I take a few moments to really understand what’s required. The best way to know if you understand something is to be able to reduce it into more specific requirements. If you can’t produce these requirements it’s likely the code you end up producing will be unnecessarily complicated and probably take longer than it needs to.

Here are the scoring rules (at least as I understand them) reduced to something more like a specification:

  • A game is made up of 10 frames.
  • Each frame has 10 pins and is reset each frame.
  • A frame can be one or two rolls, depending on:
    • A strike (knocking all 10 pins down on the first roll of the frame) ends the frame.
    • Any number below 10 on the first roll allows a second roll.
  • The final (or running) score is the sum of all frames where:
    • A strike is 10 points + the next two rolls.
    • A spare is 10 points + the next roll.
    • Anything else is 1 point per pin cleared in each roll.
  • If a spare is thrown in the final (10th) frame the player is awarded one more roll. This roll awards the number of pins cleared onto their score (0 to 10 bonus points).

Understanding the Solution

I’m excited to get right in there, but there is another vital step. After understanding the problem I need to understand the solution. By this I mean we should start asking questions against the derived (or provided) requirements like:

  • Do the requirements cover all the obvious cases?
  • What about the edge cases?
  • Is there anything potentially missing from the requirements?
  • What are the error conditions?
  • How should we handle error conditions?

Some of the answers will look like:

  • An error condition is anything that steps outside of the normal rules (for example, bowling 50 rolls).
  • An error should throw an exception with an understandable message of exactly why the score could not be calculated.
  • Any value out of bounds (rolling a negative number) is an error condition.
  • The score should be rolling (pun intended) since a final score at the end isn’t very useful to a player that wants to know their progress.

Implementing the Solution

Now we can start the TDD cycles. Hopefully I have a greater understanding and more confidence that the tests and the solution I produce are more robust.

As I have demonstrated in a previous article I encourage error conditions to be treated as regular cycles when they become appropriate, ideally as early as possible.

def test_the_score_starts_at_zero():
game = Game()
assert game.score() == 0

class Game:
def score(self):
return 0

Starting simple, a score still exists if no ball have been rolled yet.

def test_rolling_less_than_zero_is_not_allowed():
game = Game()
with pytest.raises(ValueError) as e:
game.roll(-1)
assert e.value.message == 'Cannot roll less than zero pins.’

#class Game:
def roll(self, pins):
raise ValueError('Cannot roll less than zero pins.’)

This is the first error condition and an obvious one.

def test_rolling_zero_is_allowed():
game = Game()
game.roll(0)
assert game.score() == 0

#class Game:
#def roll(self, pins):
if pins != 0:
raise ValueError('Cannot roll less than zero pins.’)

Should I be splitting the path so early? Would it be better to have the score() return the roll() instead of handling the error condition in the previous test requiring me to branch now?

def test_rolling_greater_than_ten_is_not_allowed():
game = Game()
with pytest.raises(ValueError) as e:
game.roll(11)
assert e.value.message == 'Cannot roll more than ten pins.’

#class Game:
#def roll(self, pins):
if pins < 0:
raise ValueError('Cannot roll less than zero pins.')
if pins > 10:
raise ValueError('Cannot roll more than ten pins.’)

Another easy error condition to pick up.

def test_a_single_roll():
game = Game()
game.roll(5)
assert game.score() == 5

#class Game:
def __init__(self):
self.total = 0

#def roll(self, pins):
# raise ValueError('Cannot roll more than ten pins.')

self.total = pins

def score(self):
return self.total

The first time we are able to generalise the constant to a variable. Looking back now I feel this should have been the second test before the error conditions.

def test_two_rolls_without_spare():
game = Game()
game.roll(5)
game.roll(3)
assert game.score() == 8

#class Game:
#def roll(self, pins):
self.total += pins

Operator transformation, feels good. :)

def test_more_than_ten_pins_in_a_frame():
game = Game()
game.roll(5)
with pytest.raises(ValueError) as e:
game.roll(6)
assert e.value.message == 'You can only clear 10 pins in a single frame.’

#class Game:
#def roll(self, pins):
#self.total += pins

if self.total > 10:
raise ValueError('You can only clear 10 pins in a single frame.’)

def test_three_rolls():
game = Game()
game.roll(5)
game.roll(3)
game.roll(7)
assert game.score() == 15

#class Game:
#def __init__(self):
#self.total = 0
self.current_frame = []

#def roll(self, pins):
#self.total += pins
self.current_frame.append(pins)

if sum(self.current_frame) > 10:
# raise ValueError('You can only clear 10 pins in a single frame.')

if len(self.current_frame) == 2:
self.current_frame = []

After writing this test I should have done some refactoring to remove all that validation away from roll() which as you will see gets far too complicated.

def test_spare_includes_bonus_roll():
game = Game()
game.roll(5)
game.roll(5)
game.roll(7)
assert game.score() == 24

#class Game:
#def roll(self, pins):
#if sum(self.current_frame) > 10:
# raise ValueError('You can only clear 10 pins in a single frame.')

if sum(self.current_frame) == 10:
self.total += 7

#if len(self.current_frame) == 2:
# self.current_frame = []

Will use a constant (7) so that I can write a more general case with a variable next.

def test_spare_may_have_no_bonus():
game = Game()
game.roll(5)
game.roll(5)
game.roll(0)
assert game.score() == 10

#class Game:
#def __init__(self):
#self.total = 0
#self.current_frame = []
self.bonus = False

#def roll(self, pins):
#if sum(self.current_frame) > 10:
# raise ValueError('You can only clear 10 pins in a single frame.')

if self.bonus:
self.total += pins

if sum(self.current_frame) == 10:
self.bonus = True

#if len(self.current_frame) == 2:
# self.current_frame = []

I'm able to track the bonus for the spare ahead of time instead of calculating it afterwards . Calculating it after feels like something I'm still a few tests away from so this is a good middle ground.

def test_strike_includes_first_bonus_roll():
game = Game()
game.roll(10)
game.roll(7)
assert game.score() == 24

#class Game:
#def __init__(self):
self.frames = [[]]

#def roll(self, pins):
#if pins > 10:
# raise ValueError('Cannot roll more than ten pins.')

self.frames[-1].append(pins)

if sum(self.frames[-1]) > 10:
raise ValueError('You can only clear 10 pins in a single frame.')

if len(self.frames[-1]) == 2 or pins == 10:
self.frames.append([])

def score(self):
total = 0

for i in xrange(0, len(self.frames)):
frame_total = sum(self.frames[i])

if frame_total == 10:
total += self.frames[i + 1][0]

total += frame_total

return total

Or not... I have to refactor now to start organising rolls into their appropriate frames and begin calculating the score properly when requested instead of just updating state. This is a big refactoring.

def test_spare_missing_next_roll():
game = Game()
game.roll(5)
game.roll(5)
assert game.score() == 10

#class Game:
#def score(self):
#total = 0
total_frames = len(self.frames)

#for i in xrange(0, total_frames):
#frame_total = sum(self.frames[i])

#if frame_total == 10:
try:
total += self.frames[i + 1][0]
except IndexError:
pass

#total += frame_total

#return total

Using a try block to handle the error of the next shot not existing is more work than simply using an if statement, this is what I should have used. Perhaps I was just too influenced by Python's EAFP (Easier to Ask Forgiveness instead of Permission) mantra.

def test_strike_includes_second_bonus_roll():
game = Game()
game.roll(10)
game.roll(7)
game.roll(2)
assert game.score() == 28

#class Game:
#def score(self):
#for i in xrange(0, total_frames):
#frame_total = sum(self.frames[i])

try:
if self.frames[i][0] == 10:
total += self.frames[i + 1][0]
total += self.frames[i + 1][1]
elif frame_total == 10:
total += self.frames[i + 1][0]
except IndexError:
pass

#total += frame_total

#return total

Now the try block becomes useful because I'm adding the score to any bonuses with exclusively adding all or none of the bonuses.

def test_strike_could_be_followed_by_another_strike():
game = Game()
game.roll(10)
game.roll(10)
game.roll(2)
assert game.score() == 36

#class Game:
#def score(self):
#for i in xrange(0, total_frames):
#frame_total = sum(self.frames[i])

try:
if self.frames[i][0] == 10:
total += self.frames[i + 1][0]

if len(self.frames[i + 1]) == 2:
total += self.frames[i + 1][1]
else:
total += self.frames[i + 2][0]

elif frame_total == 10:
total += self.frames[i + 1][0]
except IndexError:
pass

#total += frame_total

#return total

More complicated bonuses are starting to appear. Once again I need to be putting more into refactoring now because my brain is going to explode really soon...

def test_too_many_rolls():
game = Game()
for roll in xrange(20):
game.roll(3)

with pytest.raises(ValueError) as e:
game.roll(5)
assert e.value.message == 'Cannot bowl more than 10 frames.’

#class Game:
#def roll(self, pins):
#if sum(self.frames[-1]) > 10:
# raise ValueError('You can only clear 10 pins in a single frame.')

if len(self.frames) > 10 and sum(self.frames[9]) < 10:
raise ValueError('Cannot bowl more than 10 frames.')

#if len(self.frames[-1]) == 2 or pins == 10:
# self.frames.append([])

This one was OK, but I didn't see the next one coming...

def test_last_frame_spare_allows_bonus_roll():
game = Game()
for i in xrange(18):
game.roll(1)
game.roll(8)
game.roll(2)
game.roll(3)

assert game.score() == 34

This was the first time I really had to stop. Honestly, I took a few guesses to get it right which is not only a terrible approach, but if I can't even understand my own code what hope does anyone else have? :(

Unfortunately I did not paste the change for this one, but rather started heavily refactoring. Here is the final solution:

class Game:
def __init__(self):
self.frames = [[]]
self.allow_bonus_roll = False

@property
def last_frame(self):
return self.frames[-1]

@property
def total_frames(self):
return len(self.frames)

def validate_roll(self, pins):
if pins < 0:
raise ValueError('Cannot roll less than zero pins.')

if pins > 10:
raise ValueError('Cannot roll more than ten pins.')

if sum(self.last_frame) > 10:
raise ValueError('You can only clear 10 pins in a single frame.')

if self.total_frames > 10 and sum(self.frames[9]) < 10:
raise ValueError('Cannot bowl more than 10 frames.')

def roll(self, pins):
self.last_frame.append(pins)
self.validate_roll(pins)

if len(self.last_frame) == 2 or pins == 10:
self.frames.append([])

def frame_is_a_strike(self, frame):
return frame == [10]

def score_for_frame_with_bonuses(self, i):
total = sum(self.frames[i])

try:
if self.frame_is_a_strike(self.frames[i]):
total += self.frames[i + 1][0]

if len(self.frames[i + 1]) == 2:
total += self.frames[i + 1][1]
else:
total += self.frames[i + 2][0]

elif total == 10:
total += self.frames[i + 1][0]
except IndexError:
pass

return total

def score(self):
total = 0

for i in xrange(self.total_frames):
total += self.score_for_frame_with_bonuses(i)

return total

def test_perfect_game():
game = Game()
for i in xrange(11):
game.roll(10)

assert game.score() == 300

This test didn't require a code change but I was kind of the ultimate validation for me that the scoring require to calculate a perfect game works.

You can find the full solution in the gist.

What I Learned aka Need to Learn

As I mentioned above I wanted this to be an honest look at myself and I can see I made many faults.

  1. It is clear that I waited far to long before performing the refactoring cycles. This may be because I was too eager to get to the next test or I though the refactoring wasn't worth it. Either way I need to slow down and refactor more often.
  2. As I got closer to the final tests I start to think how different it would be for a bottom-up instead of a top-down approach. What I mean by this is if I split the requirements up into more separated responsibilities like a Frame class with it's own tests so that the Game does not need to have so much knowledge about how a strike, spare etc work. I'd like to try this kata again with this approach.
  3. There was at least one case where I initially used a more general operator like > instead of a specific case first (like ==). This would have created at least a couple more tests and I should have done this.
  4. Unfortunately I didn't realise until it was too late that I haven't keep all the full versions of each test cycle (usually only the code that changed). This makes it difficult to clearly see the changes. In the future I will use a gist with a version for each test so that you can clear see each version and a pretty diff between them.
  5. Overall it was more challenging than I expected and the last couple of tests cycles were difficult to get my head around. I had to try more complex transformations than necessary to get the test to pass and I felt like this means I was missing test in-between but I could not figure out what they were. Perhaps I took the wrong direction near the end?
  6. I feel proud for completing it regardless of the faults and am excited to try again another way once my mind has forgotten the previous answers enough that I can produce new test cycles.