Python Asteroids on GitHub

I really liked the “decentralized” version of Asteroids that I did in Kotlin. Thinking about doing it again, in Python. Refactor to a new design? Why not?

The essential notion of the “decentralized” design for the game was that the universe consisted of undifferentiated objects in space. There were asteroids, missiles, ships, and saucers, of course. There were also other objects, scores, scorekeepers, and hyperspace bombs.

The Kotlin design had a lot of mechanism that I was trying out, allowing me to “inherit” some behavior and get around type checking, but we’ll not worry about that and just talk about how it works conceptually.

There are all these objects in the universe. I called them “flyers”. The game gave each flyer a chance to update. It then processed interactions, in three phases. First, “begin interactions” was sent to every object, in case it wanted to initialize any members. Second, “process interactions” went through all unique pairs of the objects, sending “interact_with” to the first, and passing the second as a parameter. By convention, the first one always responded to “interact_with” by sending a message to the second one, saying e.g. “interact_with_asteroid”, or whatever its class might be.

The second one now knows everything it needs to know about the interaction. Generally it would ignore the interaction, such as asteroid vs asteroid, or it would check range and possibly destroy itself, as in asteroid vs missile.

There were other possibilities. A ScoreKeeper receiving “interact_with_score” would add the score to the total score. A Score, upon receiving “interact_with_scorekeeper” would destroy itself, knowing that its contents had been tallied.

A HyperspaceBomb (called something else, perhaps Destructor?) was such that a Ship encountering it would destroy itself, and the Bomb, upon encountering a Ship would destroy itself.

What I liked about this design was that all the objects took care of themselves. There was no grand overseer, no master collider object, and so on. Everyone just got a series of update, before, interact, after, draw calls, and everything sorts out on an individual basis. It was as if each of the objects were running their own process, although of course they were not.

No object needs knowledge of what others might do: it only decides what it will do.

Naturally, as the interaction process goes on, objects can be destroyed or created. The missile might just destroy itself. The asteroid might destroy itself but spawn two new asteroids. So the interaction process passed a Transaction to each object as it interacted, and the object could “add” or “remove” objects in the transaction. It would only remove itself, but could add any object, a new Asteroid, a Score, a HyperspaceBomb, whatever.

After all pairs had interacted, the transaction was applied, and the process continued.

The pattern of interactions in this scheme is pretty simple. I think this is approximately right:


col vs row aste bomb miss sauc ship scor keep
asteroid - - d[s] d d - -
bomb - - - - d - -
missile d[s] - d d[s] d - -
saucer d - d[s] - d - -
ship d d d d - - -
score - - - - - - d [*]
scorekeeper - - - - - d[*] -

Legend

  • d - destroyed
  • d[s] - destroyed with possible score creation
  • d[*] - accumulates score (destroyed), keeper never destroyed

The table is symmetric, although the actual operations are split between the participants. Generally either they mutually destroy or they do not interact, but there is a bit of rigmarole about scoring, depending on who fired the missile.

One can also note that the table is partitioned. There are true flyers, which are mutually destructive in some interactions, and the two score-related objects, which interact with each other and with nothing else.

But wait, there’s more!

How do we handle running out of asteroids, or the saucer needing to run, or spawning a new ship? With more flyers:

SaucerWatchers, ShipWatchers, AsteroidWatchers can be created to count the saucers, ships, asteroids, and take action when there are none present. These objects will include timers, of course.

I’m not at all sure which of these one needs, and there are different ways to handle them. I recall that there was some very interesting hand-off in the Kotlin version where one object spawned another when the first was destroyed, and then the other waited a while, spawned the first and destroyed itself. That may have been a ShipWatcher or ShipDropper or something like that.

Maybe I even made a TimedDropper that would run for a while, time out, then drop something it was given into the mix, to make something happen after a while.

The Point

The point is, and I do have one, the design was really quite interesting, in that everything (most everything) that went on was essentially handled by the atomic objects, an asteroid or missile or ship or one of the special ones, and nothing to speak of was handled by some all-knowing or somewhat-knowing “god” or “demi-god” object.

I like the idea so well that I think I’ll demand that the dev team convert to a more decentralized design for some reason that I’ll make up. Of course, I’ll ask the team to make progress every day, keeping the program running all the time. That might be possible, though I am honestly not sure quite how. But I just thought of it, give me a moment here, OK?

I think it’ll be fun. And that’s why I’m here.

See you next time!