Python 144- Bang
Let’s begin by getting rid of the Explosion Flyer, at least the Flyer part of it.
The Explosion object is a Flyer that is created, tossed into the mix, which then immediately removes itself on its first cycle, adding in Fragment Flyers instead. This isn’t particularly wasteful, and I rather like it, but it adds complexity to the mix that just makes it that little bit harder to understand. So we’ll remove it.
A few days back, we removed the Coin object, which did a similar thing, replacing itself with the objects needed to start the game in play mode or attract mode.
We have some Coin tests written for that occasion. They’re all similar so here’s one to look at:
def test_quarter(self):
fleets = Fleets()
fi = FI(fleets)
Coin.quarter(fleets)
assert fi.saucermakers
assert fi.scorekeepers
assert fi.thumpers
assert fi.wavemakers
assert fi.shipmakers
Explosion currently looks like this:
class Explosion(Flyer):
@classmethod
def from_ship(cls,position):
simple = Fragment.simple_fragment
vee = Fragment.v_fragment
guy = Fragment.astronaut_fragment
return cls(position, [vee, guy, simple, simple, simple, simple, simple])
def __init__(self, position, fragment_factory_methods):
self.position = position
self.fragment_factory_methods = fragment_factory_methods
def tick(self, delta_time, fleets):
fleets.remove(self)
self.explosion_at(self.position, fleets)
def explosion_at(self, _position, fleets):
random.shuffle(self.fragment_factory_methods)
how_many = len(self.fragment_factory_methods)
for i in range(how_many):
factory_method = self.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.append(fragment)
We’d best look at some of Fragment to get the drift of it as well:
class Fragment(Flyer):
@classmethod
def v_fragment(cls, position, angle=None, speed_mul=None):
line = "line"
side_1 = [line, Vector2(-7, 5), Vector2(7, 0)]
side_2 = [line, Vector2(7, 0), Vector2(-7, -5)]
return cls(position, angle, speed_mul, [side_1, side_2])
def __init__(self, position, angle=None, speed_mul=None, fragments=None):
angle = angle if angle is not None else random.randrange(360)
self.position = position
speed_mul = speed_mul if speed_mul is not None else random.uniform(0.25, 0.5)
self.velocity = speed_mul*Vector2(u.FRAGMENT_SPEED, 0).rotate(angle)
self.theta = random.randrange(0, 360)
self.delta_theta = random.uniform(180, 360)*random.choice((1, -1))
self.timer = Timer(u.FRAGMENT_LIFETIME)
self.fragments = fragments
@classmethod
def astronaut_fragment(cls, position, angle=None, speed_mul=None):
line = "line"
head = ["head", Vector2(0, 24), 8, 2]
body_bottom = Vector2(0, 2)
body = [line, Vector2(0, 16), body_bottom]
left_leg = [line, Vector2(-5, -16), body_bottom]
right_leg = [line, Vector2(5, -16), body_bottom]
arm = [line, Vector2(-9, 10), Vector2(9, 10)]
return cls(position, angle, speed_mul, [head, body, arm, left_leg, right_leg])
If we were to drill into Fragment further, we’d see that it has a tiny little “language” that draws a line or a circle, and that a given Fragment is a collection of those commands that draws the desired shape.
When it ticks, the Explosion iterates the list of factory methods, creating however many Fragment instances are needed, and dumps them into fleets.
We want to do that without putting Explosion into fleets first. Let’s do a test:
def test_saucer_explosion(self):
fleets = Fleets()
fi = FI(fleets)
Explosion.from_saucer(fleets)
assert len(fi.fragments) == 7
Test fails, as intended.
Expected :7
Actual :0
Not the right failure. Test isn’t quite right: the class methods expect a position.
def test_saucer_explosion(self):
fleets = Fleets()
fi = FI(fleets)
pos = Vector2(100, 100)
Explosion.from_saucer(pos, fleets)
assert len(fi.fragments) == 7
Better, now it can’t make the call. We extend explosion:
@classmethod
def from_saucer(cls,position, fleets=None):
simple = Fragment.simple_fragment
vee = Fragment.v_fragment
explosion = cls(position, [vee, vee, simple, vee, simple, vee, simple])
if fleets:
explosion.explosion_at(position, fleets)
explosion.fragment_factory_methods = []
return explosion
I did something a bit tricky here. If fleets is provided, we immediately call the explosion_at
method, which will immediately emit the explosion into fleets. Then we empty the Explosion. That ensures that if fleets is provided but the Explosion is also dumped into fleets, it will not produce a second explosion.
We’ll be working to make sure that doesn’t happen but for now we should be safer.
And the test is green. That makes me want to look for from_saucer
senders and hook them into fleets.
class Saucer(Flyer):
def explode(self, fleets):
player.play("bang_large", self._location)
player.play("bang_small", self._location)
fleets.remove(self)
fleets.append(Explosion.from_saucer(self.position))
We change that:
def explode(self, fleets):
player.play("bang_large", self._location)
player.play("bang_small", self._location)
fleets.remove(self)
Explosion.from_saucer(self.position, fleets)
In game play we should get the explosion just as before, through the new mechanism.
It takes a while for the saucer to make a mistake but it does explode properly.
Let’s commit: Saucer Explosion is not added to mix, just adds fragments directly.
Now a test for the from_ship
, similar to the other.
def test_ship_explosion(self):
fleets = Fleets()
fi = FI(fleets)
pos = Vector2(100, 100)
Explosion.from_ship(pos, fleets)
assert len(fi.fragments) == 7
As before, from_ship
needs to deal with fleets.
@classmethod
def from_ship(cls,position, fleets=None):
simple = Fragment.simple_fragment
vee = Fragment.v_fragment
guy = Fragment.astronaut_fragment
explosion = cls(position, [vee, guy, simple, simple, simple, simple, simple])
if fleets:
explosion.explosion_at(position, fleets)
explosion.fragment_factory_methods = []
return explosion
Test runs. Find sender and fix it up.
class Ship(Flyer):
def explode(self, fleets):
player.play("bang_large", self._location)
fleets.remove(self)
Explosion.from_ship(self.position, fleets)
Again we check in Game. Works a treat. Commit: Ship Explosion is not added to mix, just adds fragments directly.
There are a couple of test senders of from_ship
and from_saucer
, I think.
This one is now redundant and wrong. Remove the file entirely.
class TestExplosion:
def test_explosion(self):
fleets = Fleets()
explosion = Explosion.from_ship(u.CENTER)
fleets.append(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 len(fragments) == 7
Removing it breaks a couple of other tests somehow. Nooo … I broke two tests and didn’t see it. I’ve committed two broken tests. Arrgh. I need some way to stop commits when I’m red.
This is in TestHyperspaceGenerator:
def test_failure(self):
fleets = Fleets()
impossible = Vector2(-5, -9)
ship = Ship(impossible)
fi = FI(fleets)
fleets.append(ship)
hg = HyperspaceGenerator(ship)
hg.recharge()
hg.press_button(0, fleets, 45) # fail = roll > 44 + tally
assert fi.explosions
Change that to check for fragments. The other has the same issue, fixed:
def test_saucer_missile_kills_ship(self):
pos = Vector2(100, 100)
ship = Ship(pos)
missile = Missile.from_saucer(pos, Vector2(0, 0))
fleets = Fleets()
fleets.append(ship)
fleets.append(missile)
fi = FI(fleets)
assert fi.missiles
assert fi.ships
fleets.perform_interactions()
assert not fi.missiles
assert not fi.ships
assert fi.fragments
Commit: fix red tests SORRY ..
Now as I was saying, remove that test_explosion file. Green. Commit: remove useless test.
Now checking for more from_saucer
or from_ship
senders. All gone.
Now explosion need not be a flyer and we can trim it. I remove all this:
def interact_with(self, other, fleets):
other.interact_with_explosion(self, fleets)
def interact_with_asteroid(self, asteroid, fleets):
pass
def interact_with_explosion(self, explosion, fleets):
pass
def interact_with_fragment(self, fragment, fleets):
pass
def interact_with_missile(self, missile, fleets):
pass
def interact_with_saucer(self, saucer, fleets):
pass
def interact_with_ship(self, ship, fleets):
pass
def draw(self, screen):
pass
def tick(self, delta_time, fleets):
fleets.remove(self)
self.explosion_at(self.position, fleets)
Commit: Explosion is no longer a Flyer subclass. Much simplified.
I think we have some duplication and perhaps some other improvements to deal with. Here’s explosion:
class Explosion:
@classmethod
def from_ship(cls,position, fleets=None):
simple = Fragment.simple_fragment
vee = Fragment.v_fragment
guy = Fragment.astronaut_fragment
explosion = cls(position, [vee, guy, simple, simple, simple, simple, simple])
if fleets:
explosion.explosion_at(position, fleets)
explosion.fragment_factory_methods = []
return explosion
@classmethod
def from_saucer(cls,position, fleets=None):
simple = Fragment.simple_fragment
vee = Fragment.v_fragment
explosion = cls(position, [vee, vee, simple, vee, simple, vee, simple])
if fleets:
explosion.explosion_at(position, fleets)
explosion.fragment_factory_methods = []
return explosion
def __init__(self, position, fragment_factory_methods):
self.position = position
self.fragment_factory_methods = fragment_factory_methods
def explosion_at(self, _position, fleets):
random.shuffle(self.fragment_factory_methods)
how_many = len(self.fragment_factory_methods)
for i in range(how_many):
factory_method = self.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.append(fragment)
We can now require the fleets parameter, skip the ifs, eliminate the clearing of explosion, and eliminate the returns:
@classmethod
def from_ship(cls,position, fleets):
simple = Fragment.simple_fragment
vee = Fragment.v_fragment
guy = Fragment.astronaut_fragment
explosion = cls(position, [vee, guy, simple, simple, simple, simple, simple])
explosion.explosion_at(position, fleets)
@classmethod
def from_saucer(cls,position, fleets):
simple = Fragment.simple_fragment
vee = Fragment.v_fragment
explosion = cls(position, [vee, vee, simple, vee, simple, vee, simple])
explosion.explosion_at(position, fleets)
We have the position and should either pass it or use it but not both. Remove the parameter and let the object use it, which it does already:
class Explosion:
@classmethod
def from_ship(cls,position, fleets):
simple = Fragment.simple_fragment
vee = Fragment.v_fragment
guy = Fragment.astronaut_fragment
explosion = cls(position, [vee, guy, simple, simple, simple, simple, simple])
explosion.explosion_at(fleets)
@classmethod
def from_saucer(cls,position, fleets):
simple = Fragment.simple_fragment
vee = Fragment.v_fragment
explosion = cls(position, [vee, vee, simple, vee, simple, vee, simple])
explosion.explosion_at(fleets)
def __init__(self, position, fragment_factory_methods):
self.position = position
self.fragment_factory_methods = fragment_factory_methods
def explosion_at(self, fleets):
random.shuffle(self.fragment_factory_methods)
how_many = len(self.fragment_factory_methods)
for i in range(how_many):
factory_method = self.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.append(fragment)
Let’s rename explosion_at
to … explode
. I’ll spare you the reprint. Commit: rename explosion_at to explode.
I think we’re good. Let’s sum up.
Summary
We have removed upwards of 27 lines from a file that is now only 37 lines long. We have removed an entire class from Flyer.
And what we haven’t done, yet, is remove all the implementations of interact_with_explosion
. I forgot until now.
There are 14 of them, in all the other flyers. Could have avoided that by not making it abstract. Anyway remove them all. Commit: remove abstract method interact_with_explosion and all implementors.
Every one of those implementations was pass
, by the way, a tragic waste of complexity. Modified 14 files, removed 42 more lines of useless code.
Anyway that went well. I’m glad I remembered to remove those methods. We need a way to remove for of them, especially for the objects that don’t really interact much if at all. Need some thinking for that.
A small but noticeable improvement. I am well pleased.
See you next time!