Python Asteroids on GitHub

While sitting in a mostly boring on-line session, I’ll do some small safe changes.

I think what I’ll do is look at the specific Fleet instances, and the references to them when talking to a Fleets instance, and see about removing the separate fleets. The overall goal will be to change Fleets so that it only has one inner collection.

There is an issue with that, hwoever, in that there are specific Fleet subclasses with actual behavior. We’ll encounter those and see what they are. This may not be a situation we can change quite yet.

AsteroidFleet
This one has a timer and methods create_wave, next_wave_size, and tick.
ExplosionFleet
This has explosion_at and make_fragment. We could provide another explosion method for the saucer, but that seems to be the reverse of where we’re going.
MissileFleet
This supports fire, which makes a callback to fetch a missile from Ship or Saucer when capacity is available. That will be an interesting one to remove. I am starting to get worried about this idea.
SaucerFleet
This supports a timer, tick, and bring_in_saucer.
ShipFleet
Here we have a timer, tick, and spawn_ship_when_ready.

Let’s look at references to the Fleets. Asteroids looks deep, let’s start with missiles:

class Fleets:
    def remove_missile(self, missile):
        self.missiles.remove(missile)

    def safe_to_emerge(self):
        if self.missiles:
            return False
        if self.saucer_missiles:
            return False
        return self.all_asteroids_are_away_from_center()

The remove can be redirected to a single fleet. The safe_to_emerge could in principle count actual missiles. We could even have two classes of Missile for firing purposes.

There is a reference in the tests, a test for the accessor, which seemed desirable at some point in time.

There is a test doing game.fleets.missiles.append(missile). That seems a bit around the horn, and anyway it’s a test so if it has to be rewired for a nicer design that’s OK. And in the controls:

class Ship:
        if keys[pygame.K_k]:
            self.fire_if_possible(fleets.missiles)

I think that will route through that other bit of stuff:

class Ship:
    def fire_if_possible(self, missiles):
        if self._can_fire and missiles.fire(self.create_missile):
            self._can_fire = False

There’s the callback. This could route through Fleets readily enough.

I don’t want to think deeply about this yet. Let’s get a look at the others.

The saucers fleet just has one call, to remove a saucer when it’s destroyed, so that would redirect easily. There are two tests, which shouldn’t be a problem.

saucer_missiles has that check we saw above, in safe_to_emerge, and a reference in Saucer.tick used in trying to fire a missile from the saucer.

The ships references include removal, checks in Fleets tick for the thumper, half a dozen tests, one check in ShipFleet.tick that seems to me could be a reference to self, and one in Saucer.tick in case the saucer wants to aim at the ship.

Reflection

While I am really in love with the idea of everything being handled by objects in the mix, it’s pretty clear that the Fleets object, which essentially is the Mix object, is a convenient place to position certain kinds of functionality.

If I were to learn that a design with a smart mix felt better than one where all the game’s logic was in the individual objects, that would be a valuable learning. That said, there are at least two different reasons why I might not push something down into decentralized objects.

One reason might be that it really seems the functionality would be better at a higher level. That is at least a possiblility. In some of the decentralized objects from Kotlin, a single responsibility seems, at least to some readers, to be split between two different objects. While it is good to get a single responsibility down to one object, it is not good to shove it down further so that it becomes shared.

Another reason, however, might be that it seems difficult to refactor from where we are to having the logic down in individual flyers. That is certainly possible, even probable. But it’s not a very good reason to stop at a design we consider to be inferior.

I think that I’ll try to push through the difficulty, to get everything down into the individual Flyers, and then assess whether I’ve gone too far. And, possibly, if it is going to be too far, I’ll be able to reason to that result without doing the work. But I’ll try not to let myself be held back by difficulty.

Disappointing

I had hoped to find something simple to do in the above, but I don’t see much. It would be possible to change things so that the individual Fleet classes act more like a filter, actually passing their adds and removes to the single “real” fleet, and then recasting their methods to refer to that one fleet. That would at least remove the multiple collections from the situation and we could redirect all the Fleets.asteroids calls to just return the basic Fleets object.

Not much benefit to be had there, it seems to me. I guess we’ll just go about adding in the special Flyers and then move on from there. To that end, let’s see if we can add drawing the Score to the ScoreKeeper. That, at least, should be easy.

ScoreKeeper Drawing

The first thing we need to accomplish is to get a ScoreKeeper into the mix. Looking forward, it seems to me that we’ll define a game by having the game create an empty Fleets object and then give it the starting objects, such as a WaveMaker and a ShipMaker and a SaucerMaker and a ScoreKeeper. So let’s find where we create the Fleets and see about enhancing that.

Hmm, it’s in __init__, which isn’t just the thing but we’ll allow it for now. Let’s start with this:

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

That will become:

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

It turns out that we already have the add method:

class Fleets:
    def add_scorekeeper(self, scorekeeper):
        self.others.append(scorekeeper)

So that’s nice. Drawing the score is done like this in game:

    def draw_score(self):
        score_surface, score_rect = self.render_score()
        self.screen.blit(score_surface, score_rect)

    def render_score(self):
        score_text = f"0000{self.score}"[-5:]
        score_surface = self.score_font.render(score_text, True, "green")
        score_rect = score_surface.get_rect(topleft=(10, 10))
        return score_surface, score_rect

What if I change that code to a simple pass and move this to ScoreKeeper. It would almost work, though we should pass in the screen. Ah, we don’t have the font. Bummer. Let’s fudge that for now and then fix it.

I had to move the Fleets init after the pygame init. Code now is:

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

class ScoreKeeper(Flyer):
    def __init__(self):
        self.score = 0
        self.score_font = pygame.font.SysFont("arial", 48)

    def draw(self, screen):
        score_surface, score_rect = self.render_score()
        screen.blit(score_surface, score_rect)

    def render_score(self):
        score_text = f"0000{self.score}"[-5:]
        score_surface = self.score_font.render(score_text, True, "green")
        score_rect = score_surface.get_rect(topleft=(10, 10))
        return score_surface, score_rect

score displays as it should

The score draws correctly. However, some tests are not running.

They are all failing because we cannot create a font unless pygame is initialized and we do not init pygame when we are testing, or at least some of it. Let’s look at that init. Right, we do not init anything:

    # noinspection PyAttributeOutsideInit
    def init_pygame_and_display(self, testing):
        if testing: return
        pygame.init()
        pygame.mixer.init()
        player.init_sounds()
        pygame.display.set_caption("Asteroids")
        self.clock = pygame.time.Clock()
        self.screen = pygame.display.set_mode((u.SCREEN_SIZE, u.SCREEN_SIZE))
        self.init_game_over()
        self.init_score()

I will take the easy way out for now, but I don’t like it. Let’s add a creation parameter to ScoreKeeper telling it whether it is to be in testing mode. It will default to True.

class ScoreKeeper(Flyer):
class ScoreKeeper(Flyer):
    def __init__(self, testing = True):
        self.score = 0
        if not testing:
            self.score_font = pygame.font.SysFont("arial", 48)

And in Game:

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

We are green. Does the score come out during play? It does.

Remove init_score from game. Commit: score now displayed by ScoreKeeper.

Next step will probably be available ships, but this is enough for now. I’m not quite sure how we’ll get the ship information. I wonder how I did it in Kotlin. Would it be cheating to look?

But for now, we’re done.

Summary

Despite how simple it was, this is a pretty big proof of our decentralized concept. Instead of keeping track of score “from above”, we now toss Score objects into space, and a waiting ScoreKeeper receives them and tallies the result, and displays the score. Frankly, I think that’s rather cool.

We scarcely had to do more than move the draw code out of game and into ScoreKeeper, which is what one would hope for. Because of the pygame init rules, we had to suppress font definition during testing, but that’s just the price of using pygame, I think.

It might be better to create a general Fonts object and put all the Fonts in there, similar to how we put the constants into the u object. I can’t say it thrills me as a much improved design. Somewhat improved, yes, it probably would be better than passing a testing flag around. I’ll make a Jira note.

Anyway, a simple change. Unwinding the complicated Fleets/Fleet situation, though: that will probably have to wait until all our little objects are in place.

See you next time!