In our first segment we got up and running with NUnit, created the foundation of our BowlingGame and got spares working. But spares aren’t what bowlers live for – it’s strikes. Let’s see if we can get that working.
Back to the problem at hand. We’ve got the tests passing for Spares, so let’s see about Strikes. The tests should be similar, except that scoring has to take into account the next 2 rolls. Here’s our first test:
[Test]
public void FirstFrameStrikeRest2sShouldScore50()
{
game.RollStrike();
RollFrames(2,2,9);
Assert.AreEqual(50, game.Score, "Should have a final score of 50");
}
Which, of course, doesn’t compile. So, let’s add the RollStrike with an empty method body and run our tests:
To pass it, we’ll have to take into account strikes in our scoring method. We’ll change RollStrike() to:
public void RollStrike()
{
Frame frame = new Frame(roll1, roll2);
frame.IsStrike = true;
AddFrame(frame);
}
But, that’s not entirely correct! A frame with a strike can only have one roll – the first one for 10 pins. So, let’s just create a Strike class which inherits from Frame to do the same thing:
using System;
namespace Bowling
{
public class Strike : Frame
{
public Strike() : base(10,0)
{
this.isStrike = true;
}
}
}
And we’ll change isStrike, roll1 and roll2 to be protected in Frame. We’ll change isSpare to protected since I think I’m going to want to do the same thing there – but not yet, because we are on a red test!
Ok, now that we’ve got the Strike class, we’ll change BowlingGame to use it:
public void RollStrike()
{
Strike strike = new Strike();
AddFrame(strike);
}
And run our tests:
Closer. Now, let’s take the strike into account in Score:
if(frame.IsSpare)
{
Frame nextFrame = (Frame)frames[currentFrame+1];
score += nextFrame.Roll1;
}
else if (frame.IsStrike)
{
Frame nextFrame = (Frame)frames[currentFrame+1];
score += nextFrame.TotalRoll;
}
Since we’re on green, I’m going to refactor the Spare method to use polymorphism:
using System;
namespace Bowling
{
public class Spare : Frame
{
public Spare(int roll1, int roll2) : base(roll1, roll2)
{
this.isSpare = true;
}
}
}
public void RollSpare(int roll1, int roll2)
{
Spare spare = new Spare(roll1, roll2);
AddFrame(spare);
}
And, all of our tests still pass! Now, let’s exercise the Strike a little more. Let’s see what happens when the first two frames are strikes.
[Test]
public void FirstTwoFramesStrikesRest2sShouldScore68()
{
game.RollStrike();
game.RollStrike();
RollFrames(2,2,8);
Assert.AreEqual(68, game.Score, "Should have a final score of 68");
}
Looks like we’re off by 2. Aha! Look at our scoring code:
else if (frame.IsStrike)
{
Frame nextFrame = (Frame)frames[currentFrame+1];
score += nextFrame.TotalRoll;
}
To score a strike, you take 10 and add the next 2 rolls. In this case, if the next roll is a strike, there is only one roll, so we are missing the second roll (really the first roll of the third frame). Let’s change that:
else if (frame.IsStrike)
{
Frame nextFrame = (Frame)frames[currentFrame+1];
score += nextFrame.Roll1;
if(nextFrame.IsStrike)
{
Frame thirdFrame = (Frame)frames[currentFrame+2];
score += thirdFrame.Roll1;
}
}
And run our tests:
Woah! Not only did that not pass our strike test, we now have a second test that is failing. Let’s see – Ah! I missed a step:
else if (frame.IsStrike)
{
Frame nextFrame = (Frame)frames[currentFrame+1];
score += nextFrame.Roll1;
if(nextFrame.IsStrike)
{
Frame thirdFrame = (Frame)frames[currentFrame+2];
score += thirdFrame.Roll1;
}
else
{
score += nextFrame.Roll2;
}
}
Much better. Although I don’t like the Score “Property” any more because it is doing a lot of work. Let’s see if we can clean that up. First, we seem to do a lot of work to figure out the next frame. If we made the frames a simple LinkedList, we could just ask a frame for the one after that. First, we’ll modify our Frame class:
protected Frame nextFrame = null;
public Frame NextFrame
{
get{return nextFrame;}
set{nextFrame = value;}
}
Next, we’ll modify our BowlingGame to hold on to the first frame and last frame bowled, and then modify AddFrame to use that:
private void AddFrame(Frame frame)
{
if(firstFrame == null)
{
firstFrame = frame;
lastFrame = frame;
}
else
{
lastFrame.NextFrame = frame;
lastFrame = frame;
}
if(frames.Count == 10)
{
throw new OutOfFramesException();
}
frames.Add(frame);
}
Note that our tests should still be passing since we’ve only added code. Let’s see if they are:
Good. Now, let’s modify score to use this:
public int Score
{
get
{
int score = 0;
Frame currentFrame = firstFrame;
while(currentFrame != null)
{
score += currentFrame.TotalRoll;
if(currentFrame.IsSpare)
{
score += currentFrame.NextFrame.Roll1;
}
else if (currentFrame.IsStrike)
{
score += currentFrame.NextFrame.Roll1;
if(currentFrame.NextFrame.IsStrike)
{
score += currentFrame.NextFrame.NextFrame.Roll1;
}
else
{
score += currentFrame.NextFrame.Roll2;
}
}
currentFrame = currentFrame.NextFrame;
}
return score;
}
}
Ok, let’s see if our tests still pass:
And they do! We can now remove the ArrayList code, and run our tests to make sure we don’t break anything:
As you can see, our OutOfFramesException is now failing. We can clean that up by numbering our frames. First we’ll add a property to our Frame class for Number:
public int Number
{
get{return frameNumber;}
set{frameNumber = value;}
}
Then we’ll modify our AddFrame method in BowlingGame to set the Frame number:
private void AddFrame(Frame frame)
{
if(firstFrame == null)
{
frame.Number = 1;
firstFrame = frame;
lastFrame = frame;
}
else
{
if(lastFrame.Number == 10)
{
throw new OutOfFramesException();
}
frame.Number = lastFrame.Number + 1;
lastFrame.NextFrame = frame;
lastFrame = frame;
}
}
And that gets us back to green. (But, did it really help? I’m not so sure…)
So now we have strikes and spares working, but we haven’t given any attention to the 10th frame rule. I can think of one test case which will force us to exercise that – the perfect game:
[Test]
public void PerfectGameShouldTotal300()
{
for(int i = 1; i <=10; i++)
{
game.RollStrike();
}
Assert.AreEqual(300, game.Score, "Perfect game should equal 300");
}
Running that gives us a null reference exception, something I was expecting:
The problem is this line from our Score in BowlingGame:
score += currentFrame.NextFrame.NextFrame.Roll1;
When we are on the 9th frame, the NextFrame would be the 10th, and the NextFrame would be null. One way we can fix this would be to do a check to see if the NextFrame of the NextFrame is null, but that wouldn’t help when we got to the 10th frame, saw we were a strike, and tried to get the next frame.
I spiked some different things, which all led to some not-so-good code. I looked at our rolls, and realized that we have RollFrame, RollSpare and RollStrike, so why not RollTenthFrame, since it is specialized?
[Test]
public void PerfectGameShouldTotal300()
{
for(int i = 1; i <=9; i++)
{
game.RollStrike();
}
game.RollTenthFrame(10,10,10);
Assert.AreEqual(300, game.Score, "Perfect game should equal 300");
}
To make it compile, I put in a RollTenthFrame method:
public void RollTenthFrame(int roll1, int roll2, int roll3)
{
Frame frame = new Frame(roll1, roll2);
AddFrame(frame);
}
And ran my tests:
Wow! That’s pretty close. I realize now that the problem before was because I was marking the tenth frame as a strike, which it sort of isn’t (from a scoring perspective, at least). So, I wager that if that tenth frame returned all 3 rolls in the TotalScore, this test would pass. To do that, I’m going to add a bonusRoll field to my Frame class, and have TotalScore take it into account:
public class Frame
{
//...
protected int bonusRoll;
//...
public int BonusRoll
{
get { return bonusRoll; }
set { bonusRoll = value; }
}
//...
public int TotalRoll { get { return roll1 + roll2 + bonusRoll; } }
}
And then modify my RollTenthFrame to set the BonusRoll:
public void RollTenthFrame(int roll1, int roll2, int roll3)
{
Frame frame = new Frame(roll1, roll2);
frame.BonusRoll = roll3;
AddFrame(frame);
}
Finally, let’s run our tests:
And we’re all green! There’s one more test I want to run that I heard about from Ron’s site. An alternating Strike/Spare game adds up to 200:
[Test]
public void AlternatingStrikesAndSparesShouldTotal200()
{
for(int i = 1; i <= 4; i++)
{
game.RollStrike();
game.RollSpare(4,6);
}
game.RollStrike();
game.RollTenthFrame(4,6,10);
Assert.AreEqual(200, game.Score, "Alternating Spares and Strikes should equal 200");
}
And…(Drum roll please)
Well, that’s all for now. We’ve bult a system, Test-First, which can calculate the scores of a bowling game. We ran into some snags along the way, but by keeping our tests green and taking small steps, we succeeded!
I have put up the full source for download, so feel free to open it up and play with it. Happy TDDing!