Python 215 - Shots
Python Asteroids+Invaders on GitHub
We have a start at player shots. Let’s get one on the screen .. and then off the screen.
We have the first player shot test running:
def test_can_fire_initially(self):
fleets = Fleets()
fi = FI(fleets)
player = InvaderPlayer()
fleets.append(player)
player.attempt_firing(fleets)
assert fi.player_shots
This just checks that there’s a shot in the fleets object. The test is supported by this minimal code:
class FleetsInspector:
@property
def player_shots(self):
return self.select_class(PlayerShot)
class InvaderFlyer(InvadersFlyer):
def attempt_firing(self, fleets):
fleets.append(PlayerShot())
Just enough, not a bit more. Let’s add a test.The player can have only one shot on screen at a time. We can express that like this:
def test_cannot_fire_with_one_on_screen(self):
fleets = Fleets()
fi = FI(fleets)
player = InvaderPlayer()
fleets.append(player)
player.attempt_firing(fleets)
assert len(fi.player_shots) == 1
player.attempt_firing(fleets)
assert len(fi.player_shots) == 1
This fails, but it’s not really good as a test. Here’s why.
The player needs to know whether there is a shot already in the mix. Now, it is true that since it has access to the fleets object, it could check directly. But that is not how we do things in this design. We detect things during interactions. So the full sequence of events needs to be like this:
def test_cannot_fire_with_one_on_screen(self):
fleets = Fleets()
fi = FI(fleets)
player = InvaderPlayer()
fleets.append(player)
player.begin_interactions(fleets)
player.attempt_firing(fleets)
assert len(fi.player_shots) == 1
shot = fi.player_shots[0]
player.begin_interactions(fleets)
player.interact_with_playershot(shot, fleets)
player.attempt_firing(fleets)
assert len(fi.player_shots) == 1
In the first attempt, we do not see an interact_with_playershot
and in the second, we do. That is the “signal” that the player has to use. Like this:
class InvaderPlayer(InvadersFlyer):
def __init__(self):
maker = BitmapMaker.instance()
...
self.free_to_fire = True
def begin_interactions(self, fleets):
self.free_to_fire = True
def interact_with_playershot(self, bumper, fleets):
self.free_to_fire = False
def attempt_firing(self, fleets):
if self.free_to_fire:
fleets.append(PlayerShot())
Test is green. Commit: player only fires if no shots on screen.
There is another issue, however. We have no way to fire a missile as the human player. The controls do not yet support that. Here’s the relevant code:
class InvaderPlayer(InvadersFlyer):
def update(self, _delta_time, _fleets):
if not pygame.get_init():
return
keys = pygame.key.get_pressed()
if keys[pygame.K_f]:
self.move(self.step)
elif keys[pygame.K_d]:
self.move(-self.step)
We want to require the firing key, which will be K, to be tapped once for each firing. That is, we won’t just fire because it is down, we’ll require it to be up before we fire again.
We need another flag.
def update(self, _delta_time, fleets):
if not pygame.get_init():
return
keys = pygame.key.get_pressed()
if keys[pygame.K_f]:
self.move(self.step)
elif keys[pygame.K_d]:
self.move(-self.step)
self.check_trigger(keys, fleets)
def check_trigger(self, keys, fleets):
if keys[pygame.K_k]:
if self.trigger_released:
self.trigger_released = False
self.attempt_firing(fleets)
else:
self.trigger_released = True
Having written that, I think I can test it. Before I wrote it, I didn’t see how. Now I can at least try. I’m still having trouble writing the test. Let me first express the logic in code. Maybe that will suggest a test:
def update(self, _delta_time, fleets):
if not pygame.get_init():
return
keys = pygame.key.get_pressed()
...
if not keys[pygame.K_k]:
self.trigger_released = True
else:
self.attempt_firing(fleets)
def attempt_firing(self, fleets):
if self.trigger_released and self.free_to_fire:
fleets.append(PlayerShot())
self.trigger_released = False
Ignore that code for a moment. My thoughts are clearing.
Here’s a cut at a test for trigger logic:
def test_trigger_logic(self):
player = InvaderPlayer()
assert player.fire_request_allowed
player.trigger_pulled()
assert not player.fire_request_allowed
player.trigger_released()
assert player.fire_request_allowed
Player starts with firing allowed, when the trigger is pulled, firing is no longer allowed, when it is released, it is allowed again.
Let’s make that work.
def trigger_pulled(self):
self.fire_request_allowed = False
def trigger_released(self):
self.fire_request_allowed = True
Trivial, of course. Now let’s wire the keys to it, while it’s on my mind:
def update(self, _delta_time, fleets):
if not pygame.get_init():
return
keys = pygame.key.get_pressed()
if keys[pygame.K_f]:
self.move(self.step)
elif keys[pygame.K_d]:
self.move(-self.step)
if not keys[pygame.K_k]:
self.trigger_pulled()
else:
self.trigger_released()
Now we need one more thing. If the trigger is pulled when fire request is allowed, we should attempt to fire. (We might still not succeed, if there is a shot in flight).
Let me refactor a bit before I show you this.
def update(self, _delta_time, fleets):
if not pygame.get_init():
return
keys = pygame.key.get_pressed()
self.check_motion(keys)
self.check_firing(fleets, keys)
def check_firing(self, fleets, keys):
if not keys[pygame.K_k]:
self.trigger_pulled(fleets)
def trigger_pulled(self, fleets):
if self.fire_request_allowed:
self.attempt_firing(fleets)
self.fire_request_allowed = False
def trigger_released(self):
self.fire_request_allowed = True
def attempt_firing(self, fleets):
if self.free_to_fire:
fleets.append(PlayerShot())
Notice that I need fleets
in the trigger_pulled
. Had to put that in the test also.
I think I like these tests now.
def test_can_fire_initially(self):
fleets = Fleets()
fi = FI(fleets)
player = InvaderPlayer()
fleets.append(player)
player.attempt_firing(fleets)
assert fi.player_shots
def test_cannot_fire_with_one_on_screen(self):
fleets = Fleets()
fi = FI(fleets)
player = InvaderPlayer()
fleets.append(player)
player.begin_interactions(fleets)
player.attempt_firing(fleets)
assert len(fi.player_shots) == 1
shot = fi.player_shots[0]
player.begin_interactions(fleets)
player.interact_with_playershot(shot, fleets)
player.attempt_firing(fleets)
assert len(fi.player_shots) == 1
def test_trigger_logic(self):
fleets = Fleets()
player = InvaderPlayer()
assert player.fire_request_allowed
player.trigger_pulled(fleets)
assert not player.fire_request_allowed
player.trigger_released()
assert player.fire_request_allowed
There is no test that checks that if fire request is not allowed, no missile is emitted. The other tests all take advantage of the fact that it’s allowed unless we turn it off.
I think we can write a suitable test:
def test_firing_with_trigger(self):
fleets = Fleets()
fi = FI(fleets)
player = InvaderPlayer()
player.trigger_pulled(fleets)
assert fi.player_shots
fleets.clear()
player.trigger_pulled(fleets)
assert not fi.player_shots
player.trigger_released()
player.trigger_pulled(fleets)
assert fi.player_shots
I am pleased. Reasonably clear tests and pretty much all the conditional logic is checked. I do wonder about a better way, but this is decent. Commit: test and implement trigger logic in player.
Now I’d really like to get a shot on the screen and fly it.
Let’s begin by rezzing it and making it visible. We’ll need a starting location when we create it, and I just noticed that I haven’t put the shot into the bitmaps yet. We’ll deal with that in a moment.First I want a location and a draw of any kind.
class PlayerShot(InvadersFlyer):
def __init__(self, position=u.CENTER):
self.position = MovableLocation(position, Vector2(0, 0))
maker = BitmapMaker.instance()
def draw(self, screen):
center = self.position.position
pygame.draw.circle(screen, "red", center, 20)
I am somewhat surprised to find a red circle at the center of the screen as soon as I start the game:
Who fired the first shot?1 This code is backward:
def check_firing(self, fleets, keys):
if not keys[pygame.K_k]:
self.trigger_pulled(fleets)
else:
self.trigger_released()
Should be:
def check_firing(self, fleets, keys):
if keys[pygame.K_k]:
self.trigger_pulled(fleets)
else:
self.trigger_released()
My excellent tests do not use check_firing as well as they might, because I didn’t want to set up the keys. Might should do that.
Now when I type K, the dot appears. Let’s put it where it belongs.
def attempt_firing(self, fleets):
if self.free_to_fire:
fleets.append(PlayerShot(self.rect.center))
That should put the dot right on top of the player. It does:
Now the shot should rez a bit above its input position and it should have a velocity upward. A little fiddling, and I get the following code. I decided not to bother with the MovableLocation stuff. We’ll think about that later, perhaps.
class PlayerShot(InvadersFlyer):
def __init__(self, position=u.CENTER):
offset = Vector2(0, -8*4)
self.position = position + offset
self.velocity = Vector2(0, -4*4)
maker = BitmapMaker.instance()
def draw(self, screen):
center = self.position
rect = pygame.Rect(0, 0, 4, 32)
rect.center = center
pygame.draw.rect(screen, "white", rect)
That looks just about right:
I’ve been at this nearly two hours. Let’s commit and sum up. Commit: player appears to fire shot. not using real bitmap, no collisions. just visual effect.
Summary
Decent tests
I created some pretty decent tests for the keyboard firing logic, but they’re still not as robust as they might be, because while I do check the interlocking logic, I do not drive the tests from actual key inputs. And that bit me because I had the key inputs upside down.
I’ve made a note to test that. It needs a bit more thought than I have available right now.
Logic seemed tricky
I didn’t see at first how to set up the firing logic to be readily testable. A mockist-style tester might have been better off, as they’ll usually test to see that the right calls are made. I am more inclined to test state, so when I finally got the states separated out as a couple of flags, testing got easier. I think the tests are better than my usual ones for dealing with input events. That’s not to say that they’re great, but better is better.
Questionable decision
I elected not to use the MovableLocation object that is used in Asteroids. We should look into that, but if we manage our own positions, that will certainly work. I certainly have mixed feelings about it, but simpler seemed, well, simpler.
Looking good
The current visual effect of firing is a step in the right direction. We create a real PlayerShot instance and fly it off the screen. There are magic numbers that need to be less magical and more accurate. We need to bring in the shot bitmap and use it. We need the shot explosion at the top of the screen.
A decent outcome
All that will come. We now have a nicely improved demo and all the real caveats are “not done yet”, not “done incorrectly”. The code seems to me to be incomplete but structured much as we would want it to be.
I felt a little ragged today, but I think we have a reasonably solid situation. Improvements are possible and probably needed, but I think everything is in just about the right place.
See you next time!
-
Han clearly fired first. Retconning it isn’t fair. ↩