Nerd Sniped Again
Our weekly meeting last week spawned a new twist on an old problem: bowling. Also: Love one another.
The assignment, as I understand it is:
Show how you would implement a bowling scoring machine’s display, given a customer who is very interested in the display and who is not satisfied with running tests as a sign of progress.
The idea came up as one of the members demonstrated a start they had made at bowling scoring in that style. The group realized that we had enjoyed and learned from a prior exercise where a number of us worked the same problem, so GeePaw tossed out the challenge for this one. More fool I, I’m going to do it.
We’ll begin with a fresh PyCharm project. I set up a test file and a hookup test, which is my standard starting approach:
# test-bowling.py
import pytest
class TestBowling:
def test_hookup(self):
assert True
This is green. Commit: initial test.
Our customer is all about the GUI, so let’s set up a pygame main program next. (I don’t think pygame is really the framework of choice for bowling displays, but it’s the framework I’m most familiar with. It won’t matter to the experiment: what’s important is more what we’d do and when we’d do it, not specifically how we’d do it.)
I borrow a Game class from Forth and strip it down:
import pygame
class Game:
WHITE = (232, 232, 232)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLACK = (0, 0, 0)
BASE_FONT = 'Courier New.ttf'
GAME_XY = 800
FORTH_SIZE = GAME_XY // 2
WINDOW_SIZE = (GAME_XY + FORTH_SIZE, GAME_XY)
BORDER = GAME_XY
LEFT_MARGIN = 10
BOTTOM_MARGIN = 20
def __init__(self):
pygame.init()
self.running = False
self.screen = pygame.display.set_mode(self.WINDOW_SIZE)
pygame.display.set_caption('Bowling')
def main_loop(self):
self.running = True
while self.running:
self.process_events()
self.screen.fill("midnightblue")
pygame.display.flip()
def process_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.running = False
elif event.type == pygame.KEYDOWN:
self.process_keystroke(event)
def process_keystroke(self, event):
pass
def text(self, location, phrase, size, front_color, back_color):
font = pygame.font.Font(self.BASE_FONT, size)
font.set_bold(True)
text = font.render(phrase, True, front_color, back_color)
text_rect = text.get_rect()
text_rect.topleft = location
self.screen.blit(text, text_rect)
if __name__ == '__main__':
game = Game()
game.main_loop()
That displays a 1200 by 800 blue screen named Bowling. Now I draw a messy sketch on a card and think about the scale of things:

That’s a picture of the tenth frame, where there are conventionally three score boxes across the top. The other nine frames just have one score box, on the far right end. So, ideally, our score box’s side should be divisible by three. I scale the window to 800 by 80. I foresee that the customer will want more than one display line but I’m going with one for now. Let’s draw the frames:
I just hammer the following out, running, drawing, running, drawing, until I get the picture I want:

Close enough. Commit: decent score sheet
That drawing code is pretty ad-hoc. When we get around to plugging numbers into the sheet, it’ll be worse.
I think we need an object to display a single frame, and that we should draw ten of them.
Can we refactor to get that? Let’s change our code to draw nine frames and then the tenth separately:
def main_loop(self):
self.running = True
while self.running:
self.process_events()
self.screen.fill("midnightblue")
step = self.GAME_X//10
mark_line = self.GAME_Y//3
for f in range(9):
x = f*step + step
pygame.draw.line(self.screen, self.WHITE, (x, 0), (x,self.GAME_Y))
pygame.draw.line(self.screen, self.WHITE, (x, mark_line), (x-mark_line, mark_line))
pygame.draw.line(self.screen, self.WHITE, (x-mark_line, mark_line), (x-mark_line, 0))
x = 10*step
pygame.draw.line(self.screen, self.WHITE, (x, 0), (x,self.GAME_Y))
pygame.draw.line(self.screen, self.WHITE, (x, mark_line), (x-3*mark_line, mark_line))
pygame.draw.line(self.screen, self.WHITE, (x-mark_line, mark_line), (x-mark_line, 0))
pygame.draw.line(self.screen, self.WHITE, (x-2*mark_line, mark_line), (x-2*mark_line, 0))
pygame.display.flip()
Extract a couple of methods. I’m not sure where I’m going here: I just want to isolate the steps:
def main_loop(self):
self.running = True
while self.running:
self.process_events()
self.screen.fill("midnightblue")
step = self.GAME_X//10
mark_line = self.GAME_Y//3
for f in range(9):
self.draw_normal_frame(f, mark_line, step)
self.draw_tenth_frame(mark_line, step)
pygame.display.flip()
def draw_tenth_frame(self, mark_line, step):
x = 10 * step
pygame.draw.line(self.screen, self.WHITE, (x, 0), (x, self.GAME_Y))
pygame.draw.line(self.screen, self.WHITE, (x, mark_line), (x - 3 * mark_line, mark_line))
pygame.draw.line(self.screen, self.WHITE, (x - mark_line, mark_line), (x - mark_line, 0))
pygame.draw.line(self.screen, self.WHITE, (x - 2 * mark_line, mark_line), (x - 2 * mark_line, 0))
def draw_normal_frame(self, f, mark_line, step):
x = f * step + step
pygame.draw.line(self.screen, self.WHITE, (x, 0), (x, self.GAME_Y))
pygame.draw.line(self.screen, self.WHITE, (x, mark_line), (x - mark_line, mark_line))
pygame.draw.line(self.screen, self.WHITE, (x - mark_line, mark_line), (x - mark_line, 0))
Let’s refactor the x calculation and inline step, build some constants:
def main_loop(self):
self.running = True
while self.running:
self.process_events()
self.screen.fill("midnightblue")
for f in range(1,10):
self.draw_normal_frame(f)
self.draw_tenth_frame()
pygame.display.flip()
def draw_normal_frame(self, f):
x = f * self.FRAME_SIZE
mark_line = self.MARK_LINE
pygame.draw.line(self.screen, self.WHITE, (x, 0), (x, self.GAME_Y))
pygame.draw.line(self.screen, self.WHITE, (x, mark_line), (x - mark_line, mark_line))
pygame.draw.line(self.screen, self.WHITE, (x - mark_line, mark_line), (x - mark_line, 0))
def draw_tenth_frame(self):
x = 10 * self.FRAME_SIZE
mark_line = self.MARK_LINE
pygame.draw.line(self.screen, self.WHITE, (x, 0), (x, self.GAME_Y))
pygame.draw.line(self.screen, self.WHITE, (x, mark_line), (x - 3 * mark_line, mark_line))
pygame.draw.line(self.screen, self.WHITE, (x - mark_line, mark_line), (x - mark_line, 0))
pygame.draw.line(self.screen, self.WHITE, (x - 2 * mark_line, mark_line), (x - 2 * mark_line, 0))
OK, nearly good enough. Let’s make two little view objects. First, a NormalFrameView.
class NormalFrameView:
def __init__(self, frame_number):
self.f = frame_number
def draw(self, game):
x = self.f * game.FRAME_SIZE
mark_line = game.MARK_LINE
pygame.draw.line(game.screen, game.WHITE, (x, 0), (x, game.GAME_Y))
pygame.draw.line(game.screen, game.WHITE, (x, mark_line), (x - mark_line, mark_line))
pygame.draw.line(game.screen, game.WHITE, (x - mark_line, mark_line), (x - mark_line, 0))
And we draw it thus:
def main_loop(self):
self.running = True
while self.running:
self.process_events()
self.screen.fill("midnightblue")
for f in range(1,10):
frame = NormalFrameView(f)
frame.draw(self)
self.draw_tenth_frame()
pygame.display.flip()
Commit: new object NormalFrameView.
Of course we don’t really want to create them all the time so we create them early and use them in main_loop:
def __init__(self):
pygame.init()
self.running = False
self.screen = pygame.display.set_mode(self.WINDOW_SIZE)
pygame.display.set_caption('Bowling')
self.frames = [NormalFrameView(f) for f in range(1, 10)]
def main_loop(self):
self.running = True
while self.running:
self.process_events()
self.screen.fill("midnightblue")
for frame in self.frames:
frame.draw(self)
self.draw_tenth_frame()
pygame.display.flip()
So far so good. Commit: create normal frame vies in init.
Now we do the TenthFrameView class and add it to the frames member.
Curiously enough, PyCharm got all squidgy about the type of self-frames, so I wind up with this:
def __init__(self):
pygame.init()
self.running = False
self.screen = pygame.display.set_mode(self.WINDOW_SIZE)
pygame.display.set_caption('Bowling')
self.frames: List[Union[NormalFrameView, TenthFrameView]] = [NormalFrameView(f) for f in range(1, 10)]
self.frames.append(TenthFrameView())
def main_loop(self):
self.running = True
while self.running:
self.process_events()
self.screen.fill("midnightblue")
for frame in self.frames:
frame.draw(self)
pygame.display.flip()
I am surprised that PyCharm was so picky about that list, but as a rule, it’s certainly convenient to be warned if you toss weird things into lists. There may be a better way to express that, but it’ll do for now.
We are green. Commit: frames now drawn by frame view instances.
Let’s reflect.
Reflection
We have a fairly decent picture of a score display. The customer can see it as progress, and we’re only an hour or so into the first day.
I foresee that we’ll have one or two “model” objects, a Frame object, and perhaps a TenthFrame object if we need it, that will be viewed by our FrameView objects. The view objects will pull the information from the model objects and display it.
I’m not sure, but I am guessing that when we create the game, we’ll pass in the model objects and it will wrap them with view objects, but we’ll hold on loosely to that idea until things become more clear.
In a NormalFrameView, there are 3 places where we might display something. The first number of pins knocked down goes to the left of the little square and the second number inside the square, unless it is a spare or strike, in which case we display a slash or X, respectively. And in the bottom part of the view, we display the frame total when it is available.
For purposes of figuring out the spacing, let’s draw some fake numbers.
class NormalFrameView:
def __init__(self, frame_number):
self.f = frame_number
def draw(self, game):
x = self.f * game.FRAME_SIZE
mark_line = game.MARK_LINE
pygame.draw.line(game.screen, game.WHITE, (x, 0), (x, game.GAME_Y))
pygame.draw.line(game.screen, game.WHITE, (x, mark_line), (x - mark_line, mark_line))
pygame.draw.line(game.screen, game.WHITE, (x - mark_line, mark_line), (x - mark_line, 0))
self.draw_frame_total(game)
def draw_frame_total(self, game):
total = 121
font = pygame.font.Font(game.BASE_FONT, 30)
font.set_bold(True)
text = font.render(str(total), True, game.FOREGROUND, game.BACKGROUND)
text_rect = text.get_rect()
text_rect.centerx = self.f * game.FRAME_SIZE - game.FRAME_SIZE / 2
text_rect.centery = game.GAME_Y - 20
game.screen.blit(text, text_rect)
The scaling and such is still very ad-hoc but the result is good:

The customer loves this, with a bit of concern over the font. I assure them that they can have any font they can afford. Let’s sum up, there is bacon in my future.
Summary
The scaling and coordinate setting of our picture is very ad hoc. We should surely build it such that everything adjusts based on the overall size of the window or some similar setting. That’s partly under way, with all those constants, and it’s only tedious, not difficult or risky. So I consider that to be well in hand.
We managed to slip a pair of view objects in, and we can see how, if the view object has a model object, we can fetch the necessary values from the model and display them. It remains to be seen who decides about the slash for spare and X for strike: is that a model thing, or a view thing? I would normally say that it’s a view thing, but I have heard a rumor that it is “bad luck” to fill in scores while the player has a streak of “marks”, spares or strikes going. If we want that feature, at least part of that display decision process might better be in the model. I’m not worried about it: we’ll put it where it needs to be, of course.
So far, we have no actual tests: everything has been done by draw and look and adjust. By my lights, that’s how it always is with the graphical side of things. We can compute some ratios and so on, but the real issue is how it looks on the screen.
Bowling scoring is nearly trivial and I’m not going to try anything fancy like I would if I were using this as a TDD demonstration, where I’d start with adding up the values and then introduce spares and then finally strikes, which makes for a nice demo of how a new feature can be refactored into the existing code. My tentative plan calls for something a bit simpler, but we’ll see what I do when I do it.
A good first session, the picture looks good, and we’re ready for bacon. See you next time! Remember to love one other.