Python Asteroids on GitHub

Let’s see if we can make hyperspace more dangerous.

Here’s my Kotlin code for hyperspace failure:

``````    // allegedly the original arcade rule
fun hyperspaceFailure(random0thru62: Int, asteroidTally: Int): Boolean
= random0thru62 >= (asteroidTally + 44)
}
``````

As you can see, you roll a random number 0-62, probably for 6502 reasons, and if that is larger than 44 plus the number of asteroids, hyperspace will fail. That will be signified by the ship exploding right where it is.

We’ll need the asteroid count, and what needs to happen in our Python version needs to happen in here:

``````class Ship:
def enter_hyperspace_if_possible(self):
if self._can_enter_hyperspace:
x = random.randrange(u.SCREEN_SIZE)
y = random.randrange(u.SCREEN_SIZE)
a = random.randrange(360)
self.move_to(Vector2(x, y))
dx = random.randrange(u.SHIP_HYPERSPACE_MAX_VELOCITY)
dy = random.randrange(u.SHIP_HYPERSPACE_MAX_VELOCITY)
self.accelerate_to(Vector2(dx, dy))
self._angle = a
self._can_enter_hyperspace = False
``````

That’s called from here:

``````class Ship:
def control_motion(self, delta_time, missiles):
if not pygame.get_init():
return
keys = pygame.key.get_pressed()
...
if keys[pygame.K_SPACE]:
self.enter_hyperspace_if_possible()
else:
self._can_enter_hyperspace = True
``````

I think we need a new function, one that we can test. We can take a cue from the Kotlin one and pass in the random number. But, gee, what is there really to test? x > y? I guess so.

``````    def test_hyperspace_failure(self):
"""hyperspace fails when random(0 thru 62) > asteroid count plus 44"""
ship = Ship(u.CENTER)
self.check_no_fail(ship, 0, 0)
self.check_fail(ship, 45, 0)
self.check_fail(ship, 62, 17)
self.check_no_fail(ship, 62, 18)

def check_no_fail(self, ship, roll, asteroids):
assert not ship.hyperspace_failure(roll, asteroids)

def check_fail(self, ship, roll, asteroids):
assert ship.hyperspace_failure(roll, asteroids)
``````

I think those are right. PyCharm hints to me that ship does not understand `hyperspace_failure`. I knew that.

``````    @staticmethod
def hyperspace_failure(roll, asteroid_count):
return roll > 44 + asteroid_count
``````

The test is green. Commit: hyperspace_failure implemented but not used.

Now to use it:

``````    def tick(self, delta_time, fleet, fleets):
self.control_motion(delta_time, fleets.missiles)
self.move(delta_time, fleet)
``````

That becomes this:

``````    def tick(self, delta_time, fleet, fleets):
self.control_motion(delta_time, fleets.missiles, fleets.asteroid_count)
self.move(delta_time, fleet)
``````

And tracking on down:

``````    def control_motion(self, delta_time, missiles, asteroid_count):
if not pygame.get_init():
return
keys = pygame.key.get_pressed()
if keys[pygame.K_f]:
self.turn_left(delta_time)
if keys[pygame.K_d]:
self.turn_right(delta_time)
if keys[pygame.K_j]:
self.power_on(delta_time)
else:
self.power_off()
if keys[pygame.K_k]:
self.fire_if_possible(missiles)
else:
self._can_fire = True
if keys[pygame.K_SPACE]:
self.enter_hyperspace_if_possible(asteroid_count)
else:
self._can_enter_hyperspace = True

def enter_hyperspace_if_possible(self, asteroid_count):
if self._can_enter_hyperspace:
x = random.randrange(u.SCREEN_SIZE)
y = random.randrange(u.SCREEN_SIZE)
a = random.randrange(360)
self.move_to(Vector2(x, y))
dx = random.randrange(u.SHIP_HYPERSPACE_MAX_VELOCITY)
dy = random.randrange(u.SHIP_HYPERSPACE_MAX_VELOCITY)
self.accelerate_to(Vector2(dx, dy))
self._angle = a
self._can_enter_hyperspace = False
``````

Let’s clean this up a bit. Also I have to figure out how to explode. How hard can it be? We may need our fleet, though.

``````    def enter_hyperspace_if_possible(self, asteroid_count):
if self._can_enter_hyperspace:
self.hyperspace_transfer()

def hyperspace_transfer(self):
x = random.randrange(u.SCREEN_SIZE)
y = random.randrange(u.SCREEN_SIZE)
a = random.randrange(360)
self.move_to(Vector2(x, y))
dx = random.randrange(u.SHIP_HYPERSPACE_MAX_VELOCITY)
dy = random.randrange(u.SHIP_HYPERSPACE_MAX_VELOCITY)
self.accelerate_to(Vector2(dx, dy))
self._angle = a
self._can_enter_hyperspace = False
``````

Now we change `enter...`:

I’ve broken something and I’m not sure when it started. Ah, it’s just that I need the parameter for asteroid count:

``````    def test_hyperspace(self):
impossible = Vector2(-5, -5)
impossible_angle = 370
ship = Ship(impossible)
ship._angle = impossible_angle
ship.enter_hyperspace_if_possible(99)
position = ship.position
angle = ship._angle
assert position != impossible and angle != impossible_angle
assert ship._location.velocity != Vector2(0,0)
ship.enter_hyperspace_if_possible(99) # cannot fail
assert ship.position == position and ship._angle == angle
``````

99 asteroids is guaranteed safety. Well, hyperspace won’t fail, anyway. Test is green, back to work.

``````    def enter_hyperspace_if_possible(self, asteroid_count):
if not self._can_enter_hyperspace:
return
self._can_enter_hyperspace = False
roll = random.randrange(0, 63)
if self.hyperspace_failure(roll, asteroid_count):
self.explode()
else:
self.hyperspace_transfer()
``````

I don’t know how to explode, but that’s what I want. Here’s how we do it now when we collide:

``````    def destroyed_by(self, attacker, ships, fleets):
if self in ships: ships.remove(self)
fleets.explosion_at(self.position)
``````

So we can do something similar but we need ships and fleets to do it.

In for a penny, here’s `tick` again:

``````    def tick(self, delta_time, fleet, fleets):
self.control_motion(delta_time, fleets.missiles, fleets.asteroid_count)
self.move(delta_time, fleet)
``````

Let’s send fleet and fleets to `control_motion` and let it sort out what people need:

``````    def control_motion(self, delta_time, fleet, fleets):
if not pygame.get_init():
return
keys = pygame.key.get_pressed()
if keys[pygame.K_f]:
self.turn_left(delta_time)
if keys[pygame.K_d]:
self.turn_right(delta_time)
if keys[pygame.K_j]:
self.power_on(delta_time)
else:
self.power_off()
if keys[pygame.K_k]:
self.fire_if_possible(fleets.missiles)
else:
self._can_fire = True
if keys[pygame.K_SPACE]:
self.enter_hyperspace_if_possible(fleet, fleets)
else:
self._can_enter_hyperspace = True
``````

Grrr. I’ve lost the thread a bit. And no commit near by. For the tests, I need to pass the asteroid count directly, so this call must be:

``````        if keys[pygame.K_SPACE]:
self.enter_hyperspace_if_possible(fleet, fleets.asteroid_count, fleets)
else:
self._can_enter_hyperspace = True
``````
``````    def enter_hyperspace_if_possible(self, ships_fleet, asteroid_count, fleets):
if not self._can_enter_hyperspace:
return
self._can_enter_hyperspace = False
roll = random.randrange(0, 63)
if self.hyperspace_failure(roll, asteroid_count):
self.explode(ships_fleet, fleets)
else:
self.hyperspace_transfer()
``````

After modifying the tests to pass in that odd sequence, I’m green. Does this work in game? It seems to. Once in a while the ship explodes where it is rather than go to hyperspace.

Commit: hyperspace can fail, explodes on entry. (not exit.)

## Summary

I needed smaller steps there, though I don’t see quite what I might have done. I had to fiddle the parameter lists too many times, in large part because the tests wanted to roll their own dice, and there are so many different things we need access to in order to cope: the Ship, its Fleet, the asteroid count, the random number, and the Fleets object so that we can explode.

But it came together. I’m not loving the parameter lists, and we’ll take a look at that soon. But we have hyperspace failure on entry and that makes things a bit better.

I think I need to work on sound. That will improve the game, and probably irritate me while I figure out how to do it.

See you next time!