Some Rank Speculation
Python Asteroids+Invaders on GitHub
This is kind of second-level speculation about what makes my program hard to understand and what might make it better.
It seems to me that one of the things that make it hard to understand how my game works is that a single idea, an asteroid and missile collision, starts out here:
class Interactor:
def __init__(self, fleets):
self.fleets = fleets
def perform_interactions(self):
self.fleets.begin_interactions()
self.interact_all_pairs()
self.fleets.end_interactions()
def interact_all_pairs(self):
for target, attacker in self.all_pairs():
self.interact_one_pair(target, attacker)
def all_pairs(self):
return itertools.combinations(self.fleets.all_objects, 2)
def interact_one_pair(self, target, attacker):
attacker.interact_with(target, self.fleets)
target.interact_with(attacker, self.fleets)
From there, we see that given a pair, which might be an asteroid and a missile, we will send two messages, one to each. We look up those and find:
class Asteroid(FLyer):
def interact_with(self, attacker, fleets):
attacker.interact_with_asteroid(self, fleets)
class Missile(Flyer):
def interact_with(self, attacker, fleets):
attacker.interact_with_missile(self, fleets)
Digging a bit further, we find:
class Asteroid(Flyer):
def interact_with_missile(self, missile, fleets):
if missile.are_we_colliding(self.position, self.radius):
self.score_and_split(missile, fleets)
class Missile(Flyer):
def interact_with_asteroid(self, asteroid, fleets):
if asteroid.are_we_colliding(self.position, self.radius):
self.die(fleets)
So to get the picture of how these two objects interact, we have to deal with a few odd things:
-
We see
attacker
andtarget
. We wonder which is which and after thinking about how theall_pairs
works, we realize it could be either way and doesn’t matter anyway, since we make symmetric calls. -
We discover the double dispatch and realize that in the missile v asteroid case, this comes down to:
missile.interact_with_asteroid(asteroid, self.fleets)
asteroid.interact_with_missile(missile, self.fleets)
- Then finally we still have to deal with the fact that the details are spread over two different objects.
Why couldn’t we at least have a single method that dealt with both sides, maybe something like this:
def missile_v_asteroid(missile, asteroid, fleets):
if missile.colliding(asteroid):
missile.score_and_die(fleets)
asteroid.die(fleets)
It seems to me that this would probably be more clear, but how can we get there? It’s not that easy.
One way, and it would be the Kotlin way, I think, would be never to lose track of the types of the Flyers, and, probably, to loop over them explicitly, something like this:
for missile in self.missiles:
for asteroid in self.asteroids:
self.missile_v_asteroid(missile, asteroid, fleets)
Of course if we did that, we’d need loops like this as well:
for missile_1 in self.missiles:
for missile_2 in self.missiles:
self.missile_v_missile(missile_1, missile_2, fleets)
And with 15 Flyer subclasses there’d be 225 nested loops if we were really going to remember all the types. Even with just asteroid, missile, saucer and ship there would be 16. That’s really not going to help. Those 225 loops would call 225 different X_v_Y methods. Screeee!
By the way, there really are about 225 possible interactions in the game as it stands, and they pretty much all occur. And yet there are not 225 special methods. We allow most of the possibilities to default to
pass
with some exceptions where we use@abstractmethod
to ensure that we don’t forget some. If we were to make them all abstract, there would be 15 methods in each Flyer subclass, for a total of 225, most of which would bepass
. That, too, would not be good, which is why I made the judgment call to allow many of the methods to be inherited.
Is there any really good way to do this? My friends, like Hill, look at this and think it’s hard to understand because types are lost, and because things are broken out oddly, and they know the things they’d do that would be supposed to make it all better, such as “Once you know a type, never forget it”, and “Delegate, don’t inherit”, and lots of other good ideas.
But if you’re going to have 15 Flyer subclasses, you’re going to have 225 things showing up in the interactions, no matter how you do it. And you’re very likely going to have to declare 225 little declarations of how to interact, even with the things you want to ignore.
So we might try something different.
Could we partition our objects so that only the “real” flyers, asteroid, missile, saucer, and ship are mixed together, and the others only get used as needed?
We use interactions to detect and count things, such as how many asteroids there are or whether there is a saucer on screen, or a ship. Could we have a Game object that just knew those facts and when we wanted to know something, we’d just ask?
We do curious things like when you want to accrue to the score you just create a Score object and fling it into space. The ScoreKeeper sees it and records the change. The Score itself just dies. We could instead have the Game have the score, or have a ScoreKeeper, and when we wanted to accrue score we’d say game.add_score(20)
or something.
With tricks like that, we could lower the number of special objects in the mix, probably getting rid of ShipMaker, ScoreKeeper, SaucerMaker, Score, and others, replacing them with calls to the Game thing.
Hey, that looks like a god object!
Yes, we are probably creating a god object when we do that. Game is now responsible for scoring, for ship making, and so on. And we have lost the ability to create a new game by just adding a new kind of coin and different individual Flyers. We have moved from a general game framework, to a very specific one. We can’t really go that way.
Is there some middle ground?
We have just begun working on an object that may hint at a direction, the InvaderFleet, a Flyer, which holds about 55 invaders, and it’s the only object that knows the Invaders exist. Perhaps there could be other game-dependent Flyers that could encapsulate some of the complexity. Would that make the program easier to understand? Or would adding that additional layer of indirection make it harder to understand?
This is just rank speculation!
Yes. We can guess. We can scribble on paper. But can we know what will be best? I don’t know whether you can, but I know that my assessments are quite often wrong. So to become sure, I’d have to let my code participate in the design discussion, as Kent Beck once put it.
Method overloading would help, if we had that. In Kotlin we could say something like this:
fun interact(missile: Missile, asteroid: Asteroid) {...}
fun interact(missile: Missile, ship: Ship) {...}
fun interact(ship: Ship, asteroid: Asteroid) {...}
...
We’d still have the problem of calling the right one, which isn’t easy, and we’d still have a metric bunload of methods. I think there’s almost no way around that.
Well, no. What I mean is “I don’t presently see a way around that”.
I don’t know that there is no way, I just personally don’t know one right now. Doesn’t mean I’ll stop looking, doesn’t mean you can’t help me, doesn’t mean I won’t think of one tomorrow. It’s best not to write the possibility off even if we choose not to explore it now. Just because we haven’t used that door is no reason to wall it off.
My friend Bruce Onder suspects there’s a solution to be had in the Entity - Component - System model. I am not convinced of that, and to get there seems like a long walk, and I’m really engaged in creating Space Invaders right now. But he could well be right, and we’d surely learn something from the attempt.
I often like to turn a complicated code structure into simpler code and some data. Could we have some kind of two-dimensional array from Asteroid, Missile -> asteroid_v_missile
?
Hm. Could we compute the method name? In Python we could, something like:
def interact(a,b)
a_name = a.class_name_lower_case
b_name = b.class_name_lower_case
if a_name < b_name:
a_name, b_name = b_name, class_name
method_name = a_name+"_v_"+b_name
method = self.gettattr(method_name)
method(a, b, fleets)
That’s a bit nasty but it’s just done once and thereafter everyone calls asteroid_v_missle
or whatever. It’s also slightly costly but probably not terribly so.
We’d still have our n-squared methods, but they’d all be explicit instead of the current split format, and we could keep them together in a nice alphabetic order.
Would that be better? I don’t know. I’d have to try it, at least in an experiment, to see how to really do it and to see whether I like it.
How would all this play against our desire to have different games just defined by a “coin”? Probably not too badly, though we might choose to partition the class that holds all the X_v_Y
methods, with a separate partition for each game.
My honest opinion right now is that the separate Flyers offer the ability to create a wide range of games without changing the basic framework, and that’s of value to me. And my honest admission is that I don’t see a way to make it more clear than it is. And my honest I swear I’m not being defensive comment is that it’s not hard for me. I think once you get it, you get it, and after that it’s pretty straightforward.
YMMV, always. Here we’re just thinking about the way the code might be. That’s part of our job. The more ways we know, the more likely we are to be able to find a decent way.
Enough speculation. Next time, we’ll get back to work.!