Python Asteroids on GitHub

I have some loose end notes in my keyboard tray Jira. Let’s have a look at some of them.

One note says “fleet timers reset”. The concern is this: the game has various timers, how long until the ship or saucer appears, and so on. When the player types “q” to insert a quarter, we reset the fleets:

class Game:
    def insert_quarter(self, number_of_ships):
        self.fleets.clear()
        self.game_over = False
        self.score = 0
        self.delta_time = 0

class Fleets:
    def clear(self):
        for fleet in self.fleets:
            fleet.clear()

class Fleet:
    def clear(self):
        self.flyers.clear()

The Fleet class itself does not have timers, but specialized Fleet classes do:

class AsteroidFleet(Fleet):
    def __init__(self, asteroids):
        super().__init__(asteroids)
        self.timer = Timer(u.ASTEROID_DELAY, self.create_wave)
        self.asteroids_in_this_wave = 2

class SaucerFleet(Fleet):
    def __init__(self, flyers):
        super().__init__(flyers)
        self.timer = Timer(u.SAUCER_EMERGENCE_TIME, self.bring_in_saucer)

class ShipFleet(Fleet):
    ships_remaining = u.SHIPS_PER_QUARTER
    game_over = False

    def __init__(self, flyers):
        super().__init__(flyers)
        self.ship_timer = Timer(u.SHIP_EMERGENCE_TIME, self.spawn_ship_when_ready)
        ShipFleet.ships_remaining = u.SHIPS_PER_QUARTER

Reading this, I am wondering how we get ships remaining set back to u.SHIPS_PER_QUARTER. I don’t see where that would be happening.

Ah. We don’t really call insert_quarter when you type “q”. We do this:

    def main_loop(self):
        self.game_init()
        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

    def asteroids_tick(self, delta_time):
        self.fleets.tick(delta_time)
        self.control_game(self.ship, delta_time)
        self.process_collisions()
        self.draw_everything()
        if ShipFleet.game_over: self.draw_game_over()

    def control_game(self, ship, dt):
        keys = pygame.key.get_pressed()
        if keys[pygame.K_q]:
            self.keep_going = True
            self.running = False

So when you type “q”, the call to control_game sets running to False, and keep_going to True. And in main:

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

We loop, because main_loop will return, and because keep_going is True, we create a whole new game and things continue. I think there is another sticky note here about that. Yes, it says “insert quarter merge up”.

Therefore, the sticky “fleet timers reset” is no longer valid, because we create a whole new Game instance and the timers are in fact reset.

Another note: “reset for new game Fleets -> Fleet …”. That is also no longer applicable, again because we create everything anew. Scrap that.

But let’s do look at merging insert_quarter back up into where it is called from.

There are two places:

class Game:
    def __init__(self, testing=False):
        self.init_general_game_values()
        self.init_asteroids_game_values()
        self.init_fleets()
        # self.init_timers()
        self.init_pygame_and_display(testing)
        if not testing:
            self.insert_quarter(u.SHIPS_PER_QUARTER)

    # noinspection PyAttributeOutsideInit
    def game_init(self):
        self.running = True
        Saucer.init_for_new_game()
        self.insert_quarter(u.SHIPS_PER_QUARTER)

Who calls game_init?

    def main_loop(self):
        self.game_init()
        self.keep_going = False
        while self.running:
        	...

This looks like a “belt and suspenders and maybe a staple in your tailbone let’s init things a lot to be sure” kind of thing.

This was all put in place before I got the bright idea to create a new Game instance on every play. Let’s first inline insert_quarter in both places.

class Game:
    def __init__(self, testing=False):
        self.init_general_game_values()
        self.init_asteroids_game_values()
        self.init_fleets()
        # self.init_timers()
        self.init_pygame_and_display(testing)
        if not testing:
            self.fleets.clear()
            self.game_over = False
            self.score = 0
            self.delta_time = 0

    def game_init(self):
        self.running = True
        Saucer.init_for_new_game()
        self.fleets.clear()
        self.game_over = False
        self.score = 0
        self.delta_time = 0

I’m not sure that was necessary, but now let’s remove the single call to game_init from main_loop. Tests are all green. Try the game. The game does not start.

That’s interesting, put it back. No, move the two lines from game_init up into __init__:

    def __init__(self, testing=False):
        self.init_general_game_values()
        self.init_asteroids_game_values()
        self.init_fleets()
        # self.init_timers()
        self.init_pygame_and_display(testing)
        if not testing:
            self.running = True
            Saucer.init_for_new_game()
            self.fleets.clear()
            self.game_over = False
            self.score = 0
            self.delta_time = 0

Tests and game are good. Now remove game_init as no longer referenced. Then, extract a method from __init__.

We could have committed here!

class Game:
    def __init__(self, testing=False):
        self.init_general_game_values()
        self.init_asteroids_game_values()
        self.init_fleets()
        # self.init_timers()
        self.init_pygame_and_display(testing)
        if not testing:
            self.init_for_game_play()

    def init_for_game_play(self):
        self.running = True
        Saucer.init_for_new_game()
        self.fleets.clear()
        self.game_over = False
        self.score = 0
        self.delta_time = 0

No let’s not do that. Let’s instead move things up from under the testing flag until we only have whatever makes the game run and the tests stay green.

We have this:

class Game:
    def __init__(self, testing=False):
        self.init_general_game_values()
        self.init_asteroids_game_values()
        self.init_fleets()
        # self.init_timers()
        self.init_pygame_and_display(testing)
        if not testing:
            self.running = True
            Saucer.init_for_new_game()
            # self.fleets.clear()
            self.game_over = False
            self.score = 0
            self.delta_time = 0

    # noinspection PyAttributeOutsideInit
    def init_general_game_values(self):
        self.delta_time = 0
        self.game_over = False
        self.running = False
        self.score = 0

I figure the Fleets are clear because they’ve just been created. We’ll want to check the Saucer.init_for_new_game but it should be redundant. score, game_over and delta_time are dealt with in init_general_game_values. The difference seems to be running.

class Game:
    def __init__(self, testing=False):
        self.init_general_game_values()
        self.init_asteroids_game_values()
        self.init_fleets()
        # self.init_timers()
        self.init_pygame_and_display(testing)
        if not testing:
            self.running = True
        else:
            self.running = False

    # noinspection PyAttributeOutsideInit
    def init_general_game_values(self):
        self.delta_time = 0
        self.score = 0

We could have committed here!

This works, and of course we see how to improve it. Inline the general stuff and change the if, and we see this:

class Game:
    def __init__(self, testing=False):
        self.delta_time = 0
        self.score = 0
        self.init_asteroids_game_values()
        self.init_fleets()
        self.init_pygame_and_display(testing)
        self.running = not testing

    # noinspection PyAttributeOutsideInit
    def init_asteroids_game_values(self):
        pass

That init looks removable. We now have:

class Game:
    def __init__(self, testing=False):
        self.delta_time = 0
        self.score = 0
        self.init_fleets()
        self.init_pygame_and_display(testing)
        self.running = not testing

    # noinspection PyAttributeOutsideInit
    def init_fleets(self):
        asteroids = []
        missiles = []
        saucers = []
        saucer_missiles = []
        self.ship = Ship(pygame.Vector2(u.SCREEN_SIZE / 2, u.SCREEN_SIZE / 2))
        ships = []
        self.fleets = Fleets(asteroids, missiles, saucers, saucer_missiles, ships)

I think we need a commit: simplifying game initialization.

Now I think that Game’s ship is no longer used.

The only usage of self.ship is here:

    def asteroids_tick(self, delta_time):
        self.fleets.tick(delta_time)
        self.control_game(self.ship, delta_time)
        self.process_collisions()
        self.draw_everything()
        if ShipFleet.game_over: self.draw_game_over()

    def control_game(self, ship, dt):
        keys = pygame.key.get_pressed()
        if keys[pygame.K_q]:
            self.keep_going = True
            self.running = False

control_game no longer uses either parm, so we can change its signature.

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

Now self.ship has no uses. Remove that line.

    def init_fleets(self):
        asteroids = []
        missiles = []
        saucers = []
        saucer_missiles = []
        ships = []
        # noinspection PyAttributeOutsideInit
        self.fleets = Fleets(asteroids, missiles, saucers, saucer_missiles, ships)

Time to test and commit: simplifying game initialization. (Again)

I am inclined to change Fleets so that you only pass it collections if you’re trying to set it up with existing objects, since the normal starting point is now with all its collections empty.

Fleets starts like this now:

class Fleets:
    def __init__(self, asteroids, missiles, saucers, saucer_missiles, ships):
        self.fleets = (AsteroidFleet(asteroids), MissileFleet(missiles, u.MISSILE_LIMIT), SaucerFleet(saucers), MissileFleet(saucer_missiles, u.SAUCER_MISSILE_LIMIT), ShipFleet(ships))

I’d like to have the Game just say Fleets() and leave the details up to Fleets.

To do that, I can’t do this:

class Fleets:
    def __init__(self, 
    	asteroids=[], 
    	missiles=[], 
    	saucers=[], 
    	saucer_missiles=[], 
    	ships=[]):

From my understanding of Python, the defaults are created at compile time, so those collections would persist even across new instances of Game and Fleets, so that once they get contents, the contents would stick around. The right way is like this:

class Fleets:
    def __init__(self, 
    	asteroids=None, 
    	missiles=None, 
    	saucers=None, s
    	aucer_missiles=None, 
    	ships=None):
        asteroids = asteroids if asteroids is not None else []
        missiles = missiles if missiles is not None else []
        saucers = saucers if saucers is not None else []
        saucer_missiles = saucer_missiles if saucer_missiles is not None else []
        ships = ships if ships is not None else []
        self.fleets = (AsteroidFleet(asteroids), MissileFleet(missiles, u.MISSILE_LIMIT), SaucerFleet(saucers), MissileFleet(saucer_missiles, u.SAUCER_MISSILE_LIMIT), ShipFleet(ships))

We have to check is not None because test users sometimes send in an empty collection [] and then check to see if it gets filled, so we cannot just say, for example:

        asteroids = asteroids if asteroids else []

We could have committed here!

We are green. Now we can change this:

    def init_fleets(self):
        asteroids = []
        missiles = []
        saucers = []
        saucer_missiles = []
        ships = []
        # noinspection PyAttributeOutsideInit
        self.fleets = Fleets(asteroids, missiles, saucers, saucer_missiles, ships)

We could have committed here!

First to this:

    def init_fleets(self):
        # noinspection PyAttributeOutsideInit
        self.fleets = Fleets()

We could have committed here!

Then let’s inline its only use:

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

We’re green and good. Commit: game init massively simplified.

Let’s sum up.

Summary

Two elapsed hours while writing, holding a conversation, giving the cat a treat. Just a series of simple steps simplifying things.

I think I could have started committing sooner and perhaps done a few more. The last three were 20 minutes apart, which is pretty good I think, but the first commit was 300 lines into the 425 I have right now, and I’m sure there were opportunities sooner. I’ve marked at least some of the places where we could have committed if I were on the ball.

In my defense, your honor, I was just kind of exploring and trying things for a while there, before we really got going.

In any case, the init logic has been trimmed down substantially, although if PyCharm can give me a line count history I do not know how to get it. It would be interesting to know how many lines, methods, and so on we removed.

In any case, we’ve done well, greatly simplifying the init logic and reducing it down pretty close to minimum.

A nice afternoon of trimming. See you next time!