Python Asteroids on GitHub

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!