Snipe Hookup
Tonight is demo night. I need to be ready. We learn why we should never hurry: The Demo Is A Lie! Also: Resist fascism, and take care of one another.
The Friday Geeks Night Out group meets every Tuesday evening, obviously, and I’d like to have my bowling scoring thing working. I can contribute to the discussion anyway, but surely it’ll be more impressive if it works. I am hopeful that I can get there this morning. If not, I might pull some overtime this afternoon. Rarely a good idea, but needs must.
Yesterday I had a bit of free time , and got an idea from Bill Wake, who is also working the problem and showed a Swift GUI with buttons. I have stolen adopted that idea, and my display now looks like this:

The button code is very ad-hoc. Let’s look at it so you can see how I figured out a quick and dirty button solution. This isn’t quite the original, as I did a little work preparing for the screen size to change, not that it ever will.
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_buttons()
pygame.display.flip()
def draw_buttons(self):
self.centers = list()
width = game.GAME_X
radius = width//22
self.draw_radius = radius//4*3
dx = width // 11
used = dx*11
remaining = Game.GAME_X - used
start = (remaining + dx) // 2
center_y = self.WINDOW_BOTTOM - 35
for i in range(11):
center = (start + dx*i, center_y)
self.centers.append(center)
pygame.draw.circle(self.screen, "dimgray", center, self.draw_radius)
font = pygame.font.Font(game.BASE_FONT, self.draw_radius)
font.set_bold(True)
text = font.render(f'{i}', True, self.BLACK, "dimgray")
text_rect = text.get_rect()
text_rect.center = center
self.screen.blit(text, text_rect)
We record the centers of the buttons in a list. After working out the spacing and most of the constants, we draw 11 equally spaced buttons, and record each center in the centers list. The drawing code is adequate but clearly could use some improvement. We could imagine that we’d want a Button class and similar things. But this will do for now.
In the game’s process_events, we field a new event, mouse up, and right now, just print which button we hit:
def process_mouse_up(self, event):
pos = event.pos
cy = self.WINDOW_BOTTOM - 35
for i, center in enumerate(self.centers):
if Vector2(center).distance_to(pos) < self.draw_radius:
print(f'roll {i}')
If the mouse goes up inside the radius of a given button’s center, we simply print the corresponding number. With a manual test, I found that this works. In the near future, we’ll send that number to the program as a roll. I have not figured out quite how that will be done. There will be some way: we just need access to the rolls collection.
I think you’re up to date now. You may recall from yesterday’s article that we can calculate an individual frame’s score, using two new objects, Scorekeeper (that’s the one we’ll give to the game display code, I bet) and Frame. Those two classes are in with the tests that drove them out, so I’ll move them to their own files now.
PyCharm makes that easy. F6 to trigger move, type the file name, accept the offer to add to git.
Attach Frames to View
I think our next step might be to attach the Frames to the FrameViews, and then to display the frame score as a visible step toward the game working. We’re trying to keep our GUI-oriented customer happy and engaged.
Scorekeeper creates ten frames:
class Scorekeeper:
def __init__(self):
self.rolls = []
self.frames = [Frame(self.rolls) for _ in range(10)]
So let’s pass a Scorekeeper to the game code:
class Game:
def __init__(self, scorekeeper: Scorekeeper):
self.scorekeeper = scorekeeper
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(0, 9)]
self.frames.append(TenthFrameView())
if __name__ == '__main__':
keeper = Scorekeeper()
game = Game(keeper)
game.main_loop()
OK, now we’d like to have our View objects accept a Frame parameter. They init like this now:
class NormalFrameView:
def __init__(self, frame_number):
self.f = frame_number
We’ll add a Frame parm and use it. I’ll show you the result, which surprised me a bit, before we look at the code.

Here’s the pertinent code so far:
class Game:
def __init__(self, scorekeeper: Scorekeeper):
self.scorekeeper = scorekeeper
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]] = list()
for i, frame in enumerate(self.scorekeeper.frames):
if i < 9:
view = NormalFrameView(i, frame)
else:
view = TenthFrameView(frame)
self.frames.append(view)
Here, we iterate over the Scorekeeper’s real frames, associating a view with each one, and stuffing them into our already-existing list of frames. Then when the frames are drawn, they fetch the frame score:
class NormalFrameView:
def draw_frame_total(self, game):
total = self.frame.frame_score()
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)
None, you may recall, is the answer we provide when the frame does not yet know its score. Let’s provide some rolls for the game to display before we move further.
if __name__ == '__main__':
keeper = Scorekeeper()
rolls = [1,2, 3,4, 5,5, 10, 2,1, 4,3, 0,1, 5,1, 5,1, 1,6]
for r in rolls:
keeper.roll(r)
game = Game(keeper)
game.main_loop()
And this is the result:

And those are the right individual frame scores. the spare in frame three scores ten for itself and the bonus ten from the strike in frame four, which scores only 13, because of the poor showing in frame five.
I think we need some helper methods in these FrameView objects but since there are two classes, I’ll have to duplicate any work that I do. Let’s hold off and focus on features rather than code quality.
- Note
- Of all the lessons we might learn from this exercise, what you see just above should be the big one. I am consciously deciding to accept inferior code quality in the interest of showing a better picture to my customer (and my pals this evening). The code is turning into legacy code before our eyes!
Anyway, I’m going to do it.
def draw_frame_total(self, game):
total = self.frame.total_score()
display_total = str(total) if total is not None else ""
font = pygame.font.Font(game.BASE_FONT, 30)
font.set_bold(True)
text = font.render(display_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)
I’m calling for a new method in Frame, total_score, which I’ll provide trivially first:
def total_score(self):
return self.frame_score()
The total score in a given frame, of course, is the frame score plus the total score of the preceding frame.
Our frames do not know their predecessor, so we need to set that up. What about the initial frame? I’m not sure yet.
I choose this, the path of weird conditionals:
class Frame:
def __init__(self, rolls, previous_frame):
self.rolls = rolls
self.previous_frame = previous_frame
self.start = None
def total_score(self):
previous_total = self.previous_frame.total_score() if self.previous_frame is not None else 0
return self.frame_score() + previous_total
class Scorekeeper:
class Scorekeeper:
def __init__(self):
self.rolls = []
self.frames = []
previous_frame = None
for _ in range(10):
new_frame = Frame(self.rolls, previous_frame)
self.frames.append(new_frame)
previous_frame = new_frame
That gives me what appears to be a correct result in the display:

I do not trust that result, entirely, because I cannot as yet see the rolls for each frame. And I suspect this will behave badly on partial games. Let’s try that first. When I provide no rolls, I get this error:
File "/Users/ron/PycharmProjects/bowlinggui/frame.py", line 19, in total_score
return self.frame_score() + previous_total
~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'
The initial frame deserves a zero from its call for the previous total. But then when it goes for frame score it gets None. Let’s try this:
def total_score(self):
previous_total = self.previous_frame.total_score() if self.previous_frame is not None else 0
frame_score = self.frame_score()
return frame_score + previous_total if frame_score is not None else None
Try it. I get a suitably empty display for the empty input. I’ll try a partial game. That displays correctly as well.
Let’s commit, I’ve not been committing any of this horror. Commit: game displays proper frame totals.
I set up the alternating strike-spare game and notice that the TenthFrame is not displaying a score. A quick print tells me that it is getting None back. I think I need to look into this with a breakpoint, which I hate ever to do but I don’t see much choice here.
Ha! I set up the game wrong, forgot the bonus roll on the final frame. Whew! That’s a relief. With the proper values in place:

And that is what we should see. Commit: game scoring displays correctly
I’m two hours in and making a terrible mess. But let’s wire up the buttons and see if we can enter a game interactively.
That turns out to be easy to hack:
def process_mouse_up(self, event):
pos = event.pos
cy = self.WINDOW_BOTTOM - 35
for i, center in enumerate(self.centers):
if Vector2(center).distance_to(pos) < self.draw_radius:
self.scorekeeper.roll(i)
That code takes advantage of the fact that the index of the button happens to be its value.
Here’s a game where I clicked twelve 10s:

I’m two hours in and feel that I’ve done enough damage. We have a fairly reasonable demo. I will have a bit of a rest and pull some overtime this afternoon. Let’s review some of the awful code I’ve created in my hurry:
Review
class NormalFrameView:
def draw_frame_total(self, game):
total = self.frame.total_score()
display_total = str(total) if total is not None else ""
font = pygame.font.Font(game.BASE_FONT, 30)
font.set_bold(True)
text = font.render(display_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)
class TenthFrameView:
def draw_frame_total(self, game):
total = self.frame.total_score()
display_total = str(total) if total is not None else ""
font = pygame.font.Font(game.BASE_FONT, 30)
font.set_bold(True)
text = font.render(display_total, True, game.FOREGROUND, game.BACKGROUND)
text_rect = text.get_rect()
text_rect.centerx = 9 * game.FRAME_SIZE + game.FRAME_SIZE / 2
text_rect.centery = game.GAME_Y - 20
game.screen.blit(text, text_rect)
This code is essentially all duplicated once we observe that the frame index of the tenth frame is 9, so that line is really duplicated as well. Every time I’ve needed to change that method, I’ve had to do it twice. All I have to do is forget once and bad things will start to happen.
This duplication needs to be removed. One way to do it would be to use the NormalFrameView in all ten frames. So far there only real difference is the extra boxes. We could deal with that in some other way.
Another way would be to have a superclass that contains all the common code. I might do that: I do not fear concrete code in superclasses as much as some folx do. I’m probably wrong about that.
Yet another way would be to have a common helper object to do these common things. That’s probably ideal. I don’t think I have time to do that before tonight. I’m letting the code get worse and worse.
Now let’s compare the frame score and total score code in Frame:
class Frame:
def frame_score(self):
try:
return self._frame_score()
except Exception:
return None
def _frame_score(self):
s = self.start
rolls = self.rolls
first_roll = rolls[s]
if first_roll == 10:
return 10 + rolls[s+1] + rolls[s+2]
second_roll = rolls[s+1]
if (two := first_roll + second_roll) != 10:
return two
return two + rolls[s+2]
def total_score(self):
previous_total = self.previous_frame.total_score() if self.previous_frame is not None else 0
frame_score = self.frame_score()
return frame_score + previous_total if frame_score is not None else None
The frame_score method expects that exceptions will be triggered when the frame doesn’t have enough rolls available to do its job. It converts any exception (there are three possible) into a result of None.
But total_score is a bit weird because it has to deal with there being no previous frame, and with the possibility that frame_score will come back None. I think we can improve that a bit in a moment.
Let’s try a little bit of extraction here.
def total_score(self):
frame_score = self.frame_score()
return frame_score + self.previous_total() if frame_score is not None else None
def previous_total(self):
return self.previous_frame.total_score() if self.previous_frame is not None else 0
Maybe a bit better. Commit: tidying
Finally, the drawing code is still all very ad hoc and too dependent on specific values. For example:
class NormalFrameView:
def draw(self, game):
x_left = self.f * game.FRAME_SIZE
x_right = x_left + game.FRAME_SIZE
mark_line = game.MARK_LINE
pygame.draw.line(game.screen, game.WHITE,
(x_right, 0), (x_right, game.GAME_Y))
pygame.draw.line(game.screen, game.WHITE,
(x_right - mark_line, mark_line), (x_right , mark_line))
pygame.draw.line(game.screen, game.WHITE,
(x_right - mark_line, mark_line), (x_right - mark_line, 0))
pygame.draw.line(game.screen, game.WHITE,
(x_left, game.GAME_Y), (x_right, game.GAME_Y))
self.draw_frame_total(game)
def draw_frame_total(self, game):
total = self.frame.total_score()
display_total = str(total) if total is not None else ""
font = pygame.font.Font(game.BASE_FONT, 30)
font.set_bold(True)
text = font.render(display_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)
Summary
When working with drawing code, I find that I almost always create a mess, because drawing primitives are so, well, primitive. I just sort of fiddle with it until it looks right. Then, any similarities are hard to spot, and often I do not refactor it. That’s happening here.
The result is that this program is going to look right on the screen, while the code that’s doing the job does not look right. Which means that a change that may seem simple on the screen, such as “turn all the strike X’s from a letter X into red diagonals across the mark box” turns into a long and involved task. This leads to trouble. Things take “too long” and when we’re asked why, our answer is “the code that draws things isn’t very good” and then the next question is “who wrote this not very good of which you speak?”.
And pressure. I really want to have something to show tonight, so I was rushing to get some numbers on the screen. I think that the code that gets the numbers isn’t too bad so far, but the code to draw the numbers is messy and the trend is not good. The more pressure I feel, the less I pay attention to writing decent code. I just want to get it working.
The demo is a lie!
That’s good for the demo and bad for the product. Which means, in essence, that the demo is a lie. It looks like everything is good, and everything isn’t good.
I owe it to myself, to the team, and to the people paying for the product, to make the demo and the code equally good. If anything, the demo should be worse than the code, certainly not better, because it is misleading. It is a lie.
Frankly, I am not surprised that hurrying has left me less well off than I’d like to be … but I am surprised to find that the pressure I felt, all self-applied, as pushed me into a corner.
Imagine what I might do if someone with control over my job was pressuring me. I can tell you at least this much: I would not be happy, and I very well might start fouling things up due to that pressure.
I’ll take a break and see if I can clear things up a bit with a little overtime later today.
I’m chuckling a bit: I feel surprisingly badly about this morning, even though it has turned out exactly like I’d expect from doing work under pressure. I still feel shame. That’s a good thing!
See you next time. Meanwhile, resist fascism, and take care of one another.