XProgramming > XP Magazine > Adventures in C#: Bowling Revisited
COLLECTED TOPICS: Kate Oneal | Adventures in C# | Documentation in XP | Book Reviews
Adventures in C#: Bowling Revisited
Ron Jeffries
11/18/2003
Here's the Bowling Game again. This time I'll take a simpler approach, along the lines I usually follow when I do a Test-Driven Development demonstration. The result is strikingly simpler than the preceding "object-oriented" solution. What does this tell us about TDD and how to use it?

Contents:

The Story, Again ...

I'm solving the same problem as in the preceding BowlingGame article. I'll quote it here:

We are going to create an object, BowlingGame, which, given a valid sequence of rolls for one line of American Ten-Pin Bowling, produces the total score for the game. This story is picked because it's about the right size for a couple of hours of Test-Driven Development demonstration. Here are some things that the program will not do:

  • We will not check for valid rolls.
  • We will not check for correct number of rolls and frames.
  • We will not provide scores for intermediate frames.

Depending on the application, this might or might not be a valid way to define a complete story, but we do it here for purposes of keeping the demonstration light. I think you'll see that improvements like those above would go in readily if they were needed.

I'll briefly summarize the scoring for this form of bowling:

  • Each game, or "line" of bowling, includes ten turns, or "frames" for the bowler.
  • In each frame, the bowler gets up to two tries to knock down all the pins.
  • If in two tries, he fails to knock them all down, his score for that frame is the total number of pins knocked down in his two tries.
  • If in two tries he knocks them all down, this is called a "spare" and his score for the frame is ten plus the number of pins knocked down on his next throw (in his next turn).
  • If on his first try in the frame he knocks down all the pins, this is called a "strike". His turn is over, and his score for the frame is ten plus the simple total of the balls knocked down in his next two rolls.
  • If he gets a spare or strike in the last (tenth) frame, the bowler gets to throw one or two more bonus balls, respectively. These bonus throws are taken as part of the same turn. If the bonus throws knock down all the pins, the process does not repeat: the bonus throws are only used to calculate the score of the final frame.
  • The game score is the total of all frame scores.

What makes this game interesting to score is the lookahead in the scoring for strike and spare. At the time we throw a strike or spare, we cannot calculate the frame score: we have to wait one or two frames to find out what the bonus is.

The Approach

Last time I started with some objects in mind. This time I am going to approach the problem with fewer assumptions, and with more simplicity. I am going to assume that I'll store the pin counts, that I'll calculate the score frame by frame from the very beginning, and that's about it. We'll see what happens.

Begin with a Test ...

As always, I begin with my standard test file, named appropriately to the situation. It looks like this:

using System;
using NUnit.Framework;

namespace BowlingProcedural
{
  [TestFixture] public class BowlingTest: Assertion {
  
    public BowlingTest() {
    }
    
    [SetUp] public void SetUp() {
    }
    
    [Test] public void Hookup() {
      Assert(true);
    }
  }
}

As soon as I hook up the reference to NUnit, this runs. I continue with my standard first test, all gutterballs. This time I put the game creation directly into SetUp. It's a bit speculative, but since I have done this example twice in the last four days, I'm pretty sure I'm safe:

  [TestFixture] public class BowlingTest: Assertion {
    BowlingGame game;
  
    public BowlingTest() {
    }
    
    [SetUp] public void SetUp() {
      game = new BowlingGame();
    }
    
    [Test] public void Hookup() {
      Assert(true);
    }

    [Test] public void GutterBalls() {
      for (int i = 0; i < 20; i++ )
        game.Roll(0);
      AssertEquals(0, game.Score());
    }
  }

This doesn't compile, of course, owing to the mysterious lack of a BowlingGame class, which I'll rectify right now. I'll use Fake It Till You Make it to make the test run while I'm at it.

using System;

namespace BowlingProcedural {

  public class BowlingGame {
  
    public BowlingGame() {
    }

    public void Roll(int roll) {
    }

    public int Score() {
      return 0;
    }
  }
}

Whee, we're nearly done. Now my famous "Threes" test:

    [Test] public void Threes() {
      for (int i = 0; i < 20; i++ )
        game.Roll(3);
      AssertEquals(60, game.Score());
    }

Now I have to actually program something. I'll save the rolls in an ArrayList, and add it up. I'm going to take a slightly different approach from my usual, just to see what will happen: I'm going to write the loop over frames, not just over the list.

  public class BowlingGame {
    ArrayList rolls = new ArrayList();
  
    public BowlingGame() {
    }

    public void Roll(int roll) {
      rolls.Add(roll);
    }

    public int Score() {
      int rollIndex = 0;
      int total = 0;
      for (int frame = 0; frame < 10; frame++){
        total += (int) rolls[rollIndex] + (int) rolls[rollIndex+1];
        rollIndex += 2;
      }
      return total;
    }
  }

Here I took advantage of the fact that I have no strikes and spares in my tests as yet, and just assumed that there are two balls in every frame. There's duplication in the tests, the two loops over Roll. Let's refactor that into a method in the TestFixture:

    [Test] public void GutterBalls() {
      RollMany(20,0);
      AssertEquals(0, game.Score());
    }

    [Test] public void Threes() {
      RollMany(20,3);
      AssertEquals(60, game.Score());
    }

    public void RollMany(int count, int roll) {
      for (int i = 0; i < count; i++ )
        game.Roll(roll);
    }

That works fine. Now let's test a spare:

    [Test] public void Spare() {
      game.Roll(4);
      game.Roll(6);
      game.Roll(5);
      game.Roll(3);
      RollMany(16,0);
      AssertEquals(23, game.Score());
    }

We'll make that work by improving Score():

    public int Score() {
      int rollIndex = 0;
      int total = 0;
      for (int frame = 0; frame < 10; frame++){
        if ( (int) rolls[rollIndex] + (int) rolls[rollIndex+1] == 10 ) {
          total += 10 + (int) rolls[rollIndex+2];
        }
        else {
          total += (int) rolls[rollIndex] + (int) rolls[rollIndex+1];
        }
        rollIndex += 2;
      }
      return total;
    }

I hate all that casting, so we'll factor out a new method, PinCount():

    public int Score() {
      int rollIndex = 0;
      int total = 0;
      for (int frame = 0; frame < 10; frame++){
          if ( PinCount(rollIndex) + PinCount(rollIndex+1) == 10 ) {
          total += 10 + PinCount(rollIndex+2);
        }
        else {
          total += PinCount(rollIndex) + PinCount(rollIndex+1);
        }
        rollIndex += 2;
      }
      return total;
    }

    private int PinCount(int pinPosition) {
      return (int) rolls[pinPosition];
    }

That's a little nicer. Let's test a strike:

    [Test] public void Strike() {
      game.Roll(10);
      game.Roll(5);
      game.Roll(3);
      game.Roll(2);
      game.Roll(1);
      RollMany(14,0);
      AssertEquals( 29, game.Score());
    }

We make this work

    public int Score() {
      int rollIndex = 0;
      int total = 0;
      for (int frame = 0; frame < 10; frame++){
        if (PinCount(rollIndex) == 10 ) {
          total += 10 + PinCount(rollIndex+1) + PinCount(rollIndex+2);
          rollIndex++;
        }
        else if ( PinCount(rollIndex) + PinCount(rollIndex+1) == 10 ) {
          total += 10 + PinCount(rollIndex+2);
          rollIndex += 2;
        }
        else {
          total += PinCount(rollIndex) + PinCount(rollIndex+1);
          rollIndex += 2;
        }
      }
      return total;
    }

This works. I'll write my usual two followup tests, Perfect and Alternating, and check them:

    [Test] public void Perfect() {
      RollMany(12,10);
      AssertEquals(300, game.Score());
    }

    [Test] public void Alternatiing() {
      for (int i = 0; i < 5; i++) {
        game.Roll(10);
        game.Roll(4);
        game.Roll(6);
      }
      game.Roll(10);
      AssertEquals(200, game.Score());
    }

These both run! I think we're finished. What else might we do? We might want to make the code more expressive by implementing Stike() and Spare() functions, so that we could type

if (Strike(rollIndex))

but frankly I think it's nearly clear enough now. I'm sure you get the idea, and we'll benefit from making the comparison between this approach and the previous article's right now. Here is all the operational code for this version:

  public class BowlingGame {
    ArrayList rolls = new ArrayList();
  
    public BowlingGame() {
    }

    public void Roll(int roll) {
      rolls.Add(roll);
    }

    public int Score() {
      int rollIndex = 0;
      int total = 0;
      for (int frame = 0; frame < 10; frame++){
        if (PinCount(rollIndex) == 10 ) {
          total += 10 + PinCount(rollIndex+1) + PinCount(rollIndex+2);
          rollIndex++;
        }
        else if ( PinCount(rollIndex) + PinCount(rollIndex+1) == 10 ) {
          total += 10 + PinCount(rollIndex+2);
          rollIndex += 2;
        }
        else {
          total += PinCount(rollIndex) + PinCount(rollIndex+1);
          rollIndex += 2;
        }
      }
      return total;
    }

    private int PinCount(int pinPosition) {
      return (int) rolls[pinPosition];
    }

That's it. One class, 29 lines. Our previous implementation, with the sexy StrikeFrame, OpenFrame, SpareFrame objects, was six classes and about 140 lines of code. That's almost five times larger than this solution!

We'll come back to this comparison, but now let's see if we can make this code a bit more expressive.

Improving Expressiveness

Compared to the OO version, the code is more compact but a bit more cryptic. There are far fewer methods, but the Score() method is something like 19 lines long. Let's see whether we can make it better. We'll introduce the concept of Strike and Spare, this way:

    public int Score() {
      int rollIndex = 0;
      int total = 0;
      for (int frame = 0; frame < 10; frame++){
        if ( Strike(rollIndex) ) {
          total += 10 + PinCount(rollIndex+1) + PinCount(rollIndex+2);
          rollIndex++;
        }
        else if ( Spare(rollIndex) ) {
          total += 10 + PinCount(rollIndex+2);
          rollIndex += 2;
        }
        else {
          total += PinCount(rollIndex) + PinCount(rollIndex+1);
          rollIndex += 2;
        }
      }
      return total;
    }

    private bool Strike(int rollIndex) {
      return PinCount(rollIndex) == 10;
    }

    private bool Spare(int rollIndex) {
      return PinCount(rollIndex) + PinCount(rollIndex+1) == 10;
    }

Tests run. Now let's do something about those repeated increments of rollIndex:

    public int Score() {
      int rollIndex = 0;
      int total = 0;
      for (int frame = 0; frame < 10; frame++){
        if (Strike(rollIndex)) {
          total += 10 + PinCount(rollIndex+1) + PinCount(rollIndex+2);
        }
        else if ( Spare(rollIndex) ) {
          total += 10 + PinCount(rollIndex+2);
        }
        else {
          total += PinCount(rollIndex) + PinCount(rollIndex+1);
        }
        rollIndex += FrameSize(rollIndex);
      }
      return total;
    }

    private int FrameSize(int rollIndex) {
      if (Strike(rollIndex)) return 1;
      return 2;
    }

The frame indexing is a bit simpler now. We could perhaps even move the indexing up into the for statement, like this:

    public int Score() {
      int rollIndex = 0;
      int total = 0;
      for (int frame = 0; frame < 10; frame++, rollIndex += FrameSize(rollIndex)){
        if ( Strike(rollIndex) ) {
          total += 10 + PinCount(rollIndex+1) + PinCount(rollIndex+2);
        }
        else if ( Spare(rollIndex) ) {
          total += 10 + PinCount(rollIndex+2);
        }
        else {
          total += PinCount(rollIndex) + PinCount(rollIndex+1);
        }
      }
      return total;
    }

This construction is fairly rare, even in C, let alone C#, but it's perfectly legal. It has the advantage, arguably, that it keeps all the looping stuff together and that mostly we wouldn't have to look at it. The line count goes down, but the complexity of one line goes up.

Are these changes improvements? You get to decide for yourself, and discuss it with colleagues local and electronic. For a program this simple, they might seem unnecessary. For a program that takes months to write instead of hours, these techniques can come in pretty handy.

There is more that could be done. It's entirely possible, in fact, to refactor this solution into one like the fully OO one in the preceding article. We might begin with Replace Method with Method Object for the the Score() method, then Replace Conditional with Polymorphism in the resulting object. I'll leave that to you for now, but will do it in a subsequent article.

Refactoring takes time, and as these articles show, we could go on for a long time making "improvements". After every stage, we should ask ourselves whether we are really making things better for our purpose. If the answer is no, we should probably revert the last changes back, and then stop. For practice, of course, we can just keep going, and going, and going ...

Concluding Remarks

What do these two exercises tell us about Test-Driven Development?

Well, the process works really well for me, in terms of smoothly generating code that works and in which I can be confident. In this exercise, I went through with no surprise red bars at all. Every step worked. I didn't even insert any typos that stopped the compiler! For me, at least, TDD helps me create code smoothly and accurately.

But what about the quality of the design that we get? Something must have happened to give us almost five times the code volume for the same problem!

The difference was in my mind. (I'm sure that people have been suspecting that for years.) In the preceding article, I had the intention of producing objects that represented strike frames, spare frames, and the like. I was fully aware that this article's shorter solution existed, but chose to create the objects as if I had done some preliminary design and concluded that I needed some Frame objects. That focus was sufficient to multiply the program volume by almost five times!

For me, simple and good solutions arise when I go in with the fewest assumptions about what is supposed to happen, with the fewest intentions as to how the design will look. If I do the simplest thing I can think of at each step, but pay attention to duplication and refactoring opportunities, I wind up with simpler solutions. I think that's a good thing.

But think about it. Less up-front design, and a better result. Could it be true? It is for me. Doing less design at the beginning causes me to do more design as I move through the solution. I get a better result that way. What about you?

XProgramming > XP Magazine > Adventures in C#: Bowling Revisited
COLLECTED TOPICS: Kate Oneal | Adventures in C# | Documentation in XP | Book Reviews