Python 60 - Not Quite Right
Some of what I did last time needs improvement. This is no surprise, but, really, I coulda shoulda woulda done better. I forgive me.
No sooner had I sat down to read than I realized that some of what I did yesterday needs improvement. Of course, even when we are perfect, we could use a little improvement, so it was no surprise. But I think what we’ll find this morning is clearly asking to be better.
I had just had the idea of Fleet subclasses, and built the ShipFleet this way:
class ShipFleet(Fleet):
def __init__(self, flyers):
super().__init__(flyers)
def move(self, delta_time):
for ship in self:
ship.control_motion(delta_time)
ship.move(delta_time, self)
OK, for now at least, our rule is that Fleet.move
sends move
to all the fleet’s flyers. But what’s this about sending control_motion
? We should leave that up to the move
in Ship.
I’ll remove this method from ShipFleet but keep the class, because I foresee the need for it. And I’ll move the control_motion
to Ship.
class Ship:
def move(self, delta_time, _ships):
self.control_motion(delta_time)
position = self.position + self.velocity * delta_time
position.x = position.x % u.SCREEN_SIZE
position.y = position.y % u.SCREEN_SIZE
self.position = position
I’m aware that all these flyer objects tend to have those four position lines, but I’m not going to worry about that just now.
There’s a test that fails:
def test_ship_move(self):
ship = Ship(Vector2(50, 60))
ship.velocity = Vector2(10, 16)
ship.move(0.5, [ship])
assert ship.position == Vector2(55, 68)
The error is pygame not initialized
. Fortunately, there is pygame.get_init()
.
def control_motion(self, delta_time):
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()
I think we’ll allow that. Commit: move control_motion
call to Ship. Remove ShipFleet.move.
So that’s better. What would be better still, I think, would be if all our flyers were to do their work on tick
rather than move
. The main asteroids event is this:
def asteroids_tick(self, delta_time):
self.fleets.tick(delta_time)
self.check_saucer_spawn(self.saucer, self.saucers, 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)
if self.ships: self.check_ship_firing(self.ship)
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()
And:
class Fleets:
def tick(self, delta_time):
result = True
for fleet in self.fleets:
result = result and fleet.tick(delta_time)
self.move_everything(delta_time)
return result
def move_everything(self, delta_time):
for fleet in self.fleets:
fleet.move(delta_time)
class Fleet:
def tick(self, delta_time):
return True
def move(self, delta_time):
for flyer in self:
flyer.move(delta_time, self)
OK, before we go nuts here, let’s think about this. As written, we tick everyone and then move everyone. If we push move down into the individual objects’ tick
method, the first object will do all its tick things and move before the second object gets to do anything.
I think that’s OK, because we’re not doing interactions on tick
, we do the process_collisions
later on. OK.
New rule: all the flyers should move on tick. We’ll remove the move_everything
from Fleets, and move
from Fleet.
class Fleet:
def tick(self, delta_time):
for flyer in self:
flyer.tick(delta_time)
That only breaks one test so far, because FakeFlyer doesn’t know tick.
Also I need to accumulate the T/F in the code above. And … we need to pass the fleet to tick, because move wants it.
class Fleet:
def tick(self, delta_time):
result = True
for flyer in self:
result = result and flyer.tick(delta_time, self)
return result
Now we’re green. Let’s fix up the flyers.
class Asteroid:
def tick(self, delta_time, fleet):
self.move(delta_time, fleet)
return True
class Missile:
def tick(self, delta_time, fleet):
self.move(delta_time, fleet)
return True
class Saucer:
def tick(self, delta_time, fleet):
self.move(delta_time, fleet)
return True
class Ship:
def tick(self, delta_time, fleet):
self.move(delta_time, fleet)
return True
That doesn’t look like much improvement, but I think it is, because now we’re calling a more generic function, tick
, not a specific one, move
. And we could make move
private if we were so inclined, although there are at least a few tests of it.
We’ll see, though. I do not guarantee to be satisfied tomorrow with what I do today.
We’re green and the game should be working. Commit: flyers move on tick
, no direct calls to move
outside tests.
Let’s pause to think:
Cortical-Thalamic Pause
Our more or less fundamental purpose here is to push responsibility down from Game into Fleets, Fleet, or the individual flyers, as far as it will go. Presumably, all the responsibility will be pushed into tick
, or somewhere called by tick
.
class Game:
def asteroids_tick(self, delta_time):
self.fleets.tick(delta_time)
self.check_saucer_spawn(self.saucer, self.saucers, 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)
if self.ships: self.check_ship_firing(self.ship)
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()
Let’s see if we can move ship firing downward.
class Game:
def check_ship_firing(self, ship):
keys = pygame.key.get_pressed()
if keys[pygame.K_k]:
ship.fire_if_possible(self.missiles)
else:
ship.not_firing()
Why can’t we do that on tick? Let’s assume that we can and remove this code and the call from Game.
Now in Ship, presumably on tick, we’ll want to do the thing.
class Ship:
def tick(self, delta_time, fleet):
self.move(delta_time, fleet)
return True
def move(self, delta_time, _ships):
self.control_motion(delta_time)
position = self.position + self.velocity * delta_time
position.x = position.x % u.SCREEN_SIZE
position.y = position.y % u.SCREEN_SIZE
self.position = position
def control_motion(self, delta_time):
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 we move the check on the firing key, “k”, we’ll need to have access to the missiles collection. I have an idea for how we’ll do that.
Let’s assume that our space object flyers have “sensors” that let them find out things about the universe. To give them that access, let’s pass not just the object’s own fleet to it on tick
, but also the Fleets object. We’ll allow certain callbacks to Fleets, to support what the objects need to know.
class Fleets:
def tick(self, delta_time):
result = True
for fleet in self.fleets:
result = result and fleet.tick(delta_time, self)
return result
class Fleet:
def tick(self, delta_time, fleets):
result = True
for flyer in self:
result = result and flyer.tick(delta_time, self, fleets)
return result
I change most of the tick calls to ignore the fleets parm but in Ship:
class Ship:
def tick(self, delta_time, fleet, fleets):
self.move(delta_time, fleet, fleets)
return True
def move(self, delta_time, _ships, fleets):
self.control_motion(delta_time, fleets.missiles)
position = self.position + self.velocity * delta_time
position.x = position.x % u.SCREEN_SIZE
position.y = position.y % u.SCREEN_SIZE
self.position = position
def control_motion(self, delta_time, missiles):
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.not_firing()
Some tests are broken. I was afraid to use Change Signature because of the differing uses of tick
at different levels.
I’ll fix them up.
def test_ship_move(self):
ship = Ship(Vector2(50, 60))
ship.velocity = Vector2(10, 16)
ship.move(0.5, [ship])
assert ship.position == Vector2(55, 68)
Since the ship is checking controls on move, we can’t just pass an empty fleets here. Let’s change Ship:
class Ship:
def tick(self, delta_time, fleet, fleets):
self.control_motion(delta_time, fleets.missiles)
self.move(delta_time, fleet)
return True
def move(self, delta_time, _ships):
position = self.position + self.velocity * delta_time
position.x = position.x % u.SCREEN_SIZE
position.y = position.y % u.SCREEN_SIZE
self.position = position
That makes more sense anyway, I think. Moving is moving, controlling is controlling.
def test_fleets_tick(self):
asteroids = [FakeFlyer()]
missiles = [FakeFlyer()]
saucers = [FakeFlyer()]
saucer_missiles = [FakeFlyer()]
ships = [FakeFlyer()]
fleets = Fleets(asteroids, missiles, saucers, saucer_missiles, ships)
result = fleets.tick(0.1)
assert result
That’s because FakeFlyer doesn’t expect the new parm:
class FakeFlyer:
def tick(self, _delta_time, _fleet, _fleets):
return True
We’re green and good. Commit: The Fleets instance is passed to all space objects’ tick
method. tick(delta_time, fleet, fleets)
.
Let’s sum up.
Summary
Are we better off?
I think we are. The big decision, though we made it casually, was to pass the Fleets instance down to Fleet.tick
and on down to the tick
method of all the flyers. That allows the flyer to manipulate its own fleet, to remove itself if need be, or to add. And the object can assess other fleets, as the Ship does to decide whether it can fire. It can only fire if there are fewer than four missiles in action.
(I’m wondering if we’ll actually make use of the object’s own fleet. We might not, and of course since we get the Fleets now, we could get it if we wanted it. We’ll keep an eye out for that. Offhand, I’m not seeing why we need access to our own fleet on tick
. We will access it during collisions, but that’s separate from tick
.)
We’re moving more slowly than I had anticipated. I thought it would be just two or maybe three sessions until most everything was moved down. It’s clearly taking more, in part because there’s more to say about each step, but in part because each step is itself usually broken into a few smaller steps. That’s a good thing, because larger steps are disproportionately inclined to include mistakes.
Looking at the main asteroids_tick
, I think we’re on a path to have it come down to just a few bits:
- Tick all the Fleets;
- Check game controls (check for quit);
- Process collisions;
- Draw.
Everything else will move down into Fleets, Fleet, or the individual objects. Remaining to be done right now are:
self.check_saucer_spawn(self.saucer, self.saucers, 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)
I expect those will go, respectively, to SaucerFleet, ShipFleet, Saucer, and AsteroidsFleet.
But we’ll see. I bet we can do the move in small steps, moving things first to Fleets and then to a specific Fleet and then, in one case, to Saucer.
I’m interested to find out what I do. I hope you’ll join me!