Python Asteroids on GitHub

The Game knows the objects that need to be loaded into the mix to start the Game. Let’s fix that up.

In Game, we have this:

class Game:
    def __init__(self, testing=False):
        self.delta_time = 0
        self.init_pygame_and_display(testing)
        self.fleets = Fleets()
        self.fleets.add_flyer(ScoreKeeper(testing))
        self.fleets.add_flyer(WaveMaker())
        self.fleets.add_flyer(SaucerMaker())
        self.fleets.add_flyer(ShipMaker())
        self.fleets.add_flyer(Thumper())
        self.running = not testing

Since the theoretical “point” of moving to the decentralized design, with all the smarts in the individual Flyer subclasses, we should arrange things so that these details are not known to Game. It will have to know something about starting the game, but none of the details.

I imagine that there should be an object, GameSeed or something, that the Game will put into the Fleets and then the Seed would flower into the game.

But there are issues.

When we type the “q” to insert_quarter, the main loop creates a new Game instance. At first, that seems OK, it would toss in the Seed and we’d be good to go. But there’s an issue.

Currently the game starts up live, controls on, ship rezzes, and so on. Long ago, Rickard observed that he wanted to try the game and didn’t know the command keys, which are shown on the GAME OVER / Help screen, which only comes out after the game is over.

I’ve put off resolving that, because I wanted to resolve it in the new scheme, but it’s not that easy. As things stand, suppose we just tossed in a GameOver as the seed. The help screen would come up OK, but when we type “q”, Game restarts and tosses in another GameOver.

N.B.
I thought it wasn’t easy. It turns out to be embarrassingly easy. Shows what happens when you just wave an idea off without really considering it.

I notice in Game.__init__ above, the only thing that happens other than initializing PyGame is the Fleets loading. What does insert_quarter really do?

class Game:
    def control_game(self):
        keys = pygame.key.get_pressed()
        if keys[pygame.K_q]:
            # noinspection PyAttributeOutsideInit
            self.keep_going = True
            self.running = False

It’s the setting of those flags that is making the game quit. What if this code were to provide a new Fleets instance with the Game stuff in it, and not change the flags at all?

I just tried it. I have this:

class Game:
    def __init__(self, testing=False):
        self.delta_time = 0
        self.init_pygame_and_display(testing)
        self.fleets = Fleets()
        self.fleets.add_flyer(GameOver())
        self.running = not testing

    def control_game(self):
        keys = pygame.key.get_pressed()
        if keys[pygame.K_q]:
            self.fleets = Fleets()
            self.fleets.add_flyer(ScoreKeeper(False))
            self.fleets.add_flyer(WaveMaker())
            self.fleets.add_flyer(SaucerMaker())
            self.fleets.add_flyer(ShipMaker())
            self.fleets.add_flyer(Thumper())

We pass the False to ScoreKeeper so that it knows whether to create its font. Probably that’s a minor crock.

It turns out that with this simple change, the game starts up in GAME OVER mode and when you type “q” it starts to play. It just works. And Game has essentially no member variables other than the core pygame connections. It maintains no real game state, except (there’s always a crock somewhere) that Game is still displaying the available ships up by the Score. That continues to work, so the fact seems to be:

Sorry, Rickard, if I had realized it was this easy to fix, I’d have fixed it.

Commit: Game starts in GAME OVER mode to display help screen.

So what we need, it seems to me, is a Flyer that the control_game code above can toss into the new mix, and that Flyer will spawn all the necessary objects.

I’ve been calling that the Seed or GameSeed this morning, but I think its name is Quarter.

Let’s remember to think first. How should it work? We know that it is in a fresh mix, but we might want it to clear the mix anyway, just in case. We might want Game to stop creating new Fleets instances. Anyway, Quarter should have a list of classes to create and it should create them and add them to the fleets instance, similar to what we have above. (We could just write it out longhand, and probably should at least start that way. A list might be easier to edit, but not what you’d call a lot easier.)

And it should destroy itself.

When should it do this? The order of events is:

    def asteroids_tick(self, delta_time):
        self.control_game()
        self.fleets.move(delta_time)
        self.perform_interactions()
        	# begin_interactions
        	# interact_with
        	#end_interactions
        self.fleets.tick(delta_time)
        self.draw_everything()

In principle, Quarter could do its work on any call that includes the fleets instance, which is all of them. But if he does it on begin, the others will exist in time for the interaction loop, but they will not receive a begin, and that breaks the protocol. So he can’t do it there. If he’s alone in the mix, there will be no interact_with, so he can’t do it there.

If he does it on end, everyone will start with tick as the first thing they ever see. The makers all expect to have determined results, updated their flags and counts, and do their actions on tick. (If this is a problem, they could probably do it on end.)

If he does it on tick … they’ll all be sent draw but that should be OK.

We’ll try doing the work on tick.

This is so simple, I can’t bring myself to test-drive this. Let’s spike it to see if it flies.

class Quarter(Flyer):
    def interact_with(self, other, fleets):
        pass

    def draw(self, screen):
        pass

    def tick(self, delta_time, fleet, fleets):
        fleets.remove_flyer(self)
        fleets.add_flyer(ScoreKeeper(False))
        fleets.add_flyer(WaveMaker())
        fleets.add_flyer(SaucerMaker())
        fleets.add_flyer(ShipMaker())
        fleets.add_flyer(Thumper())

class Game:
    def control_game(self):
        keys = pygame.key.get_pressed()
        if keys[pygame.K_q]:
            self.fleets = Fleets()
            self.fleets.add_flyer(Quarter())

This works perfectly. I am not actually surprised. I think I should alphabetize that list to make it less error-prone.

I do have a red test. What’s up with that? Ah … it’s the interact_with_ test. Flyer needs to implement interact_with_quarter, and Quarter has to follow the rules.

class Quarter(Flyer):
    def interact_with(self, other, fleets):
        other.interact_with_quarter(self, fleets)

class Flyer:
    def interact_with_quarter(self, missile, fleets):
        pass

I like that test, it keeps me honest. It’s important that all our objects follow what little protocol we demand. We are green and the game works.

Commit: Game now instantiated only once, with GameOver, and “q” command just adds a Quarter object. Game runs continuously.

We should fix up the relationship between Game and Main. Now that Game runs until we truly quit, with Command+w to close the window or Command+q to quit, main does not need to loop.

main.py
if __name__ == "__main__":
    keep_going = True
    while keep_going:
        asteroids_game = Game()
        keep_going = asteroids_game.main_loop()

That should just start the game and be done with it.

asteroids_game: Game

if __name__ == "__main__":
    asteroids_game = Game()
    asteroids_game.main_loop(

Everything works as before. We only ever return from game when we want out. In the main loop, we see this:

class Game:
    def main_loop(self):
        self.keep_going = False
        while self.running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.running = False
                    self.keep_going = False

            self.asteroids_tick(self.delta_time)

            pygame.display.flip()
            self.delta_time = self.clock.tick(60) / 1000
        pygame.quit()
        return self.keep_going

The return stuff is irrelevant. Remove it.

Could we just break when we see QUIT? No, that would just break the event for. No reason for running to be a member, however.

    def main_loop(self):
        running = True
        while running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False

            self.asteroids_tick(self.delta_time)

            pygame.display.flip()
            self.delta_time = self.clock.tick(60) / 1000
        pygame.quit()

Find any other self.running and remove. There was just one, here. Note that it is now gone:

class Game:
    def __init__(self, testing=False):
        self.delta_time = 0
        self.init_pygame_and_display(testing)
        self.fleets = Fleets()
        self.fleets.add_flyer(GameOver())

Tests are green, no surprise there. Game still works fine, also no surprise.

Commit: simplify main-game relationship to reflect that game runs continuously until quit.

We spoke of not creating a new fleets, just clearing the existing one. Let’s just add the Quarter, and have it clear Fleets. It wants to be removed anyway.

class Game:
    def control_game(self):
        keys = pygame.key.get_pressed()
        if keys[pygame.K_q]:
            self.fleets.add_flyer(Quarter())

class Quarter:
    def tick(self, delta_time, fleet, fleets):
        fleets.clear()
        fleets.add_flyer(SaucerMaker())
        fleets.add_flyer(ScoreKeeper(False))
        fleets.add_flyer(ShipMaker())
        fleets.add_flyer(Thumper())
        fleets.add_flyer(WaveMaker())

Works fine. Commit: fleets is never recreated in Game, cleared by Quarter.

Lets draw the coding line here, unless something comes up as we reflect.

ReflectoSummary

We didn’t TDD anything. Is that OK?

There was basically no branching logic anywhere in what we did. We even removed some branching logic. What could we check, that Quarter, when ticked, creates those objects? I guess we could … but I didn’t see the value. I do feel like I should wash my hands, though. I feel that I “should” TDD these things. But the value just didn’t seem to be there. Perhaps I am a bad person.

Where are we now?

The Game is now almost disconnected from the Asteroids concept. It does still draw the available ships, so that remains to be done. But all it does otherwise is create a GameOver when it starts up, and when a “q” is typed, it adds a Quarter object. It has no idea what those objects will do.

We’re nearly where we want to be, with all the game logic embedded in the Flyers.

Attract Mode

There is an issue. When you’re finally shot down for the final time during a game, the GAME OVER comes up, but asteroids continue moving and the saucer still flies. If you wait long enough, the saucer will kill all the asteroids and a new wave will come out. It’s “attract mode”, action going on to tempt the passers-by to put in a quarter.

When we start the game, it doesn’t do that: the GAME OVER is on the screen alone.

We’ll take that as a story for next time. I think we’ll do it with a new object, a Seed (remember that idea?) that spawns a GameOver, a WaveMaker, and a SaucerMaker but no ShipMaker. The Game will remain clueless.

Are you pleased?

Why yes, I am pleased with how this is going. I am particularly pleased with my simple yet powerful test that makes sure that all our Flyer objects implement the things that they must implement. That, together with PyCharm’s assistance, makes creating Flyer subclasses easy and safe.

Emergence is so powerful!

And honestly I just love the way the game just emerges from the objects interacting with each other with no overall god-like object choreographing. The main flyers mostly interact by destroying each other, and the “meta” objects interact by noting whether they interact with this or that kind of object and doing simple things like “oh, there are no ships, let’s create one”.

Simple interactions creating complex behavior. I do like it. I hope you appreciate it as well: the technique of leaving matters to smaller, independent, interacting objects has more applications than just Asteroids. Maybe we’ll even demonstrate that here someday.

For now … it’s Asteroids for a while yet. I hope you’ll tag along. And don’t forget to write!