Python 011 - Surfeit of Surfacing
One more tweak on the SurfaceMaker, then lets see about using it to make more asteroids.
ian molee points out correctly that I am making instances of SurfaceMaker and that it could just as well be used in a full static form. I agree that that’s the case, and I have some kind of inbuilt aversion to doing it. That said, I wouldn’t hesitate to use Kotlin’s object
, which amounts to the same thing.
Anyway, thanks Ian, and I just might try the switch. I should learn how to use static methods in Python anyway.
Meanwhile, at some point in the middle of the night I was called to the basement to check the sump pump, and then I made and tested a tiny change to the existing SurfaceMaker code:
def asteroid_surface(self, size):
shape = random.randint(0,3)
scale = [4, 8, 16][size]
return self.create_scaled_surface((128, 128), Vector2(4, 4), scale, raw_rocks[shape])
I removed the shape
parameter entirely, and set shape randomly from 0 to 3. Then, because lists exist, I use the size
as an index into a list of the scale multipliers. So size 0, 1, 2 get scale 4, 8, 16 respectively, and that’s just what we want.
Now some of you, and I on a given day, might feel that the scale setting should be protected against someone asking for a size other than 1, 2, 3. PyGame does have a handy function for that purpose. Let’s test it and then use it.
def test_clamp(self):
zero = clamp(-1, 0, 3)
assert zero == 0
three = clamp(4, 0, 3)
assert three == 3
Works as advertised. Nice. Therefore:
def asteroid_surface(self, size):
shape = random.randint(0,3)
scale = [4, 8, 16][clamp(size, 0, 3)]
return self.create_scaled_surface((128, 128), Vector2(4, 4), scale, raw_rocks[shape])
Feel better now? I certainly do. Now let’s learn how to use static methods. I’ll create a test and class just for that purpose.
class TestStaticMethods:
def test_one(self):
assert AllStatic.static_one(1, 1) == 101
def test_two(self):
assert AllStatic.static_two(1, 1) == 202
class AllStatic:
@staticmethod
def static_one(x, y):
return 10 0 *x + y
@staticmethod
def static_two(a, b):
return AllStatic.static_one(2 * a, 2 * b)
OK. If you want to call another static method from a static (or non-static?) method, you name the class and call on it. I’m not sure I like that any better. Let’s try it in play.
This took me a long time to get right: you have to unwind all the references to self
, and remove the self
from all the functions. I found that the best way to do it was to do one method at a time, then its callers, etc. Here’s where we are:
class SurfaceMaker:
@staticmethod
def adjust(point, center_adjustment, scale_factor):
return (point + center_adjustment) * scale_factor
@staticmethod
def ship_surfaces():
ship_surface = SurfaceMaker.create_scaled_surface((60, 36), Vector2(7, 4), 4, raw_ship_points)
accelerating_surface = SurfaceMaker.create_scaled_surface((60, 36), Vector2(7, 4), 4, raw_ship_points, raw_flare_points)
return ship_surface, accelerating_surface
@staticmethod
def asteroid_surface(size):
shape = random.randint(0, 3)
scale = [4, 8, 16][clamp(size, 0, 3)]
return SurfaceMaker.create_scaled_surface((128, 128), Vector2(4, 4), scale, raw_rocks[shape])
@staticmethod
def create_scaled_surface(dimensions, offset, scale, *point_lists):
surface = pygame.Surface(dimensions)
surface.set_colorkey((0, 0, 0))
for point_list in point_lists:
adjusted = [SurfaceMaker.adjust(point, offset, scale) for point in point_list]
pygame.draw.lines(surface, "white", False, adjusted, 3)
return surface
And that permits me to call my surface makers with SurfaceMaker.asteroid_surface
instead of SurfaceMaker().asteroid_surface
. Not what I’d call a big win, but it was well worth the experience and the learning. Thanks, Ian.
Just for the look of things, let’s increase the size of the window and create four asteroids.
class Asteroid:
def __init__(self):
height = random.randrange(0, u.SCREEN_SIZE)
self.position = vector2(0, height)
angle_of_travel = random.randint(0, 360)
self.velocity = u.ASTEROID_SPEED.rotate(angle_of_travel)
self.surface = SurfaceMaker.asteroid_surface(size=2)
# main
...
ship = Ship(pygame.Vector2(u.SCREEN_SIZE / 2, u.SCREEN_SIZE / 2))
asteroids = [Asteroid() for i in range(0, 4)]
...
ship.draw(screen)
[asteroid.draw(screen) for asteroid in asteroids]
...
ship.move(dt)
[asteroid.move(dt) for asteroid in asteroids]
That’s arguably a nasty use of a list comprehension, but it works nicely. Let’s do it legitimately, however, it’ll be OK.
for asteroid in asteroids:
asteroid.draw(screen)
...
for asteroid in asteroids:
asteroid.move(dt)
OK, happy now? I guess I am. I would have liked to one-line those, but PEP does not agree, and I get squiggly warning lines. I’m trying to learn Python style, so I adhere to most of the warnings.
I notice that since I set up pytest I’ve not been getting the tests auto-run. PyCharm has a toggle that will cause it to run the tests after every change, or thereabouts. It just pops up a little window over the run tab, green and saying how many passed, or red and saying how many passed and how many failed. I think I’ll leave that turned on for a while and see if I like it. In a game like this, I tend to test mostly by running it, so running the tests automatically will be a good check.
I know that some people find it distracting. I’m hoping that I’ll like it.
Let’s sum up.
Summary
I’ve just been working about an hour and a quarter, but the day has gone chaotic owing to being up at 0430 to look into the sump. I’ve reported on changing the asteroid surface code to allow for all the sizes and shapes. We’ve changed SurfaceMaker to all static, which I have mixed feelings about but suspect it’s better on some dimension. We’ve given asteroids a new starting rule (start on the vertical edge) and created four of them when the game starts up.
That last won’t stand quite as written for at least two reasons:
- We need different wave counts after each wave is destroyed;
- When we shoot an asteroid, it can split and the split bits won’t start on the edge, they’ll start where the original asteroid was when it was hit.
That’ll be fine, we’ll deal with that when the time comes. I’m thinking a defaulted position parameter in Asteroid.__init__
.
We’d better commit this. Commit: Static SurfaceMaker. Four asteroids, assorted shapes and sizes.
I’d like to call out my practice of writing tests, not for my code but to determine and document how things in the language or library work:
class TestAsteroids:
def test_mod(self):
assert 1005 % 1000 == 5
assert -5 % 1000 == 995
def test_args(self):
def local_function(a, b):
return 10*a + b
result = local_function(b=5, a=6)
assert result == 65
def test_clamp(self):
zero = clamp(-1, 0, 3)
assert zero == 0
three = clamp(4, 0, 3)
assert three == 3
class TestStaticMethods:
def test_one(self):
assert AllStatic.static_one(1, 1) == 101
def test_two(self):
assert AllStatic.static_two(1, 1) == 202
class AllStatic:
@staticmethod
def static_one(x, y):
return 100 * x + y
@staticmethod
def static_two(a, b):
return AllStatic.static_one(2 * a, 2 * b)
I find that sort of test useful when I’m just learning a new language or library, and from time to time when I’m just off in the weeds a bit trying something different from my usual. I find that writing a test like that helps me focus separately on the particular language or library issue, and then on the application of that knowledge to the actual problem.
I feel like I’ve been just fiddling around the past few days, but that’s OK, because I’m just learning how Python and PyGame want to be used. I think in the next session we’ll get down to something more interesting, like firing missiles and having them crack asteroids. If that’s interesting. Relatively interesting at best, I suppose.
My general reaction to Python is interesting, to me at least. I find it more comfortable than I did Kotlin, even after a lot more experience with Kotlin than I have so far in Python. I found Kotlin’s strict typing kept me on the rails, but I didn’t like the need to mention the types that I had in my head all the time. In Python, I just say (surface, position)
, not (surface: Surface, position: Vector2)
, and despite the help that those declarations give me, and despite that IDEA almost always guesses correctly about what should be there, I feel constrained.
It’s an odd feeling. I’m glad to have the help, and I’m mildly irritated by the constraint. It’s a little bit like someone who corrects your grammar and pronunciation all the time. Even if it’s valuable, it kind of ticks you off. (Sorry, friends, for the many times I’ve done something like that.)
Python fits me like a broken-in T-shirt, and Kotlin is a bit more like a fresh white shirt with a stiff collar. I look good, but I’m not as comfortable.
I’m surprised by the feeling, not that I don’t understand why, but surprised that I am actually noticing the greater comfort, for me, in the Python space.
As for how well I like Python? For my own use, I think I’m going to like it more than Kotlin, but less than Ruby. And I probably like Ruby less than Smalltalk, but I haven’t used Smalltalk for so long that it’s hard to compare. Anyway, my guess is that my preference will be:
Smalltalk > Ruby > Python > Kotlin > … > Java
I hope you’ll join me next time!