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