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!