Python Asteroids+Invaders on GitHub

The game lacks the terrifying beat sound that characterizes the arcade game. Let’s put it in. Begin with a mostly unrelated question.

Question

I am running out of things to do with Asteroids / Invaders, and will find myself in need of something to do and to write about quite soon. I am interested in your ideas, while not promising to change what I do in any fashion. My rough criteria may include:

  1. I freely grant that I do these things to keep my mind active, and am grateful for the few of you who bother to read them. The articles are for me as much as they are for you.
  2. I’d prefer to continue with Python and PyCharm on the Mac. I could imagine going back to Kotlin and IDEA. I’m not very open right now to picking up a new language, as I am enjoying Python quite a bit.
  3. I enjoy doing the game type things, but simple graphics is a limitation. I wouldn’t want to get involved with a game that required me to do a lot of art or 3-D modeling or some such thing.
  4. I’ve tried the Raspberry Pi and similar hardware, and did not enjoy the limited affordances of those systems. Too much fiddling wondering why nothing was happening. And I am terrible with hardware, which made wiring up little sensors and lights mostly irritating.
  5. I suppose that if I were a good person, I’d do something with a more or less standard windowing GUI, but I do not enjoy that kind of programming and don’t feel that I have much to offer on the topic.

I could, I suppose, write more general non-code kinds of articles, as I sometimes do, but as I am not out there coaching, training, or consulting, I am not in touch with what people are encountering, so much of my advice is probably worth less than you pay for it.

Ideas are welcome. I may not choose any of them, certainly not yours, [REDACTED], but they may give me an idea that I find tasty.

The Beat

There are four tones involved in the relentless, pounding, frighteningly ominous sounds that accompany the inexorable approach of the implacable evil alien invaders.

The sounds, according to Computer Archaeology, speed up as the number of invaders on the screen is reduced:

The speed of the fleet tones does NOT match the actual 
speed of the alien rack. The delay-between-tones is read 
from a table and depends on how many aliens are left in play.

; Alien delay lists. First list is the number of aliens. 
; The second list is the corresponding delay.
; This delay is only for the rate of change in the fleet's sound.
; The check takes the first num-aliens-value that is 
; lower or the same as the actual num-aliens on screen.
;
; The game starts with 55 aliens. The aliens are move/drawn 
; one per interrupt which means it
; takes 55 interrupts. The first delay value is 52 ... 
; which is almost in sync with the number
; of aliens. It is a tad faster and you can observe 
;the sound and steps getting out of sync.
;
1A11: 32 2B 24 1C 16 11 0D 0A 08 07 06 05 04 03 02 01
1A21: 34 2E 27 22 1C 18 15 13 10 0E 0D 0C 0B 09 07 05     
1A31: FF   ; ** Needless terminator. The list value "1" catches everything.
The first value in the table is a delay of 52 interrupts 
between sounds when there are 50 or more aliens. When there 
are 55 aliens the step sounds are faster than the rack. 
When there are 50 aliens the step sounds are slower than the rack.

When there are 43 aliens the step speed changes to 46 interrupts 
between changes. Again, the sounds are faster than the aliens. 
When there is only 1 alien left the delay minimizes to 5 interrupts 
between changes. Anything faster sounds unpleasing.

Even though the alien racks speeds up very smoothly as they die, 
the step sounds take sudden changes. If you wait a bit between 
killings you can hear the sudden changes -- especially at the 
beginning of the game when the deltas in the table are large.

How shall we manage this issue? It seems to me that since the InvaderFleet knows how many invaders there are, and is responsible for marching them about, it is probably the place to put the sounds. Other objects play the sounds that relate to their activities, so this makes at least some sense.

The timing table above, at 1A21, is the number of 1/60th of a second ticks between tone changes, given that the number of invaders is greater than the number in 1A11.

I think we’ll want some kind of helper object for the InvaderFleet to use. It will keep track of which tone has been played, and how long it has been since it was played, and we’ll call it every cycle. It’ll just emit a tone as needed.

It will need to know how many invaders exist, to set the delay time.

I propose a TonePlayer object, with a single useful method, maybe play_tone(number_of_invaders). Let’s test-drive a little something.

class TestTonePlayer:
    def test_exists(self):
        TonePlayer()

class TonePlayer:
    pass

Perfect. Commit: initial TonePlayer tests and class.

Now the way this thing is going to work is that it’ll be called on update, because InvaderFleet happens to implement that method and does not implement tick, which might make more sense but which we rarely use. Some have argued that we should get rid of that event entirely.

The TonePlayer will have, I don’t know, maybe a variable called ticks_remaining, which it will, I don’t know, initialize once in a while based on the number of aliens, and which it will generally count, oh, down, and when it reaches zero, play the current tone, which ever it is.

So we’d better give TonePlayer the tones and a current_tone value, probably an index.

    def test_tone_order(self):
        player = TonePlayer()
        assert player.next_tone() == "fastinvader4"

Why 4? Because it is the highest of the tones, so we’ll start with it. Weird, but that’s how the files came, and we certainly don’t really care what they are named.

Test won’t compile. Fake it till you make it:

class TonePlayer:
    def next_tone(self):
        return "fastinvader4"

Green. Commit: TonePlayer can return one tone.

Why am I committing these stupid things? Because small commits are good and I want to build the habit of committing every time I possibly can.

We need a harder test. I’ll go the whole way this time, extending the test we have:

    def test_tone_order(self):
        player = TonePlayer()
        assert player.next_tone() == "fastinvader4"
        assert player.next_tone() == "fastinvader1"
        assert player.next_tone() == "fastinvader2"
        assert player.next_tone() == "fastinvader3"
        assert player.next_tone() == "fastinvader4"

We’ll want that nice wraparound now.

class TonePlayer:
    def __init__(self):
        self._tones = ("fastinvader4", "fastinvader1", "fastinvader2", "fastinvader3")
        self._tone_index = 3
        
    def next_tone(self):
        self._tone_index = (self._tone_index + 1) % 4
        return self._tones[self._tone_index]

Now we have the names and the cycling. Commit: TonePlayer cycles tones.

Now we need to work toward the TonePlayer actually playing the tone every so often, and to changing how often it will do it. I think we could use a fake sound player here but I am inclined toward something less intricate. I think we’ll give the TonePLayer a debug flag or something. But first we can test the lookup of the timing number.

    def test_delay(self):
        player = TonePlayer()
        assert player.delay(55) == 0x34

Again we fake it:

class TonePlayer:
    def delay(self, number_of_aliens):
        return 0x34

I think there is some clever way to test a batch of values with pytest, but I do it so seldom that if there is I don’t know it. Ah, here we go:

    @pytest.mark.parametrize("invaders,delay", 
        [(55, 0x34), (49, 0x2E)])
    def test_delays(self, invaders, delay):
        player = TonePlayer()
        assert player.delay(invaders) == delay

The above test is run twice, once for each pair in the list. The second one fails, since we still have our fake it method. Which I forgot to commit. Ah well, no one here is perfect.

I’ve put this much into the object, two new tables of values:

class TonePlayer:
    _invader_count = [0x32, 0x2B, 0x24, 0x1C, 0x16, 0x11, 0x0D, 0x0A, 
        0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]
    _delays = [0x34, 0x2E, 0x27, 0x22, 0x1C, 0x18, 0x15, 0x13, 0x10, 
        0x0E, 0x0D, 0x0C, 0x0B, 0x09, 0x07, 0x05]

Those are just copied from the original source. We want our delay method to return the value of _delays that corresponds to the first value in _invader_count that is less than or equal to the input number.

I’m not sure of the best way to get that first value, so I’ll code a bit by intention:

    def delay(self, number_of_aliens):
        index = self.first_index(number_of_aliens)
        return self._delays[index]

It remains to create first_index. We can do that in the dumbest possible way, since it is isolated and we can improve it readily.

    def first_index(self, number):
        less_thans = [index for index in range(len(self._invader_count)) if self._invader_count[index] <= number]
        return less_thans[0]

This passes the test. Let’s add in a couple more values to the list before we finish up.

    @pytest.mark.parametrize("invaders,delay", 
        [(55, 0x34), (49, 0x2E), (0x11, 0x18), (0, 0x05), (-8, 0x05)])
    def test_delays(self, invaders, delay):
        player = TonePlayer()
        assert player.delay(invaders) == delay

And I improve the method:

    def delay(self, number_of_aliens):
        for invaders, delay in zip(self._invader_count, self._delays):
            if invaders <= number_of_aliens:
                return delay
        return 0x05

Commit: correctly compute delay as f(number_of_aliens)

Let’s normalize the names just a bit.

class TonePlayer:
    _invader_count_limits = [0x32, 0x2B, 0x24, 0x1C, 0x16, 0x11, 0x0D, 0x0A, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]
    _delays = [0x34, 0x2E, 0x27, 0x22, 0x1C, 0x18, 0x15, 0x13, 0x10, 0x0E, 0x0D, 0x0C, 0x0B, 0x09, 0x07, 0x05]

    def __init__(self):
        self._tones = ("fastinvader4", "fastinvader1", "fastinvader2", "fastinvader3")
        self._tone_index = 3

    def next_tone(self):
        self._tone_index = (self._tone_index + 1) % 4
        return self._tones[self._tone_index]

    def delay(self, current_invader_count):
        for invader_count_limit, delay in zip(self._invader_count_limits, self._delays):
            if invader_count_limit <= current_invader_count:
                return delay
        return 0x05

Those names are kind of long but I think better. We’ll allow it. Commit: tidying.

My dear wife has come home with Original Olga’s for lunch, so I’ll call this done.

Summary

We’re taking small steps toward playing the beat sounds. We have a little object that knows how many ticks to delay given how many invaders exist, and which tone to play, cycling 4, 1, 2, 3 indefinitely. We’ll assemble those into some useful sounds next time.

Odd place to stop? Yes … but with our tests running and the code that exists working, we can stop any time. So we do.

See you next time!