Bowling Game Tracker

# Bowling Game Tracker

When I started reading Agile Software Development, Principles, Patterns, and Practices one of the very first chapters described an exercise in TDD that had to do with scoring a bowling game. I thought it was a nifty toy example, and skipped that chapter, figuring I'd do it myself before reading the solution in the book. That was a few years ago, and I don't remember what happened to my first attempt at the problem but recently I was thinking about providing a simple TDD exercise, I figured I'd try this one again. I still haven't seen the solution in the book, so after I publish this I'll go have a look to see if my approach is a really terrible way to do it! By the way, you can download a zip file with all of the source code. However, I suggest you first try this out on your own, then see to what extent your solution matches up with mine. Feedback would be great! First things first though, let's write a story to summarize what we want to do.

# The Story

Develop a piece of code that will keep track of the score for bowling games. In this initial version, there is no need to support multiple bowling lanes or multiple players. For now, simply track the score as a game proceeds. After each throw, the tracker will display a standard bowling score sheet with the number of pins dropped in each frame and the cumulative score for each frame. Strikes and spares will be displayed in the usual way, "X" for a strike and "/" for a spare. In case of a strike or spare, the score for a given frame cannot be determined right away. In this case simply display a blank for the cumulative score. Once the game is over, any extra throws submitted should be ignored. Example of the first three frames of a game:
```   ---------    ---------   ---------
| X  |    |  |  3 | /  | |  4  | 5 |
|---------|  |---------| |---------|
|   20    |  |   34    | |   43    |
---------    ---------   ---------
```

# Scoring Rules

If you knock down all the pins on your first throw in a given frame, it is called a strike. The score doesn't get added on straight away because for a strike, you get the values of your next two balls as a bonus. For example, if you score a strike in the first frame, then an 7 and 1 in the second frame, you would score 18 (10+7+1) for the first frame, and 8 for the second frame, making a total of 26 after two frames. If you knock down some of the pins on the first ball, and knocked down the remainder of the pins in the second ball, it is known as a spare. Again, the score doesn't get added on straight away because for a spare, you get the values of your next ball as a bonus. For example, you if score a spare in the first frame, say an 6 and a 4, then got an 8 and a 1 in the second frame, you would score 18 (6+4+8) for the first frame, and 9 for the second frame, making a total of 27 after two frames. When it comes to the final frame, it is slightly different. In the final frame, you get bonus balls if you strike or spare, to a maximum of three deliveries. If you strike in the first delivery you have the opportunity to strike in the remaining two and have three deliveries in total. If you scored strikes in each of your final three deliveries, the score for the final frame would be 30 (10+10+10). If you spare the final frame, you get the third delivery as a bonus. So, a spare, 9 and 1, followed by a strike would equal 20 (9+1+10).

# Development Overview

Approximate Development Time: 10 hours Here are the tests in the order I wrote them. Unfortunately I didn't save the state of the code as I was developing it so I can't show its evolution through each of the refactoring steps. I may try to do the exercise again in the future to display the changes the tests required the code to become increasingly complicated. I tried to keep the code as simple as possible throughout the exercise, refactoring only in the interest of making the code easier to work with. I tried starting with the scoring function itself, but I found this approach was too complicated - I was writing way too much code to satisfy a single test. I wanted to find an approach that allowed me to write as little code as possible for each test so that I'd have the opportunity to work on the design in small increments. Ultimately I settled on the following approach: First just keep track of the current frame. If the player throws a strike, then immediately move to the next frame. If the player doesn't throw a strike right away, then of course he or she must throw again before moving to the next frame. Finally, make sure to properly handle the last frame, which has its own special rules. In this verion of the code I wasn't even keeping track of the value of throws. I was just writing enough code to keep track of the current frame correctly. My next step was to track the actual throws themselves and to correctly display strikes and spares. Here I stopped to do some refactoring and introduced the Frame class into the code, where prior to that only BowlingLane existed. The last objective was to actually score any given frame correctly, taking into account that the score might not be currently available in the case of a strike or spare. Here I introduced the Throw class to replace simple primitive ints. Note that I've avoided using any mock object frameworks or complex tools. This is just pure Java code and Junit.

# The test code (in a separate folder called 'test')

 ```package bowling; import junit.framework.TestCase; public class TestBowling extends TestCase { public void testFirstFrame() { //setup BowlingLane scorer = new BowlingLane(); //assert assertFrame(scorer, false, 1); } public void testDropPins_Strike() { //setup BowlingLane scorer = new BowlingLane(); //execute scorer.dropPins(10); //assert assertFrame(scorer, false, 2); } public void testDropPins_IncompleteFrame() { //setup BowlingLane scorer = new BowlingLane(); //execute scorer.dropPins(0); assertFrame(scorer, false, 1); } public void testDropPins_CompleteFrame() { //setup BowlingLane scorer = new BowlingLane(); scorer.dropPins(0); //execute scorer.dropPins(0); //assert assertFrame(scorer, false, 2); } public void testDropPins_IncompleteSecondFrame() { //setup BowlingLane scorer = new BowlingLane(); scorer.dropPins(0); scorer.dropPins(0); //execute scorer.dropPins(0); //assert assertFrame(scorer, false, 2); } public void testDropPins_CompleteSecondFrame() { //setup BowlingLane scorer = new BowlingLane(); scorer.dropPins(0); scorer.dropPins(0); scorer.dropPins(0); //execute scorer.dropPins(0); //assert assertFrame(scorer, false, 3); } public void testDropPins_LastFrame_IncompleteGame() { //setup BowlingLane scorer = new LastFrameBowlingLane(); //execute scorer.dropPins(0); //assert assertFrame(scorer, false, 10); } public void testDropPins_LastFrame_CompleteGame() { //setup BowlingLane scorer = new LastFrameBowlingLane(); scorer.dropPins(0); //execute scorer.dropPins(0); //assert assertFrame(scorer, true, 10); } public void testDropPins_LastFrame_Strike_IncompleteGame() { //setup BowlingLane scorer = new LastFrameBowlingLane(); //execute scorer.dropPins(10); //assert assertFrame(scorer, false, 10); } public void testDropPins_LastFrame_TwoStrikes_IncompleteGame() { //setup BowlingLane scorer = new LastFrameBowlingLane(); scorer.dropPins(10); //execute scorer.dropPins(10); //assert assertFrame(scorer, false, 10); } public void testDropPins_LastFrame_Strike_CompleteGame() { //setup BowlingLane scorer = new LastFrameBowlingLane(); scorer.dropPins(10); scorer.dropPins(10); //execute scorer.dropPins(10); //assert assertFrame(scorer, true, 10); } public void testDropPins_LastFrame_Spare_IncompleteGame() { //setup BowlingLane scorer = new LastFrameBowlingLane(); scorer.dropPins(5); //execute scorer.dropPins(5); //assert assertFrame(scorer, false, 10); } public void testDropPins_LastFrame_Spare_CompleteGame() { //setup BowlingLane scorer = new LastFrameBowlingLane(); scorer.dropPins(5); scorer.dropPins(5); //execute scorer.dropPins(0); //assert assertFrame(scorer, true, 10); } private void assertFrame(BowlingLane scorer, boolean gameOver, int currentFrame) { assertEquals("game over", gameOver, scorer.gameOver()); assertEquals("current frame", currentFrame, scorer.currentFramePosition()); } public void testPinsDropped_NormalFrame_OneThrow() { //setup BowlingLane scorer = new BowlingLane(); scorer.dropPins(5); scorer.dropPins(4); //execute String[] pinsDropped = scorer.pinsDropped(1); //assert assertPins(pinsDropped, "5", "4"); } public void testPinsDropped_TwoThrows() { //setup BowlingLane scorer = new BowlingLane(); scorer.dropPins(5); //execute String[] pinsDropped = scorer.pinsDropped(1); //assert assertPins(pinsDropped, "5", ""); } public void testPinsDropped_Strike() { //setup BowlingLane scorer = new BowlingLane(); scorer.dropPins(10); //execute String[] pinsDropped = scorer.pinsDropped(1); //assert assertPins(pinsDropped, "X", ""); } public void testPinsDropped_Spare() { //setup BowlingLane scorer = new BowlingLane(); scorer.dropPins(4); scorer.dropPins(6); //execute String[] pinsDropped = scorer.pinsDropped(1); //assert assertPins(pinsDropped, "4", "/"); } public void testPinsDropped_LastFrame_OneThrow() { //setup BowlingLane scorer = new LastFrameBowlingLane(); scorer.dropPins(4); //execute String[] pinsDropped = scorer.pinsDropped(10); //assert assertPins(pinsDropped, "4", "", ""); } public void testPinsDropped_LastFrame_TwoThrows() { //setup BowlingLane scorer = new LastFrameBowlingLane(); scorer.dropPins(2); scorer.dropPins(6); //execute String[] pinsDropped = scorer.pinsDropped(10); //assert assertPins(pinsDropped, "2", "6", ""); } public void testPinsDropped_LastFrame_Strike() { //setup BowlingLane scorer = new LastFrameBowlingLane(); scorer.dropPins(10); //execute String[] pinsDropped = scorer.pinsDropped(10); //assert assertPins(pinsDropped, "X", "", ""); } public void testPinsDropped_LastFrame_TwoStrikes() { //setup BowlingLane scorer = new LastFrameBowlingLane(); scorer.dropPins(10); scorer.dropPins(10); //execute String[] pinsDropped = scorer.pinsDropped(10); //assert assertPins(pinsDropped, "X", "X", ""); } public void testPinsDropped_LastFrame_ThreeStrikes() { //setup BowlingLane scorer = new LastFrameBowlingLane(); scorer.dropPins(10); scorer.dropPins(10); scorer.dropPins(10); //execute String[] pinsDropped = scorer.pinsDropped(10); //assert assertPins(pinsDropped, "X", "X", "X"); } public void testPinsDropped_LastFrame_TwoStrikesPlusExtraThrow() { //setup BowlingLane scorer = new LastFrameBowlingLane(); scorer.dropPins(10); scorer.dropPins(10); scorer.dropPins(5); //execute String[] pinsDropped = scorer.pinsDropped(10); //assert assertPins(pinsDropped, "X", "X", "5"); } public void testPinsDropped_LastFrame_Spare() { //setup BowlingLane scorer = new LastFrameBowlingLane(); scorer.dropPins(7); scorer.dropPins(3); //execute String[] pinsDropped = scorer.pinsDropped(10); //assert assertPins(pinsDropped, "7", "/", ""); } public void testPinsDropped_LastFrame_SparePlusExtraThrow() { //setup BowlingLane scorer = new LastFrameBowlingLane(); scorer.dropPins(7); scorer.dropPins(3); scorer.dropPins(9); //execute String[] pinsDropped = scorer.pinsDropped(10); //assert assertPins(pinsDropped, "7", "/", "9"); } public void testPinsDropped_LastFrame_SparePlusStrike() { //setup BowlingLane scorer = new LastFrameBowlingLane(); scorer.dropPins(7); scorer.dropPins(3); scorer.dropPins(10); //execute String[] pinsDropped = scorer.pinsDropped(10); //assert assertPins(pinsDropped, "7", "/", "X"); } private void assertPins(String[] pinsDropped, String firstThrow, String secondThrow) { assertEquals("number of throws", 2, pinsDropped.length); assertEquals("first throw", firstThrow, pinsDropped[0]); assertEquals("second throw", secondThrow, pinsDropped[1]); } private void assertPins(String[] pinsDropped, String firstThrow, String secondThrow, String thirdThrow) { assertEquals("number of throws", 3, pinsDropped.length); assertEquals("first throw", firstThrow, pinsDropped[0]); assertEquals("second throw", secondThrow, pinsDropped[1]); assertEquals("third throw", thirdThrow, pinsDropped[2]); } public void testScore_EmptyFrame() { //setup BowlingLane scorer = new BowlingLane(); //execute String score = scorer.score(1); //assert assertEquals("score", "", score); } public void testScore_IncompleteFrame() { //setup BowlingLane scorer = new BowlingLane(); scorer.dropPins(7); //execute String score = scorer.score(1); //assert assertEquals("score", "", score); } public void testScore_CompleteFrame() { //setup BowlingLane scorer = new BowlingLane(); scorer.dropPins(7); scorer.dropPins(2); //execute String score = scorer.score(1); //assert assertEquals("score", "9", score); } public void testScore_Strike_MissingThrows() { //setup BowlingLane scorer = new BowlingLane(); scorer.dropPins(10); //execute String score = scorer.score(1); //assert assertEquals("score", "", score); } public void testScore_Spare() { //setup BowlingLane scorer = new BowlingLane(); scorer.dropPins(5); scorer.dropPins(5); scorer.dropPins(1); //execute String score = scorer.score(1); //assert assertEquals("score", "11", score); } public void testScore_Strike() { //setup BowlingLane scorer = new BowlingLane(); scorer.dropPins(10); scorer.dropPins(5); scorer.dropPins(1); //execute String score = scorer.score(1); //assert assertEquals("score", "16", score); } public void testScore_TwoStrikes() { //setup BowlingLane scorer = new BowlingLane(); scorer.dropPins(10); scorer.dropPins(10); scorer.dropPins(3); //execute String score = scorer.score(1); //assert assertEquals("score", "23", score); } public void testScore_LastFrame_Empty() { //setup BowlingLane scorer = new LastFrameBowlingLane(); //execute String score = scorer.score(10); //assert assertEquals("score", "", score); } public void testScore_LastFrame_OneThrow() { //setup BowlingLane scorer = new LastFrameBowlingLane(); scorer.dropPins(1); //execute String score = scorer.score(10); //assert assertEquals("score", "", score); } public void testScore_LastFrame_TwoThrows() { //setup BowlingLane scorer = new LastFrameBowlingLane(); scorer.dropPins(1); scorer.dropPins(6); //execute String score = scorer.score(10); //assert assertEquals("score", "7", score); } public void testScore_LastFrame_IgnoreThirdThrow() { //setup BowlingLane scorer = new LastFrameBowlingLane(); scorer.dropPins(1); scorer.dropPins(6); scorer.dropPins(2); //execute String score = scorer.score(10); //assert assertEquals("score", "7", score); } public void testScore_LastFrame_Strike() { //setup BowlingLane scorer = new LastFrameBowlingLane(); scorer.dropPins(10); scorer.dropPins(6); scorer.dropPins(2); //execute String score = scorer.score(10); //assert assertEquals("score", "18", score); } public void testScore_LastFrame_Spare() { //setup BowlingLane scorer = new LastFrameBowlingLane(); scorer.dropPins(4); scorer.dropPins(6); scorer.dropPins(2); //execute String score = scorer.score(10); //assert assertEquals("score", "12", score); } public void testScore_CumulativeScore() { //setup BowlingLane scorer = new BowlingLane(); scorer.dropPins(4); scorer.dropPins(2); scorer.dropPins(10); scorer.dropPins(10); scorer.dropPins(5); //execute String score = scorer.score(2); //assert assertEquals("score", "31", score); } public void testPerfectGame() { BowlingLane scorer = new BowlingLane(); scorer.dropPins(10); scorer.dropPins(10); scorer.dropPins(10); scorer.dropPins(10); scorer.dropPins(10); scorer.dropPins(10); scorer.dropPins(10); scorer.dropPins(10); scorer.dropPins(10); scorer.dropPins(10); scorer.dropPins(10); scorer.dropPins(10); String score = scorer.score(10); assertEquals("score", "300", score); assertFrame(scorer, true, 10); String[] pinsDropped = scorer.pinsDropped(1); assertPins(pinsDropped, "X", ""); pinsDropped = scorer.pinsDropped(2); assertPins(pinsDropped, "X", ""); pinsDropped = scorer.pinsDropped(3); assertPins(pinsDropped, "X", ""); pinsDropped = scorer.pinsDropped(4); assertPins(pinsDropped, "X", ""); pinsDropped = scorer.pinsDropped(5); assertPins(pinsDropped, "X", ""); pinsDropped = scorer.pinsDropped(6); assertPins(pinsDropped, "X", ""); pinsDropped = scorer.pinsDropped(7); assertPins(pinsDropped, "X", ""); pinsDropped = scorer.pinsDropped(8); assertPins(pinsDropped, "X", ""); pinsDropped = scorer.pinsDropped(9); assertPins(pinsDropped, "X", ""); pinsDropped = scorer.pinsDropped(10); assertPins(pinsDropped, "X", "X", "X"); } } ```

 ```package bowling; import java.util.List; class NullFrame extends Frame { NullFrame(List frames) { super(frames); } Throw score() { return new Throw(0); } } ```

 ```package bowling; class LastFrameBowlingLane extends BowlingLane { LastFrameBowlingLane() { this.currentFrame = frames.get(frames.size() - 1); frames.set(frames.size() - 2, new NullFrame(frames)); } } ```

# The actual code for the bowling game tracker (in a separate folder called 'bowling')

 ```package bowling; import java.util.ArrayList; import java.util.List; public class BowlingLane { public static final int LAST_FRAME = 10; protected Frame currentFrame; protected List frames = new ArrayList(); public BowlingLane() { for (int i = 0; i < LAST_FRAME - 1; i++) { frames.add(new Frame(frames)); } frames.add(new LastFrame(frames)); currentFrame = frames.get(0); } public int currentFramePosition() { return currentFrame.index() + 1; } public void dropPins(int pinsDropped) { currentFrame.drop(pinsDropped); if (!currentFrame.isLastFrame() && currentFrame.isComplete()) nextFrame(); } private void nextFrame() { currentFrame = currentFrame.nextFrame(); } public boolean gameOver() { return currentFrame.isLastFrame() && currentFrame.isComplete(); } public String[] pinsDropped(int frameNumber) { return frames.get(frameNumber - 1).displayPinsDropped(); } public String score(int frameNumber) { Frame frame = frames.get(frameNumber - 1); Throw score = frame.score(); return score.toString(); } } ```

 ```package bowling; import java.util.List; class Frame { private static final int TOTAL_PINS = 10; private List frames; protected Throw[] pinsDropped = new Throw[2]; protected int throwsMadeInFrame; Frame(List frames) { this.frames = frames; pinsDropped[0] = new Throw(); pinsDropped[1] = new Throw(); } boolean isFirstFrame() { return frames.indexOf(this) == 0; } boolean isLastFrame() { return frames.indexOf(this) == frames.size() - 1; } Frame nextFrame() { return frames.get(frames.indexOf(this) + 1); } Frame previousFrame() { return frames.get(frames.indexOf(this) - 1); } int index() { return frames.indexOf(this); } String[] displayPinsDropped() { String[] pinsDroppedAsStrings = new String[pinsDropped.length]; for (int i = 0; i < pinsDropped.length; i++) { pinsDroppedAsStrings[i] = displayThrow(i, pinsDropped[i]); } return pinsDroppedAsStrings; } private String displayThrow(int throwIndex, Throw pinsDropped) { if (pinsDropped.intValue() == TOTAL_PINS) { return "X"; } else if (isSpare() && throwIndex == 1) { return "/"; } else { return pinsDropped.toString(); } } void drop(int pinsDropped) { this.pinsDropped[throwsMadeInFrame++] = new Throw(pinsDropped); } int throwsMadeInFrame() { return throwsMadeInFrame; } boolean isComplete() { return throwsMadeInFrame() == requiredThrowsForFrame(); } protected int requiredThrowsForFrame() { if (isStrike()) { return 1; } else { return 2; } } protected boolean isStrike() { return firstThrowPinsDropped() == TOTAL_PINS; } protected boolean isSpare() { return !isStrike() && firstThrowPinsDropped() + secondThrowPinsDropped() == TOTAL_PINS; } private int firstThrowPinsDropped() { return getThrow(0).intValue(); } private int secondThrowPinsDropped() { return getThrow(1).intValue(); } Throw score() { Throw scoreForCurrentFrame = scoreCurrentFrame(); if (!isFirstFrame()) { scoreForCurrentFrame = previousFrame().score().add(scoreForCurrentFrame); } return scoreForCurrentFrame; } protected Throw scoreCurrentFrame() { Throw score = null; if (isSpare()) { score = new Throw(10).add(nextFrame().firstThrowForScoring()); } else if (isStrike()) { score = new Throw(10).add(nextFrame().firstThrowForScoring()).add(nextFrame().secondThrowForScoring()); } else { score = firstThrowForScoring().add(secondThrowForScoring()); } return score; } private Throw firstThrowForScoring() { return getThrow(0); } protected Throw secondThrowForScoring() { if (isStrike()) { return nextFrame().firstThrowForScoring(); } return getThrow(1); } protected Throw getThrow(int throwIndex) { return pinsDropped[throwIndex]; } } ```

 ```package bowling; import java.util.List; class LastFrame extends Frame { LastFrame(List frames) { super(frames); pinsDropped = new Throw[3]; pinsDropped[0] = new Throw(); pinsDropped[1] = new Throw(); pinsDropped[2] = new Throw(); } protected int requiredThrowsForFrame() { if (isStrike() || isSpare()) { return 3; } else { return 2; } } protected Throw secondThrowForScoring() { return getThrow(1); } protected Throw scoreCurrentFrame() { if (isSpare() || isStrike()) { return getThrow(0).add(getThrow(1)).add(getThrow(2)); } else { return super.scoreCurrentFrame(); } } } ```

 ```package bowling; import java.util.List; class LastFrame extends Frame { LastFrame(List frames) { super(frames); pinsDropped = new Throw[3]; pinsDropped[0] = new Throw(); pinsDropped[1] = new Throw(); pinsDropped[2] = new Throw(); } protected int requiredThrowsForFrame() { if (isStrike() || isSpare()) { return 3; } else { return 2; } } protected Throw secondThrowForScoring() { return getThrow(1); } protected Throw scoreCurrentFrame() { if (isSpare() || isStrike()) { return getThrow(0).add(getThrow(1)).add(getThrow(2)); } else { return super.scoreCurrentFrame(); } } } ```

 ```package bowling; class Throw { private Integer pinsDropped; Throw() { } Throw(int pinsDropped) { this.pinsDropped = new Integer(pinsDropped); } Throw add(Throw anotherScore) { if (pinsDropped == null || anotherScore.pinsDropped == null) { return new Throw(); } return new Throw(pinsDropped.intValue() + anotherScore.pinsDropped.intValue()); } public String toString() { if (pinsDropped == null) { return ""; } return pinsDropped.toString(); } int intValue() { if (pinsDropped == null) { return 0; } return pinsDropped.intValue(); } } ```

# Further Development

Some things I may add to this example in the future: Handling multiple players per bowling lane and multiple lanes in a given bowling alley. Also, perhaps add error handling if the number of pins dropped is invalid. Finally, while performance is not a significant issue in this example, it may be an interesting exercise to cache the score for a given frame so that scoring frame a particular frame does not cause all previous frame scores to be re-calculated.