Python 108 - Game Over? Quarter?
We need a GameOver object and a Quarter object. Let’s try one or both of those.
OK, what will these things do?
The Quarter object will get tossed into the mix when you type “q”. When it gets a “tick”, it should clear the Fleets and then insert a WaveMaker, SaucerMaker, and ShipMaker, and remove itself. (It will already be removed, I reckon.)
The GameOver should just display the GAME OVER screen on draw
.
No testing for that one, I’ll just create it.
The basic class is mostly up to PyCharm:
class GameOver(Flyer):
def draw(self, screen):
pass
def interact_with(self, other, fleets):
pass
def tick(self, delta_time, fleet, fleets):
pass
But how does it draw? How do the other guys work? Game has this code:
# noinspection PyAttributeOutsideInit
def init_game_over(self):
big_font = pygame.font.SysFont("arial", 64)
small_font = pygame.font.SysFont("arial", 48)
self.game_over_surface = big_font.render("GAME OVER", True, "white")
self.game_over_pos = self.game_over_surface.get_rect(centerx=u.CENTER.x, centery=u.CENTER.y / 2)
pos_left = u.CENTER.x - 150
pos_top = self.game_over_pos.centery
self.help_lines = []
messages = ["d - turn left", "f - turn right", "j - accelerate", "k - fire missile", "q - insert quarter", ]
for message in messages:
pos_top += 60
text = small_font.render(message, True, "white")
text_rect = text.get_rect(topleft=(pos_left, pos_top))
pair = (text, text_rect)
self.help_lines.append(pair)
The Flyers use SurfaceMaker to do their surfaces. We can presumably move this code over to SurfaceMaker as it stands, as our first step. If it works.
Ah, not that easy. We render the text in help_lines separately:
def draw_game_over(self):
screen = self.screen
screen.blit(self.game_over_surface, self.game_over_pos)
for text, pos in self.help_lines:
screen.blit(text, pos)
I think I’ll move this code to the new GameOver instance and work from there. Roll back. Copy code to GameOver:
class GameOver(Flyer):
def __init__(self):
self.init_game_over()
# noinspection PyAttributeOutsideInit
def init_game_over(self):
big_font = pygame.font.SysFont("arial", 64)
small_font = pygame.font.SysFont("arial", 48)
self.game_over_surface = big_font.render("GAME OVER", True, "white")
self.game_over_pos = self.game_over_surface.get_rect(centerx=u.CENTER.x, centery=u.CENTER.y / 2)
pos_left = u.CENTER.x - 150
pos_top = self.game_over_pos.centery
self.help_lines = []
messages = ["d - turn left", "f - turn right", "j - accelerate", "k - fire missile", "q - insert quarter", ]
for message in messages:
pos_top += 60
text = small_font.render(message, True, "white")
text_rect = text.get_rect(topleft=(pos_left, pos_top))
pair = (text, text_rect)
self.help_lines.append(pair)
def draw(self, screen):
screen.blit(self.game_over_surface, self.game_over_pos)
for text, pos in self.help_lines:
screen.blit(text, pos)
That works as advertised. In ShipMaker, I did this:
def create_ship(self, fleets):
if not self._safe_to_emerge:
return False
if fleets.ships_remaining > 0:
fleets.ships_remaining -= 1
fleets.add_ship(Ship(u.CENTER))
else:
# fleets.game_over = True
fleets.add_flyer(GameOver())
return True
So the game never finds out that the game is over. Inserting a quarter works, but only because we restart the game when that happens. We’ll need to adjust that.
Remove the commented-out line. Remove the draw_game stuff and the init_game from Game.
One test failing:
def test_can_run_out_of_ships(self):
fleets = Fleets()
fleets.ships_remaining = 2
fleets.add_flyer(ShipMaker())
interactor = Interactor(fleets)
fi = FI(fleets)
interactor.perform_interactions()
fleets.tick(u.SHIP_EMERGENCE_TIME)
assert fi.ships
assert fleets.ships_remaining == 1
for ship in fi.ships:
fleets.remove_ship(ship)
interactor.perform_interactions()
fleets.tick(u.SHIP_EMERGENCE_TIME)
assert fi.ships
assert fleets.ships_remaining == 0
assert not fleets.game_over
for ship in fi.ships:
fleets.remove_ship(ship)
interactor.perform_interactions()
fleets.tick(u.SHIP_EMERGENCE_TIME)
assert not fi.ships
assert fleets.game_over
This cannot check fleets.game_over, as we are not setting it and would like to be rid of it. We can do this in FI:
class FleetsInspector:
@property
def game_over(self):
return self.select(lambda game_over: isinstance(game_over, GameOver))
And change the test to check that. There’s an issue, which is that I cannot create the screen stuff in GameOver during testing because pygame isn’t initialized.
This suffices:
class GameOver:
# noinspection PyAttributeOutsideInit
def init_game_over(self):
if not pygame.get_init():
return
We are green and the game over logic has been removed from Game. Let’s search and destroy references to the flag. There seems to be just the init in Fleets, the others got removed with the Game and ShipMaker changes.
Still green. Try the game once more to be sure. Seems solid. Commit: Game over now handled in GameOver object.
Reflection
That went well enough. A false start trying to move to SurfaceMaker. For that to go well, I should build a huge surface and blit the help into it, and then just display the whole thing. That will require just a bit more work than I am prepared to do right now. And creating the surface where it is isn’t bad, exactly, but if we were ever to want to go to a different drawing scheme, we’d have to deal with it.
I think we’ll call ourselves done for the afternoon. A whole new class, that’s a lot of heavy lifting, even if most of us was just moving code from one place to another.
The trickiest part was the game over flag, which, as I think I mentioned this morning, was a design glitch in that it was just a flag set on a convenient object that had no reason to know about game over. Now, interestingly enough … no one knows about the game being over. It’s just that if you run out of ships, the shipMaker will rez a GameOver for you.
Oops …
It does seem to me that it will do that repeatedly, now that I mention it, because it will repeatedly discover that there are no ships and add a GameOver. Is that true? With a print I see that it does. We want to fix that because otherwise, after years of waiting for a quarter, the universe would be full of GameOver instances.
class GameOver(Flyer):
def interact_with(self, other, fleets):
other.interact_with_game_over(self, fleets)
class Flyer:
def interact_with_game_over(self, game_over, fleets):
pass
class ShipMaker(Flyer):
class ShipMaker(Flyer):
def __init__(self):
self._timer = Timer(u.SHIP_EMERGENCE_TIME, self.create_ship)
self._game_over = False
self._need_ship = True
self._safe_to_emerge = False
def begin_interactions(self, fleets):
self._game_over = False
self._need_ship = True
self._safe_to_emerge = True
def interact_with_game_over(self, game_over, fleets):
self._game_over = True
def tick(self, delta_time, fleet, fleets):
if self._need_ship and not self._game_over:
self._timer.tick(delta_time, fleets)
There might be something more clever than adding the new flag, but the flag is clear and something more clever would not likely be that clear. Commit: ensure only one GameOver per game.
Summary
Again, pretty easy, and again a little bit ragged. Partly I think that’s due to raggedness in the design, but partly I think I’m just a bit off today. Don’t know why. But raggedness aside, I think we have a solid implementation. I just wish I had thought about adding more than one GameOver before I got into writing the Reflection.
Anyway, we’re good, green, and one step closer to game independence from its Flyers.
See you next time!