Python 020 - Stories
We’ll review and refine the list of things to do and pick something. Doubtless fun of some kind will ensue. Loving PyCharm’s auto-run of the tests!
Features
Let’s list some of the things that the product needs, or that the program needs.
- Ship should explode;
- Ship should reappear after an interval, at center screen;
- Ship should appear only when center screen is relatively safe;
- Saucer, with all its works and all its pomps;
- Multiple waves of asteroids;
- Score should appear on screen;
- Number of remaining ships should appear on screen.
- Tests.
Those are not in priority order. One of them is not like the others: tests.
We have 16 tests, including a few that just test and document Python behavior. This is not what you’d call robust testing. And PyCharm has a really nice feature that I’ve finally learned how to use.
PyCharm can be set to run your tests automatically after any change to the code. If the code doesn’t compile, it doesn’t bug you about it, but whenever it does, PyCharm runs the tests and you get a tiny popup, green if they all passed, and red if they didn’t, each with a test count.
I found it difficult to set up the feature, because sometimes I run the program to see what it does, and that removes the tests from the PyCharm “Run” tab, and when the tests aren’t there, it doesn’t run them.
Last night1, some combination of three highly-priced consultants helped me work it out. It goes like this:
- Run the tests
- Click the …
- Toggle Auto-Run until it is checked
- On the run window, on the tab showing the tests, right click
- Pin that tab
After that, the tab stays there. If you run the program or something else, it will run in a new tab in the run window. And the tests will continue to auto-run even if you run the program once in a while.
For me, the little green thing popping up provides confidence without being distracting, and even the red one doesn’t seem to bug me, unless I think I’m done, in which case it’s a quick warning that I’m not as done as I think.
The only issue is that I don’t have very many tests, because … well, because I’ve always got what seems like a simple problem to solve, I don’t quite know how I’ll solve it, I’m not sure how to test it, I’m a pretty good programmer … all this in spite of the fact that I love knowing my tests are supporting me, and I hate feeling uncertain, and it delays me to have to run the program when the tests could have answered my question if only they were stronger, and then I always waste time shooting down a few asteroids just because I’m running the game anyway …
If I weren’t so insensitive I’d think I’m not a very good person. But I am, and I am … and I want to bear down on the tests.
So it’s not #8 in priority. It’s number x.5 for x from 0 to 8. I want to test ideally before and if necessary after I do any feature.
Ship Respawn
Let’s do the next slice of ship re-spawning. Right now, there’s a kludge in the collision code:
def check_asteroids_vs_ship():
for ship in ships.copy(): # there's only one, do it first
for asteroid in asteroids.copy():
asteroid.collide_with_attacker(ship, ships, asteroids)
if not ships:
ship.active = True
ships.append(ship)
Recall that we have a very small collection of ships, containing zero or one ships. If an asteroid collides with the ship, that code will remove the ship from the ships collection:
def collide_with_attacker(self, attacker, attackers, asteroids):
if self.withinRange(attacker.position, attacker.radius):
if attacker in attackers: attackers.remove(attacker)
self.split_or_die(asteroids)
I think but am not sure that the active
flag is no longer used. We should sort that. Be that as it may, after the all to collide_with_attacker
, if ships
is empty, we put the ship back. It doesn’t even flicker.
We should break that loop before we’re done here, since with the ship gone there’s no sense checking more asteroids. I’m making sticky notes about these ideas, by the way.
I’m getting an idea.
Let’s assume that there will be a separate check in the game loop to decide whether to spawn the ship. It’ll have a timer in it that it’ll count down, and it’ll someday include code to be sure the center is clear. So that code will look something like
if not ships[]:
timer -= 1
if timer <= 0:
if safe():
ships.append(ship)
Probably the timer is in seconds and we’ll be adding delta_time. Same pattern. OK … let’s imagine what the respawn function will look like, and test it. Here goes.
def test_respawn_ship(self):
ship = Ship(Vector2(0,0))
ships = []
set_ship_timer(3)
check_ship_spawn(ship, ships, 0.1)
assert not ships
check_ship_spawn(ship, sjips, 3.0)
assert ships
I figured that if I had a function set_ship_timer
, the main could own the actual time and it could be set by the test without reaching for a global. And check_ship_spawn
is our new function.
I need those two functions, in main. I try this:
def set_ship_timer(seconds):
global ship_timer
ship_timer = seconds
def check_ship_spawn(ship, ships, delta_time):
if ships: return
global ship_timer
ship_timer -= delta_time
if ship_timer <= 0:
ships.append(ship)
If we have ships, checking’s done. Otherwise we decrement the global timer. If it’s timed out we toss our ship into ships. I think this should be working. I think the tests may disagree.
Ah. The test needs to import those names. And there’s a typo. Fix the test:
def test_respawn_ship(self):
ship = Ship(Vector2(0,0))
ships = []
set_ship_timer(3)
check_ship_spawn(ship, ships, 0.1)
assert not ships
check_ship_spawn(ship, ships, 3.0)
assert ships
But something weird has started to happen: when the tests auto-run, it’s starting the game. That’s new and I don’t know why. Hm. It’s happening because I’ve imported main into the tests, which I’m guessing means that when the tests run it runs the code in main, including this code:
# 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))
ships = [ship]
asteroids = [Asteroid(2) for i in range(0, 4)]
missiles = []
ship_timer = 0
I think that in general the done thing with PyGame is that you set up one of these things:
if __name__=="__main__":
main()
When Bryan and I set up this program as a spike, we skipped all that. So let’s see if we can wrap this baby up and make it more like that.
Just a bit of grotesque hackery and we’re back to normal. Let me show you the basics of what I did:
main ...
ship = Ship(pygame.Vector2(u.SCREEN_SIZE / 2, u.SCREEN_SIZE / 2))
ships = [ship]
asteroids = [Asteroid(2) for i in range(0, 4)]
missiles = []
ship_timer = 0
running = False
dt = 0
clock = pygame.time.Clock()
# pygame setup
def game_init():
global screen, clock, running, dt
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
def main_loop():
global running, ship, clock, dt
game_init()
while running:
... # and on and on ...
pygame.quit()
if __name__ == "__main__":
main_loop()
And all the tests are running, and they don’t start the game, and after all that excitement I want a few moments to reflect and absorb what I’ve learned.
Reflection
It probably seems really ignorant of me, but that is almost certainly the first time I’ve ever put one of those __name__=="__main__"
things in a program, ever. I’ve run programs that had them but I don’t recall ever having actually done it. So these changes were a bit scary.
What I think I really want in the future is a Game object, where all the things that are now global will be member variables referenced by self.this_and_that
. Right now we have — let me count — seven global functions in main, and what looks to be eight global variables. Not what I’d call neat. Added to Jira2
The sudden appearance of the game auto-running, and the need to do the thing I’d never done before has distracted me. I’m taking this break to get myself reset into the story we were working on, whatever it was. Oh, right, spawning the ship after a while3.
So it should be that if we change the code in that check, and we call our check in the main loop, the ship should disappear when hit and appear later. Which reminds me of something else to test:
def test_respawn_ship(self):
ship = Ship(Vector2(0, 0))
ships = []
set_ship_timer(3)
check_ship_spawn(ship, ships, 0.1)
assert not ships
check_ship_spawn(ship, ships, 3.0)
assert ships
assert ship.position == u.CENTER
The little red thing starts showing up. No surprise there. Change the function:
def check_ship_spawn(ship, ships, delta_time):
if ships: return
global ship_timer
ship_timer -= delta_time
if ship_timer <= 0:
ship.position = u.CENTER
ships.append(ship)
The green thing pops up. Very nice. Let’s call the function somewhere in the main loop.
I put it somewhere. We’ll review that and clean up the main loop soon. Now when we destroy the ship here:
def check_asteroids_vs_ship():
for ship in ships.copy(): # there's only one, do it first
for asteroid in asteroids.copy():
asteroid.collide_with_attacker(ship, ships, asteroids)
if not ships:
ship.active = True
ships.append(ship)
Thunderstorm here. If I disappear you’ll know why. As I was saying:
def check_asteroids_vs_ship():
for ship in ships.copy(): # there's only one, do it first
for asteroid in asteroids.copy():
asteroid.collide_with_attacker(ship, ships, asteroids)
if not ships:
set_ship_timer(3)
return
I expect that to work as intended, with the ship disappearing when it crashes into an asteroid, and appearing three seconds later at the center. Let’s find out. It does … and it’s still moving as it was. Ha. Improve the test:
def test_respawn_ship(self):
ship = Ship(Vector2(0, 0))
ship.velocity = Vector2(31, 32)
ships = []
set_ship_timer(3)
check_ship_spawn(ship, ships, 0.1)
assert not ships
check_ship_spawn(ship, ships, 3.0)
assert ships
assert ship.position == u.CENTER
assert ship.velocity == Vector2(0, 0)
Red thingie pops up. Change spawn.
def check_ship_spawn(ship, ships, delta_time):
if ships: return
global ship_timer
ship_timer -= delta_time
if ship_timer <= 0:
ship.position = u.CENTER
ship.velocity = Vector2(0,0)
ships.append(ship)
Tests are green. I love that auto-run. Test the game just because I want to see it happen.
SLow learner. I should think about the spec once in a while. Should appear at angle zero.
def test_respawn_ship(self):
ship = Ship(Vector2(0, 0))
ship.velocity = Vector2(31, 32)
ship.angle = 90
ships = []
set_ship_timer(3)
check_ship_spawn(ship, ships, 0.1)
assert not ships
check_ship_spawn(ship, ships, 3.0)
assert ships
assert ship.position == u.CENTER
assert ship.velocity == Vector2(0, 0)
assert ship.angle == 0
And, of course:
def check_ship_spawn(ship, ships, delta_time):
if ships: return
global ship_timer
ship_timer -= delta_time
if ship_timer <= 0:
ship.position = u.CENTER
ship.velocity = Vector2(0,0)
ship.angle = 0
ships.append(ship)
We seem to be experiencing a bit of feature envy here. Let’s reset the ship:
def check_ship_spawn(ship, ships, delta_time):
if ships: return
global ship_timer
ship_timer -= delta_time
if ship_timer <= 0:
ship.reset()
ships.append(ship)
class Ship ...
def reset(self):
self.position = u.CENTER
self.velocity = Vector2(0, 0)
self.angle = 0
I must run one more time. Love to see things work. Let me make you a little movie so you can enjoy the feeling of accomplishment.
You don’t have to play very long before you crave the safe entry. We’ll do that in an upcoming session. For now, let’s commit this: ship disappears on collision with asteroid, spawns 3 seconds later.
Let’s sum up:
Summary
A nice PyCharm feature, auto-running the tests, solved by three highly-paid consultants last night, inspired me to test re-spawning the ship upon its inevitable and tragic destruction. The test resulting it a fairly reasonable design, with a function to set the timer, and one to check to see if it’s time for a new ship. And playing the game — instead of thinking — reminded me to set it to center, stop its motion, and set its angle to zero. It all went in nicely.
Except that, because I started with a rudimentary spike that had all the game code at top level, importing the main into the tests caused the main to run. So I had the opportunity to learn quickly how to deal with that, resulting in my now knowing what probably every other programmer in the universe already knew. Yay me, I’m catching up!
A good morning’s work. I’ll see you next time!
-
Tuesday night. At the Friday Geeks Night Out Zoom Ensemble. When did you think we’d meet? ↩
-
The collection of yellow sticky notes on my keyboard tray. What did you think I meant? ↩
-
How do you tell the difference between a crocodile and an alligator? One you’ll see in a while, and the other one you’ll see later. Thanks, Mitch. ↩