Python 63 - Keep trying ...
Let’s move some more Game responsibilities closer to where they belong. (It goes better this time!)
def asteroids_tick(self, delta_time):
self.fleets.tick(delta_time)
self.check_ship_spawn(self.ship, self.ships, delta_time)
self.check_saucer_firing(delta_time, self. saucers, self.saucer_missiles, self.ships)
self.check_next_wave(delta_time)
self.control_game(self.ship, delta_time)
self.process_collisions()
self.draw_everything()
if self.game_over: self.draw_game_over()
def check_saucer_firing(self, delta_time, saucers, saucer_missiles, ships):
for saucer in saucers:
saucer.fire_if_possible(delta_time, saucer_missiles, ships)
It seems rather clear that the Saucer could be checking for itself, if it had access to saucer_missiles
. And we’re at least part of the way there, passing Fleets to tick
:
class Fleets:
def tick(self, delta_time):
all_true = True
for fleet in self.fleets:
if not fleet.tick(delta_time, self):
all_true = False
return all_true
class SaucerFleet:
def tick(self, delta_time, fleets):
super().tick(delta_time, fleets)
if not self.flyers:
self.timer.tick(delta_time)
return True
class Fleet:
def tick(self, delta_time, fleets):
result = True
for flyer in self:
result = flyer.tick(delta_time, self, fleets) and result
return result
class Saucer:
def tick(self, delta_time, fleet, _fleets):
self.move(delta_time, fleet)
return True
The saucer already has the timer in itself:
class Saucer:
# noinspection PyAttributeOutsideInit
def set_firing_timer(self):
self.missile_timer = Timer(u.SAUCER_MISSILE_DELAY, self.fire_if_missile_available)
def fire_if_missile_available(self, saucer_missiles, ships):
if self.a_missile_is_available(saucer_missiles):
self.fire_a_missile(saucer_missiles, ships)
return True
else:
return False
@staticmethod
def a_missile_is_available(saucer_missiles):
return len(saucer_missiles) < u.SAUCER_MISSILE_LIMIT
def fire_a_missile(self, saucer_missiles, ships):
saucer_missiles.append(self.create_missile(ships))
Seems like we need to provide both the saucer_missiles
and ships
collections (not even the fleets?). I wouldn’t expect len
to work on the saucer_missiles fleet. I’d best do a test.
Right. Turns out that Fleets.saucer_missiles is this:
@property
def saucer_missiles(self):
return self.fleets[3].flyers
That’s not really on. We shouldn’t be exposing those inner collections.
Let me change that one to just return the fleet and see what breaks. My new test fails:
def test_len(self):
saucer_missiles = []
fleets = Fleets([], [], [], saucer_missiles, [])
s_m_fleet = fleets.saucer_missiles
assert len(s_m_fleet) == 0
Fleet does not understand len
but it could. Well, actually, it couldn’t. You can’t just implement len
on an arbitrary class. There must be a trick to it. Yes, you have to implement __len__
OK:
def __len__(self):
return len(self.flyers)
Right, test passes. I’ll change the other accessors. I’m just trying to get everyone accessing a Fleet instead of the flyers collection.
One test needed to drill deeper to work but it works.
def test_access(self):
asteroids = ["asteroid"]
missiles = ["missile"]
saucers = ["saucer"]
saucer_missiles = ["saucer_missile"]
ships = ["ship"]
fleets = Fleets(asteroids, missiles, saucers, saucer_missiles, ships)
assert fleets.asteroids.flyers == asteroids
assert fleets.missiles.flyers == missiles
assert fleets.saucers.flyers == saucers
assert fleets.saucer_missiles.flyers == saucer_missiles
assert fleets.ships.flyers == ships
Let’s try the game, surely that can’t work. Right: Fleet has no clear
. Build it:
class Fleet:
def clear(self):
self.flyers.clear()
Fleet does not know extend
. I’ll see if this works:
class Fleet:
def extend(self, *args):
self.flyers.extend(*args)
And:
targeting_angle = self.angle_to(ships[0])
~~~~~^^^
TypeError: 'ShipFleet' object is not subscriptable
Hm. When you go to make a collection you run into stuff. This one calls for this:
class Fleet:
def __getitem__(self, item):
return self.flyers[item]
It seems that we are green and the game works. Commit: Fleets returns Fleet instance, not flyer collection via properties. Fleet instances understand subscripting, extend, and len.
Reflection
This is substantially less interesting than I had hoped for, but the tests and running the game have shown me what’s needed. I think I’ll add to the tests. If I had known I needed those things, I’d have done it already.
def test_len_etc(self):
saucer_missiles = []
fleets = Fleets([], [], [], saucer_missiles, [])
s_m_fleet = fleets.saucer_missiles
assert len(s_m_fleet) == 0
s_m_fleet.extend([1, 20, 300])
assert len(s_m_fleet) == 3
assert s_m_fleet[1] == 20
This was worth doing, because I learned that extend
takes one list, not variable args, so:
def extend(self, list):
self.flyers.extend(list)
Green. Commit: added tests for Fleet.extend and subscripting.
Reflection Continued
Still not very interesting but sometimes you just have to prepare for what you really want to do. Let’s review again how saucer firing works now, since our buffers got flushed with all that collection rigmarole.
def check_saucer_firing(self, delta_time, saucers, saucer_missiles, ships):
for saucer in saucers:
saucer.fire_if_possible(delta_time, saucer_missiles, ships)
Let’s see if we can change this one to pass
and do the job in SaucerFleet.
class SaucerFleet:
def tick(self, delta_time, fleets):
super().tick(delta_time, fleets)
saucer_missiles = fleets.saucer_missiles
ships = fleets.ships
for saucer in self:
saucer.fire_if_possible(delta_time, saucer_missiles, ships)
if not self.flyers:
self.timer.tick(delta_time)
return True
That works in game, with three tests failing. I’ll remove the check and its call from Game. Now let’s make those tests good.
The first one is just that FakeFlyer doesn’t understand fire_if_possible
. Fix that:
class FakeFlyer:
def fire_if_possible(self, _delta_time, _saucer_missiles, _ships):
pass
Next:
def test_saucer_spawn(self):
saucers = []
saucer_fleet = SaucerFleet(saucers)
saucer_fleet.tick(0.1, None)
assert not saucers
saucer_fleet.tick(u.SAUCER_EMERGENCE_TIME, None)
assert saucers
Now the code is looking for saucer_missiles
in None. Will also be looking for ships
, no doubt. I need to pass in a Fleets, I think.
def test_saucer_spawn(self):
saucers = []
fleets = Fleets([], [], saucers, [], [])
saucer_fleet = fleets.saucers
saucer_fleet.tick(0.1, fleets)
assert not saucers
saucer_fleet.tick(u.SAUCER_EMERGENCE_TIME, fleets)
assert saucers
That runs.
The final one is another test for saucer spawning. I must have created one not realizing I already had one. Remove the second one.
We are green. Double-check the game. Game is solid. Commit: revise tests verifying move of saucer firing logic.
Are we there yet?
No, we are not. We should move the logic from here:
class SaucerFleet:
def tick(self, delta_time, fleets):
super().tick(delta_time, fleets)
saucer_missiles = fleets.saucer_missiles
ships = fleets.ships
for saucer in self:
saucer.fire_if_possible(delta_time, saucer_missiles, ships)
if not self.flyers:
self.timer.tick(delta_time)
return True
It should go down into Saucer.tick
, so we know that that method wants saucer_missiles
and ships
(or else fleets
). I think all the tick
methods expect fleets
, so our code should just work, if we move this logic to Saucer.tick
:
class Saucer:
def tick(self, delta_time, fleet, fleets):
saucer_missiles = fleets.saucer_missiles
ships = fleets.ships
self.fire_if_possible(delta_time, saucer_missiles, ships)
self.move(delta_time, fleet)
return True
class SaucerFleet:
def tick(self, delta_time, fleets):
super().tick(delta_time, fleets)
if not self.flyers:
self.timer.tick(delta_time)
return True
Tests remain green, game continues to work (and the tests should assure me of that but I check anyway). Commit: move saucer firing logic to Saucer.tick.
Summary
I think that went a bit more nicely. It was more than a little boring changing Fleet to be more like a collection, but it was easy to do and worth having done it at least once. After that I just had to turn off the check in Game and move the logic to SaucerFleet, and after the tests were aligned with that, move the logic again to Saucer.
Going in two steps allowed me to be sure that it was going to work before committing to the three-rail shot that it now is, Game to Fleets to Fleet to Saucer.
I feel somewhat redeemed. Let’s print this before I break something.
See you next time!