Python Asteroids on GitHub

A Mastodon exchange with Rickard makes me wonder whether this program violates Kent Beck’s “Rules of Simple Design”. I need to think about this.

It started a couple of days ago.

Rickard Lindberg (2 days ago)
@RonJeffries What about writing domain/”game rule” tests at a higher level? That is, you put a bunch of flyers in the collection, call update (or whatever it was called), have the collection do all the interact_with_, and assert something on the collection.

Then you could more easily trust your tests and you are free to implement it however you want (implementation inheritance, events, etc).

I think such tests might even read quite well as descriptions of the game rules.

Ron (in reply)
a good idea … with a centralized test there’s at least a place to look. Unfortunately such tests have to involve a lot of time stuff due to all the timers.

And continued today.

Rickard Lindberg (today)

@RonJeffries I don’t know. I still feel that you have to somehow write tests for the game rules/mechanics.

I don’t think I would trust objects interacting in various ways to produce a “correct” game. I think it’s fine to implement it that way, but I don’t think I would feel confident enough testing it that way.

Hopefully I will get around to trying this design in my game, and maybe then I will have some more insights. Perhaps I will change my mind about my statement above. ;)

Ron

If every object does the right thing at the right time, then all the right things happen.

However, this design may break one of Beck’s four rules of simple code: the code must express all the programmer’s ideas about the code.

Must think about that …

So, let’s think about those two ideas.

  1. Certainly it must be true that if every object does the right thing at the right time, the program must work. This is the root reason why micro-testing works. Of course, it would have to be done perfectly, which never happens, but the principle, I think, remains true. Given those truths, would we profit from some tests the deal with many flyers? How much are they needed, and why?

  2. Beck’s rules ask the code to express all our ideas about the code, and, implicitly, to express them clearly. What aspects of our design for Asteroids™ are not clearly expressed in this code, and what could we do about this? One way of meeting Beck’s criterion is with clarifying tests. Would they help us here with understanding?

I don’t think these are really two independent ideas. Clarity comes from code plus tests, so these two ideas are inextricably intertwingled. We’ll tease them apart a bit and then let them come back together a bit.

Individual Objects and Interactions.

Let’s begin by talking about our individual objects and their interactions, and whether those describe everything about the game. As we go, I’ll comment on whether there are tests of those things and what those tests are like. Let me say right here that I think some of those existing tests are at a higher level than I would prefer, which is somewhat counter to Rickard’s notion: he seems to desire more higher-level testing.

Everything about the game is about what we see on the screen, and about its five control buttons, and I’ll speak in those terms.

Basic Game Play
The game displays asteroids, a ship, and a saucer. The ship is controlled by the player, and the player’s mission is to gain points by shooting down the asteroids. The saucer appears from time to time and shoots missiles which can destroy the ship. If an asteroid hits the ship, the ship will be destroyed. The player gets some finite number of ships for their quarter investment.
Asteroids
The game displays waves of asteroids that move across the screen. When a wave of asteroids is created, they appear at random positions along the left and right edges of the screen, and move at a constant speed and angle. When all the asteroids in a wave are destroyed, a new larger wave is created. Wave size starts at four and increases to eleven and then stays at eleven thereafter.

I do not believe that we have specific tests that show that asteroids start at the edge. We do have tests for MovableLocation that indicate that, unless it is told otherwise, it will move at its existing velocity. I think the programmer created the asteroids early on and since their behavior on the screen is so visible, he neglected to write any such tests. I would not argue that we shouldn’t have them, but I would argue that we don’t need a larger scale test to verify how they move. We should probably have some test.

The Ship (q.v) can fire Missiles (q.v). Missiles emit from the front of the ship and their intrinsic velocity is a standard speed directly ahead. If the ship is moving, the ship’s velocity is added to that of the Missile as one would expect. There can be no more than four ship missiles in flight at a time.

There is a test for the limitation that the ship can fire only four missiles. There is a test of the calculation that the ship does to get the missile velocity. There is no test that shows that it actually assigns that velocity to the missile it fires. The code for doing that is clear and trivial, but it is not tested.

class Ship(Flyer):
    def create_missile(self):
        player.play("fire", self._location)
        return Missile.from_ship(
        	self.missile_start(), 
        	self.missile_velocity())
Asteroid vs Missile
Asteroids come in three sizes, 2, 1, and zero. If a ship’s missile hits an asteroid, the larger asteroids will split into two asteroids moving randomly away from the location of the collision, and points will be awarded to the player depending on the size of the asteroid hit. Smaller asteroids are harder to hit and score more points.

There are a couple of tests that check that hitting an asteroid of size two correctly scores 20. There is no test for the score for hitting the other sizes. There is a ScoreKeeper test that checks that ScoreKeeper correctly accumulates any scores it is given. There is, however, a test that a missile from_ship is provided the correct score list. There is no test that the Asteroid actually uses that information, or if there is, I cannot find it.

I don’t mind saying that this is getting a bit embarrassing. I think I have more tests in this version of Asteroids™ than I’ve ever had before, but there are certainly things like this that have no tests at all. Before this article is finished I may have to erase it to maintain my reputation. On the other hand, my reputation is, I hope, one of someone who shows what really happens when he does his best to write programs, not someone who polishes his programs and writings until they appear to be perfect. I’m about reality, not perfection.

Let’s pause here and reflect about Rickard’s notion. If I understand it, he would like to have one or more tests that put a bunch of objects into the Fleets, executed the game for a while, and then checked the results. Practicality aside, you’d think that such a test might involve asteroids being shot, split and scored, and saucers shooting asteroids and not being scored, and shooting ships and at the end of the test there would be these objects and no others and the score would be this value, with so many ships left.

Honestly, I wish I had a test for scoring on size one and zero asteroids, but I do not wish I had a big story kind of test that did that deep down in the game. In fact, I want that test badly enough that I think I’ll write it.

    def test_missile_vs_asteroid_scoring(self):
        fleets = Fleets()
        fi = FI(fleets)
        pos = Vector2(100, 100)
        vel = Vector2(0, 0)
        asteroid = Asteroid(2, pos)
        missile = Missile.from_ship(pos, vel)
        asteroid.interact_with_missile(missile, fleets)
        scores = fi.scores
        assert scores[0].score \
        	== u.MISSILE_SCORE_LIST[asteroid.size]
        asteroid.size = 1
        asteroid.interact_with_missile(missile, fleets)
        scores = fi.scores
        assert scores[1].score \
        	== u.MISSILE_SCORE_LIST[asteroid.size]
        asteroid.size = 0
        asteroid.interact_with_missile(missile, fleets)
        scores = fi.scores
        assert scores[2].score \
        	== u.MISSILE_SCORE_LIST[asteroid.size]

OK, so there is a test that checks all three sizes of asteroids against a ship missile that hits them, and determines that the correct score is returned in all cases.

Added
Now, since Rickard specifically mentions using interact_with in the tests he’s talking about, perhaps this is an example of just what he means. If that’s the case, we probably don’t have any real disagreement at all. That would be nice.

So if the spec says how many points will be awarded for ship missile hitting an asteroid, this test, it seems to me, clearly checks the truth of that. Is it good for all such ship missile vs asteroid strikes? It convinces me.

At this moment, I would ask Rickard whether he agrees that we can be sure that ship missile vs asteroid collisions score correctly. And if he said we cannot be sure, I’d try to devise another low-level test to help him be sure.

I don’t know what I’d say if he continued to demur, saying he would only be convinced by a large-scale test with lots of objects. Well, I do know. The rules of pair programming are that if one of the pair is not convinced by existing tests, we write more tests. So we might wind up writing a larger story test for this … but I would hope not.

I could, of course, be wrong to hope not. I could be deluding myself, or it could just be harder than I think it is to see that this test allows us to be sure that asteroid hits are correct.

But wait. What if there was a bug in the code that does the hitting and scoring. Here it is:

    def interact_with_missile(self, missile, fleets):
        self.split_or_die_on_collision(fleets, missile)

    def split_or_die_on_collision(self, fleets, missile):
        if missile.are_we_colliding(self.position, self.radius):
            fleets.append(Score(self.score_for_hitting(missile)))
            self.split_or_die(fleets)

    def are_we_colliding(self, position, radius):
        kill_range = self.radius + radius
        dist = self.position.distance_to(position)
        return dist <= kill_range

    def score_for_hitting(self, attacker):
        return attacker.scores_for_hitting_asteroid()[self.size]

    def split_or_die(self, fleets):
        fleets.remove(self)
        self.explode()
        if self.size > 0:
            a1 = Asteroid(self.size - 1, self.position)
            fleets.append(a1)
            a2 = Asteroid(self.size - 1, self.position)
            fleets.append(a2)

I want to say that the above code clearly says that if a missile and asteroid are colliding, the score will be emitted. And I want to say that the code clearly says that they are colliding if their distance apart is less than the sum of their radii. And I want to say, “therefore we are sure that missile asteroid collisions score correctly”.

And if you’re not convinced then I get to challenge you to write a test that abides by our intention, but that will fail. And I’m betting that you can’t do it. Not in an antagonistic way, because if you can come up with that test, you have saved us from a defect. I am convinced that this code works. I could be wrong about that and if I am, I owe it to myself to be glad to have learned. And to my pair, and my team.

Micro-Tests Convince Me

This excursion should help you see why micro-tests are convincing to me. And, frankly, to see that some of my tests are not as micro as they might be. I think I’m a long way from the finest-grain possible.

If these tests are not convincing for you, I would be most interested to understand why not. I’d want you to base your concerns in what the code is, not what it might be.

Our job is to write a combination of code and tests that in concert, work. Our job is not — I believe — to test everything we can think of. We test only things that we think of that could actually go wrong.

“What if the missile comes close enough to break the asteroid but not close enough to score?”

That can’t happen: we look at the code and can see that a single check covers both scoring and breaking. Do we need a test for that? How would we write it? Make the two distance between missile and asteroid one more than the sum of radii? Exactly equal? One less?

“What if there are four missiles on the screen and there’s a bug where the third one hits an asteroid but the game doesn’t notice?”

That can’t happen: the game interacts all pairs of objects, and we can see that it does that.

“What if two asteroids are close together and the missile comes upon them such that in one step is it within range of both?”

Good one! That can happen in principle and I think I have seen it happen in practice. I am rather certain that the missile will kill both asteroids and accumulate both scores. I do not have a test for that. Should I have?

Why not? Let’s have one.

    def test_missile_scores_two_asteroids_at_once(self):
        fleets = Fleets()
        fi = FI(fleets)
        pos = Vector2(100, 100)
        vel = Vector2(0, 0)
        expected = u.MISSILE_SCORE_LIST[2] + u.MISSILE_SCORE_LIST[1]
        asteroid1 = Asteroid(2, pos)
        asteroid2 = Asteroid(1, pos)
        missile = Missile.from_ship(pos, vel)
        keeper = ScoreKeeper()
        fleets.append(asteroid1)
        fleets.append(asteroid2)
        fleets.append(missile)
        fleets.append(keeper)
        fleets.perform_interactions()
        assert len(fi.asteroids) == 4
        assert len(fi.scores) == 2
        fleets.perform_interactions()
        assert fi.score == expected

There we go. Two asteroids in range of one missile, both split, both score. We could do three, or choose other sizes but if this much works as we expect, do we really need more?

Micro-Test Summary

Probably you can see why I think sufficient micro-tests will tell us everything we need to know about the game. You may not agree, but I hope you can see what I’m thinking, that we can always write a fairly low-level test to ensure that things work.

On the other hand, a fleets-level test like this last one isn’t all that low-level, but it seems to be a pretty easy level at which to have tested. I don’t want to quibble over how micro is micro, my point only is that (I find) that a sufficient number of small tests works better for me than larger ones. If I knew a way to make that last one smaller, more micro, I would do it.

Expresses All Our Ideas

I’ve gone on long enough for this article, so let’s just touch this topic briefly. I think the key questions, as I understand it, are these:

Can everything about Asteroids™ be clearly expressed and tested in terms of interactions of the Flyers, or are there things that can only be shown to work by large scale tests?

Does the existing test and code base express our design ideas sufficently clearly?

I should say right off the bat that you’ve seen everything that Rickard and I have shared on the subject, so there’s no reason to be certain that we’re even talking about the same kind of tests. I’m thinking that he means a big story test with all the objects in it and a long series of time ticks and checks on the game state throughout. He might be thinking of tests like the one I just wrote, or even simpler ones, with just a few objects and a few interactions, but more than one test. We may not disagree at all on that.

And I’m sure that if we sat down to do it, we’d do something different from what either one of us is thinking now. That’s the nature of collaboration, and it’s certainly something that is missing from almost all of my articles, because I never have anyone working side by side with me, helping to adjust my path through the weeds.

But what about expressing ideas?

I think that if we were to describe the game, we’d continue in the vein above, with more descriptions like:

Saucer
The saucer appears at seven-second intervals, at some point along the right or left side of the screen. It alternates direction left to right then right to left at each appearance.

Every second, the saucer considers whether to change direction. Half the time, it will set its direction to straight across, 1/4 of the time it will move upward at 45 degrees, 1/4 downward at 45 degrees.

The saucer can have only two missiles on screen at a time and it tries to fire every half second. The large saucer fires in a random direction 3/4 of the time and fires directly at the ship 1/4 of the time. The small saucer (q.v.) fires at the ship on every shot.

We actually do have tests for a lot of that, I think. But my point now is to ask whether the code itself expresses all our intentions, and does so clearly. Yes, clarity comes from both tests and code, but we do need the code to be quite clear.

Just to pick something at random, I think the code does express that the saucer changes direction every second:

class Saucer(Flyer):
	def __init__(...)
	    ...
        velocity = Saucer.direction * u.SAUCER_VELOCITY
        self._directions = (
        	velocity.rotate(45), 
        	velocity, 
        	velocity, 
        	velocity.rotate(-45))
        self._zig_timer = Timer(u.SAUCER_ZIG_TIME)
        ...

    def update(self, delta_time, fleets):
        player.play("saucer_big", self._location, False)
        self.fire_if_possible(delta_time, fleets)
        self.check_zigzag(delta_time)
        self._move(delta_time, fleets)

    def check_zigzag(self, delta_time):
        self._zig_timer.tick(delta_time, self.zig_zag_action)

    def zig_zag_action(self):
        self.accelerate_to(self.new_direction())

    def new_direction(self):
        return random.choice(self._directions)

Now I grant freely that that doesn’t just say exactly what the text above says.

So does the code express that idea? Yes. Does it do so as clearly as it might? Perhaps not.

And if we got the code expressing each micro rule very clearly … would we still wonder about the bigger picture?

Or is Asteroids™ truly only defined in terms of the interactions of its Flyers?

I do not know the answer, and plan to explore these questions further. Let me sum up by saying why I do this.

Summary

I wonder whether the code’s design is clear, to a new reader, or even to an old reader like me. And I wonder how it can be made more clear.

My real point, in all these articles, is to show that there is a lot to think about in programming, an almost infinite space of idea and detail, and that it is both interesting, and valuable to do that thinking. It makes our code better, it makes it more reliable, and, for me at least, it’s just very interesting.

Wishes

If you program for a living and do not find it interesting, well, I’m sorry that you have to spend so many hours doing something that isn’t interesting. I wish better than that for you.

I have the luxury that I can choose things that interest me, but in all my years of programming, I have found the actual work always to be interesting and always to offer the possibility of learning and improving my skills. I’ve always loved programming, and when I was trying to be a manager or executive, I never loved those as much.

I hope that you love what you do, and hope that if you don’t, you’ll be able to shift over to something that you do love. Life is too short to do work that we cannot enjoy.

See you next time!