Bowling scoring kata I've been writing basic Python scripts for a while now to help process data or automate some task but I've decided I should start picking up unit testing and objective orientated programming (the vast majority of my scripts so far have been procedural).

As a starter I decided to follow along with Uncle Bob's bowling scoring kata to try and get my mind around TDD and the idea of writing the absolute minimal code at every step to either make the test go red or green (plus any refactoring steps).

As it's a bare bones example of following TDD the main program doesn't actually have an entry point other than via the tests.

Things that stand out to my beginner's eye:


There are a lot of selfs which look like a lot of visual clutter when I read through the code. Is there a better way of doing this? I think it's the density of them that really gets me so I wasn't sure if I could abstract some of them somehow?
unittest seems to have a lot of boilerplate. I had a play with nose a while back which seemed to strip a lot of that out but I thought it might be a good idea to start properly with unittest until I have a use case for anything that nose (or any other library) offers. 


bowling_game.py

#!/usr/bin/env python

class Game:

  _rolls = [0] * 21
  _current_roll = 0

  def roll(self, pins):
    self._rolls[self._current_roll] = pins
    self._current_roll += 1

  def score(self):
    score = 0
    frame_index = 0
    for frame in range(0, 10):
      if self._is_strike(frame_index):
        score += 10 + self._strike_bonus(frame_index)
        frame_index += 1
      elif self._is_spare(frame_index):
        score += 10 + self._spare_bonus(frame_index)
        frame_index += 2
      else:
        score += self._rolls[frame_index] + self._rolls[frame_index + 1]
        frame_index += 2
    return score

  def _sum_of_balls_in_frame(self, frame_index):
    return self._rolls[frame_index] + self._rolls[frame_index + 1]

  def _spare_bonus(self, frame_index):
    return self._rolls[frame_index + 2]

  def _strike_bonus(self, frame_index):
    return self._rolls[frame_index + 1] + self._rolls[frame_index + 2]

  def _is_spare(self, frame_index):
    return self._rolls[frame_index] + self._rolls[frame_index + 1] == 10

  def _is_strike(self, frame_index):
    return self._rolls[frame_index] == 10


bowling_game_test.py

#!/usr/bin/env python

import unittest

from bowling_game import Game

class BowlingGameTest(unittest.TestCase):

  def setUp(self):
    self.g = Game()

  def roll_many(self, rolls, pins):
    for roll in range(0, rolls):
      self.g.roll(pins)

  def roll_spare(self):
    self.g.roll(5)
    self.g.roll(5)

  def roll_strike(self):
    self.g.roll(10)

  def test_gutter_game(self):
    rolls = 20
    pins = 0
    self.roll_many(rolls, pins)
    self.assertEquals(self.g.score(),0)

  def test_all_ones(self):
    rolls = 20
    pins = 1
    self.roll_many(rolls, pins)
    self.assertEquals(self.g.score(),20)

  def test_one_spare(self):
    self.roll_spare()
    self.g.roll(3)
    self.roll_many(17, 0)
    self.assertEquals(self.g.score(),16)

  def test_one_strike(self):
    self.roll_strike()
    self.g.roll(3)
    self.g.roll(4)
    self.roll_many(16, 0)
    self.assertEquals(self.g.score(),24)

  def test_perfect_game(self):
    self.roll_many(12, 10)
    self.assertEquals(self.g.score(),300)

if __name__ == '__main__':
    unittest.main()


The commit history is available on GitHub if anyone fancies taking a look at that to see where I might have gone better with the red -> green -> refactor cycle.
It doesn't look like too many selfs to me.




class Game:

  _rolls = [0] * 21
  _current_roll = 0

  ...



3 things right off the bat:


What's with the blank line between class Game: and _rolls = [0] * 21?
You are using 2 spaces to indent. Use 4 spaces.
Shouldn't this be in the __init__ function?:


_rolls = [0] * 21
_current_roll = 0




So it would look like:

class Game:
    def __init__(self):
        self._rolls = [0] * 21
        self._current_roll = 0

    def roll(self, pins):
        self._rolls[self._current_roll] = pins
        self._current_roll += 1

    ...