Yes, actually. I’ve thought of a way to solve bowling scoring that is unlike any way I’ve done it before. Gotta try it. Sudden stop, see next article. Also: Resist fascism, and take care of one another.

In the most recent FGNO bowling exercise, we were “driven” by the need to have a GUI. The intention was to find out how we might work differently in that situation than we do in the typical bowling demo, where we generally deal only with computing the final score, or possibly the score in each frame.

In my implementation of that, I ran into some difficulty computing the values that go into the small boxes in a bowling score sheet. Those values include various symbols, X for a strike, / for a spare, and so on. in my implementation, those values are calculated after the fact, separately from the total score. And it has taken me three or four tries to get code for the small boxes that I don’t dislike too intensely.

This morning as I was trying to get back to sleep, I was imagining a mechanical device that somehow scored bowling. It had two boxes for each frame, except for the tenth, which had three. When pins were dropped, a score ball would roll along the tops of the boxes, dropping into the first open box. The next score ball would roll further, and so on until all the boxes were full.

It’s immediately clear that this machine needs a trigger kind of mechanism so that if a score ball weighing 10 drops into the first box of a frame, the second box is closed without a ball in it. That ensures that the next score goes to the next frame.

It’s less clear how a frame would find its bonus balls, all of which occur in higher-numbered frames, and I don’t see how the frames would find the score of the preceding frame so as to provide a total.

Let’s be realistic here: I was trying to sleep, and a machine like this would make no sense outside some kind of steampunk universe. But what if it did make sense?

So today, I plan to experiment with this idea, just to see what it would do to the bowling game program if that was the underlying design concept.

I propose to do this in a new PyCharm project, though I may pull over some tests if they seem useful. I don’t think I’ll do the graphics part, but if I do, I’ll surely borrow some of that as well. The main point here will be scaling.

Through some miracle, I think I have a testing configuration right out of the box. I wonder if there’s a way to save a project template. Anyway, let’s have a test. What are we going to test? Let’s imagine that we just have a list of boxes that we feed rolls to. That list should be an object. Try this:

    def test_score_machine(self):
        sm = ScoreMachine()
        assert len(sm.boxes) == 2*9 + 3

It has some boxes, 2 each for 9 frames, then three. Test fails, can’t imagine why. Implement:

class Box:
    pass


class ScoreMachine:
    def __init__(self):
        boxes = []
        for frame in range(1, 10):
            boxes.append(Box())
            boxes.append(Box())
        boxes.append(Box())
        boxes.append(Box())
        boxes.append(Box())
        self.boxes = boxes

I really expect the boxes to come in five types, NormalFirst, NormalSecond, TenthFirst, etc.

Let’s assert that just a bit:

    def test_score_machine(self):
        sm = ScoreMachine()
        assert len(sm.boxes) == 2*9 + 3
        boxes = sm.boxes
        for index in range(0, 9):
            assert isinstance(boxes[2*index], NormalFirst)

That’s enough to drive this:

class NormalFirst:
    pass


class ScoreMachine:
    def __init__(self):
        boxes = []
        for frame in range(1, 10):
            boxes.append(NormalFirst())
            boxes.append(Box())
        boxes.append(Box())
        boxes.append(Box())
        boxes.append(Box())
        self.boxes = boxes

Let’s commit, can’t hurt. Commit: shell objects ScoreMachine and NormalFirst.

I think I have the pattern, so I will do the rest as a batch:

class TestBoxes:
    def test_score_machine(self):
        sm = ScoreMachine()
        assert len(sm.boxes) == 2*9 + 3
        boxes = sm.boxes
        for index in range(0, 9):
            assert isinstance(boxes[2*index], NormalFirst)
            assert isinstance(boxes[2*index + 1], NormalSecond)
        assert isinstance(boxes[18], TenthFirst)
        assert isinstance(boxes[19], TenthSecond)
        assert isinstance(boxes[20], TenthThird)

And this:

class ScoreMachine:
    def __init__(self):
        boxes = []
        for frame in range(1, 10):
            boxes.append(NormalFirst())
            boxes.append(NormalSecond())
        boxes.append(TenthFirst())
        boxes.append(TenthSecond())
        boxes.append(TenthThird())
        self.boxes = boxes

With the requisite dummy classes, of course.

Now I want a bit of behavior here. And it seems that our boxes are going to want to know whether they should accept a roll or not. Let’s try a test:

    def test_open_rolls(self):
        sm = ScoreMachine()
        for roll in range(20):
            sm.roll(4)
        for box in sm.boxes:
            assert box.is_closed()
            assert box.roll == 4

This is pretty weak but probably a decent start. Unfortunately all my boxes will need to understand those messages.

Let’s simplify the test to just do a few:

    def test_open_rolls(self):
        sm = ScoreMachine()
        for roll in range(1):
            sm.roll(4)
        boxes = sm.boxes
        assert boxes[0].is_closed
        assert boxes[0].score == 4

I made a few design decisions based on writing the test. I’m only rolling one roll. I’ll make the is_closed and score properties rather than methods, and I’ll call it score so that I don’t get too confused about the word roll all over.

Now I just need to fix up a little bit for this to work:

class ScoreMachine:
    def roll(self, count):
        for box in self.boxes:
            accepted = box.roll(count)
            if accepted:
                return

class NormalFirst:
    def __init__(self):
        self.open = True
        self._score = None

    def roll(self, count):
        if self.open:
            self._score = count
            self.open = False
            return True
        else:
            return False

    @property
    def is_closed(self):
        return not self.open
    
    @property
    def score(self):
        return self._score

Green. Commit: NormalFirst accepts low roll OK.

OK, I think I’ll go back to the full test and just copy the code to the other boxes. I’m sort of wishing I hadn’t committed to all those classes.

Expand the test:

    def test_open_rolls(self):
        sm = ScoreMachine()
        for roll in range(21):
            sm.roll(4)
        for index, box in enumerate(sm.boxes):
            print(index)
            assert box.is_closed
            assert box.score == 4

And cheat by making all the classes inherit from NormalFirst for now:

class NormalSecond(NormalFirst):
    def __init__(self):
        super().__init__()

And so on.

This test passes. I am a bit surprised, now that I think about it. How did the last one get closed? I think there’s something off in my counting.

Wednesday Morning

I’ll wrap this here and start a Wednesday article. Short summary: going to start over and I’ll tell you why.