by Steve Howell, 2009-12-15
This page describes a short exercise where I implemented a solution to the Bowling Game in Python using a test-driven design. The basic rhythm was to create a new test, make it pass, refactor, repeat.
The whole exercise took about an hour. It was not as painstaking as it might look--I simply recorded each step then later added commentary. I only show diffs that were on the green bar, as this writeup is not intended to show the detailed mechanics of TDD; it is intended more to show how a design evolves from a series of progressively more challenging tests.
The final design has a bowling_score() method that uses a very lightweight Frame object, along with Strike/Spare objects that use functionality from a Bonus class.
Set the parameters of the problem. This is a slight variation from some other "bowling game" solutions that take a complete game as input. My program reports progress on partial games, as if you were at a real bowling alley.
'''
score() needs to return the running score of a
game...if frames are still in bonus, do not count
them yet
'''
|
@@ -1 +1,5 @@ - + ''' + score() needs to return the running score of a + game...if frames are still in bonus, do not count + them yet + ''' |
I just use a home-grown assert_equals method to test my code. Although not shown here, I first verified that it does report problems. Then I verified that it succeeds silently. My topic for today is the bowling game, not test runners. But I will say that a lightweight testing methodology can serve you well.
'''
score() needs to return the running score of a
game...if frames are still in bonus, do not count
them yet
'''
def assert_equals(expected, actual):
if expected != actual:
print expected
print actual
raise Exception('assert_equals fails!')
assert_equals([1], [1])
|
@@ -5 +5,9 @@
'''
+
+ def assert_equals(expected, actual):
+ if expected != actual:
+ print expected
+ print actual
+ raise Exception('assert_equals fails!')
+
+ assert_equals([1], [1])
|
Now on to the bowling game!
The simplest test I can think of is a game with no rolls yet, and it is easy to make pass. (And again I am not showing code that I wrote before the green bar; the topic here is evolution of design, not the mechanics of TDD. But, yes, you must always first get a failing test!)
'''
score() needs to return the running score of a
game...if frames are still in bonus, do not count
them yet
'''
def assert_equals(expected, actual):
if expected != actual:
print expected
print actual
raise Exception('assert_equals fails!')
def score(rolls):
return 0
assert_equals(0, score([]))
|
@@ -12,2 +12,5 @@
- assert_equals([1], [1])
+ def score(rolls):
+ return 0
+
+ assert_equals(0, score([]))
|
I write a test for the situation where I roll only one roll, and it is not a strike. My semantics are that the score is still 0 until the frame is complete.
The test passes for free. Hmmmmm. Whenever a test passes for free, it is cause for reflection. Is the free passing test simply validating your design, or did you code too much too early? Well, here, it is a no-brainer; my implementation is still ridiculously simple. So I move on with two passing tests already under my belt and no "real" code yet written.
'''
score() needs to return the running score of a
game...if frames are still in bonus, do not count
them yet
'''
def assert_equals(expected, actual):
if expected != actual:
print expected
print actual
raise Exception('assert_equals fails!')
def score(rolls):
return 0
assert_equals(0, score([]))
assert_equals(0, score([1]))
|
@@ -16 +16,2 @@
assert_equals(0, score([]))
+ assert_equals(0, score([1]))
|
It is good to reduce duplication in tests, and I do it quite aggressively. It works for me. Here is assert_score:
'''
score() needs to return the running score of a
game...if frames are still in bonus, do not count
them yet
'''
def assert_equals(expected, actual):
if expected != actual:
print expected
print actual
raise Exception('assert_equals fails!')
def score(rolls):
return 0
def assert_score(expected, rolls):
assert_equals(expected, score(rolls))
assert_score(0, [])
assert_score(0, [1])
|
@@ -15,3 +15,5 @@
- assert_equals(0, score([]))
- assert_equals(0, score([1]))
+ def assert_score(expected, rolls):
+ assert_equals(expected, score(rolls))
+ assert_score(0, [])
+ assert_score(0, [1])
|
After rolling a frame, which I can still assume will be two rolls (no strikes or spares yet in my tests), I can score the frame. So I add frame_rolls and frame_score. I also introduce "score," a loop, and a conditional. It is a fairly big step, but nothing too complex. I do step back and read my own code to internalize the current design. So far I am doing simple iteration with per-frame bookkeeping.
'''
score() needs to return the running score of a
game...if frames are still in bonus, do not count
them yet
'''
def assert_equals(expected, actual):
if expected != actual:
print expected
print actual
raise Exception('assert_equals fails!')
def bowling_score(rolls):
score = 0
frame_rolls = 0
frame_score = 0
for roll in rolls:
frame_rolls += 1
frame_score += roll
if frame_rolls == 2:
score += frame_score
return score
def assert_score(expected, rolls):
assert_equals(expected, bowling_score(rolls))
assert_score(0, [])
assert_score(0, [1])
assert_score(2, [1, 1])
|
@@ -12,8 +12,18 @@
- def score(rolls):
- return 0
+ def bowling_score(rolls):
+ score = 0
+ frame_rolls = 0
+ frame_score = 0
+ for roll in rolls:
+ frame_rolls += 1
+ frame_score += roll
+ if frame_rolls == 2:
+ score += frame_score
+ return score
def assert_score(expected, rolls):
- assert_equals(expected, score(rolls))
+ assert_equals(expected, bowling_score(rolls))
+
assert_score(0, [])
assert_score(0, [1])
+ assert_score(2, [1, 1])
|
The duplication of "frame_" in frame_rolls and frame_score says there is a class. I do not overthink this too much; for now, I simply extract a simple class for bookkeeping.
'''
score() needs to return the running score of a
game...if frames are still in bonus, do not count
them yet
'''
def assert_equals(expected, actual):
if expected != actual:
print expected
print actual
raise Exception('assert_equals fails!')
class Frame:
pass
def bowling_score(rolls):
score = 0
frame = Frame()
frame.rolls = 0
frame.score = 0
for roll in rolls:
frame.rolls += 1
frame.score += roll
if frame.rolls == 2:
score += frame.score
return score
def assert_score(expected, rolls):
assert_equals(expected, bowling_score(rolls))
assert_score(0, [])
assert_score(0, [1])
assert_score(2, [1, 1])
|
@@ -12,11 +12,15 @@
+ class Frame:
+ pass
+
def bowling_score(rolls):
score = 0
- frame_rolls = 0
- frame_score = 0
+ frame = Frame()
+ frame.rolls = 0
+ frame.score = 0
for roll in rolls:
- frame_rolls += 1
- frame_score += roll
- if frame_rolls == 2:
- score += frame_score
+ frame.rolls += 1
+ frame.score += roll
+ if frame.rolls == 2:
+ score += frame.score
return score
|
And then I move the initialization steps into the class. In Pythonic style, I do not fret over attributes still used by the calling code. They might go away later, anyway.
'''
score() needs to return the running score of a
game...if frames are still in bonus, do not count
them yet
'''
def assert_equals(expected, actual):
if expected != actual:
print expected
print actual
raise Exception('assert_equals fails!')
class Frame:
def __init__(frame):
frame.rolls = 0
frame.score = 0
def bowling_score(rolls):
score = 0
frame = Frame()
for roll in rolls:
frame.rolls += 1
frame.score += roll
if frame.rolls == 2:
score += frame.score
return score
def assert_score(expected, rolls):
assert_equals(expected, bowling_score(rolls))
assert_score(0, [])
assert_score(0, [1])
assert_score(2, [1, 1])
|
@@ -13,3 +13,5 @@
class Frame:
- pass
+ def __init__(frame):
+ frame.rolls = 0
+ frame.score = 0
@@ -18,4 +20,2 @@
frame = Frame()
- frame.rolls = 0
- frame.score = 0
for roll in rolls:
|
The decision to extract a Frame class pays off immediately, as my next test is to roll three balls, which motivates resetting the frame.
'''
score() needs to return the running score of a
game...if frames are still in bonus, do not count
them yet
'''
def assert_equals(expected, actual):
if expected != actual:
print expected
print actual
raise Exception('assert_equals fails!')
class Frame:
def __init__(frame):
frame.reset()
def reset(frame):
frame.rolls = 0
frame.score = 0
def bowling_score(rolls):
score = 0
frame = Frame()
for roll in rolls:
frame.rolls += 1
frame.score += roll
if frame.rolls == 2:
score += frame.score
frame.reset()
return score
def assert_score(expected, rolls):
assert_equals(expected, bowling_score(rolls))
assert_score(0, [])
assert_score(0, [1])
assert_score(2, [1, 1])
assert_score(2, [1, 1, 1])
|
@@ -14,2 +14,5 @@
def __init__(frame):
+ frame.reset()
+
+ def reset(frame):
frame.rolls = 0
@@ -25,2 +28,3 @@
score += frame.score
+ frame.reset()
return score
@@ -33 +37,2 @@
assert_score(2, [1, 1])
+ assert_score(2, [1, 1, 1])
|
As my tests are starting to get more subtle, I add a description field to all the tests, which will conveniently show up at least in the backtraces, even if I do nothing else with it.
'''
score() needs to return the running score of a
game...if frames are still in bonus, do not count
them yet
'''
def assert_equals(expected, actual):
if expected != actual:
print expected
print actual
raise Exception('assert_equals fails!')
class Frame:
def __init__(frame):
frame.reset()
def reset(frame):
frame.rolls = 0
frame.score = 0
def bowling_score(rolls):
score = 0
frame = Frame()
for roll in rolls:
frame.rolls += 1
frame.score += roll
if frame.rolls == 2:
score += frame.score
frame.reset()
return score
def assert_score(expected, rolls, description):
assert_equals(expected, bowling_score(rolls))
assert_score(0, [], 'no rolls')
assert_score(0, [1], 'half frame')
assert_score(2, [1, 1], 'full frame')
assert_score(2, [1, 1, 1], '1.5 frames')
|
@@ -31,8 +31,8 @@
- def assert_score(expected, rolls):
+ def assert_score(expected, rolls, description):
assert_equals(expected, bowling_score(rolls))
- assert_score(0, [])
- assert_score(0, [1])
- assert_score(2, [1, 1])
- assert_score(2, [1, 1, 1])
+ assert_score(0, [], 'no rolls')
+ assert_score(0, [1], 'half frame')
+ assert_score(2, [1, 1], 'full frame')
+ assert_score(2, [1, 1, 1], '1.5 frames')
|
I get another passing test for free when I roll the fourth ball.
'''
score() needs to return the running score of a
game...if frames are still in bonus, do not count
them yet
'''
def assert_equals(expected, actual):
if expected != actual:
print expected
print actual
raise Exception('assert_equals fails!')
class Frame:
def __init__(frame):
frame.reset()
def reset(frame):
frame.rolls = 0
frame.score = 0
def bowling_score(rolls):
score = 0
frame = Frame()
for roll in rolls:
frame.rolls += 1
frame.score += roll
if frame.rolls == 2:
score += frame.score
frame.reset()
return score
def assert_score(expected, rolls, description):
assert_equals(expected, bowling_score(rolls))
assert_score(0, [], 'no rolls')
assert_score(0, [1], 'half frame')
assert_score(2, [1, 1], 'full frame')
assert_score(2, [1, 1, 1], '1.5 frames')
assert_score(4, [1, 1, 1, 1], '2 frames')
|
@@ -38 +38,2 @@
assert_score(2, [1, 1, 1], '1.5 frames')
+ assert_score(4, [1, 1, 1, 1], '2 frames')
|
Now to start tackling spares. Spares do not actually get scored until the subsequent roll, so I just set a flag for now and bypass scoring until I write the next test. The flag does not do anything except express my current mindset about the problem. Some purists might defer the flag until the next checkin.
'''
score() needs to return the running score of a
game...if frames are still in bonus, do not count
them yet
'''
def assert_equals(expected, actual):
if expected != actual:
print expected
print actual
raise Exception('assert_equals fails!')
class Frame:
def __init__(frame):
frame.reset()
def reset(frame):
frame.rolls = 0
frame.score = 0
def bowling_score(rolls):
score = 0
frame = Frame()
spare_pending = 0
for roll in rolls:
frame.rolls += 1
frame.score += roll
if frame.rolls == 2:
if frame.score == 10:
spare_pending = 1
else:
score += frame.score
frame.reset()
return score
def assert_score(expected, rolls, description):
assert_equals(expected, bowling_score(rolls))
assert_score(0, [], 'no rolls')
assert_score(0, [1], 'half frame')
assert_score(2, [1, 1], 'full frame')
assert_score(2, [1, 1, 1], '1.5 frames')
assert_score(4, [1, 1, 1, 1], '2 frames')
assert_score(0, [5, 5], 'spare pending')
|
@@ -23,2 +23,3 @@
frame = Frame()
+ spare_pending = 0
for roll in rolls:
@@ -27,3 +28,6 @@
if frame.rolls == 2:
- score += frame.score
+ if frame.score == 10:
+ spare_pending = 1
+ else:
+ score += frame.score
frame.reset()
@@ -39 +43,2 @@
assert_score(4, [1, 1, 1, 1], '2 frames')
+ assert_score(0, [5, 5], 'spare pending')
|
Now to reward the spare, I look for spare_pending, and if it has the value 1, I reward the spare. I go ahead and clear the flag now while I am thinking of it, although I should really drive that line of code with a test.
'''
score() needs to return the running score of a
game...if frames are still in bonus, do not count
them yet
'''
def assert_equals(expected, actual):
if expected != actual:
print expected
print actual
raise Exception('assert_equals fails!')
class Frame:
def __init__(frame):
frame.reset()
def reset(frame):
frame.rolls = 0
frame.score = 0
def bowling_score(rolls):
score = 0
frame = Frame()
spare_pending = 0
for roll in rolls:
frame.rolls += 1
frame.score += roll
if spare_pending:
score += 10 + roll
spare_pending = 0
if frame.rolls == 2:
if frame.score == 10:
spare_pending = 1
else:
score += frame.score
frame.reset()
return score
def assert_score(expected, rolls, description):
assert_equals(expected, bowling_score(rolls))
assert_score(0, [], 'no rolls')
assert_score(0, [1], 'half frame')
assert_score(2, [1, 1], 'full frame')
assert_score(2, [1, 1, 1], '1.5 frames')
assert_score(4, [1, 1, 1, 1], '2 frames')
assert_score(0, [5, 5], 'spare pending')
assert_score(13, [5, 5, 3], 'spare rewarded')
|
@@ -27,2 +27,5 @@
frame.score += roll
+ if spare_pending:
+ score += 10 + roll
+ spare_pending = 0
if frame.rolls == 2:
@@ -44 +47,2 @@
assert_score(0, [5, 5], 'spare pending')
+ assert_score(13, [5, 5, 3], 'spare rewarded')
|
The next test passes for free. In retrospect I should have written a test where the follow-up frame to the spare has the bowler knocking down pins in both rolls, especially the second roll, which is no longer in the "bonus." Using test values of zero can lead to false positives where you are not properly resetting counts or clearing flags. It did not burn me this time, but it was good to record this session so I do not make that mistake in the future.
'''
score() needs to return the running score of a
game...if frames are still in bonus, do not count
them yet
'''
def assert_equals(expected, actual):
if expected != actual:
print expected
print actual
raise Exception('assert_equals fails!')
class Frame:
def __init__(frame):
frame.reset()
def reset(frame):
frame.rolls = 0
frame.score = 0
def bowling_score(rolls):
score = 0
frame = Frame()
spare_pending = 0
for roll in rolls:
frame.rolls += 1
frame.score += roll
if spare_pending:
score += 10 + roll
spare_pending = 0
if frame.rolls == 2:
if frame.score == 10:
spare_pending = 1
else:
score += frame.score
frame.reset()
return score
def assert_score(expected, rolls, description):
assert_equals(expected, bowling_score(rolls))
assert_score(0, [], 'no rolls')
assert_score(0, [1], 'half frame')
assert_score(2, [1, 1], 'full frame')
assert_score(2, [1, 1, 1], '1.5 frames')
assert_score(4, [1, 1, 1, 1], '2 frames')
assert_score(0, [5, 5], 'spare pending')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(16, [5, 5, 3, 0], 'spare then normal')
|
@@ -48 +48,2 @@
assert_score(13, [5, 5, 3], 'spare rewarded')
+ assert_score(16, [5, 5, 3, 0], 'spare then normal')
|
Perhaps I should have tested spares a little more, but the implementation seemed correct to me, so I perhaps prematurely moved on to strikes. If I had been pairing, I think my partner would have asked for a little more coverage on spares, which are a particularly good warmup for strikes (in bowling and coding!).
Oh well, on to strikes!
To get the first strike test passing, I just need to make sure strikes do not get scored right away. I mimic the technique with spares, except this time I set the pending count to 2, as strikes require two more rolls before they get scored.
'''
score() needs to return the running score of a
game...if frames are still in bonus, do not count
them yet
'''
def assert_equals(expected, actual):
if expected != actual:
print expected
print actual
raise Exception('assert_equals fails!')
class Frame:
def __init__(frame):
frame.reset()
def reset(frame):
frame.rolls = 0
frame.score = 0
def bowling_score(rolls):
score = 0
frame = Frame()
spare_pending = 0
strike_pending = 0
for roll in rolls:
frame.rolls += 1
frame.score += roll
if spare_pending:
score += 10 + roll
spare_pending = 0
if frame.rolls == 1:
if frame.score == 10:
strike_pending = 2
else:
if frame.score == 10:
spare_pending = 1
else:
score += frame.score
frame.reset()
return score
def assert_score(expected, rolls, description):
assert_equals(expected, bowling_score(rolls))
assert_score(0, [], 'no rolls')
assert_score(0, [1], 'half frame')
assert_score(2, [1, 1], 'full frame')
assert_score(2, [1, 1, 1], '1.5 frames')
assert_score(4, [1, 1, 1, 1], '2 frames')
assert_score(0, [5, 5], 'spare pending')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(16, [5, 5, 3, 0], 'spare then normal')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(0, [10], 'strike pending')
|
@@ -24,2 +24,3 @@
spare_pending = 0
+ strike_pending = 0
for roll in rolls:
@@ -30,3 +31,6 @@
spare_pending = 0
- if frame.rolls == 2:
+ if frame.rolls == 1:
+ if frame.score == 10:
+ strike_pending = 2
+ else:
if frame.score == 10:
@@ -49 +53,3 @@
assert_score(16, [5, 5, 3, 0], 'spare then normal')
+ assert_score(13, [5, 5, 3], 'spare rewarded')
+ assert_score(0, [10], 'strike pending')
|
The next test motivates the need to reset the frame.
'''
score() needs to return the running score of a
game...if frames are still in bonus, do not count
them yet
'''
def assert_equals(expected, actual):
if expected != actual:
print expected
print actual
raise Exception('assert_equals fails!')
class Frame:
def __init__(frame):
frame.reset()
def reset(frame):
frame.rolls = 0
frame.score = 0
def bowling_score(rolls):
score = 0
frame = Frame()
spare_pending = 0
strike_pending = 0
for roll in rolls:
frame.rolls += 1
frame.score += roll
if spare_pending:
score += 10 + roll
spare_pending = 0
if frame.rolls == 1:
if frame.score == 10:
strike_pending = 2
frame.reset()
else:
if frame.score == 10:
spare_pending = 1
else:
score += frame.score
frame.reset()
return score
def assert_score(expected, rolls, description):
assert_equals(expected, bowling_score(rolls))
assert_score(0, [], 'no rolls')
assert_score(0, [1], 'half frame')
assert_score(2, [1, 1], 'full frame')
assert_score(2, [1, 1, 1], '1.5 frames')
assert_score(4, [1, 1, 1, 1], '2 frames')
assert_score(0, [5, 5], 'spare pending')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(16, [5, 5, 3, 0], 'spare then normal')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(0, [10], 'strike pending')
assert_score(0, [10, 1], 'strike pending')
|
@@ -34,2 +34,3 @@
strike_pending = 2
+ frame.reset()
else:
@@ -55 +56,2 @@
assert_score(0, [10], 'strike pending')
+ assert_score(0, [10, 1], 'strike pending')
|
The next step requires me to actually reward the strike. Here I made an explicit design decision to just do a little countdown on each subsequent roll. My metaphor here is the bowler mentally keeping track of rolls that are in the "bonus." I actually do bowl, so I know that as a bowler I keep track of when I'm rolling for double points, and if my brain can handle that when I'm bowling (which usually involves revelry and lots of noise), then it is a simple enough scorekeeping strategy. So I run with it.
'''
score() needs to return the running score of a
game...if frames are still in bonus, do not count
them yet
'''
def assert_equals(expected, actual):
if expected != actual:
print expected
print actual
raise Exception('assert_equals fails!')
class Frame:
def __init__(frame):
frame.reset()
def reset(frame):
frame.rolls = 0
frame.score = 0
def bowling_score(rolls):
score = 0
frame = Frame()
spare_pending = 0
strike_pending = 0
strike_bonus = 0
for roll in rolls:
frame.rolls += 1
frame.score += roll
if spare_pending:
score += 10 + roll
spare_pending = 0
if strike_pending:
strike_bonus += roll
strike_pending -= 1
if strike_pending == 0:
score += strike_bonus + 10
if frame.rolls == 1:
if frame.score == 10:
strike_pending = 2
frame.reset()
else:
if frame.score == 10:
spare_pending = 1
else:
score += frame.score
frame.reset()
return score
def assert_score(expected, rolls, description):
assert_equals(expected, bowling_score(rolls))
assert_score(0, [], 'no rolls')
assert_score(0, [1], 'half frame')
assert_score(2, [1, 1], 'full frame')
assert_score(2, [1, 1, 1], '1.5 frames')
assert_score(4, [1, 1, 1, 1], '2 frames')
assert_score(0, [5, 5], 'spare pending')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(16, [5, 5, 3, 0], 'spare then normal')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(0, [10], 'strike pending')
assert_score(0, [10, 1], 'strike pending')
assert_score(14, [10, 1, 1], 'strike rewarded')
|
@@ -25,2 +25,3 @@
strike_pending = 0
+ strike_bonus = 0
for roll in rolls:
@@ -31,2 +32,7 @@
spare_pending = 0
+ if strike_pending:
+ strike_bonus += roll
+ strike_pending -= 1
+ if strike_pending == 0:
+ score += strike_bonus + 10
if frame.rolls == 1:
@@ -57 +63,2 @@
assert_score(0, [10, 1], 'strike pending')
+ assert_score(14, [10, 1, 1], 'strike rewarded')
|
In looking at the code I notice that the line of code to score strikes could be made more parallel to the line of the code that scores spares, so I rearrange an expression. I also boldly combine two steps into one before running the test by moving assert_score() higher in the file. This is purely to minimize scrolling within the file, so my brain has to do fewer context switches.
'''
score() needs to return the running score of a
game...if frames are still in bonus, do not count
them yet
'''
def assert_equals(expected, actual):
if expected != actual:
print expected
print actual
raise Exception('assert_equals fails!')
def assert_score(expected, rolls, description):
assert_equals(expected, bowling_score(rolls))
class Frame:
def __init__(frame):
frame.reset()
def reset(frame):
frame.rolls = 0
frame.score = 0
def bowling_score(rolls):
score = 0
frame = Frame()
spare_pending = 0
strike_pending = 0
strike_bonus = 0
for roll in rolls:
frame.rolls += 1
frame.score += roll
if spare_pending:
score += 10 + roll
spare_pending = 0
if strike_pending:
strike_bonus += roll
strike_pending -= 1
if strike_pending == 0:
score += 10 + strike_bonus
if frame.rolls == 1:
if frame.score == 10:
strike_pending = 2
frame.reset()
else:
if frame.score == 10:
spare_pending = 1
else:
score += frame.score
frame.reset()
return score
assert_score(0, [], 'no rolls')
assert_score(0, [1], 'half frame')
assert_score(2, [1, 1], 'full frame')
assert_score(2, [1, 1, 1], '1.5 frames')
assert_score(4, [1, 1, 1, 1], '2 frames')
assert_score(0, [5, 5], 'spare pending')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(16, [5, 5, 3, 0], 'spare then normal')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(0, [10], 'strike pending')
assert_score(0, [10, 1], 'strike pending')
assert_score(14, [10, 1, 1], 'strike rewarded')
|
@@ -12,2 +12,5 @@
+ def assert_score(expected, rolls, description):
+ assert_equals(expected, bowling_score(rolls))
+
class Frame:
@@ -36,3 +39,3 @@
if strike_pending == 0:
- score += strike_bonus + 10
+ score += 10 + strike_bonus
if frame.rolls == 1:
@@ -49,5 +52,2 @@
- def assert_score(expected, rolls, description):
- assert_equals(expected, bowling_score(rolls))
-
assert_score(0, [], 'no rolls')
|
A little more file reorganization. How can I advocate test-first if I do not actually put the tests first within the file? :) After moving the tests around, rest assured that I got a quick failing test (not shown here) to make sure the tests still actually run.
'''
score() needs to return the running score of a
game...if frames are still in bonus, do not count
them yet
'''
def assert_equals(expected, actual):
if expected != actual:
print expected
print actual
raise Exception('assert_equals fails!')
def assert_score(expected, rolls, description):
assert_equals(expected, bowling_score(rolls))
def run_tests():
assert_score(0, [], 'no rolls')
assert_score(0, [1], 'half frame')
assert_score(2, [1, 1], 'full frame')
assert_score(2, [1, 1, 1], '1.5 frames')
assert_score(4, [1, 1, 1, 1], '2 frames')
assert_score(0, [5, 5], 'spare pending')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(16, [5, 5, 3, 0], 'spare then normal')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(0, [10], 'strike pending')
assert_score(0, [10, 1], 'strike pending')
assert_score(14, [10, 1, 1], 'strike rewarded')
class Frame:
def __init__(frame):
frame.reset()
def reset(frame):
frame.rolls = 0
frame.score = 0
def bowling_score(rolls):
score = 0
frame = Frame()
spare_pending = 0
strike_pending = 0
strike_bonus = 0
for roll in rolls:
frame.rolls += 1
frame.score += roll
if spare_pending:
score += 10 + roll
spare_pending = 0
if strike_pending:
strike_bonus += roll
strike_pending -= 1
if strike_pending == 0:
score += 10 + strike_bonus
if frame.rolls == 1:
if frame.score == 10:
strike_pending = 2
frame.reset()
else:
if frame.score == 10:
spare_pending = 1
else:
score += frame.score
frame.reset()
return score
run_tests()
|
@@ -15,2 +15,16 @@
+ def run_tests():
+ assert_score(0, [], 'no rolls')
+ assert_score(0, [1], 'half frame')
+ assert_score(2, [1, 1], 'full frame')
+ assert_score(2, [1, 1, 1], '1.5 frames')
+ assert_score(4, [1, 1, 1, 1], '2 frames')
+ assert_score(0, [5, 5], 'spare pending')
+ assert_score(13, [5, 5, 3], 'spare rewarded')
+ assert_score(16, [5, 5, 3, 0], 'spare then normal')
+ assert_score(13, [5, 5, 3], 'spare rewarded')
+ assert_score(0, [10], 'strike pending')
+ assert_score(0, [10, 1], 'strike pending')
+ assert_score(14, [10, 1, 1], 'strike rewarded')
+
class Frame:
@@ -52,13 +66,2 @@
- assert_score(0, [], 'no rolls')
- assert_score(0, [1], 'half frame')
- assert_score(2, [1, 1], 'full frame')
- assert_score(2, [1, 1, 1], '1.5 frames')
- assert_score(4, [1, 1, 1, 1], '2 frames')
- assert_score(0, [5, 5], 'spare pending')
- assert_score(13, [5, 5, 3], 'spare rewarded')
- assert_score(16, [5, 5, 3, 0], 'spare then normal')
- assert_score(13, [5, 5, 3], 'spare rewarded')
- assert_score(0, [10], 'strike pending')
- assert_score(0, [10, 1], 'strike pending')
- assert_score(14, [10, 1, 1], 'strike rewarded')
+ run_tests()
|
Again, the strike-underscore duplication (see "strike_pending" and "strike_bonus" vars) suggests a class. Absracting Strike may be premature, but the whole object of bowling is to roll Strikes, and I am going to believe the code is speaking to me with the simple duplication smell. We'll see if it works out.
'''
score() needs to return the running score of a
game...if frames are still in bonus, do not count
them yet
'''
def assert_equals(expected, actual):
if expected != actual:
print expected
print actual
raise Exception('assert_equals fails!')
def assert_score(expected, rolls, description):
assert_equals(expected, bowling_score(rolls))
def run_tests():
assert_score(0, [], 'no rolls')
assert_score(0, [1], 'half frame')
assert_score(2, [1, 1], 'full frame')
assert_score(2, [1, 1, 1], '1.5 frames')
assert_score(4, [1, 1, 1, 1], '2 frames')
assert_score(0, [5, 5], 'spare pending')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(16, [5, 5, 3, 0], 'spare then normal')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(0, [10], 'strike pending')
assert_score(0, [10, 1], 'strike pending')
assert_score(14, [10, 1, 1], 'strike rewarded')
class Frame:
def __init__(frame):
frame.reset()
def reset(frame):
frame.rolls = 0
frame.score = 0
class Strike:
pass
def bowling_score(rolls):
score = 0
frame = Frame()
spare_pending = 0
strike = Strike()
strike.pending = 0
strike.bonus = 0
for roll in rolls:
frame.rolls += 1
frame.score += roll
if spare_pending:
score += 10 + roll
spare_pending = 0
if strike.pending:
strike.bonus += roll
strike.pending -= 1
if strike.pending == 0:
score += 10 + strike.bonus
if frame.rolls == 1:
if frame.score == 10:
strike.pending = 2
frame.reset()
else:
if frame.score == 10:
spare_pending = 1
else:
score += frame.score
frame.reset()
return score
run_tests()
|
@@ -37,2 +37,5 @@
+ class Strike:
+ pass
+
def bowling_score(rolls):
@@ -41,4 +44,5 @@
spare_pending = 0
- strike_pending = 0
- strike_bonus = 0
+ strike = Strike()
+ strike.pending = 0
+ strike.bonus = 0
for roll in rolls:
@@ -49,10 +53,10 @@
spare_pending = 0
- if strike_pending:
- strike_bonus += roll
- strike_pending -= 1
- if strike_pending == 0:
- score += 10 + strike_bonus
+ if strike.pending:
+ strike.bonus += roll
+ strike.pending -= 1
+ if strike.pending == 0:
+ score += 10 + strike.bonus
if frame.rolls == 1:
if frame.score == 10:
- strike_pending = 2
+ strike.pending = 2
frame.reset()
|
Well, if nothing else, extracting a Strike class does allow me to move some code out of bowling_score, which is getting a bit long.
'''
score() needs to return the running score of a
game...if frames are still in bonus, do not count
them yet
'''
def assert_equals(expected, actual):
if expected != actual:
print expected
print actual
raise Exception('assert_equals fails!')
def assert_score(expected, rolls, description):
assert_equals(expected, bowling_score(rolls))
def run_tests():
assert_score(0, [], 'no rolls')
assert_score(0, [1], 'half frame')
assert_score(2, [1, 1], 'full frame')
assert_score(2, [1, 1, 1], '1.5 frames')
assert_score(4, [1, 1, 1, 1], '2 frames')
assert_score(0, [5, 5], 'spare pending')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(16, [5, 5, 3, 0], 'spare then normal')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(0, [10], 'strike pending')
assert_score(0, [10, 1], 'strike pending')
assert_score(14, [10, 1, 1], 'strike rewarded')
class Frame:
def __init__(frame):
frame.reset()
def reset(frame):
frame.rolls = 0
frame.score = 0
class Strike:
def __init__(strike):
strike.pending = 0
strike.bonus = 0
def bowling_score(rolls):
score = 0
frame = Frame()
spare_pending = 0
strike = Strike()
for roll in rolls:
frame.rolls += 1
frame.score += roll
if spare_pending:
score += 10 + roll
spare_pending = 0
if strike.pending:
strike.bonus += roll
strike.pending -= 1
if strike.pending == 0:
score += 10 + strike.bonus
if frame.rolls == 1:
if frame.score == 10:
strike.pending = 2
frame.reset()
else:
if frame.score == 10:
spare_pending = 1
else:
score += frame.score
frame.reset()
return score
run_tests()
|
@@ -38,3 +38,5 @@
class Strike:
- pass
+ def __init__(strike):
+ strike.pending = 0
+ strike.bonus = 0
@@ -45,4 +47,2 @@
strike = Strike()
- strike.pending = 0
- strike.bonus = 0
for roll in rolls:
|
And we can pull out a little more code! I might not completely understand the Strike abstraction yet, but it is serving me well. In Python you usually use "self" where I use "strike," but it is only a convention.
'''
score() needs to return the running score of a
game...if frames are still in bonus, do not count
them yet
'''
def assert_equals(expected, actual):
if expected != actual:
print expected
print actual
raise Exception('assert_equals fails!')
def assert_score(expected, rolls, description):
assert_equals(expected, bowling_score(rolls))
def run_tests():
assert_score(0, [], 'no rolls')
assert_score(0, [1], 'half frame')
assert_score(2, [1, 1], 'full frame')
assert_score(2, [1, 1, 1], '1.5 frames')
assert_score(4, [1, 1, 1, 1], '2 frames')
assert_score(0, [5, 5], 'spare pending')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(16, [5, 5, 3, 0], 'spare then normal')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(0, [10], 'strike pending')
assert_score(0, [10, 1], 'strike pending')
assert_score(14, [10, 1, 1], 'strike rewarded')
class Frame:
def __init__(frame):
frame.reset()
def reset(frame):
frame.rolls = 0
frame.score = 0
class Strike:
def __init__(strike):
strike.pending = 0
strike.bonus = 0
def reward(strike, roll):
if strike.pending:
strike.bonus += roll
strike.pending -= 1
if strike.pending == 0:
return 10 + strike.bonus
return 0
def bowling_score(rolls):
score = 0
frame = Frame()
spare_pending = 0
strike = Strike()
for roll in rolls:
frame.rolls += 1
frame.score += roll
if spare_pending:
score += 10 + roll
spare_pending = 0
score += strike.reward(roll)
if frame.rolls == 1:
if frame.score == 10:
strike.pending = 2
frame.reset()
else:
if frame.score == 10:
spare_pending = 1
else:
score += frame.score
frame.reset()
return score
run_tests()
|
@@ -42,2 +42,10 @@
+ def reward(strike, roll):
+ if strike.pending:
+ strike.bonus += roll
+ strike.pending -= 1
+ if strike.pending == 0:
+ return 10 + strike.bonus
+ return 0
+
def bowling_score(rolls):
@@ -53,7 +61,3 @@
spare_pending = 0
- if strike.pending:
- strike.bonus += roll
- strike.pending -= 1
- if strike.pending == 0:
- score += 10 + strike.bonus
+ score += strike.reward(roll)
if frame.rolls == 1:
|
Okay, the next change was kinda big. I was about to write the test for the "turkey," which is three strikes in a row. It got me thinking that, unlike a spare, with strikes you often have two pending.
Wasn't it convenient that I already had a Strike class?
Even though my tests so far only have one pending strike, I decide to do a spike of having a collection of Strike objects pending. I go ahead and do this all on the green bar. It looks like a positive step that I am now creating Strike objects when I actually roll a strike, and there is no more need to reach into Strike to set the pending count to "2," because I do it in the initializer. I make a mental note to roll back to this version if the collection concept fails.
(In making this change, I inadvertently leave a useless line of code around, which I catch later just by observation.)
'''
score() needs to return the running score of a
game...if frames are still in bonus, do not count
them yet
'''
def assert_equals(expected, actual):
if expected != actual:
print expected
print actual
raise Exception('assert_equals fails!')
def assert_score(expected, rolls, description):
assert_equals(expected, bowling_score(rolls))
def run_tests():
assert_score(0, [], 'no rolls')
assert_score(0, [1], 'half frame')
assert_score(2, [1, 1], 'full frame')
assert_score(2, [1, 1, 1], '1.5 frames')
assert_score(4, [1, 1, 1, 1], '2 frames')
assert_score(0, [5, 5], 'spare pending')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(16, [5, 5, 3, 0], 'spare then normal')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(0, [10], 'strike pending')
assert_score(0, [10, 1], 'strike pending')
assert_score(14, [10, 1, 1], 'strike rewarded')
class Frame:
def __init__(frame):
frame.reset()
def reset(frame):
frame.rolls = 0
frame.score = 0
class Strike:
def __init__(strike):
strike.pending = 2
strike.bonus = 0
def reward(strike, roll):
if strike.pending:
strike.bonus += roll
strike.pending -= 1
if strike.pending == 0:
return 10 + strike.bonus
return 0
def bowling_score(rolls):
score = 0
frame = Frame()
spare_pending = 0
strikes = []
strike = Strike()
for roll in rolls:
frame.rolls += 1
frame.score += roll
if spare_pending:
score += 10 + roll
spare_pending = 0
for strike in strikes:
score += strike.reward(roll)
if frame.rolls == 1:
if frame.score == 10:
strikes.append(Strike())
frame.reset()
else:
if frame.score == 10:
spare_pending = 1
else:
score += frame.score
frame.reset()
return score
run_tests()
|
@@ -39,3 +39,3 @@
def __init__(strike):
- strike.pending = 0
+ strike.pending = 2
strike.bonus = 0
@@ -54,2 +54,3 @@
spare_pending = 0
+ strikes = []
strike = Strike()
@@ -61,6 +62,7 @@
spare_pending = 0
- score += strike.reward(roll)
+ for strike in strikes:
+ score += strike.reward(roll)
if frame.rolls == 1:
if frame.score == 10:
- strike.pending = 2
+ strikes.append(Strike())
frame.reset()
|
Having done a little bit of "prefactoring," I write the "turkey" test and it passes for free!
'''
score() needs to return the running score of a
game...if frames are still in bonus, do not count
them yet
'''
def assert_equals(expected, actual):
if expected != actual:
print expected
print actual
raise Exception('assert_equals fails!')
def assert_score(expected, rolls, description):
assert_equals(expected, bowling_score(rolls))
def run_tests():
assert_score(0, [], 'no rolls')
assert_score(0, [1], 'half frame')
assert_score(2, [1, 1], 'full frame')
assert_score(2, [1, 1, 1], '1.5 frames')
assert_score(4, [1, 1, 1, 1], '2 frames')
assert_score(0, [5, 5], 'spare pending')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(16, [5, 5, 3, 0], 'spare then normal')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(0, [10], 'strike pending')
assert_score(0, [10, 1], 'strike pending')
assert_score(14, [10, 1, 1], 'strike rewarded')
assert_score(30, [10, 10, 10], '1 of 3 strike rewarded')
class Frame:
def __init__(frame):
frame.reset()
def reset(frame):
frame.rolls = 0
frame.score = 0
class Strike:
def __init__(strike):
strike.pending = 2
strike.bonus = 0
def reward(strike, roll):
if strike.pending:
strike.bonus += roll
strike.pending -= 1
if strike.pending == 0:
return 10 + strike.bonus
return 0
def bowling_score(rolls):
score = 0
frame = Frame()
spare_pending = 0
strikes = []
strike = Strike()
for roll in rolls:
frame.rolls += 1
frame.score += roll
if spare_pending:
score += 10 + roll
spare_pending = 0
for strike in strikes:
score += strike.reward(roll)
if frame.rolls == 1:
if frame.score == 10:
strikes.append(Strike())
frame.reset()
else:
if frame.score == 10:
spare_pending = 1
else:
score += frame.score
frame.reset()
return score
run_tests()
|
@@ -28,2 +28,3 @@
assert_score(14, [10, 1, 1], 'strike rewarded')
+ assert_score(30, [10, 10, 10], '1 of 3 strike rewarded')
|
Now I notice that strikes are treated as a collection, but I assume only one spare. The code seems to lack symmetry. The only difference between a spare and a strike is how many rolls you get in the "bonus." I decide to generalize. Generalizations can be premature, and this might be a good example. But the code is speaking to me with its lack of symmetry. So I make a mental note to roll back to this version if need be and see how a Bonus class would work. I first make Strike a subclass of Bonus. (Well, actually Strike is just a method that returns a Bonus object, but in Python, the caller does not care, and there is no reason to create a full blown subclass just to set one variable during instantiation.)
'''
score() needs to return the running score of a
game...if frames are still in bonus, do not count
them yet
'''
def assert_equals(expected, actual):
if expected != actual:
print expected
print actual
raise Exception('assert_equals fails!')
def assert_score(expected, rolls, description):
assert_equals(expected, bowling_score(rolls))
def run_tests():
assert_score(0, [], 'no rolls')
assert_score(0, [1], 'half frame')
assert_score(2, [1, 1], 'full frame')
assert_score(2, [1, 1, 1], '1.5 frames')
assert_score(4, [1, 1, 1, 1], '2 frames')
assert_score(0, [5, 5], 'spare pending')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(16, [5, 5, 3, 0], 'spare then normal')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(0, [10], 'strike pending')
assert_score(0, [10, 1], 'strike pending')
assert_score(14, [10, 1, 1], 'strike rewarded')
assert_score(30, [10, 10, 10], '1 of 3 strike rewarded')
class Frame:
def __init__(frame):
frame.reset()
def reset(frame):
frame.rolls = 0
frame.score = 0
class Bonus:
def __init__(self, pending):
self.pending = pending
self.bonus = 0
def reward(self, roll):
if self.pending:
self.bonus += roll
self.pending -= 1
if self.pending == 0:
return 10 + self.bonus
return 0
def Strike():
return Bonus(2)
def bowling_score(rolls):
score = 0
frame = Frame()
spare_pending = 0
strikes = []
strike = Strike()
for roll in rolls:
frame.rolls += 1
frame.score += roll
if spare_pending:
score += 10 + roll
spare_pending = 0
for strike in strikes:
score += strike.reward(roll)
if frame.rolls == 1:
if frame.score == 10:
strikes.append(Strike())
frame.reset()
else:
if frame.score == 10:
spare_pending = 1
else:
score += frame.score
frame.reset()
return score
run_tests()
|
@@ -38,15 +38,18 @@
- class Strike:
- def __init__(strike):
- strike.pending = 2
- strike.bonus = 0
+ class Bonus:
+ def __init__(self, pending):
+ self.pending = pending
+ self.bonus = 0
- def reward(strike, roll):
- if strike.pending:
- strike.bonus += roll
- strike.pending -= 1
- if strike.pending == 0:
- return 10 + strike.bonus
+ def reward(self, roll):
+ if self.pending:
+ self.bonus += roll
+ self.pending -= 1
+ if self.pending == 0:
+ return 10 + self.bonus
return 0
+ def Strike():
+ return Bonus(2)
+
def bowling_score(rolls):
|
Oops, there is some dead code here. I could have caught this line of code earlier with a tool like Heckle. But I am a little old school in the sense that I step back and just read the code from time to time and trace it in my mind--that's how I caught it.
'''
score() needs to return the running score of a
game...if frames are still in bonus, do not count
them yet
'''
def assert_equals(expected, actual):
if expected != actual:
print expected
print actual
raise Exception('assert_equals fails!')
def assert_score(expected, rolls, description):
assert_equals(expected, bowling_score(rolls))
def run_tests():
assert_score(0, [], 'no rolls')
assert_score(0, [1], 'half frame')
assert_score(2, [1, 1], 'full frame')
assert_score(2, [1, 1, 1], '1.5 frames')
assert_score(4, [1, 1, 1, 1], '2 frames')
assert_score(0, [5, 5], 'spare pending')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(16, [5, 5, 3, 0], 'spare then normal')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(0, [10], 'strike pending')
assert_score(0, [10, 1], 'strike pending')
assert_score(14, [10, 1, 1], 'strike rewarded')
assert_score(30, [10, 10, 10], '1 of 3 strike rewarded')
class Frame:
def __init__(frame):
frame.reset()
def reset(frame):
frame.rolls = 0
frame.score = 0
class Bonus:
def __init__(self, pending):
self.pending = pending
self.bonus = 0
def reward(self, roll):
if self.pending:
self.bonus += roll
self.pending -= 1
if self.pending == 0:
return 10 + self.bonus
return 0
def Strike():
return Bonus(2)
def bowling_score(rolls):
score = 0
frame = Frame()
spare_pending = 0
strikes = []
for roll in rolls:
frame.rolls += 1
frame.score += roll
if spare_pending:
score += 10 + roll
spare_pending = 0
for strike in strikes:
score += strike.reward(roll)
if frame.rolls == 1:
if frame.score == 10:
strikes.append(Strike())
frame.reset()
else:
if frame.score == 10:
spare_pending = 1
else:
score += frame.score
frame.reset()
return score
run_tests()
|
@@ -59,3 +59,2 @@
strikes = []
- strike = Strike()
for roll in rolls:
|
Setting up for the strike/spare-as-bonus generalization, I rename some vars in bowling_score.
'''
score() needs to return the running score of a
game...if frames are still in bonus, do not count
them yet
'''
def assert_equals(expected, actual):
if expected != actual:
print expected
print actual
raise Exception('assert_equals fails!')
def assert_score(expected, rolls, description):
assert_equals(expected, bowling_score(rolls))
def run_tests():
assert_score(0, [], 'no rolls')
assert_score(0, [1], 'half frame')
assert_score(2, [1, 1], 'full frame')
assert_score(2, [1, 1, 1], '1.5 frames')
assert_score(4, [1, 1, 1, 1], '2 frames')
assert_score(0, [5, 5], 'spare pending')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(16, [5, 5, 3, 0], 'spare then normal')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(0, [10], 'strike pending')
assert_score(0, [10, 1], 'strike pending')
assert_score(14, [10, 1, 1], 'strike rewarded')
assert_score(30, [10, 10, 10], '1 of 3 strike rewarded')
class Frame:
def __init__(frame):
frame.reset()
def reset(frame):
frame.rolls = 0
frame.score = 0
class Bonus:
def __init__(self, pending):
self.pending = pending
self.bonus = 0
def reward(self, roll):
if self.pending:
self.bonus += roll
self.pending -= 1
if self.pending == 0:
return 10 + self.bonus
return 0
def Strike():
return Bonus(2)
def bowling_score(rolls):
score = 0
frame = Frame()
spare_pending = 0
bonuses = []
for roll in rolls:
frame.rolls += 1
frame.score += roll
if spare_pending:
score += 10 + roll
spare_pending = 0
for bonus in bonuses:
score += bonus.reward(roll)
if frame.rolls == 1:
if frame.score == 10:
bonuses.append(Strike())
frame.reset()
else:
if frame.score == 10:
spare_pending = 1
else:
score += frame.score
frame.reset()
return score
run_tests()
|
@@ -58,3 +58,3 @@
spare_pending = 0
- strikes = []
+ bonuses = []
for roll in rolls:
@@ -65,7 +65,7 @@
spare_pending = 0
- for strike in strikes:
- score += strike.reward(roll)
+ for bonus in bonuses:
+ score += bonus.reward(roll)
if frame.rolls == 1:
if frame.score == 10:
- strikes.append(Strike())
+ bonuses.append(Strike())
frame.reset()
|
And then Spare works easily, with a one-line implementation and lots of code removed from bowling_score! Score!
'''
score() needs to return the running score of a
game...if frames are still in bonus, do not count
them yet
'''
def assert_equals(expected, actual):
if expected != actual:
print expected
print actual
raise Exception('assert_equals fails!')
def assert_score(expected, rolls, description):
assert_equals(expected, bowling_score(rolls))
def run_tests():
assert_score(0, [], 'no rolls')
assert_score(0, [1], 'half frame')
assert_score(2, [1, 1], 'full frame')
assert_score(2, [1, 1, 1], '1.5 frames')
assert_score(4, [1, 1, 1, 1], '2 frames')
assert_score(0, [5, 5], 'spare pending')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(16, [5, 5, 3, 0], 'spare then normal')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(0, [10], 'strike pending')
assert_score(0, [10, 1], 'strike pending')
assert_score(14, [10, 1, 1], 'strike rewarded')
assert_score(30, [10, 10, 10], '1 of 3 strike rewarded')
class Frame:
def __init__(frame):
frame.reset()
def reset(frame):
frame.rolls = 0
frame.score = 0
class Bonus:
def __init__(self, pending):
self.pending = pending
self.bonus = 0
def reward(self, roll):
if self.pending:
self.bonus += roll
self.pending -= 1
if self.pending == 0:
return 10 + self.bonus
return 0
def Strike():
return Bonus(2)
def Spare():
return Bonus(1)
def bowling_score(rolls):
score = 0
frame = Frame()
bonuses = []
for roll in rolls:
frame.rolls += 1
frame.score += roll
for bonus in bonuses:
score += bonus.reward(roll)
if frame.rolls == 1:
if frame.score == 10:
bonuses.append(Strike())
frame.reset()
else:
if frame.score == 10:
bonuses.append(Spare())
else:
score += frame.score
frame.reset()
return score
run_tests()
|
@@ -54,2 +54,5 @@
+ def Spare():
+ return Bonus(1)
+
def bowling_score(rolls):
@@ -57,3 +60,2 @@
frame = Frame()
- spare_pending = 0
bonuses = []
@@ -62,5 +64,2 @@
frame.score += roll
- if spare_pending:
- score += 10 + roll
- spare_pending = 0
for bonus in bonuses:
@@ -73,3 +72,3 @@
if frame.score == 10:
- spare_pending = 1
+ bonuses.append(Spare())
else:
|
Add the test for the perfect game, and I call it quits. The test passes for free, which caused me some pondering. Why did the 11th and 12th pseudo-frame not (directly) come into play? Well, they do not get scored on their own right in bowling. They just add to the bonus. So my implementation seems to be validated on a level I was not even predicting.
At this point I would add more tests for corner cases, but the overall code seems sound. I like how bowling_score reads; it fits my mental model of a bowler tracking his own score. Every roll you know you are not only scoring pins for this frame, but you are also getting bonus for marks on prior frames. The code seems to fit the simplest possible metaphor!
The code could still be improved.
Frame is a little misnamed, as you do not have a new Frame object for each new frame you bowl, so it is really a FrameTracker. But if you did add a feature to track scoring frame-by-frame, I think the code would easily adapt.
It is also a little smelly that the bonus objects stick around well after they no longer have effect, but it would be a premature optimization to fix that, and I cannot think of a good failing test to force the optimization. This would be a case where I might write a comment in the code, or maybe consult with my partner on a simpler design. By now I at least have working code and working tests.
Finally, notice that I do not have an object for the Game itself. This is by design, so far. A Game object seems too heavyweight until there is code to actually use a Game object. Right now there is just a method to return the score of the game. A good exercise would be to have a GUI that collects rolls one at a time. This would probably drive out a Game class very quickly.
'''
score() needs to return the running score of a
game...if frames are still in bonus, do not count
them yet
'''
def assert_equals(expected, actual):
if expected != actual:
print expected
print actual
raise Exception('assert_equals fails!')
def assert_score(expected, rolls, description):
assert_equals(expected, bowling_score(rolls))
def run_tests():
assert_score(0, [], 'no rolls')
assert_score(0, [1], 'half frame')
assert_score(2, [1, 1], 'full frame')
assert_score(2, [1, 1, 1], '1.5 frames')
assert_score(4, [1, 1, 1, 1], '2 frames')
assert_score(0, [5, 5], 'spare pending')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(16, [5, 5, 3, 0], 'spare then normal')
assert_score(13, [5, 5, 3], 'spare rewarded')
assert_score(0, [10], 'strike pending')
assert_score(0, [10, 1], 'strike pending')
assert_score(14, [10, 1, 1], 'strike rewarded')
assert_score(30, [10, 10, 10], '1 of 3 strike rewarded')
assert_score(300, [10] * 12, 'perfect game')
class Frame:
def __init__(frame):
frame.reset()
def reset(frame):
frame.rolls = 0
frame.score = 0
class Bonus:
def __init__(self, pending):
self.pending = pending
self.bonus = 0
def reward(self, roll):
if self.pending:
self.bonus += roll
self.pending -= 1
if self.pending == 0:
return 10 + self.bonus
return 0
def Strike():
return Bonus(2)
def Spare():
return Bonus(1)
def bowling_score(rolls):
score = 0
frame = Frame()
bonuses = []
for roll in rolls:
frame.rolls += 1
frame.score += roll
for bonus in bonuses:
score += bonus.reward(roll)
if frame.rolls == 1:
if frame.score == 10:
bonuses.append(Strike())
frame.reset()
else:
if frame.score == 10:
bonuses.append(Spare())
else:
score += frame.score
frame.reset()
return score
run_tests()
|
@@ -29,2 +29,3 @@
assert_score(30, [10, 10, 10], '1 of 3 strike rewarded')
+ assert_score(300, [10] * 12, 'perfect game')
|
You have reached the end of the Bowling Game!
If you liked this, you might also like SHPAML, which brings DRYness to HTML!
by Steve Howell, 2009-12-15