For the November meeting of Front Range Pythoneers we did a bowling code kata. We worked as a group on the projector, but I also worked on my own version on my laptop. Here’s my code, which was fun to write. It’s a single file which contains its test cases and can be run on the command line or imported:
import unittest
class Frame(object):
def __init__(self, tenth=False):
self.rolls = []
self.tenth = tenth
def full(self):
if len(self.rolls) >= self.max_rolls():
return True
if self.tenth:
has_special = all([roll in ('X', '/') for roll in self.rolls])
return len(self.rolls) == 2 and not has_special
else:
return self.strike()
def roll(self, score):
if self.full():
raise RuntimeError('attempted to record a roll on a full frame')
self.rolls.append(score)
def pins(self):
if len(self.rolls) == 0:
return 0
if self.strike() or self.spare():
return 10
else:
return sum([int(roll) for roll in self.rolls])
def first_roll_pins(self):
if len(self.rolls) == 0:
return 0
elif self.rolls[0] == 'X':
return 10
else:
return int(self.rolls[0])
def score(self, subsequent_frames):
if self.tenth:
return self.tenth_frame_score()
score = self.pins()
if self.strike():
score += sum([frame.pins() for frame in subsequent_frames])
elif self.spare():
if len(subsequent_frames) > 0:
score += subsequent_frames[0].first_roll_pins()
return score
def tenth_frame_score(self):
return min(Game(''.join(self.rolls)).score(), 40)
def strike(self):
return len(self.rolls) > 0 and self.rolls[0] == 'X'
def spare(self):
return len(self.rolls) > 0 and self.rolls[-1] == '/'
def max_rolls(self):
return 3 if self.tenth else 2
class Game(object):
def __init__(self, roll_scores=''):
self.frames = []
for score in roll_scores:
self.roll(score)
def roll(self, score):
if len(self.frames) == 0 or self.frames[-1].full():
tenth = len(self.frames) == 9
self.frames.append(Frame(tenth))
self.frames[-1].roll(score)
def score(self):
return sum(self.frame_scores())
def frame_scores(self):
frame_scores = []
for frame_index in xrange(len(self.frames)):
frame = self.frames[frame_index]
subsequent_frames = []
if frame.strike() or frame.spare():
subsequent_frames = self.frames[frame_index+1:]
added_frames = 1
if frame.strike() and len(subsequent_frames) > 0:
added_frames = 2 if subsequent_frames[0].strike() else 1
subsequent_frames = subsequent_frames[:added_frames]
frame_scores.append(frame.score(subsequent_frames))
return frame_scores
class GameTest(unittest.TestCase):
def test_initial_strike(self):
self.assertEqual(Game('X').score(), 10)
def test_two_strikes(self):
self.assertEqual(Game('XX').score(), 20+10)
def test_three_strikes(self):
self.assertEqual(Game('XXX').score(), 30+20+10)
def test_strike_spare_strike(self):
self.assertEqual(Game('X9/X').score(), 20+20+10)
def test_strike_strike_spare(self):
self.assertEqual(Game('XX9/').score(), 30+20+10)
self.assertEqual(Game('XX9/71').score(), 30+20+17+8)
def test_perfect_game(self):
game = Game('X'*12)
self.assertEqual(len(game.frames), 10)
self.assertEqual(game.score(), 300)
def test_made_up_game(self):
game = Game('X907/818/X70070/72')
self.assertEqual(len(game.frames), 10)
self.assertEqual(game.score(), 132)
if __name__ == '__main__':
unittest.main()
I think that the tenth frame calculation is incorrect. My limited understanding of bowling slowed me down a fair bit. I got an object system that I’m fairly happy with, though!