Python 023 - GAME OVER
Let’s clean up startup and maybe we can come up with a finite supply of ships and maybe even GAME OVER.
Since we have both ship and asteroids being created as needed, I though I’d change things so that the game starts with a blank screen, then the ship comes in, and then the asteroids. The asteroids are already done that way. For the ship, I just did this:
def game_init():
global screen, clock, running, dt, asteroids_in_this_wave
global wave_timer, ships
pygame.init()
screen = pygame.display.set_mode((u.SCREEN_SIZE, u.SCREEN_SIZE))
pygame.display.set_caption("Asteroids")
clock = pygame.time.Clock()
asteroids_in_this_wave = 2
wave_timer = u.ASTEROID_TIMER_STOPPED
# change below
ships = []
set_ship_timer(u.SHIP_EMERGENCE_TIME)
# change above
running = True
dt = 0
That does the trick. No test for it, though. Let’s think about that in a moment. Let’s also rename dt
to delta_time
. Thanks PyCharm. Tests are running green. Game starts as intended. Commit: game now starts on blank screen.
Now let’s spike in a “game over” screen. I’ll just display it all the time for now, to learn how to do it. I find an example to crib from.
I don’t know why this took so long, but in game_init
:
some_font = pygame.font.SysFont("arial", 64)
global text_surface, text_pos
text_surface = some_font.render("GAME OVER", True, "white")
text_pos = text_surface.get_rect(centerx=u.CENTER.x, centery=u.CENTER.y)
And in the main loop:
screen.blit(text_surface, text_pos)
We could use some better names here. Let’s do that, comment out the blit, and then commit.
some_font = pygame.font.SysFont("arial", 64)
global game_over, game_over_pos
game_over = some_font.render("GAME OVER", True, "white")
game_over_pos = game_over.get_rect(centerx=u.CENTER.x, centery=u.CENTER.y)
Commit: Can display text “GAME OVER”.
And the pizza is here. I’ll continue this later, or tomorrow …
Friday, 0916
Here we are, tomorrow as advertised. We have a “GAME OVER” text spiked, so I think I know enough text stuff to create the full GAME OVER screen when it’s needed. When is it needed? It’s needed when there are no more ships available. When are there no more ships available? When the initial supply runs out. What does a would-be player do when she wants to play? She inserts a quarter, signified by typing “q” as it will by then indicate on the GAME OVER screen. So it all hangs together, sort of. We have a new story, that goes something like this:
The game should start in the GAME OVER state, displaying GAME OVER and lines indicating the keys that drive the ship and asking the user to type “q” to “insert quarter”. When they type q, a configurable number of ships will become available and the game starts. Every time a ship is destroyed, a new ship is removed from the available ships, until all are gone, at which point the GAME OVER screen comes back.
In GAME OVER state, the asteroids should still fly and the saucer should appear from time to time.
Now we are experienced developers here chez Ron, and we know that we’re going to have to do this story in slices. Ideally, each one will show some value to our evil overlords, so that they’ll continue to provide our daily gruel.
Bruce Onder from Twitter offered an idea which I admit has a certain kind of appeal. The idea, in essence, would be that we have a collection of Ships, starting with however many we paid for. Each time the game notices that there is no ship on screen, it fetches one from the list. When the list is empty, game over. Meanwhile, the scoring facility could just display the ships available using the same list.
Cool, huh?
Thing is, things are:
- We are set up to use the same ship over and over;
- Ships in storage would need to be oriented vertically and spaced up at the top, while ships in use start at the center;
- There’s nothing wrong with an integer 4,3,2,1.
I really like Bruce’s idea. It would be quite nifty, just pop a ship off the list and use it. Except that Python’s pop method throws an exception when there’s nothing to pop, so the code would actually have to look a lot like if the length of the list is more than zero, pop it, which spoils some of the fun. Unless you like exceptions, and if you do we should have a little talk about their cost in space, time, and clarity.
Digression
I am enjoying Python, as I thought I would. However, I find it quite disappointing when viewed as an object-oriented language. You can say my_list.pop()
, but you can’t say my_list.len()
. Python feels like a procedural language with objects grafted in under duress. And rumor suggests that that’s exactly the case. There are some hideous ways that one could add methods to a built-in type, but they are vicious in nature.
Anyway, it is what it is. It’s hard to argue with Python’s popularity and broad range of applications, so I guess I should sit down and deal with it.
As I was saying …
As much as I like Bruce’s idea, I think the payoff just isn’t there. We could do it as an exercise but it’s really too cool for school. So let’s figure out an approach that we can live with.
First I want to do an experiment. At present, if the ship is active, it is in the list ships
. I’d like to see whether the GAME OVER screen would be visible for a moment between destroying the ship and adding a new one. Oh, heck, of course it would be, we leave the ship absent for three seconds. I was thinking we could just check for ships
being empty to display GAME OVER. Can’t quite do that.
Back to the drawing board … we do have ship_timer, which is used here:
def check_ship_spawn(ship, ships, delta_time):
if ships: return
global ship_timer
ship_timer -= delta_time
if ship_timer <= 0 and safe_to_emerge(missiles, asteroids):
ship.reset()
ships.append(ship)
No, too tricky. I was thinking I could do some arcane check for displaying GAME OVER like if there are no ships and ship timer is negative and etc etc hand-wave hand-wave.
Let’s go direct. Here’s a tentative plan:
- We’ll have a variable ship_count, starting at n.
- When we need a ship, we tick the count down and
- If it’s >= 0, we rez a ship.
- If it’s < 0, we do not rez a ship.
- If it’s < 0, we display the GAME OVER screen.
That seems simple. Why did I think about all those other things first? Let’s just spike it in. Or, no, let’s test it in.
We’ll be enhancing check_ship_spawn
to use this new counter, so let’s see our tests for that. I should have said “our test”, because we just have the one:
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, u.SHIP_EMERGENCE_TIME)
assert ships
assert ship.position == u.CENTER
assert ship.velocity == Vector2(0, 0)
assert ship.angle == 0
I think I’ll write a new test that’s about the new ship_count
, but I also think we’ll have to plug some ship_count
logic into the test above. I just feel better about writing a more direct test of the new feature.
def test_respawn_count(self):
ship = Ship(Vector2(0,0))
ships = []
main.available_ship_count = 2
check_ship_spawn(ship, ships, 3.1)
assert main.available_ship_count == 1
assert len(ships) == 1
ships = []
check_ship_spawn(ship, ships, 3.1)
assert main.available_ship_count == 0
assert len(ships) == 1
ships = []
check_ship_spawn(ship, ships, 3.1)
assert main.available_ship_count == -1
assert not ships
I am really not sure about how I’m supposed to access my globals over in main but this seems to work. In the check:
def check_ship_spawn(ship, ships, delta_time):
if ships: return
global available_ship_count
available_ship_count -= 1
if available_ship_count < 0: return
global ship_timer
ship_timer -= delta_time
if ship_timer <= 0 and safe_to_emerge(missiles, asteroids):
ship.reset()
ships.append(ship)
Is that the right way to access the globals? It seems to work but OMG it is ugly.
That passes the test but it doesn’t actually work. Why not? Because we call check a million times before the ship shows up. The decrement needs to be done when we use the ship. Which means I can’t decrement first, which will change the test.
Why is this so hard? It’s one of those first this then that things. I spike this into the check:
def check_ship_spawn(ship, ships, delta_time):
global game_over, ship_timer, available_ship_count
if ships: return
if available_ship_count <= 0:
game_over = True
return
ship_timer -= delta_time
if ship_timer <= 0 and safe_to_emerge(missiles, asteroids):
available_ship_count -= 1
ship.reset()
ships.append(ship)
And in the draw, just:
if game_over: screen.blit(game_over_surface, game_over_pos)
With available_ship_count
initialized to 2, that gives me two ships and then, the instant the second ship dies, GAME OVER. I think that’s good.
The test is failing, however. I think its count is off.
test_colliisions.py:37 (TestCollisions.test_respawn_count)
0 != -1
Expected :-1
Actual :0
We don’t ever count it down, we set game_over
instead. So:
def test_respawn_count(self):
ship = Ship(Vector2(0,0))
ships = []
main.available_ship_count = 2
check_ship_spawn(ship, ships, 3.1)
assert main.available_ship_count == 1
assert len(ships) == 1
ships = []
check_ship_spawn(ship, ships, 3.1)
assert main.available_ship_count == 0
assert len(ships) == 1
ships = []
check_ship_spawn(ship, ships, 3.1)
assert main.game_over
assert not ships
That passes. I think we should rename available_ship_count
to ships_remaining
. And I think we should reorder the globals.
asteroids = []
asteroids_in_this_wave = 2
clock = pygame.time.Clock()
delta_time = 0
game_over = False
missiles = []
running = False
ship = Ship(pygame.Vector2(u.SCREEN_SIZE / 2, u.SCREEN_SIZE / 2))
ship_timer = 0
ships = []
ships_remaining = 2
wave_timer = u.ASTEROID_TIMER_STOPPED
I think we have a feature. Let’s start with 4 ships, not 2. And a new u
constant:
ships_remaining = u.SHIPS_PER_QUARTER
Commit: Game allows four ships before game over. No quarters yet.
Let’s sum up, this article is long enough and I’m a bit tired.
Summary
I seemed to have a lot of intricate ideas for the ship count / game over logic, but what we have is nearly good. I say nearly, because I resorted to a high-level flag, game_over
rather than “just” detecting that we are out of ships.
But couldn’t I just check the ships_remaining
variable for zero now? No, I don’t think so, because it can be zero for a long time while ships exist. We’re stuck with what we have, I think.
There may be some better way, but this is at least pretty clear.
def check_ship_spawn(ship, ships, delta_time):
global game_over, ship_timer, ships_remaining
if ships: return
if ships_remaining <= 0:
game_over = True
return
ship_timer -= delta_time
if ship_timer <= 0 and safe_to_emerge(missiles, asteroids):
ships_remaining -= 1
ship.reset()
ships.append(ship)
I think that final code would be more clear if the ships_remaining decrement was last, i.e. after we consume the ship. Done:
def check_ship_spawn(ship, ships, delta_time):
global game_over, ship_timer, ships_remaining
if ships: return
if ships_remaining <= 0:
game_over = True
return
ship_timer -= delta_time
if ship_timer <= 0 and safe_to_emerge(missiles, asteroids):
ship.reset()
ships.append(ship)
ships_remaining -= 1
This feature took me more thinking and more time than I’d have liked. Part of the issue was that I’m not clear on how to use globals and module names and whatnot. It does look like importing the file and saying file.whatever is good. That’s what I do with u
.
I think the next thing will be to create the game over screen with its help lines. I’ll study up on how we do text in PyGame. I’m not clear on whether I should do it with lots of surfaces positioned just so, or one big surface into which I write text positioned just so. Half of one six a dozen of the other, probably.
I’m troubled by how many globals there are, about a dozen of them, but they mostly seem to be needed. And there are 16 lines of init, which seems like a lot.
The main loop needs better organization:
global running, ship, clock, delta_time, game_over_surface, game_over_pos
game_init()
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
check_ship_spawn(ship, ships, delta_time)
check_next_wave(asteroids, delta_time)
control_ship(ship, delta_time)
for missile in missiles.copy():
missile.update(missiles, delta_time)
move_everything(ship, delta_time)
check_collisions()
draw_everything()
if game_over: screen.blit(game_over_surface, game_over_pos)
pygame.display.flip()
delta_time = clock.tick(60) / 1000
pygame.quit()
I think we should consider combining the notions of move and update, the latter of which is really only used to let missiles time out, but will surely be used so that saucers can zig-zag.
I’m not clear about the delta_time=clock.tick(60)/1000
line. It seems odd to be setting delta_time at the end of the loop, not the beginning. I should study that a bit more. PyGame is really kind of odd compared to other game systems I’ve used.
My overall feelings are mixed. I like Python but not as much as I’d hoped. I very much like PyCharm and very much like that I am far away from Gradle and difficult setup in general. I think PyGame is pretty retro in its thinking, and suspect that we’ll swap it out for something else as a late refactoring problem in this series. So far I don’t expect to like the alternative much better but it’ll be an interesting challenge.
I feel like I’m maybe 80% of the way there with Python. It feels mostly comfortable but does not yet fall immediately to hand sometimes. More practice will help with that, more reading, and perhaps a little advice from my friends. Feel free to offer some yourself.
See you next time!