Python 213 - Display Something
Python Asteroids+Invaders on GitHub
It’s time to make the game look a bit more like a game. I’d like to think briefly about what to do and how to do it.
Among the things we need to display are: the player’s gun turret, the player’s shot, the invaders’ shots, the shields, and the saucer. There is also the matter of the score, and there’s a line at the bottom of the game, under which it displays how many turrets you have left.
Now, some uninformed person on the thing formerly known as Twitter was saying that TDD is about making it up as you go along. I’m wondering what the alternatives are. Possibly “make it up before you do it and then treat anything you learn as a mistake in planning”?
Back when Agile actually meant something …
My own practice is a form of incremental development, informed by the ideas and practices I learned back when Agile Software Development actually meant something about doing software development in a fashion that provided faster feedback and more flexibility than the approaches that many folks were taking back then. The term “Agile” has now been taken over, misunderstood, co-opted, and bent to mean almost anything but what we had in mind back then, but the knowledge persists and remains useful to those who will seek it out.
So I think all the time. I think when I start a project, I think while going to sleep or waking up, I think just before I start some task, I think right in the middle of it, and I even think when the task is done, to see how I might do it better now or later.
But yes, I’m making it up as I go along, because making it up in the distant past is wasteful and often wrong. So just now, let’s imagine how we might do some of the display things.
- Turret
- The player’s token on the screen is a gun turret thing that can move back and forth and fire shots up at the aliens. I figure I’ll move it using the same D and F keys that turn the ship in Asteroids, and fire shots using the K key, which fires shots in Asteroids. I can imagine that I might also associate firing with the J key and the space bar, just because the Asteroids player is used to tapping those as well. Or maybe just K. I haven’t decided.
-
Once we have the Y position of the thing set, we’ll just move it left and right, and we’ll use the D and F keys such that while they are down, the turret moves in that direction at some set speed. We’ll see whether we can get an estimate of that speed from the original game, and then we’ll tune it.
-
The turret will have some limits in the x direction. We probably don’t need to do anything clever about that, as we did with the invaders. We can just build the limits into the Turret / Player object.
- Player Shot
-
The player shot is a two-pixel blob that goes at some speed, straight up, until it either goes above the invaders, having missed them all, or until it hits an invader. When either of those happens it makes a small explosion graphic.
-
Collision, I think, is supposed to be done by actual contact between the visible bits of the shot and the visible bits of the invader. Just intersecting the rectangles will not do, because invaders’ rectangles are wider than the invaders, and the top invader is notably more narrow. So we’ll have to see how to intersect bitmaps. I think pygame has support for that. If not, we’ll figure it out.
- Invader Shot
-
Invader shots come in three styles, squiggle, plunger, and roller, and while two of those drop randomly, one of them is always dropped directly above the player. I’ll have to look up which one that is.
-
In the original game, the dropping of the regular shots followed a pattern determining which column they came from. I don’t know if we’ll emulate that or not. We might hold back on it and do it later. I suppose a truly adept player would always know how to stay out of the way of the regular shots.
- Shields
-
There are four shields above the player. These have the interesting characteristic that when shots hit them, they are damaged. Pieces of them get removed, and as subsequent shots hit them, deeper damage appears. It really looks as if the shot’s shape is affecting the damage done, which looks pretty ragged. It’s a very nice effect.
-
I am not sure how we’ll do this, but we’ll probably be glad to have bitmap collision detection.
-
An interesting aspect of the shields is that in a two-player game, the players get their own shields, so that the damaged versions get saved for player one when player two is up, and then put back when it’s player one’s turn again.
- Saucer
-
A saucer flies across every now and then and if you hit it with a shot you get a mystery score. We’ll surely save this for last, and it will take some research to see what the original did and how much we want to emulate it.
Today’s Plan
I am torn between doing shields first or player first. An initial shields implementation would be easy, we’d just plunk them down somewhere. The player would be more interesting, we’d have to make it move.
Of course, we could make the player immobile at first and probably would.
Let’s do player, if we get the motion working it’ll be more thrilling for the customers to see the progress.
I haven’t been able to work out the y coordinate of the player turret. We’ll just pick one and adjust for best appearance, unless I can figure out the assembly code. The picture below, from the ancient scrolls, makes me think the player is about 5 player-heights above the bottom of the screen. We’ll try that.
How will this work?
In the fullness of time, we’ll have some kind of PlayerMaker that counts free players and rezzes a new player when there is no player on the screen. For now, I think we’ll just create a Player object that knows how to draw itself, and we’ll put one in the coin for invaders. Let’s do the absolute least work we can reasonably do to get it on screen.
A very small amount of programming, plus help from PyCharm, and I have this:
coin.py
def invaders(fleets):
fleets.clear()
fleets.append(Bumper(64, -1))
fleets.append(Bumper(960, +1))
fleets.append(InvaderFleet())
fleets.append(InvaderPlayer())
class InvaderPlayer(Flyer):
class InvaderPlayer(Flyer):
def __init__(self):
maker = BitmapMaker.instance()
self.players = maker.players # one turret, two explosions
self.player = self.players[0]
self.rect = pygame.Rect(0, 0, 64, 32)
self.rect.center = (u.SCREEN_SIZE/2, u.SCREEN_SIZE - 5*32 - 16)
def draw(self, screen):
screen.blit(self.player, self.rect)
I allowed PyCharm to create all the various interact_with methods, tick, and so on. And a run-time error told me that I also need interact_with_bumper
, which I implemented as pass
.
I’ve thought about this before. I really need for the invaders objects and the asteroids objects to inherit from different superclasses (both subclasses of Flyer, though), so that they can have different interaction methods. This idea just got another priority bump, since I needed to be reminded about bumpers.
And one of my special object-checking tests is also failing. This one:
def all_known_flyer_subclasses():
return {
Asteroid, BeginChecker, Bumper, EndChecker,
Fragment, GameOver, InvaderFleet, Missile, Saucer, SaucerMaker,
Score, ScoreKeeper, Ship, ShipMaker, Signal, Thumper, WaveMaker}
# @pytest.mark.skip("needs updating")
def test_no_unchecked_classes(self):
# if this fails, we need to update `all_known_flyer_subclasses`
# and re-verify the coin tests to be sure they don't need updating
subs = set(Flyer.__subclasses__())
assert not subs - self.all_known_flyer_subclasses()
OK, so I’ll add InvaderPlayer to the list and see what breaks next. Nothing breaks.
Commit: Player appears on screen!
Fantastic. Now let’s make it move. We’ll check to see how the ship does it.
class Ship(Flyer):
def update(self, delta_time, fleets):
self.control_motion(delta_time, fleets)
self._location.move(delta_time)
self.control_firing(fleets)
def control_motion(self, delta_time, fleets):
if not pygame.get_init():
return
keys = pygame.key.get_pressed()
if keys[pygame.K_s]:
...
We can do this. What do we want to have happen?
If F is down, we want to move the ship some amount to the left. If D is down, move it some amount to the right. If both are down or both are up, nothing should happen.
I think I’ll do this in the dumbest possible way, and I plan to berate myself about it during our next reflection.
class InvaderPlayer(Flyer):
def __init__(self):
maker = BitmapMaker.instance()
self.players = maker.players # one turret, two explosions
self.player = self.players[0]
self.rect = pygame.Rect(0, 0, 64, 32)
self.rect.center = Vector2(u.SCREEN_SIZE/2, u.SCREEN_SIZE - 5*32 - 16)
self.step = 4
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)
def move(self, amount):
self.rect.center = self.rect.center + Vector2(amount, 0)
Let me show you how nicely that works:
Commit: player moves back and forth, can go off screen.
I’d kind of like to know where to put the limits. What do the bumpers say? We should be able to move the player clear to the edge, so it can shoot at invaders right up until they turn. The bumpers are at 64, and 960. Let’s limit the player to that range as well. We’ll just code it.
def move(self, amount):
x = self.rect.centerx
x = x + amount
x = min(x, self.right)
x = max(self.left, x)
self.rect.centerx = x
We’ll fix that in a moment, but first I want to try it. It works but it goes too far. I want its edge to get to the bumper, not the center. Adjust by 8 pixels, i.e. 32 of our little pixels.
half_width = 4 * 8
self.left = 64 + half_width
self.right = 960 - half_width
That looks pretty good. Let’s improve the move
by inlining a few times:
def move(self, amount):
self.rect.centerx = max(self.left, min(self.rect.centerx + amount, self.right))
That was all machine refactoring, so it’s good. I should really write a test for that, shouldn’t I?
First commit: change move to stop at edges.
Is player going to need enough tests to warrant its own file? I don’t know. Harmless to do it.
class TestPlayer:
def test_left_edge(self):
player = InvaderPlayer()
player.move(-10000)
assert player.rect.centerx == player.left
def test_right_edge(self):
player = InvaderPlayer()
player.move(10000)
assert player.rect.centerx == player.right
That seems good enough. Commit: tests for player not going over edge.
Reflection
Let’s review the code in player.
class InvaderPlayer(Flyer):
def __init__(self):
maker = BitmapMaker.instance()
self.players = maker.players # one turret, two explosions
self.player = self.players[0]
self.rect = pygame.Rect(0, 0, 64, 32)
self.rect.center = Vector2(u.SCREEN_SIZE/2, u.SCREEN_SIZE - 5*32 - 16)
self.step = 4
half_width = 4 * 8
self.left = 64 + half_width
self.right = 960 - half_width
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)
def move(self, amount):
self.rect.centerx = max(self.left, min(self.rect.centerx + amount, self.right))
def interact_with(self, other, fleets):
pass
def draw(self, screen):
screen.blit(self.player, self.rect)
I’m glad we did this. Let’s put in the interact_with
that we need.
def interact_with(self, other, fleets):
other.interact_with_invaderplayer(self, fleets)
That will save me some confusion later. Aside from that, how bad is this, really? I really only object to the magic numbers. 64 and 960 are some kind of left and right bumper values and should be promoted to wherever we keep invaders magic numbers. The 4*8 half_width is extra magical, as is the rectangle thing. I have an idea for how to do those better.
class InvaderPlayer(Flyer):
def __init__(self):
maker = BitmapMaker.instance()
self.players = maker.players # one turret, two explosions
self.player = self.players[0]
self.rect = self.player.get_rect()
self.rect.center = Vector2(u.SCREEN_SIZE/2, u.SCREEN_SIZE - 5*32 - 16)
self.step = 4
half_width = self.rect.width / 2
self.left = 64 + half_width
self.right = 960 - half_width
We can get the size info we need from the surface. The other magic will have to wait until we get to creating a place for invaders constants.
Commit: get critical values from Player Surface rather than ad hoc.
I thought that I would have to berate myself for doing this all in “the dumbest possible way” but in fact it looks nearly decent to me, at least for now. It’s all a bit down in the dust, but I don’t see an object just crying out to be created to help me out.
Summary
Just a tiny bit of work and we have a lovely player object moving back and forth on the screen. The speed even seems about right to me. We’ll know more when we start shooting.
I think that should be next.
Design-wise, I’m encountering issues with the interact methods. I got actual errors until I installed interact_with_invaderplayer
in Bumper and InvaderFleet. Inheritance is so tempting here but we should put our designer hat firmly on our head and decide what’s right and then do that.
Right now, I feel that I am just hammering the objects into place, but they’re going in nicely and without any wedging or filing needing to be done. We might see some improvements as we get more capability in here, but for now, I am alert but not concerned.
See you next time!