« back

Tutorial:The Bowling Game Kata in Ruby

05 Jul 2012

bowling montage

I was given the task of The Bowling Game Kata. While reading this page, the main thing that stuck with me was this: "A kata is meant to be memorized. Students of a kata study it as a form, not as a conclusion. It is not the conclusion of the kata that matters, it's the steps that lead to the conclusion."

The biggest challenge for me was that the original kata is done in Java (which I know close to nothing about) and I would be writing it in Ruby. Although at first uncertain about having to look at a foreign language and make the translation, after thinking it through and starting the kata, I could see that this in itself will be worth a lot to me. This provided me the opportunity to de-mystify syntax of other languages and allow me to boil things down to the meat and potatoes of non-language specific programming. Building confidence in this area will allow me to be more comfortable looking to examples from other languages of how to solve problems in any language, not just Ruby.

I took Uncle Bob's advice of breaking the kata down into sections; tests 1-5. You can download the original slides or watch Uncle Bob's Video. I really enjoyed seeing him go through it in real time.

Something I observed was that I was trying too hard to mimic the Java code. When I would see a for loop, I would think I needed a for loop. I would start searching high and low on how to get my code into that format when all I needed in this case was one of the basic Ruby methods. Instead of trying to mimic the Java format, I just needed to step back, and consider what was trying to be accomplished with the code itself. Then I could move to thinking about how to do that in Ruby. If you are not familiar with Ruby or you work with multiple languages, try converting it for yourself.

So here goes...

The First Test

The first test. I want to create a game so the first thing I must do is create a failing test for the game.

1 it "should be able to create a new game" do
2   g = Game.new
3 end

This will fail so I want to create a Game class.

1 class Game
2 
3 end

This will pass and there is nothing to refactor so, next I want to be able to roll. To be able to roll I need a new game as well.

1 it "should roll" do
2   g = Game.new
3   g.roll(0)
4 end

This will fail because there is no roll method. Then I will create my roll method, which will take an argument of pins.

1 def roll(pins)
2 
3 end

This will pass so now I can check for refactoring opportunities. There is duplicate code so I want to extract it.

1 let (:g) {g = Game.new}
2 
3 it "should be able to create a new game" do
4 end
5 
6 it "should roll" do
7     g.roll(0)
8 end

Once I have that extracted I can see that my tests are still passing so I have the opportunity to completely remove the first test which we have proven. Now I need to write the score function but I can not write this until a complete game has been rolled. I want to write the simplest code I can to achieve this, which would be a gutter game.

1 it "should roll a gutter game" do
2   20.times do
3     g.roll(0)
4   end
5   g.score.should == 0
6 end

This will fail so I need to write the score method.

1 def score
2   0
3 end

There is no refactoring to do so we can move on to the next test case.

The Second Test

Let's test I can roll all ones. I can pretty much copy and paste from the gutter game test which means that there will likely be refactoring but let's go with it for now.

1 it "can roll all ones" do
2   20.times do
3     g.roll(1)
4   end
5   g.score.should == 20
6 end

Now that I have a failing test I could simply return 20 in the score method but then my first test case will fail. I need to create the simplest code that will allow both cases to pass. I need to add an initialize method to the class and get @score = 0, let roll add up all the pins and return @score. I end up with this.

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

Now that my tests are passing I need to move to the refactoring phase. There is duplicate code in my tests and I can remedy it. Before extracting the duplicate code out into a method called roll_many, I am going to change out variable names and add a method call like so, just in the first method.

1   it "can roll a gutter game" do
2     n.times do
3       g.roll(pins)
4     end
5     roll_many(n,pins)
6     g.score.should == 0
7   end

Then extract the code into a method called roll_many that will take n and pins as arguments.

1     let (:g) { g = Game .new }
2 
3   def roll_many(n, pins)
4     n.times do
5       g.roll(pins)
6     end
7   end

Then substitute the roll many method in the second test as well. Then you can remove the "can roll" test.

 1       it "can roll a gutter game" do
 2         roll_many(20, 0)
 3         g.score.should == 0
 4       end
 5 
 6       it "can roll all ones" do
 7         roll_many(20, 1)
 8         g.score.should == 20
 9       end
10     end

Your tests should still be passing.

The third test

Rolling a game of all 5's will not pass because of spares. The simplest example we could do is one spare followed by all gutter balls.

1     it "can roll a spare" do
2     g.roll(5)
3     g.roll(5) #spare
4     g.roll(3)
5     roll_many(17, 0)
6     g.score.should == 16
7   end

The 3 gets counted twice as the bonus for the spare. Then run the tests and you should fail. You may be tempted to start hard coding values in based on your current roll and the last roll, but we don't want to do that. Score should be calculating score but actually roll is. This is 'misplaced responsibility'. We want to refactor but our test is failing. We can rewind by commenting it out and then go back to the refactoring phase. I want to save all the rolls, so I am going to put them into an array. I also need an index for the array.

1     def initialize
2     @score = 0
3     @rolls = []
4   end
5 
6   def roll(pins)
7     @rolls << pins
8   end

Then in the score function I am going to iterate over the array.

1 def score
2     @rolls.each do |roll|
3         @score += roll
4     end
5 end

Then I don't really need a score variable anywhere else but my score function, so i can make it a local variable.

 1   def initialize
 2     @rolls = []
 3   end
 4 
 5   def score
 6     score = 0
 7     @rolls.each do |roll|
 8       score += roll
 9     end
10     score
11   end

Now the test passes and we no longer have misplaced responsibility. At his point we will uncomment the last test we wrote and the test should fail.

Now to try to get our test to pass we can calculate to see if we roll 2 balls to equal 10 (a spare). Wait, there will be a problem. If the second ball of a frame and the first ball of the next frame = 10, this will count as a spare. We are not accounting for frames. We need to find a way to be on frame. Let's refactor. We can comment out our current failing test so that we are passing.

 1   def score
 2     score = 0
 3     frame = 0
 4     i = 0
 5       while frame < 10
 6         score += @rolls[i] + @rolls[i + 1]
 7         frame +=1
 8         i += 2
 9       end
10     score
11   end
12 
13   def score
14     score = 0
15     i = 0
16     frame = 0
17     while frame < 10
18       score += @rolls[i] + @rolls[i + 1]
19       frame +=1
20       i += 2
21     end
22     score
23   end

This should pass and now we will uncomment out the last test so that it is failing again. Now we should be able to calculate that two balls together that = 10 will be a spare. If they do, we want the program to do one thing, and if not, we want it to do another.

 1     def score
 2     score = 0
 3     i = 0
 4     frame = 0
 5     while frame < 10
 6       if @rolls[i] + @rolls[i + 1] == 10 #spare
 7         score += 10 + @rolls[i + 2]
 8         i += 2
 9       else
10         score += @rolls[i] + @rolls[i + 1]
11        i += 2
12       end
13       frame +=1
14     end
15     score
16   end

This should pass but now it is refactoring time!!!!! Let's change the anonymous i variable to first_in_frame to be more clear.

 1     def score
 2     score = 0
 3     first_in_frame 0
 4     frame = 0
 5     while frame < 10
 6       if @rolls[first_in_frame] + @rolls[first_in_frame + 1] == 10 #spare
 7         score += 10 + @rolls[first_in_frame + 2]
 8         first_in_frame += 2
 9       else
10        score += @rolls[first_in_frame] + @rolls[first_in_frame + 1]
11        first_in_frame += 2
12       end
13       frame +=1
14     end
15     score
16   end

Then we want to work on removing the spare comment. Let's extract that line into a method called spare?.

 1     def score
 2     score = 0
 3     first_in_frame = 0
 4     frame = 0
 5     while frame < 10
 6       if spare?(first_in_frame)
 7         score += 10 + @rolls[first_in_frame + 2]
 8         first_in_frame += 2
 9       else
10        score += @rolls[first_in_frame] + @rolls[first_in_frame + 1]
11        first_in_frame += 2
12       end
13       frame +=1
14     end
15     score
16   end
17 
18   def spare?(first_in_frame)
19     @rolls[first_in_frame] + @rolls[first_in_frame + 1] == 10
20   end

Then we can refactor our tests as well to remove the comment. Let's create a method called roll_spare.

 1   it 'can roll a spare' do
 2     roll_spare
 3     g.roll(3)
 4     roll_many(17, 0)
 5   end
 6 
 7   def roll_spare
 8     g.roll(5)
 9     g.roll(5)
10   end

You should still be passing.

The Fourth Test

Now let's work on testing a spare.

1     it 'can roll a strike' do
2         g.roll(10)  #strike
3         g.roll(3)
4         g.roll(4)
5         roll_many(16, 0)
6         g.score.should == 24
7       end

This fails so now let's work on making the test pass. We can pretty much go with what we were doing with spare for strike. Let's add another if statement in the score method.

1     while frame < 10
2      if @rolls[first_in_frame] == 10 #strike
3       if spare?(first_in_frame)
4         score += 10 + @rolls[first_in_frame + 2]
5         first_in_frame += 2
6       else

Let's turn the next if into an elsif

1     if @rolls[first_in_frame] == 10 #strike
2       elsif spare?(first_in_frame)
3         score += 10 + @rolls[first_in_frame + 2]
4         first_in_frame += 2
5       else

Then after our first if statement let's add some more logic.

1      while frame < 10
2           if @rolls[first_in_frame] == 10 #strike
3             score += 10 + @rolls[first_in_frame + 1] + @rolls[first_in_frame + 2]
4             first_in_frame +=1
5           elsif spare?(first_in_frame)
6             score += 10 + @rolls[first_in_frame + 2]
7             first_in_frame += 2
8           else

Now we can see if our test will pass. It does, so you know what that means. Time to refactor. The first thing we can work on is removing the strike comment by extracting hat out into it's own method. Let's call it strike? and pass it first_in_frame and make sure we are calling it where we extracted it from.

 1  while frame < 10
 2      if strike?(first_in_frame)
 3        score += 10 + @rolls[first_in_frame + 1] + @rolls[first_in_frame + 2]
 4        first_in_frame +=1
 5      elsif spare?(first_in_frame)
 6      ...
 7 
 8     def spare?(first_in_frame)
 9     @rolls[first_in_frame] + @rolls[first_in_frame + 1] == 10
10   end
11 
12   def strike?(first_in_frame)
13     @rolls[first_in_frame] == 10
14   end

Next let's break the line under the first if statement out because there is a lot going on there. To clarify, let's call it strike_bonus_balls and pass it first_in_frame. We will create the method down with the others we've broken out.

 1        while frame < 10
 2           if strike?(first_in_frame)
 3             score += 10 + strike_bonus_balls(first_in_frame)
 4             first_in_frame +=1
 5           elsif spare?(first_in_frame) ...
 6 
 7     ...
 8 
 9       def strike_bonus_balls(first_in_frame)
10         @rolls[first_in_frame + 1] + @rolls[first_in_frame + 2]
11       end

Since we've done that, let's do that for the spare case as well. let's call it spare_bonus_ball and pass it first_in_frame.

 1      while frame < 10
 2           if strike?(first_in_frame)
 3             score += 10 + strike_bonus_balls(first_in_frame)
 4             first_in_frame +=1
 5           elsif spare?(first_in_frame)
 6             score += 10 + spare_bonus_ball(first_in_frame)
 7             first_in_frame += 2
 8           else
 9     ...
10 
11     ...
12       def spare_bonus_ball(first_in_frame)
13         @rolls[first_in_frame + 2]
14       end

Now let's extract another line of code similar to those two. We will call this method two_balls_in_frame and pass it first in frame.

 1       else
 2            score += two_balls_in_frame(first_in_frame)
 3            first_in_frame += 2
 4           end
 5 
 6     create the method  ..
 7 
 8       def two_balls_in_frame(first_in_frame)
 9         @rolls[first_in_frame] + @rolls[first_in_frame + 1]
10       end

We did also have a comment on our test so let's take a look at that but let's make sure we are passing first. We are. Great. Let's remove the line that has the strike comment in it to it's own method like we did with the spare. let's call it roll_strike. Then we can remove the comment.

 1   def roll_strike
 2     g.roll(10)
 3   end
 4 
 5  def roll_spare
 6     g.roll(5)
 7     g.roll(5)
 8   end
 9 
10   it 'can roll' do
11     g.roll(0)
12   end

This should pass.

The Fifth Test

1   it 'can roll a perfect game' do
2     roll_many(12, 10)
3     g.score.should == 300
4   end

This should fail. Or should it? It does not. We have successfully covered all of the test cases in the game of bowling. Sweet!

Subscribe below to keep up with me on my journey.

comments powered by Disqus