The One Machine to Rule Them All is going well. Today let’s test total score and the small box values, if we get that far. Also: Resist fascism, and take care of one another!

Let’s get right to it. I think we should be able to sketch a little scorekeeper object in some tests and then either build the implied class or borrow it. This will be just like ScoreKeeper from the previous version, only simpler.

New test file? Why not, it’s good practice and might even be a good practice as well.

class TestScorekeeper:
    def test_perfect(self):
        boxes = [Box()] * 10
        assert len(boxes) == 10
        rolls = [10] * 12
        for roll in rolls:
            for box in boxes:
                result = box.add_score(roll)
                if result: break
        scores = [box.score() for box in boxes]
        assert scores == [30] * 10

The test passes. Now let’s figure out how to get the cumulative scores, which are what we want what we really really want.

from itertools import accumulate

    def test_perfect_cumulative(self):
        boxes = [Box()] * 10
        assert len(boxes) == 10
        rolls = [10] * 12
        for roll in rolls:
            for box in boxes:
                result = box.add_score(roll)
                if result: break
        scores = [box.score() for box in boxes]
        frame_totals = list(accumulate(scores))
        assert frame_totals == [30*i for i in range(1,11)]

So that was dead easy once I discovered accumulate. The itertools module has a lot of really good stuff.

Reflection

I think we’re beginning to see how to do score-keeping, and we’re getting some assurance that our Box works correctly. However, do we know how to do what we actually need. In the other bowling exercise, the one with the GUI, the GUI has a view of a single frame, which will, I presume, come down to a single Box if and when we plug this in. So the GUI will want to ask each Box for its total score, rather than somehow compute its own list of scores.

So we want this test to pass:

    def test_perfect_boxes_answer_total_score(self):
        boxes = [Box()] * 10
        assert len(boxes) == 10
        rolls = [10] * 12
        for roll in rolls:
            for box in boxes:
                result = box.add_score(roll)
                if result: break
        scores = [box.total_score() for box in boxes]
        assert scores == [30*i for i in range(1,11)]

It gets worse before it gets better, because if the game is not complete, some boxes are not scored and they are expected to return None as their total score. What we need is a few ways to make that total_score() method work.

In the previous program, we linked the boxes together and when asked for total_score, each box asked its predecessor for its total_score and added that to its own frame score, and returned that result, unless the incoming total was None, in which case it also returned None.

So that’s one way.

Another way would be to require the GUI to access and accumulate the totals, but that doesn’t seem like a design we could be proud of. Another way might be some kind of MetaFrame that was a single object that knew all the frames and somehow looked like 10 different frames to the GUI. I’m not even sure how to do that, which makes me suspect it’s not a good idea.

We’ll go with the linking, even though it makes our Boxes a bit harder to set up.

Let’s create a new member in the Box, _previous_frame, and default it to None. I have a slightly better idea, defaulting it to a special object, but one thing at a time here, OK?

Wait! I have another idea. What if the frame were given a function to call to get the total, not an object? That function would be the total_score method of the prior object if there is one.

That’s slightly deep in the bag, but our Box is already full of pointers to functions. Let’s try it.

class Box:
    def __init__(self, previous_total = lambda: 0):
        self._previous_total = previous_total
        ...

    def total_score(self):
        prev = self._previous_total()
        mine = self.score()
        if prev is None or mine is None:
            return None
        return prev + mine

Now to create the buggers will be a bit tricky but we’re up to it, I think. We’ll init a temp to … ah! What we have won’t quite work. We’ll init a temp to None and thereafter init it to a call to the previous_total method of the one we just did. So the init has to be like this:

class Box:
    def __init__(self, previous_total = None):
        if previous_total is None:
            self._previous_total = lambda: 0
        else:
            self._previous_total = previous_total
        self._scores = []
        self._next_action = self._record_first_roll
        self._complete = (self._strike_complete, self._spare_complete, self._open_frame_complete)

I think that’s close, now to to build the thing, first, inline in the test. Then I think we’ll do a creation method.

I mess with that for about ten minutes and can’t make it work. This is a good sign that we should just link the boxes together.

After more time than it should have required, this test:

    def test_perfect_boxes_answer_total_score(self):
        previous_box = None
        boxes = []
        for i in range(10):
            box = Box(previous_box)
            boxes.append(box)
            previous_box = box
        assert len(boxes) == 10
        rolls = [10] * 12
        for roll in rolls:
            for box in boxes:
                result = box.add_score(roll)
                if result: break
        totals = [box.total_score() for box in boxes]
        assert totals == [30*i for i in range(1,11)]

Is passed with this code:

class Box:
    def __init__(self, previous_box = None):
        self.previous_box = previous_box
        self._scores = []
        self._next_action = self._record_first_roll
        self._complete = (self._strike_complete, self._spare_complete, self._open_frame_complete)


    def total_score(self):
        if self.previous_box is None:
            return self.score()
        prev = self.previous_box.total_score()
        mine = self.score()
        if prev is None or mine is None:
            return None
        return prev + mine

It took too long because I was returning zero, not self.score() from the early-out check for previous_box, and didn’t twig to it until I got fairly deep into printing out stuff. The test may have been a bit too large for a first step.

Anyway, we have total_score so let’s commit and then improve this: Box instances can be linked to previous for total score.

What if we always had a previous box, just a special one at the beginning, a, I don’t know, ZeroBox:

class ZeroBox:
    def total_score(self):
        return 0

class Box:
    def __init__(self, previous_box = None):
        self.previous_box = previous_box if previous_box else ZeroBox()
        self._scores = []
        self._next_action = self._record_first_roll
        self._complete = (self._strike_complete, self._spare_complete, self._open_frame_complete)

    def total_score(self):
        prev = self.previous_box.total_score()
        mine = self.score()
        if prev is None or mine is None:
            return None
        return prev + mine

Much nicer, I think. Commit: use ZeroBox as previous if no previous provided.

Let’s reflect and sum up, I think this will do for a Saturday morning.

Sum | muS

I do rather wish that the lambda idea had worked, and probably my mistake was a simple one, but the fact that I couldn’t make it work for ten minutes told me that it was too tricky to be allowed to live. And I would have been tempted to tell my friends “Hey, look at this!”, and that is a dead giveaway that one’s code is too clever.

Still, I’d like to know how to do it that way. Maybe I’ll play with it later. Maybe I’ll just play with matches instead.

You’ll have noticed that I’ve only tested perfect games this morning. I am OK with that because we have lots of tests for all the cases in the individual single box tests, so I am quite sure that it all works. But what has not really been tested is the transition from boxes with scores to boxes without, in the middle of a game. I am confident about that, but only at about the 0.75 level, and I’m up in the 0.9+ for the full game totals.

So, especially if my pair were not deeply confident, we might call for some more tests of what’s going on, but I’d bet a beverage of your choice that things are OK. A reasonable beverage, not the Macallan 81 or anything like that. I’m thinking Starbucks-level beverage, OK? I’m confident, not crazy. OK, at least not crazy in that particular way.

I do like the little ZeroBox class that returns a zero total. That initializes the stream very nicely.

A thing one might not like, which you may not have noticed, is that when we calculate the totals for the whole game, we calculate 0, box 1, then 0, box 1, box 2, then 0, box 1, box 2, box 3, and so on. So we do 45 calls to total_score, or some number like that. We could memoize the total if we cared. I think Python even has some clever way to do that. It would be a bit tricky, because we only want to remember our total when we finally get a number, not when we return None. I’m not going to worry about it, that’s my entire plan on the subject.

A good morning. So far this state machine idea seems quite pleasing. See you next time, and please write or call your congress-persons. Maybe tell them you’d rather retain things like democracy, health care, education, and that people have rights even if they are not citizens. But I digress.

See you next time!