Encapsulation, Cohesion, Continued
Python Asteroids+Invaders on GitHub
Today I’ll try to gin up a little object containing the mask, the rectangle, and the position, to see if it makes things better. I am hopeful.
Yesterday we looked around and speculated. Today, let’s try something. I’ll do this with TDD. My biggest problem is what to call this object. After a little consideration last night as I dozed off, I’ve decided to call it Sprite. It will be a picture, with possible variations, that can be positioned anywhere. That’s pretty much what a Sprite is, and by some miracle, neither our program so far, nor PyGame, includes the term. Perfect!
I will follow my recent practice of building the class in the test file and moving it elsewhere later. At least until the class starts getting large, this is a convenient way to go. I can rationalize further if need be.
class Sprite:
pass
class TestSprite:
def test_exists(self):
Sprite()
Perfect again. Shall we commit? Sure, why not: initial sprite test and class.
Now, having thought about it, we know that our Sprites will be created based on bitmaps, which are presently returned from BitmapMaker. Our bitmaps are just sequences of integers representing the bits in a columnar format, 8 bits representing a column. We’d better review how the maps are used, as that’s where our masks and rectangles come from. Here’s the simplest case:
class PlayerShot(InvadersFlyer):
def __init__(self, position=u.CENTER):
offset = Vector2(2, -8*4)
self.velocity = Vector2(0, -4*4)
maker = BitmapMaker.instance()
self.bits = maker.player_shot
self._mask = pygame.mask.from_surface(self.bits)
self._rect = self.bits.get_rect()
self.position = position + offset
self.should_die = False
explosion = BitmapMaker.instance().player_shot_explosion
self.explosion_mask = pygame.mask.from_surface(explosion)
@property
def position(self):
return Vector2(self.rect.center)
@position.setter
def position(self, vector):
self.rect.center = vector
def update(self, delta_time, fleets):
self.position = self.position + self.velocity
def draw(self, screen):
screen.blit(self.bits, self.rect)
What is coming back in the thing we call bits
? We can see in BitmapMaker that it is a Surface, a PyGame bitmap that can be blitted to the screen:
class BitmapMaker:
def __init__(self):
...
player_shot = (0x0F, )
...
self.player_shot = self.make_and_scale_surface(player_shot, scale, (1, 8))
...
We’ll skip the details of make_and_scale_surface
. Know that it returns a properly sized instance of Surface.
So, after getting the surface, our PlayerShot creates a mask from it, and gets the rectangle of the surface. That will be a “raw” rectangle starting at (0, 0). We poke the screen position into it as needed, as the game proceeds.
So a rudimentary Sprite could be created with a Surface, provided by BitmapMaker, and it could create the mask and rectangle as done here, and it could support whatever access is required, via properties or methods.
However, I am making my thinking more complicated than I’d like, because some of the objects have multiple forms and the BitmapMaker returns more than one Surface. I might be wise to do the simple case first, but I can’t resist looking at the general case. This may be a mistake.
class InvaderShot(InvadersFlyer):
def __init__(self, position, maps):
self.maps = maps
self.masks = [pygame.mask.from_surface(bitmap) for bitmap in self.maps]
self._map = maps[0]
self.map_index = 0
self._rect = self._map.get_rect()
self.rect.center = position
self.count = 0
self.moves = 0
self._available = True
explosion = BitmapMaker.instance().invader_shot_explosion
self.explosion_mask = pygame.mask.from_surface(explosion)
Eep, the InvaderShot is currently given a collection of maps. Where does it get them?
class ShotController(InvadersFlyer):
max_firing_time = 0x30
off_screen = Vector2(-1, -1)
def __init__(self):
self.time_since_firing = 0
self.shots = [
InvaderShot(self.off_screen, BitmapMaker.instance().squiggles),
InvaderShot(self.off_screen, BitmapMaker.instance().rollers),
InvaderShot(self.off_screen, BitmapMaker.instance().plungers)]
self.columns = [
Cycler([0x00, 0x06, 0x00, 0x00, 0x00, 0x03, 0x0A, 0x00, 0x05, 0x02, 0x00, 0x00, 0x0A, 0x08, 0x01, 0x07]),
Cycler([0x0A, 0x00, 0x05, 0x02, 0x00, 0x00, 0x0A, 0x08, 0x01, 0x07, 0x01, 0x0A, 0x03, 0x06, 0x09])]
self.shot_index = 0
self.invader_fleet = None
self.player_x = 0
And, if we look at squiggles:
self.squiggles = [self.make_and_scale_surface(squig, scale, (3, 8)) for squig in squiggles]
We see that it is a collection of Surfaces.
If we (I mean “I”) had been more clever, we might have had BitmapMaker returning Sprites all along. As things stand now, we probably can’t just change it, because then we’d have to change all the user classes at the same time. Instead, we’ll do the transformation to Sprite separately.
I’ve decided to switch direction a bit: I’m going to do the multiple-map Sprite first. Here goes:
class TestSprite:
def test_exists(self):
maps = BitmapMaker.instance().squiggles
squiggles = Sprite(maps)
assert squiggles._surfaces == maps
class Sprite:
def __init__(self, surfaces):
self._surfaces = surfaces
Let’s commit every time we can, if we’re bright enough to think of it. Commit: Sprite accepts map collection.
Now we know we want to make masks for each map, and we need a rectangle. I am not at all sure how to test the masks, but the rectangle we can at least do an approval test.
class TestSprite:
def test_exists(self):
maps = BitmapMaker.instance().squiggles
squiggles = Sprite(maps)
assert squiggles._surfaces == maps
assert squiggles.rectangle == pygame.Rect(0, 0, 12, 32)
That drives out this much:
class Sprite:
def __init__(self, surfaces):
self._surfaces = surfaces
self._rectangle = self._surfaces[0].get_rect()
@property
def rectangle(self):
return self._rectangle
I still don’t know what test to write for the masks. I’ll put them in and assert on one, just to see what I get.
class TestSprite:
def test_exists(self):
maps = BitmapMaker.instance().squiggles
squiggles = Sprite(maps)
assert squiggles._surfaces == maps
assert squiggles.rectangle == pygame.Rect(0, 0, 12, 32)
assert isinstance(squiggles._masks[0], Mask)
class Sprite:
def __init__(self, surfaces):
self._surfaces = surfaces
self._masks = [pygame.mask.from_surface(surface) for surface in self._surfaces]
self._rectangle = self._surfaces[0].get_rect()
This isn’t really a very robust test but we’re all friends here. Commit: makes masks.
I think I’ll change that test name to test_creation
.
def test_position(self):
maps = BitmapMaker.instance().squiggles
squiggles = Sprite(maps)
assert squiggles.position == Vector2(6, 16)
squiggles.position = Vector2(100, 200)
assert squiggles.position == Vector2(100, 200)
class Sprite:
@property
def position(self):
return Vector2(self.rectangle.center)
@position.setter
def position(self, value):
self.rectangle.center = value
Reflect
My body is telling me that I’m nervous. Why? We have a lot of functionality here, and it is all speculative. It’s very good speculation, I feel sure of that, but it is still speculative. We need to get this baby into play asap.
Let’s commit this: position added.
I want to try it, in PlayerShot, the simplest case. Move the class over to prod. Commit: moving to prod.
That’s just F6 and type the path in PyCharm. It moves the whole class and the right imports. Well done, PyCharm.
Spike
What follows is a spike. We’re going to try patching this thing into PlayerShot. It may get grubby and if it gets too grubby we’ll back it out. If it’s not too awful, we’ll let it be.
In doing this I learn that pygame does in fact have a thing called Sprite. I wonder what it is. It makes me think that I should probably not shadow that name. Note made.
First I just make a Sprite:
class PlayerShot(InvadersFlyer):
def __init__(self, position=u.CENTER):
offset = Vector2(2, -8*4)
self.velocity = Vector2(0, -4*4)
maker = BitmapMaker.instance()
self.bits = maker.player_shot
self._sprite = Sprite([self.bits])
self._mask = pygame.mask.from_surface(self.bits)
self._rect = self.bits.get_rect()
self.position = position + offset
self.should_die = False
explosion = BitmapMaker.instance().player_shot_explosion
self.explosion_mask = pygame.mask.from_surface(explosion)
But let’s go further. We’ll not create the mask and rect things and we’ll change our properties appropriately:
@property
def mask(self):
return self._sprite.mask
@property
def rect(self):
return self._sprite.rectangle
@property
def position(self):
return self._sprite.position
@position.setter
def position(self, vector):
self._sprite.position = vector
Sprite does not know mask. Add it:
class Sprite:
@property
def mask(self):
return self._masks[0]
Tests are all running. I doubt that the game runs. I am mistaken: it does. However, let’s look at draw:
def draw(self, screen):
screen.blit(self.bits, self.rect)
We shouldn’t even save bits, and should send draw to Sprite.
class PlayerShot(InvadersFlyer):
def __init__(self, position=u.CENTER):
offset = Vector2(2, -8*4)
self.velocity = Vector2(0, -4*4)
maker = BitmapMaker.instance()
bits = maker.player_shot
self._sprite = Sprite([bits])
self.position = position + offset
self.should_die = False
explosion = BitmapMaker.instance().player_shot_explosion
self.explosion_mask = pygame.mask.from_surface(explosion)
def draw(self, screen):
self._sprite.draw(screen)
class Sprite:
def draw(self, screen):
screen.blit(self._surfaces[0], self.rectangle)
And we are green and the game runs. The PlayerShot code looks about as I would expect. I wonder whether it needs both mask
and rect
properties.
I only get one test failure if I return None from rect
. The game works. The Collider, however, does want to get the masks from the things it checks. I’ll leave the properties for now. However, look at the properties now:
@property
def mask(self):
return self._sprite.mask
@property
def rect(self):
return self._sprite.rectangle
@property
def position(self):
return self._sprite.position
@position.setter
def position(self, vector):
self._sprite.position = vector
By the time we’re done, these will be the same for everyone who uses the Sprites. Given that we allow inheritance of things like this, and we are perilously close to doing so, we could eliminate that clutter from all our classes that draw things, and there were around a dozen of them.
I’m goin to turn off the Spike warning lamp and commit this: PlayerShot uses Sprite.
I’m at a convenient stopping point and need to put new wipers on the car. Let’s sum up.
Summary
I feel good about this. I have in my mind a rough sketch of what we’ll do about the multi-picture guys, which will be to add a picture index and some kind of setting / incrementing methods, and then draw and return mask for whatever the index is. With that in place, I think we’ll be able to use the Sprite for most if not all our drawing things (except Score).
The TDD wasn’t deep, but it helped me focus my attention and I think it’ll pay off by providing a place for testing the picture indexing, which, without tests, I’d be tempted to just jam in because it’s so easy. We all know that that works except when it doesn’t.
I foresee two ways this may turn out:
We may wind up with a much more standard implementation of our drawable things, still requiring that they produce rectangle and mask on demand. It is likely that they’ll all be exactly the same, suggesting some kind of inheritance or delegation hack.
We may wind up being able to hide most of this inside BitmapMaker and Collider, so that the individual objects need only set up their Sprite, and position and increment it as needed, and basically know far less about how things work.
And we should unshadow pygame.Sprite
. I’ll look and see if it seems useful in my copious free time. At a first glance, it’s not useful for us. I may explain later or then again, I may not.
This went well. I am pleased. Now for a little relaxation. See you next time!