Python 005 - What Are These Rect Things?
My plan is to wrap the flight path, which I realized last night is 100x easier than I’ve been doing it. But I was reading about the Rect object …
I am finding PyGame a bit frustrating in that its documentation says words about what the methods do, but rarely if ever shows an example. That’s not as helpful as it might be. And there are many ways to do things and little advice over which might be best. No doubt over time I’ll come up to a better speed, with approaches that work for me.
One of the things in there is the Rect class. Think of all the time they saved by calling it Rect instead of Rectangle. Anyway, a Rect represents a rectangle with x and y coordinates, and width and height, w and h. They actually have a raft of words that can be used, including top, left, bottom, right, topleft, bottomleft, midtop … it goes on and on. Conveniently, they do also provide center, which I think may come in handy.
Rect has 80 such attributes. TIL that you can say dir(a_rect)
to get a list of all its attributes, so that’s nice. Apparently that works for all objects.
Another thing I’m not liking much about Python is that it doesn’t support obvious methods on things, like a length
for a list. You have to say len(my_list)
like some kind of barbarian.
Anyway, it turns out that Rect has methods like move
, which returns a new rectangle. It also has move_ip
for move in place, which modifies an existing Rect. I haven’t built up a good sense of all this but in our current code we have this:
class Ship:
def __init__(self, position):
self.position = position
self.velocity = vector2(0, 0)
self.angle = 0
self.acceleration = 120
self.accelerating = False
self.ship_surface, self.ship_accelerating_surface = self.prepare_surfaces()
def draw(self, screen):
ship_source = self.select_ship_source()
copy = pygame.transform.rotate(ship_source.copy(), self.angle)
half = pygame.Vector2(copy.get_size())/2
screen.blit(copy, self.position - half)
def move(self, dt):
self.position += self.velocity*dt
For some purposes, people seem to prefer to represent a thing’s position with its Rect. If you could live with top-left coordinates, you can kind of see how that might be more convenient. For us, here, now? I don’t see an advantage. I’m glad we had this little chat.
Wrap
The ship is supposed to wrap from one side of the screen to the other. In past versions, I’ve done this with a little bit of procedural code, because I wasn’t sure what the mod operator did, and for some reason didn’t just find out. Today, I know what it does and I plan to document it with a test and then use it.
def test_mod(self):
self.assertEqual(1005 % 1000, 5)
self.assertEqual(-5 % 1000, 995)
I am rather embarrassed at not knowing this, but if you knew some of the things I don’t know, this would pale in comparison. Anyway we can make the universe wrap if we only knew the height and width of it. It happens to be 512 right now. We’ll devise some universal constants soon now.
def move(self, dt):
self.position += self.velocity*dt
self.position.x = self.position.x % 512
self.position.y = self.position.y % 512
Tiring of seeing my most excellent game called “pygame window”, I make extensive changes to the initialization code:
pygame.display.set_caption("Asteroids")
Henceforth the frame at the top will say “Asteroids”. Much improved. We should commit these changes. I forgot. Commit: Ship wraps. Caption Asteroids.
Facing Facts
I was thinking this would be a spike and that after learning a bit, I’d throw it away and start working on a real game, almost certainly Asteroids, on the grounds that I know the game well enough to let me focus on Python and PyGame. It appears, however, that instead of starting over, I’m going to extend this program until it’s Asteroids or until I’m tired of the whole thing. So we should start developing better habits as we go.
One of those habits should be constants. I think I’ll just declare them at the top level, in their own file. I think I’ll give them a conventional name starting with U_ even though Python folks seem to just whale away with names like there’s no tomorrow.
I name the module u, so that I can say things like this:
def move(self, dt):
self.position += self.velocity*dt
self.position.x = self.position.x % u.SCREEN_SIZE
self.position.y = self.position.y % u.SCREEN_SIZE
I think that’ll be OK. I apply my lovely new constant in a few other places:
# pygame setup
pygame.init()
screen = pygame.display.set_mode((u.SCREEN_SIZE, u.SCREEN_SIZE))
pygame.display.set_caption("Asteroids")
clock = pygame.time.Clock()
running = True
dt = 0
ship = Ship(pygame.Vector2(u.SCREEN_SIZE / 2, u.SCREEN_SIZE / 2))
A few more constants that you’ll see when next we view the relevant code, tests run, committed.
So far today I’ve just been messing around. I think that the ship is nearly good enough for now, though I think it could turn a bit faster, and I don’t have a constant for that.
# U - Universal Constants
import pygame
SCREEN_SIZE = 512
SHIP_ACCELERATION = pygame.Vector2(120, 0)
SHIP_ROTATION_STEP = 120
def turn_left(self, dt):
self.angle -= u.SHIP_ROTATION_STEP*dt
def turn_right(self, dt):
self.angle += u.SHIP_ROTATION_STEP*dt
That seems better. Commit: increase ship rotation speed.
Still just fiddling around. Let’s at least list things that we might do next:
- Add missiles
- Add asteroids
- Missiles kill asteroids
- Asteroids kill ship
- Asteroid split into smaller asteroids
- Saucer
- Asteroid waves
- New ship after inevitable tragic death.
Another thing that has been suggested to me is to use pytest instead of unittest. That’s probably a more productive use of my time in these early days. Let’s try that.
The exercise goes smoothly. In PyCharm Settings, add pytest to the list of packages on the Python Interpreter page:
That seems to suck in a few things extra, pluggy and such. I don’t mind.
A quick search told me that PyCharm automatically switches to using pytest when you add it to the packages, and sure enough I get the nicer-looking pytest output right away.
============================= test session starts ==============================
collecting ... collected 5 items
test_asteroids.py::TestAsteroids::test_something PASSED [ 20%]
test_asteroids.py::TestAsteroids::test_map_lambda PASSED [ 40%]
test_asteroids.py::TestAsteroids::test_ship_move PASSED [ 60%]
test_asteroids.py::TestAsteroids::test_ship_acceleration PASSED [ 80%]
test_asteroids.py::TestAsteroids::test_mod PASSED [100%]
============================== 5 passed in 0.06s ===============================
I went through and modified the tests and test file to use pytest form, just to learn. I needed a bit of searching to find the trick for “almost equal”. The test file now looks like this:
import pygame
import pytest
from ship import Ship
vector2 = pygame.Vector2
class TestAsteroids():
def test_something(self):
assert True == True
def test_map_lambda(self):
points = [vector2(-3.0, -2.0), vector2(-3.0, 2.0), vector2(-5.0, 4.0),
vector2(7.0, 0.0), vector2(-5.0, -4.0), vector2(-3.0, -2.0)]
new_points = map(lambda point: point + vector2(7, 4), points)
for point in new_points:
assert point.x >= 0
assert point.x <= 14
assert point.y >= 0
assert point.y <= 9
def test_ship_move(self):
ship = Ship(vector2(50, 60))
ship.velocity = vector2(10, 16)
ship.move(0.5)
assert ship.position == vector2(55, 68)
def test_ship_acceleration(self):
ship = Ship(vector2(0, 0))
ship.angle = 45
ship.acceleration = pygame.Vector2(100, 0)
ship.power_on(0.5)
assert ship.velocity.x == pytest.approx(35.3553, 0.01)
assert ship.velocity.y == pytest.approx(-35.3553, 0.01)
def test_mod(self):
assert 1005 % 1000 == 5
assert -5 % 1000 == 995
I had to change the name of the class to TestAsteroids, since pytest looks for the prefix “Test” on classes to decide to collect them into the test suite. I think I like the simple use of assert
to do the checking, although I shudder to think what pytest must be doing behind the scenes to make that work. But work it does:
============================= test session starts ==============================
collecting ... collected 5 items
test_asteroids.py::TestAsteroids::test_something PASSED [ 20%]
test_asteroids.py::TestAsteroids::test_map_lambda PASSED [ 40%]
test_asteroids.py::TestAsteroids::test_ship_move PASSED [ 60%]
test_asteroids.py::TestAsteroids::test_ship_acceleration PASSED [ 80%]
test_asteroids.py::TestAsteroids::test_mod FAILED [100%]
test_asteroids.py:37 (TestAsteroids.test_mod)
995 != 996
Expected :996
Actual :995
It turns out that pytest will run your unittest tests, but I figured I’d change them to the new form to get with the flow. I’m not sure what all it can do for me, but I’ll read about it and learn. Thanks to chbndrhnns for the tip!
In view of the random tree destruction going on here, I think I’ll call it a morning. Next time I’ll do something to advance the state of the Asteroids art. Or at least to advance the state of this program.
See you then!