The Bowling Game Revisited - exploring TDD with Boo
when the subject of TDD is brought up, the first thing that comes to my mind is the classic Bowling Game problem. i must admit to be a little bit surprised that the solution the two Bobs came up with did not even make use of a Frame class. i really wonder how the thing would look like if i wrote a solution that is anchored around a Frame class.

to get things going start SharpDevelop and create a new Boo class library project. delete the dummy class and then add a reference to nunit.framework.  create a test called TestGame as shown below.

namespace MyBowlingGame

import System
import NUnit.Framework

[TestFixture]
class TestGame:
    g as Game

    [SetUp]
    def Setup():
        g = Game()

    [Test]
    def TestOneBadThrow():
        g.AddThrow(0)
        assert g.Score() == 0

this wont compile since the Game class doesnt exist yet. create a new class called Game and define the methods Score() and AddThrow() as shown below.

namespace MyBowlingGame

import System
class Game:
    def Score():
        return 0
		
    def AddThrow(pins as int):
        pass

compile the project and run the test. green means success while red means failure. add another test for a single throw.

	
    [Test]
    def TestOneGoodThrow():
        g.AddThrow(7)
        assert g.Score() == 7

now this will also fail since the method Score() always returns a zero.  let me spell out the mechanics of bowling before proceeding. 

a typical bowling game is divided into ten frames each where the bowler is given two chances or throws to clear the ten pins in a frame.  if the bowler clears all the ten pins on his first throw - a strike in bowling parlance - his score for the frame will be 10 plus the combined score of his next two throws in the succeeding frame or frames as bonus.  if the bowler manages to cleanup the remainder of the pins on his second throw - called a spare - he gets a 10 for the frame plus the score of his first throw in  the succedding frame as bonus.  if the bowler doesnt cleanup on his second throw - an open frame - his score for the frame would be the combined result of his two throws on that frame.  during the 10th frame, if a bowler makes a strike he is automatically given two more throws for the bonus points.  if the bowler manages a spare on the 10th frame he gets an extra throw for the bonus.  the maximum score in bowling game is 300 and the maximum number of throws is 22.

the preceding paragraph says that a bowling game is divided into frames. for starters, add a single frame to the Game class.

class Game:
    frame = Frame()
	
    def Score():
        return 0
		
    def AddThrow(pins as int):
        pass

from the bowling game description one can surmise that a Frame has a score property which is the sum of all the pins cleared as a result of a throw or throws.

class Frame:
    _score = 0
    
    def Score():
        return _score
        
    def AddThrow(pins as int):
        _score += pins	

it sure looks like the functionality needed by the Game class are in the Frame class already, so adjust the Game class to just defer to the Frame class for tracking scores.

class Game:
    frame = Frame()
	
    def Score():
        return frame.Score()
		
    def AddThrow(pins as int):
        frame.AddThrow(pins)

let's prove the changes by writing a test for single open frame.

    [Test]
    def TestOpenFrame():
        g.AddThrow(4)
        g.AddThrow(3)
        assert g.Score() == 7

no problem with this test. explore further by adding another open frame to preceding test.

    [Test]
    def TestTwoOpenFrames():
        g.AddThrow(4)
        g.AddThrow(3)
        g.AddThrow(6)
        g.AddThrow(2)
        assert g.Score() == 15

i have a feeling the solution will hold up for open frames. what about validating the score of a particular frame? create a new test as shown below, then insert the ScoreInFrame() method to the Game class.

    [Test]
    def TestTwoOpenFrames():
        g.AddThrow(4)
        g.AddThrow(3)
        g.AddThrow(6)
        g.AddThrow(2)
        assert g.Score() == 15
        assert g.ScoreInFrame(1) == 7
        assert g.ScoreInFrame(2) == 8

class Game:
    frame = Frame()
    
    def Score():
        return frame.Score()

    def ScoreInFrame(index as int):
        return frame.Score()
        
    def AddThrow(pins as int):
        frame.AddThrow(pins)

the test did fail as expected since there's only one Frame in the Game class so far . now get rid of the single frame field and replace it with a list or array of 10 frames. i'll settle for a list, which is populated with 10 Frame objects. to keep track of the current frame, add the _index field to the Game class. modify the Score() method to return the aggregate score from all the frames. [NOTE] we really could have used a generic list of Frame objects here but unfortunately Boo doesnt have any syntax for generics yet - but its coming real soon says Rodrigo Oliveira, its creator. so for convenience add the GetFrame() method to extract the Frame object from the list.

class Game:
    _frames = []
    _index = 0

    def constructor():
        for i in range(10):
            _frames.Add(Frame())
    
    def Score():
        score = 0
        for frame as Frame in _frames:
            score += frame.Score()
        return score

    def ScoreInFrame(index as int):
        return GetFrame(index-1).Score()
        
    def AddThrow(pins as int):
        GetFrame(_index).AddThrow(pins)

    def GetFrame(index as int):
        return _frames[index] as Frame

the test still failed because the code doesnt do anything to advance _index to the next frame after the player has rolled the first two balls. modify the AddThrow method of the Game class as shown below:

        
    def AddThrow(pins as int):
        frame = GetFrame(_index)
        frame.AddThrow(pins)
        _index++ if frame.IsClosed()

or the shortcut version:

    def AddThrow(pins as int):
        (frame = GetFrame(_index)).AddThrow(pins)
        _index++ if frame.IsClosed()

the preceding code says that the value of _index is incremented only if the current frame is already closed i.e. two balls for the frame had already been been rolled. so the Frame class should also track the number of rolls. define the IsClosed() method of the Frame class as shown below.

class Frame:
    _rolls = 0
    _score = 0
    
    def Score():
        return _score
        
    def AddThrow(pins as int):
        _score += pins
        _rolls++
        
    def IsClosed():
        return _rolls == 2

the test succeeds this time. the solution looks very promising. ok lets get some strike on the board by creating the test below.

    [Test]
    def TestStrikeAndOpenFrame():
        g.AddThrow(10)
        g.AddThrow(4)
        g.AddThrow(3)
        assert g.ScoreInFrame(1) == 17
        assert g.ScoreInFrame(2) == 7
        assert g.Score() == 24

again the test fails, so its time for some sleuthing work. lets see, the player makes a strike on the first throw - this effectively closes the  frame since there are no more pins left standing. the IsClosed() method should be revised to include a check for a strike condition i.e.  when a player scores a 10 on the first throw. this means the Frame class should also keep track of the player's score on the first throw for it to be able to tell whether it's a strike  or not. revise the Frame class as shown below.

class Frame:
    _rolls = 0
    _score = 0
    _firstThrow = 0
    
    def Score():
        return _score
        
    def AddThrow(pins as int):
        _firstThrow = pins if _rolls == 0
        _score += pins
        _rolls++
        
    def IsClosed():
        return IsStrike() or _rolls == 2

    def IsStrike():
        return _firstThrow == 10

the test still failed. thats because the bonus pins for the strike frame are not being added to it yet. lets review our Game class.

class Game:
    _frames = []
    _index = 0

    def constructor():
        for i in range(10):
            _frames.Add(Frame())
    
    def Score():
        score = 0
        for frame as Frame in _frames:
            score += frame.Score()
        return score

    def ScoreInFrame(index as int):
        return GetFrame(index-1).Score()
        
    def AddThrow(pins as int):             
        (frame = GetFrame(_index)).AddThrow(pins)
        _index++ if frame.IsClosed()
        
    def GetFrame(index as int) as Frame:
        return _frames[index] as Frame

inside the AddThrow() method of the Game class, the pins are only being added to the score of the current frame. the pins should also be added to the score of the previous strike frame.

        
    def AddThrow(pins as int):             
        GetFrame(_index-1).AddThrow(pins) if _index>0
        (frame = GetFrame(_index)).AddThrow(pins)
        _index++ if frame.IsClosed()

the result of running the test is shown below. the strike test was hurdled but  the open frames test failed.  its quite evident that the code patch above introduced some side effects which led to the failure of the open frames test again.

Image Hosted by ImageShack.us

the problem could be in the behavior of the AddThrow() method of the Frame class. either modify it to handle the bonus points from the free balls or put this functionality in some other method. i'm more inclined to follow the latter approach so as to avoid any further complication. recall that in the event of a strike the next two throws must be credited to the strike frame, no more no less. this means the Frame class should track the number of bonus throws credited to it.

class Frame:
    _rolls = 0
    _score = 0
    _firstThrow = 0
    _freeBall = 0
    
    def Score():
        return _score
        
    def AddThrow(pins as int):
        _firstThrow = pins if _rolls == 0
        _score += pins
        _rolls++

   def AddBonus(pins as int):
        if IsStrike() and _freeBall<2:
            _score += pins
            _freeBall++
        
    def IsClosed():
        return IsStrike() or _rolls == 2

    def IsStrike():
        return _firstThrow == 10

modify the AddThrow() method of the Game class to use the new AddBonus() method of the Frame class to handle the bonus pins.

  def AddThrow(pins as int):                    
        GetFrame(_index-1).AddBonus(pins) if _index>0
        (frame = GetFrame(_index)).AddThrow(pins)
        _index++ if frame.IsClosed()

the open frames test is now cleared. what about two consecutive strikes followed by an open frame.

    [Test]        
    def TestTwoStrikesAndOpenFrame():
        g.AddThrow(10)
        g.AddThrow(10)
        g.AddThrow(4)
        g.AddThrow(3)
        assert g.ScoreInFrame(1) == 24
        assert g.ScoreInFrame(2) == 17
        assert g.ScoreInFrame(3) == 7
        assert g.Score() == 48

Image Hosted by ImageShack.us

the preceding test  fails, but if 24 is replaced by 20 the test will pass. it seems the result of the 2nd free ball is not added to the strike frame. by the 3rd throw, the game is already in its 3rd frame. to reference the 1st frame we simply subtract 2 from _index.

    def AddThrow(pins as int):                 
        GetFrame(_index-2).AddBonus(pins) if _index>1
        GetFrame(_index-1).AddBonus(pins) if _index>0
        (frame = GetFrame(_index)).AddThrow(pins)
        _index++ if frame.IsClosed()

solved. so far, most of the tests are limited to 3 frames or less. i think now is a good time to make a test for a perfect game.  in bowling, a perfect game happens if the bowler makes 12 consecutive strikes - where the last 2 throws are free balls for the 10th frame.

    [Test]
    def TestPerfectGame():
        for i in range(12):
            g.AddThrow(10)    
        assert g.Score() == 300

NUnit reports an IndexOutOfRangeException. some range checking is in order: 

    def AddThrow(pins as int):                 
        GetFrame(_index-2).AddBonus(pins) if _index>1
        GetFrame(_index-1).AddBonus(pins) if _index>0
        if _index<10:
            (frame = GetFrame(_index)).AddThrow(pins)
            _index++ if frame.IsClosed()

Image Hosted by ImageShack.us

that was annoying.  so far the tests prove that the code can handle open frames and strikes, what about spares?  a spare happens if the player cleans up on his second throw.  this means the Frame class must also track the score of the second throw so it can check for a spare. in the event of a spare the next throw is added to the spare frame as bonus. these translates to the changes shown below.

class Frame:
    _rolls = 0
    _score = 0
    _firstThrow = 0
    _secondThrow = 0
    _freeBall = 0
    
    def Score():
        return _score
        
    def AddThrow(pins as int):
        _firstThrow = pins if _rolls == 0
        _secondThrow = pins if _rolls == 1
        _score += pins
        _rolls++

   def AddBonus(pins as int):
        if (IsStrike() and _freeBall<2) or (IsSpare() and _freeBall<1):
            _score += pins
            _freeBall++
        
    def IsClosed():
        return IsStrike() or _rolls == 2

    def IsStrike():
        return _firstThrow == 10

    def IsSpare():
        return (_firstThrow<10) and (_firstThrow+_secondThrow==10)

with the modifications above in place, create a test for spare.

    [Test]
    def TestSpareAndOpenFrame():
        g.AddThrow(6)
        g.AddThrow(4) 
        g.AddThrow(3)
        g.AddThrow(5)
        assert g.ScoreInFrame(1) == 13
        assert g.ScoreInFrame(2) == 8
        assert g.Score() == 21

passed that one. the bowling game article from ObjectMentor included a test for complete game. let's see if our code can pass this one.

    [Test]
    def TestSampleGame():
        g.AddThrow(1)
        g.AddThrow(4)
        g.AddThrow(4)
        g.AddThrow(5)
        g.AddThrow(6)
        g.AddThrow(4)
        g.AddThrow(5)
        g.AddThrow(5)
        g.AddThrow(10)
        g.AddThrow(0)
        g.AddThrow(1)
        g.AddThrow(7)
        g.AddThrow(3)
        g.AddThrow(6)
        g.AddThrow(4)
        g.AddThrow(10)
        g.AddThrow(2)
        g.AddThrow(8)
        g.AddThrow(6)		
        assert g.Score() == 133

Image Hosted by ImageShack.us

no problem with that either.

namespace MyBowlingGame

import System

class Game:
    _frames = []
    _index = 0

    def constructor():
        for i in range(10):
            _frames.Add(Frame())
    
    def Score():
        score = 0
        for frame as Frame in _frames:
            score += frame.Score()
        return score

    def ScoreInFrame(index as int):
        return GetFrame(index-1).Score()
        
    def AddThrow(pins as int):                 
        GetFrame(_index-2).AddBonus(pins) if _index>1
        GetFrame(_index-1).AddBonus(pins) if _index>0
        if _index<10:
            (frame = GetFrame(_index)).AddThrow(pins)
            _index++ if frame.IsClosed()
        
    def GetFrame(index as int) as Frame:
        return _frames[index] as Frame 

class Frame:
    _rolls = 0
    _score = 0
    _firstThrow = 0
    _secondThrow = 0
    _freeBall = 0
    
    def Score():
        return _score
        
    def AddThrow(pins as int):
        _firstThrow = pins if _rolls == 0
        _secondThrow = pins if _rolls == 1
        _score += pins 
        _rolls++ 
        
    def AddBonus(pins as int):
        if (IsStrike() and _freeBall<2) or (IsSpare() and _freeBall<1):
            _score += pins
            _freeBall++
            
    def IsClosed():
        return IsStrike() or _rolls == 2

    def IsStrike():
        return _firstThrow == 10
        
    def IsSpare():
        return (_firstThrow<10) and (_firstThrow+_secondThrow==10)
Attachment: MyBowlingGame.zip
Published 09-05-2006 7:29 PM by smash
Filed under: , ,