Python 99 - Special? How?
What additional special objects do we need, and how might they work?
The Need
We’re trying to provide everything an Asteroids game needs using objects in the mix, such that there is no logic in the Game object or other higher level objects relating specifically to the game of Asteroids. Here are some of the needs we still have:
- Inserting a Quarter
- When you type the “q” key, a new game should start. To do that, we need to clear out the existing objects in the mix and create whatever objects are needed to start the game running, such as a WaveMaker, ShipMaker, etc.
-
It seems we can do this from any of the calls that an object gets. Perhaps we’ll have a CoinSlot object or something.
- Spawning New Ships
- When a ship is destroyed, another one is provided a few seconds later, unless you have used up all available ships, in which case you get the GAME OVER screen.
-
This can surely be done similarly to how we just did new asteroid waves.
- Spawning Saucers
- A new saucer runs every N seconds after the previous one has been seen to disappear. Probably we’ll have a SaucerMaker like the WaveMaker and ShipMaker.
- Firing Missiles
- The Ship can fire up to four missiles at a time. How will it know how many are available? Missiles time out, so a slot might come up soon, or later. The Saucer can fire up to two missiles at a time. Same issue as the ship, knowing how many are available.
-
We can probably create two identifiable types of missiles and count them during the interaction cycle. In fact, even now they contain different scoring tables, so they can be told apart and we can certainly make that easier.
- Explosions
- When the ship is destroyed, it spawns an explosion. I see no reason why that can’t be done right in the collision logic: it is now, more or less.
- Hyperspace
- The ship can explode going into hyperspace. This may not require anything much at all, other than spawning an explosion.
Special Fleets
What do the special fleets do now?
- ExplosionFleet
- The fleet knows how to create an explosion by adding Fragments to the mix. There’s no reason why that can’t be done by an explosion creation object that is passed the Fleets instance to send Fragments to.
-
One possibility is for the exploding object to drop an Explosion into the mix, and for the Explosion to create the Fragments and remove itself.
- MissileFleet
- This fleet knows how many missiles it contains and receives a “fire” message, calling back when there is room to add another missile.
-
I suspect something simpler might fall out of just having the ship and saucer count live missiles during interactions.
- SaucerFleet
- This fleet manages creating saucers every N seconds. Probably this logic just moves to a SaucerMaker.
- ShipFleet
- Contains the logic for spawning a new ship, including checking for a safe opportunity to spawn. Probably this logic just moves to a ShipMaker.
An Approach
What we did this morning with the WaveMaker was to test-drive a suitable object, and then remove the logic from the AsteroidFleet, and ultimately remove that whole class, once the object was doing the job.
I think the ShipMaker and SaucerMaker will be quite similar to WaveMaker. I expect no big issues there.
Missile firing will be interesting but seems straightforward. We could test-drive the missile-counting into Ship and Saucer.
An Explosion object seems kind of fun. We might test-drive that.
Yes, that might be just a perfect afternoon-sized effort. Here goes.
I bang out this test:
class TestExplosion:
def test_explosion(self):
fleets = Fleets()
explosion = Explosion(u.CENTER)
fleets.add_flyer(explosion)
explosion.tick(0.1, [], fleets)
mix = fleets.all_objects
for o in mix:
print(o, o is Fragment)
assert explosion not in mix
fragments = [f for f in mix if isinstance(f, Fragment)]
assert fragments
Then this starting class:
class Explosion(Flyer):
def __init__(self, position):
self.position = position
def interact_with(self, other, fleets):
pass
def draw(self, screen):
pass
def tick(self, delta_time, fleet, fleets):
fleets.remove_flyer(self)
fleets.add_flyer(Fragment(self.position))
This object has a very short lifetime, but that’s fine. Let’s see how we really create an explosion, cribbing from ExplosionFleet:
class ExplosionFleet:
def explosion_at(self, position):
simple = Fragment.simple_fragment
vee = Fragment.v_fragment
guy = Fragment.astronaut_fragment
fragment_factory_methods = [vee, guy, simple, simple, simple, simple, simple]
random.shuffle(fragment_factory_methods)
how_many = len(fragment_factory_methods)
for i in range(how_many):
factory_method = fragment_factory_methods[i]
base_direction = 360 * i / how_many
self.make_fragment(factory_method, position, base_direction)
def make_fragment(self, factory_method, position, base_direction):
twiddle = random.randrange(-20, 20)
fragment = factory_method(position=position, angle=base_direction+twiddle)
self.flyers.append(fragment)
I added the new methods add_flyer
and remove_flyer
to Fleets:
class Fleets:
def add_flyer(self, flyer):
self.others.append(flyer)
def remove_flyer(self, flyer):
self.others.remove(flyer)
I’ll copy that logic into my new Explosion object. With just a bit of hammering:
class Explosion(Flyer):
def __init__(self, position):
self.position = position
def interact_with(self, other, fleets):
pass
def draw(self, screen):
pass
def tick(self, delta_time, fleet, fleets):
fleets.remove_flyer(self)
self.explosion_at(self.position, fleets)
def explosion_at(self, position, fleets):
simple = Fragment.simple_fragment
vee = Fragment.v_fragment
guy = Fragment.astronaut_fragment
fragment_factory_methods = [vee, guy, simple, simple, simple, simple, simple]
random.shuffle(fragment_factory_methods)
how_many = len(fragment_factory_methods)
for i in range(how_many):
factory_method = fragment_factory_methods[i]
base_direction = 360 * i / how_many
self.make_fragment(factory_method, base_direction, fleets)
def make_fragment(self, factory_method, base_direction, fleets):
twiddle = random.randrange(-20, 20)
fragment = factory_method(position=self.position, angle=base_direction+twiddle)
fleets.add_flyer(fragment)
I extended the test to check len(fragments) == 7
. It’s passing.
I’ll commit this: Explosion object passing test.
I think I should be able to change ship to issue an explosion, and stop using the explosion fleet for anything.
class Ship(Flyer):
def explode(self, fleets):
player.play("bang_large", self._location)
fleets.remove_ship(self)
fleets.explosion_at(self.position)
That becomes:
def explode(self, fleets):
player.play("bang_large", self._location)
fleets.remove_ship(self)
fleets.add_flyer(Explosion(self.position))
Now I can actually remove the whole ExplosionFleet class and just use Fleet, at least for now. That works perfectly. Commit: Explosion class now used in ship.
Summary
We have removed a class, ExplosionFleet, and have a new object, Explosion, which is created by the ship when it wants to explode. The Explosion exists for only one tick. Its sole purpose is to create explosion Fragments and add them to the mix.
The Explosion object is little or no more complicated than the ExplosionFleet was, and it moves all the knowledge of creating explosions down into the flyer objects, which is our continuing design change.
Again, in a short time, we have made a significant design change without breaking the system. And we even have a decent little test for the object.
Everything went smoothly. I take this as a sign that the existing design is pretty decent, and that the scheme of moving all the Asteroids-related object into Flyer subclass instances will work well.
A pleasant afternoon jaunt. See you next time!