Python Asteroids+Invaders on GitHub

Can we figure out a decent way to improve the attract mode? Should we make the robot player a better player? Perhaps not.

First of all, the term “robot player” just popped into my mind as I was writing the article blurb, because Driver isn’t a very good name and Driver / Player is probably worse. Let’s rename that class. Should it be Robot, or RobotPlayer? I would usually lean against RobotPlayer, because it’s not really a subtype of Player, and I generally follow Beck’s pattern of qualifying a class name by prepending a subclass name, so that RobotPlayer would be a subclass of Player. It is, however, a conceptual player. Let’s rename it. And let’s remember to rename its interaction methods.

As soon as I rename the class, one of my subclass checking tests break, signaling that I do need to rename the method. PyCharm manages both tasks perfectly. Well done, PyCharm. Commit: rename Driver to RobotPlayer.

Now we have some work items left over from yesterday (and the more distant past):

To fancy up the attract mode, there are things we could do, such as:

  • Randomize the start a bit so that the Driver behavior looks different each time;
  • Randomize the driver a bit, perhaps just delaying shots sometimes;
  • Give driver the ability to be concerned about invaders getting too low, shifting priority from nearest to lowest.
  • Maybe make it always shoot the lowest open invader? Might be interesting.

For now we have a decent attract mode and our other issues remain:

  • No invader shots;
  • Game Over comes up too fast after last Player is hit.

As I watch the robot playing, I notice that it makes two serious mistakes. Perhaps the lesser mistake is that when there are only a few invaders left, it misses a lot. I think this is because they move faster and it needs to lead them. It tried to position directly under them, and if the top invader is the target, and there aren’t many invaders, it can be have moved a full invader-width or more by the time the shot gets there.

Perhaps the larger mistake is that the robot never selects the left-most column, because its early moves take it off to the right, and that column is never the nearest open column, so it is that column that ends the Robot’s existence.

I’m pretty sure, based on six decades of programming, that we could solve those issues and make the robot an even better player than it already is. But I think it is already better than I am.

We might undertake writing a perfect robot at some future time. I think for now, we should see about randomizing it a bit, and we should address some of the other issues on our list. But is fun to speculate about a better robot and maybe we’ll do that in our copious free time. For now let’s do some work. Nothing too tricky, mind you.

Let’s see about game over coming up so soon.

The RobotPlayer is spawned in a coin:

coin.py
def invaders_game_over(fleets):
    keeper = InvaderScoreKeeper()
    for flyer in fleets.all_objects:
        if isinstance(flyer, InvaderScoreKeeper):
            keeper = flyer
    fleets.clear()
    fleets.append(keeper)
    fleets.append(RobotPlayer())
    fleets.append(InvadersGameOver())
    ...

Who sends that message and when and why? Two places:

class InvaderFleet(InvadersFlyer):
    def process_result(self, result, fleets):
        if result == CycleStatus.CONTINUE:
            pass
        elif result == CycleStatus.NEW_CYCLE:
            self.step_origin()
        elif result == CycleStatus.REVERSE:
            self.reverse_travel()
        elif result == CycleStatus.EMPTY:
            fleets.remove(self)
            capsule = TimeCapsule(2, self.next_fleet())
            fleets.append(capsule)
        elif result == CycleStatus.TOO_LOW:
            from core import coin
            coin.invaders_game_over(fleets)

class PlayerMaker(InvadersFlyer):
    def reserve_absent_game_over(self, fleets):
        fleets.remove(self)
        coin.invaders_game_over(fleets)

Both of these are quite abrupt. The one in PlayerMaker is perhaps most interesting and useful to figure out first. If there is a ReservePlayer available, we use a TimeCapsule:

class PlayerMaker(InvadersFlyer):
    def reserve_give_player_another_turn(self, fleets):
        fleets.remove(self)
        delay_until_new_player = 2.0
        a_bit_longer = 0.1
        self.provide_new_player(delay_until_new_player, fleets)
        self.provide_new_maker(delay_until_new_player + a_bit_longer, fleets)

    def provide_new_player(self, delay, fleets):
        player_capsule = TimeCapsule(delay, InvaderPlayer(), self.reserve)
        fleets.append(player_capsule)

    def provide_new_maker(self, delay, fleets):
        maker_capsule = TimeCapsule(delay, PlayerMaker())
        fleets.append(maker_capsule)

We toss a new player into the mix with a delay of two seconds, and a new PlayerMaker at 2.1, to give the new player a chance to be established, lest the maker accidentally runs first and finds no player.

But there is no delay when we have no reserve, we just fire the coin immediately. We need a way to defer a coin for a couple of seconds.

Asteroids’ game over is different, in that it doesn’t clear the screen as we do in Invaders: it just brings up its Game Over screen and does not rez a new ship, because they have run out. So the Asteroids game continues on screen from wherever you fail.

I wonder what would happen if we did that.

I try it, and two interesting things happen. First, the Game Over screen comes up immediately but the explosion continues. Second, the invaders just keep coming from where they are, and third (I was mistaken about the “two”), no robot appears, and fourth (very mistaken) the invaders are still firing their shots. That last bit is notable because we aren’t getting any shots from the game over coin.

I’ll put a robot in as well:

    def reserve_absent_game_over(self, fleets):
        fleets.remove(self)
        fleets.append(InvadersGameOver())
        fleets.append(RobotPlayer())

That starts the robot right away and it just starts blazing away. And the invaders keep shooting. When they finally reach the bottom, we get back to our standard state, with the invaders not shooting. We should put the robot in a time capsule in the above code.

That works nicely. I’ll make a short video for your amusement. Commit: game now just inserts a robot two seconds after your last player is hit by a shot.

A careful look will see the robot beginning to miss as the invader count decreases and the invaders move faster. If I abused your bandwidth even further with a long enough video, we would sooner or later see the robot get preempted by the other game-ending event, invaders too low. What should we do in that case? Recall that the code is currently this:

    def process_result(self, result, fleets):
        if result == CycleStatus.CONTINUE:
            pass
        elif result == CycleStatus.NEW_CYCLE:
            self.step_origin()
        elif result == CycleStatus.REVERSE:
            self.reverse_travel()
        elif result == CycleStatus.EMPTY:
            fleets.remove(self)
            capsule = TimeCapsule(2, self.next_fleet())
            fleets.append(capsule)
        elif result == CycleStatus.TOO_LOW:
            from core import coin
            coin.invaders_game_over(fleets)

Let’s assume that we are going to do something similar to what we did above, just tossing a new robot into the fray. But we also need to clear out this rack and trigger a new rack of invaders.

I think there are at least three things to be done:

  1. Clear all existing invaders;
  2. Put in a timed new fleet, much as we do in the code above for CycleStatus.EMPTY;
  3. Put in a timed robot.

We should also make a card to make the robot explode, and for that matter, to make it vulnerable to invader shots, which it currently is not. And on the gripping hand, we should explode the current player or robot, whichever is present. It could be either, because the TOO_LOW code can trigger for either a player or a robot. Anyway those are all for later.

Do we have a method on the InvaderFleet that will clear all the invaders? Or will the new fleet just do that? Ah. It is a completely new invader fleet. We should “just” remove ourselves, the existing fleet. Like this:

    def process_result(self, result, fleets):
        if result == CycleStatus.CONTINUE:
            pass
        elif result == CycleStatus.NEW_CYCLE:
            self.step_origin()
        elif result == CycleStatus.REVERSE:
            self.reverse_travel()
        elif result == CycleStatus.EMPTY:
            fleets.remove(self)
            fleet_capsule = TimeCapsule(2, self.next_fleet())
            fleets.append(fleet_capsule)
        elif result == CycleStatus.TOO_LOW:
            fleets.remove(self)
            fleet_capsule = TimeCapsule(2, self.next_fleet())
            fleets.append(fleet_capsule)
            robot_capsule = TimeCapsule(3, RobotPlayer())
            fleets.append(robot_capsule)

That’s not quite right. Among other issues, it creates a new robot every time the invaders get too low and they pile up on top of each other and all do the same thing at the same time. Let’s roll this back and think. Perhaps we’ll save it for another session.

Reflection

Thinking strategically, it seems to me that what we are missing here is a direct specification (in code or equivalent) of what we want the mix to look like. Normally, we let it evolve over time, and the objects are all programmed to interact in a way that brings a game into being. They’re very independent.

That’s fine, but if we ever get to a bad mix, weird things can clearly happen, like two or three robots on top of each other, engaged in who knows what bizarre mechanical hijinks.

Thinking tactically, I believe it to be very likely that if the TOO_LOW state were to remove whatever robot or player is currently active, and then toss in a new robot, it would all sort out. And that should happen anyway, since we would really like to have an explosion before everything goes black.

As an experiment, we could put the above code back, adding in some code that would root through the mix and remove any player or robot that was already there. Not really our style, but the experiment would quickly confirm whether it was going to work. I would very much prefer to make small “edits” to the mix and have the objects’ generally cooperative behavior make the right things happen. It’s not like the universe gets to rewind and restart itself … as far as we know. Possibly it does that all the time and it’s really only 4 seconds old, it’s just that we all got booted up with full memories and jobs to do. Probably not, but there’d be no way to be sure. But I digress.

I think we’ll wrap this up here. We have better robot behavior, in that it takes up where the player left off, and we have time to view the player explosion before that happens. We have moved forward.

There’s just one thing …

This was all done without writing any new tests, and there are no tests that check what this code does in any way, shape, or form.

That’s not my preferred way of working, but it is a way of working that isn’t entirely without merit, since we do get code that works and does what we want. It’s just slower, more fragile, and makes me feel just a bit dirty, because I know things go better, not just with Jello, but with tests.

I’ll make a card for that, too, and deal with it in an upcoming session.

See you then! And if you are celebrating, have a good one! If you’re not, perhaps give it some consideration. There is probably something worth celebrating.