Solving the Bowling Game kata

Statement of the kata

The kata consists in creating a program to calculate the scores of a Bowling game, although to avoid complicating it too much, only the final result is calculated without performing any validations.

A brief remainder of the rules:

  • Each game has 10 frames, each one with 2 rolls.
  • In each turn, the knocked down bowls are counted, and that number is the score
    * 0 points is a gutter
    * If all the pins are knocked down in two rolls, it’s called a spare, and the score of the next roll is added as a bonus
    * If all the pins are knocked down in just one roll, it’s called a strike, and the score of the next two rolls is added as a bonus
  • If a strike or spare are achieved in the last frame, there are extra rolls.

Language and approach

To do this kata, I’ve chose Ruby and RSpec. You may notice that I have a certain preference towards the *Spec family testing frameworks. The thing is that they have been designed with TDD in mind, considering the tests as specifications, which helps a lot to escape the mindset of thinking about the tests as QA.

Having said that, there’s no problem in using any other testing framework, such as those from the *Unit family.

On the other hand, we’ll use object oriented programming.

Starting the game

At this point, the first test should be enough to force us to define and instantiate the class:

1 require 'rspec'
2 
3 RSpec.describe 'A Bowling Game' do
4   it 'should start a new game' do
5     BowlingGame.new
6   end
7 end

The test will fail, forcing us to write the minimum production code necessary to make it pass.

1 class BowlingGame
2 
3 end

And once we’ve made the test pass, we move the class to its own file, and make the test require it:

1 require 'rspec'
2 require_relative '../src/bowling_game'
3 
4 RSpec.describe 'A Bowling Game' do
5   it 'should start a new game' do
6     BowlingGame.new
7   end
8 end

We’re ready for the next test.

Let’s throw the ball

For our BowlingGame to be useful, we’ll need at least two things:

  • A way to indicate the result of a roll, passing the number of knocked down pins, which would be a command A command results in an effect in the state of an object, but doesn’t return anything. We’ll need an alternative way to observe that effect.
  • A way to obtain the score at a given moment, which would be a query. A query returns an answer, so we can verify that it’s the one that we expect.

You may be wondering: which of the two should we tackle first?

There is not a fixed rule, but a of seeing it could be the following:

Query methods return a result, so their effect can be tested, but we have to make sure that the returned responses won’t make it harder for us to create new failing tests.

On the other hand, command methods are easy to introduce with a minimum amount of code without having to worry about their effect in future tests, except from making sure that the parameters that they receive are valid.

So, we’re going to start by introducing a method to throw the ball, which simply expects to receive the number of knocked down pins, which can be 0. But to force that, we must first write a test:

 1 require 'rspec'
 2 require_relative '../src/bowling_game'
 3 
 4 RSpec.describe 'A Bowling Game' do
 5   it 'should start a new game' do
 6     game = BowlingGame.new
 7   end
 8 
 9   it 'should roll a ball knocking down 0 pins' do
10     game = BowlingGame.new
11     game.roll 0
12   end
13 end

And the minimum necessary code to make the test pass is, simply, the definition of the method. Basically, we can now communicate to BowlingGame that we’ve thrown the ball.

1 class BowlingGame
2   def roll(pins_down)
3 
4   end
5 end

Time to refactor

In this kata, we’ll pay special attention to the refactoring phase. We have strike find a balance so that certain refactorings don’t condition our chances to make the code evolve. In the same way that premature optimization is a smell, premature over-engineering also is.

The production code doesn’t offer any refactoring opportunity yet, but the tests start showing a pattern. The game object could live as an instance variable, and be initialized in a setup method of the specification or test case. Here, we use before.

 1 require 'rspec'
 2 require_relative '../src/bowling_game'
 3 
 4 RSpec.describe 'A Bowling Game' do
 5 
 6   before do
 7     @game = BowlingGame.new
 8   end
 9 
10   it 'should start a new game' do
11     game = BowlingGame.new
12   end
13 
14   it 'should roll a ball knocking down 0 pins' do
15     @game.roll 0
16   end
17 end

And this makes the first test redundant:

 1 require 'rspec'
 2 require_relative '../src/bowling_game'
 3 
 4 RSpec.describe 'A Bowling Game' do
 5 
 6   before do
 7     @game = BowlingGame.new
 8   end
 9 
10   it 'should roll a ball knocking down 0 pins' do
11     @game.roll 0
12   end
13 end

With this, the specification will be more manageable.

Counting the points

It’s time to introduce a method that lets us check the game scoreboard. We call it from a failing test:

 1 require 'rspec'
 2 require_relative '../src/bowling_game'
 3 
 4 RSpec.describe 'A Bowling Game' do
 5 
 6   before do
 7     @game = BowlingGame.new
 8   end
 9 
10   it 'should roll a ball knocking down 0 pins' do
11     @game.roll 0
12   end
13 
14   it 'should score after a gutter roll' do
15     @game.roll 0
16     expect(@game.score).to eq(0)
17   end
18 end

The test will fail, since the score method doesn’t exist.

1 class BowlingGame
2   def roll(pins_down)
3 
4   end
5 
6   def score
7     
8   end
9 end

And it will keep failing because it has to return 0. The minimum to make it pass is this:

1 class BowlingGame
2   def roll(pins_down)
3 
4   end
5 
6   def score
7     0
8   end
9 end

The world’s worst thrower

Many of the solutions of this kata jump directly to the point where they start to define the behavior of BowlingGame after the 20 rolls. We’ve chosen a path of smaller steps, and we’re going to see what it entails.

Our next test will try to make it possible to obtain a scoreboard after 20 throws. A way to do it is to simulate them, and the simplest simulation would be to consider all them as failed rolls, that is, not a single pin would be knocked down and the final score would be 0.

This seems like a good test to start with:

 1 require 'rspec'
 2 require_relative '../src/bowling_game'
 3 
 4 RSpec.describe 'A Bowling Game' do
 5 
 6   before do
 7     @game = BowlingGame.new
 8   end
 9 
10   it 'should roll a ball knocking down 0 pins' do
11     @game.roll 0
12   end
13 
14   it 'should score after a gutter roll' do
15     @game.roll 0
16     expect(@game.score).to eq(0)
17   end
18 
19   it 'should score a gutter game' do
20     20.times do
21       @game.roll 0
22     end
23     expect(@game.score).to eq(0)
24   end
25 end

But it’s not. We run it, and it passes in the first try.

This test doesn’t force us to introduce any changes in the production code because it doesn’t fail. Ultimately, it’s the same test that we had before. However, in a way it’s a better test, since our objective is to make score return the results after all of the rolls.

Organizing the code

We just remove the previous test for being redundant, as that behavior would already be implicitly contained in the one that we’ve just defined.

 1 require 'rspec'
 2 require_relative '../src/bowling_game'
 3 
 4 RSpec.describe 'A Bowling Game' do
 5 
 6   before do
 7     @game = BowlingGame.new
 8   end
 9 
10   it 'should roll a ball knocking down 0 pins' do
11     @game.roll 0
12   end
13   
14   it 'should score a gutter game' do
15     20.times do
16       @game.roll 0
17     end
18     expect(@game.score).to eq(0)
19   end
20 end

As the test hasn’t required us to write any production code, we need a test that does fail.

Teaching our game to count

We need to expect a result different than zero in score to be forced to implement new production code. From all the possible results of a complete bowling game, maybe the simplest one to test is the case where every throws only knocks down one pin each. This way, we expect the final score to be 20, and there isn’t any chance for extra points or throws to be generated.

 1 require 'rspec'
 2 require_relative '../src/bowling_game'
 3 
 4 RSpec.describe 'A Bowling Game' do
 5 
 6   before do
 7     @game = BowlingGame.new
 8   end
 9 
10   it 'should roll a ball knocking down 0 pins' do
11     @game.roll 0
12   end
13 
14   it 'should score a gutter game' do
15     20.times do
16       @game.roll 0
17     end
18     expect(@game.score).to eq(0)
19   end
20 
21   it 'should score all ones' do
22     20.times do
23       @game.roll 1
24     end
25     expect(@game.score).to eq(20)
26   end
27 end

This test already fails because there’s nothing counting and accumulating the points of each roll. Therefore, we need to set a variable, which initializes as zero and accumulates the results.

But, hold on a second… Aren’t these too many things?

A step back to reach further

Let’s review, to pass the current failing test, we need to:

  • Add a variable to the class to store the scores
  • Initialize it to 0
  • Accumulate the results in it

Those are many things to add in single cycle while having a failing test.

The thing is, actually, we could forget about this test for a moment, and go back to the previous state, when we were still in green. To do so, we comment out the new test so it doesn’t get executed.

 1 require 'rspec'
 2 require_relative '../src/bowling_game'
 3 
 4 RSpec.describe 'A Bowling Game' do
 5 
 6   before do
 7     @game = BowlingGame.new
 8   end
 9 
10   it 'should roll a ball knocking down 0 pins' do
11     @game.roll 0
12   end
13 
14   it 'should score a gutter game' do
15     20.times do
16       @game.roll 0
17     end
18     expect(@game.score).to eq(0)
19   end
20 
21   # it 'should score all ones' do
22   #   20.times do
23   #     @game.roll 1
24   #   end
25   #   expect(@game.score).to eq(20)
26   # end
27 end

And now we proceed to the refactor. We start by changing the constant 0 by a variable:

1 class BowlingGame
2   def roll(pins_down)
3 
4   end
5 
6   def score
7     @score = 0
8   end
9 end

We can improve this code, storing the points that were obtained in the throw in the variable. This code still passes the test and involves a minimal change:

1 class BowlingGame
2   def roll(pins_down)
3     @score = pins_down
4   end
5 
6   def score
7     @score
8   end
9 end

Recovering a cancelled test

Now we do run the fourth test, observing that it fails again:

 1 require 'rspec'
 2 require_relative '../src/bowling_game'
 3 
 4 RSpec.describe 'A Bowling Game' do
 5 
 6   before do
 7     @game = BowlingGame.new
 8   end
 9 
10   it 'should roll a ball knocking down 0 pins' do
11     @game.roll 0
12   end
13 
14   it 'should score a gutter game' do
15     20.times do
16       @game.roll 0
17     end
18     expect(@game.score).to eq(0)
19   end
20 
21   it 'should score all ones' do
22     20.times do
23       @game.roll 1
24     end
25     expect(@game.score).to eq(20)
26   end
27 end

The necessary change in the code is smaller now. We have to initialize the variable in construction, so that each game starts at 0 and then accumulates the points. Note that apart from the constructor, it will be enough to add a + sign.

 1 class BowlingGame
 2 
 3   def initialize
 4     @score = 0
 5   end
 6 
 7   def roll(pins_down)
 8     @score += pins_down
 9   end
10 
11   def score
12     @score
13   end
14 end

Again in green, knowing that we’re already accumulating points.

Getting more comfortable

If we observe the tests, we see that it might be useful to have a method to roll the ball several times with the same result. So we extract it, and of course, we use it:

 1 require 'rspec'
 2 require_relative '../src/bowling_game'
 3 
 4 RSpec.describe 'A Bowling Game' do
 5 
 6   before do
 7     @game = BowlingGame.new
 8   end
 9 
10   it 'should roll a ball knocking down 0 pins' do
11     @game.roll 0
12   end
13 
14   it 'should score a gutter game' do
15     roll_many 20, 0
16     expect(@game.score).to eq(0)
17   end
18 
19   it 'should score all ones' do
20     roll_many 20, 1
21     expect(@game.score).to eq(20)
22   end
23 
24   def roll_many(times, pins_down)
25     times.times do
26       @game.roll pins_down
27     end
28   end
29 end

How to handle a spare

Now that we’re sure that our BowlingGame is able to accumulate the points achieved in each throw, it’s time to keep going. We can start to handle special cases like, for example, how to process a spare, that is, knocking down the ten pins with the two rolls that are in a frame.

So, we write a test to simulate this situation. The simplest would be to imagine that the spare occurs in the first frame, and that the result of the third roll is the bonus. To make things easier, the rest of the rolls in the game are 0, so we don’t introduce any strange scores.

Here’s a possible test:

 1 require 'rspec'
 2 require_relative '../src/bowling_game'
 3 
 4 RSpec.describe 'A Bowling Game' do
 5 
 6   before do
 7     @game = BowlingGame.new
 8   end
 9 
10   it 'should roll a ball knocking down 0 pins' do
11     @game.roll 0
12   end
13 
14   it 'should score a gutter game' do
15     roll_many 20, 0
16     expect(@game.score).to eq(0)
17   end
18 
19   it 'should score all ones' do
20     roll_many 20, 1
21     expect(@game.score).to eq(20)
22   end
23 
24   it 'should score an spare' do
25     @game.roll 5
26     @game.roll 5
27     @game.roll 3
28     roll_many 17, 0
29     expect(@game.score).to eq(16)
30   end
31 
32   def roll_many(times, pins_down)
33     times.times do
34       @game.roll pins_down
35     end
36   end
37 end

The test fails because score returns 13 points when the should be 16. Right now, there isn’t any mechanism that counts the throw after the spare as a bonus.

The problem is that we shouldn’t be counting the points by roll, but rather by frame, in order to know if a frame has resulted in a spare or not, and to act consequently. Moreover, now it’s not enough to simply add the points. Instead, we have to pass the counting responsibility to the score method, so that roll is limited to storing the partials, leaving the logic of calculating points at the frame level to score.

Introducing the concept of frame

First, we go back to the previous test, temporarily cancelling the one that’s falling right now:

 1 require 'rspec'
 2 require_relative '../src/bowling_game'
 3 
 4 RSpec.describe 'A Bowling Game' do
 5 
 6   before do
 7     @game = BowlingGame.new
 8   end
 9 
10   it 'should roll a ball knocking down 0 pins' do
11     @game.roll 0
12   end
13 
14   it 'should score a gutter game' do
15     roll_many 20, 0
16     expect(@game.score).to eq(0)
17   end
18 
19   it 'should score all ones' do
20     roll_many 20, 1
21     expect(@game.score).to eq(20)
22   end
23 
24   # it 'should score an spare' do
25   #   @game.roll 5
26   #   @game.roll 5
27   #   @game.roll 3
28   #   roll_many 17, 0
29   #   expect(@game.score).to eq(16)
30   # end
31 
32   def roll_many(times, pins_down)
33     times.times do
34       @game.roll pins_down
35     end
36   end
37 end

Let’s refactor. In the first place, we change the name of the variable:

 1 class BowlingGame
 2 
 3   def initialize
 4     @rolls = 0
 5   end
 6 
 7   def roll(pins_down)
 8     @rolls += pins_down
 9   end
10 
11   def score
12     @rolls
13   end
14 end

The tests continue to pass. Now we change its meaning, and move the sum to score:

 1 class BowlingGame
 2 
 3   def initialize
 4     @rolls = []
 5   end
 6 
 7   def roll(pins_down)
 8     @rolls.push pins_down
 9   end
10 
11   def score
12     score = 0
13     @rolls.each do |roll|
14       score += roll
15     end
16     score
17   end
18 end

We check that the tests keep passing. It could be a good time to introduce the concept of frame. We know that there’s a maximum of 10 frames.

 1 class BowlingGame
 2 
 3   def initialize
 4     @rolls = []
 5   end
 6 
 7   def roll(pins_down)
 8     @rolls.push pins_down
 9   end
10 
11   def score
12     score = 0
13     roll = 0
14 
15     10.times do
16       frame_score = @rolls[roll] + @rolls[roll+1]
17       score += frame_score
18       roll += 2
19     end
20     score
21   end
22 end

With this change, the tests still pass, and now we have access to the score by frame. It looks like we’re ready to reintroduce the previous test.

Continue handling spare

We reactivate the failing test.

 1 require 'rspec'
 2 require_relative '../src/bowling_game'
 3 
 4 RSpec.describe 'A Bowling Game' do
 5 
 6   before do
 7     @game = BowlingGame.new
 8   end
 9 
10   it 'should roll a ball knocking down 0 pins' do
11     @game.roll 0
12   end
13 
14   it 'should score a gutter game' do
15     roll_many 20, 0
16     expect(@game.score).to eq(0)
17   end
18 
19   it 'should score all ones' do
20     roll_many 20, 1
21     expect(@game.score).to eq(20)
22   end
23 
24   it 'should score an spare' do
25       @game.roll 5
26       @game.roll 5
27       @game.roll 3
28       roll_many 17, 0
29       expect(@game.score).to eq(16)
30     end
31 
32   def roll_many(times, pins_down)
33     times.times do
34       @game.roll pins_down
35     end
36   end
37 end

Now we’re better set to introduce the desired behavior through a pretty small change:

 1 class BowlingGame
 2 
 3   def initialize
 4     @rolls = []
 5   end
 6 
 7   def roll(pins_down)
 8     @rolls.push pins_down
 9   end
10 
11   def score
12     score = 0
13     roll = 0
14 
15     10.times do
16       frame_score = @rolls[roll] + @rolls[roll + 1]
17       if frame_score == 10
18         frame_score += @rolls[roll + 2]
19       end
20       score += frame_score
21       roll += 2
22     end
23     score
24   end
25 end

Adding an if block is sufficient to make the test pass.

Removing magic numbers and other refactorings

At this point, with all tests in green, we can make several improvements to the code. Let’s go bit by bit:

Let’s give meaning to some magic numbers that are in the production code:

 1 class BowlingGame
 2   
 3   def initialize
 4     @rolls = []
 5   end
 6 
 7   def roll(pins_down)
 8     @rolls.push pins_down
 9   end
10 
11   FRAMES_IN_A_GAME = 10
12   ALL_PINS_DOWN = 10
13 
14   def score
15     score = 0
16     roll = 0
17 
18     FRAMES_IN_A_GAME.times do
19       frame_score = @rolls[roll] + @rolls[roll + 1]
20       if frame_score == ALL_PINS_DOWN
21         frame_score += @rolls[roll + 2]
22       end
23       score += frame_score
24       roll += 2
25     end
26     score
27   end
28 end

The calculation of the frame score could be extracted to a method, which would save us the temporary variable:

 1 class BowlingGame
 2 
 3   def initialize
 4     @rolls = []
 5   end
 6 
 7   def roll(pins_down)
 8     @rolls.push pins_down
 9   end
10 
11   FRAMES_IN_A_GAME = 10
12   ALL_PINS_DOWN = 10
13 
14   def score
15     score = 0
16     roll = 0
17 
18     FRAMES_IN_A_GAME.times do
19       score += frame_score(roll)
20       roll += 2
21     end
22     score
23   end
24 
25   private
26 
27   def frame_score(roll)
28     frame_score = @rolls[roll] + @rolls[roll + 1]
29     if frame_score == ALL_PINS_DOWN
30       frame_score += @rolls[roll + 2]
31     end
32     frame_score
33   end
34 end

We can give meaning to the sum of the points of each of the frame’s throws, as well as to the question of it being a spare or not. Also, we can rubify the code a little:

 1 class BowlingGame
 2 
 3   def initialize
 4     @rolls = []
 5   end
 6 
 7   def roll(pins_down)
 8     @rolls.push pins_down
 9   end
10 
11   FRAMES_IN_A_GAME = 10
12   ALL_PINS_DOWN = 10
13 
14   def score
15     score = 0
16     roll = 0
17 
18     FRAMES_IN_A_GAME.times do
19       frame_score = base_frame_score(roll)
20       frame_score += @rolls[roll + 2] if spare? frame_score
21       score += frame_score
22       roll += 2
23     end
24     score
25   end
26 
27   private
28 
29   def spare?(frame_score)
30     frame_score == ALL_PINS_DOWN
31   end
32 
33   def base_frame_score(roll)
34     @rolls[roll] + @rolls[roll + 1]
35   end
36 end

The truth is that this is crying out to be extracted to a Frame class, but we’re not going to do it right now, as we could be committing a smell due to an excess of design.

On the other hand, looking at the test, we can see some points of improvement. Such as being more explicit in the example:

 1 require 'rspec'
 2 require_relative '../src/bowling_game'
 3 
 4 RSpec.describe 'A Bowling Game' do
 5 
 6   before do
 7     @game = BowlingGame.new
 8   end
 9 
10   it 'should roll a ball knocking down 0 pins' do
11     @game.roll 0
12   end
13 
14   it 'should score a gutter game' do
15     roll_many 20, 0
16     expect(@game.score).to eq(0)
17   end
18 
19   it 'should score all ones' do
20     roll_many 20, 1
21     expect(@game.score).to eq(20)
22   end
23 
24   it 'should score an spare' do
25     roll_spare
26     @game.roll 3
27     roll_many 17, 0
28     expect(@game.score).to eq(16)
29   end
30 
31   def roll_many(times, pins_down)
32     times.times do
33       @game.roll pins_down
34     end
35   end
36 
37   def roll_spare
38     @game.roll 5
39     @game.roll 5
40   end
41 end

And with this, we finish the refactoring. Next, we want to handle the strike case.

Strike!

A strike implies knocking down all of the pins in a single throw. In this case, the bonus equals the sum of the points obtained in the two following rolls. The next test sets up an example:

 1 require 'rspec'
 2 require_relative '../src/bowling_game'
 3 
 4 RSpec.describe 'A Bowling Game' do
 5 
 6   before do
 7     @game = BowlingGame.new
 8   end
 9 
10   it 'should roll a ball knocking down 0 pins' do
11     @game.roll 0
12   end
13 
14   it 'should score a gutter game' do
15     roll_many 20, 0
16     expect(@game.score).to eq(0)
17   end
18 
19   it 'should score all ones' do
20     roll_many 20, 1
21     expect(@game.score).to eq(20)
22   end
23 
24   it 'should score an spare' do
25     roll_spare
26     @game.roll 3
27     roll_many 17, 0
28     expect(@game.score).to eq(16)
29   end
30 
31   it 'should score an strike' do
32     @game.roll(10)
33     @game.roll(4)
34     @game.roll(3)
35     roll_many 17, 0
36     expect(@game.score).to eq(24)
37   end
38 
39   def roll_many(times, pins_down)
40     times.times do
41       @game.roll pins_down
42     end
43   end
44 
45   def roll_spare
46     @game.roll 5
47     @game.roll 5
48   end
49 end

This time the test fails because the production code calculates a total of 17 points (10 from the strike, plus 7 more from the two next throws). However, it should be counting that 7 twice: the bonus and the normal score.

Now, we have everything that we need in the production code, and in principle, we shouldn’t have to go back. Just introduce the necessary changes. Fundamentally, we’re interested in detecting that the strike has happened.

 1 class BowlingGame
 2 
 3   def initialize
 4     @rolls = []
 5   end
 6 
 7   def roll(pins_down)
 8     @rolls.push pins_down
 9   end
10 
11   FRAMES_IN_A_GAME = 10
12   ALL_PINS_DOWN = 10
13 
14   def score
15     score = 0
16     roll = 0
17 
18     FRAMES_IN_A_GAME.times do
19       if @rolls[roll] == 10
20         frame_score = 10 + @rolls[roll + 1] + @rolls[roll + 2]
21         roll += 1
22       else
23         frame_score = base_frame_score roll
24         frame_score += @rolls[roll + 2] if spare? frame_score
25         roll += 2
26       end
27       score += frame_score
28     end
29 
30     score
31   end
32 
33   private
34 
35   def spare?(frame_score)
36     frame_score == ALL_PINS_DOWN
37   end
38 
39   def base_frame_score(roll)
40     @rolls[roll] + @rolls[roll + 1]
41   end
42 end

Reorganizing the game knowledge

Our current production code lets us pass the tests, so we’re ready to fix its structure

Let’s start by making some things about the strike more explicit:

 1 class BowlingGame
 2 
 3   def initialize
 4     @rolls = []
 5   end
 6 
 7   def roll(pins_down)
 8     @rolls.push pins_down
 9   end
10 
11   FRAMES_IN_A_GAME = 10
12   ALL_PINS_DOWN = 10
13 
14   def score
15     score = 0
16     roll = 0
17 
18     FRAMES_IN_A_GAME.times do
19       if strike? roll
20         frame_score = 10 + base_frame_score(roll + 1)
21         roll += 1
22       else
23         frame_score = base_frame_score roll
24         frame_score += @rolls[roll + 2] if spare? frame_score
25         roll += 2
26       end
27       score += frame_score
28     end
29 
30     score
31   end
32 
33   private
34 
35   def strike?(roll)
36     @rolls[roll] == ALL_PINS_DOWN
37   end
38 
39   def spare?(frame_score)
40     frame_score == ALL_PINS_DOWN
41   end
42 
43   def base_frame_score(roll)
44     @rolls[roll] + @rolls[roll + 1]
45   end
46 end

The structure of the frame’s score calculation is not very clear, so we’re going to go back and change it so it’s more expressive:

 1 class BowlingGame
 2 
 3   def initialize
 4     @rolls = []
 5   end
 6 
 7   def roll(pins_down)
 8     @rolls.push pins_down
 9   end
10 
11   FRAMES_IN_A_GAME = 10
12   ALL_PINS_DOWN = 10
13 
14   def score
15     score = 0
16     roll = 0
17 
18     FRAMES_IN_A_GAME.times do
19       if strike? roll
20         frame_score = 10 + base_frame_score(roll + 1)
21         roll += 1
22       elsif spare? base_frame_score roll
23         frame_score = 10 + @rolls[roll + 2]
24         roll += 2
25       else
26         frame_score = base_frame_score roll
27         roll += 2
28       end
29       score += frame_score
30     end
31 
32     score
33   end
34 
35   private
36 
37   def strike?(roll)
38     @rolls[roll] == ALL_PINS_DOWN
39   end
40 
41   def spare?(frame_score)
42     frame_score == ALL_PINS_DOWN
43   end
44 
45   def base_frame_score(roll)
46     @rolls[roll] + @rolls[roll + 1]
47   end
48 end

This refactor makes it clear that strike and spare have a different structure, which makes them harder to understand and manage. We change spare to make them the same, and while we’re at it, we also remove the magic numbers.

 1 class BowlingGame
 2 
 3   def initialize
 4     @rolls = []
 5   end
 6 
 7   def roll(pins_down)
 8     @rolls.push pins_down
 9   end
10 
11   FRAMES_IN_A_GAME = 10
12   ALL_PINS_DOWN = 10
13 
14   def score
15     score = 0
16     roll = 0
17 
18     FRAMES_IN_A_GAME.times do
19       if strike? roll
20         frame_score = ALL_PINS_DOWN + base_frame_score(roll + 1)
21         roll += 1
22       elsif spare? roll
23         frame_score = ALL_PINS_DOWN + @rolls[roll + 2]
24         roll += 2
25       else
26         frame_score = base_frame_score roll
27         roll += 2
28       end
29       score += frame_score
30     end
31 
32     score
33   end
34 
35   private
36 
37   def strike?(roll)
38     ALL_PINS_DOWN == @rolls[roll]
39   end
40 
41   def spare?(roll)
42     ALL_PINS_DOWN == base_frame_score(roll)
43   end
44 
45   def base_frame_score(roll)
46     @rolls[roll] + @rolls[roll + 1]
47   end
48 end

Now we can extract methods to make the calculations more explicit:

 1 class BowlingGame
 2 
 3   def initialize
 4     @rolls = []
 5   end
 6 
 7   def roll(pins_down)
 8     @rolls.push pins_down
 9   end
10 
11   FRAMES_IN_A_GAME = 10
12   ALL_PINS_DOWN = 10
13 
14   def score
15     score = 0
16     roll = 0
17 
18     FRAMES_IN_A_GAME.times do
19       if strike? roll
20         frame_score = strike_score roll
21         roll += 1
22       elsif spare? roll
23         frame_score = spare_score roll
24         roll += 2
25       else
26         frame_score = base_frame_score roll
27         roll += 2
28       end
29       score += frame_score
30     end
31 
32     score
33   end
34 
35   private
36 
37   def spare_score(roll)
38     ALL_PINS_DOWN + @rolls[roll + 2]
39   end
40 
41   def strike_score(roll)
42     ALL_PINS_DOWN + base_frame_score(roll + 1)
43   end
44 
45   def strike?(roll)
46     ALL_PINS_DOWN == @rolls[roll]
47   end
48 
49   def spare?(roll)
50     ALL_PINS_DOWN == base_frame_score(roll)
51   end
52 
53   def base_frame_score(roll)
54     @rolls[roll] + @rolls[roll + 1]
55   end
56 end

The world’s best player

In principle, our current development is sufficient. However, it’s convenient to have test to certify it. For example, this new test corresponds to a perfect game: all of the rolls are strikes:

 1 require 'rspec'
 2 require_relative '../src/bowling_game'
 3 
 4 RSpec.describe 'A Bowling Game' do
 5 
 6   before do
 7     @game = BowlingGame.new
 8   end
 9 
10   it 'should roll a ball knocking down 0 pins' do
11     @game.roll 0
12   end
13 
14   it 'should score a gutter game' do
15     roll_many 20, 0
16     expect(@game.score).to eq(0)
17   end
18 
19   it 'should score all ones' do
20     roll_many 20, 1
21     expect(@game.score).to eq(20)
22   end
23 
24   it 'should score an spare' do
25     roll_spare
26     @game.roll 3
27     roll_many 17, 0
28     expect(@game.score).to eq(16)
29   end
30 
31   it 'should score an strike' do
32     @game.roll(10)
33     @game.roll(4)
34     @game.roll(3)
35     roll_many 17, 0
36     expect(@game.score).to eq(24)
37   end
38 
39   def roll_many(times, pins_down)
40     times.times do
41       @game.roll pins_down
42     end
43   end
44 
45   it 'should score perfect game' do
46     roll_many 12, 10
47     expect(@game.score).to eq(300)
48   end
49 
50   def roll_spare
51     @game.roll 5
52     @game.roll 5
53   end
54 end

When we run it, the test passes, which confirms us that BowlingGame is working as expected.

With all of the tests passing and the functionality completely implemented, we could try to evolve the code towards a better design. In the following example, we’ve extracted a Rolls class which is basically an array that also has all of the score calculation methods that we’ve been extracting so far:

 1 class Rolls<Array
 2   ALL_PINS_DOWN = 10
 3 
 4   def spare_score(roll)
 5     ALL_PINS_DOWN + self[roll + 2]
 6   end
 7 
 8   def strike_score(roll)
 9     ALL_PINS_DOWN + base_frame_score(roll + 1)
10   end
11 
12   def strike?(roll)
13     ALL_PINS_DOWN == self[roll]
14   end
15 
16   def spare?(roll)
17     ALL_PINS_DOWN == base_frame_score(roll)
18   end
19 
20   def base_frame_score(roll)
21     self[roll] + self[roll + 1]
22   end
23 end
24 
25 
26 class BowlingGame
27 
28   def initialize
29     @rolls = Rolls.new
30   end
31 
32   def roll(pins_down)
33     @rolls.push pins_down
34   end
35 
36   FRAMES_IN_A_GAME = 10
37 
38   def score
39     score = 0
40     roll = 0
41 
42     FRAMES_IN_A_GAME.times do
43       if @rolls.strike? roll
44         frame_score = @rolls.strike_score roll
45         roll += 1
46       elsif @rolls.spare? roll
47         frame_score = @rolls.spare_score roll
48         roll += 2
49       else
50         frame_score = @rolls.base_frame_score roll
51         roll += 2
52       end
53       score += frame_score
54     end
55 
56     score
57   end
58 end

What have we learned in this kata

  • Refactoring is the the design stage in classic TDD, it’s the moment at which, once we’ve implemented a behavior, we reorganize the code so that it’s clearer and better expressed
  • We must take the refactoring opportunities right when we detect them
  • We refactor both the test and the production code