Bowling Game Kata Part Five - The Strike Game

The second special case is the game where you knock down all of the pins on your first throw. This is called a "strike".

strike

When you make a strike, you put an X in the score box. You also get a double bonus. You take the 10 pins you knocked down in first roll and add the amount of the next two rolls in the next frame.

In our example above: 10 + 0 + 2 = 12. The rest of the game is scored normally.

BowlingGame.Test/GameTest.cs
[Test]
public void Game_Strike_ReturnsScorePlusDoubleBonus()
{
        game.Roll(10);
        game.Roll(0);
        game.Roll(2);
        RollMany(16, 0);
                 
        Assert.AreEqual(14,
        game.Score());
}

We compile and run the after refactoring.

4 tests passed

Wait! That test was supposed to fail! That's why we always want to see red when we are writing tests. Red, Green, Refactor.

We can read the code and try to figure out what went wrong, or we can set a breakpoint and step through the code.

Setting breakpoints and stepping through code is called "debugging".

Unfortunately, you cannot debug in the Express Editions of Visual Studio 2010.

Set a breakpoint somewhere inside the "Game_Strike_ReturnsScorePlusDoubleBonus" test.

If you are using ReSharper, you can debug the test right inside Visual Studio. If you are using the NUnit test runner, here are the steps:

Launch the NUnit test runner. If you used NuGet a runner will be installed inside the packages folder. For example: ~\BowlingGame\packages\NUnit.Runners.2.6.1\tools\nunit.exe

Run the tests. You should see them all pass.

In Visual Studio, go to the Debug menu and select "Attach to Process…

Attach to Process

In the process list, choose "nunit-agent.exe". Click the Attach button to start debugging.

Go back to the NUnit test runner and run the tests again. This time your breakpoint will stop the test and allow you to step through the code.

As you step through, you will see that the Spare method is returning true. Our first roll is a 10, our next roll is a 0. So 10 + 0 = 10 and that means a spare to our program.

So, do we fix the test or fix the code? Well, where's the bug? It's in the code. Our code is evaluating a strike as a spare if it is followed by a gutterball.

Let's get this test to fail by writing a conditional statement to check for a strike.

BowlingGame/Game.cs
public int Score()
{
    int roll = 0;
    for (int frame = 0; frame < 10; frame++)
    {
        if (rolls[roll] == 10)
        {
            throw new System.NotImplementedException();
        }
        else if (Spare(roll))
        {
            score += 10 + rolls[roll + 2];
        }
        else
        {
            score += rolls[roll] + rolls[roll + 1];
        }
        roll += 2;
    }

    return score;
}

We bring our old friend NotImplementedException back. Let's run the tests.

1 test failed "System.NotImplementedException"

Good, now that we are seeing red, we can make it green.

BowlingGame/Game.cs
public int Score()
{
    int roll = 0;
    for (int frame = 0; frame < 10; frame++)
    {
        if (rolls[roll] == 10)
        {
            score += 10 + rolls[roll + 1] + rolls[roll + 2];
            roll++;
        }
        else if (Spare(roll))
        {
            score += 10 + rolls[roll + 2];
            roll += 2;
        }
        else
        {
            score += rolls[roll] + rolls[roll + 1];
            roll += 2;
        }
    }

    return score;
}

4 tests passed

Now that our tests pass, let's refactor. First the Game.

To Do:

  1. Abstract the score bonus and normal score.
  2. Add the Strike method to determine if the roll is a strike.
BowlingGame/Game.cs
public int Score()
{
    int roll = 0;
    for (int frame = 0; frame < 10; frame++)
    {
        if (Strike(roll))
        {
            score += StrikeBonus(roll);
            roll++;
        }
        else if (Spare(roll))
        {
            score += SpareBonus(roll);
            roll += 2;
        }
        else
        {
            score += NormalScore(roll);
            roll += 2;
        }
    }

    return score;
}

private int NormalScore(int roll)
{
    return rolls[roll] + rolls[roll + 1];
}

private int SpareBonus(int roll)
{
    return 10 + rolls[roll + 2];
}

private int StrikeBonus(int roll)
{
    return 10 + rolls[roll + 1] + rolls[roll + 2];
}

private bool Strike(int roll)
{
    return rolls[roll] == 10;
}

We abstracted out the bonuses and normal scoring because it was a low level detail. We are striving to keep the Score method at "one level of abstraction" (Clean Code page 36).

After refactoring we compile and run the tests.

4 tests passed

Now, let's take a look at the tests.

To Do:

  1. Add a RollStrike method.
BowlingGame.Test/GameTest.cs
[Test]
public void Game_Strike_ReturnsScorePlusDoubleBonus()
{
    RollStrike();
    game.Roll(0);
    game.Roll(2);
    RollMany(16, 0);

    Assert.AreEqual(14, game.Score());
}

private void RollStrike()
{
    game.Roll(10);
}

Here's our code at the end of the strike test.

BowlingGame.Test/GameTest.cs
using NUnit.Framework;

namespace BowlingGame.Test
{
    [TestFixture]
    public class GameTest
    {
        private Game game;

        [SetUp]
        public void TestSetup()
        {
            game = new Game();
        }

        [Test]
        public void Game_AllZeros_ReturnsScore()
        {
            RollMany(20, 0);

            Assert.AreEqual(0, game.Score());
        }

        [Test]
        public void Game_AllOnes_ReturnsScore()
        {
            RollMany(20, 1);

            Assert.AreEqual(20, game.Score());
        }

        [Test]
        public void Game_Spare_ReturnsScorePlusBonus()
        {
            RollSpare();
            game.Roll(3);
            RollMany(17, 0);

            Assert.AreEqual(16, game.Score());
        }

        [Test]
        public void Game_Strike_ReturnsScorePlusDoubleBonus()
        {
            RollStrike();
            game.Roll(0);
            game.Roll(2);
            RollMany(16, 0);

            Assert.AreEqual(14, game.Score());
        }

        private void RollStrike()
        {
            game.Roll(10);
        }

        private void RollSpare()
        {
            game.Roll(5);
            game.Roll(5);
        }

        private void RollMany(int rolls, int pins)
        {
            for (int i = 0; i < rolls; i++)
            {
                game.Roll(pins);
            }
        }
    }
}
BowlingGame/Game.cs
namespace BowlingGame
{
    public class Game
    {
        private readonly int[] rolls = new int[21];
        private int currentRoll;
        private int score;

        public void Roll(int pins)
        {
            rolls[currentRoll] = pins;
            currentRoll++;
        }

        public int Score()
        {
            int roll = 0;
            for (int frame = 0; frame < 10; frame++)
            {
                if (Strike(roll))
                {
                    score += StrikeBonus(roll);
                    roll++;
                }
                else if (Spare(roll))
                {
                    score += SpareBonus(roll);
                    roll += 2;
                }
                else
                {
                    score += NormalScore(roll);
                    roll += 2;
                }
            }

            return score;
        }

        private int NormalScore(int roll)
        {
            return rolls[roll] + rolls[roll + 1];
        }

        private int SpareBonus(int roll)
        {
            return 10 + rolls[roll + 2];
        }

        private int StrikeBonus(int roll)
        {
            return 10 + rolls[roll + 1] + rolls[roll + 2];
        }

        private bool Strike(int roll)
        {
            return rolls[roll] == 10;
        }

        private bool Spare(int roll)
        {
            return rolls[roll] + rolls[roll + 1] == 10;
        }
    }
}