13 April 2008

bowling.py

One Saturday morning, I had the urge to learn the scoring rules for the game of bowling. This program resulted from that. Guido, here, talks about his bowling program that uses a simple data structure: list of lists to represent frames. I do the same.

# Sat March 22, 2008 (9am)
# bowling.py -- calculates scores, doesn't enforce rules

# create new instance for each player.
# Public API: knocked_down(npins)
# just invoke that -- will automatically deduce
# which frame you are in.
class BowlingGame(object):
    def __init__(self):
        self.f = [[None, None] for x in range(12)]
        self.cf = 0 # current frame

    def knocked_down(self, npins):
        f = self.f[self.cf]
        if f[0] is None:
            f[0] = npins
        else:
            f[1] = npins
        if npins == 10 or f[1] is not None:
            self.cf += 1

    def get_score(self):
        total = 0
        complete = True
        for i, f in enumerate(self.f):
            if i > 9:
                break
            elif f[0] is None: # hasn't bowled this frame
                break
            elif f[0] == 10: # strike
                if self.f[i + 1][0] is None and \
                   (self.f[i + 1][1] is None or self.f[i + 2][0] is None):
                    complete = False
                    break
                # strike -- next 2 bowls (both of which can be in the same frame!)
                total += 10 + self.f[i + 1][0]
                if self.f[i + 1][1] is None:
                    total += self.f[i + 2][0]
                else:
                    total += self.f[i + 1][1]
            elif f[1] is None: # hasn't bowled 2nd bowl
                complete = False
                break
            elif f[0] + f[1] == 10: # spare
                if self.f[i + 1][0] is None:
                    complete = False
                    break
                total += 10 + self.f[i + 1][0]
            else:
                total += f[0] + f[1]            
        return total, complete


#
# Tests
#
def reporterror(actual, expected):
    if actual != expected:
        print 'actual: %s\nexpected: %s' % (actual, expected)
        raise Exception

def test1():
    game = BowlingGame()
    game.knocked_down(7)
    game.knocked_down(2)
    reporterror(game.get_score()[0], 9)

def test2():
    game = BowlingGame()
    for i in range(12):
        game.knocked_down(10)
    reporterror(game.get_score()[0], 300)

def test3():
    import random
    for i in range(1000):
        bowls = [random.randint(0, 4) for i in range(20)]
        game = BowlingGame()
        for b in bowls:
            game.knocked_down(b)
        reporterror(game.get_score(), (sum(bowls), True))

# wikipedia data:
# http://en.wikipedia.org/wiki/Ten-pin_bowling#Rules_of_play
def test4():
    game = BowlingGame()
    map(game.knocked_down, [10, 10, 4, 2])
    reporterror(game.get_score(), (46, True))
def test5():
    game = BowlingGame()
    map(game.knocked_down, [7, 3, 4, 2])
    reporterror(game.get_score(), (20, True))
def test6():
    game = BowlingGame()
    map(game.knocked_down, [10, 3, 6])
    reporterror(game.get_score(), (28, True))

def test7():
    game = BowlingGame()
    for i in range(10):
        game.knocked_down(10)
    game.knocked_down(7)
    game.knocked_down(1)
    reporterror(game.get_score(), (285, True))

def t():
    for i in range(100):
        try:
            name = "test%d" % (i+1)
            fn = globals()[name]
            print "testing", name
        except KeyError:
            break
        else:
            fn()

if __name__ == "__main__":
    t()